From f42fff74db129982735851b8ecbba8312addebd2 Mon Sep 17 00:00:00 2001 From: Roxy Light Date: Sun, 16 Feb 2025 22:31:15 -0800 Subject: [PATCH] Add `*State.XMove` method Same signature as `lua_xmove`, but permits moving between unrelated states if the values are frozen. If we support coroutines later, we can relax the restriction between related states. Updates #75 --- internal/lua/lua.go | 38 +++++++++ internal/lua/lua_test.go | 171 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) diff --git a/internal/lua/lua.go b/internal/lua/lua.go index 57f77de..fc637f3 100644 --- a/internal/lua/lua.go +++ b/internal/lua/lua.go @@ -394,6 +394,44 @@ func (l *State) grow(wantTop int) bool { return true } +// XMove exchanges values between states: +// n values are popped from src, +// then pushed onto the stack of l. +// If l and src are different states +// and the top n values of src's stack are not frozen using [*State.Freeze], +// then XMove returns an error. +func (l *State) XMove(src *State, n int) error { + if n < 0 { + return errors.New("negative count to move") + } + if n == 0 { + return nil + } + if src.Top() < n { + return errMissingArguments + } + if src == l { + // No-op on same state. + return nil + } + + src.init() + l.init() + if len(l.stack)+n > cap(l.stack) { + return errStackOverflow + } + newTop := len(src.stack) - n + elems := src.stack[newTop:] + for _, v := range elems { + if !isFrozen(v) { + return errors.New("moving unfrozen values between independent states") + } + } + l.stack = append(l.stack, elems...) + src.setTop(newTop) + return nil +} + // IsNumber reports if the value at the given index is a number // or a string convertible to a number. func (l *State) IsNumber(idx int) bool { diff --git a/internal/lua/lua_test.go b/internal/lua/lua_test.go index c59b118..18a754d 100644 --- a/internal/lua/lua_test.go +++ b/internal/lua/lua_test.go @@ -1546,6 +1546,177 @@ func TestFreeze(t *testing.T) { }) } +func TestXMove(t *testing.T) { + t.Run("SameState", func(t *testing.T) { + state := new(State) + defer func() { + if err := state.Close(); err != nil { + t.Error("state1.Close:", err) + } + }() + + state.PushInteger(555) + state.PushInteger(123) + state.CreateTable(2, 0) + tableID := state.ID(-1) + state.PushInteger(456) + if err := state.RawSetIndex(-2, 1); err != nil { + t.Fatal("t[1] = 456 raised", err) + } + state.PushInteger(789) + if err := state.RawSetIndex(-2, 2); err != nil { + t.Fatal("t[2] = 789 raised", err) + } + if got, want := state.Top(), 3; got != want { + t.Errorf("before XMove, state.Top() = %d; want %d", got, want) + } + + if err := state.XMove(state, 2); err != nil { + t.Error("XMove:", err) + } + + if got, want := state.Top(), 3; got != want { + t.Errorf("after XMove, state.Top() = %d; want %d", got, want) + } + if got, err := valueToGo(state, 1); err != nil { + t.Errorf("after XMove, state.stack[1] %v", err) + } else if want := int64(555); !cmp.Equal(got, want) { + t.Errorf("after XMove, state.stack[1] = %v; want %v", got, want) + } + if got, err := valueToGo(state, 2); err != nil { + t.Errorf("after XMove, state.stack[2] %v", err) + } else if want := int64(123); !cmp.Equal(got, want) { + t.Errorf("after XMove, state.stack[2] = %v; want %v", got, want) + } + if got, want := state.Type(3), TypeTable; got != want { + t.Errorf("after XMove, state.Type(3) = %v; want %v", got, want) + } else if got, want := state.ID(3), tableID; got != want { + t.Errorf("after XMove, state.ID(3) = %d; want %d", got, want) + } + }) + + t.Run("DifferentStates", func(t *testing.T) { + state1 := new(State) + state2 := new(State) + defer func() { + if err := state1.Close(); err != nil { + t.Error("state1.Close:", err) + } + if err := state2.Close(); err != nil { + t.Error("state2.Close:", err) + } + }() + + state1.PushInteger(555) + state1.PushInteger(123) + state1.CreateTable(2, 0) + tableID := state1.ID(-1) + state1.PushInteger(456) + if err := state1.RawSetIndex(-2, 1); err != nil { + t.Fatal("t[1] = 456 raised", err) + } + state1.PushInteger(789) + if err := state1.RawSetIndex(-2, 2); err != nil { + t.Fatal("t[2] = 789 raised", err) + } + if err := state1.Freeze(-1); err != nil { + t.Fatal(err) + } + if got, want := state1.Top(), 3; got != want { + t.Errorf("before XMove, state1.Top() = %d; want %d", got, want) + } + if got, want := state2.Top(), 0; got != want { + t.Errorf("before XMove, state2.Top() = %d; want %d", got, want) + } + + if err := state2.XMove(state1, 2); err != nil { + t.Error("XMove:", err) + } + + if got, want := state1.Top(), 1; got != want { + t.Errorf("after XMove, state1.Top() = %d; want %d", got, want) + } + if got, err := valueToGo(state1, 1); err != nil { + t.Errorf("after XMove, state1.stack[1] %v", err) + } else if want := int64(555); !cmp.Equal(got, want) { + t.Errorf("after XMove, state1.stack[1] = %v; want %v", got, want) + } + + if got, want := state2.Top(), 2; got != want { + t.Errorf("after XMove, state2.Top() = %d; want %d", got, want) + } + if got, err := valueToGo(state2, 1); err != nil { + t.Errorf("after XMove, state2.stack[1] %v", err) + } else if want := int64(123); !cmp.Equal(got, want) { + t.Errorf("after XMove, state2.stack[1] = %v; want %v", got, want) + } + if got, want := state2.Type(2), TypeTable; got != want { + t.Errorf("after XMove, state2.Type(2) = %v; want %v", got, want) + } else if got, want := state2.ID(2), tableID; got != want { + t.Errorf("after XMove, state2.ID(2) = %d; want %d", got, want) + } + }) + + t.Run("DifferentStatesWithoutFreeze", func(t *testing.T) { + state1 := new(State) + state2 := new(State) + defer func() { + if err := state1.Close(); err != nil { + t.Error("state1.Close:", err) + } + if err := state2.Close(); err != nil { + t.Error("state2.Close:", err) + } + }() + + state1.PushInteger(555) + state1.PushInteger(123) + state1.CreateTable(2, 0) + tableID := state1.ID(-1) + state1.PushInteger(456) + if err := state1.RawSetIndex(-2, 1); err != nil { + t.Fatal("t[1] = 456 raised", err) + } + state1.PushInteger(789) + if err := state1.RawSetIndex(-2, 2); err != nil { + t.Fatal("t[2] = 789 raised", err) + } + if got, want := state1.Top(), 3; got != want { + t.Errorf("before XMove, state1.Top() = %d; want %d", got, want) + } + if got, want := state2.Top(), 0; got != want { + t.Errorf("before XMove, state2.Top() = %d; want %d", got, want) + } + + if err := state2.XMove(state1, 2); err == nil { + t.Error("XMove did not return an error") + } + + if got, want := state1.Top(), 3; got != want { + t.Errorf("after XMove, state1.Top() = %d; want %d", got, want) + } + if got, err := valueToGo(state1, 1); err != nil { + t.Errorf("after XMove, state1.stack[1] %v", err) + } else if want := int64(555); !cmp.Equal(got, want) { + t.Errorf("after XMove, state1.stack[1] = %v; want %v", got, want) + } + if got, err := valueToGo(state1, 2); err != nil { + t.Errorf("after XMove, state1.stack[2] %v", err) + } else if want := int64(123); !cmp.Equal(got, want) { + t.Errorf("after XMove, state1.stack[2] = %v; want %v", got, want) + } + if got, want := state1.Type(3), TypeTable; got != want { + t.Errorf("after XMove, state1.Type(3) = %v; want %v", got, want) + } else if got, want := state1.ID(3), tableID; got != want { + t.Errorf("after XMove, state1.ID(3) = %d; want %d", got, want) + } + + if got, want := state2.Top(), 0; got != want { + t.Errorf("after XMove, state2.Top() = %d; want %d", got, want) + } + }) +} + func TestRotate(t *testing.T) { tests := []struct { s []int