Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions alloc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
21 changes: 19 additions & 2 deletions compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -1407,14 +1415,23 @@ 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
}
}
}
code.SetB(tablepc, int2Fb(arraycount))
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))
}
} // }}}

Expand Down
18 changes: 16 additions & 2 deletions state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/* }}} */
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down
88 changes: 88 additions & 0 deletions table_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package lua

import (
"fmt"
"strings"
"testing"
)

Expand Down Expand Up @@ -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))
}