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
22 changes: 18 additions & 4 deletions compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ type codeStore struct { // {{{
codes *[]uint32
lines *[]int
pc int
// lastTarget is the highest pc recorded as a jump-label position.
// Instruction-merging peepholes must not fold across it: an instruction
// at or before a label boundary can be skipped or jumped past, so
// extending it changes behavior on the jumping path. Mirrors PUC Lua's
// fs->lasttarget.
lastTarget int
}

func newCodeStore() *codeStore {
Expand All @@ -218,9 +224,10 @@ func newCodeStore() *codeStore {
*codes = (*codes)[:0]
*lines = (*lines)[:0]
return &codeStore{
codes: codes,
lines: lines,
pc: 0,
codes: codes,
lines: lines,
pc: 0,
lastTarget: -1,
}
}

Expand Down Expand Up @@ -297,9 +304,15 @@ func (cd *codeStore) PropagateMV(top int, save *int, reg *int, inc int) {
*reg = *reg + inc
}

func (cd *codeStore) MarkLabelPc(pc int) {
if pc > cd.lastTarget {
cd.lastTarget = pc
}
}

func (cd *codeStore) AddLoadNil(a, b, line int) {
last := cd.Last()
if opGetOpCode(last) == OP_LOADNIL && (opGetArgB(last)+1) == a {
if opGetOpCode(last) == OP_LOADNIL && (opGetArgB(last)+1) == a && cd.LastPC() > cd.lastTarget {
cd.SetB(cd.LastPC(), b)
} else {
cd.AddABC(OP_LOADNIL, a, b, 0, line)
Expand Down Expand Up @@ -592,6 +605,7 @@ func (fc *funcContext) NewLabel() int {

func (fc *funcContext) SetLabelPc(label int, pc int) {
fc.labelPc[label] = pc
fc.Code.MarkLabelPc(pc)
}

func (fc *funcContext) GetLabelPc(label int) int {
Expand Down
112 changes: 112 additions & 0 deletions compile_loadnil_fence_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-License-Identifier: MPL-2.0

package lua

import (
"testing"
)

// The LOADNIL merge peephole (codeStore.AddLoadNil) must not extend a LOADNIL
// that sits before a jump target: an `or nil` arm ends in a skippable LOADNIL,
// and folding the next statement's nil-register init into it leaves that
// register uninitialized on the short-circuit path. The VM then dereferences
// an empty stack slot. Mirrors PUC Lua's `fs->lasttarget` fence in luaK_nil.
func TestLoadNilMergeStopsAtJumpTarget(t *testing.T) {
tests := []struct {
name string
code string
expected string
}{
{
// The or takes the truthy lhs: the skipped nil arm must not own
// the comparison's nil register.
name: "or nil over a table hit inside ipairs",
code: `
local t = { k = { skip = true } }
local out = 0
for _, key in ipairs({ "k" }) do
local v = t[key] or nil
if v ~= nil then out = out + 1 end
end
return out
`,
expected: "1",
},
{
name: "or nil over a table miss inside ipairs",
code: `
local t = { k = { skip = true } }
local out = 0
for _, key in ipairs({ "absent" }) do
local v = t[key] or nil
if v == nil then out = out + 1 end
end
return out
`,
expected: "1",
},
{
name: "scalar or nil inside ipairs",
code: `
local out = 0
for _, key in ipairs({ "k" }) do
local x = key or nil
if x ~= nil then out = out + 1 end
end
return out
`,
expected: "1",
},
{
name: "or nil inside pairs",
code: `
local t = { k = { skip = true } }
local out = 0
for key in pairs({ k = 1 }) do
local v = t[key] or nil
if v ~= nil then out = out + 1 end
end
return out
`,
expected: "1",
},
{
name: "guarded chain with mixed hit and miss keys",
code: `
local t = { k = { skip = true } }
local out = 0
for _, key in ipairs({ "a", "k" }) do
local v = key ~= nil and t[key] or nil
if v ~= nil and v.skip == true then out = out + 1 end
end
return out
`,
expected: "1",
},
{
// Adjacent nil locals with no label in between keep merging.
name: "plain adjacent nil locals still fold",
code: `
local a, b
local c = nil
if a == nil and b == nil and c == nil then return 1 end
return 0
`,
expected: "1",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
L := NewState()
defer L.Close()
if err := L.DoString(tt.code); err != nil {
t.Fatalf("runtime error: %v", err)
}
got := L.Get(-1)
if got.String() != tt.expected {
t.Fatalf("expected %s, got %s", tt.expected, got.String())
}
})
}
}
Loading