diff --git a/alloc.go b/alloc.go index 7a8cd63a..aeef13b5 100644 --- a/alloc.go +++ b/alloc.go @@ -47,6 +47,13 @@ func newAllocator(size int) *allocator { return al } +// LNumber2I is the same as the allocator method, exposed on LState +// so callers building LValues from Go-side data (e.g. encoders) can +// share the same amortized alloc strategy used internally. +func (ls *LState) LNumber2I(v LNumber) LValue { + return ls.alloc.LNumber2I(v) +} + // LNumber2I takes a number value and returns an interface LValue representing the same number. // Converting an LNumber to a LValue naively, by doing: // `var val LValue = myLNumber` diff --git a/compile.go b/compile.go index f9fbf576..68e64ea0 100644 --- a/compile.go +++ b/compile.go @@ -1361,6 +1361,13 @@ func compileTableExpr(context *funcContext, reg int, ex *ast.TableExpr, ec *expc tablepc := code.LastPC() regbase := reg + // Tracks whether any extended (C=0) OP_SETLIST was emitted. The next + // instruction in the stream is then a raw page-index word that + // happens to look like OP_MOVE A=0 B=0 to PropagateKMV. If no real + // instruction follows, the page-word gets popped by the caller's + // propagation pass, corrupting the bytecode. See safety MOVE below. + extendedSetlistEmitted := false + arraycount := 0 lastvararg := false for i, field := range ex.Fields { @@ -1393,7 +1400,8 @@ func compileTableExpr(context *funcContext, reg int, ex *ast.TableExpr, ec *expc if num == 0 { num = FieldsPerFlush } - c := (arraycount-1)/FieldsPerFlush + 1 + realC := (arraycount-1)/FieldsPerFlush + 1 + c := realC b := num if islast && isVarArgReturnExpr(field.Value) { b = 0 @@ -1407,7 +1415,10 @@ func compileTableExpr(context *funcContext, reg int, ex *ast.TableExpr, ec *expc } code.AddABC(OP_SETLIST, tablereg, b, c, sline(line)) if c == 0 { - code.Add(uint32(c), sline(line)) + // Extended OP_SETLIST: the next code word is the real + // page index (>= 512), bypassing the 9-bit C field. + code.Add(uint32(realC), sline(line)) + extendedSetlistEmitted = true } } } @@ -1415,6 +1426,12 @@ func compileTableExpr(context *funcContext, reg int, ex *ast.TableExpr, ec *expc code.SetC(tablepc, int2Fb(len(ex.Fields)-arraycount)) if shouldmove(ec, tablereg) { code.AddABC(OP_MOVE, ec.reg, tablereg, 0, sline(ex)) + } else if extendedSetlistEmitted { + // Guard: ensure the last instruction is a real OP_MOVE that the + // caller's PropagateKMV pass can safely pop, instead of the raw + // page-word left by the extended OP_SETLIST. MOVE tablereg <- + // tablereg is a no-op at runtime and propagates correctly. + code.AddABC(OP_MOVE, tablereg, tablereg, 0, sline(ex)) } } // }}} diff --git a/state.go b/state.go index 292f93b4..6f922952 100644 --- a/state.go +++ b/state.go @@ -109,6 +109,11 @@ type Options struct { // If `MinimizeStackMemory` is set, the call stack will be automatically grown or shrank up to a limit of // `CallStackSize` in order to minimize memory usage. This does incur a slight performance penalty. MinimizeStackMemory bool + // NumberPoolSize controls the page size of the internal LNumber + // allocator. Larger values reduce per-call allocations when many + // LNumbers are produced (e.g., encoding large numeric slices). + // Defaults to 32 if zero. + NumberPoolSize int } /* }}} */ @@ -662,7 +667,11 @@ func panicWithoutTraceback(L *LState) { } func newLState(options Options) *LState { - al := newAllocator(32) + pageSize := options.NumberPoolSize + if pageSize <= 0 { + pageSize = 32 + } + al := newAllocator(pageSize) ls := &LState{ G: newGlobal(), Parent: nil, @@ -787,7 +796,12 @@ func (ls *LState) where(level int, skipg bool) string { } line := "" if proto != nil { - line = fmt.Sprintf("%v:", proto.DbgSourcePositions[cf.Pc-1]) + pos := cf.Pc - 1 + if pos >= 0 && pos < len(proto.DbgSourcePositions) { + line = fmt.Sprintf("%v:", proto.DbgSourcePositions[pos]) + } else { + line = "?:" + } } return fmt.Sprintf("%v:%v", sourcename, line) } diff --git a/table_test.go b/table_test.go index 6acbbb2c..320bc4e1 100644 --- a/table_test.go +++ b/table_test.go @@ -1,6 +1,8 @@ package lua import ( + "fmt" + "strings" "testing" ) @@ -231,3 +233,89 @@ func TestTableForEach(t *testing.T) { } }) } + +// genArrayLiteral builds `return { ["outer"] = { ["arr"] = { 1, 2, ..., n } } }`. +// The nested shape is important: it puts the array inside a parent table +// expression, which is what triggers the PropagateKMV cascade for the +// second bug (see compile.go compileTableExpr / extended OP_SETLIST). +func genArrayLiteral(n int) string { + var b strings.Builder + b.WriteString(`return { ["outer"] = { ["arr"] = {`) + for i := 0; i < n; i++ { + fmt.Fprintf(&b, "%d,", i+1) + } + b.WriteString("} } }") + return b.String() +} + +// TestCompileTableExpr_LargeArrayLiteral exercises OP_SETLIST's extended +// (C=0) form, which is required once a single array literal exceeds +// 25,550 elements (MAXARG_C * FieldsPerFlush). Pre-fix, the compiler +// emitted a 0 page-index instead of the real one AND the page-index +// word was incorrectly popped by PropagateKMV, dropping the parent +// key's SETTABLE entirely. We assert both the array contents and the +// parent key assignment survive. +func TestCompileTableExpr_LargeArrayLiteral(t *testing.T) { + for _, n := range []int{ + FieldsPerFlush, // 50 — one full batch + FieldsPerFlush * 511, // 25550 — last single-instruction page (C=511) + FieldsPerFlush*511 + 1, // 25551 — first extended (C=0) page + FieldsPerFlush * 600, // 30000 — well past the threshold + FieldsPerFlush*511 + (50 * 50), // hybrid: many extended pages + } { + t.Run(fmt.Sprintf("n=%d", n), func(t *testing.T) { + L := NewState() + defer L.Close() + if err := L.DoString(genArrayLiteral(n)); err != nil { + t.Fatalf("DoString: %v", err) + } + root := L.Get(-1) + if root.Type() != LTTable { + t.Fatalf("root: want table, got %s", root.Type()) + } + outer := root.(*LTable).RawGetString("outer") + if outer.Type() != LTTable { + t.Fatalf("outer: want table, got %s — parent SETTABLE was dropped", outer.Type()) + } + arr := outer.(*LTable).RawGetString("arr") + if arr.Type() != LTTable { + t.Fatalf("outer.arr: want table, got %s — parent SETTABLE was dropped", arr.Type()) + } + arrT := arr.(*LTable) + errorIfNotEqual(t, n, arrT.Len()) + // Spot-check first, middle, last to confirm we didn't get a + // zero-offset corruption (where elements land at index 0/-1). + errorIfNotEqual(t, LNumber(1), arrT.RawGetInt(1)) + errorIfNotEqual(t, LNumber(n/2), arrT.RawGetInt(n/2)) + errorIfNotEqual(t, LNumber(n), arrT.RawGetInt(n)) + }) + } +} + +// TestCompileTableExpr_ExtendedSetlistAtTopLevel covers the simpler case +// where the oversized array literal is returned directly (not nested). +// Only Bug #1 (page-index = 0) hits here; the PropagateKMV cascade +// requires a parent expression. Pre-fix, every element after index +// 25,550 was written to negative array indices and lost. +func TestCompileTableExpr_ExtendedSetlistAtTopLevel(t *testing.T) { + const n = 30000 + var b strings.Builder + b.WriteString("return {") + for i := 0; i < n; i++ { + fmt.Fprintf(&b, "%d,", i+1) + } + b.WriteString("}") + + L := NewState() + defer L.Close() + if err := L.DoString(b.String()); err != nil { + t.Fatalf("DoString: %v", err) + } + tv := L.Get(-1) + if tv.Type() != LTTable { + t.Fatalf("want table, got %s", tv.Type()) + } + arr := tv.(*LTable) + errorIfNotEqual(t, n, arr.Len()) + errorIfNotEqual(t, LNumber(n), arr.RawGetInt(n)) +}