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
47 changes: 28 additions & 19 deletions compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,10 +541,10 @@ func (fc *funcContext) BlockLocalVarsCount() int {
return count
}

func (fc *funcContext) RegisterLocalVar(name string) int {
func (fc *funcContext) RegisterLocalVar(name string, line int) int {
ret := fc.Block.LocalVars.Register(name)
fc.Proto.DbgLocals = append(fc.Proto.DbgLocals, &DbgLocalInfo{Name: name, StartPc: fc.Code.LastPC() + 1})
fc.SetRegTop(fc.RegTop() + 1)
fc.SetRegTop(fc.RegTop()+1, line)
return ret
}

Expand Down Expand Up @@ -592,7 +592,7 @@ func (fc *funcContext) LeaveBlock() int {
fc.ResolveCurrentBlockGotosWithParentBlock()
}
fc.Block = fc.Block.Parent
fc.SetRegTop(fc.Block.LocalVars.LastIndex())
fc.SetRegTop(fc.Block.LocalVars.LastIndex(), fc.Proto.LineDefined)
return closed
}

Expand All @@ -602,9 +602,9 @@ func (fc *funcContext) EndScope() {
}
}

func (fc *funcContext) SetRegTop(top int) {
func (fc *funcContext) SetRegTop(top, line int) {
if top > maxRegisters {
raiseCompileError(fc, fc.Proto.LineDefined, "too many local variables")
raiseCompileError(fc, line, "too many local variables")
}
fc.regTop = top
}
Expand Down Expand Up @@ -857,15 +857,15 @@ func compileLocalAssignStmt(context *funcContext, stmt *ast.LocalAssignStmt) { /
reg := context.RegTop()
if len(stmt.Names) == 1 && len(stmt.Exprs) == 1 {
if _, ok := stmt.Exprs[0].(*ast.FunctionExpr); ok {
context.RegisterLocalVar(stmt.Names[0])
context.RegisterLocalVar(stmt.Names[0], sline(stmt))
compileRegAssignment(context, stmt.Names, stmt.Exprs, reg, len(stmt.Names), sline(stmt))
return
}
}

compileRegAssignment(context, stmt.Names, stmt.Exprs, reg, len(stmt.Names), sline(stmt))
for _, name := range stmt.Names {
context.RegisterLocalVar(name)
context.RegisterLocalVar(name, sline(stmt))
}
} // }}}

Expand Down Expand Up @@ -1054,17 +1054,17 @@ func compileNumberForStmt(context *funcContext, stmt *ast.NumberForStmt) { // {{

context.EnterBlock(endlabel, stmt)
reg := context.RegTop()
rindex := context.RegisterLocalVar("(for index)")
rindex := context.RegisterLocalVar("(for index)", sline(stmt))
ecupdate(ec, ecLocal, rindex, 0)
compileExpr(context, reg, stmt.Init, ec)

reg = context.RegTop()
rlimit := context.RegisterLocalVar("(for limit)")
rlimit := context.RegisterLocalVar("(for limit)", sline(stmt))
ecupdate(ec, ecLocal, rlimit, 0)
compileExpr(context, reg, stmt.Limit, ec)

reg = context.RegTop()
rstep := context.RegisterLocalVar("(for step)")
rstep := context.RegisterLocalVar("(for step)", sline(stmt))
if stmt.Step == nil {
stmt.Step = &ast.NumberExpr{Value: "1"}
stmt.Step.SetLine(sline(stmt.Init))
Expand All @@ -1074,7 +1074,7 @@ func compileNumberForStmt(context *funcContext, stmt *ast.NumberForStmt) { // {{

code.AddASbx(OP_FORPREP, rindex, 0, sline(stmt))

context.RegisterLocalVar(stmt.Name)
context.RegisterLocalVar(stmt.Name, sline(stmt))

bodypc := code.LastPC()
compileChunk(context, stmt.Stmts, false)
Expand All @@ -1097,16 +1097,16 @@ func compileGenericForStmt(context *funcContext, stmt *ast.GenericForStmt) { //
nnames := len(stmt.Names)

context.EnterBlock(endlabel, stmt)
rgen := context.RegisterLocalVar("(for generator)")
context.RegisterLocalVar("(for state)")
context.RegisterLocalVar("(for control)")
rgen := context.RegisterLocalVar("(for generator)", sline(stmt))
context.RegisterLocalVar("(for state)", sline(stmt))
context.RegisterLocalVar("(for control)", sline(stmt))

compileRegAssignment(context, stmt.Names, stmt.Exprs, context.RegTop()-3, 3, sline(stmt))

code.AddASbx(OP_JMP, 0, fllabel, sline(stmt))

for _, name := range stmt.Names {
context.RegisterLocalVar(name)
context.RegisterLocalVar(name, sline(stmt))
}

context.SetLabelPc(bodylabel, code.LastPC())
Expand Down Expand Up @@ -1313,16 +1313,16 @@ func compileFunctionExpr(context *funcContext, funcexpr *ast.FunctionExpr, ec *e
context.Proto.NumParameters = uint8(len(funcexpr.ParList.Names))
if ec.ctype == ecMethod {
context.Proto.NumParameters += 1
context.RegisterLocalVar("self")
context.RegisterLocalVar("self", sline(funcexpr))
}
for _, name := range funcexpr.ParList.Names {
context.RegisterLocalVar(name)
context.RegisterLocalVar(name, sline(funcexpr))
}
if funcexpr.ParList.HasVargs {
if CompatVarArg {
context.Proto.IsVarArg = VarArgHasArg | VarArgNeedsArg
if context.Parent != nil {
context.RegisterLocalVar("arg")
context.RegisterLocalVar("arg", sline(funcexpr))
}
}
context.Proto.IsVarArg |= VarArgIsVarArg
Expand All @@ -1336,6 +1336,9 @@ func compileFunctionExpr(context *funcContext, funcexpr *ast.FunctionExpr, ec *e
context.Proto.Code = context.Code.List()
context.Proto.DbgSourcePositions = context.Code.PosList()
context.Proto.DbgUpvalues = context.Upvalues.Names()
if len(context.Proto.DbgUpvalues) > math.MaxUint8 {
raiseCompileError(context, context.Proto.LineDefined, "too many upvalues")
}
context.Proto.NumUpvalues = uint8(len(context.Proto.DbgUpvalues))
for _, clv := range context.Proto.Constants {
sv := ""
Expand Down Expand Up @@ -1770,13 +1773,16 @@ func getExprName(context *funcContext, expr ast.Expr) string { // {{{

func patchCode(context *funcContext) { // {{{
maxreg := 1
overflowLine := context.Proto.LineDefined
if np := int(context.Proto.NumParameters); np > 1 {
maxreg = np
}
moven := 0
code := context.Code.List()
lines := context.Code.PosList()
for pc := 0; pc < len(code); pc++ {
inst := code[pc]
previousMax := maxreg
curop := opGetOpCode(inst)
switch curop {
case OP_CLOSURE:
Expand Down Expand Up @@ -1827,6 +1833,9 @@ func patchCode(context *funcContext) { // {{{
maxreg = reg
}
}
if previousMax < maxRegisters && maxreg >= maxRegisters {
overflowLine = lines[pc]
}

// bulk move optimization(reducing op dipatch costs)
if curop == OP_MOVE {
Expand All @@ -1841,7 +1850,7 @@ func patchCode(context *funcContext) { // {{{
}
maxreg++
if maxreg > maxRegisters {
raiseCompileError(context, context.Proto.LineDefined, "register overflow(too many local variables)")
raiseCompileError(context, overflowLine, "register overflow(too many local variables)")
}
context.Proto.NumUsedRegisters = uint8(maxreg)
} // }}}
Expand Down
138 changes: 138 additions & 0 deletions compile_locations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package lua

import (
"fmt"
"strings"
"testing"

"github.com/yuin/gopher-lua/parse"
)

func TestCompileRegisterLimitLocations(t *testing.T) {
locals := func(count int, initializer string) string {
var source strings.Builder
for i := 0; i < count; i++ {
fmt.Fprintf(&source, "local v%d%s\n", i, initializer)
}
return source.String()
}
call := func(arguments int) string {
return "consume(" + strings.TrimSuffix(strings.Repeat("1,", arguments), ",") + ")\n"
}
const mainPrefix = "-- main chunk\n"
const nestedPrefix = "-- nested function\n\n\n\nlocal function oversized()\n"
const localError = "too many local variables"
const registerError = "register overflow(too many local variables)"
locals195 := mainPrefix + locals(195, " = 1")
locals200 := mainPrefix + locals(200, " = 1")
multilineCall := "consume(\n" + strings.Repeat("1,\n", 19) + "1\n)\n"

tests := []struct {
name string
source string
wantLine int
wantMessage string
}{
{
name: "initialized_locals_in_main",
source: mainPrefix + locals(210, " = 1"),
wantLine: 202,
wantMessage: localError,
},
{
name: "initialized_locals_in_nested_function",
source: nestedPrefix + locals(210, " = 1") + "end\n",
wantLine: 206,
wantMessage: localError,
},
{
name: "uninitialized_locals_in_main",
source: mainPrefix + locals(210, ""),
wantLine: 202,
wantMessage: localError,
},
{
name: "uninitialized_locals_in_nested_function",
source: nestedPrefix + locals(210, "") + "end\n",
wantLine: 206,
wantMessage: localError,
},
{
name: "multiline_initializer_uses_declaration_start",
source: locals200 + "local overflow =\n1\n",
wantLine: 202,
wantMessage: localError,
},
{
name: "local_function_declaration",
source: locals200 + "local function overflow()\nend\n",
wantLine: 202,
wantMessage: localError,
},
{
name: "call_arguments_on_one_line",
source: locals195 + call(20),
wantLine: 197,
wantMessage: registerError,
},
{
name: "multiline_call_uses_first_overflowing_argument",
source: locals195 + multilineCall,
// The function uses register 195; the fifth argument uses register
// 200, the first beyond the 200-register limit, on source line 202.
wantLine: 202,
wantMessage: registerError,
},
{
name: "later_larger_call_keeps_first_overflow_location",
source: locals195 + call(20) + "\n" + call(40),
wantLine: 197,
wantMessage: registerError,
},
{
name: "accepted_200_locals",
source: locals200 + "return v199\n",
},
{
name: "accepted_call_below_register_limit",
source: locals195 + call(4),
},
{
name: "accepted_locals_reused_after_scope_exit",
source: "do\n" + locals(200, " = 1") + "end\n" +
"do\n" + locals(200, " = 1") + "end\nreturn 1\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name := tt.name + ".lua"
chunk, err := parse.Parse(strings.NewReader(tt.source), name)
if err != nil {
t.Fatalf("Parse failed before compilation: %v", err)
}
proto, err := Compile(chunk, name)
if tt.wantMessage == "" {
if err != nil {
t.Fatalf("Compile rejected an accepted register count: %v", err)
}
if proto == nil {
t.Fatal("Compile returned no function prototype")
}
return
}
if err == nil {
t.Fatal("Compile accepted an overflowing register count")
}
compileErr, ok := err.(*CompileError)
if !ok {
t.Fatalf("Compile error = %v (%T), want *CompileError", err, err)
}
if compileErr.Message != tt.wantMessage {
t.Errorf("Compile error message = %q, want %q", compileErr.Message, tt.wantMessage)
}
if compileErr.Line != tt.wantLine {
t.Errorf("Compile error line = %d, want %d", compileErr.Line, tt.wantLine)
}
})
}
}
68 changes: 68 additions & 0 deletions compile_upvalues_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package lua

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

func TestCompileUpvalueLimit(t *testing.T) {
tests := []struct {
upvalues int
wantError bool
}{
{254, false},
{255, false},
{256, true},
{300, true},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%d", tt.upvalues), func(t *testing.T) {
// Split locals between two enclosing functions so the leaf closure
// reaches the upvalue limit without reaching the local-variable limit.
var source strings.Builder
source.WriteString("local function outer()\n")
for i := 0; i < tt.upvalues/2; i++ {
fmt.Fprintf(&source, "local v%d = 1\n", i)
}
source.WriteString("return function()\n")
for i := tt.upvalues / 2; i < tt.upvalues; i++ {
fmt.Fprintf(&source, "local v%d = 1\n", i)
}
source.WriteString("return function()\nreturn ")
for i := 0; i < tt.upvalues; i++ {
if i > 0 {
source.WriteString(" + ")
}
fmt.Fprintf(&source, "v%d", i)
}
source.WriteString("\nend\nend\nend\nreturn outer()()()\n")

L := NewState()
defer L.Close()
fn, err := L.LoadString(source.String())
if tt.wantError {
if err == nil {
t.Fatal("LoadString accepted too many upvalues")
}
apiErr, ok := err.(*ApiError)
if !ok || apiErr.Type != ApiErrorSyntax {
t.Fatalf("LoadString error = %v (%T), want ApiErrorSyntax", err, err)
}
if !strings.Contains(err.Error(), "too many upvalues") {
t.Fatalf("LoadString error = %q, want too many upvalues", err)
}
return
}
if err != nil {
t.Fatalf("LoadString failed: %v", err)
}
if err := L.CallByParam(P{Fn: fn, NRet: 1, Protect: true}); err != nil {
t.Fatalf("closure execution failed: %v", err)
}
if got := L.Get(-1); got != LNumber(tt.upvalues) {
t.Fatalf("closure returned %v, want %d", got, tt.upvalues)
}
})
}
}