Files
plainleaf/client/space_lua/goto_test.lua
T
Matouš Jan FialkaandGitHub 7912f05949 Space Lua: Add goto and labels support (#1686)
* 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>
2025-11-18 16:35:08 +01:00

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