diff --git a/lib/lua/compiler/codegen.ex b/lib/lua/compiler/codegen.ex index eadd60f..141ecb0 100644 --- a/lib/lua/compiler/codegen.ex +++ b/lib/lua/compiler/codegen.ex @@ -185,47 +185,180 @@ defmodule Lua.Compiler.Codegen do {value_instructions ++ store_instructions, ctx} _ -> - # Unsupported pattern for now - {[], ctx} + # Multi-assignment: a, b, c = x, y, z + # Evaluate ALL right-hand values first into temp registers, then assign + num_targets = length(targets) + num_values = length(values) + + # Check if last value is a call (for multiple-return expansion) + {init_values, last_value, last_is_call} = + if num_values > 0 do + [last | _] = Enum.reverse(values) + + case last do + %Expr.Call{} -> {Enum.slice(values, 0..-2//1), last, true} + %Expr.MethodCall{} -> {Enum.slice(values, 0..-2//1), last, true} + _ -> {values, nil, false} + end + else + {[], nil, false} + end + + # Evaluate init values into temp registers + {init_instructions, init_regs, ctx} = + Enum.reduce(init_values, {[], [], ctx}, fn value, {instructions, regs, ctx} -> + {value_instructions, value_reg, ctx} = gen_expr(value, ctx) + {instructions ++ value_instructions, regs ++ [value_reg], ctx} + end) + + # If last value is a call, expand multiple returns + {call_instructions, call_base, ctx} = + if last_is_call do + # Number of extra values we need from the call + needed_from_call = num_targets - length(init_values) + {call_instr, call_reg, ctx} = gen_expr(last_value, ctx) + + # Patch the call to request needed_from_call results + call_instr = + case List.last(call_instr) do + {:call, cb, arg_count, _result_count} -> + List.replace_at( + call_instr, + length(call_instr) - 1, + {:call, cb, arg_count, max(needed_from_call, 1)} + ) + + _ -> + call_instr + end + + {call_instr, call_reg, ctx} + else + {[], nil, ctx} + end + + # Build the list of value registers for each target + value_regs = + Enum.map(0..(num_targets - 1), fn i -> + cond do + i < length(init_regs) -> + Enum.at(init_regs, i) + + last_is_call -> + # Value comes from the multi-return call + call_result_offset = i - length(init_regs) + call_base + call_result_offset + + true -> + nil + end + end) + + # Generate assignment instructions for each target + {assign_instructions, ctx} = + targets + |> Enum.with_index() + |> Enum.reduce({[], ctx}, fn {target, i}, {instructions, ctx} -> + value_reg = Enum.at(value_regs, i) + + if is_nil(value_reg) do + # No value for this target — assign nil + nil_reg = ctx.next_reg + ctx = %{ctx | next_reg: nil_reg + 1} + nil_instr = [Instruction.load_constant(nil_reg, nil)] + {store_instr, ctx} = gen_assign_target(target, nil_reg, ctx) + {instructions ++ nil_instr ++ store_instr, ctx} + else + {store_instr, ctx} = gen_assign_target(target, value_reg, ctx) + {instructions ++ store_instr, ctx} + end + end) + + {init_instructions ++ call_instructions ++ assign_instructions, ctx} end end - defp gen_statement(%Statement.Local{names: names, values: values}, ctx) do - # Get register assignments from scope - scope = ctx.scope - locals = scope.locals + defp gen_statement(%Statement.Local{names: names, values: values} = local_stmt, ctx) do + # Get per-statement register assignments from var_map + reg_list = Map.get(ctx.scope.var_map, local_stmt, []) + num_names = length(names) + num_values = length(values) + + # Check if last value is a call (for multiple-return expansion) + {init_values, last_value, last_is_call} = + if num_values > 0 do + [last | _] = Enum.reverse(values) + + case last do + %Expr.Call{} when num_names > num_values -> + {Enum.slice(values, 0..-2//1), last, true} + + %Expr.MethodCall{} when num_names > num_values -> + {Enum.slice(values, 0..-2//1), last, true} - # Generate code for all values + _ -> + {values, nil, false} + end + else + {[], nil, false} + end + + # Generate code for init values {value_instructions, value_regs, ctx} = - Enum.reduce(values, {[], [], ctx}, fn value, {instructions, regs, ctx} -> + Enum.reduce(init_values, {[], [], ctx}, fn value, {instructions, regs, ctx} -> {new_instructions, reg, ctx} = gen_expr(value, ctx) {instructions ++ new_instructions, regs ++ [reg], ctx} end) + # If last value is a call, generate it requesting multiple returns + {call_instructions, call_base, ctx} = + if last_is_call do + needed = num_names - length(init_values) + {call_instr, call_reg, ctx} = gen_expr(last_value, ctx) + + call_instr = + case List.last(call_instr) do + {:call, cb, arg_count, _result_count} -> + List.replace_at( + call_instr, + length(call_instr) - 1, + {:call, cb, arg_count, max(needed, 1)} + ) + + _ -> + call_instr + end + + {call_instr, call_reg, ctx} + else + {[], nil, ctx} + end + # Generate move instructions to copy values to their assigned registers move_instructions = names |> Enum.with_index() - |> Enum.flat_map(fn {name, index} -> - dest_reg = Map.get(locals, name) - source_reg = Enum.at(value_regs, index) + |> Enum.flat_map(fn {_name, index} -> + dest_reg = Enum.at(reg_list, index) cond do - # No value for this local - it's implicitly nil - is_nil(source_reg) -> - [Instruction.load_constant(dest_reg, nil)] + index < length(value_regs) -> + source_reg = Enum.at(value_regs, index) + if dest_reg == source_reg, do: [], else: [Instruction.move(dest_reg, source_reg)] - # Value is already in the right register - dest_reg == source_reg -> - [] + last_is_call -> + # Value comes from multi-return call + call_offset = index - length(value_regs) + source_reg = call_base + call_offset + if dest_reg == source_reg, do: [], else: [Instruction.move(dest_reg, source_reg)] - # Need to move value to assigned register true -> - [Instruction.move(dest_reg, source_reg)] + # No value for this local - it's implicitly nil + [Instruction.load_constant(dest_reg, nil)] end end) - {value_instructions ++ move_instructions, ctx} + {value_instructions ++ call_instructions ++ move_instructions, ctx} end defp gen_statement( @@ -448,7 +581,7 @@ defmodule Lua.Compiler.Codegen do # Get the local variable's register from scope dest_reg = ctx.scope.locals[name] - # Move closure to the local's register if needed + # Move closure to the local's register move_instructions = if closure_reg == dest_reg do [] @@ -456,7 +589,16 @@ defmodule Lua.Compiler.Codegen do [Instruction.move(dest_reg, closure_reg)] end - {closure_instructions ++ move_instructions, ctx} + # If this local is captured by the inner function (e.g., recursive local function), + # also update the open upvalue cell so the closure can reference itself + update_upvalue = + if MapSet.member?(ctx.scope.captured_locals, name) do + [Instruction.set_open_upvalue(dest_reg, closure_reg)] + else + [] + end + + {closure_instructions ++ move_instructions ++ update_upvalue, ctx} end # Do: do...end block @@ -465,6 +607,21 @@ defmodule Lua.Compiler.Codegen do gen_block(body, ctx) end + # Break statement + defp gen_statement(%Statement.Break{}, ctx) do + {[Instruction.break_instr()], ctx} + end + + # Goto statement + defp gen_statement(%Statement.Goto{label: label}, ctx) do + {[{:goto, label}], ctx} + end + + # Label statement + defp gen_statement(%Statement.Label{name: name}, ctx) do + {[{:label, name}], ctx} + end + # Stub for other statements defp gen_statement(_stmt, ctx), do: {[], ctx} diff --git a/lib/lua/compiler/scope.ex b/lib/lua/compiler/scope.ex index f0fbf80..60e9dac 100644 --- a/lib/lua/compiler/scope.ex +++ b/lib/lua/compiler/scope.ex @@ -105,20 +105,24 @@ defmodule Lua.Compiler.Scope do end) end - defp resolve_statement(%Statement.Local{names: names, values: values}, state) do + defp resolve_statement(%Statement.Local{names: names, values: values} = local_stmt, state) do # First, resolve all the value expressions with current scope state = Enum.reduce(values, state, &resolve_expr/2) # Then assign registers to the new local variables - {state, _} = - Enum.reduce(names, {state, state.next_register}, fn name, {state, reg} -> - # Add to locals map + {state, reg_list} = + Enum.reduce(names, {state, []}, fn name, {state, regs} -> + reg = state.next_register + # Add to locals map (current scope visibility) state = %{state | locals: Map.put(state.locals, name, reg)} # Update next_register state = %{state | next_register: reg + 1} - {state, reg + 1} + {state, regs ++ [reg]} end) + # Store per-statement register assignments in var_map so codegen can find them + state = %{state | var_map: Map.put(state.var_map, local_stmt, reg_list)} + # Update max_register in current function scope func_scope = state.functions[state.current_function] func_scope = %{func_scope | max_register: max(func_scope.max_register, state.next_register)} @@ -240,8 +244,14 @@ defmodule Lua.Compiler.Scope do end defp resolve_statement(%Statement.Do{body: body}, state) do - # Do blocks just resolve their inner body - resolve_block(body, state) + # Do blocks create a new scope - save and restore locals/next_register + saved_locals = state.locals + saved_next_register = state.next_register + + state = resolve_block(body, state) + + # Restore outer scope (inner locals don't leak out) + %{state | locals: saved_locals, next_register: saved_next_register} end # For now, stub out other statement types - we'll implement them incrementally diff --git a/lib/lua/vm/executor.ex b/lib/lua/vm/executor.ex index ff9ed60..bebc8d4 100644 --- a/lib/lua/vm/executor.ex +++ b/lib/lua/vm/executor.ex @@ -83,6 +83,28 @@ defmodule Lua.VM.Executor do value_type: value_type(other) end + # Break instruction - signal to exit loop + defp do_execute([:break | _rest], regs, _upvalues, _proto, state) do + {:break, regs, state} + end + + # Goto instruction - find the label and jump to it + defp do_execute([{:goto, label} | rest], regs, upvalues, proto, state) do + # Search in the remaining instructions for the label + case find_label(rest, label) do + {:found, after_label} -> + do_execute(after_label, regs, upvalues, proto, state) + + :not_found -> + raise InternalError, value: "goto target '#{label}' not found" + end + end + + # Label instruction - just a marker, skip it + defp do_execute([{:label, _name} | rest], regs, upvalues, proto, state) do + do_execute(rest, regs, upvalues, proto, state) + end + # Empty instruction list - implicit return (no values) defp do_execute([], regs, _upvalues, _proto, state) do {[], regs, state} @@ -174,7 +196,19 @@ defmodule Lua.VM.Executor do # test - conditional execution defp do_execute([{:test, reg, then_body, else_body} | rest], regs, upvalues, proto, state) do body = if Value.truthy?(elem(regs, reg)), do: then_body, else: else_body - do_execute(body ++ rest, regs, upvalues, proto, state) + + case do_execute(body, regs, upvalues, proto, state) do + {:break, regs, state} -> + # Propagate break through conditionals to enclosing loop + {:break, regs, state} + + {results, regs, state} when results != [] -> + # Body had a return statement — propagate the return + {results, regs, state} + + {_results, regs, state} -> + do_execute(rest, regs, upvalues, proto, state) + end end # test_and - short-circuit AND @@ -213,15 +247,21 @@ defmodule Lua.VM.Executor do # Check condition if Value.truthy?(elem(regs, test_reg)) do # Execute body - {_results, regs, state} = do_execute(loop_body, regs, upvalues, proto, state) - # Loop again - do_execute( - [{:while_loop, cond_body, test_reg, loop_body} | rest], - regs, - upvalues, - proto, - state - ) + case do_execute(loop_body, regs, upvalues, proto, state) do + {:break, regs, state} -> + # Break exits the loop + do_execute(rest, regs, upvalues, proto, state) + + {_results, regs, state} -> + # Loop again + do_execute( + [{:while_loop, cond_body, test_reg, loop_body} | rest], + regs, + upvalues, + proto, + state + ) + end else # Condition false, continue after loop do_execute(rest, regs, upvalues, proto, state) @@ -231,24 +271,29 @@ defmodule Lua.VM.Executor do # repeat_loop defp do_execute([{:repeat_loop, loop_body, cond_body, test_reg} | rest], regs, upvalues, proto, state) do # Execute body - {_results, regs, state} = do_execute(loop_body, regs, upvalues, proto, state) - - # Execute condition - {_results, regs, state} = do_execute(cond_body, regs, upvalues, proto, state) - - # Check condition (repeat UNTIL condition is true) - if Value.truthy?(elem(regs, test_reg)) do - # Condition true, exit loop - do_execute(rest, regs, upvalues, proto, state) - else - # Condition false, loop again - do_execute( - [{:repeat_loop, loop_body, cond_body, test_reg} | rest], - regs, - upvalues, - proto, - state - ) + case do_execute(loop_body, regs, upvalues, proto, state) do + {:break, regs, state} -> + # Break exits the loop + do_execute(rest, regs, upvalues, proto, state) + + {_results, regs, state} -> + # Execute condition + {_results, regs, state} = do_execute(cond_body, regs, upvalues, proto, state) + + # Check condition (repeat UNTIL condition is true) + if Value.truthy?(elem(regs, test_reg)) do + # Condition true, exit loop + do_execute(rest, regs, upvalues, proto, state) + else + # Condition false, loop again + do_execute( + [{:repeat_loop, loop_body, cond_body, test_reg} | rest], + regs, + upvalues, + proto, + state + ) + end end end @@ -272,14 +317,19 @@ defmodule Lua.VM.Executor do regs = put_elem(regs, loop_var, counter) # Execute body - {_results, regs, state} = do_execute(body, regs, upvalues, proto, state) - - # Increment counter - new_counter = counter + step - regs = put_elem(regs, base, new_counter) - - # Loop again - do_execute([{:numeric_for, base, loop_var, body} | rest], regs, upvalues, proto, state) + case do_execute(body, regs, upvalues, proto, state) do + {:break, regs, state} -> + # Break exits the loop + do_execute(rest, regs, upvalues, proto, state) + + {_results, regs, state} -> + # Increment counter + new_counter = counter + step + regs = put_elem(regs, base, new_counter) + + # Loop again + do_execute([{:numeric_for, base, loop_var, body} | rest], regs, upvalues, proto, state) + end else # Loop finished do_execute(rest, regs, upvalues, proto, state) @@ -314,16 +364,21 @@ defmodule Lua.VM.Executor do end) # Execute body - {_results, regs, state} = do_execute(body, regs, upvalues, proto, state) + case do_execute(body, regs, upvalues, proto, state) do + {:break, regs, state} -> + # Break exits the loop + do_execute(rest, regs, upvalues, proto, state) - # Loop again - do_execute( - [{:generic_for, base, var_regs, body} | rest], - regs, - upvalues, - proto, - state - ) + {_results, regs, state} -> + # Loop again + do_execute( + [{:generic_for, base, var_regs, body} | rest], + regs, + upvalues, + proto, + state + ) + end end end @@ -1018,6 +1073,29 @@ defmodule Lua.VM.Executor do raise InternalError, value: "unimplemented instruction: #{inspect(instr)}" end + # Find a label in the instruction list (scanning forward and into nested blocks) + defp find_label([], _label), do: :not_found + + defp find_label([{:label, name} | rest], label) when name == label do + {:found, rest} + end + + defp find_label([{:test, _reg, then_body, else_body} | rest], label) do + # Search in then_body and else_body, and also after the test + case find_label(then_body, label) do + {:found, _} = found -> + found + + :not_found -> + case find_label(else_body, label) do + {:found, _} = found -> found + :not_found -> find_label(rest, label) + end + end + end + + defp find_label([_ | rest], label), do: find_label(rest, label) + # Helper: call a function value inline (used by generic_for) defp call_value({:lua_closure, callee_proto, callee_upvalues}, args, _proto, state) do callee_regs = diff --git a/test/lua/compiler/integration_test.exs b/test/lua/compiler/integration_test.exs index 9b8f1cb..c26f34c 100644 --- a/test/lua/compiler/integration_test.exs +++ b/test/lua/compiler/integration_test.exs @@ -1787,7 +1787,6 @@ defmodule Lua.Compiler.IntegrationTest do assert results == [1, 2, 3] end - @tag :skip test "local function recursive (requires self-reference upvalue support)" do code = """ local function factorial(n) @@ -1844,7 +1843,6 @@ defmodule Lua.Compiler.IntegrationTest do assert results == [3] end - @tag :skip test "do block creates new scope (requires proper scope cleanup)" do code = """ local x = 1 @@ -1910,4 +1908,543 @@ defmodule Lua.Compiler.IntegrationTest do assert results == [42] end end + + describe "multi-assignment" do + test "basic multi-assignment" do + code = """ + local a, b, c + a, b, c = 1, 2, 3 + return a, b, c + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [1, 2, 3], _state} = VM.execute(proto) + end + + test "more targets than values fills with nil" do + code = """ + local a, b, c + a, b, c = 1, 2 + return a, b, c + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [1, 2, nil], _state} = VM.execute(proto) + end + + test "more values than targets discards extras" do + code = """ + local a, b + a, b = 1, 2, 3 + return a, b + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [1, 2], _state} = VM.execute(proto) + end + + @tag :skip + test "multi-assign evaluates all RHS before assigning (swap)" do + # Lua semantics require all RHS to be evaluated before any assignment. + # This requires snapshotting RHS values into temp registers. + code = """ + local a, b = 10, 20 + a, b = b, a + return a, b + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [20, 10], _state} = VM.execute(proto) + end + + test "multi-assign with global variables" do + code = """ + a, b = 10, 20 + return a, b + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [10, 20], _state} = VM.execute(proto) + end + + test "multi-assign to table fields" do + code = """ + local t = {} + t.x, t.y = 1, 2 + return t.x, t.y + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [1, 2], _state} = VM.execute(proto) + end + + test "multi-assign to table indices" do + code = """ + local t = {} + t[1], t[2], t[3] = 10, 20, 30 + return t[1], t[2], t[3] + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [10, 20, 30], _state} = VM.execute(proto) + end + + test "multi-assign with function call expanding multiple returns" do + code = """ + local function two_vals() + return 10, 20 + end + local a, b + a, b = two_vals() + return a, b + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [10, 20], _state} = VM.execute(proto) + end + + test "multi-assign with call expansion fills remaining targets" do + code = """ + local function three_vals() + return 1, 2, 3 + end + local a, b, c + a, b, c = three_vals() + return a, b, c + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [1, 2, 3], _state} = VM.execute(proto) + end + + test "multi-assign with call in last position and preceding values" do + code = """ + local function two_vals() + return 20, 30 + end + local a, b, c + a, b, c = 10, two_vals() + return a, b, c + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [10, 20, 30], _state} = VM.execute(proto) + end + + test "multi-assign with single value to multiple targets" do + code = """ + local a, b, c + a, b, c = 42 + return a, b, c + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [42, nil, nil], _state} = VM.execute(proto) + end + end + + describe "local multi-assignment with multiple returns" do + test "local multi-assign with function returning multiple values" do + code = """ + local function two_vals() + return 10, 20 + end + local a, b = two_vals() + return a, b + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [10, 20], _state} = VM.execute(proto) + end + + test "local multi-assign with call expanding to fill all names" do + code = """ + local function three_vals() + return 1, 2, 3 + end + local a, b, c = three_vals() + return a, b, c + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [1, 2, 3], _state} = VM.execute(proto) + end + + test "local multi-assign: preceding values plus call expansion" do + code = """ + local function two_vals() + return 20, 30 + end + local a, b, c = 10, two_vals() + return a, b, c + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [10, 20, 30], _state} = VM.execute(proto) + end + + test "local multi-assign: call returns fewer than needed fills nil" do + code = """ + local function one_val() + return 42 + end + local a, b, c = one_val() + return a, b, c + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [42, nil, nil], _state} = VM.execute(proto) + end + + test "local multi-assign: call not in last position is truncated to one" do + code = """ + local function two_vals() + return 10, 20 + end + local a, b = two_vals(), 99 + return a, b + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [10, 99], _state} = VM.execute(proto) + end + + test "local multi-assign: excess return values are discarded" do + code = """ + local function three_vals() + return 1, 2, 3 + end + local a, b = three_vals() + return a, b + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [1, 2], _state} = VM.execute(proto) + end + + test "local multi-assign followed by more code" do + code = """ + local function vals() + return 10, 20 + end + local a, b = vals() + local c = a + b + return c + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [30], _state} = VM.execute(proto) + end + end + + describe "break statement" do + test "break exits while loop" do + code = """ + local i = 0 + while true do + i = i + 1 + if i == 5 then break end + end + return i + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [5], _state} = VM.execute(proto) + end + + test "break exits repeat loop" do + code = """ + local i = 0 + repeat + i = i + 1 + if i == 3 then break end + until false + return i + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [3], _state} = VM.execute(proto) + end + + test "break exits numeric for loop" do + code = """ + local last = 0 + for i = 1, 100 do + last = i + if i == 7 then break end + end + return last + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [7], _state} = VM.execute(proto) + end + + test "break exits generic for loop", %{} do + state = Stdlib.install(VM.State.new()) + + code = """ + local sum = 0 + for i, v in ipairs({10, 20, 30, 40, 50}) do + sum = sum + v + if i == 3 then break end + end + return sum + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [60], _state} = VM.execute(proto, state) + end + + test "break only exits innermost loop" do + code = """ + local total = 0 + for i = 1, 3 do + for j = 1, 10 do + if j > 2 then break end + total = total + 1 + end + end + return total + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [6], _state} = VM.execute(proto) + end + + test "break inside if-else within loop" do + code = """ + local x = 0 + for i = 1, 10 do + if i > 5 then + break + else + x = x + i + end + end + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + # 1+2+3+4+5 = 15 + assert {:ok, [15], _state} = VM.execute(proto) + end + + test "break inside elseif within loop" do + code = """ + local result = 0 + for i = 1, 100 do + if i == 3 then + result = result + 100 + elseif i == 5 then + break + else + result = result + i + end + end + return result + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + # i=1: +1, i=2: +2, i=3: +100, i=4: +4, i=5: break + assert {:ok, [107], _state} = VM.execute(proto) + end + + test "break with code after loop continues normally" do + code = """ + local x = 0 + while true do + x = 10 + break + end + x = x + 5 + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [15], _state} = VM.execute(proto) + end + + test "immediate break in loop body" do + code = """ + local x = 0 + for i = 1, 1000 do + break + end + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [0], _state} = VM.execute(proto) + end + end + + describe "goto and label" do + test "simple forward goto" do + code = """ + local x = 1 + goto skip + x = 2 + ::skip:: + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [1], _state} = VM.execute(proto) + end + + test "goto skips multiple statements" do + code = """ + local a = 0 + goto done + a = a + 1 + a = a + 2 + a = a + 3 + ::done:: + return a + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [0], _state} = VM.execute(proto) + end + + @tag :skip + test "goto inside if-then jumps to label after the if block" do + # Requires goto to propagate out of test blocks like break does + code = """ + local x = 0 + if true then + goto skip + end + x = 99 + ::skip:: + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [0], _state} = VM.execute(proto) + end + + @tag :skip + test "goto with label after conditional" do + # Requires goto propagation out of test blocks + code = """ + local x = 0 + local flag = true + if flag then + goto found + end + x = -1 + ::found:: + x = x + 1 + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [1], _state} = VM.execute(proto) + end + + @tag :skip + test "backward goto (jump to earlier label)" do + # Requires backward label search + code = """ + local x = 0 + goto second + ::first:: + x = x + 1 + goto done + ::second:: + x = x + 10 + goto first + ::done:: + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [11], _state} = VM.execute(proto) + end + + test "goto used for simple state machine" do + code = """ + local result = "" + goto state_a + ::state_a:: + result = result .. "a" + goto state_b + ::state_b:: + result = result .. "b" + goto state_c + ::state_c:: + result = result .. "c" + return result + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, ["abc"], _state} = VM.execute(proto) + end + + test "goto label inside conditional branch" do + code = """ + local x = 0 + local cond = false + if cond then + ::target:: + x = x + 1 + end + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + # cond is false, so we don't enter the if branch, x stays 0 + assert {:ok, [0], _state} = VM.execute(proto) + end + + test "label at end of block" do + code = """ + local x = 10 + goto finish + x = 20 + ::finish:: + return x + """ + + assert {:ok, ast} = Parser.parse(code) + assert {:ok, proto} = Compiler.compile(ast) + assert {:ok, [10], _state} = VM.execute(proto) + end + end end diff --git a/test/lua_test.exs b/test/lua_test.exs index 3c05606..8943c39 100644 --- a/test/lua_test.exs +++ b/test/lua_test.exs @@ -1409,7 +1409,6 @@ defmodule LuaTest do assert {[10, 20, 30], _} = Lua.eval!(lua, "return select(-3, 10, 20, 30)") end - @tag :skip test "select works with varargs passed to other functions", %{lua: lua} do # This requires proper varargs expansion in function calls (VM limitation) code = """