* Space Lua: Add AST block type label/goto and duplicated label bools * Space Lua: Add label/goto resolution and evaluation Add per block map of labels metadata, per function block metadata cache and `processBlock` to track AST blocks (`If`, `While`, `Repeat`, `For` and `ForIn`, and `Do`) while recording labels and their positions. Align with Lua scoping rules (_e.g._, a `goto` can not jump into the scope of a local). Also allow the super-special "safe" end of block cases (except inside `Repeat`). Duplicated labels are detected (error `label 'foo' is already defined` etc.). The AST context for error reporting is preserved. The `WeakMap` caches map any block to its function's metadata. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: The `goto` parse flags and eval path At parser mark each block with `hasLabel`/`hasGoto` and record the first duplicated label. Flags then propagate through blocks and control flow. If no `goto`, run the block in a simple loop and raise duplicate label error if any is present. If `goto` exists, jump via a label map. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Add labels and `goto` statement test suite Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Minor `goto`/labels improvement and test hotfix - The `getBlockGotoMeta` now called only when a block has either `hasGoto` or `hasLabel`. - Add `BlocksFlags` type to get rid of some `(... as any)`s by view. - Add `blockMetaOrThrow` helper. - Fix test to reflect standard Lua semantics. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Goto: Add missing Lua semantics checks Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Goto: Add Lua semantics checks also covered in Lua tests Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Goto: Fix semantic divergence revealed by fixing unfinished test Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix typo Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Optimize block env creation and cache goto metadata Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Avoid using the venomous snake case Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Add comment for WeakMap use Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Block Label Fast-path Optimization Add block flag `hasLabelHere` during parsing and use it in eval to skip expensive metadata generation for blocks and entire functions without label or goto. Refactor eval logic: - early fast path when a function is known to have no label/goto, - later fast path for blocks in functions with goto. Fetch metadata only when needed. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Remove `BlockFlags` indirection from eval - Add `hasLabelHere` flag to `LuaBlock`. - Remove `BlockFlags` indirection from eval. - Set `hasLabelHere` directly. - Add some "guards". - Improve consistency of child filtering. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Remove redundant return statement Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> --------- Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
452 lines
8.7 KiB
Lua
452 lines
8.7 KiB
Lua
local function assertEqual(a, b, message)
|
|
if a ~= b then
|
|
error("Assertion failed: " .. tostring(a)
|
|
.. " != " .. tostring(b) .. " " .. (message or ""))
|
|
end
|
|
end
|
|
|
|
local function contains(hay, needle)
|
|
return string.find(tostring(hay), needle, 1, true) ~= nil
|
|
end
|
|
|
|
local function expectError(fn, needle, msg)
|
|
local ok, err = pcall(fn)
|
|
if ok then
|
|
error("Expected error, but call succeeded: " .. (msg or ""))
|
|
end
|
|
local s = tostring(err)
|
|
if not contains(s, needle) then
|
|
error("Error missing expected text '" .. needle .. "': got " .. s)
|
|
end
|
|
end
|
|
|
|
-- Basic forward jump
|
|
do
|
|
local x = 0
|
|
goto after_init
|
|
x = -1 -- skipped
|
|
::after_init:: x = x + 2
|
|
assertEqual(x, 2)
|
|
end
|
|
|
|
-- Backward jump (simulated loop)
|
|
do
|
|
local n = 0
|
|
::loop_start::
|
|
n = n + 1
|
|
if n < 3 then goto loop_start end
|
|
assertEqual(n, 3)
|
|
end
|
|
|
|
-- Consecutive labels (two labels before one statement)
|
|
do
|
|
local v = 0
|
|
::first_label::::second_label:: v = v + 1
|
|
assertEqual(v, 1)
|
|
end
|
|
|
|
-- No visible label (forward into inner block / outward from inner)
|
|
do
|
|
local function f() goto missing; do ::missing:: end end
|
|
expectError(f, "no visible label 'missing'")
|
|
local function g() do ::inner:: end; goto inner end
|
|
expectError(g, "no visible label 'inner'")
|
|
end
|
|
|
|
-- Duplicate label in same block
|
|
do
|
|
local function f() ::dup:: ::dup:: end
|
|
expectError(f, "label 'dup'")
|
|
end
|
|
|
|
-- Jump into local scope (illegal forward into variable region)
|
|
do
|
|
local function f()
|
|
goto after_local
|
|
local a = 1
|
|
::after_local:: return a
|
|
end
|
|
expectError(f, "jumps into the scope")
|
|
end
|
|
|
|
-- Repeat loop: safe-end label NOT allowed to bypass scope
|
|
do
|
|
local function f()
|
|
local flag = false
|
|
repeat
|
|
if flag then goto after_repeat end
|
|
local hidden = 10
|
|
::after_repeat:: -- repeat end label scope test
|
|
until hidden and flag
|
|
end
|
|
expectError(f, "jumps into the scope")
|
|
end
|
|
|
|
-- Safe end-of-block backward jump (allowed)
|
|
do
|
|
local function f()
|
|
do local a = 1 end
|
|
::end_block:: return 42
|
|
end
|
|
assertEqual(f(), 42)
|
|
end
|
|
|
|
-- Multi-label end sentinel (extra labels + semicolons)
|
|
do
|
|
local x
|
|
do
|
|
local y = 12
|
|
goto set_x
|
|
::after_set:: x = x + 1; goto check_x
|
|
::set_x:: x = y; goto after_set
|
|
end
|
|
::check_x:: ::check_x_extra:: assertEqual(x, 13)
|
|
end
|
|
|
|
-- Skip local declarations (labels before local)
|
|
do
|
|
goto after_vars
|
|
local a = 23
|
|
a = a
|
|
::after_vars:: ;
|
|
end
|
|
|
|
-- While loop labels inside loop; external exit
|
|
do
|
|
local global = 13
|
|
while true do
|
|
goto exit_loop
|
|
goto loop_label
|
|
goto loop_label
|
|
local inner = 45
|
|
::loop_label:: ;;;
|
|
end
|
|
::exit_loop:: assertEqual(global, 13)
|
|
end
|
|
|
|
-- Mixed returns via labels (branch selection)
|
|
local function branch_test(a)
|
|
if a == 1 then
|
|
goto l1
|
|
elseif a == 2 then
|
|
goto l2
|
|
elseif a == 3 then
|
|
goto l3
|
|
elseif a == 4 then
|
|
goto l1
|
|
::l1:: a = a + 1
|
|
else
|
|
goto l4
|
|
::l4_a:: a = a * 2; goto l4_b
|
|
::l4:: goto l4_a
|
|
::l4_b::
|
|
end
|
|
do return a end
|
|
::l2:: do return "2" end
|
|
::l3:: do return "3" end
|
|
::l1:: return "1"
|
|
end
|
|
|
|
assertEqual(branch_test(1), "1")
|
|
assertEqual(branch_test(2), "2")
|
|
assertEqual(branch_test(3), "3")
|
|
assertEqual(branch_test(4), 5)
|
|
assertEqual(branch_test(5), 10)
|
|
|
|
-- Same label name in different functions (allowed)
|
|
do
|
|
local function f() ::same:: return 1 end
|
|
local function g() ::same:: return 2 end
|
|
assertEqual(f(), 1)
|
|
assertEqual(g(), 2)
|
|
end
|
|
|
|
-- Upward jump into ancestor block
|
|
do
|
|
local n = 0
|
|
::up_loop::
|
|
n = n + 1
|
|
if n < 2 then
|
|
do
|
|
goto up_loop
|
|
end
|
|
end
|
|
assertEqual(n, 2)
|
|
end
|
|
|
|
-- Escape potential infinite loop via early goto
|
|
do
|
|
goto escaped
|
|
::inf_a:: goto inf_a
|
|
::inf_b:: goto inf_c
|
|
::inf_c:: goto inf_b
|
|
::escaped:: ;
|
|
end
|
|
|
|
-- Sequential building of array with gotos
|
|
local function build_array()
|
|
local a = {}
|
|
goto start
|
|
::add_one:: a[#a + 1] = 1; goto add_two
|
|
::add_two:: a[#a + 1] = 2; goto add_five
|
|
::start:: ::start_extra:: a[#a + 1] = 3; goto add_one
|
|
::add_four:: a[#a + 1] = 4; goto finish
|
|
::add_five:: a[#a + 1] = 5; goto add_four
|
|
::finish:: return a
|
|
end
|
|
|
|
do
|
|
local arr = build_array()
|
|
assertEqual(arr[1], 3)
|
|
assertEqual(arr[2], 1)
|
|
assertEqual(arr[3], 2)
|
|
assertEqual(arr[4], 5)
|
|
assertEqual(arr[5], 4)
|
|
end
|
|
|
|
-- Long label name (basic acceptance)
|
|
do
|
|
::very_long_label_end_marker_example_for_stress_test_end::
|
|
local ok = true
|
|
assertEqual(ok, true)
|
|
end
|
|
|
|
-- Sibling block visibility (cannot jump to sibling label)
|
|
do
|
|
local function f()
|
|
do ::sib:: end
|
|
do goto sib end
|
|
end
|
|
expectError(f, "no visible label 'sib'", "cannot jump to sibling label")
|
|
end
|
|
|
|
-- Forward to inside for: label not visible from outside
|
|
do
|
|
local function f()
|
|
goto in_for
|
|
for i = 1, 2 do
|
|
::in_for:: return i
|
|
end
|
|
end
|
|
expectError(f, "no visible label 'in_for'")
|
|
end
|
|
|
|
-- Forward to inside for-in: label not visible from outside
|
|
do
|
|
local function f()
|
|
goto in_forin
|
|
for k, v in pairs({1,2}) do
|
|
::in_forin:: return k
|
|
end
|
|
end
|
|
expectError(f, "no visible label 'in_forin'")
|
|
end
|
|
|
|
-- Repeat block end scope jump (illegal)
|
|
do
|
|
local function f()
|
|
repeat
|
|
goto end_repeat
|
|
local z = 10
|
|
::end_repeat::
|
|
until z == 0
|
|
end
|
|
expectError(f, "jumps into the scope")
|
|
end
|
|
|
|
-- Nested upward multi-jump
|
|
do
|
|
local count = 0
|
|
::multi_up::
|
|
count = count + 1
|
|
if count < 3 then
|
|
do
|
|
do
|
|
goto multi_up
|
|
end
|
|
end
|
|
end
|
|
assertEqual(count, 3)
|
|
end
|
|
|
|
-- Late label after inner block; allowed upward visibility
|
|
do
|
|
local function f()
|
|
do
|
|
goto late_label
|
|
end
|
|
::late_label:: return 1
|
|
end
|
|
assertEqual(f(), 1)
|
|
end
|
|
|
|
-- Upward goto to ancestor label inside nested block
|
|
do
|
|
local function f()
|
|
local n = 0
|
|
::ancestor:: n = n + 1
|
|
do
|
|
if n < 2 then goto ancestor end
|
|
end
|
|
return n
|
|
end
|
|
assertEqual(f(), 2)
|
|
end
|
|
|
|
-- Same label name reused in separate blocks (legal)
|
|
do
|
|
local function f()
|
|
local t = {}
|
|
do ::lbl:: table.insert(t, "first") end
|
|
do ::lbl:: table.insert(t, "second") end
|
|
return t
|
|
end
|
|
local r = f()
|
|
assertEqual(r[1], "first")
|
|
assertEqual(r[2], "second")
|
|
end
|
|
|
|
-- Label visibility restricted to branch (goto in else to label in then)
|
|
do
|
|
local function f_bad(flag)
|
|
if flag then
|
|
::branch_label:: return "yes"
|
|
else
|
|
goto branch_label
|
|
end
|
|
end
|
|
expectError(function() f_bad(true) end, "no visible label 'branch_label'")
|
|
expectError(function() f_bad(false) end, "no visible label 'branch_label'")
|
|
end
|
|
|
|
-- Label visible to both branches (label placed after the if)
|
|
do
|
|
local function f_good(flag)
|
|
if not flag then goto after end
|
|
::after:: return "yes"
|
|
end
|
|
assertEqual(f_good(true), "yes")
|
|
assertEqual(f_good(false), "yes")
|
|
end
|
|
|
|
-- Jump over multiple locals but not into them (allowed backward)
|
|
do
|
|
local function f()
|
|
local a = 1
|
|
::compute::
|
|
local b = a + 1
|
|
local c = b + 1
|
|
if c < 5 then
|
|
a = c
|
|
goto compute -- backward: allowed
|
|
end
|
|
return c
|
|
end
|
|
assertEqual(f(), 5)
|
|
end
|
|
|
|
-- Label at end of block with only semicolons after (safe end)
|
|
do
|
|
local function f()
|
|
local x = 10
|
|
::end_ok:: ; ; ;
|
|
return x
|
|
end
|
|
assertEqual(f(), 10)
|
|
end
|
|
|
|
-- Goto into local scope through chained labels (error)
|
|
do
|
|
local function f()
|
|
goto after_chain
|
|
local p = 99
|
|
::chain_a::::chain_b::::after_chain:: return p
|
|
end
|
|
expectError(f, "jumps into the scope")
|
|
end
|
|
|
|
-- Cross-function goto (outer to inner label, invalid)
|
|
do
|
|
local function inner()
|
|
::in_label:: return 1
|
|
end
|
|
local function outer()
|
|
goto in_label
|
|
return 0
|
|
end
|
|
expectError(outer, "no visible label 'in_label'")
|
|
end
|
|
|
|
-- Cross-function goto (inner to outer label, invalid)
|
|
do
|
|
local function outer()
|
|
::outer_label:: ;
|
|
local function inner()
|
|
goto outer_label
|
|
end
|
|
return inner()
|
|
end
|
|
expectError(outer, "no visible label 'outer_label'")
|
|
end
|
|
|
|
-- Cross-function goto (outer after defining inner label, invalid)
|
|
do
|
|
local function outer()
|
|
local function inner()
|
|
::nested_label:: return 2
|
|
end
|
|
goto nested_label
|
|
end
|
|
expectError(outer, "no visible label 'nested_label'")
|
|
end
|
|
|
|
-- Forward goto to label not safe end (fails)
|
|
do
|
|
local function f()
|
|
goto after_locals
|
|
local a = 1
|
|
::after_locals:: local b = 2
|
|
return b
|
|
end
|
|
expectError(f, "jumps into the scope")
|
|
end
|
|
|
|
-- Repeat safe-end style still illegal
|
|
do
|
|
local function f()
|
|
repeat
|
|
goto done
|
|
local x = 1
|
|
::done:: ;
|
|
until x == 0
|
|
end
|
|
expectError(f, "jumps into the scope")
|
|
end
|
|
|
|
-- goto to correct label when nested
|
|
::l1::
|
|
do
|
|
local v = 0
|
|
goto l1
|
|
v = -1
|
|
::l1:: -- inner label shadowing the outer label
|
|
v = v + 1
|
|
assertEqual(v, 1)
|
|
end
|
|
|
|
-- regression test for Lua 5.2 bug
|
|
do
|
|
local x
|
|
::l1::
|
|
local y
|
|
assert(y == nil)
|
|
y = true
|
|
if x == nil then
|
|
x = 1
|
|
goto l1
|
|
else
|
|
x = x + 1
|
|
end
|
|
assert(x == 2 and y == true)
|
|
end
|