diff --git a/lib/lua/vm/executor.ex b/lib/lua/vm/executor.ex index da2184d..69905d4 100644 --- a/lib/lua/vm/executor.ex +++ b/lib/lua/vm/executor.ex @@ -2,7 +2,14 @@ defmodule Lua.VM.Executor do @moduledoc """ Instruction executor for the Lua VM. - Tail-recursive dispatch loop that executes instructions. + Fully tail-recursive CPS dispatch loop. The do_execute/8 function never + grows the Erlang call stack for Lua-to-Lua function calls or control flow. + + Signature: do_execute(instructions, registers, upvalues, proto, state, cont, frames, line) + + cont — continuation stack: list of instruction lists or loop/CPS markers + frames — call frame stack: saved caller context for each active Lua call + line — current source line (threaded to avoid State struct allocation) """ alias Lua.VM.InternalError @@ -20,7 +27,7 @@ defmodule Lua.VM.Executor do {list(), tuple(), State.t()} def execute(instructions, registers, upvalues, proto, state) do state = %{state | open_upvalues: %{}} - do_execute(instructions, registers, upvalues, proto, state) + do_execute(instructions, registers, upvalues, proto, state, [], [], 0) end @doc """ @@ -52,13 +59,7 @@ defmodule Lua.VM.Executor do state = %{state | open_upvalues: %{}} {results, _callee_regs, state} = - do_execute( - callee_proto.instructions, - callee_regs, - callee_upvalues, - callee_proto, - state - ) + do_execute(callee_proto.instructions, callee_regs, callee_upvalues, callee_proto, state, [], [], 0) state = %{state | open_upvalues: saved_open_upvalues} {results, state} @@ -106,216 +107,299 @@ defmodule Lua.VM.Executor do end end - # Break instruction - signal to exit loop - defp do_execute([:break | _rest], regs, _upvalues, _proto, state) do - {:break, regs, state} + # ── Break ────────────────────────────────────────────────────────────────── + + defp do_execute([:break | _rest], regs, upvalues, proto, state, cont, frames, line) do + {exit_is, rest_cont} = find_loop_exit(cont) + do_execute(exit_is, regs, upvalues, proto, state, rest_cont, frames, line) 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 + # ── Goto ─────────────────────────────────────────────────────────────────── + + defp do_execute([{:goto, label} | rest], regs, upvalues, proto, state, cont, frames, line) do case find_label(rest, label) do {:found, after_label} -> - do_execute(after_label, regs, upvalues, proto, state) + do_execute(after_label, regs, upvalues, proto, state, cont, frames, line) :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) + # ── Label ────────────────────────────────────────────────────────────────── + + defp do_execute([{:label, _name} | rest], regs, upvalues, proto, state, cont, frames, line) do + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # Empty instruction list - implicit return (no values) - defp do_execute([], regs, _upvalues, _proto, state) do - {[], regs, state} + # ── Instructions exhausted — handle continuations and frames ─────────────── + + defp do_execute([], regs, upvalues, proto, state, cont, frames, line) do + case cont do + # Normal instruction continuation + [next_is | rest_cont] when is_list(next_is) -> + do_execute(next_is, regs, upvalues, proto, state, rest_cont, frames, line) + + # Fell off end of a loop body normally — consume the loop_exit marker + [{:loop_exit, _} | rest_cont] -> + do_execute([], regs, upvalues, proto, state, rest_cont, frames, line) + + # After while condition body — check test_reg; enter body or exit loop + [{:cps_while_test, test_reg, loop_body, cond_body, rest, outer_cont} | _] -> + loop_exit_cont = [{:loop_exit, rest} | outer_cont] + + if Value.truthy?(elem(regs, test_reg)) do + body_done = {:cps_while_body, test_reg, loop_body, cond_body, rest, outer_cont} + do_execute(loop_body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line) + else + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line) + end + + # After while loop body — restart condition + [{:cps_while_body, test_reg, loop_body, cond_body, rest, outer_cont} | _] -> + loop_exit_cont = [{:loop_exit, rest} | outer_cont] + cond_check = {:cps_while_test, test_reg, loop_body, cond_body, rest, outer_cont} + do_execute(cond_body, regs, upvalues, proto, state, [cond_check | loop_exit_cont], frames, line) + + # After repeat body — execute condition + [{:cps_repeat_body, loop_body, cond_body, test_reg, rest, outer_cont} | _] -> + loop_exit_cont = [{:loop_exit, rest} | outer_cont] + cond_check = {:cps_repeat_cond, loop_body, cond_body, test_reg, rest, outer_cont} + do_execute(cond_body, regs, upvalues, proto, state, [cond_check | loop_exit_cont], frames, line) + + # After repeat condition — check test_reg; exit or repeat + [{:cps_repeat_cond, loop_body, cond_body, test_reg, rest, outer_cont} | _] -> + if Value.truthy?(elem(regs, test_reg)) do + # Condition true = exit loop (repeat UNTIL) + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line) + else + # Condition false = repeat body + loop_exit_cont = [{:loop_exit, rest} | outer_cont] + body_done = {:cps_repeat_body, loop_body, cond_body, test_reg, rest, outer_cont} + do_execute(loop_body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line) + end + + # After numeric_for body — increment counter and re-check + [{:cps_numeric_for, base, loop_var, body, rest, outer_cont} | _] -> + counter = elem(regs, base) + step = elem(regs, base + 2) + new_counter = counter + step + regs = put_elem(regs, base, new_counter) + limit = elem(regs, base + 1) + should_continue = if step > 0, do: new_counter <= limit, else: new_counter >= limit + + if should_continue do + regs = put_elem(regs, loop_var, new_counter) + + state = %{ + state + | open_upvalues: Map.reject(state.open_upvalues, fn {reg, _} -> reg >= loop_var end) + } + + loop_exit_cont = [{:loop_exit, rest} | outer_cont] + body_done = {:cps_numeric_for, base, loop_var, body, rest, outer_cont} + do_execute(body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line) + else + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line) + end + + # After generic_for body — call iterator and re-check + [{:cps_generic_for, base, var_regs, body, rest, outer_cont} | _] -> + iter_func = elem(regs, base) + invariant_state = elem(regs, base + 1) + control = elem(regs, base + 2) + + {results, state} = call_value(iter_func, [invariant_state, control], proto, state, line) + first_result = List.first(results) + + if first_result == nil do + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line) + else + regs = put_elem(regs, base + 2, first_result) + + regs = + var_regs + |> Enum.with_index() + |> Enum.reduce(regs, fn {var_reg, i}, r -> put_elem(r, var_reg, Enum.at(results, i)) end) + + first_var_reg = List.first(var_regs) + + state = %{ + state + | open_upvalues: Map.reject(state.open_upvalues, fn {reg, _} -> reg >= first_var_reg end) + } + + loop_exit_cont = [{:loop_exit, rest} | outer_cont] + body_done = {:cps_generic_for, base, var_regs, body, rest, outer_cont} + do_execute(body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line) + end + + # Continuation stack exhausted — check frames for pending function return + [] -> + case frames do + [] -> + {[], regs, state} + + [frame | rest_frames] -> + do_frame_return([], regs, state, frame, rest_frames, line) + end + end end - # load_constant - defp do_execute([{:load_constant, dest, value} | rest], regs, upvalues, proto, state) do + # ── load_constant ────────────────────────────────────────────────────────── + + defp do_execute([{:load_constant, dest, value} | rest], regs, upvalues, proto, state, cont, frames, line) do regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # load_boolean - defp do_execute([{:load_boolean, dest, value} | rest], regs, upvalues, proto, state) do + # ── load_boolean ─────────────────────────────────────────────────────────── + + defp do_execute([{:load_boolean, dest, value} | rest], regs, upvalues, proto, state, cont, frames, line) do regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # get_global - defp do_execute([{:get_global, dest, name} | rest], regs, upvalues, proto, state) do + # ── get_global ───────────────────────────────────────────────────────────── + + defp do_execute([{:get_global, dest, name} | rest], regs, upvalues, proto, state, cont, frames, line) do value = Map.get(state.globals, name, nil) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # set_global - defp do_execute([{:set_global, name, source} | rest], regs, upvalues, proto, state) do + # ── set_global ───────────────────────────────────────────────────────────── + + defp do_execute([{:set_global, name, source} | rest], regs, upvalues, proto, state, cont, frames, line) do value = elem(regs, source) state = %{state | globals: Map.put(state.globals, name, value)} - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # get_upvalue - defp do_execute([{:get_upvalue, dest, index} | rest], regs, upvalues, proto, state) do + # ── get_upvalue ──────────────────────────────────────────────────────────── + + defp do_execute([{:get_upvalue, dest, index} | rest], regs, upvalues, proto, state, cont, frames, line) do cell_ref = elem(upvalues, index) value = Map.get(state.upvalue_cells, cell_ref) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # set_upvalue - defp do_execute([{:set_upvalue, index, source} | rest], regs, upvalues, proto, state) do + # ── set_upvalue ──────────────────────────────────────────────────────────── + + defp do_execute([{:set_upvalue, index, source} | rest], regs, upvalues, proto, state, cont, frames, line) do cell_ref = elem(upvalues, index) value = elem(regs, source) state = %{state | upvalue_cells: Map.put(state.upvalue_cells, cell_ref, value)} - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # get_open_upvalue - read a captured local through its open upvalue cell - defp do_execute([{:get_open_upvalue, dest, reg} | rest], regs, upvalues, proto, state) do + # ── get_open_upvalue ─────────────────────────────────────────────────────── + + defp do_execute([{:get_open_upvalue, dest, reg} | rest], regs, upvalues, proto, state, cont, frames, line) do cell_ref = Map.fetch!(state.open_upvalues, reg) value = Map.get(state.upvalue_cells, cell_ref) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # set_open_upvalue - write a captured local through its open upvalue cell - defp do_execute([{:set_open_upvalue, reg, source} | rest], regs, upvalues, proto, state) do + # ── set_open_upvalue ─────────────────────────────────────────────────────── + + defp do_execute([{:set_open_upvalue, reg, source} | rest], regs, upvalues, proto, state, cont, frames, line) do cell_ref = Map.fetch!(state.open_upvalues, reg) value = elem(regs, source) state = %{state | upvalue_cells: Map.put(state.upvalue_cells, cell_ref, value)} - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # source_line - track current source location - defp do_execute([{:source_line, line, _file} | rest], regs, upvalues, proto, state) do - state = %{state | current_line: line} - do_execute(rest, regs, upvalues, proto, state) + # ── source_line — Target A: update line param only, no State struct copy ─── + + defp do_execute([{:source_line, new_line, _file} | rest], regs, upvalues, proto, state, cont, frames, _line) do + do_execute(rest, regs, upvalues, proto, state, cont, frames, new_line) end - # move - defp do_execute([{:move, dest, source} | rest], regs, upvalues, proto, state) do + # ── move ─────────────────────────────────────────────────────────────────── + + defp do_execute([{:move, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line) do value = elem(regs, source) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # 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 - - 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} + # ── test — push rest as continuation, tail-call body ────────────────────── - {_results, regs, state} -> - do_execute(rest, regs, upvalues, proto, state) - end + defp do_execute([{:test, reg, then_body, else_body} | rest], regs, upvalues, proto, state, cont, frames, line) do + body = if Value.truthy?(elem(regs, reg)), do: then_body, else: else_body + do_execute(body, regs, upvalues, proto, state, [rest | cont], frames, line) end - # test_and - short-circuit AND - defp do_execute([{:test_and, dest, source, rest_body} | rest], regs, upvalues, proto, state) do + # ── test_and — short-circuit AND, push rest as continuation ─────────────── + + defp do_execute([{:test_and, dest, source, rest_body} | rest], regs, upvalues, proto, state, cont, frames, line) do value = elem(regs, source) if Value.truthy?(value) do - # Value is truthy, execute rest_body to compute final result - do_execute(rest_body ++ rest, regs, upvalues, proto, state) + do_execute(rest_body, regs, upvalues, proto, state, [rest | cont], frames, line) else - # Value is falsy, store it in dest and continue regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end end - # test_or - short-circuit OR - defp do_execute([{:test_or, dest, source, rest_body} | rest], regs, upvalues, proto, state) do + # ── test_or — short-circuit OR, push rest as continuation ───────────────── + + defp do_execute([{:test_or, dest, source, rest_body} | rest], regs, upvalues, proto, state, cont, frames, line) do value = elem(regs, source) if Value.truthy?(value) do - # Value is truthy, store it in dest and continue regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) else - # Value is falsy, execute rest_body to compute final result - do_execute(rest_body ++ rest, regs, upvalues, proto, state) + do_execute(rest_body, regs, upvalues, proto, state, [rest | cont], frames, line) end end - # while_loop - defp do_execute([{:while_loop, cond_body, test_reg, loop_body} | rest], regs, upvalues, proto, state) do - # Execute condition - {_results, regs, state} = do_execute(cond_body, regs, upvalues, proto, state) - - # Check condition - if Value.truthy?(elem(regs, test_reg)) do - # Execute body - 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) - end - end + # ── while_loop — CPS: condition → check → body → restart ───────────────── - # repeat_loop - defp do_execute([{:repeat_loop, loop_body, cond_body, test_reg} | rest], regs, upvalues, proto, state) do - # Execute body - case do_execute(loop_body, regs, upvalues, proto, state) do - {:break, regs, state} -> - # Break exits the loop - do_execute(rest, regs, upvalues, proto, state) + defp do_execute( + [{:while_loop, cond_body, test_reg, loop_body} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line + ) do + loop_exit_cont = [{:loop_exit, rest} | cont] + cond_check = {:cps_while_test, test_reg, loop_body, cond_body, rest, cont} + do_execute(cond_body, regs, upvalues, proto, state, [cond_check | loop_exit_cont], frames, line) + end - {_results, regs, state} -> - # Execute condition - {_results, regs, state} = do_execute(cond_body, regs, upvalues, proto, state) + # ── repeat_loop — CPS: body → condition → check → restart ──────────────── - # 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 + defp do_execute( + [{:repeat_loop, loop_body, cond_body, test_reg} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line + ) do + loop_exit_cont = [{:loop_exit, rest} | cont] + body_done = {:cps_repeat_body, loop_body, cond_body, test_reg, rest, cont} + do_execute(loop_body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line) end - # numeric_for - defp do_execute([{:numeric_for, base, loop_var, body} | rest], regs, upvalues, proto, state) do - # Get internal loop state + # ── numeric_for — CPS ───────────────────────────────────────────────────── + + defp do_execute([{:numeric_for, base, loop_var, body} | rest], regs, upvalues, proto, state, cont, frames, line) do counter = elem(regs, base) limit = elem(regs, base + 1) step = elem(regs, base + 2) - # Check if we should enter/continue the loop should_continue = if step > 0 do counter <= limit @@ -324,64 +408,43 @@ defmodule Lua.VM.Executor do end if should_continue do - # Copy counter to loop variable regs = put_elem(regs, loop_var, counter) - # Clear open upvalue cells for loop-local registers (loop var + body locals) - # so each iteration gets fresh upvalue cells for its own variables state = %{ state | open_upvalues: Map.reject(state.open_upvalues, fn {reg, _} -> reg >= loop_var end) } - # Execute body - 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 + loop_exit_cont = [{:loop_exit, rest} | cont] + body_done = {:cps_numeric_for, base, loop_var, body, rest, cont} + do_execute(body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line) else - # Loop finished - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end end - # generic_for - generic for loop (for k, v in iterator do ... end) - defp do_execute([{:generic_for, base, var_regs, body} | rest], regs, upvalues, proto, state) do - # Read iterator function, invariant state, control from internal registers + # ── generic_for — CPS ───────────────────────────────────────────────────── + + defp do_execute([{:generic_for, base, var_regs, body} | rest], regs, upvalues, proto, state, cont, frames, line) do iter_func = elem(regs, base) invariant_state = elem(regs, base + 1) control = elem(regs, base + 2) - # Call iterator: f(state, control) - {results, state} = call_value(iter_func, [invariant_state, control], proto, state) - - # If first result is nil, exit loop + {results, state} = call_value(iter_func, [invariant_state, control], proto, state, line) first_result = List.first(results) if first_result == nil do - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) else - # Update control variable regs = put_elem(regs, base + 2, first_result) - # Copy results to loop variable registers regs = var_regs |> Enum.with_index() - |> Enum.reduce(regs, fn {var_reg, i}, regs -> - put_elem(regs, var_reg, Enum.at(results, i)) + |> Enum.reduce(regs, fn {var_reg, i}, r -> + put_elem(r, var_reg, Enum.at(results, i)) end) - # Clear open upvalue cells for loop-local registers first_var_reg = List.first(var_regs) state = %{ @@ -389,37 +452,22 @@ defmodule Lua.VM.Executor do | open_upvalues: Map.reject(state.open_upvalues, fn {reg, _} -> reg >= first_var_reg end) } - # Execute body - 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} -> - # Loop again - do_execute( - [{:generic_for, base, var_regs, body} | rest], - regs, - upvalues, - proto, - state - ) - end + loop_exit_cont = [{:loop_exit, rest} | cont] + body_done = {:cps_generic_for, base, var_regs, body, rest, cont} + do_execute(body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line) end end - # closure - create a closure value from a prototype, capturing upvalues - defp do_execute([{:closure, dest, proto_index} | rest], regs, upvalues, proto, state) do + # ── closure ──────────────────────────────────────────────────────────────── + + defp do_execute([{:closure, dest, proto_index} | rest], regs, upvalues, proto, state, cont, frames, line) do nested_proto = Enum.at(proto.prototypes, proto_index) - # Capture upvalues based on descriptors, reusing open upvalue cells when available. - # Accumulate in reverse (prepend) for O(N) collection, then reverse at the end. {captured_upvalues_reversed, state} = Enum.reduce(nested_proto.upvalue_descriptors, {[], state}, fn {:parent_local, reg, _name}, {cells, state} -> case Map.get(state.open_upvalues, reg) do nil -> - # Create a new cell for this local variable cell_ref = make_ref() value = elem(regs, reg) @@ -432,255 +480,210 @@ defmodule Lua.VM.Executor do {[cell_ref | cells], state} existing_cell -> - # Reuse existing open upvalue cell {[existing_cell | cells], state} end {:parent_upvalue, index, _name}, {cells, state} -> - # Share the parent's upvalue cell {[elem(upvalues, index) | cells], state} end) captured_upvalues = Enum.reverse(captured_upvalues_reversed) closure = {:lua_closure, nested_proto, List.to_tuple(captured_upvalues)} regs = put_elem(regs, dest, closure) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # call - invoke a function value - defp do_execute([{:call, base, arg_count, result_count} | rest], regs, upvalues, proto, state) do + # ── call — Lua closures via CPS frames; native functions inline ──────────── + + defp do_execute([{:call, base, arg_count, result_count} | rest], regs, upvalues, proto, state, cont, frames, line) do func_value = elem(regs, base) - # Collect arguments from registers base+1..base+arg_count - # arg_count < 0 encodes fixed args + varargs: - # -1 means 0 fixed + varargs, -2 means 1 fixed + varargs, etc. - # arg_count = {:multi, fixed} encodes fixed args + multi-return expansion args = case arg_count do {:multi, fixed_count} -> - # Fixed args + results from a multi-return call multi_count = state.multi_return_count total = fixed_count + multi_count - - if total > 0 do - for i <- 1..total, do: elem(regs, base + i) - else - [] - end + if total > 0, do: for(i <- 1..total, do: elem(regs, base + i)), else: [] n when is_integer(n) and n > 0 -> for i <- 1..n, do: elem(regs, base + i) n when is_integer(n) and n < 0 -> - # Collect fixed args + all varargs - # Decode: -1 => 0 fixed, -2 => 1 fixed, -3 => 2 fixed, etc. fixed_arg_count = -(n + 1) total_args = fixed_arg_count + state.multi_return_count - - if total_args > 0 do - for i <- 1..total_args, do: elem(regs, base + i) - else - [] - end + if total_args > 0, do: for(i <- 1..total_args, do: elem(regs, base + i)), else: [] 0 -> [] end - {results, state} = - case func_value do - {:lua_closure, callee_proto, callee_upvalues} -> - # Push call stack frame - frame = %{ - source: proto.source, - line: Map.get(state, :current_line, 0), - name: nil - } - - state = %{state | call_stack: [frame | state.call_stack]} - - # Create new register file for the callee - callee_regs = - Tuple.duplicate(nil, max(callee_proto.max_registers, callee_proto.param_count) + 16) - - # Copy arguments into callee registers (params are R[0..N-1]) - callee_regs = - args - |> Enum.with_index() - |> Enum.reduce(callee_regs, fn {arg, i}, regs -> - if i < callee_proto.param_count, do: put_elem(regs, i, arg), else: regs - end) - - # Populate varargs if function is vararg - callee_proto = - if callee_proto.is_vararg do - %{callee_proto | varargs: Enum.drop(args, callee_proto.param_count)} - else - callee_proto - end - - # Execute the callee with fresh open_upvalues - saved_open_upvalues = state.open_upvalues - state = %{state | open_upvalues: %{}} - - {results, _callee_regs, state} = - do_execute( - callee_proto.instructions, - callee_regs, - callee_upvalues, - callee_proto, - state - ) - - # Pop call stack frame, restore open_upvalues - state = %{state | call_stack: tl(state.call_stack), open_upvalues: saved_open_upvalues} + case func_value do + {:lua_closure, callee_proto, callee_upvalues} -> + callee_regs = + Tuple.duplicate(nil, max(callee_proto.max_registers, callee_proto.param_count) + 16) - {results, state} + callee_regs = + args + |> Enum.with_index() + |> Enum.reduce(callee_regs, fn {arg, i}, r -> + if i < callee_proto.param_count, do: put_elem(r, i, arg), else: r + end) - {:native_func, fun} -> + callee_proto = + if callee_proto.is_vararg, + do: %{callee_proto | varargs: Enum.drop(args, callee_proto.param_count)}, + else: callee_proto + + frame = %{ + rest: rest, + cont: cont, + regs: regs, + upvalues: upvalues, + proto: proto, + base: base, + result_count: result_count, + open_upvalues: state.open_upvalues + } + + call_info = %{source: proto.source, line: line, name: nil} + + state = %{state | call_stack: [call_info | state.call_stack], open_upvalues: %{}} + + # Tail call — Erlang stack does not grow + do_execute( + callee_proto.instructions, + callee_regs, + callee_upvalues, + callee_proto, + state, + [], + [frame | frames], + line + ) + + {:native_func, fun} -> + {results, state} = case fun.(args, state) do - {results, %State{} = new_state} when is_list(results) -> - {results, new_state} + {r, %State{} = s} when is_list(r) -> + {r, s} - {results, %State{} = new_state} -> - {List.wrap(results), new_state} + {r, %State{} = s} -> + {List.wrap(r), s} other -> raise InternalError, value: "native function returned invalid result: #{inspect(other)}, expected {results, state}" end - nil -> - raise TypeError, - value: "attempt to call a nil value", - source: proto.source, - call_stack: state.call_stack, - line: Map.get(state, :current_line), - error_kind: :call_nil, - value_type: nil - - other -> - # Check for __call metamethod - case get_metatable(other, state) do - nil -> - raise TypeError, - value: "attempt to call a #{Value.type_name(other)} value", - source: proto.source, - call_stack: state.call_stack, - line: Map.get(state, :current_line), - error_kind: :call_non_function, - value_type: value_type(other) - - {:tref, mt_id} -> - mt = Map.fetch!(state.tables, mt_id) - - case Map.get(mt.data, "__call") do - nil -> - raise TypeError, - value: "attempt to call a #{Value.type_name(other)} value", - source: proto.source, - call_stack: state.call_stack, - line: Map.get(state, :current_line), - error_kind: :call_non_function, - value_type: value_type(other) - - call_mm -> - call_function(call_mm, [other | args], state) - end - end - end - - cond do - # result_count == -1 means "return all results" (used in return f() position) - result_count == -1 -> - {results, regs, state} - - # result_count == -2 means "multi-return expansion": place all results into - # registers starting at base, store count in state, continue execution - result_count == -2 -> - results_list = List.wrap(results) - - regs = - results_list - |> Enum.with_index() - |> Enum.reduce(regs, fn {val, i}, regs -> - put_elem(regs, base + i, val) - end) + continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, base, result_count) - state = %{state | multi_return_count: length(results_list)} - do_execute(rest, regs, upvalues, proto, state) + nil -> + raise TypeError, + value: "attempt to call a nil value", + source: proto.source, + call_stack: state.call_stack, + line: line, + error_kind: :call_nil, + value_type: nil - true -> - # Place results into caller registers starting at base - regs = - if result_count > 0 do - results_list = List.wrap(results) + other -> + case get_metatable(other, state) do + nil -> + raise TypeError, + value: "attempt to call a #{Value.type_name(other)} value", + source: proto.source, + call_stack: state.call_stack, + line: line, + error_kind: :call_non_function, + value_type: value_type(other) - Enum.reduce(0..(result_count - 1), regs, fn i, regs -> - value = Enum.at(results_list, i) - put_elem(regs, base + i, value) - end) - else - regs - end + {:tref, mt_id} -> + mt = Map.fetch!(state.tables, mt_id) - do_execute(rest, regs, upvalues, proto, state) + case Map.get(mt.data, "__call") do + nil -> + raise TypeError, + value: "attempt to call a #{Value.type_name(other)} value", + source: proto.source, + call_stack: state.call_stack, + line: line, + error_kind: :call_non_function, + value_type: value_type(other) + + call_mm -> + {results, state} = call_function(call_mm, [other | args], state) + continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, base, result_count) + end + end end end - # vararg - load vararg values into registers - # count == 0 means load all varargs, count > 0 means load exactly count values - defp do_execute([{:vararg, base, count} | rest], regs, upvalues, proto, state) do + # ── vararg ───────────────────────────────────────────────────────────────── + + defp do_execute([{:vararg, base, count} | rest], regs, upvalues, proto, state, cont, frames, line) do varargs = Map.get(proto, :varargs, []) {regs, state} = if count == 0 do - # Load all varargs and track the count for set_list/call regs = - Enum.reduce(Enum.with_index(varargs), regs, fn {val, i}, regs -> - put_elem(regs, base + i, val) + Enum.reduce(Enum.with_index(varargs), regs, fn {val, i}, r -> + put_elem(r, base + i, val) end) {regs, %{state | multi_return_count: length(varargs)}} else - # Load exactly count values regs = - Enum.reduce(0..(count - 1), regs, fn i, regs -> - put_elem(regs, base + i, Enum.at(varargs, i)) + Enum.reduce(0..(count - 1), regs, fn i, r -> + put_elem(r, base + i, Enum.at(varargs, i)) end) {regs, state} end - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # return_vararg - return all varargs - defp do_execute([{:return_vararg} | _rest], regs, _upvalues, proto, state) do + # ── return_vararg ────────────────────────────────────────────────────────── + + defp do_execute([{:return_vararg} | _rest], regs, _upvalues, proto, state, _cont, frames, line) do varargs = Map.get(proto, :varargs, []) - {varargs, regs, state} + + case frames do + [] -> {varargs, regs, state} + [frame | rest_frames] -> do_frame_return(varargs, regs, state, frame, rest_frames, line) + end end - # return - # count == -1 means return from base including all varargs - # count == 0 means return nil - # count > 0 means return exactly count values - # count == {:multi_return, fixed} means return fixed values + multi-return expanded values - defp do_execute([{:return, base, {:multi_return, fixed_count}} | _rest], regs, _upvalues, _proto, state) do + # ── return (multi_return variant) ────────────────────────────────────────── + + defp do_execute( + [{:return, base, {:multi_return, fixed_count}} | _rest], + regs, + _upvalues, + _proto, + state, + _cont, + frames, + line + ) do total = fixed_count + state.multi_return_count results = if total > 0, do: for(i <- 0..(total - 1), do: elem(regs, base + i)), else: [] - {results, regs, state} + + case frames do + [] -> {results, regs, state} + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line) + end end - defp do_execute([{:return, base, count} | _rest], regs, _upvalues, _proto, state) do + # ── return ───────────────────────────────────────────────────────────────── + + defp do_execute([{:return, base, count} | _rest], regs, _upvalues, _proto, state, _cont, frames, line) do results = cond do count == 0 -> [nil] count < 0 -> - # Negative count encodes fixed values + variable values (vararg or multi-return) - # -(init_count + 1): e.g. -1 = 0 fixed, -2 = 1 fixed, -3 = 2 fixed init_count = -(count + 1) total = init_count + state.multi_return_count @@ -694,11 +697,15 @@ defmodule Lua.VM.Executor do for i <- 0..(count - 1), do: elem(regs, base + i) end - {results, regs, state} + case frames do + [] -> {results, regs, state} + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line) + end end - # Arithmetic operations - defp do_execute([{:add, dest, a, b} | rest], regs, upvalues, proto, state) do + # ── Arithmetic operations ────────────────────────────────────────────────── + + defp do_execute([{:add, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -706,10 +713,10 @@ defmodule Lua.VM.Executor do try_binary_metamethod("__add", val_a, val_b, state, fn -> safe_add(val_a, val_b) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:subtract, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:subtract, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -717,10 +724,10 @@ defmodule Lua.VM.Executor do try_binary_metamethod("__sub", val_a, val_b, state, fn -> safe_subtract(val_a, val_b) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:multiply, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:multiply, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -728,10 +735,10 @@ defmodule Lua.VM.Executor do try_binary_metamethod("__mul", val_a, val_b, state, fn -> safe_multiply(val_a, val_b) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:divide, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:divide, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -739,10 +746,10 @@ defmodule Lua.VM.Executor do try_binary_metamethod("__div", val_a, val_b, state, fn -> safe_divide(val_a, val_b) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:floor_divide, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:floor_divide, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -752,10 +759,10 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:modulo, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:modulo, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -763,10 +770,10 @@ defmodule Lua.VM.Executor do try_binary_metamethod("__mod", val_a, val_b, state, fn -> safe_modulo(val_a, val_b) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:power, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:power, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -774,11 +781,12 @@ defmodule Lua.VM.Executor do try_binary_metamethod("__pow", val_a, val_b, state, fn -> safe_power(val_a, val_b) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - # String concatenation - defp do_execute([{:concatenate, dest, a, b} | rest], regs, upvalues, proto, state) do + # ── String concatenation ─────────────────────────────────────────────────── + + defp do_execute([{:concatenate, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do left = elem(regs, a) right = elem(regs, b) @@ -788,11 +796,12 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - # Bitwise operations - defp do_execute([{:bitwise_and, dest, a, b} | rest], regs, upvalues, proto, state) do + # ── Bitwise operations ───────────────────────────────────────────────────── + + defp do_execute([{:bitwise_and, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -802,10 +811,10 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:bitwise_or, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:bitwise_or, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -815,10 +824,10 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:bitwise_xor, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:bitwise_xor, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -828,10 +837,10 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:shift_left, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:shift_left, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -841,10 +850,10 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:shift_right, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:shift_right, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -854,10 +863,10 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:bitwise_not, dest, source} | rest], regs, upvalues, proto, state) do + defp do_execute([{:bitwise_not, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line) do val = elem(regs, source) {result, new_state} = @@ -866,11 +875,12 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - # Comparison operations - defp do_execute([{:equal, dest, a, b} | rest], regs, upvalues, proto, state) do + # ── Comparison operations ────────────────────────────────────────────────── + + defp do_execute([{:equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -878,10 +888,10 @@ defmodule Lua.VM.Executor do try_equality_metamethod(val_a, val_b, state, fn -> val_a == val_b end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:less_than, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:less_than, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -889,10 +899,10 @@ defmodule Lua.VM.Executor do try_binary_metamethod("__lt", val_a, val_b, state, fn -> safe_compare_lt(val_a, val_b) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:less_equal, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:less_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -900,42 +910,43 @@ defmodule Lua.VM.Executor do try_binary_metamethod("__le", val_a, val_b, state, fn -> safe_compare_le(val_a, val_b) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:greater_than, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:greater_than, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do result = safe_compare_gt(elem(regs, a), elem(regs, b)) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - defp do_execute([{:greater_equal, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:greater_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do result = safe_compare_ge(elem(regs, a), elem(regs, b)) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - defp do_execute([{:not_equal, dest, a, b} | rest], regs, upvalues, proto, state) do + defp do_execute([{:not_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do result = elem(regs, a) != elem(regs, b) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # Unary operations - defp do_execute([{:negate, dest, source} | rest], regs, upvalues, proto, state) do + # ── Unary operations ─────────────────────────────────────────────────────── + + defp do_execute([{:negate, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line) do val = elem(regs, source) {result, new_state} = try_unary_metamethod("__unm", val, state, fn -> safe_negate(val) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - defp do_execute([{:not, dest, source} | rest], regs, upvalues, proto, state) do + defp do_execute([{:not, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line) do result = not Value.truthy?(elem(regs, source)) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - defp do_execute([{:length, dest, source} | rest], regs, upvalues, proto, state) do + defp do_execute([{:length, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line) do value = elem(regs, source) {result, new_state} = @@ -957,58 +968,73 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) end - # new_table - defp do_execute([{:new_table, dest, _array_hint, _hash_hint} | rest], regs, upvalues, proto, state) do + # ── new_table ────────────────────────────────────────────────────────────── + + defp do_execute([{:new_table, dest, _array_hint, _hash_hint} | rest], regs, upvalues, proto, state, cont, frames, line) do {tref, state} = State.alloc_table(state) regs = put_elem(regs, dest, tref) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # get_table — R[dest] = table[R[key_reg]] - defp do_execute([{:get_table, dest, table_reg, key_reg} | rest], regs, upvalues, proto, state) do + # ── get_table ────────────────────────────────────────────────────────────── + + defp do_execute([{:get_table, dest, table_reg, key_reg} | rest], regs, upvalues, proto, state, cont, frames, line) do table_val = elem(regs, table_reg) key = elem(regs, key_reg) {value, state} = index_value(table_val, key, state) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # set_table — table[R[key_reg]] = R[value_reg] - defp do_execute([{:set_table, table_reg, key_reg, value_reg} | rest], regs, upvalues, proto, state) do + # ── set_table ────────────────────────────────────────────────────────────── + + defp do_execute([{:set_table, table_reg, key_reg, value_reg} | rest], regs, upvalues, proto, state, cont, frames, line) do {:tref, _} = elem(regs, table_reg) key = elem(regs, key_reg) value = elem(regs, value_reg) state = table_newindex(elem(regs, table_reg), key, value, state) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # get_field — R[dest] = table[name] (string key literal) - defp do_execute([{:get_field, dest, table_reg, name} | rest], regs, upvalues, proto, state) do + # ── get_field ────────────────────────────────────────────────────────────── + + defp do_execute([{:get_field, dest, table_reg, name} | rest], regs, upvalues, proto, state, cont, frames, line) do table_val = elem(regs, table_reg) {value, state} = index_value(table_val, name, state) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # set_field — table[name] = R[value_reg] - defp do_execute([{:set_field, table_reg, name, value_reg} | rest], regs, upvalues, proto, state) do + # ── set_field ────────────────────────────────────────────────────────────── + + defp do_execute([{:set_field, table_reg, name, value_reg} | rest], regs, upvalues, proto, state, cont, frames, line) do {:tref, _} = elem(regs, table_reg) value = elem(regs, value_reg) state = table_newindex(elem(regs, table_reg), name, value, state) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # set_list with {:multi, init_count} — multi-return expansion in table constructor - defp do_execute([{:set_list, table_reg, start, {:multi, init_count}, offset} | rest], regs, upvalues, proto, state) do + # ── set_list (multi-return variant) ─────────────────────────────────────── + + defp do_execute( + [{:set_list, table_reg, start, {:multi, init_count}, offset} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line + ) do {:tref, id} = elem(regs, table_reg) total = init_count + state.multi_return_count @@ -1027,20 +1053,18 @@ defmodule Lua.VM.Executor do %{table | data: new_data} end) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # set_list — bulk store: table[offset+i] = R[start+i-1] for i in 1..count - # count == 0 means variable number of values (from vararg or multi-return) - defp do_execute([{:set_list, table_reg, start, count, offset} | rest], regs, upvalues, proto, state) do + # ── set_list ─────────────────────────────────────────────────────────────── + + defp do_execute([{:set_list, table_reg, start, count, offset} | rest], regs, upvalues, proto, state, cont, frames, line) do {:tref, id} = elem(regs, table_reg) state = State.update_table(state, {:tref, id}, fn table -> new_data = if count == 0 do - # Variable number of values - use multi_return_count which is set by - # both vararg (count=0) and call (result_count=-2) instructions values_to_collect = state.multi_return_count if values_to_collect > 0 do @@ -1052,7 +1076,6 @@ defmodule Lua.VM.Executor do table.data end else - # Fixed number of values Enum.reduce(1..count, table.data, fn i, data -> value = elem(regs, start + i - 1) Map.put(data, offset + i, value) @@ -1062,25 +1085,127 @@ defmodule Lua.VM.Executor do %{table | data: new_data} end) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # self — R[base+1] = R[obj_reg], R[base] = R[obj_reg]["method"] - defp do_execute([{:self, base, obj_reg, method_name} | rest], regs, upvalues, proto, state) do + # ── self ─────────────────────────────────────────────────────────────────── + + defp do_execute([{:self, base, obj_reg, method_name} | rest], regs, upvalues, proto, state, cont, frames, line) do obj = elem(regs, obj_reg) {func, state} = index_value(obj, method_name, state) regs = put_elem(regs, base + 1, obj) regs = put_elem(regs, base, func) - do_execute(rest, regs, upvalues, proto, state) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) end - # Catch-all for unimplemented instructions - defp do_execute([instr | _rest], _regs, _upvalues, _proto, _state) do + # ── Catch-all for unimplemented instructions ─────────────────────────────── + + defp do_execute([instr | _rest], _regs, _upvalues, _proto, _state, _cont, _frames, _line) do raise InternalError, value: "unimplemented instruction: #{inspect(instr)}" end - # Find a label in the instruction list (scanning forward and into nested blocks) + # ── do_frame_return — restore caller context after a Lua function returns ── + + defp do_frame_return(results, _callee_regs, state, frame, rest_frames, line) do + %{ + rest: rest, + cont: caller_cont, + regs: caller_regs, + upvalues: caller_upvalues, + proto: caller_proto, + base: base, + result_count: result_count, + open_upvalues: saved_open_upvalues + } = frame + + state = %{state | call_stack: tl(state.call_stack), open_upvalues: saved_open_upvalues} + + case result_count do + -1 -> + # Return-position call (return f()): pass results to the caller's caller + case rest_frames do + [] -> + {results, caller_regs, state} + + [outer_frame | outer_rest_frames] -> + do_frame_return(results, caller_regs, state, outer_frame, outer_rest_frames, line) + end + + -2 -> + # Multi-return expansion: place all results into caller regs from base + results_list = List.wrap(results) + + caller_regs = + results_list + |> Enum.with_index() + |> Enum.reduce(caller_regs, fn {val, i}, r -> put_elem(r, base + i, val) end) + + state = %{state | multi_return_count: length(results_list)} + do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line) + + 0 -> + # No results captured + do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line) + + n when n > 0 -> + # Fixed count: place first n results into caller regs from base + results_list = List.wrap(results) + + caller_regs = + Enum.reduce(0..(n - 1), caller_regs, fn i, r -> + put_elem(r, base + i, Enum.at(results_list, i)) + end) + + do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line) + end + end + + # ── continue_after_call — place results for native/metamethod calls ───────── + + defp continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, base, result_count) do + case result_count do + -1 -> + # Results from this native call become the return from the current function + case frames do + [] -> {results, regs, state} + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line) + end + + -2 -> + results_list = List.wrap(results) + + regs = + results_list + |> Enum.with_index() + |> Enum.reduce(regs, fn {val, i}, r -> put_elem(r, base + i, val) end) + + state = %{state | multi_return_count: length(results_list)} + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + + 0 -> + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + + n when n > 0 -> + results_list = List.wrap(results) + + regs = + Enum.reduce(0..(n - 1), regs, fn i, r -> + put_elem(r, base + i, Enum.at(results_list, i)) + end) + + do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + end + end + + # ── find_loop_exit — scan cont for the nearest {:loop_exit, _} marker ────── + + defp find_loop_exit([{:loop_exit, exit_is} | rest_cont]), do: {exit_is, rest_cont} + defp find_loop_exit([_ | rest_cont]), do: find_loop_exit(rest_cont) + defp find_loop_exit([]), do: raise(InternalError, value: "break outside loop") + + # ── find_label — scan instruction list for a :label marker ──────────────── + defp find_label([], _label), do: :not_found defp find_label([{:label, name} | rest], label) when name == label do @@ -1088,7 +1213,6 @@ defmodule Lua.VM.Executor do 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 @@ -1103,8 +1227,9 @@ defmodule Lua.VM.Executor do 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 + # ── call_value — invoke a function for generic_for iterator ─────────────── + + defp call_value({:lua_closure, callee_proto, callee_upvalues}, args, _proto, state, _line) do callee_regs = Tuple.duplicate(nil, max(callee_proto.max_registers, callee_proto.param_count) + 16) @@ -1126,19 +1251,13 @@ defmodule Lua.VM.Executor do state = %{state | open_upvalues: %{}} {results, _callee_regs, state} = - do_execute( - callee_proto.instructions, - callee_regs, - callee_upvalues, - callee_proto, - state - ) + do_execute(callee_proto.instructions, callee_regs, callee_upvalues, callee_proto, state, [], [], 0) state = %{state | open_upvalues: saved_open_upvalues} {results, state} end - defp call_value({:native_func, fun}, args, _proto, state) do + defp call_value({:native_func, fun}, args, _proto, state, _line) do case fun.(args, state) do {results, %State{} = new_state} when is_list(results) -> {results, new_state} @@ -1148,25 +1267,24 @@ defmodule Lua.VM.Executor do end end - defp call_value(nil, _args, proto, state) do + defp call_value(nil, _args, proto, state, line) do raise TypeError, value: "attempt to call a nil value", source: proto.source, call_stack: state.call_stack, - line: Map.get(state, :current_line), + line: line, error_kind: :call_nil, value_type: nil end - defp call_value(other, args, proto, state) do - # Check for __call metamethod + defp call_value(other, args, proto, state, line) do case get_metatable(other, state) do nil -> raise TypeError, value: "attempt to call a #{Value.type_name(other)} value", source: proto.source, call_stack: state.call_stack, - line: Map.get(state, :current_line), + line: line, error_kind: :call_non_function, value_type: value_type(other) @@ -1179,17 +1297,18 @@ defmodule Lua.VM.Executor do value: "attempt to call a #{Value.type_name(other)} value", source: proto.source, call_stack: state.call_stack, - line: Map.get(state, :current_line), + line: line, error_kind: :call_non_function, value_type: value_type(other) call_mm -> - call_value(call_mm, [other | args], proto, state) + call_value(call_mm, [other | args], proto, state, line) end end end - # Coerce a value to a string for concatenation (Lua semantics: numbers become strings) + # ── Coerce a value to string for concatenation ───────────────────────────── + defp concat_coerce(value) when is_binary(value), do: value defp concat_coerce(value) when is_integer(value), do: Integer.to_string(value) defp concat_coerce(value) when is_float(value), do: Value.to_string(value) @@ -1201,9 +1320,8 @@ defmodule Lua.VM.Executor do value_type: value_type(value) end - # Metamethod support + # ── Metamethod support ───────────────────────────────────────────────────── - # Depth limit for metamethod chains (prevents infinite loops) @metamethod_chain_limit 200 defp get_metatable({:tref, id}, state) do @@ -1217,7 +1335,6 @@ defmodule Lua.VM.Executor do defp get_metatable(_value, _state), do: nil - # Index any value — dispatches to table_index or type metatable __index, or raises defp index_value({:tref, _} = tref, key, state) do table_index(tref, key, state) end @@ -1250,7 +1367,6 @@ defmodule Lua.VM.Executor do end end - # Resolve table[key] with __index metamethod chain support defp table_index({:tref, id}, key, state, depth \\ 0) do if depth >= @metamethod_chain_limit do raise RuntimeError, value: "'__index' chain too long; possible loop" @@ -1260,7 +1376,6 @@ defmodule Lua.VM.Executor do case Map.get(table.data, key) do nil -> - # Key not found, check for __index metamethod case table.metatable do nil -> {nil, state} @@ -1273,11 +1388,9 @@ defmodule Lua.VM.Executor do {nil, state} {:tref, _} = index_table -> - # __index is a table, recursively look up in it table_index(index_table, key, state, depth + 1) func when is_tuple(func) -> - # __index is a function, call it with (table, key) {results, state} = call_function(func, [{:tref, id}, key], state) {List.first(results), state} end @@ -1288,7 +1401,6 @@ defmodule Lua.VM.Executor do end end - # Resolve table[key] = value with __newindex metamethod chain support defp table_newindex({:tref, id}, key, value, state, depth \\ 0) do if depth >= @metamethod_chain_limit do raise RuntimeError, value: "'__newindex' chain too long; possible loop" @@ -1297,15 +1409,12 @@ defmodule Lua.VM.Executor do table = Map.fetch!(state.tables, id) if Map.has_key?(table.data, key) do - # Key exists, update directly (rawset) State.update_table(state, {:tref, id}, fn t -> %{t | data: Map.put(t.data, key, value)} end) else - # Key doesn't exist, check for __newindex metamethod case table.metatable do nil -> - # No metatable, just set the value State.update_table(state, {:tref, id}, fn t -> %{t | data: Map.put(t.data, key, value)} end) @@ -1315,17 +1424,14 @@ defmodule Lua.VM.Executor do case Map.get(mt.data, "__newindex") do nil -> - # No __newindex, just set the value State.update_table(state, {:tref, id}, fn t -> %{t | data: Map.put(t.data, key, value)} end) {:tref, _} = newindex_table -> - # __newindex is a table, set in that table (with chaining) table_newindex(newindex_table, key, value, state, depth + 1) func when is_tuple(func) -> - # __newindex is a function, call it with (table, key, value) {_results, state} = call_function(func, [{:tref, id}, key, value], state) state end @@ -1334,7 +1440,6 @@ defmodule Lua.VM.Executor do end defp try_binary_metamethod(metamethod_name, a, b, state, default_fn) do - # Try a's metatable first mt_a = get_metatable(a, state) mt_b = get_metatable(b, state) @@ -1358,20 +1463,13 @@ defmodule Lua.VM.Executor do {result, new_state} {:lua_closure, callee_proto, callee_upvalues} -> - # Call the Lua closure metamethod args = [a, b] initial_regs = List.to_tuple(args ++ List.duplicate(nil, 248)) saved_open_upvalues = state.open_upvalues state = %{state | open_upvalues: %{}} {results, _final_regs, new_state} = - do_execute( - callee_proto.instructions, - initial_regs, - callee_upvalues, - callee_proto, - state - ) + do_execute(callee_proto.instructions, initial_regs, callee_upvalues, callee_proto, state, [], [], 0) new_state = %{new_state | open_upvalues: saved_open_upvalues} @@ -1410,20 +1508,13 @@ defmodule Lua.VM.Executor do {result, new_state} {:lua_closure, callee_proto, callee_upvalues} -> - # Call the Lua closure metamethod args = [a] initial_regs = List.to_tuple(args ++ List.duplicate(nil, 249)) saved_open_upvalues = state.open_upvalues state = %{state | open_upvalues: %{}} {results, _final_regs, new_state} = - do_execute( - callee_proto.instructions, - initial_regs, - callee_upvalues, - callee_proto, - state - ) + do_execute(callee_proto.instructions, initial_regs, callee_upvalues, callee_proto, state, [], [], 0) new_state = %{new_state | open_upvalues: saved_open_upvalues} @@ -1443,13 +1534,10 @@ defmodule Lua.VM.Executor do end end - # Special handling for __eq metamethod - # In Lua, __eq only triggers if both operands have the exact same __eq metamethod defp try_equality_metamethod(a, b, state, default_fn) do mt_a = get_metatable(a, state) mt_b = get_metatable(b, state) - # Get __eq from both metatables eq_a = case mt_a do nil -> nil @@ -1462,7 +1550,6 @@ defmodule Lua.VM.Executor do {:tref, mt_id} -> Map.get(Map.fetch!(state.tables, mt_id).data, "__eq") end - # Only use metamethod if both have the SAME __eq metamethod if not is_nil(eq_a) and eq_a == eq_b do case eq_a do {:native_func, func} -> @@ -1476,13 +1563,7 @@ defmodule Lua.VM.Executor do state = %{state | open_upvalues: %{}} {results, _final_regs, new_state} = - do_execute( - callee_proto.instructions, - initial_regs, - callee_upvalues, - callee_proto, - state - ) + do_execute(callee_proto.instructions, initial_regs, callee_upvalues, callee_proto, state, [], [], 0) new_state = %{new_state | open_upvalues: saved_open_upvalues} @@ -1498,12 +1579,12 @@ defmodule Lua.VM.Executor do {default_fn.(), state} end else - # No metamethod or different metamethods, use default comparison {default_fn.(), state} end end - # Type-safe arithmetic operations + # ── Type-safe arithmetic ─────────────────────────────────────────────────── + defp safe_add(a, b) do with {:ok, na} <- to_number(a), {:ok, nb} <- to_number(b) do @@ -1546,9 +1627,6 @@ defmodule Lua.VM.Executor do defp safe_divide(a, b) do with {:ok, na} <- to_number(a), {:ok, nb} <- to_number(b) do - # Check for division by zero - # Note: Standard Lua 5.3 returns inf/-inf/nan for float division by zero, - # but Elixir doesn't support creating these values easily, so we raise an error if nb == 0 or nb == 0.0 do raise RuntimeError, value: "attempt to divide by zero" else @@ -1571,11 +1649,9 @@ defmodule Lua.VM.Executor do raise RuntimeError, value: "attempt to divide by zero" is_integer(na) and is_integer(nb) -> - # Lua floor division for integers lua_idiv(na, nb) true -> - # Float floor division Float.floor(na / nb) * 1.0 end else @@ -1595,11 +1671,9 @@ defmodule Lua.VM.Executor do raise RuntimeError, value: "attempt to perform modulo by zero" is_integer(na) and is_integer(nb) -> - # Lua floor modulo for integers: a - floor_div(a, b) * b na - lua_idiv(na, nb) * nb true -> - # Float floor modulo: a - floor(a/b) * b na - Float.floor(na / nb) * nb end else @@ -1611,11 +1685,9 @@ defmodule Lua.VM.Executor do end end - # Lua-style integer floor division (rounds toward negative infinity) defp lua_idiv(a, b) do q = div(a, b) r = rem(a, b) - # Adjust if remainder has different sign than divisor if r != 0 and Bitwise.bxor(r, b) < 0, do: q - 1, else: q end @@ -1645,7 +1717,8 @@ defmodule Lua.VM.Executor do end end - # Type-safe comparison operations + # ── Type-safe comparison ─────────────────────────────────────────────────── + defp safe_compare_lt(a, b) do cond do is_number(a) and is_number(b) -> @@ -1710,7 +1783,8 @@ defmodule Lua.VM.Executor do end end - # Convert value to number (coerce strings like Lua does) + # ── Number coercion ──────────────────────────────────────────────────────── + defp to_number(v) when is_number(v), do: {:ok, v} defp to_number(v) when is_binary(v) do @@ -1722,7 +1796,6 @@ defmodule Lua.VM.Executor do defp to_number(v), do: {:error, v} - # Convert value to integer for bitwise operations (with string coercion) defp to_integer!(v) when is_integer(v), do: v defp to_integer!(v) when is_float(v), do: trunc(v) @@ -1746,7 +1819,8 @@ defmodule Lua.VM.Executor do value_type: value_type(v) end - # Lua 5.3 shift semantics: negative shift reverses direction, shift >= 64 yields 0 + # ── Lua 5.3 shift semantics ──────────────────────────────────────────────── + defp lua_shift_left(_val, shift) when shift >= 64, do: 0 defp lua_shift_left(_val, shift) when shift <= -64, do: 0 defp lua_shift_left(val, shift) when shift < 0, do: lua_shift_right(val, -shift) @@ -1760,12 +1834,12 @@ defmodule Lua.VM.Executor do defp lua_shift_right(val, shift) when shift < 0, do: lua_shift_left(val, -shift) defp lua_shift_right(val, shift) do - # Unsigned right shift - mask to 64-bit first unsigned_val = Bitwise.band(val, 0xFFFFFFFFFFFFFFFF) Bitwise.bsr(unsigned_val, shift) end - # Helper to determine Lua type from Elixir value + # ── Value type helper ────────────────────────────────────────────────────── + defp value_type(nil), do: nil defp value_type(v) when is_boolean(v), do: :boolean defp value_type(v) when is_number(v), do: :number diff --git a/lib/lua/vm/state.ex b/lib/lua/vm/state.ex index af657b9..396eefb 100644 --- a/lib/lua/vm/state.ex +++ b/lib/lua/vm/state.ex @@ -15,8 +15,6 @@ defmodule Lua.VM.State do userdata: %{}, userdata_next_id: 0, private: %{}, - current_line: 0, - current_source: nil, multi_return_count: 0 @type t :: %__MODULE__{ @@ -29,8 +27,6 @@ defmodule Lua.VM.State do userdata: %{optional(non_neg_integer()) => term()}, userdata_next_id: non_neg_integer(), private: map(), - current_line: non_neg_integer(), - current_source: binary() | nil, multi_return_count: non_neg_integer() } diff --git a/lib/lua/vm/stdlib.ex b/lib/lua/vm/stdlib.ex index 1df70d9..17b4fe3 100644 --- a/lib/lua/vm/stdlib.ex +++ b/lib/lua/vm/stdlib.ex @@ -180,7 +180,7 @@ defmodule Lua.VM.Stdlib do message = case rest do [msg | _] -> msg - [] -> "assertion failed! (line #{state.current_line})" + [] -> "assertion failed!" end raise AssertionError, value: message diff --git a/test/lua/vm/call_stack_test.exs b/test/lua/vm/call_stack_test.exs index f188ff5..a78b890 100644 --- a/test/lua/vm/call_stack_test.exs +++ b/test/lua/vm/call_stack_test.exs @@ -21,8 +21,8 @@ defmodule Lua.VM.CallStackTest do state = State.new() assert {:ok, _results, final_state} = VM.execute(proto, state) - # State should have current_line field - assert Map.has_key?(final_state, :current_line) + # Execution should succeed and return a valid state + assert is_struct(final_state, State) end test "pushes call stack frame on function call" do