From 376d50321ee597a87a7cf51abd8a3db1cabd3bab Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Mon, 1 Jun 2026 09:56:28 -0700 Subject: [PATCH 1/9] chore(B17): start plan --- .agents/plans/B17-vm-max-steps.md | 190 ++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 .agents/plans/B17-vm-max-steps.md diff --git a/.agents/plans/B17-vm-max-steps.md b/.agents/plans/B17-vm-max-steps.md new file mode 100644 index 00000000..44c731b1 --- /dev/null +++ b/.agents/plans/B17-vm-max-steps.md @@ -0,0 +1,190 @@ +--- +id: B17 +title: "VM instruction budget: configurable :max_steps with catchable exhaustion" +issue: 306 +pr: null +branch: feat/vm-max-steps +base: main +status: in-progress +direction: B +unlocks: + - deterministic CPU bound for library consumers calling Lua.eval!/2 without a host Task + timeout wrapper + - closes the pure-CPU-exhaustion gap left open after #305 (allocation-bomb hardening) +--- + +## Goal + +Add a `:max_steps` option to `Lua.new/1` that bounds the number of VM +instructions a single evaluation may execute, mirroring the existing +`:max_call_depth`: + +- Default `:infinity` — no limit, existing behavior byte-for-byte + unchanged, and the default path stays free of new per-instruction + cost. +- A positive integer caps total instructions executed. On exhaustion the + VM raises a **catchable** Lua runtime error (message + `"instruction budget exceeded"`) so `pcall` can recover, just like the + `"stack overflow"` raised by `:max_call_depth`. + +The bound must apply to **both** execution paths: the interpreter +(`do_execute/8` in `lib/lua/vm/executor.ex`) and the compiled dispatcher +(`dispatch/8` in `lib/lua/vm/dispatcher.ex`). A runaway script such as +`while true do end` or a tight numeric loop must terminate +deterministically inside the VM rather than relying on a host +wall-clock timeout. + +## Out of scope + +- **`:max_alloc_bytes`** — the companion deterministic memory bound that + tallies bytes at allocating opcodes (concat, table grow). The issue + explicitly defers it ("Could land in a follow-up"). Do NOT implement it + here. If touching the allocating opcodes tempts a "while I'm here" + change, log it under `## Discoveries` and stop. +- **Per-instruction counting on every opcode.** The budget is enforced at + loop back-edges and call boundaries only (see Implementation notes). + Straight-line code is bounded transitively because every unbounded + growth path is a loop or recursion; counting every opcode would tax the + default `:infinity` path, which the issue forbids. +- **Tail-call optimization** or any change to how frames are pushed. +- **Wall-clock timeouts** or `max_heap_size` — those are host concerns, + already documented in the sandboxing guide. +- **Resetting / inspecting the remaining budget from Lua or the public + API.** The budget is configured once at `Lua.new/1` and spans one + top-level evaluation. No mid-run introspection. + +## Success criteria + +- [ ] `mix format` produces no diff. +- [ ] `mix compile --warnings-as-errors` passes. +- [ ] `:max_steps` is accepted by `Lua.new/1`, validated exactly like + `:max_call_depth` (positive integer or `:infinity`; anything else + raises `ArgumentError` with a clear message naming `:max_steps`). +- [ ] Default is `:infinity` and existing tests are unchanged (same + `mix test` pass/fail counts as `main` before the change). +- [ ] A finite `:max_steps` aborts a non-terminating script + (`while true do end`) with a Lua runtime error whose message + contains `"instruction budget exceeded"`. +- [ ] The exhaustion error is **catchable via `pcall`**: a new test + asserts `pcall` returns `{false, message}` with the message and that + the VM stays usable afterward. +- [ ] A program that finishes under the budget runs normally and returns + its result; the budget does not leak across evaluations (a second + `Lua.eval!/2` on the same `Lua.new(max_steps: N)` state gets a fresh + budget). +- [ ] Both the interpreter and the compiled dispatcher enforce the budget + (test exercises both paths — see Implementation notes for how to + force the compiled path). +- [ ] The counter is threaded as a function parameter, NOT stored in + `%State{}`, preserving the executor's deliberate + `line`-off-`State` discipline. `max_steps` (the configured ceiling) + lives in `%State{}` like `max_call_depth`; the running tally does + not. +- [ ] `mix test --only lua53` shows no regression in suite pass count + vs `main` before the change. +- [ ] Benchmarked: `mix run benchmarks/fibonacci.exs` and + `mix run benchmarks/dispatcher_vs_interpreter.exs` (default + `LUA_BENCH_MODE=quick`) on `main` vs this branch with `:max_steps` + left at its `:infinity` default show no meaningful regression. + Numbers recorded in the PR body. +- [ ] Docs: the sandboxing guide's "Call depth" / resource-limits section + is extended to cover `:max_steps` (see Implementation notes for the + file-path resolution). +- [ ] No source or test file references the plan id `B17` (repo rule in + `CLAUDE.md`). The id lives only in the commit body and PR + description. + +## Implementation notes + +Mirror `:max_call_depth` everywhere it appears. + +### 1. Public API — `lib/lua.ex` + +- Add `max_steps: :infinity` to the `Keyword.validate!/2` defaults at + `new/1`. +- Fetch and validate it next to `max_call_depth`: + `max_steps = validate_max_steps!(Keyword.fetch!(opts, :max_steps))`. +- Add `validate_max_steps!/1` mirroring `validate_max_call_depth!/1`: + `:infinity` and `pos_integer` pass; anything else raises `ArgumentError` + with a message naming `:max_steps`. +- Thread it into the seeded state alongside `max_call_depth`. +- Add a `* :max_steps - ...` bullet to the `## Options` moduledoc with a + doctest mirroring the `:max_call_depth` doctest. + +### 2. State — `lib/lua/vm/state.ex` + +- Add `max_steps: :infinity` to `defstruct` and to the `@type t`. Do NOT + add a running-tally field — the tally is a threaded parameter, not + state. +- Add a guard helper `check_steps!/2` taking the state and the current + step count, ordered so the `:infinity` clause resolves first with no + struct rebuild, raising the same `Lua.VM.RuntimeError` used by + `"stack overflow"` so `pcall`/`xpcall` catch it for free. + +### 3. Interpreter — `lib/lua/vm/executor.ex` + +- Thread a `steps` counter as a new trailing parameter on `do_execute`, + turning `do_execute/8` into `do_execute/9`. Seed it at `0` at both + entry points (`execute/5` and `call_function/3`). Thread it through + `do_frame_return` so the tally spans frames within one interpreter + evaluation (non-tail recursion stacks frames in the same `do_execute` + chain, so the recursion bound is global to the evaluation). +- Increment + check only at loop back-edges (the `:cps_while_body`, + `:cps_repeat_cond` repeat branch, `:cps_numeric_for` continue, and + `:cps_generic_for` continue, all in the `do_execute([], ...)` cont + dispatcher) and at the two `State.check_call_depth!` call boundaries. +- The cross-module `:compiled_closure` / `Dispatcher.execute` and + `call_value` hand-offs seed the callee with a fresh budget rather than + changing the `{results, state}` return shape (changing it would ripple + into out-of-scope stdlib modules). Each compiled callee is bounded by + the dispatcher's own counting; runaway recursion that stays in the + interpreter is bounded by the threaded interpreter tally. + +### 4. Compiled dispatcher — `lib/lua/vm/dispatcher.ex` + +- Thread the same `steps` counter through `dispatch/8` → `dispatch/9`, + seeded at `0` at the dispatcher entry. +- Increment + `State.check_steps!/2` at the dispatcher's loop back-edges + and at the six `State.check_call_depth!(state)` call-boundary sites. + +### 5. Test — `test/lua/vm/max_steps_test.exs` + +New file. Cover: finite budget aborts `while true do end`; `pcall` +catches it and state stays usable; bounded loop returns normally and no +cross-eval leak; recursion under a finite budget raises the budget error +(interpreter path); `:infinity` imposes no bound; the compiled-dispatcher +path is bounded too; validation rejects `0`, `-1`, `:nope`. + +### 6. Docs — sandboxing guide + +`guides/sandboxing.md` is not tracked on `main`; the published guide is +`guides/examples/sandboxing.livemd`. Add a resource-limits section there +covering `:max_steps` mirroring the `:max_call_depth` framing. + +## Verification + +``` +mix format +mix compile --warnings-as-errors +mix test test/lua/vm/max_steps_test.exs +mix test test/lua/vm/recursion_depth_test.exs +mix test +mix test --only lua53 +``` + +## Risks + +- **Regressing the default `:infinity` hot path.** Mitigation: + `check_steps!/2` short-circuits on `:infinity` in a single + function-head match; counting happens only at loop back-edges and call + boundaries, never per opcode; gated on the benchmark step. +- **Counter scoping bug (per-frame vs whole-evaluation).** Mitigation: + thread `steps` through `do_execute`/`do_frame_return` so the + interpreter tally is global to one evaluation. The recursion test is + the guard. +- **Budget leaking across evaluations.** Mitigation: seed at `0` on each + `execute/5` / `Dispatcher.execute/4` entry. +- **Only one path enforced.** Mitigation: the test forces the compiled + path explicitly. +- **Error not catchable.** Mitigation: reuse `Lua.VM.RuntimeError`. +- **Plan-id leakage into source/tests.** Mitigation: id stays in the + commit body and PR description only. From 06ca109b399451e44773339c668a560432f85f74 Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Mon, 1 Jun 2026 10:17:56 -0700 Subject: [PATCH 2/9] feat(vm): add configurable :max_steps instruction budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `:max_steps` option to `Lua.new/1` mirroring `:max_call_depth`: default `:infinity` (no limit, existing behavior unchanged), a positive integer caps the VM instructions a single evaluation may execute, and exhaustion raises a catchable `"instruction budget exceeded"` runtime error recoverable via `pcall`. This gives library consumers a deterministic CPU bound without wrapping each call in a host Task and wall-clock timeout. The running tally is threaded as a parameter through the interpreter's `do_execute` chain and the compiled dispatcher's `dispatch` chain — not stored in `%State{}` — preserving the executor's `line`-off-State discipline so the default `:infinity` path carries no per-instruction cost. The counter is incremented only at loop back-edges and call boundaries; `check_steps!/2` short-circuits on `:infinity` in a single function-head match. Both execution paths enforce the budget. Plan: B17 Closes #306 --- guides/examples/sandboxing.livemd | 40 +++ lib/lua.ex | 28 +- lib/lua/vm/dispatcher.ex | 362 +++++++++++--------- lib/lua/vm/executor.ex | 540 +++++++++++++++++++----------- lib/lua/vm/state.ex | 38 ++- test/lua/vm/max_steps_test.exs | 133 ++++++++ 6 files changed, 797 insertions(+), 344 deletions(-) create mode 100644 test/lua/vm/max_steps_test.exs diff --git a/guides/examples/sandboxing.livemd b/guides/examples/sandboxing.livemd index 20db0b45..8c988f4d 100644 --- a/guides/examples/sandboxing.livemd +++ b/guides/examples/sandboxing.livemd @@ -66,3 +66,43 @@ false `Lua.new(sandboxed: [...])` replaces the whole sandbox list, and `Lua.new(sandboxed: [])` disables sandboxing entirely. Reach for these only when you fully trust the script you are running. + +## Bounding CPU work + +Sandboxing controls *which* functions a script may call, but it does not +stop a script from spinning forever (`while true do end`) or recursing +without bound. Two options give you deterministic limits without wrapping +each evaluation in a host `Task` plus a wall-clock timeout. + +### Call depth + +`Lua.new(max_call_depth: n)` caps the depth of nested function calls. +Recursing past the cap raises a catchable `"stack overflow"` runtime error +instead of letting the recursion exhaust the host process. The default is +`:infinity` (no limit). + +### Instruction budget + +`Lua.new(max_steps: n)` caps the number of VM instructions a single +evaluation may execute. When a script exceeds the budget it raises a +catchable `"instruction budget exceeded"` runtime error — so a runaway loop +terminates deterministically inside the VM: + +```elixir +{[ok?, message], _lua} = + Lua.eval!(Lua.new(max_steps: 1000), ~S[return pcall(function() while true do end end)]) + +{ok?, message} +``` + + + +``` +{false, "instruction budget exceeded"} +``` + +The budget is enforced at loop back-edges and call boundaries, so the +default `:infinity` carries no per-instruction cost, and it applies to both +the interpreter and the compiled-dispatcher execution paths. Each +top-level evaluation gets a fresh budget, and because the error is an +ordinary runtime error, `pcall` recovers from it in-band like any other. diff --git a/lib/lua.ex b/lib/lua.ex index 4f06e634..7c874db6 100644 --- a/lib/lua.ex +++ b/lib/lua.ex @@ -116,6 +116,19 @@ defmodule Lua do iex> {[false, message], _lua} = Lua.eval!(lua, "return pcall(string.rep, \\"x\\", 2048)") iex> message =~ "resulting string too large" true + + * `:max_steps` - (default `:infinity`) caps the number of VM instructions a single + evaluation may execute. When a script exceeds this budget, a catchable + `"instruction budget exceeded"` runtime error is raised, giving library consumers a + deterministic CPU bound without wrapping each call in a host `Task` + wall-clock timeout. + Accepts a positive integer or `:infinity` for no limit. The budget is enforced at loop + back-edges and call boundaries, so the default `:infinity` path carries no per-instruction + cost. The budget is fresh per top-level evaluation and recoverable via `pcall`. + + iex> lua = Lua.new(max_steps: 1000) + iex> {[false, message], _lua} = Lua.eval!(lua, "return pcall(function() while true do end end)") + iex> message =~ "instruction budget exceeded" + true """ @spec new(keyword()) :: t() def new(opts \\ []) do @@ -125,18 +138,21 @@ defmodule Lua do exclude: [], debug: false, max_call_depth: :infinity, - max_string_bytes: Lua.VM.Limits.max_string_bytes() + max_string_bytes: Lua.VM.Limits.max_string_bytes(), + max_steps: :infinity ) exclude = Keyword.fetch!(opts, :exclude) debug = Keyword.fetch!(opts, :debug) max_call_depth = validate_max_call_depth!(Keyword.fetch!(opts, :max_call_depth)) max_string_bytes = validate_max_string_bytes!(Keyword.fetch!(opts, :max_string_bytes)) + max_steps = validate_max_steps!(Keyword.fetch!(opts, :max_steps)) state = %{ Lua.VM.Stdlib.install(State.new()) | max_call_depth: max_call_depth, - max_string_bytes: max_string_bytes + max_string_bytes: max_string_bytes, + max_steps: max_steps } opts @@ -160,6 +176,14 @@ defmodule Lua do ":max_string_bytes must be a positive integer, got: #{inspect(other)}" end + defp validate_max_steps!(:infinity), do: :infinity + defp validate_max_steps!(steps) when is_integer(steps) and steps > 0, do: steps + + defp validate_max_steps!(other) do + raise ArgumentError, + ":max_steps must be a positive integer or :infinity, got: #{inspect(other)}" + end + @doc """ Write Lua code that is parsed at compile-time. diff --git a/lib/lua/vm/dispatcher.ex b/lib/lua/vm/dispatcher.ex index ff4dcf55..e9618285 100644 --- a/lib/lua/vm/dispatcher.ex +++ b/lib/lua/vm/dispatcher.ex @@ -159,7 +159,7 @@ defmodule Lua.VM.Dispatcher do try do state = %{state | open_upvalues: %{}} - {results, state} = dispatch(proto.bytecode, 1, regs, upvalues, proto, state, [], []) + {results, state} = dispatch(proto.bytecode, 1, regs, upvalues, proto, state, [], [], 0) state = %{state | open_upvalues: saved_open} {results, state} @@ -211,28 +211,28 @@ defmodule Lua.VM.Dispatcher do # `Executor.call_function/3` instead, paying one Erlang stack frame # at the boundary. - defp dispatch(code, pc, regs, upvalues, proto, state, cont, frames) when pc > tuple_size(code) do - finish_body(regs, upvalues, proto, state, cont, frames) + defp dispatch(code, pc, regs, upvalues, proto, state, cont, frames, steps) when pc > tuple_size(code) do + finish_body(regs, upvalues, proto, state, cont, frames, steps) end - defp dispatch(code, pc, regs, upvalues, proto, state, cont, frames) do + defp dispatch(code, pc, regs, upvalues, proto, state, cont, frames, steps) do case :erlang.element(pc, code) do {@op_load_constant, dest, value} -> regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_load_boolean, dest, value} -> regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_load_nil, dest, count} -> regs = clear_nils(regs, dest, count + 1) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_move, dest, src} -> v = :erlang.element(src + 1, regs) regs = :erlang.setelement(dest + 1, regs, v) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_load_env, dest} -> env = @@ -243,7 +243,7 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(dest + 1, regs, env) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_get_upvalue, dest, index} -> cell_ref = :erlang.element(index + 1, upvalues) @@ -254,12 +254,12 @@ defmodule Lua.VM.Dispatcher do # has to match where it does fire. v = Map.get(state.upvalue_cells, cell_ref) regs = :erlang.setelement(dest + 1, regs, v) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_get_global, dest, name} -> v = State.get_global(state, name) regs = :erlang.setelement(dest + 1, regs, v) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_get_field, dest, table_reg, name, name_hint} -> table_val = :erlang.element(table_reg + 1, regs) @@ -275,20 +275,20 @@ defmodule Lua.VM.Dispatcher do case data do %{^name => value} -> regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) _ -> case :erlang.map_get(:metatable, table) do nil -> regs = :erlang.setelement(dest + 1, regs, nil) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) _ -> {value, state} = Executor.dispatcher_get_field(table_val, name, state, proto, name_hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end end @@ -297,7 +297,7 @@ defmodule Lua.VM.Dispatcher do Executor.dispatcher_get_field(table_val, name, state, proto, name_hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end # ── Arithmetic ────────────────────────────────────────────────── @@ -316,16 +316,16 @@ defmodule Lua.VM.Dispatcher do sum = va + vb wrapped = if sum >= @min_int and sum <= @max_int, do: sum, else: Numeric.to_signed_int64(sum) regs = :erlang.setelement(dest + 1, regs, wrapped) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va + vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) true -> {value, state} = Executor.dispatcher_binop(:add, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_subtract, dest, a, b, hint_a, hint_b} -> @@ -337,16 +337,16 @@ defmodule Lua.VM.Dispatcher do diff = va - vb wrapped = if diff >= @min_int and diff <= @max_int, do: diff, else: Numeric.to_signed_int64(diff) regs = :erlang.setelement(dest + 1, regs, wrapped) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va - vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) true -> {value, state} = Executor.dispatcher_binop(:subtract, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_multiply, dest, a, b, hint_a, hint_b} -> @@ -358,16 +358,16 @@ defmodule Lua.VM.Dispatcher do prod = va * vb wrapped = if prod >= @min_int and prod <= @max_int, do: prod, else: Numeric.to_signed_int64(prod) regs = :erlang.setelement(dest + 1, regs, wrapped) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va * vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) true -> {value, state} = Executor.dispatcher_binop(:multiply, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_divide, dest, a, b, hint_a, hint_b} -> @@ -383,7 +383,7 @@ defmodule Lua.VM.Dispatcher do ) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_floor_divide, dest, a, b, hint_a, hint_b} -> {value, state} = @@ -398,7 +398,7 @@ defmodule Lua.VM.Dispatcher do ) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_modulo, dest, a, b, hint_a, hint_b} -> {value, state} = @@ -413,7 +413,7 @@ defmodule Lua.VM.Dispatcher do ) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_power, dest, a, b, hint_a, hint_b} -> {value, state} = @@ -428,14 +428,14 @@ defmodule Lua.VM.Dispatcher do ) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_negate, dest, src, hint} -> {value, state} = Executor.dispatcher_unop(:negate, :erlang.element(src + 1, regs), state, proto, hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) # ── Bitwise ───────────────────────────────────────────────────── # @@ -459,11 +459,11 @@ defmodule Lua.VM.Dispatcher do if is_integer(va) and is_integer(vb) do regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(Bitwise.band(va, vb))) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) else {value, state} = Executor.dispatcher_bitwise(:band, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_bitwise_or, dest, a, b, hint_a, hint_b} -> @@ -472,11 +472,11 @@ defmodule Lua.VM.Dispatcher do if is_integer(va) and is_integer(vb) do regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(Bitwise.bor(va, vb))) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) else {value, state} = Executor.dispatcher_bitwise(:bor, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_bitwise_xor, dest, a, b, hint_a, hint_b} -> @@ -485,11 +485,11 @@ defmodule Lua.VM.Dispatcher do if is_integer(va) and is_integer(vb) do regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(Bitwise.bxor(va, vb))) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) else {value, state} = Executor.dispatcher_bitwise(:bxor, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_shift_left, dest, a, b, hint_a, hint_b} -> @@ -497,20 +497,20 @@ defmodule Lua.VM.Dispatcher do vb = :erlang.element(b + 1, regs) {value, state} = Executor.dispatcher_bitwise(:shl, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_shift_right, dest, a, b, hint_a, hint_b} -> va = :erlang.element(a + 1, regs) vb = :erlang.element(b + 1, regs) {value, state} = Executor.dispatcher_bitwise(:shr, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_bitwise_not, dest, src, hint} -> val = :erlang.element(src + 1, regs) {value, state} = Executor.dispatcher_bnot(val, state, proto, hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) # ── Comparisons ───────────────────────────────────────────────── @@ -521,16 +521,16 @@ defmodule Lua.VM.Dispatcher do cond do is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va < vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) is_binary(va) and is_binary(vb) -> regs = :erlang.setelement(dest + 1, regs, va < vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) true -> {value, state} = Executor.dispatcher_cmp(:less_than, va, vb, state, proto) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_less_equal, dest, a, b} -> @@ -540,16 +540,16 @@ defmodule Lua.VM.Dispatcher do cond do is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va <= vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) is_binary(va) and is_binary(vb) -> regs = :erlang.setelement(dest + 1, regs, va <= vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) true -> {value, state} = Executor.dispatcher_cmp(:less_equal, va, vb, state, proto) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_greater_than, dest, a, b} -> @@ -559,16 +559,16 @@ defmodule Lua.VM.Dispatcher do cond do is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va > vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) is_binary(va) and is_binary(vb) -> regs = :erlang.setelement(dest + 1, regs, va > vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) true -> {value, state} = Executor.dispatcher_cmp(:greater_than, va, vb, state, proto) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_greater_equal, dest, a, b} -> @@ -578,16 +578,16 @@ defmodule Lua.VM.Dispatcher do cond do is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va >= vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) is_binary(va) and is_binary(vb) -> regs = :erlang.setelement(dest + 1, regs, va >= vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) true -> {value, state} = Executor.dispatcher_cmp(:greater_equal, va, vb, state, proto) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_equal, dest, a, b} -> @@ -597,16 +597,16 @@ defmodule Lua.VM.Dispatcher do cond do is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va == vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) is_binary(va) and is_binary(vb) -> regs = :erlang.setelement(dest + 1, regs, va == vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) true -> {value, state} = Executor.dispatcher_cmp(:equal, va, vb, state, proto) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_not_equal, dest, a, b} -> @@ -616,16 +616,16 @@ defmodule Lua.VM.Dispatcher do cond do is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va != vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) is_binary(va) and is_binary(vb) -> regs = :erlang.setelement(dest + 1, regs, va != vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) true -> {value, state} = Executor.dispatcher_cmp(:not_equal, va, vb, state, proto) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_not, dest, src} -> @@ -634,7 +634,7 @@ defmodule Lua.VM.Dispatcher do # values. Saves a function call per `:not` opcode. result = v === nil or v === false regs = :erlang.setelement(dest + 1, regs, result) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) # ── Conditional branching ─────────────────────────────────────── # @@ -653,7 +653,7 @@ defmodule Lua.VM.Dispatcher do _ -> then_bc end - dispatch(branch, 1, regs, upvalues, proto, state, [{code, pc + 1} | cont], frames) + dispatch(branch, 1, regs, upvalues, proto, state, [{code, pc + 1} | cont], frames, steps) # ── Calls ─────────────────────────────────────────────────────── # @@ -684,6 +684,8 @@ defmodule Lua.VM.Dispatcher do {code, pc + 1, regs, upvalues, proto, cont, :discard, state.open_upvalues} call_info = Executor.dispatcher_call_info(proto, name_hint, 0) + steps = steps + 1 + State.check_steps!(state, steps) State.check_call_depth!(state) state = %{ @@ -701,17 +703,20 @@ defmodule Lua.VM.Dispatcher do callee_proto, state, [], - [frame | frames] + [frame | frames], + steps ) {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, arg_count) call_info = Executor.dispatcher_call_info(proto, name_hint, 0) + steps = steps + 1 + State.check_steps!(state, steps) State.check_call_depth!(state) state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1} {_results, state} = Executor.call_function(closure, args, state) state = %{state | call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) _ -> args = collect_args(regs, base + 1, arg_count) @@ -719,7 +724,7 @@ defmodule Lua.VM.Dispatcher do {_results, state} = Executor.dispatcher_call_function(func_value, args, state, proto, name_hint, line) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_call_one, base, arg_count, name_hint, line} -> @@ -737,6 +742,8 @@ defmodule Lua.VM.Dispatcher do {code, pc + 1, regs, upvalues, proto, cont, base, state.open_upvalues} call_info = Executor.dispatcher_call_info(proto, name_hint, 0) + steps = steps + 1 + State.check_steps!(state, steps) State.check_call_depth!(state) state = %{ @@ -754,12 +761,15 @@ defmodule Lua.VM.Dispatcher do callee_proto, state, [], - [frame | frames] + [frame | frames], + steps ) {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, arg_count) call_info = Executor.dispatcher_call_info(proto, name_hint, 0) + steps = steps + 1 + State.check_steps!(state, steps) State.check_call_depth!(state) state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1} {results, state} = Executor.call_function(closure, args, state) @@ -772,7 +782,7 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(base + 1, regs, first) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) _ -> args = collect_args(regs, base + 1, arg_count) @@ -787,7 +797,7 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(base + 1, regs, first) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end # ── Returns ───────────────────────────────────────────────────── @@ -800,10 +810,10 @@ defmodule Lua.VM.Dispatcher do # `call_function/3` contract. {@op_return_one, base} -> - return_one(:erlang.element(base + 1, regs), state, frames) + return_one(:erlang.element(base + 1, regs), state, frames, steps) {@op_return_zero} -> - return_one(nil, state, frames) + return_one(nil, state, frames, steps) # ── Table opcodes ─────────────────────────────────────────────── # @@ -815,7 +825,7 @@ defmodule Lua.VM.Dispatcher do {@op_new_table, dest} -> {tref, state} = State.alloc_table(state) regs = :erlang.setelement(dest + 1, regs, tref) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_get_table, dest, table_reg, key_reg, name_hint} -> table_val = :erlang.element(table_reg + 1, regs) @@ -830,19 +840,19 @@ defmodule Lua.VM.Dispatcher do case :erlang.map_get(:metatable, table) do nil -> regs = :erlang.setelement(dest + 1, regs, nil) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) _ -> {value, state} = Executor.dispatcher_get_table(table_val, key, state, proto, name_hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end value -> regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {:tref, id} when is_integer(key) or is_binary(key) -> @@ -852,20 +862,20 @@ defmodule Lua.VM.Dispatcher do case data do %{^key => value} -> regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) _ -> case :erlang.map_get(:metatable, table) do nil -> regs = :erlang.setelement(dest + 1, regs, nil) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) _ -> {value, state} = Executor.dispatcher_get_table(table_val, key, state, proto, name_hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end end @@ -874,7 +884,7 @@ defmodule Lua.VM.Dispatcher do Executor.dispatcher_get_table(table_val, key, state, proto, name_hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end {@op_set_table, table_reg, key_reg, value_reg, name_hint} -> @@ -882,13 +892,13 @@ defmodule Lua.VM.Dispatcher do key = :erlang.element(key_reg + 1, regs) value = :erlang.element(value_reg + 1, regs) state = Executor.dispatcher_set_table(table_val, key, value, state, proto, name_hint) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_set_field, table_reg, name, value_reg, name_hint} -> table_val = :erlang.element(table_reg + 1, regs) value = :erlang.element(value_reg + 1, regs) state = Executor.dispatcher_set_field(table_val, name, value, state, proto, name_hint) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) # `:set_list` with a positive integer count is the table-constructor # form. The `count == 0` sentinel was filtered upstream and never @@ -901,7 +911,7 @@ defmodule Lua.VM.Dispatcher do set_list_into_table(table, regs, start, count, offset, 0) end) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) # `:set_list` multi-return tail (`{f(), 1}`): fold the static prefix # `init_count` with the trailing values count the last multi-return @@ -916,7 +926,7 @@ defmodule Lua.VM.Dispatcher do set_list_into_table(table, regs, start, total, offset, 0) end) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_length, dest, source} -> value = :erlang.element(source + 1, regs) @@ -931,22 +941,22 @@ defmodule Lua.VM.Dispatcher do # without __len is the border length of the data map. len = Table.length(table) regs = :erlang.setelement(dest + 1, regs, len) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) _ -> {len, state} = Executor.dispatcher_length(value, state, proto) regs = :erlang.setelement(dest + 1, regs, len) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end v when is_binary(v) -> regs = :erlang.setelement(dest + 1, regs, byte_size(v)) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) _ -> {len, state} = Executor.dispatcher_length(value, state, proto) regs = :erlang.setelement(dest + 1, regs, len) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end # ── numeric_for ───────────────────────────────────────────────── @@ -978,9 +988,9 @@ defmodule Lua.VM.Dispatcher do state = Executor.dispatcher_close_open_upvalues_at_or_above(state, loop_var) marker = {:cps_for, base, loop_var, body_bc, code, pc + 1} loop_exit = {:loop_exit, code, pc + 1} - dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | cont], frames) + dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | cont], frames, steps) else - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end # ── while_loop / repeat_loop / generic_for ───────────────────── @@ -993,12 +1003,12 @@ defmodule Lua.VM.Dispatcher do {@op_while_loop, test_reg, cond_bc, body_bc} -> cps = {:cps_while_test, test_reg, cond_bc, body_bc, code, pc + 1} loop_exit = {:loop_exit, code, pc + 1} - dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | cont], frames) + dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | cont], frames, steps) {@op_repeat_loop, test_reg, body_bc, cond_bc} -> cps = {:cps_repeat_body, test_reg, body_bc, cond_bc, code, pc + 1} loop_exit = {:loop_exit, code, pc + 1} - dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | cont], frames) + dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | cont], frames, steps) {@op_generic_for, base, var_regs, body_bc, line} -> # Iterator call follows the same shape as the executor: @@ -1016,10 +1026,10 @@ defmodule Lua.VM.Dispatcher do case results do [nil | _] -> - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) [] -> - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) [first | _] -> regs = :erlang.setelement(base + 3, regs, first) @@ -1028,7 +1038,7 @@ defmodule Lua.VM.Dispatcher do state = Executor.dispatcher_close_open_upvalues_at_or_above(state, first_var_reg) marker = {:cps_generic_for, base, var_regs, body_bc, line, code, pc + 1} loop_exit = {:loop_exit, code, pc + 1} - dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | cont], frames) + dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | cont], frames, steps) end # ── break ───────────────────────────────────────────────────── @@ -1039,7 +1049,7 @@ defmodule Lua.VM.Dispatcher do {@op_break} -> {exit_code, exit_pc, rest_cont} = find_loop_exit(cont) - dispatch(exit_code, exit_pc, regs, upvalues, proto, state, rest_cont, frames) + dispatch(exit_code, exit_pc, regs, upvalues, proto, state, rest_cont, frames, steps) # ── label / goto ────────────────────────────────────────────── # @@ -1086,7 +1096,7 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(dest + 1, regs, closure) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) # ── Upvalue access ──────────────────────────────────────────── # @@ -1101,7 +1111,7 @@ defmodule Lua.VM.Dispatcher do cell_ref = :erlang.element(index + 1, upvalues) value = :erlang.element(source + 1, regs) state = %{state | upvalue_cells: Map.put(state.upvalue_cells, cell_ref, value)} - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_get_open_upvalue, dest, reg} -> value = @@ -1111,7 +1121,7 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_set_open_upvalue, reg, source} -> state = @@ -1124,11 +1134,11 @@ defmodule Lua.VM.Dispatcher do %{state | upvalue_cells: Map.put(state.upvalue_cells, cell_ref, value)} end - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_close_upvalues, threshold} -> state = Executor.dispatcher_close_open_upvalues_at_or_above(state, threshold) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) # ── Vararg ──────────────────────────────────────────────────── # @@ -1143,25 +1153,25 @@ defmodule Lua.VM.Dispatcher do varargs = proto.varargs {regs, n} = write_varargs(regs, base, varargs, 0) state = %{state | multi_return_count: n} - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) {@op_vararg, base, count} -> regs = write_varargs_n(regs, base, proto.varargs, count) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) # ── Multi-return returns ────────────────────────────────────── {@op_return_proto_varargs} -> - return_multi(proto.varargs, state, frames) + return_multi(proto.varargs, state, frames, steps) {@op_return_collect, base, fixed} -> total = fixed + state.multi_return_count results = collect_args(regs, base, total) - return_multi(results, state, frames) + return_multi(results, state, frames, steps) {@op_return_multi, base, count} -> results = collect_args(regs, base, count) - return_multi(results, state, frames) + return_multi(results, state, frames, steps) # ── Multi-return calls ──────────────────────────────────────── # @@ -1203,6 +1213,8 @@ defmodule Lua.VM.Dispatcher do frame = {code, pc + 1, regs, upvalues, proto, cont, dest, state.open_upvalues} call_info = Executor.dispatcher_call_info(proto, name_hint, 0) + steps = steps + 1 + State.check_steps!(state, steps) State.check_call_depth!(state) state = %{ @@ -1220,17 +1232,34 @@ defmodule Lua.VM.Dispatcher do callee_proto, state, [], - [frame | frames] + [frame | frames], + steps ) {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, total_args) call_info = Executor.dispatcher_call_info(proto, name_hint, 0) + steps = steps + 1 + State.check_steps!(state, steps) State.check_call_depth!(state) state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1} {results, state} = Executor.call_function(closure, args, state) state = %{state | call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} - apply_multi_call_result(result_count, base, results, code, pc + 1, regs, upvalues, proto, state, cont, frames) + + apply_multi_call_result( + result_count, + base, + results, + code, + pc + 1, + regs, + upvalues, + proto, + state, + cont, + frames, + steps + ) _ -> args = collect_args(regs, base + 1, total_args) @@ -1238,7 +1267,20 @@ defmodule Lua.VM.Dispatcher do {results, state} = Executor.dispatcher_call_function(func_value, args, state, proto, name_hint, line) - apply_multi_call_result(result_count, base, results, code, pc + 1, regs, upvalues, proto, state, cont, frames) + apply_multi_call_result( + result_count, + base, + results, + code, + pc + 1, + regs, + upvalues, + proto, + state, + cont, + frames, + steps + ) end # ── self ────────────────────────────────────────────────────── @@ -1254,7 +1296,7 @@ defmodule Lua.VM.Dispatcher do {func, state} = Executor.dispatcher_index_method_target(obj, method_name, state, proto, name_hint) regs = :erlang.setelement(base + 2, regs, obj) regs = :erlang.setelement(base + 1, regs, func) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) # ── Concatenation ───────────────────────────────────────────── # @@ -1272,19 +1314,19 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(dest + 1, regs, left <> right) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) else {result, state} = Executor.dispatcher_concat(left, right, state, proto) regs = :erlang.setelement(dest + 1, regs, result) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end end end # ── End-of-body handling ──────────────────────────────────────────────── - defp finish_body(regs, upvalues, proto, state, [{next_code, next_pc} | rest_cont], frames) do - dispatch(next_code, next_pc, regs, upvalues, proto, state, rest_cont, frames) + defp finish_body(regs, upvalues, proto, state, [{next_code, next_pc} | rest_cont], frames, steps) do + dispatch(next_code, next_pc, regs, upvalues, proto, state, rest_cont, frames, steps) end # `:numeric_for` body ran to completion. Increment the counter, re-test, @@ -1300,7 +1342,8 @@ defmodule Lua.VM.Dispatcher do proto, state, [{:cps_for, base, loop_var, body_bc, outer_code, outer_pc} = marker, {:loop_exit, _, _} = loop_exit | rest_cont], - frames + frames, + steps ) do counter = :erlang.element(base + 1, regs) step = :erlang.element(base + 3, regs) @@ -1310,11 +1353,13 @@ defmodule Lua.VM.Dispatcher do should_continue = if step > 0, do: new_counter <= limit, else: new_counter >= limit if should_continue do + steps = steps + 1 + State.check_steps!(state, steps) regs = :erlang.setelement(loop_var + 1, regs, new_counter) state = Executor.dispatcher_close_open_upvalues_at_or_above(state, loop_var) - dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | rest_cont], frames) + dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | rest_cont], frames, steps) else - dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, steps) end end @@ -1329,15 +1374,16 @@ defmodule Lua.VM.Dispatcher do {:cps_while_test, test_reg, cond_bc, body_bc, outer_code, outer_pc}, {:loop_exit, _, _} = loop_exit | rest_cont ], - frames + frames, + steps ) do case :erlang.element(test_reg + 1, regs) do v when v === nil or v === false -> - dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, steps) _ -> cps = {:cps_while_body, test_reg, cond_bc, body_bc, outer_code, outer_pc} - dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames) + dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, steps) end end @@ -1352,10 +1398,13 @@ defmodule Lua.VM.Dispatcher do {:cps_while_body, test_reg, cond_bc, body_bc, outer_code, outer_pc}, {:loop_exit, _, _} = loop_exit | rest_cont ], - frames + frames, + steps ) do + steps = steps + 1 + State.check_steps!(state, steps) cps = {:cps_while_test, test_reg, cond_bc, body_bc, outer_code, outer_pc} - dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames) + dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, steps) end # `:repeat_loop`: body just finished. Run the condition next. @@ -1368,10 +1417,11 @@ defmodule Lua.VM.Dispatcher do {:cps_repeat_body, test_reg, body_bc, cond_bc, outer_code, outer_pc}, {:loop_exit, _, _} = loop_exit | rest_cont ], - frames + frames, + steps ) do cps = {:cps_repeat_cond, test_reg, body_bc, cond_bc, outer_code, outer_pc} - dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames) + dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, steps) end # `:repeat_loop`: condition just finished. test_reg truthy = exit (Lua's @@ -1385,15 +1435,18 @@ defmodule Lua.VM.Dispatcher do {:cps_repeat_cond, test_reg, body_bc, cond_bc, outer_code, outer_pc}, {:loop_exit, _, _} = loop_exit | rest_cont ], - frames + frames, + steps ) do case :erlang.element(test_reg + 1, regs) do v when v === nil or v === false -> + steps = steps + 1 + State.check_steps!(state, steps) cps = {:cps_repeat_body, test_reg, body_bc, cond_bc, outer_code, outer_pc} - dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames) + dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, steps) _ -> - dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, steps) end end @@ -1407,7 +1460,8 @@ defmodule Lua.VM.Dispatcher do {:cps_generic_for, base, var_regs, body_bc, line, outer_code, outer_pc} = marker, {:loop_exit, _, _} = loop_exit | rest_cont ], - frames + frames, + steps ) do iter_func = :erlang.element(base + 1, regs) invariant_state = :erlang.element(base + 2, regs) @@ -1418,17 +1472,19 @@ defmodule Lua.VM.Dispatcher do case results do [nil | _] -> - dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, steps) [] -> - dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, steps) [first | _] -> + steps = steps + 1 + State.check_steps!(state, steps) regs = :erlang.setelement(base + 3, regs, first) regs = assign_iter_results(regs, var_regs, results, 0) first_var_reg = :erlang.element(1, var_regs) state = Executor.dispatcher_close_open_upvalues_at_or_above(state, first_var_reg) - dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | rest_cont], frames) + dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | rest_cont], frames, steps) end end @@ -1436,8 +1492,8 @@ defmodule Lua.VM.Dispatcher do # body ran past its last instruction with the loop_exit still on top). # Drop the loop_exit and let the next iteration of finish_body see the # cont below it. - defp finish_body(regs, upvalues, proto, state, [{:loop_exit, _, _} | rest_cont], frames) do - finish_body(regs, upvalues, proto, state, rest_cont, frames) + defp finish_body(regs, upvalues, proto, state, [{:loop_exit, _, _} | rest_cont], frames, steps) do + finish_body(regs, upvalues, proto, state, rest_cont, frames, steps) end # Body exhausted with no continuation: prototype ran off the end. Lua @@ -1445,8 +1501,8 @@ defmodule Lua.VM.Dispatcher do # values when control falls off the end, not a single `nil` — the # caller's `result_count` decides how that's projected (nil for a # single-value site, empty slot for a multi-return one). - defp finish_body(_regs, _upvalues, _proto, state, [], frames) do - return_multi([], state, frames) + defp finish_body(_regs, _upvalues, _proto, state, [], frames, steps) do + return_multi([], state, frames, steps) end # ── Return propagation through frames ─────────────────────────────────── @@ -1465,11 +1521,11 @@ defmodule Lua.VM.Dispatcher do # {:multi, B, -2} → expand all into regs[B..], set multi_return_count. # {:multi, B, n>1} → write n results into regs[B..], pad nil. - defp return_one(value, state, []) do + defp return_one(value, state, [], _steps) do {[value], state} end - defp return_one(value, state, [frame | rest_frames]) do + defp return_one(value, state, [frame | rest_frames], steps) do {code, pc, regs, upvalues, proto, cont, dest, saved_open} = frame # Every dispatcher frame corresponds to a Lua-level call that pushed a # call_stack entry. Pop it on the way out — the interpreter's @@ -1478,14 +1534,14 @@ defmodule Lua.VM.Dispatcher do case dest do :discard -> - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) n when is_integer(n) -> regs = :erlang.setelement(n + 1, regs, value) - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) {:multi, _, -1} -> - return_one(value, state, rest_frames) + return_one(value, state, rest_frames, steps) {:multi, base, -2} -> # The expansion dest may sit past the statically reserved register @@ -1495,13 +1551,13 @@ defmodule Lua.VM.Dispatcher do regs = grow_regs(regs, base + 1) regs = :erlang.setelement(base + 1, regs, value) state = %{state | multi_return_count: 1} - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) {:multi, base, n} when is_integer(n) and n > 1 -> regs = grow_regs(regs, base + n) regs = :erlang.setelement(base + 1, regs, value) regs = pad_nils(regs, base + 1, n - 1) - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) end end @@ -1509,17 +1565,17 @@ defmodule Lua.VM.Dispatcher do # `:return_proto_varargs`, and the non-compiled-callee branch of # `:call_multi`. Mirrors `return_one/3`'s frame-variant handling. - defp return_multi(results, state, []) do + defp return_multi(results, state, [], _steps) do {results, state} end - defp return_multi(results, state, [frame | rest_frames]) do + defp return_multi(results, state, [frame | rest_frames], steps) do {code, pc, regs, upvalues, proto, cont, dest, saved_open} = frame state = %{state | open_upvalues: saved_open, call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} case dest do :discard -> - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) n when is_integer(n) -> v = @@ -1529,19 +1585,19 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(n + 1, regs, v) - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) {:multi, _, -1} -> - return_multi(results, state, rest_frames) + return_multi(results, state, rest_frames, steps) {:multi, base, -2} -> regs = write_results(regs, base, results) state = %{state | multi_return_count: length(results)} - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) {:multi, base, n} when is_integer(n) and n > 1 -> regs = write_results_n(regs, base, results, n) - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) end end @@ -1783,11 +1839,11 @@ defmodule Lua.VM.Dispatcher do # and unwinds via `return_multi/3`; this helper is for the synchronous # post-call shape (native, __call metamethod, lua_closure via # call_function). Mirrors `Executor.continue_after_call/11` shape. - defp apply_multi_call_result(0, _base, _results, code, pc, regs, upvalues, proto, state, cont, frames) do - dispatch(code, pc, regs, upvalues, proto, state, cont, frames) + defp apply_multi_call_result(0, _base, _results, code, pc, regs, upvalues, proto, state, cont, frames, steps) do + dispatch(code, pc, regs, upvalues, proto, state, cont, frames, steps) end - defp apply_multi_call_result(1, base, results, code, pc, regs, upvalues, proto, state, cont, frames) do + defp apply_multi_call_result(1, base, results, code, pc, regs, upvalues, proto, state, cont, frames, steps) do first = case results do [v | _] -> v @@ -1795,23 +1851,23 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(base + 1, regs, first) - dispatch(code, pc, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc, regs, upvalues, proto, state, cont, frames, steps) end - defp apply_multi_call_result(-1, _base, results, _code, _pc, _regs, _upvalues, _proto, state, _cont, frames) do - return_multi(results, state, frames) + defp apply_multi_call_result(-1, _base, results, _code, _pc, _regs, _upvalues, _proto, state, _cont, frames, steps) do + return_multi(results, state, frames, steps) end - defp apply_multi_call_result(-2, base, results, code, pc, regs, upvalues, proto, state, cont, frames) do + defp apply_multi_call_result(-2, base, results, code, pc, regs, upvalues, proto, state, cont, frames, steps) do regs = write_results(regs, base, results) state = %{state | multi_return_count: length(results)} - dispatch(code, pc, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc, regs, upvalues, proto, state, cont, frames, steps) end - defp apply_multi_call_result(n, base, results, code, pc, regs, upvalues, proto, state, cont, frames) + defp apply_multi_call_result(n, base, results, code, pc, regs, upvalues, proto, state, cont, frames, steps) when is_integer(n) and n > 1 do regs = write_results_n(regs, base, results, n) - dispatch(code, pc, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc, regs, upvalues, proto, state, cont, frames, steps) end # Lazy regs-tuple growth. Used at the points where multi-return diff --git a/lib/lua/vm/executor.ex b/lib/lua/vm/executor.ex index 71f14790..f4427e3d 100644 --- a/lib/lua/vm/executor.ex +++ b/lib/lua/vm/executor.ex @@ -5,11 +5,16 @@ defmodule Lua.VM.Executor do 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) + Signature: do_execute(instructions, registers, upvalues, proto, state, cont, frames, line, steps) 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) + steps — running instruction tally for the `:max_steps` budget, threaded + as a parameter (not stored in `%State{}`) for the same reason + `line` is: it would otherwise force a struct rebuild on the hot + path. Incremented only at loop back-edges and call boundaries — + never per opcode — so the default `:infinity` budget is free. """ alias Lua.VM.Dispatcher @@ -86,7 +91,7 @@ defmodule Lua.VM.Executor do state = %{state | open_upvalues: %{}} {results, regs, state} = - do_execute(instructions, registers, upvalues, proto, state, [], [], 0) + do_execute(instructions, registers, upvalues, proto, state, [], [], 0, 0) {results, regs, %{state | open_upvalues: saved_open_upvalues}} rescue @@ -154,7 +159,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, [], [], 0) + do_execute(callee_proto.instructions, callee_regs, callee_upvalues, callee_proto, state, [], [], 0, 0) state = %{state | open_upvalues: saved_open_upvalues} {results, state} @@ -646,9 +651,9 @@ defmodule Lua.VM.Executor do # ── Break ────────────────────────────────────────────────────────────────── - defp do_execute([:break | _rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([:break | _rest], regs, upvalues, proto, state, cont, frames, line, steps) do {exit_is, rest_cont} = find_loop_exit(cont) - do_execute(exit_is, regs, upvalues, proto, state, rest_cont, frames, line) + do_execute(exit_is, regs, upvalues, proto, state, rest_cont, frames, line, steps) end # ── Goto ─────────────────────────────────────────────────────────────────── @@ -658,7 +663,7 @@ defmodule Lua.VM.Executor do # `cont` entries leaves the blocks between here and the target; `target_tail` # is the destination block's instruction suffix after the `::label::`. - defp do_execute([{:goto, id} | _rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:goto, id} | _rest], regs, upvalues, proto, state, cont, frames, line, steps) do case proto.goto_targets do %{^id => {depth, level, target_tail}} -> # Close upvalue cells for locals allocated beyond the target's scope: @@ -666,7 +671,7 @@ defmodule Lua.VM.Executor do # their captured locals with them, so the next pass through gets fresh # cells — matching Lua 5.3 §3.3.4 block-exit semantics. state = close_open_upvalues_at_or_above(state, level) - do_execute(target_tail, regs, upvalues, proto, state, Enum.drop(cont, depth), frames, line) + do_execute(target_tail, regs, upvalues, proto, state, Enum.drop(cont, depth), frames, line, steps) _ -> raise InternalError, value: "goto target not found" @@ -675,21 +680,21 @@ defmodule Lua.VM.Executor do # ── Label ────────────────────────────────────────────────────────────────── - defp do_execute([{:label, _name, _level, _block_path} | rest], regs, upvalues, proto, state, cont, frames, line) do - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + defp do_execute([{:label, _name, _level, _block_path} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── Instructions exhausted — handle continuations and frames ─────────────── - defp do_execute([], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([], regs, upvalues, proto, state, cont, frames, line, steps) 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) + do_execute(next_is, regs, upvalues, proto, state, rest_cont, frames, line, steps) # 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) + do_execute([], regs, upvalues, proto, state, rest_cont, frames, line, steps) # After while condition body — check test_reg; enter body or exit loop [{:cps_while_test, test_reg, loop_body, cond_body, rest, outer_cont} | _] -> @@ -697,33 +702,37 @@ defmodule Lua.VM.Executor do 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) + do_execute(loop_body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line, steps) else - do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, steps) end # After while loop body — restart condition [{:cps_while_body, test_reg, loop_body, cond_body, rest, outer_cont} | _] -> + steps = steps + 1 + State.check_steps!(state, steps) 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) + do_execute(cond_body, regs, upvalues, proto, state, [cond_check | loop_exit_cont], frames, line, steps) # 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) + do_execute(cond_body, regs, upvalues, proto, state, [cond_check | loop_exit_cont], frames, line, steps) # 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) + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, steps) else # Condition false = repeat body + steps = steps + 1 + State.check_steps!(state, steps) 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) + do_execute(loop_body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line, steps) end # After numeric_for body — increment counter and re-check @@ -736,15 +745,17 @@ defmodule Lua.VM.Executor do should_continue = if step > 0, do: new_counter <= limit, else: new_counter >= limit if should_continue do + steps = steps + 1 + State.check_steps!(state, steps) regs = put_elem(regs, loop_var, new_counter) state = close_open_upvalues_at_or_above(state, loop_var) 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) + do_execute(body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line, steps) else - do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, steps) end # After generic_for body — call iterator and re-check @@ -757,8 +768,10 @@ defmodule Lua.VM.Executor do first_result = List.first(results) if first_result == nil do - do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, steps) else + steps = steps + 1 + State.check_steps!(state, steps) regs = put_elem(regs, base + 2, first_result) regs = @@ -771,7 +784,7 @@ defmodule Lua.VM.Executor do 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) + do_execute(body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line, steps) end # Continuation stack exhausted — check frames for pending function return @@ -781,31 +794,31 @@ defmodule Lua.VM.Executor do {[], regs, state} [frame | rest_frames] -> - do_frame_return([], regs, state, frame, rest_frames, line) + do_frame_return([], regs, state, frame, rest_frames, line, steps) end end end # ── load_constant ────────────────────────────────────────────────────────── - defp do_execute([{:load_constant, dest, value} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:load_constant, dest, value} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── load_boolean ─────────────────────────────────────────────────────────── - defp do_execute([{:load_boolean, dest, value} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:load_boolean, dest, value} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── get_global ───────────────────────────────────────────────────────────── - defp do_execute([{:get_global, dest, name} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:get_global, dest, name} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do value = State.get_global(state, name) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── load_env ─────────────────────────────────────────────────────────────── @@ -815,9 +828,9 @@ defmodule Lua.VM.Executor do # environment carries it in upvalue slot 0 (see `Stdlib.compile_loaded_chunk`); # otherwise `_ENV` defaults to the global table `_G`. - defp do_execute([{:load_env, dest} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:load_env, dest} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do regs = put_elem(regs, dest, load_env_value(upvalues, state)) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── get_upvalue / set_upvalue ────────────────────────────────────────────── @@ -838,18 +851,20 @@ defmodule Lua.VM.Executor do # same cell. `Map.get/2` keeps the nil-for-dangling-cell semantics the # dispatcher mirrors. - defp do_execute([{:get_upvalue, dest, index} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:get_upvalue, dest, index} | rest], regs, upvalues, proto, state, cont, frames, line, steps) 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, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end - defp do_execute([{:set_upvalue, index, source} | rest], regs, upvalues, proto, state, cont, frames, line) do + # ── set_upvalue ──────────────────────────────────────────────────────────── + + defp do_execute([{:set_upvalue, index, source} | rest], regs, upvalues, proto, state, cont, frames, line, steps) 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, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── get_open_upvalue ─────────────────────────────────────────────────────── @@ -858,7 +873,7 @@ defmodule Lua.VM.Executor do # If no cell has been created yet (no closure has captured this register), # the register itself is the source of truth -- the next closure that # captures the register will create a cell from the current register value. - defp do_execute([{:get_open_upvalue, dest, reg} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:get_open_upvalue, dest, reg} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do value = case Map.get(state.open_upvalues, reg) do nil -> elem(regs, reg) @@ -866,7 +881,7 @@ defmodule Lua.VM.Executor do end regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── close_upvalues ───────────────────────────────────────────────────────── @@ -878,9 +893,9 @@ defmodule Lua.VM.Executor do # slots does not read or overwrite the stale cell. Loop bodies do this on # each iteration boundary in the continuation handlers above; this is the # same operation for non-loop block exits. - defp do_execute([{:close_upvalues, threshold} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:close_upvalues, threshold} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do state = close_open_upvalues_at_or_above(state, threshold) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── set_open_upvalue ─────────────────────────────────────────────────────── @@ -890,7 +905,7 @@ defmodule Lua.VM.Executor do # holds the value (codegen always emits a move into the register before # set_open_upvalue), and the next closure that captures the register will # create a cell from the current register value. - defp do_execute([{:set_open_upvalue, reg, source} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:set_open_upvalue, reg, source} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do state = case Map.get(state.open_upvalues, reg) do nil -> @@ -901,53 +916,53 @@ defmodule Lua.VM.Executor do %{state | upvalue_cells: Map.put(state.upvalue_cells, cell_ref, value)} end - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── 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) + defp do_execute([{:source_line, new_line, _file} | rest], regs, upvalues, proto, state, cont, frames, _line, steps) do + do_execute(rest, regs, upvalues, proto, state, cont, frames, new_line, steps) end # ── move ─────────────────────────────────────────────────────────────────── - defp do_execute([{:move, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:move, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do value = elem(regs, source) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── test — push rest as continuation, tail-call body ────────────────────── - defp do_execute([{:test, reg, then_body, else_body} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:test, reg, then_body, else_body} | rest], regs, upvalues, proto, state, cont, frames, line, steps) 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) + do_execute(body, regs, upvalues, proto, state, [rest | cont], frames, line, steps) end # ── 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 + defp do_execute([{:test_and, dest, source, rest_body} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do value = elem(regs, source) if Value.truthy?(value) do - do_execute(rest_body, regs, upvalues, proto, state, [rest | cont], frames, line) + do_execute(rest_body, regs, upvalues, proto, state, [rest | cont], frames, line, steps) else regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end end # ── 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 + defp do_execute([{:test_or, dest, source, rest_body} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do value = elem(regs, source) if Value.truthy?(value) do regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) else - do_execute(rest_body, regs, upvalues, proto, state, [rest | cont], frames, line) + do_execute(rest_body, regs, upvalues, proto, state, [rest | cont], frames, line, steps) end end @@ -961,11 +976,12 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + steps ) 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) + do_execute(cond_body, regs, upvalues, proto, state, [cond_check | loop_exit_cont], frames, line, steps) end # ── repeat_loop — CPS: body → condition → check → restart ──────────────── @@ -978,16 +994,17 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + steps ) 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) + do_execute(loop_body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line, steps) end # ── numeric_for — CPS ───────────────────────────────────────────────────── - defp do_execute([{:numeric_for, base, loop_var, body} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:numeric_for, base, loop_var, body} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do # Lua 5.3 §3.3.5: the three control values are coerced to numbers using # the same rules as arithmetic operators. If both initial value and step # are integers (after coercion), the loop is done with integers; if @@ -1017,15 +1034,15 @@ defmodule Lua.VM.Executor do 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) + do_execute(body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line, steps) else - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end end # ── generic_for — CPS ───────────────────────────────────────────────────── - defp do_execute([{:generic_for, base, var_regs, body} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:generic_for, base, var_regs, body} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do iter_func = elem(regs, base) invariant_state = elem(regs, base + 1) control = elem(regs, base + 2) @@ -1034,7 +1051,7 @@ defmodule Lua.VM.Executor do first_result = List.first(results) if first_result == nil do - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) else regs = put_elem(regs, base + 2, first_result) @@ -1050,13 +1067,13 @@ defmodule Lua.VM.Executor do 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) + do_execute(body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line, steps) end end # ── closure ──────────────────────────────────────────────────────────────── - defp do_execute([{:closure, dest, proto_index} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:closure, dest, proto_index} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do nested_proto = Enum.at(proto.prototypes, proto_index) {captured_upvalues_reversed, state} = @@ -1098,7 +1115,7 @@ defmodule Lua.VM.Executor do end regs = put_elem(regs, dest, closure) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── call — Lua closures via CPS frames; native functions inline ──────────── @@ -1111,7 +1128,8 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + steps ) do func_value = elem(regs, base) @@ -1139,11 +1157,13 @@ defmodule Lua.VM.Executor do # without going through this branch. args = collect_args(regs, base + 1, total_args) call_info = {proto.source, line, name_hint} + steps = steps + 1 + State.check_steps!(state, steps) State.check_call_depth!(state) state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1} {results, state} = Dispatcher.execute(callee_proto, args, callee_upvalues, state) state = %{state | call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} - continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, base, result_count) + continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, steps, base, result_count) {:lua_closure, callee_proto, callee_upvalues} -> param_count = callee_proto.param_count @@ -1182,6 +1202,8 @@ defmodule Lua.VM.Executor do call_info = {proto.source, line, name_hint} + steps = steps + 1 + State.check_steps!(state, steps) State.check_call_depth!(state) state = %{ @@ -1200,7 +1222,8 @@ defmodule Lua.VM.Executor do state, [], [frame | frames], - line + line, + steps ) {:native_func, fun} -> @@ -1235,7 +1258,7 @@ defmodule Lua.VM.Executor do restore_position(prev_pos) end - continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, base, result_count) + continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, steps, base, result_count) nil -> raise TypeError, @@ -1276,7 +1299,21 @@ defmodule Lua.VM.Executor do call_mm -> args = collect_args(regs, base + 1, total_args) {results, state} = call_function(call_mm, [other | args], state) - continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, base, result_count) + + continue_after_call( + results, + regs, + rest, + upvalues, + proto, + state, + cont, + frames, + line, + steps, + base, + result_count + ) end end end @@ -1284,7 +1321,7 @@ defmodule Lua.VM.Executor do # ── vararg ───────────────────────────────────────────────────────────────── - defp do_execute([{:vararg, base, count} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:vararg, base, count} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do varargs = Map.get(proto, :varargs, []) {regs, state} = @@ -1311,17 +1348,17 @@ defmodule Lua.VM.Executor do {regs, state} end - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── return_vararg ────────────────────────────────────────────────────────── - defp do_execute([{:return_vararg} | _rest], regs, _upvalues, proto, state, _cont, frames, line) do + defp do_execute([{:return_vararg} | _rest], regs, _upvalues, proto, state, _cont, frames, line, steps) do varargs = Map.get(proto, :varargs, []) case frames do [] -> {varargs, regs, state} - [frame | rest_frames] -> do_frame_return(varargs, regs, state, frame, rest_frames, line) + [frame | rest_frames] -> do_frame_return(varargs, regs, state, frame, rest_frames, line, steps) end end @@ -1335,14 +1372,15 @@ defmodule Lua.VM.Executor do state, _cont, frames, - line + line, + steps ) do total = fixed_count + state.multi_return_count results = if total > 0, do: for(i <- 0..(total - 1), do: elem(regs, base + i)), else: [] case frames do [] -> {results, regs, state} - [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line) + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, steps) end end @@ -1351,16 +1389,16 @@ defmodule Lua.VM.Executor do # Fast path: single-value return is by far the most common return shape # (every fib/factorial style recursion hits this every call). Avoid the # comprehension and Range allocation; just read one element. - defp do_execute([{:return, base, 1} | _rest], regs, _upvalues, _proto, state, _cont, frames, line) do + defp do_execute([{:return, base, 1} | _rest], regs, _upvalues, _proto, state, _cont, frames, line, steps) do results = [elem(regs, base)] case frames do [] -> {results, regs, state} - [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line) + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, steps) end end - defp do_execute([{:return, base, count} | _rest], regs, _upvalues, _proto, state, _cont, frames, line) do + defp do_execute([{:return, base, count} | _rest], regs, _upvalues, _proto, state, _cont, frames, line, steps) do results = cond do count == 0 -> @@ -1377,7 +1415,7 @@ defmodule Lua.VM.Executor do case frames do [] -> {results, regs, state} - [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line) + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, steps) end end @@ -1390,20 +1428,20 @@ defmodule Lua.VM.Executor do # for Lua 5.3 §3.4.1 wrap-around; mixed and float-only fall through to # native `+`/`-`/`*`. - defp do_execute([{:add, dest, a, b, _hint_a, _hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) + defp do_execute([{:add, dest, a, b, _hint_a, _hint_b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) when is_integer(:erlang.element(a + 1, regs)) and is_integer(:erlang.element(b + 1, regs)) do sum = :erlang.element(a + 1, regs) + :erlang.element(b + 1, regs) regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(sum)) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end - defp do_execute([{:add, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:add, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do val_a = elem(regs, a) val_b = elem(regs, b) if is_number(val_a) and is_number(val_b) do regs = put_elem(regs, dest, val_a + val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) else src = proto.source @@ -1413,24 +1451,44 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end end - defp do_execute([{:subtract, dest, a, b, _hint_a, _hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) + defp do_execute( + [{:subtract, dest, a, b, _hint_a, _hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + steps + ) when is_integer(:erlang.element(a + 1, regs)) and is_integer(:erlang.element(b + 1, regs)) do diff = :erlang.element(a + 1, regs) - :erlang.element(b + 1, regs) regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(diff)) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end - defp do_execute([{:subtract, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute( + [{:subtract, dest, a, b, hint_a, hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + steps + ) do val_a = elem(regs, a) val_b = elem(regs, b) if is_number(val_a) and is_number(val_b) do regs = put_elem(regs, dest, val_a - val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) else src = proto.source @@ -1440,24 +1498,44 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end end - defp do_execute([{:multiply, dest, a, b, _hint_a, _hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) + defp do_execute( + [{:multiply, dest, a, b, _hint_a, _hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + steps + ) when is_integer(:erlang.element(a + 1, regs)) and is_integer(:erlang.element(b + 1, regs)) do prod = :erlang.element(a + 1, regs) * :erlang.element(b + 1, regs) regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(prod)) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end - defp do_execute([{:multiply, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute( + [{:multiply, dest, a, b, hint_a, hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + steps + ) do val_a = elem(regs, a) val_b = elem(regs, b) if is_number(val_a) and is_number(val_b) do regs = put_elem(regs, dest, val_a * val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) else src = proto.source @@ -1467,11 +1545,11 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end end - defp do_execute([{:divide, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:divide, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1482,10 +1560,20 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end - defp do_execute([{:floor_divide, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute( + [{:floor_divide, dest, a, b, hint_a, hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + steps + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1496,10 +1584,10 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end - defp do_execute([{:modulo, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:modulo, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1510,10 +1598,10 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end - defp do_execute([{:power, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:power, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1524,12 +1612,12 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end # ── String concatenation ─────────────────────────────────────────────────── - defp do_execute([{:concatenate, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:concatenate, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do left = elem(regs, a) right = elem(regs, b) @@ -1541,13 +1629,13 @@ defmodule Lua.VM.Executor do cond do is_binary(left) and is_binary(right) -> regs = put_elem(regs, dest, concat_checked(left, right, state)) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) (is_binary(left) or is_number(left)) and (is_binary(right) or is_number(right)) -> src = proto.source result = concat_checked(concat_coerce(left, line, src, state), concat_coerce(right, line, src, state), state) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) true -> src = proto.source @@ -1558,13 +1646,23 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end end # ── Bitwise operations ───────────────────────────────────────────────────── - defp do_execute([{:bitwise_and, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute( + [{:bitwise_and, dest, a, b, hint_a, hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + steps + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1577,10 +1675,20 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end - defp do_execute([{:bitwise_or, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute( + [{:bitwise_or, dest, a, b, hint_a, hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + steps + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1593,10 +1701,20 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end - defp do_execute([{:bitwise_xor, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute( + [{:bitwise_xor, dest, a, b, hint_a, hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + steps + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1609,10 +1727,20 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end - defp do_execute([{:shift_left, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute( + [{:shift_left, dest, a, b, hint_a, hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + steps + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1623,10 +1751,20 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end - defp do_execute([{:shift_right, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute( + [{:shift_right, dest, a, b, hint_a, hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + steps + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1637,10 +1775,10 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end - defp do_execute([{:bitwise_not, dest, source, hint} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:bitwise_not, dest, source, hint} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do val = elem(regs, source) src = proto.source @@ -1650,7 +1788,7 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end # ── Comparison operations ────────────────────────────────────────────────── @@ -1658,40 +1796,40 @@ defmodule Lua.VM.Executor do # Comparison fast paths: number-vs-number and string-vs-string skip the # metamethod machinery — neither primitive type can carry __eq/__lt/__le. - defp do_execute([{:equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do val_a = elem(regs, a) val_b = elem(regs, b) cond do is_number(val_a) and is_number(val_b) -> regs = put_elem(regs, dest, val_a == val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) is_binary(val_a) and is_binary(val_b) -> regs = put_elem(regs, dest, val_a == val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) true -> {result, new_state} = try_equality_metamethod(val_a, val_b, state, fn -> lua_equal(val_a, val_b) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end end - defp do_execute([{:less_than, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:less_than, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do val_a = elem(regs, a) val_b = elem(regs, b) cond do is_number(val_a) and is_number(val_b) -> regs = put_elem(regs, dest, val_a < val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) is_binary(val_a) and is_binary(val_b) -> regs = put_elem(regs, dest, val_a < val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) true -> src = proto.source @@ -1700,43 +1838,43 @@ defmodule Lua.VM.Executor do try_binary_metamethod("__lt", val_a, val_b, state, fn -> safe_compare_lt(val_a, val_b, line, src, state) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end end - defp do_execute([{:less_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:less_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do val_a = elem(regs, a) val_b = elem(regs, b) cond do is_number(val_a) and is_number(val_b) -> regs = put_elem(regs, dest, val_a <= val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) is_binary(val_a) and is_binary(val_b) -> regs = put_elem(regs, dest, val_a <= val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) true -> {result, new_state} = compare_le(val_a, val_b, state, line, proto.source) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end end - defp do_execute([{:greater_than, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:greater_than, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do val_a = elem(regs, a) val_b = elem(regs, b) cond do is_number(val_a) and is_number(val_b) -> regs = put_elem(regs, dest, val_a > val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) is_binary(val_a) and is_binary(val_b) -> regs = put_elem(regs, dest, val_a > val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) true -> src = proto.source @@ -1746,71 +1884,71 @@ defmodule Lua.VM.Executor do try_binary_metamethod("__lt", val_b, val_a, state, fn -> safe_compare_lt(val_b, val_a, line, src, state) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end end - defp do_execute([{:greater_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:greater_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do val_a = elem(regs, a) val_b = elem(regs, b) cond do is_number(val_a) and is_number(val_b) -> regs = put_elem(regs, dest, val_a >= val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) is_binary(val_a) and is_binary(val_b) -> regs = put_elem(regs, dest, val_a >= val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) true -> # Lua 5.3 §3.4.4: a >= b is translated to b <= a. {result, new_state} = compare_le(val_b, val_a, state, line, proto.source) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end end - defp do_execute([{:not_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:not_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do val_a = elem(regs, a) val_b = elem(regs, b) cond do is_number(val_a) and is_number(val_b) -> regs = put_elem(regs, dest, val_a != val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) is_binary(val_a) and is_binary(val_b) -> regs = put_elem(regs, dest, val_a != val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) true -> {eq_result, new_state} = try_equality_metamethod(val_a, val_b, state, fn -> lua_equal(val_a, val_b) end) regs = put_elem(regs, dest, not eq_result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end end # ── Unary operations ─────────────────────────────────────────────────────── - defp do_execute([{:negate, dest, source, hint} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:negate, dest, source, hint} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do val = elem(regs, source) src = proto.source {result, new_state} = try_unary_metamethod("__unm", val, state, fn -> safe_negate(val, line, src, hint, state) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end - defp do_execute([{:not, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:not, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do result = not Value.truthy?(elem(regs, source)) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end - defp do_execute([{:length, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute([{:length, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do value = elem(regs, source) {result, new_state} = @@ -1832,15 +1970,25 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) end # ── new_table ────────────────────────────────────────────────────────────── - defp do_execute([{:new_table, dest, _array_hint, _hash_hint} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute( + [{:new_table, dest, _array_hint, _hash_hint} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + steps + ) do {tref, state} = State.alloc_table(state) regs = put_elem(regs, dest, tref) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── get_table ────────────────────────────────────────────────────────────── @@ -1853,7 +2001,8 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + steps ) do table_val = elem(regs, table_reg) key = elem(regs, key_reg) @@ -1868,17 +2017,17 @@ defmodule Lua.VM.Executor do case :erlang.map_get(:metatable, table) do nil -> regs = put_elem(regs, dest, nil) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) _ -> {value, state} = index_value(table_val, key, state, line, proto.source, name_hint) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end value -> regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end {:tref, id} when is_integer(key) or is_binary(key) -> @@ -1888,25 +2037,25 @@ defmodule Lua.VM.Executor do case :erlang.map_get(:data, table) do %{^key => value} -> regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) _data -> case :erlang.map_get(:metatable, table) do nil -> regs = put_elem(regs, dest, nil) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) _ -> {value, state} = index_value(table_val, key, state, line, proto.source, name_hint) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end end _ -> {value, state} = index_value(table_val, key, state, line, proto.source, name_hint) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end end @@ -1920,7 +2069,8 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + steps ) do table_val = elem(regs, table_reg) @@ -1929,7 +2079,7 @@ defmodule Lua.VM.Executor do key = elem(regs, key_reg) value = elem(regs, value_reg) state = table_newindex(table_val, key, value, state) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) _ -> raise_index_type_error(table_val, line, proto.source, name_hint, state) @@ -1946,7 +2096,8 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + steps ) do table_val = elem(regs, table_reg) @@ -1962,25 +2113,25 @@ defmodule Lua.VM.Executor do case :erlang.map_get(:data, table) do %{^name => value} -> regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) _data -> case :erlang.map_get(:metatable, table) do nil -> regs = put_elem(regs, dest, nil) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) _ -> {value, state} = index_value(table_val, name, state, line, proto.source, name_hint) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end end _ -> {value, state} = index_value(table_val, name, state, line, proto.source, name_hint) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end end @@ -1994,7 +2145,8 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + steps ) do table_val = elem(regs, table_reg) @@ -2002,7 +2154,7 @@ defmodule Lua.VM.Executor do {:tref, _} -> value = elem(regs, value_reg) state = table_newindex(table_val, name, value, state) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) _ -> raise_index_type_error(table_val, line, proto.source, name_hint, state) @@ -2019,7 +2171,8 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + steps ) do {:tref, id} = elem(regs, table_reg) total = init_count + state.multi_return_count @@ -2029,12 +2182,22 @@ defmodule Lua.VM.Executor do Table.put_many(table, set_list_pairs(regs, start, total, offset)) end) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── set_list ─────────────────────────────────────────────────────────────── - defp do_execute([{:set_list, table_reg, start, count, offset} | rest], regs, upvalues, proto, state, cont, frames, line) do + defp do_execute( + [{:set_list, table_reg, start, count, offset} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + steps + ) do {:tref, id} = elem(regs, table_reg) total = if count == 0, do: state.multi_return_count, else: count @@ -2043,7 +2206,7 @@ defmodule Lua.VM.Executor do Table.put_many(table, set_list_pairs(regs, start, total, offset)) end) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── self ─────────────────────────────────────────────────────────────────── @@ -2056,19 +2219,20 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + steps ) do obj = elem(regs, obj_reg) {func, state} = index_value(obj, method_name, state, line, proto.source, name_hint) regs = put_elem(regs, base + 1, obj) regs = put_elem(regs, base, func) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end # ── Catch-all for unimplemented instructions ─────────────────────────────── - defp do_execute([instr | _rest], _regs, _upvalues, _proto, _state, _cont, _frames, _line) do + defp do_execute([instr | _rest], _regs, _upvalues, _proto, _state, _cont, _frames, _line, _steps) do raise InternalError, value: "unimplemented instruction: #{inspect(instr)}" end @@ -2090,7 +2254,7 @@ defmodule Lua.VM.Executor do # ── do_frame_return — restore caller context after a Lua function returns ── - defp do_frame_return(results, _callee_regs, state, frame, rest_frames, line) do + defp do_frame_return(results, _callee_regs, state, frame, rest_frames, line, steps) do %{ rest: rest, cont: caller_cont, @@ -2117,7 +2281,7 @@ defmodule Lua.VM.Executor do {results, caller_regs, state} [outer_frame | outer_rest_frames] -> - do_frame_return(results, caller_regs, state, outer_frame, outer_rest_frames, line) + do_frame_return(results, caller_regs, state, outer_frame, outer_rest_frames, line, steps) end -2 -> @@ -2128,11 +2292,11 @@ defmodule Lua.VM.Executor do caller_regs = write_list_to_regs(caller_regs, base, results_list) state = %{state | multi_return_count: count} - do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line) + do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line, steps) 0 -> # No results captured - do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line) + do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line, steps) 1 -> # Fast path: single-result return (the overwhelmingly common case, @@ -2148,7 +2312,7 @@ defmodule Lua.VM.Executor do caller_regs = ensure_regs_capacity(caller_regs, base + 1) caller_regs = :erlang.setelement(base + 1, caller_regs, first) - do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line) + do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line, steps) n when n > 0 -> # Fixed count: place first n results into caller regs from base @@ -2156,19 +2320,19 @@ defmodule Lua.VM.Executor do caller_regs = ensure_regs_capacity(caller_regs, base + n) caller_regs = write_list_to_regs_n(caller_regs, base, results_list, n) - do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line) + do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line, steps) 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 + defp continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, steps, 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) + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, steps) end -2 -> @@ -2178,10 +2342,10 @@ defmodule Lua.VM.Executor do regs = write_list_to_regs(regs, base, results_list) state = %{state | multi_return_count: count} - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) 0 -> - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) 1 -> first = @@ -2193,14 +2357,14 @@ defmodule Lua.VM.Executor do regs = ensure_regs_capacity(regs, base + 1) regs = :erlang.setelement(base + 1, regs, first) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) n when n > 0 -> results_list = List.wrap(results) regs = ensure_regs_capacity(regs, base + n) regs = write_list_to_regs_n(regs, base, results_list, n) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) end end @@ -2261,7 +2425,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, [], [], 0) + do_execute(callee_proto.instructions, callee_regs, callee_upvalues, callee_proto, state, [], [], 0, 0) state = %{state | open_upvalues: saved_open_upvalues} {results, state} diff --git a/lib/lua/vm/state.ex b/lib/lua/vm/state.ex index 74f416ad..68a9ec04 100644 --- a/lib/lua/vm/state.ex +++ b/lib/lua/vm/state.ex @@ -4,6 +4,7 @@ defmodule Lua.VM.State do """ alias Lua.VM.Limits + alias Lua.VM.RuntimeError alias Lua.VM.Table defstruct call_stack: [], @@ -20,6 +21,13 @@ defmodule Lua.VM.State do # cap so an allocation bomb is refused deterministically instead # of racing the GC-time heap check. max_string_bytes: Limits.max_string_bytes(), + # `max_steps` is the configured instruction ceiling; `:infinity` + # (the default) means no limit. The running tally is NOT stored + # here — it is threaded as a parameter through the executor / + # dispatcher loops, mirroring the `line`-off-State discipline so + # the default `:infinity` path carries no per-instruction struct + # rebuild. See `check_steps!/2`. + max_steps: :infinity, metatables: %{}, upvalue_cells: %{}, open_upvalues: %{}, @@ -40,6 +48,7 @@ defmodule Lua.VM.State do call_depth: non_neg_integer(), max_call_depth: pos_integer() | :infinity, max_string_bytes: pos_integer(), + max_steps: pos_integer() | :infinity, metatables: map(), upvalue_cells: map(), tables: %{optional(non_neg_integer()) => Table.t()}, @@ -79,7 +88,34 @@ defmodule Lua.VM.State do def check_call_depth!(%__MODULE__{call_depth: depth, max_call_depth: max}) when depth < max, do: :ok def check_call_depth!(%__MODULE__{call_stack: call_stack} = state) do - raise Lua.VM.RuntimeError, value: "stack overflow", call_stack: call_stack, state: state + raise RuntimeError, value: "stack overflow", call_stack: call_stack, state: state + end + + @doc """ + Guards against unbounded CPU work within a single evaluation. + + Raises a catchable Lua `"instruction budget exceeded"` runtime error when + the running instruction tally `steps` has reached `max_steps`. Call it at + loop back-edges and call boundaries — never per opcode — so the default + `:infinity` path stays free of per-instruction cost. + + The tally is threaded as a parameter, not stored in `%State{}`. No-op when + the tally is under the limit or when `max_steps` is `:infinity` (the + default). The clauses are ordered so both common cases resolve in a single + function-head match with no struct rebuild. + + Raises the same `Lua.VM.RuntimeError` used by `"stack overflow"`, carrying + the raise-time `state:` so `pcall`/`xpcall` recover heap effects for free. + """ + @spec check_steps!(t(), non_neg_integer()) :: :ok + def check_steps!(%__MODULE__{max_steps: :infinity}, _steps), do: :ok + def check_steps!(%__MODULE__{max_steps: max}, steps) when steps < max, do: :ok + + def check_steps!(%__MODULE__{call_stack: call_stack} = state, _steps) do + raise RuntimeError, + value: "instruction budget exceeded", + call_stack: call_stack, + state: state end @doc """ diff --git a/test/lua/vm/max_steps_test.exs b/test/lua/vm/max_steps_test.exs new file mode 100644 index 00000000..8f678d9e --- /dev/null +++ b/test/lua/vm/max_steps_test.exs @@ -0,0 +1,133 @@ +defmodule Lua.VM.MaxStepsTest do + @moduledoc """ + Pins the `:max_steps` instruction budget: a finite budget aborts a + non-terminating script with a catchable `"instruction budget exceeded"` + runtime error, the budget is recoverable via `pcall`, it bounds both the + interpreter and the compiled-dispatcher path, the budget is fresh per + top-level evaluation (no cross-eval leak), and `:infinity` (the default) + imposes no bound. + """ + use ExUnit.Case, async: true + + alias Lua.Compiler + alias Lua.Parser + alias Lua.RuntimeException + alias Lua.VM.Dispatcher + alias Lua.VM.State + alias Lua.VM.Stdlib + + defp eval!(lua, code), do: Lua.eval!(lua, code) + + describe ":max_steps enforcement (interpreter path)" do + test "a finite budget aborts a non-terminating while loop" do + lua = Lua.new(max_steps: 1000) + + assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> + eval!(lua, "while true do end") + end + end + + test "a finite budget aborts a tight numeric-for loop" do + lua = Lua.new(max_steps: 1000) + + assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> + eval!(lua, "local s = 0 for i = 1, 1000000000 do s = s + i end return s") + end + end + + test "unbounded recursion is bounded by step counting at call boundaries" do + # A finite budget far below the depth a deep recursion would reach. The + # call-boundary increment trips before the recursion exhausts itself, + # and the message is the budget error — distinct from the + # `:max_call_depth` "stack overflow". + lua = Lua.new(max_steps: 100) + + assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> + eval!(lua, "local function f() return f() end f()") + end + end + end + + describe ":max_steps catchability and recovery" do + test "pcall catches the budget error and the VM keeps working afterward" do + lua = Lua.new(max_steps: 1000) + + {[false, msg], lua} = + eval!(lua, "return pcall(function() while true do end end)") + + assert msg =~ "instruction budget exceeded" + + # The VM is healthy after a caught budget error. + assert {[2], _lua} = eval!(lua, "return 1 + 1") + end + end + + describe "budget scoping" do + test "a loop under the budget returns normally" do + lua = Lua.new(max_steps: 10_000) + + assert {[5050], _lua} = + eval!(lua, "local s = 0 for i = 1, 100 do s = s + i end return s") + end + + test "the budget is fresh per evaluation (no cross-eval leak)" do + lua = Lua.new(max_steps: 5000) + + # First eval consumes ~100 iterations of budget. + {[5050], lua} = eval!(lua, "local s = 0 for i = 1, 100 do s = s + i end return s") + + # A second eval on the same state gets a fresh budget — it must not + # see the first eval's tally carried over. + assert {[5050], _lua} = + eval!(lua, "local s = 0 for i = 1, 100 do s = s + i end return s") + end + end + + describe "default behavior (:infinity)" do + test "a long-but-terminating loop completes with no bound" do + assert {[500_500], _lua} = + Lua.eval!(Lua.new(), "local s = 0 for i = 1, 1000 do s = s + i end return s") + end + end + + describe "compiled-dispatcher path" do + test "a finite budget bounds an infinite loop inside a compiled function" do + # A `function` body compiles to a `:compiled_closure`; calling it routes + # the loop through `Lua.VM.Dispatcher`, exercising the dispatcher's + # back-edge counting rather than the interpreter's. + lua = Lua.new(max_steps: 1000) + + assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> + eval!(lua, "local function spin() while true do end end spin()") + end + end + + test "the dispatcher enforces the budget when driven directly" do + {:ok, ast} = Parser.parse("local function spin() while true do end end return spin") + {:ok, proto} = Compiler.compile(ast, source: "test.lua") + state = %{Stdlib.install(State.new()) | max_steps: 1000} + + # Run the chunk to obtain the compiled closure it returns, then drive + # the dispatcher with the closure's prototype directly. + {:ok, [closure], state} = Lua.VM.execute(proto, state) + {:compiled_closure, callee_proto, upvalues} = closure + + assert_raise Lua.VM.RuntimeError, ~r/instruction budget exceeded/, fn -> + Dispatcher.execute(callee_proto, [], upvalues, state) + end + end + end + + describe ":max_steps validation" do + test "rejects non-positive integers and non-integers" do + assert_raise ArgumentError, ~r/:max_steps/, fn -> Lua.new(max_steps: 0) end + assert_raise ArgumentError, ~r/:max_steps/, fn -> Lua.new(max_steps: -1) end + assert_raise ArgumentError, ~r/:max_steps/, fn -> Lua.new(max_steps: :nope) end + end + + test "accepts :infinity and positive integers" do + assert %Lua{} = Lua.new(max_steps: :infinity) + assert %Lua{} = Lua.new(max_steps: 1) + end + end +end From cfc0db609b9ae80b24ad2aa0fbf1368041becadb Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Mon, 1 Jun 2026 10:19:21 -0700 Subject: [PATCH 3/9] chore(B17): mark plan as review --- .agents/plans/B17-vm-max-steps.md | 52 +++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/.agents/plans/B17-vm-max-steps.md b/.agents/plans/B17-vm-max-steps.md index 44c731b1..a68352e8 100644 --- a/.agents/plans/B17-vm-max-steps.md +++ b/.agents/plans/B17-vm-max-steps.md @@ -2,10 +2,10 @@ id: B17 title: "VM instruction budget: configurable :max_steps with catchable exhaustion" issue: 306 -pr: null +pr: 320 branch: feat/vm-max-steps base: main -status: in-progress +status: review direction: B unlocks: - deterministic CPU bound for library consumers calling Lua.eval!/2 without a host Task + timeout wrapper @@ -188,3 +188,51 @@ mix test --only lua53 - **Error not catchable.** Mitigation: reuse `Lua.VM.RuntimeError`. - **Plan-id leakage into source/tests.** Mitigation: id stays in the commit body and PR description only. + +## What changed + +PR #320. + +Files touched: + +- `lib/lua.ex` — `:max_steps` added to `new/1` defaults, validated via + `validate_max_steps!/1`, threaded into the seeded state; `## Options` + moduledoc bullet + doctest. +- `lib/lua/vm/state.ex` — `max_steps` field on `defstruct` and `@type t`; + `check_steps!/2` guard raising a catchable `Lua.VM.RuntimeError` + (`"instruction budget exceeded"`). +- `lib/lua/vm/executor.ex` — `steps` threaded as a 9th parameter through + `do_execute`, `do_frame_return`, and `continue_after_call`; increment + + `check_steps!/2` at the four loop back-edges and the two call boundaries. +- `lib/lua/vm/dispatcher.ex` — `steps` threaded through `dispatch`, + `finish_body`, `apply_multi_call_result`, `return_one`, `return_multi`; + increment + `check_steps!/2` at the loop back-edges and the six call + boundaries. +- `test/lua/vm/max_steps_test.exs` — new: enforcement on both paths, + catchability via `pcall`, no cross-eval leak, `:infinity` default, + validation. +- `guides/examples/sandboxing.livemd` — "Bounding CPU work" section + documenting `:max_call_depth` and `:max_steps`. + +Test delta: `mix test` 2114 → 2126 passed (11 new cases + 1 new doctest), +19 skipped, 1 excluded. `mix test --only lua53` unchanged (17 passed, +12 skipped). + +Discoveries / deviation from the original plan: + +- The cross-module `Executor ↔ Dispatcher` hand-off seeds the callee with a + fresh budget rather than threading `steps` through `Dispatcher.execute/4`'s + return shape. Changing that return shape would have rippled into + out-of-scope stdlib modules (`stdlib.ex`, `table.ex`, `string.ex`, + `vm.ex`). Each compiled callee is bounded by the dispatcher's own + back-edge/call-boundary counting, and runaway recursion that stays in the + interpreter is bounded by the threaded interpreter tally — so both + runaway-loop and runaway-recursion criteria still hold within the named + in-scope files. +- The benchmark gate could not run in the execution sandbox: + `MIX_ENV=benchmark` pulls in `luaport`, whose native build needs C Lua / + LuaJIT headers that are not installed (`fatal error: 'lua.h' file not + found`). The default `:infinity` path is zero-cost by construction (one + integer increment + one short-circuiting function-head match per back-edge + / call boundary, never per opcode), structurally identical to the existing + `check_call_depth!/1`. From f5717f42e0bd609a9342268cba59a9ddec04086d Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Mon, 1 Jun 2026 10:32:47 -0700 Subject: [PATCH 4/9] feat(vm): address review feedback Make the :max_steps instruction budget durable across Executor<->Dispatcher engine hand-offs so recursion that alternates execution engines is bounded rather than resetting its budget at each boundary. The running tally now rides through a `steps` field on %State{} at engine boundaries only (where the struct is already rebuilt to push a call frame), never per opcode: the crossing engine writes its threaded tally into state.steps and the entered engine seeds from it, stamping the final tally back at its terminal. This closes the gap between max_call_depth: :infinity and a deterministic CPU bound for a compiled/interpreted mutually-recursive pair with no loop on either side. Adds regression coverage in test/lua/vm/max_steps_test.exs: a goto-bearing interpreted closure and a plain compiled closure in unbounded mutual recursion trip the budget, plus a guard asserting the pair is genuinely split across both engines. Plan: B17 --- .agents/plans/B17-vm-max-steps.md | 73 ++++++++++++++++++++----------- lib/lua/vm/dispatcher.ex | 45 +++++++++++++++---- lib/lua/vm/executor.ex | 51 ++++++++++++++++----- lib/lua/vm/state.ex | 17 +++++-- test/lua/vm/max_steps_test.exs | 53 ++++++++++++++++++++++ 5 files changed, 191 insertions(+), 48 deletions(-) diff --git a/.agents/plans/B17-vm-max-steps.md b/.agents/plans/B17-vm-max-steps.md index a68352e8..308c92ad 100644 --- a/.agents/plans/B17-vm-max-steps.md +++ b/.agents/plans/B17-vm-max-steps.md @@ -133,11 +133,13 @@ Mirror `:max_call_depth` everywhere it appears. `:cps_generic_for` continue, all in the `do_execute([], ...)` cont dispatcher) and at the two `State.check_call_depth!` call boundaries. - The cross-module `:compiled_closure` / `Dispatcher.execute` and - `call_value` hand-offs seed the callee with a fresh budget rather than - changing the `{results, state}` return shape (changing it would ripple - into out-of-scope stdlib modules). Each compiled callee is bounded by - the dispatcher's own counting; runaway recursion that stays in the - interpreter is bounded by the threaded interpreter tally. + `call_value` hand-offs carry the tally through a `steps` field on + `%State{}` rather than changing the `{results, state}` return shape + (changing it would ripple into out-of-scope stdlib modules). The crossing + engine writes its threaded tally into `state.steps` at the boundary — only + where the struct is already rebuilt to push a call frame, never per opcode + — and the entered engine seeds from it and stamps the final tally back, so + the budget spans recursion that alternates execution engines. ### 4. Compiled dispatcher — `lib/lua/vm/dispatcher.ex` @@ -200,39 +202,58 @@ Files touched: moduledoc bullet + doctest. - `lib/lua/vm/state.ex` — `max_steps` field on `defstruct` and `@type t`; `check_steps!/2` guard raising a catchable `Lua.VM.RuntimeError` - (`"instruction budget exceeded"`). + (`"instruction budget exceeded"`); a `steps` field that carries the tally + across engine boundaries (never written per opcode). - `lib/lua/vm/executor.ex` — `steps` threaded as a 9th parameter through `do_execute`, `do_frame_return`, and `continue_after_call`; increment + - `check_steps!/2` at the four loop back-edges and the two call boundaries. + `check_steps!/2` at the four loop back-edges and the two call boundaries; + seeds its tally from `state.steps` at the interpreter entry points and + stamps the final tally back via `finish_steps/2` at evaluation terminals + so the budget survives `Executor ↔ Dispatcher` hand-offs. - `lib/lua/vm/dispatcher.ex` — `steps` threaded through `dispatch`, `finish_body`, `apply_multi_call_result`, `return_one`, `return_multi`; increment + `check_steps!/2` at the loop back-edges and the six call - boundaries. + boundaries; seeds from / writes back `state.steps` at the dispatcher entry, + terminals, and `Executor` bridges so the budget is continuous across the + boundary. - `test/lua/vm/max_steps_test.exs` — new: enforcement on both paths, catchability via `pcall`, no cross-eval leak, `:infinity` default, - validation. + validation, and cross-engine mutual recursion (budget spans an + interpreted/compiled alternating call chain, plus a guard asserting the + pair is genuinely split across both engines). - `guides/examples/sandboxing.livemd` — "Bounding CPU work" section documenting `:max_call_depth` and `:max_steps`. -Test delta: `mix test` 2114 → 2126 passed (11 new cases + 1 new doctest), +Test delta: `mix test` 2114 → 2128 passed (13 new cases + 1 new doctest), 19 skipped, 1 excluded. `mix test --only lua53` unchanged (17 passed, 12 skipped). Discoveries / deviation from the original plan: -- The cross-module `Executor ↔ Dispatcher` hand-off seeds the callee with a - fresh budget rather than threading `steps` through `Dispatcher.execute/4`'s - return shape. Changing that return shape would have rippled into - out-of-scope stdlib modules (`stdlib.ex`, `table.ex`, `string.ex`, - `vm.ex`). Each compiled callee is bounded by the dispatcher's own - back-edge/call-boundary counting, and runaway recursion that stays in the - interpreter is bounded by the threaded interpreter tally — so both - runaway-loop and runaway-recursion criteria still hold within the named - in-scope files. -- The benchmark gate could not run in the execution sandbox: - `MIX_ENV=benchmark` pulls in `luaport`, whose native build needs C Lua / - LuaJIT headers that are not installed (`fatal error: 'lua.h' file not - found`). The default `:infinity` path is zero-cost by construction (one - integer increment + one short-circuiting function-head match per back-edge - / call boundary, never per opcode), structurally identical to the existing - `check_call_depth!/1`. +- The cross-module `Executor ↔ Dispatcher` hand-off carries the running + tally through a `steps` field on `%State{}` rather than changing + `Dispatcher.execute/4`'s `{results, state}` return shape (which would have + rippled into out-of-scope stdlib modules). The crossing engine writes its + threaded tally into `state.steps` at the boundary — where the struct is + already rebuilt to push a call frame, so the default `:infinity` path adds + no per-opcode cost — and the entered engine seeds its own threaded tally + from `state.steps`, stamping the final tally back at its terminal. The + budget therefore spans a call chain that alternates execution engines + (e.g. a `goto`-bearing interpreted closure and a plain compiled closure in + unbounded mutual recursion) instead of resetting at each boundary, closing + the gap a fresh-budget reseed would have left open between + `max_call_depth: :infinity` and a deterministic CPU bound. Regression + coverage: `test/lua/vm/max_steps_test.exs` "cross-engine mutual recursion". +- The benchmark gate could not run: the benchee harness only loads under + `MIX_ENV=benchmark`, which pulls in `luaport`, whose native build needs a + matching C Lua toolchain (the comparison baselines are PUC-Lua / Luaport, + not just the internal engines). The default `:infinity` path remains + zero-cost by construction: one integer increment + one short-circuiting + `check_steps!/2` head-match per back-edge / call boundary, never per + opcode, structurally identical to the existing `check_call_depth!/1`. The + cross-boundary fix adds only a `steps:` field assignment inside the + `%State{}` rebuild that each engine hand-off already performs to push a + call frame (`call_stack` / `call_depth`), so it costs nothing on + straight-line code and nothing extra beyond the already-present boundary + struct rebuild. Still owed: an actual run in CI or on a host with a + compatible C Lua before merge. diff --git a/lib/lua/vm/dispatcher.ex b/lib/lua/vm/dispatcher.ex index e9618285..69319dd4 100644 --- a/lib/lua/vm/dispatcher.ex +++ b/lib/lua/vm/dispatcher.ex @@ -159,7 +159,11 @@ defmodule Lua.VM.Dispatcher do try do state = %{state | open_upvalues: %{}} - {results, state} = dispatch(proto.bytecode, 1, regs, upvalues, proto, state, [], [], 0) + # Seed the dispatcher tally from the budget carried across the boundary + # so an alternating-engine call chain accumulates against one budget + # instead of resetting here; the terminals stamp the final tally back + # into `state.steps`. + {results, state} = dispatch(proto.bytecode, 1, regs, upvalues, proto, state, [], [], state.steps) state = %{state | open_upvalues: saved_open} {results, state} @@ -713,17 +717,22 @@ defmodule Lua.VM.Dispatcher do steps = steps + 1 State.check_steps!(state, steps) State.check_call_depth!(state) - state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1} + state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1, steps: steps} {_results, state} = Executor.call_function(closure, args, state) + steps = state.steps state = %{state | call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) _ -> args = collect_args(regs, base + 1, arg_count) + state = %{state | steps: steps} + {_results, state} = Executor.dispatcher_call_function(func_value, args, state, proto, name_hint, line) + steps = state.steps + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) end @@ -771,8 +780,9 @@ defmodule Lua.VM.Dispatcher do steps = steps + 1 State.check_steps!(state, steps) State.check_call_depth!(state) - state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1} + state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1, steps: steps} {results, state} = Executor.call_function(closure, args, state) + steps = state.steps state = %{state | call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} first = @@ -787,9 +797,13 @@ defmodule Lua.VM.Dispatcher do _ -> args = collect_args(regs, base + 1, arg_count) + state = %{state | steps: steps} + {results, state} = Executor.dispatcher_call_function(func_value, args, state, proto, name_hint, line) + steps = state.steps + first = case results do [v | _] -> v @@ -1021,9 +1035,13 @@ defmodule Lua.VM.Dispatcher do invariant_state = :erlang.element(base + 2, regs) control = :erlang.element(base + 3, regs) + state = %{state | steps: steps} + {results, state} = Executor.dispatcher_call_value(iter_func, [invariant_state, control], proto, state, line) + steps = state.steps + case results do [nil | _] -> dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) @@ -1242,8 +1260,9 @@ defmodule Lua.VM.Dispatcher do steps = steps + 1 State.check_steps!(state, steps) State.check_call_depth!(state) - state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1} + state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1, steps: steps} {results, state} = Executor.call_function(closure, args, state) + steps = state.steps state = %{state | call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} apply_multi_call_result( @@ -1264,9 +1283,13 @@ defmodule Lua.VM.Dispatcher do _ -> args = collect_args(regs, base + 1, total_args) + state = %{state | steps: steps} + {results, state} = Executor.dispatcher_call_function(func_value, args, state, proto, name_hint, line) + steps = state.steps + apply_multi_call_result( result_count, base, @@ -1467,9 +1490,13 @@ defmodule Lua.VM.Dispatcher do invariant_state = :erlang.element(base + 2, regs) control = :erlang.element(base + 3, regs) + state = %{state | steps: steps} + {results, state} = Executor.dispatcher_call_value(iter_func, [invariant_state, control], proto, state, line) + steps = state.steps + case results do [nil | _] -> dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, steps) @@ -1521,8 +1548,10 @@ defmodule Lua.VM.Dispatcher do # {:multi, B, -2} → expand all into regs[B..], set multi_return_count. # {:multi, B, n>1} → write n results into regs[B..], pad nil. - defp return_one(value, state, [], _steps) do - {[value], state} + defp return_one(value, state, [], steps) do + # Top of this dispatcher sub-evaluation: stamp the tally back into the + # state so a caller in the other engine can resume the same budget. + {[value], %{state | steps: steps}} end defp return_one(value, state, [frame | rest_frames], steps) do @@ -1565,8 +1594,8 @@ defmodule Lua.VM.Dispatcher do # `:return_proto_varargs`, and the non-compiled-callee branch of # `:call_multi`. Mirrors `return_one/3`'s frame-variant handling. - defp return_multi(results, state, [], _steps) do - {results, state} + defp return_multi(results, state, [], steps) do + {results, %{state | steps: steps}} end defp return_multi(results, state, [frame | rest_frames], steps) do diff --git a/lib/lua/vm/executor.ex b/lib/lua/vm/executor.ex index f4427e3d..fcab8175 100644 --- a/lib/lua/vm/executor.ex +++ b/lib/lua/vm/executor.ex @@ -91,7 +91,7 @@ defmodule Lua.VM.Executor do state = %{state | open_upvalues: %{}} {results, regs, state} = - do_execute(instructions, registers, upvalues, proto, state, [], [], 0, 0) + do_execute(instructions, registers, upvalues, proto, state, [], [], 0, state.steps) {results, regs, %{state | open_upvalues: saved_open_upvalues}} rescue @@ -158,8 +158,12 @@ defmodule Lua.VM.Executor do try do state = %{state | open_upvalues: %{}} + # Seed the interpreter tally from the budget carried across the boundary + # so an alternating-engine call chain accumulates against one budget + # rather than resetting here. The terminal writes the final tally back + # into `state.steps` (see `finish_steps/2`). {results, _callee_regs, state} = - do_execute(callee_proto.instructions, callee_regs, callee_upvalues, callee_proto, state, [], [], 0, 0) + do_execute(callee_proto.instructions, callee_regs, callee_upvalues, callee_proto, state, [], [], 0, state.steps) state = %{state | open_upvalues: saved_open_upvalues} {results, state} @@ -764,7 +768,9 @@ defmodule Lua.VM.Executor do invariant_state = elem(regs, base + 1) control = elem(regs, base + 2) + state = %{state | steps: steps} {results, state} = call_value(iter_func, [invariant_state, control], proto, state, line) + steps = state.steps first_result = List.first(results) if first_result == nil do @@ -791,7 +797,7 @@ defmodule Lua.VM.Executor do [] -> case frames do [] -> - {[], regs, state} + {[], regs, finish_steps(state, steps)} [frame | rest_frames] -> do_frame_return([], regs, state, frame, rest_frames, line, steps) @@ -1047,7 +1053,9 @@ defmodule Lua.VM.Executor do invariant_state = elem(regs, base + 1) control = elem(regs, base + 2) + state = %{state | steps: steps} {results, state} = call_value(iter_func, [invariant_state, control], proto, state, line) + steps = state.steps first_result = List.first(results) if first_result == nil do @@ -1160,8 +1168,12 @@ defmodule Lua.VM.Executor do steps = steps + 1 State.check_steps!(state, steps) State.check_call_depth!(state) - state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1} + # Carry the tally into the dispatcher so the budget spans the + # boundary; `Dispatcher.execute/4` seeds from `state.steps` and + # writes its final tally back there. + state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1, steps: steps} {results, state} = Dispatcher.execute(callee_proto, args, callee_upvalues, state) + steps = state.steps state = %{state | call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, steps, base, result_count) @@ -1239,6 +1251,11 @@ defmodule Lua.VM.Executor do prev_pos = Process.get(@position_key, @unset) set_position(line, proto.source) + # Carry the tally into the native callback so a callback that + # re-enters Lua (pcall, a sort comparator, a gsub function) keeps + # accumulating against the same budget instead of restarting it. + state = %{state | steps: steps} + {results, state} = try do case fun.(args, state) do @@ -1258,6 +1275,8 @@ defmodule Lua.VM.Executor do restore_position(prev_pos) end + steps = state.steps + continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, steps, base, result_count) nil -> @@ -1298,7 +1317,9 @@ defmodule Lua.VM.Executor do call_mm -> args = collect_args(regs, base + 1, total_args) + state = %{state | steps: steps} {results, state} = call_function(call_mm, [other | args], state) + steps = state.steps continue_after_call( results, @@ -1379,7 +1400,7 @@ defmodule Lua.VM.Executor do results = if total > 0, do: for(i <- 0..(total - 1), do: elem(regs, base + i)), else: [] case frames do - [] -> {results, regs, state} + [] -> {results, regs, finish_steps(state, steps)} [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, steps) end end @@ -1393,7 +1414,7 @@ defmodule Lua.VM.Executor do results = [elem(regs, base)] case frames do - [] -> {results, regs, state} + [] -> {results, regs, finish_steps(state, steps)} [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, steps) end end @@ -1414,7 +1435,7 @@ defmodule Lua.VM.Executor do end case frames do - [] -> {results, regs, state} + [] -> {results, regs, finish_steps(state, steps)} [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, steps) end end @@ -2254,6 +2275,15 @@ defmodule Lua.VM.Executor do # ── do_frame_return — restore caller context after a Lua function returns ── + # Stamp the running tally into the state at a top-of-evaluation terminal + # so the entry wrapper (`call_function/3`, `call_value/5`, `execute/5`) + # can carry it back across an engine boundary. Only ever fired when the + # frame stack is empty — i.e. the interpreter sub-evaluation as a whole + # is unwinding — never per intra-evaluation return, so the default + # `:infinity` path pays only at boundaries, exactly like the call-frame + # bookkeeping it sits beside. + defp finish_steps(state, steps), do: %{state | steps: steps} + defp do_frame_return(results, _callee_regs, state, frame, rest_frames, line, steps) do %{ rest: rest, @@ -2278,7 +2308,7 @@ defmodule Lua.VM.Executor do # Return-position call (return f()): pass results to the caller's caller case rest_frames do [] -> - {results, caller_regs, state} + {results, caller_regs, finish_steps(state, steps)} [outer_frame | outer_rest_frames] -> do_frame_return(results, caller_regs, state, outer_frame, outer_rest_frames, line, steps) @@ -2331,7 +2361,7 @@ defmodule Lua.VM.Executor do -1 -> # Results from this native call become the return from the current function case frames do - [] -> {results, regs, state} + [] -> {results, regs, finish_steps(state, steps)} [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, steps) end @@ -2424,8 +2454,9 @@ defmodule Lua.VM.Executor do try do state = %{state | open_upvalues: %{}} + # Seed/recover the cross-boundary budget tally (see `call_function/3`). {results, _callee_regs, state} = - do_execute(callee_proto.instructions, callee_regs, callee_upvalues, callee_proto, state, [], [], 0, 0) + do_execute(callee_proto.instructions, callee_regs, callee_upvalues, callee_proto, state, [], [], 0, state.steps) state = %{state | open_upvalues: saved_open_upvalues} {results, state} diff --git a/lib/lua/vm/state.ex b/lib/lua/vm/state.ex index 68a9ec04..e78386e1 100644 --- a/lib/lua/vm/state.ex +++ b/lib/lua/vm/state.ex @@ -23,11 +23,19 @@ defmodule Lua.VM.State do max_string_bytes: Limits.max_string_bytes(), # `max_steps` is the configured instruction ceiling; `:infinity` # (the default) means no limit. The running tally is NOT stored - # here — it is threaded as a parameter through the executor / - # dispatcher loops, mirroring the `line`-off-State discipline so - # the default `:infinity` path carries no per-instruction struct - # rebuild. See `check_steps!/2`. + # here on the per-opcode hot path — it is threaded as a parameter + # through the executor / dispatcher loops, mirroring the + # `line`-off-State discipline so the default `:infinity` path + # carries no per-instruction struct rebuild. See `check_steps!/2`. max_steps: :infinity, + # `steps` carries the running tally ACROSS engine boundaries + # only. The interpreter and dispatcher each thread their tally as + # a loop parameter; at an `Executor`↔`Dispatcher` hand-off (where + # the struct is already rebuilt to push a call frame) the crossing + # engine writes its tally here and the entered engine seeds from + # it, so the budget spans a chain that alternates engines instead + # of resetting at each boundary. Never written per opcode. + steps: 0, metatables: %{}, upvalue_cells: %{}, open_upvalues: %{}, @@ -49,6 +57,7 @@ defmodule Lua.VM.State do max_call_depth: pos_integer() | :infinity, max_string_bytes: pos_integer(), max_steps: pos_integer() | :infinity, + steps: non_neg_integer(), metatables: map(), upvalue_cells: map(), tables: %{optional(non_neg_integer()) => Table.t()}, diff --git a/test/lua/vm/max_steps_test.exs b/test/lua/vm/max_steps_test.exs index 8f678d9e..1e3ab0c5 100644 --- a/test/lua/vm/max_steps_test.exs +++ b/test/lua/vm/max_steps_test.exs @@ -118,6 +118,59 @@ defmodule Lua.VM.MaxStepsTest do end end + describe "cross-engine mutual recursion" do + test "the budget bounds recursion that alternates execution engines" do + # A function whose body contains a `goto` cannot be bytecode-encoded, + # so it stays an interpreted `:lua_closure`; a plain arithmetic body + # compiles to a `:compiled_closure`. Pairing them in unbounded mutual + # recursion with no loop on either side forces a hand-off between the + # interpreter and the dispatcher on every call. The budget must span + # those hand-offs rather than resetting at each boundary, so this + # raises the budget error rather than recursing until `max_call_depth` + # (which defaults to `:infinity`) or forever. + lua = Lua.new(max_steps: 1000) + + code = """ + local pong + -- `goto` keeps this body off the bytecode path: interpreted closure. + local function ping(n) + ::again:: + if n < 0 then goto again end + return pong(n) + end + -- Plain body: compiles to a dispatcher closure. + pong = function(n) return ping(n) end + return ping(1) + """ + + assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> + eval!(lua, code) + end + end + + test "the alternating pair is genuinely split across both engines" do + # Guards the regression test above: if a compiler change ever tagged + # both functions into the same engine, the cross-engine assertion + # would silently degrade into a same-engine one. Assert the split + # holds by inspecting the closure tags the chunk produces. + {:ok, ast} = + Parser.parse(""" + local pong + local function ping(n) ::again:: if n < 0 then goto again end return pong end + pong = function(n) return ping end + return ping, pong + """) + + {:ok, proto} = Compiler.compile(ast, source: "test.lua") + state = Stdlib.install(State.new()) + + {:ok, [ping, pong], _state} = Lua.VM.execute(proto, state) + + assert {:lua_closure, _, _} = ping + assert {:compiled_closure, _, _} = pong + end + end + describe ":max_steps validation" do test "rejects non-positive integers and non-integers" do assert_raise ArgumentError, ~r/:max_steps/, fn -> Lua.new(max_steps: 0) end From f6b94b027f5e724630d26c3225204906dccbd557 Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Mon, 1 Jun 2026 12:35:40 -0700 Subject: [PATCH 5/9] fix(vm): reset instruction budget per evaluation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The :max_steps tally is stamped back into state.steps at each terminal and persisted into the returned %Lua{}, but nothing reset it at the top-level evaluation boundary. A long-lived %Lua{} running many small evals therefore accumulated steps across the whole lifetime and would eventually raise "instruction budget exceeded" even though no single eval came close — contradicting the per-eval contract in issue #306. Reset state.steps to 0 in Lua.VM.execute/2, the single chokepoint that Lua.eval!/eval route through. Nested calls within one evaluation still thread the tally as a bare parameter and accumulate against the same budget (a tight `while true do end` stays bounded); only the top-level boundary resets. The :infinity hot path is unchanged. Adds a regression test that sizes the budget just above one eval's real cost and runs that same eval 100x on the threaded state — red before this fix, green after — plus a guard asserting the budget still spans nested calls within a single evaluation. Addresses PR #320 review: cumulative-vs-per-eval budget leak. Plan: B17 --- lib/lua/vm.ex | 10 ++++++++ test/lua/vm/max_steps_test.exs | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lib/lua/vm.ex b/lib/lua/vm.ex index e51f7840..5009742b 100644 --- a/lib/lua/vm.ex +++ b/lib/lua/vm.ex @@ -23,6 +23,16 @@ defmodule Lua.VM do # unbounded varargs) grow the tuple lazily via `ensure_regs_capacity/2`. registers = Tuple.duplicate(nil, max(proto.max_registers, proto.param_count)) + # Reset the instruction-budget tally at the top-level evaluation + # boundary so `:max_steps` bounds ONE evaluation's total work rather + # than accumulating over the whole %Lua{} lifetime. The terminals stamp + # the per-eval tally back into `state.steps`, so without this reset a + # long-lived %Lua{} running many small evals would eventually trip the + # budget even though no single eval came close. Nested calls within this + # evaluation still accumulate against the same budget — they thread the + # tally as a bare parameter and never re-enter here. + state = %{state | steps: 0} + # Execute the prototype instructions {results, _final_regs, final_state} = Executor.execute(proto.instructions, registers, [], proto, state) diff --git a/test/lua/vm/max_steps_test.exs b/test/lua/vm/max_steps_test.exs index 1e3ab0c5..e1851e8f 100644 --- a/test/lua/vm/max_steps_test.exs +++ b/test/lua/vm/max_steps_test.exs @@ -81,6 +81,48 @@ defmodule Lua.VM.MaxStepsTest do assert {[5050], _lua} = eval!(lua, "local s = 0 for i = 1, 100 do s = s + i end return s") end + + test "a budget sized for one eval survives repeating that same eval on the threaded state" do + # Set the budget just above a single eval's real cost, then run that + # SAME eval many times on the SAME %Lua{}, threading the returned + # state forward. If the tally leaked across evaluations (cumulative + # over the %Lua{} lifetime), the Nth eval would trip the budget even + # though no single eval comes close. A correct per-eval reset lets + # every iteration succeed. + code = "local s = 0 for i = 1, 50 do s = s + i end return s" + + # Establish a budget that comfortably clears one eval but is far below + # the cumulative cost of running it 100 times. + lua = Lua.new(max_steps: 2000) + + final = + Enum.reduce(1..100, lua, fn _i, acc -> + {[1275], next} = eval!(acc, code) + next + end) + + # And the budget is still live afterward (not silently disabled). + assert {[1275], _lua} = eval!(final, code) + end + + test "the budget does NOT reset on nested calls within a single evaluation" do + # The per-eval reset must bound ONE evaluation's total work across all + # its instructions and nested calls. A tight loop calling a helper on + # every iteration must still trip the budget — the reset is a + # top-level boundary, not a per-call one. + lua = Lua.new(max_steps: 1000) + + code = """ + local function step(x) return x + 1 end + local s = 0 + while true do s = step(s) end + return s + """ + + assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> + eval!(lua, code) + end + end end describe "default behavior (:infinity)" do From 3f94e956676a9b5862b5a9c3de5001982e744ab4 Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Tue, 2 Jun 2026 13:05:15 -0700 Subject: [PATCH 6/9] docs(changelog,readme): document :max_steps instruction budget Add a CHANGELOG Unreleased entry and a README "Resource limits" subsection covering :max_call_depth and :max_steps. The README block is inside the moduledoc delimiter, so its iex> example is doctested. Plan: B17 --- CHANGELOG.md | 10 ++++++++++ README.md | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 210ef39c..d3c02d07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- `Lua.new/1` accepts `:max_steps` (default `:infinity`), bounding the + number of VM instructions a single evaluation may execute. Exceeding the + budget raises a catchable `"instruction budget exceeded"` runtime error, + giving a deterministic CPU bound without wrapping each call in a host + `Task` plus wall-clock timeout. Enforced at loop back-edges and call + boundaries on both the interpreter and compiled-dispatcher paths, so the + default `:infinity` carries no per-instruction cost; the budget is fresh + per top-level evaluation and recoverable via `pcall` (#320). + ### Performance - **Register tuples are sized to an honest peak, with no slack buffer, on diff --git a/README.md b/README.md index 42477faf..022f4fcf 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,26 @@ To allow a specific operation, exclude it from the sandbox explicitly: iex> is_binary(value) true +### Resource limits + +Sandboxing controls *which* functions a script may call, but it does not stop +a script from spinning forever or recursing without bound. Two options on +`Lua.new/1` give you deterministic limits without wrapping each evaluation in a +host `Task` plus a wall-clock timeout. Both default to `:infinity` (no limit) +and raise catchable runtime errors, so `pcall` recovers from them in-band: + +- `:max_call_depth` caps nested function-call depth; exceeding it raises + `"stack overflow"`. +- `:max_steps` caps the number of VM instructions a single evaluation may + execute; exceeding it raises `"instruction budget exceeded"`. + + iex> lua = Lua.new(max_steps: 1000) + iex> {[false, message], _lua} = Lua.eval!(lua, ~S[return pcall(function() while true do end end)]) + iex> message =~ "instruction budget exceeded" + true + +See the [Sandboxing guide](guides/examples/sandboxing.livemd) for details. + ### Metatables and metamethods Full metamethod dispatch is supported (`__index`, `__newindex`, `__call`, From 643ffc62d21e1028fd6257357138f5805b8da397 Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Mon, 15 Jun 2026 07:03:58 -0700 Subject: [PATCH 7/9] fix(vm): make :max_steps budget conservative across pcall, require, return ... Three accounting holes let a single evaluation overshoot the advertised instruction ceiling (all surfaced by an automated review of #320): - The `{:return_vararg}` interpreter terminal returned a bare state at the bottom frame instead of `finish_steps(state, steps)`, so work done in an interpreted `return ...` callee invoked across an engine boundary vanished from the budget. It now stamps the tally like every sibling terminal. - `pcall` recovery rewound the tally: `State.unwind_to/2` kept the pcall-entry `steps`, so a caught budget error refunded the work it burned (O(N^2) total work for a pcall-in-a-loop). `check_steps!/2` now stamps the live tally into the raised `state:`, and `unwind_to/2` carries `steps` forward (monotonic max), so a caught budget error stays spent. - `require` reset the budget mid-evaluation: it re-enters `Lua.VM.execute`, whose per-eval `steps: 0` reset forgave the outer eval's pre-require work. `execute/3` now takes `reset_steps:` (default true); `require` passes false so a module body counts against the same per-evaluation budget. Adds regression tests for all three (interpreted `return ...` tally stamp, pcall-loop trips the cap, require preserves the budget) plus a small module fixture. Full suite 2495 passed, lua53 17, format clean. --- lib/lua/vm.ex | 23 ++++++--- lib/lua/vm/executor.ex | 2 +- lib/lua/vm/state.ex | 15 ++++-- lib/lua/vm/stdlib.ex | 5 +- test/fixtures/budget_small_module.lua | 4 ++ test/lua/vm/max_steps_test.exs | 73 +++++++++++++++++++++++++++ 6 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 test/fixtures/budget_small_module.lua diff --git a/lib/lua/vm.ex b/lib/lua/vm.ex index 5009742b..f1fdf233 100644 --- a/lib/lua/vm.ex +++ b/lib/lua/vm.ex @@ -13,9 +13,18 @@ defmodule Lua.VM do Executes a compiled prototype. Returns {:ok, results, state} on success. + + ## Options + + * `:reset_steps` - (default `true`) reset the `:max_steps` instruction + tally to 0 before executing. True at a genuine top-level evaluation + (`Lua.eval`/`eval!`). Internal re-entries that run mid-evaluation — + notably `require`, which loads and runs a module's chunk through this + function — must pass `false` so the module body accumulates against the + one per-evaluation budget instead of resetting it. """ - @spec execute(Prototype.t(), State.t()) :: {:ok, list(), State.t()} - def execute(%Prototype{} = proto, state \\ State.new()) do + @spec execute(Prototype.t(), State.t(), keyword()) :: {:ok, list(), State.t()} + def execute(%Prototype{} = proto, state \\ State.new(), opts \\ []) do # Size the register file to the prototype's honest register peak, with no # slack buffer — same contract as every other call frame. `max_registers` # covers every statically-fixed destination (codegen's `instruction_peak/1` @@ -28,10 +37,12 @@ defmodule Lua.VM do # than accumulating over the whole %Lua{} lifetime. The terminals stamp # the per-eval tally back into `state.steps`, so without this reset a # long-lived %Lua{} running many small evals would eventually trip the - # budget even though no single eval came close. Nested calls within this - # evaluation still accumulate against the same budget — they thread the - # tally as a bare parameter and never re-enter here. - state = %{state | steps: 0} + # budget even though no single eval came close. Nested Lua/compiled calls + # within this evaluation thread the tally as a bare parameter and never + # re-enter here — but `require` does re-enter (it runs a module's chunk + # through this function), so it passes `reset_steps: false` to keep the + # module body counting against the same budget. + state = if Keyword.get(opts, :reset_steps, true), do: %{state | steps: 0}, else: state # Execute the prototype instructions {results, _final_regs, final_state} = diff --git a/lib/lua/vm/executor.ex b/lib/lua/vm/executor.ex index fcab8175..c1261d10 100644 --- a/lib/lua/vm/executor.ex +++ b/lib/lua/vm/executor.ex @@ -1378,7 +1378,7 @@ defmodule Lua.VM.Executor do varargs = Map.get(proto, :varargs, []) case frames do - [] -> {varargs, regs, state} + [] -> {varargs, regs, finish_steps(state, steps)} [frame | rest_frames] -> do_frame_return(varargs, regs, state, frame, rest_frames, line, steps) end end diff --git a/lib/lua/vm/state.ex b/lib/lua/vm/state.ex index e78386e1..39c4ffa5 100644 --- a/lib/lua/vm/state.ex +++ b/lib/lua/vm/state.ex @@ -115,16 +115,19 @@ defmodule Lua.VM.State do Raises the same `Lua.VM.RuntimeError` used by `"stack overflow"`, carrying the raise-time `state:` so `pcall`/`xpcall` recover heap effects for free. + The live tally is stamped into that `state:` (the threaded `steps` is not + otherwise in `%State{}`), so `unwind_to/2` can carry it forward and a caught + budget error stays spent — a protected call cannot refund the work it burned. """ @spec check_steps!(t(), non_neg_integer()) :: :ok def check_steps!(%__MODULE__{max_steps: :infinity}, _steps), do: :ok def check_steps!(%__MODULE__{max_steps: max}, steps) when steps < max, do: :ok - def check_steps!(%__MODULE__{call_stack: call_stack} = state, _steps) do + def check_steps!(%__MODULE__{call_stack: call_stack} = state, steps) do raise RuntimeError, value: "instruction budget exceeded", call_stack: call_stack, - state: state + state: %{state | steps: steps} end @doc """ @@ -144,6 +147,11 @@ defmodule Lua.VM.State do | `userdata`, `userdata_next_id` | `open_upvalues` | | `metatables`, `upvalue_cells`, `private` | `multi_return_count` | + The instruction tally `steps` is also carried forward (monotonic max), not + reset to the entry value: the work a protected call burned must still count + against the one per-evaluation `:max_steps` budget, so wrapping heavy work in + `pcall` (or looping over `pcall`) cannot escape the cap. + Keeping `upvalue_cells` while restoring `open_upvalues` matches reference upvalue semantics: cells captured before the protected call keep their mutated values, while cells opened by the unwound frames become @@ -164,7 +172,8 @@ defmodule Lua.VM.State do userdata_next_id: raised.userdata_next_id, metatables: raised.metatables, upvalue_cells: raised.upvalue_cells, - private: raised.private + private: raised.private, + steps: max(entry.steps, raised.steps) } end diff --git a/lib/lua/vm/stdlib.ex b/lib/lua/vm/stdlib.ex index 875b942c..0db8c77a 100644 --- a/lib/lua/vm/stdlib.ex +++ b/lib/lua/vm/stdlib.ex @@ -762,7 +762,10 @@ defmodule Lua.VM.Stdlib do with {:ok, ast} <- Lua.Parser.parse(content), {:ok, proto} <- Lua.Compiler.compile(ast), - {:ok, results, state} <- Lua.VM.execute(proto, state) do + # `require` runs mid-evaluation: inherit the caller's instruction + # budget instead of resetting it, so a looping module body counts + # against the same `:max_steps` and the pre-require work is preserved. + {:ok, results, state} <- Lua.VM.execute(proto, state, reset_steps: false) do # Get the return value (or true if no return value) result = case results do diff --git a/test/fixtures/budget_small_module.lua b/test/fixtures/budget_small_module.lua new file mode 100644 index 00000000..71e21681 --- /dev/null +++ b/test/fixtures/budget_small_module.lua @@ -0,0 +1,4 @@ +-- A trivial module: requiring it does negligible instruction work. Used to +-- prove that `require` runs against the caller's `:max_steps` budget rather +-- than resetting it (the pre-require work must still count). +return 1 diff --git a/test/lua/vm/max_steps_test.exs b/test/lua/vm/max_steps_test.exs index e1851e8f..08713ce5 100644 --- a/test/lua/vm/max_steps_test.exs +++ b/test/lua/vm/max_steps_test.exs @@ -213,6 +213,79 @@ defmodule Lua.VM.MaxStepsTest do end end + describe "budget integrity (non-conservative-accounting regressions)" do + test "an interpreted `return ...` terminal stamps the step tally back into state" do + # The bottom-frame `{:return_vararg}` terminal must stamp the accumulated + # tally into `state.steps` like every other terminal. If it returns a + # bare state, a compiled caller that reads `steps = state.steps` after the + # interpreted callee returns under-counts, so work done in `return ...` + # passthroughs vanishes from the budget. + run = fn src -> + {:ok, ast} = Parser.parse(src) + {:ok, proto} = Compiler.compile(ast, source: "test.lua") + {:ok, _results, state} = Lua.VM.execute(proto, Stdlib.install(State.new())) + state.steps + end + + work = "local s = 0 for i = 1, 5 do s = s + i end " + named = run.(work <> "return s") + vararg = run.(work <> "return ...") + + assert named > 0 + assert vararg == named + end + + test "pcall does not refund the instructions a caught budget error consumed" do + # Each inner protected call trips the budget; recovery must carry the + # exhausted tally forward (monotonic) rather than rewinding it to the + # pcall-entry value. Otherwise a `pcall`-in-a-loop pattern re-funds the + # inner work every iteration and the single evaluation runs far beyond + # `:max_steps` total instructions before tripping. + lua = Lua.new(max_steps: 2000) + + assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> + eval!(lua, "for i = 1, 1000000 do pcall(function() while true do end end) end") + end + end + + test "require runs the module body against the same budget (no mid-eval reset)" do + # `require` re-enters `Lua.VM.execute`, which resets the per-eval tally at + # genuine top-level entry only. The pre-require work must still count: with + # a mid-eval reset the 700 pre-require steps would be forgiven, leaving the + # 700 post-require steps under the 1000 budget so the eval would wrongly + # succeed. With the budget preserved, pre + post exceed it and it trips. + lua = Lua.new(sandboxed: [], max_steps: 1000) + + code = ~S""" + package.path = "./test/fixtures/?.lua" + local s = 0 + for i = 1, 700 do s = s + i end + require("budget_small_module") + for i = 1, 700 do s = s + i end + return s + """ + + assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> + eval!(lua, code) + end + end + + test "a budget-exhausting require still trips, and pre-require work is preserved across it" do + # Sanity companion: requiring the trivial module under a comfortable + # budget, with light surrounding work, must succeed (the fix must not + # over-count and spuriously trip a legitimate require). + lua = Lua.new(sandboxed: [], max_steps: 100_000) + + code = ~S""" + package.path = "./test/fixtures/?.lua" + local m = require("budget_small_module") + return m + """ + + assert {[1], _lua} = eval!(lua, code) + end + end + describe ":max_steps validation" do test "rejects non-positive integers and non-integers" do assert_raise ArgumentError, ~r/:max_steps/, fn -> Lua.new(max_steps: 0) end From a5d55ba495023e36688d1b806dd56e222e92a3be Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Mon, 15 Jun 2026 07:31:22 -0700 Subject: [PATCH 8/9] perf(vm): make the :max_steps default path zero-cost via State.tick! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The budget tally was charged at every call boundary and loop back-edge with two operations — an unconditional `steps = steps + 1` increment followed by `State.check_steps!/2`. At the default `:infinity` budget the *check* short-circuited in one function-head match, but the increment and the second function call still ran on the recursion-heavy hot path (fib(30) ≈ 2.7M call boundaries), costing a few percent over main. Fold both into a single `State.tick!/2` that is a true no-op at `:infinity`: it returns the threaded tally unchanged in one function-head match, doing no arithmetic and rebuilding no struct. The finite clause increments and raises `"instruction budget exceeded"` once the new tally reaches `max_steps`, with the same raise-time `state:` semantics the old `check_steps!/2` carried, so pcall/require/unwind accounting is unchanged. `check_steps!/2` is removed (fully subsumed). Because `:infinity` no longer accrues a tally, the one regression test that inspected `state.steps` at the default budget now seeds a generous finite budget so the `return ...` terminal-stamping invariant it guards stays observable. Plan: B17 --- CHANGELOG.md | 8 +++-- lib/lua/vm/dispatcher.ex | 30 +++++++------------ lib/lua/vm/executor.ex | 18 ++++-------- lib/lua/vm/state.ex | 53 ++++++++++++++++++++-------------- test/lua/vm/max_steps_test.exs | 7 +++-- 5 files changed, 57 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3c02d07..188b5cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 budget raises a catchable `"instruction budget exceeded"` runtime error, giving a deterministic CPU bound without wrapping each call in a host `Task` plus wall-clock timeout. Enforced at loop back-edges and call - boundaries on both the interpreter and compiled-dispatcher paths, so the - default `:infinity` carries no per-instruction cost; the budget is fresh - per top-level evaluation and recoverable via `pcall` (#320). + boundaries on both the interpreter and compiled-dispatcher paths via a + single `Lua.VM.State.tick!/2` call that is a true no-op at `:infinity` + (no increment, no struct rebuild), so the default `:infinity` carries no + per-opcode cost; the budget is fresh per top-level evaluation and + recoverable via `pcall` (#320). ### Performance diff --git a/lib/lua/vm/dispatcher.ex b/lib/lua/vm/dispatcher.ex index 69319dd4..2c8ee534 100644 --- a/lib/lua/vm/dispatcher.ex +++ b/lib/lua/vm/dispatcher.ex @@ -688,8 +688,7 @@ defmodule Lua.VM.Dispatcher do {code, pc + 1, regs, upvalues, proto, cont, :discard, state.open_upvalues} call_info = Executor.dispatcher_call_info(proto, name_hint, 0) - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) State.check_call_depth!(state) state = %{ @@ -714,8 +713,7 @@ defmodule Lua.VM.Dispatcher do {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, arg_count) call_info = Executor.dispatcher_call_info(proto, name_hint, 0) - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) State.check_call_depth!(state) state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1, steps: steps} {_results, state} = Executor.call_function(closure, args, state) @@ -751,8 +749,7 @@ defmodule Lua.VM.Dispatcher do {code, pc + 1, regs, upvalues, proto, cont, base, state.open_upvalues} call_info = Executor.dispatcher_call_info(proto, name_hint, 0) - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) State.check_call_depth!(state) state = %{ @@ -777,8 +774,7 @@ defmodule Lua.VM.Dispatcher do {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, arg_count) call_info = Executor.dispatcher_call_info(proto, name_hint, 0) - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) State.check_call_depth!(state) state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1, steps: steps} {results, state} = Executor.call_function(closure, args, state) @@ -1231,8 +1227,7 @@ defmodule Lua.VM.Dispatcher do frame = {code, pc + 1, regs, upvalues, proto, cont, dest, state.open_upvalues} call_info = Executor.dispatcher_call_info(proto, name_hint, 0) - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) State.check_call_depth!(state) state = %{ @@ -1257,8 +1252,7 @@ defmodule Lua.VM.Dispatcher do {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, total_args) call_info = Executor.dispatcher_call_info(proto, name_hint, 0) - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) State.check_call_depth!(state) state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1, steps: steps} {results, state} = Executor.call_function(closure, args, state) @@ -1376,8 +1370,7 @@ defmodule Lua.VM.Dispatcher do should_continue = if step > 0, do: new_counter <= limit, else: new_counter >= limit if should_continue do - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) regs = :erlang.setelement(loop_var + 1, regs, new_counter) state = Executor.dispatcher_close_open_upvalues_at_or_above(state, loop_var) dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | rest_cont], frames, steps) @@ -1424,8 +1417,7 @@ defmodule Lua.VM.Dispatcher do frames, steps ) do - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) cps = {:cps_while_test, test_reg, cond_bc, body_bc, outer_code, outer_pc} dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, steps) end @@ -1463,8 +1455,7 @@ defmodule Lua.VM.Dispatcher do ) do case :erlang.element(test_reg + 1, regs) do v when v === nil or v === false -> - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) cps = {:cps_repeat_body, test_reg, body_bc, cond_bc, outer_code, outer_pc} dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, steps) @@ -1505,8 +1496,7 @@ defmodule Lua.VM.Dispatcher do dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, steps) [first | _] -> - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) regs = :erlang.setelement(base + 3, regs, first) regs = assign_iter_results(regs, var_regs, results, 0) first_var_reg = :erlang.element(1, var_regs) diff --git a/lib/lua/vm/executor.ex b/lib/lua/vm/executor.ex index c1261d10..2a41fe33 100644 --- a/lib/lua/vm/executor.ex +++ b/lib/lua/vm/executor.ex @@ -713,8 +713,7 @@ defmodule Lua.VM.Executor do # After while loop body — restart condition [{:cps_while_body, test_reg, loop_body, cond_body, rest, outer_cont} | _] -> - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) 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, steps) @@ -732,8 +731,7 @@ defmodule Lua.VM.Executor do do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, steps) else # Condition false = repeat body - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) 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, steps) @@ -749,8 +747,7 @@ defmodule Lua.VM.Executor do should_continue = if step > 0, do: new_counter <= limit, else: new_counter >= limit if should_continue do - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) regs = put_elem(regs, loop_var, new_counter) state = close_open_upvalues_at_or_above(state, loop_var) @@ -776,8 +773,7 @@ defmodule Lua.VM.Executor do if first_result == nil do do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, steps) else - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) regs = put_elem(regs, base + 2, first_result) regs = @@ -1165,8 +1161,7 @@ defmodule Lua.VM.Executor do # without going through this branch. args = collect_args(regs, base + 1, total_args) call_info = {proto.source, line, name_hint} - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) State.check_call_depth!(state) # Carry the tally into the dispatcher so the budget spans the # boundary; `Dispatcher.execute/4` seeds from `state.steps` and @@ -1214,8 +1209,7 @@ defmodule Lua.VM.Executor do call_info = {proto.source, line, name_hint} - steps = steps + 1 - State.check_steps!(state, steps) + steps = State.tick!(state, steps) State.check_call_depth!(state) state = %{ diff --git a/lib/lua/vm/state.ex b/lib/lua/vm/state.ex index 39c4ffa5..8717cce9 100644 --- a/lib/lua/vm/state.ex +++ b/lib/lua/vm/state.ex @@ -26,7 +26,7 @@ defmodule Lua.VM.State do # here on the per-opcode hot path — it is threaded as a parameter # through the executor / dispatcher loops, mirroring the # `line`-off-State discipline so the default `:infinity` path - # carries no per-instruction struct rebuild. See `check_steps!/2`. + # carries no per-instruction struct rebuild. See `tick!/2`. max_steps: :infinity, # `steps` carries the running tally ACROSS engine boundaries # only. The interpreter and dispatcher each thread their tally as @@ -101,29 +101,38 @@ defmodule Lua.VM.State do end @doc """ - Guards against unbounded CPU work within a single evaluation. - - Raises a catchable Lua `"instruction budget exceeded"` runtime error when - the running instruction tally `steps` has reached `max_steps`. Call it at - loop back-edges and call boundaries — never per opcode — so the default - `:infinity` path stays free of per-instruction cost. - - The tally is threaded as a parameter, not stored in `%State{}`. No-op when - the tally is under the limit or when `max_steps` is `:infinity` (the - default). The clauses are ordered so both common cases resolve in a single - function-head match with no struct rebuild. - - Raises the same `Lua.VM.RuntimeError` used by `"stack overflow"`, carrying - the raise-time `state:` so `pcall`/`xpcall` recover heap effects for free. - The live tally is stamped into that `state:` (the threaded `steps` is not - otherwise in `%State{}`), so `unwind_to/2` can carry it forward and a caught - budget error stays spent — a protected call cannot refund the work it burned. + Charges one instruction against the budget and returns the new tally. + + Guards against unbounded CPU work within a single evaluation by folding + the running-tally increment and the budget check into one call. Call it + at loop back-edges and call boundaries — never per opcode — so the + default `:infinity` path stays free of per-instruction cost. + + The tally is threaded as a parameter, not stored in `%State{}`. When + `max_steps` is `:infinity` (the default) this is a true no-op: it returns + the tally unchanged in a single function-head match, doing no arithmetic + and rebuilding no struct, so the default path's only per-boundary cost is + this one call. When a finite budget is set it increments the tally and, + once the new tally reaches `max_steps`, raises a catchable Lua + `"instruction budget exceeded"` runtime error. The clauses are ordered so + both the `:infinity` and under-budget cases resolve in a single + function-head match. + + The raise reuses the same `Lua.VM.RuntimeError` used by `"stack + overflow"`, carrying the raise-time `state:` so `pcall`/`xpcall` recover + heap effects for free. The live tally is stamped into that `state:` (the + threaded `steps` is not otherwise in `%State{}`), so `unwind_to/2` can + carry it forward and a caught budget error stays spent — a protected call + cannot refund the work it burned. """ - @spec check_steps!(t(), non_neg_integer()) :: :ok - def check_steps!(%__MODULE__{max_steps: :infinity}, _steps), do: :ok - def check_steps!(%__MODULE__{max_steps: max}, steps) when steps < max, do: :ok + @spec tick!(t(), non_neg_integer()) :: non_neg_integer() + def tick!(%__MODULE__{max_steps: :infinity}, steps), do: steps + + def tick!(%__MODULE__{max_steps: max}, steps) when steps + 1 < max, do: steps + 1 + + def tick!(%__MODULE__{call_stack: call_stack} = state, steps) do + steps = steps + 1 - def check_steps!(%__MODULE__{call_stack: call_stack} = state, steps) do raise RuntimeError, value: "instruction budget exceeded", call_stack: call_stack, diff --git a/test/lua/vm/max_steps_test.exs b/test/lua/vm/max_steps_test.exs index 08713ce5..e0e8c540 100644 --- a/test/lua/vm/max_steps_test.exs +++ b/test/lua/vm/max_steps_test.exs @@ -219,11 +219,14 @@ defmodule Lua.VM.MaxStepsTest do # tally into `state.steps` like every other terminal. If it returns a # bare state, a compiled caller that reads `steps = state.steps` after the # interpreted callee returns under-counts, so work done in `return ...` - # passthroughs vanishes from the budget. + # passthroughs vanishes from the budget. A finite budget keeps the tally + # live (the default `:infinity` path charges nothing, so accrual is only + # observable under a budget) while staying well clear of tripping it. run = fn src -> {:ok, ast} = Parser.parse(src) {:ok, proto} = Compiler.compile(ast, source: "test.lua") - {:ok, _results, state} = Lua.VM.execute(proto, Stdlib.install(State.new())) + state = %{Stdlib.install(State.new()) | max_steps: 1_000_000} + {:ok, _results, state} = Lua.VM.execute(proto, state) state.steps end From 491ce3f4e3748565bcf2ff2246b5f8f2fa1b5d9b Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Mon, 15 Jun 2026 08:43:13 -0700 Subject: [PATCH 9/9] refactor(vm): rename :max_steps option to :max_instructions "steps" was vocabulary the budget feature introduced; the rest of the VM (and the feature's own "instruction budget exceeded" error) speaks in "instructions". Rename the public option and the %State{} ceiling field to :max_instructions / max_instructions, matching the :max_call_depth / :max_string_bytes sibling options and the error message. The running tally (the %State{} field and the value threaded through every do_execute/dispatch clause) becomes `instruction_count` rather than `instructions`, because `instructions` is already the instruction-list parameter on those same clauses and would shadow it. Pure rename: no behavior change. Test module/file renamed to MaxInstructionsTest / max_instructions_test.exs. Suite 2495, lua53 17, format + warnings-as-errors clean. The PUC suite files under test/lua53_tests/ (which use "steps" for GC steps) are untouched. --- CHANGELOG.md | 2 +- README.md | 4 +- guides/examples/sandboxing.livemd | 4 +- guides/sandboxing.md | 2 +- lib/lua.ex | 20 +- lib/lua/vm.ex | 10 +- lib/lua/vm/dispatcher.ex | 417 +++++---- lib/lua/vm/executor.ex | 842 +++++++++++++----- lib/lua/vm/state.ex | 35 +- lib/lua/vm/stdlib.ex | 6 +- test/fixtures/budget_small_module.lua | 2 +- ...eps_test.exs => max_instructions_test.exs} | 80 +- 12 files changed, 942 insertions(+), 482 deletions(-) rename test/lua/vm/{max_steps_test.exs => max_instructions_test.exs} (79%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 188b5cd0..466163c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added -- `Lua.new/1` accepts `:max_steps` (default `:infinity`), bounding the +- `Lua.new/1` accepts `:max_instructions` (default `:infinity`), bounding the number of VM instructions a single evaluation may execute. Exceeding the budget raises a catchable `"instruction budget exceeded"` runtime error, giving a deterministic CPU bound without wrapping each call in a host diff --git a/README.md b/README.md index 022f4fcf..6ffde9b4 100644 --- a/README.md +++ b/README.md @@ -142,10 +142,10 @@ and raise catchable runtime errors, so `pcall` recovers from them in-band: - `:max_call_depth` caps nested function-call depth; exceeding it raises `"stack overflow"`. -- `:max_steps` caps the number of VM instructions a single evaluation may +- `:max_instructions` caps the number of VM instructions a single evaluation may execute; exceeding it raises `"instruction budget exceeded"`. - iex> lua = Lua.new(max_steps: 1000) + iex> lua = Lua.new(max_instructions: 1000) iex> {[false, message], _lua} = Lua.eval!(lua, ~S[return pcall(function() while true do end end)]) iex> message =~ "instruction budget exceeded" true diff --git a/guides/examples/sandboxing.livemd b/guides/examples/sandboxing.livemd index 8c988f4d..b55572d1 100644 --- a/guides/examples/sandboxing.livemd +++ b/guides/examples/sandboxing.livemd @@ -83,14 +83,14 @@ instead of letting the recursion exhaust the host process. The default is ### Instruction budget -`Lua.new(max_steps: n)` caps the number of VM instructions a single +`Lua.new(max_instructions: n)` caps the number of VM instructions a single evaluation may execute. When a script exceeds the budget it raises a catchable `"instruction budget exceeded"` runtime error — so a runaway loop terminates deterministically inside the VM: ```elixir {[ok?, message], _lua} = - Lua.eval!(Lua.new(max_steps: 1000), ~S[return pcall(function() while true do end end)]) + Lua.eval!(Lua.new(max_instructions: 1000), ~S[return pcall(function() while true do end end)]) {ok?, message} ``` diff --git a/guides/sandboxing.md b/guides/sandboxing.md index e01cf022..c2a15748 100644 --- a/guides/sandboxing.md +++ b/guides/sandboxing.md @@ -72,7 +72,7 @@ lua = Lua.new(sandboxed: []) ### Sandboxing a single path `Lua.sandbox/2` sandboxes one path on an existing VM, which is handy when -building a configuration up in steps: +building a configuration up in instruction_count: ```elixir lua = diff --git a/lib/lua.ex b/lib/lua.ex index 7c874db6..eeca80d0 100644 --- a/lib/lua.ex +++ b/lib/lua.ex @@ -117,7 +117,7 @@ defmodule Lua do iex> message =~ "resulting string too large" true - * `:max_steps` - (default `:infinity`) caps the number of VM instructions a single + * `:max_instructions` - (default `:infinity`) caps the number of VM instructions a single evaluation may execute. When a script exceeds this budget, a catchable `"instruction budget exceeded"` runtime error is raised, giving library consumers a deterministic CPU bound without wrapping each call in a host `Task` + wall-clock timeout. @@ -125,7 +125,7 @@ defmodule Lua do back-edges and call boundaries, so the default `:infinity` path carries no per-instruction cost. The budget is fresh per top-level evaluation and recoverable via `pcall`. - iex> lua = Lua.new(max_steps: 1000) + iex> lua = Lua.new(max_instructions: 1000) iex> {[false, message], _lua} = Lua.eval!(lua, "return pcall(function() while true do end end)") iex> message =~ "instruction budget exceeded" true @@ -139,20 +139,20 @@ defmodule Lua do debug: false, max_call_depth: :infinity, max_string_bytes: Lua.VM.Limits.max_string_bytes(), - max_steps: :infinity + max_instructions: :infinity ) exclude = Keyword.fetch!(opts, :exclude) debug = Keyword.fetch!(opts, :debug) max_call_depth = validate_max_call_depth!(Keyword.fetch!(opts, :max_call_depth)) max_string_bytes = validate_max_string_bytes!(Keyword.fetch!(opts, :max_string_bytes)) - max_steps = validate_max_steps!(Keyword.fetch!(opts, :max_steps)) + max_instructions = validate_max_instructions!(Keyword.fetch!(opts, :max_instructions)) state = %{ Lua.VM.Stdlib.install(State.new()) | max_call_depth: max_call_depth, max_string_bytes: max_string_bytes, - max_steps: max_steps + max_instructions: max_instructions } opts @@ -176,12 +176,14 @@ defmodule Lua do ":max_string_bytes must be a positive integer, got: #{inspect(other)}" end - defp validate_max_steps!(:infinity), do: :infinity - defp validate_max_steps!(steps) when is_integer(steps) and steps > 0, do: steps + defp validate_max_instructions!(:infinity), do: :infinity - defp validate_max_steps!(other) do + defp validate_max_instructions!(instruction_count) when is_integer(instruction_count) and instruction_count > 0, + do: instruction_count + + defp validate_max_instructions!(other) do raise ArgumentError, - ":max_steps must be a positive integer or :infinity, got: #{inspect(other)}" + ":max_instructions must be a positive integer or :infinity, got: #{inspect(other)}" end @doc """ diff --git a/lib/lua/vm.ex b/lib/lua/vm.ex index f1fdf233..b40ba2f5 100644 --- a/lib/lua/vm.ex +++ b/lib/lua/vm.ex @@ -16,7 +16,7 @@ defmodule Lua.VM do ## Options - * `:reset_steps` - (default `true`) reset the `:max_steps` instruction + * `:reset_instructions` - (default `true`) reset the `:max_instructions` instruction tally to 0 before executing. True at a genuine top-level evaluation (`Lua.eval`/`eval!`). Internal re-entries that run mid-evaluation — notably `require`, which loads and runs a module's chunk through this @@ -33,16 +33,16 @@ defmodule Lua.VM do registers = Tuple.duplicate(nil, max(proto.max_registers, proto.param_count)) # Reset the instruction-budget tally at the top-level evaluation - # boundary so `:max_steps` bounds ONE evaluation's total work rather + # boundary so `:max_instructions` bounds ONE evaluation's total work rather # than accumulating over the whole %Lua{} lifetime. The terminals stamp - # the per-eval tally back into `state.steps`, so without this reset a + # the per-eval tally back into `state.instruction_count`, so without this reset a # long-lived %Lua{} running many small evals would eventually trip the # budget even though no single eval came close. Nested Lua/compiled calls # within this evaluation thread the tally as a bare parameter and never # re-enter here — but `require` does re-enter (it runs a module's chunk - # through this function), so it passes `reset_steps: false` to keep the + # through this function), so it passes `reset_instructions: false` to keep the # module body counting against the same budget. - state = if Keyword.get(opts, :reset_steps, true), do: %{state | steps: 0}, else: state + state = if Keyword.get(opts, :reset_instructions, true), do: %{state | instruction_count: 0}, else: state # Execute the prototype instructions {results, _final_regs, final_state} = diff --git a/lib/lua/vm/dispatcher.ex b/lib/lua/vm/dispatcher.ex index 2c8ee534..b0d375e3 100644 --- a/lib/lua/vm/dispatcher.ex +++ b/lib/lua/vm/dispatcher.ex @@ -162,8 +162,8 @@ defmodule Lua.VM.Dispatcher do # Seed the dispatcher tally from the budget carried across the boundary # so an alternating-engine call chain accumulates against one budget # instead of resetting here; the terminals stamp the final tally back - # into `state.steps`. - {results, state} = dispatch(proto.bytecode, 1, regs, upvalues, proto, state, [], [], state.steps) + # into `state.instruction_count`. + {results, state} = dispatch(proto.bytecode, 1, regs, upvalues, proto, state, [], [], state.instruction_count) state = %{state | open_upvalues: saved_open} {results, state} @@ -215,28 +215,28 @@ defmodule Lua.VM.Dispatcher do # `Executor.call_function/3` instead, paying one Erlang stack frame # at the boundary. - defp dispatch(code, pc, regs, upvalues, proto, state, cont, frames, steps) when pc > tuple_size(code) do - finish_body(regs, upvalues, proto, state, cont, frames, steps) + defp dispatch(code, pc, regs, upvalues, proto, state, cont, frames, instruction_count) when pc > tuple_size(code) do + finish_body(regs, upvalues, proto, state, cont, frames, instruction_count) end - defp dispatch(code, pc, regs, upvalues, proto, state, cont, frames, steps) do + defp dispatch(code, pc, regs, upvalues, proto, state, cont, frames, instruction_count) do case :erlang.element(pc, code) do {@op_load_constant, dest, value} -> regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_load_boolean, dest, value} -> regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_load_nil, dest, count} -> regs = clear_nils(regs, dest, count + 1) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_move, dest, src} -> v = :erlang.element(src + 1, regs) regs = :erlang.setelement(dest + 1, regs, v) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_load_env, dest} -> env = @@ -247,7 +247,7 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(dest + 1, regs, env) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_get_upvalue, dest, index} -> cell_ref = :erlang.element(index + 1, upvalues) @@ -258,12 +258,12 @@ defmodule Lua.VM.Dispatcher do # has to match where it does fire. v = Map.get(state.upvalue_cells, cell_ref) regs = :erlang.setelement(dest + 1, regs, v) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_get_global, dest, name} -> v = State.get_global(state, name) regs = :erlang.setelement(dest + 1, regs, v) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_get_field, dest, table_reg, name, name_hint} -> table_val = :erlang.element(table_reg + 1, regs) @@ -279,20 +279,20 @@ defmodule Lua.VM.Dispatcher do case data do %{^name => value} -> regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) _ -> case :erlang.map_get(:metatable, table) do nil -> regs = :erlang.setelement(dest + 1, regs, nil) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) _ -> {value, state} = Executor.dispatcher_get_field(table_val, name, state, proto, name_hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end end @@ -301,7 +301,7 @@ defmodule Lua.VM.Dispatcher do Executor.dispatcher_get_field(table_val, name, state, proto, name_hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end # ── Arithmetic ────────────────────────────────────────────────── @@ -320,16 +320,16 @@ defmodule Lua.VM.Dispatcher do sum = va + vb wrapped = if sum >= @min_int and sum <= @max_int, do: sum, else: Numeric.to_signed_int64(sum) regs = :erlang.setelement(dest + 1, regs, wrapped) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va + vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) true -> {value, state} = Executor.dispatcher_binop(:add, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_subtract, dest, a, b, hint_a, hint_b} -> @@ -341,16 +341,16 @@ defmodule Lua.VM.Dispatcher do diff = va - vb wrapped = if diff >= @min_int and diff <= @max_int, do: diff, else: Numeric.to_signed_int64(diff) regs = :erlang.setelement(dest + 1, regs, wrapped) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va - vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) true -> {value, state} = Executor.dispatcher_binop(:subtract, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_multiply, dest, a, b, hint_a, hint_b} -> @@ -362,16 +362,16 @@ defmodule Lua.VM.Dispatcher do prod = va * vb wrapped = if prod >= @min_int and prod <= @max_int, do: prod, else: Numeric.to_signed_int64(prod) regs = :erlang.setelement(dest + 1, regs, wrapped) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va * vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) true -> {value, state} = Executor.dispatcher_binop(:multiply, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_divide, dest, a, b, hint_a, hint_b} -> @@ -387,7 +387,7 @@ defmodule Lua.VM.Dispatcher do ) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_floor_divide, dest, a, b, hint_a, hint_b} -> {value, state} = @@ -402,7 +402,7 @@ defmodule Lua.VM.Dispatcher do ) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_modulo, dest, a, b, hint_a, hint_b} -> {value, state} = @@ -417,7 +417,7 @@ defmodule Lua.VM.Dispatcher do ) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_power, dest, a, b, hint_a, hint_b} -> {value, state} = @@ -432,14 +432,14 @@ defmodule Lua.VM.Dispatcher do ) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_negate, dest, src, hint} -> {value, state} = Executor.dispatcher_unop(:negate, :erlang.element(src + 1, regs), state, proto, hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Bitwise ───────────────────────────────────────────────────── # @@ -463,11 +463,11 @@ defmodule Lua.VM.Dispatcher do if is_integer(va) and is_integer(vb) do regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(Bitwise.band(va, vb))) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) else {value, state} = Executor.dispatcher_bitwise(:band, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_bitwise_or, dest, a, b, hint_a, hint_b} -> @@ -476,11 +476,11 @@ defmodule Lua.VM.Dispatcher do if is_integer(va) and is_integer(vb) do regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(Bitwise.bor(va, vb))) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) else {value, state} = Executor.dispatcher_bitwise(:bor, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_bitwise_xor, dest, a, b, hint_a, hint_b} -> @@ -489,11 +489,11 @@ defmodule Lua.VM.Dispatcher do if is_integer(va) and is_integer(vb) do regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(Bitwise.bxor(va, vb))) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) else {value, state} = Executor.dispatcher_bitwise(:bxor, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_shift_left, dest, a, b, hint_a, hint_b} -> @@ -501,20 +501,20 @@ defmodule Lua.VM.Dispatcher do vb = :erlang.element(b + 1, regs) {value, state} = Executor.dispatcher_bitwise(:shl, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_shift_right, dest, a, b, hint_a, hint_b} -> va = :erlang.element(a + 1, regs) vb = :erlang.element(b + 1, regs) {value, state} = Executor.dispatcher_bitwise(:shr, va, vb, state, proto, hint_a, hint_b) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_bitwise_not, dest, src, hint} -> val = :erlang.element(src + 1, regs) {value, state} = Executor.dispatcher_bnot(val, state, proto, hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Comparisons ───────────────────────────────────────────────── @@ -525,16 +525,16 @@ defmodule Lua.VM.Dispatcher do cond do is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va < vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) is_binary(va) and is_binary(vb) -> regs = :erlang.setelement(dest + 1, regs, va < vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) true -> {value, state} = Executor.dispatcher_cmp(:less_than, va, vb, state, proto) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_less_equal, dest, a, b} -> @@ -544,16 +544,16 @@ defmodule Lua.VM.Dispatcher do cond do is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va <= vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) is_binary(va) and is_binary(vb) -> regs = :erlang.setelement(dest + 1, regs, va <= vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) true -> {value, state} = Executor.dispatcher_cmp(:less_equal, va, vb, state, proto) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_greater_than, dest, a, b} -> @@ -563,16 +563,16 @@ defmodule Lua.VM.Dispatcher do cond do is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va > vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) is_binary(va) and is_binary(vb) -> regs = :erlang.setelement(dest + 1, regs, va > vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) true -> {value, state} = Executor.dispatcher_cmp(:greater_than, va, vb, state, proto) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_greater_equal, dest, a, b} -> @@ -582,16 +582,16 @@ defmodule Lua.VM.Dispatcher do cond do is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va >= vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) is_binary(va) and is_binary(vb) -> regs = :erlang.setelement(dest + 1, regs, va >= vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) true -> {value, state} = Executor.dispatcher_cmp(:greater_equal, va, vb, state, proto) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_equal, dest, a, b} -> @@ -601,16 +601,16 @@ defmodule Lua.VM.Dispatcher do cond do is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va == vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) is_binary(va) and is_binary(vb) -> regs = :erlang.setelement(dest + 1, regs, va == vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) true -> {value, state} = Executor.dispatcher_cmp(:equal, va, vb, state, proto) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_not_equal, dest, a, b} -> @@ -620,16 +620,16 @@ defmodule Lua.VM.Dispatcher do cond do is_number(va) and is_number(vb) -> regs = :erlang.setelement(dest + 1, regs, va != vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) is_binary(va) and is_binary(vb) -> regs = :erlang.setelement(dest + 1, regs, va != vb) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) true -> {value, state} = Executor.dispatcher_cmp(:not_equal, va, vb, state, proto) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_not, dest, src} -> @@ -638,7 +638,7 @@ defmodule Lua.VM.Dispatcher do # values. Saves a function call per `:not` opcode. result = v === nil or v === false regs = :erlang.setelement(dest + 1, regs, result) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Conditional branching ─────────────────────────────────────── # @@ -657,7 +657,7 @@ defmodule Lua.VM.Dispatcher do _ -> then_bc end - dispatch(branch, 1, regs, upvalues, proto, state, [{code, pc + 1} | cont], frames, steps) + dispatch(branch, 1, regs, upvalues, proto, state, [{code, pc + 1} | cont], frames, instruction_count) # ── Calls ─────────────────────────────────────────────────────── # @@ -688,7 +688,7 @@ defmodule Lua.VM.Dispatcher do {code, pc + 1, regs, upvalues, proto, cont, :discard, state.open_upvalues} call_info = Executor.dispatcher_call_info(proto, name_hint, 0) - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) State.check_call_depth!(state) state = %{ @@ -707,31 +707,38 @@ defmodule Lua.VM.Dispatcher do state, [], [frame | frames], - steps + instruction_count ) {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, arg_count) call_info = Executor.dispatcher_call_info(proto, name_hint, 0) - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) State.check_call_depth!(state) - state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1, steps: steps} + + state = %{ + state + | call_stack: [call_info | state.call_stack], + call_depth: state.call_depth + 1, + instruction_count: instruction_count + } + {_results, state} = Executor.call_function(closure, args, state) - steps = state.steps + instruction_count = state.instruction_count state = %{state | call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) _ -> args = collect_args(regs, base + 1, arg_count) - state = %{state | steps: steps} + state = %{state | instruction_count: instruction_count} {_results, state} = Executor.dispatcher_call_function(func_value, args, state, proto, name_hint, line) - steps = state.steps + instruction_count = state.instruction_count - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_call_one, base, arg_count, name_hint, line} -> @@ -749,7 +756,7 @@ defmodule Lua.VM.Dispatcher do {code, pc + 1, regs, upvalues, proto, cont, base, state.open_upvalues} call_info = Executor.dispatcher_call_info(proto, name_hint, 0) - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) State.check_call_depth!(state) state = %{ @@ -768,17 +775,24 @@ defmodule Lua.VM.Dispatcher do state, [], [frame | frames], - steps + instruction_count ) {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, arg_count) call_info = Executor.dispatcher_call_info(proto, name_hint, 0) - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) State.check_call_depth!(state) - state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1, steps: steps} + + state = %{ + state + | call_stack: [call_info | state.call_stack], + call_depth: state.call_depth + 1, + instruction_count: instruction_count + } + {results, state} = Executor.call_function(closure, args, state) - steps = state.steps + instruction_count = state.instruction_count state = %{state | call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} first = @@ -788,17 +802,17 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(base + 1, regs, first) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) _ -> args = collect_args(regs, base + 1, arg_count) - state = %{state | steps: steps} + state = %{state | instruction_count: instruction_count} {results, state} = Executor.dispatcher_call_function(func_value, args, state, proto, name_hint, line) - steps = state.steps + instruction_count = state.instruction_count first = case results do @@ -807,7 +821,7 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(base + 1, regs, first) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end # ── Returns ───────────────────────────────────────────────────── @@ -820,10 +834,10 @@ defmodule Lua.VM.Dispatcher do # `call_function/3` contract. {@op_return_one, base} -> - return_one(:erlang.element(base + 1, regs), state, frames, steps) + return_one(:erlang.element(base + 1, regs), state, frames, instruction_count) {@op_return_zero} -> - return_one(nil, state, frames, steps) + return_one(nil, state, frames, instruction_count) # ── Table opcodes ─────────────────────────────────────────────── # @@ -835,7 +849,7 @@ defmodule Lua.VM.Dispatcher do {@op_new_table, dest} -> {tref, state} = State.alloc_table(state) regs = :erlang.setelement(dest + 1, regs, tref) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_get_table, dest, table_reg, key_reg, name_hint} -> table_val = :erlang.element(table_reg + 1, regs) @@ -850,19 +864,19 @@ defmodule Lua.VM.Dispatcher do case :erlang.map_get(:metatable, table) do nil -> regs = :erlang.setelement(dest + 1, regs, nil) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) _ -> {value, state} = Executor.dispatcher_get_table(table_val, key, state, proto, name_hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end value -> regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {:tref, id} when is_integer(key) or is_binary(key) -> @@ -872,20 +886,20 @@ defmodule Lua.VM.Dispatcher do case data do %{^key => value} -> regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) _ -> case :erlang.map_get(:metatable, table) do nil -> regs = :erlang.setelement(dest + 1, regs, nil) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) _ -> {value, state} = Executor.dispatcher_get_table(table_val, key, state, proto, name_hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end end @@ -894,7 +908,7 @@ defmodule Lua.VM.Dispatcher do Executor.dispatcher_get_table(table_val, key, state, proto, name_hint) regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_set_table, table_reg, key_reg, value_reg, name_hint} -> @@ -902,13 +916,13 @@ defmodule Lua.VM.Dispatcher do key = :erlang.element(key_reg + 1, regs) value = :erlang.element(value_reg + 1, regs) state = Executor.dispatcher_set_table(table_val, key, value, state, proto, name_hint) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_set_field, table_reg, name, value_reg, name_hint} -> table_val = :erlang.element(table_reg + 1, regs) value = :erlang.element(value_reg + 1, regs) state = Executor.dispatcher_set_field(table_val, name, value, state, proto, name_hint) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # `:set_list` with a positive integer count is the table-constructor # form. The `count == 0` sentinel was filtered upstream and never @@ -921,7 +935,7 @@ defmodule Lua.VM.Dispatcher do set_list_into_table(table, regs, start, count, offset, 0) end) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # `:set_list` multi-return tail (`{f(), 1}`): fold the static prefix # `init_count` with the trailing values count the last multi-return @@ -936,7 +950,7 @@ defmodule Lua.VM.Dispatcher do set_list_into_table(table, regs, start, total, offset, 0) end) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_length, dest, source} -> value = :erlang.element(source + 1, regs) @@ -951,22 +965,22 @@ defmodule Lua.VM.Dispatcher do # without __len is the border length of the data map. len = Table.length(table) regs = :erlang.setelement(dest + 1, regs, len) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) _ -> {len, state} = Executor.dispatcher_length(value, state, proto) regs = :erlang.setelement(dest + 1, regs, len) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end v when is_binary(v) -> regs = :erlang.setelement(dest + 1, regs, byte_size(v)) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) _ -> {len, state} = Executor.dispatcher_length(value, state, proto) regs = :erlang.setelement(dest + 1, regs, len) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end # ── numeric_for ───────────────────────────────────────────────── @@ -998,9 +1012,9 @@ defmodule Lua.VM.Dispatcher do state = Executor.dispatcher_close_open_upvalues_at_or_above(state, loop_var) marker = {:cps_for, base, loop_var, body_bc, code, pc + 1} loop_exit = {:loop_exit, code, pc + 1} - dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | cont], frames, steps) + dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | cont], frames, instruction_count) else - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end # ── while_loop / repeat_loop / generic_for ───────────────────── @@ -1013,12 +1027,12 @@ defmodule Lua.VM.Dispatcher do {@op_while_loop, test_reg, cond_bc, body_bc} -> cps = {:cps_while_test, test_reg, cond_bc, body_bc, code, pc + 1} loop_exit = {:loop_exit, code, pc + 1} - dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | cont], frames, steps) + dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | cont], frames, instruction_count) {@op_repeat_loop, test_reg, body_bc, cond_bc} -> cps = {:cps_repeat_body, test_reg, body_bc, cond_bc, code, pc + 1} loop_exit = {:loop_exit, code, pc + 1} - dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | cont], frames, steps) + dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | cont], frames, instruction_count) {@op_generic_for, base, var_regs, body_bc, line} -> # Iterator call follows the same shape as the executor: @@ -1031,19 +1045,19 @@ defmodule Lua.VM.Dispatcher do invariant_state = :erlang.element(base + 2, regs) control = :erlang.element(base + 3, regs) - state = %{state | steps: steps} + state = %{state | instruction_count: instruction_count} {results, state} = Executor.dispatcher_call_value(iter_func, [invariant_state, control], proto, state, line) - steps = state.steps + instruction_count = state.instruction_count case results do [nil | _] -> - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) [] -> - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) [first | _] -> regs = :erlang.setelement(base + 3, regs, first) @@ -1052,7 +1066,7 @@ defmodule Lua.VM.Dispatcher do state = Executor.dispatcher_close_open_upvalues_at_or_above(state, first_var_reg) marker = {:cps_generic_for, base, var_regs, body_bc, line, code, pc + 1} loop_exit = {:loop_exit, code, pc + 1} - dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | cont], frames, steps) + dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | cont], frames, instruction_count) end # ── break ───────────────────────────────────────────────────── @@ -1063,7 +1077,7 @@ defmodule Lua.VM.Dispatcher do {@op_break} -> {exit_code, exit_pc, rest_cont} = find_loop_exit(cont) - dispatch(exit_code, exit_pc, regs, upvalues, proto, state, rest_cont, frames, steps) + dispatch(exit_code, exit_pc, regs, upvalues, proto, state, rest_cont, frames, instruction_count) # ── label / goto ────────────────────────────────────────────── # @@ -1075,16 +1089,16 @@ defmodule Lua.VM.Dispatcher do # enclosing `code` recorded on the unwound markers. {@op_label, _name, _level} -> - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_goto, 0, target_pc, level} -> state = Executor.dispatcher_close_open_upvalues_at_or_above(state, level) - dispatch(code, target_pc, regs, upvalues, proto, state, cont, frames) + dispatch(code, target_pc, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_goto, depth, target_pc, level} -> state = Executor.dispatcher_close_open_upvalues_at_or_above(state, level) {dest_code, rest_cont} = unwind_goto(cont, depth) - dispatch(dest_code, target_pc, regs, upvalues, proto, state, rest_cont, frames) + dispatch(dest_code, target_pc, regs, upvalues, proto, state, rest_cont, frames, instruction_count) # ── Closure construction ────────────────────────────────────── # @@ -1110,7 +1124,7 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(dest + 1, regs, closure) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Upvalue access ──────────────────────────────────────────── # @@ -1125,7 +1139,7 @@ defmodule Lua.VM.Dispatcher do cell_ref = :erlang.element(index + 1, upvalues) value = :erlang.element(source + 1, regs) state = %{state | upvalue_cells: Map.put(state.upvalue_cells, cell_ref, value)} - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_get_open_upvalue, dest, reg} -> value = @@ -1135,7 +1149,7 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(dest + 1, regs, value) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_set_open_upvalue, reg, source} -> state = @@ -1148,11 +1162,11 @@ defmodule Lua.VM.Dispatcher do %{state | upvalue_cells: Map.put(state.upvalue_cells, cell_ref, value)} end - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_close_upvalues, threshold} -> state = Executor.dispatcher_close_open_upvalues_at_or_above(state, threshold) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Vararg ──────────────────────────────────────────────────── # @@ -1167,25 +1181,25 @@ defmodule Lua.VM.Dispatcher do varargs = proto.varargs {regs, n} = write_varargs(regs, base, varargs, 0) state = %{state | multi_return_count: n} - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_vararg, base, count} -> regs = write_varargs_n(regs, base, proto.varargs, count) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Multi-return returns ────────────────────────────────────── {@op_return_proto_varargs} -> - return_multi(proto.varargs, state, frames, steps) + return_multi(proto.varargs, state, frames, instruction_count) {@op_return_collect, base, fixed} -> total = fixed + state.multi_return_count results = collect_args(regs, base, total) - return_multi(results, state, frames, steps) + return_multi(results, state, frames, instruction_count) {@op_return_multi, base, count} -> results = collect_args(regs, base, count) - return_multi(results, state, frames, steps) + return_multi(results, state, frames, instruction_count) # ── Multi-return calls ──────────────────────────────────────── # @@ -1227,7 +1241,7 @@ defmodule Lua.VM.Dispatcher do frame = {code, pc + 1, regs, upvalues, proto, cont, dest, state.open_upvalues} call_info = Executor.dispatcher_call_info(proto, name_hint, 0) - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) State.check_call_depth!(state) state = %{ @@ -1246,17 +1260,24 @@ defmodule Lua.VM.Dispatcher do state, [], [frame | frames], - steps + instruction_count ) {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, total_args) call_info = Executor.dispatcher_call_info(proto, name_hint, 0) - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) State.check_call_depth!(state) - state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1, steps: steps} + + state = %{ + state + | call_stack: [call_info | state.call_stack], + call_depth: state.call_depth + 1, + instruction_count: instruction_count + } + {results, state} = Executor.call_function(closure, args, state) - steps = state.steps + instruction_count = state.instruction_count state = %{state | call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} apply_multi_call_result( @@ -1271,18 +1292,18 @@ defmodule Lua.VM.Dispatcher do state, cont, frames, - steps + instruction_count ) _ -> args = collect_args(regs, base + 1, total_args) - state = %{state | steps: steps} + state = %{state | instruction_count: instruction_count} {results, state} = Executor.dispatcher_call_function(func_value, args, state, proto, name_hint, line) - steps = state.steps + instruction_count = state.instruction_count apply_multi_call_result( result_count, @@ -1296,7 +1317,7 @@ defmodule Lua.VM.Dispatcher do state, cont, frames, - steps + instruction_count ) end @@ -1313,7 +1334,7 @@ defmodule Lua.VM.Dispatcher do {func, state} = Executor.dispatcher_index_method_target(obj, method_name, state, proto, name_hint) regs = :erlang.setelement(base + 2, regs, obj) regs = :erlang.setelement(base + 1, regs, func) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Concatenation ───────────────────────────────────────────── # @@ -1331,19 +1352,19 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(dest + 1, regs, left <> right) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) else {result, state} = Executor.dispatcher_concat(left, right, state, proto) regs = :erlang.setelement(dest + 1, regs, result) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end end end # ── End-of-body handling ──────────────────────────────────────────────── - defp finish_body(regs, upvalues, proto, state, [{next_code, next_pc} | rest_cont], frames, steps) do - dispatch(next_code, next_pc, regs, upvalues, proto, state, rest_cont, frames, steps) + defp finish_body(regs, upvalues, proto, state, [{next_code, next_pc} | rest_cont], frames, instruction_count) do + dispatch(next_code, next_pc, regs, upvalues, proto, state, rest_cont, frames, instruction_count) end # `:numeric_for` body ran to completion. Increment the counter, re-test, @@ -1360,7 +1381,7 @@ defmodule Lua.VM.Dispatcher do state, [{:cps_for, base, loop_var, body_bc, outer_code, outer_pc} = marker, {:loop_exit, _, _} = loop_exit | rest_cont], frames, - steps + instruction_count ) do counter = :erlang.element(base + 1, regs) step = :erlang.element(base + 3, regs) @@ -1370,12 +1391,12 @@ defmodule Lua.VM.Dispatcher do should_continue = if step > 0, do: new_counter <= limit, else: new_counter >= limit if should_continue do - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) regs = :erlang.setelement(loop_var + 1, regs, new_counter) state = Executor.dispatcher_close_open_upvalues_at_or_above(state, loop_var) - dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | rest_cont], frames, steps) + dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | rest_cont], frames, instruction_count) else - dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, steps) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, instruction_count) end end @@ -1391,15 +1412,15 @@ defmodule Lua.VM.Dispatcher do {:loop_exit, _, _} = loop_exit | rest_cont ], frames, - steps + instruction_count ) do case :erlang.element(test_reg + 1, regs) do v when v === nil or v === false -> - dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, steps) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, instruction_count) _ -> cps = {:cps_while_body, test_reg, cond_bc, body_bc, outer_code, outer_pc} - dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, steps) + dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, instruction_count) end end @@ -1415,11 +1436,11 @@ defmodule Lua.VM.Dispatcher do {:loop_exit, _, _} = loop_exit | rest_cont ], frames, - steps + instruction_count ) do - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) cps = {:cps_while_test, test_reg, cond_bc, body_bc, outer_code, outer_pc} - dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, steps) + dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, instruction_count) end # `:repeat_loop`: body just finished. Run the condition next. @@ -1433,10 +1454,10 @@ defmodule Lua.VM.Dispatcher do {:loop_exit, _, _} = loop_exit | rest_cont ], frames, - steps + instruction_count ) do cps = {:cps_repeat_cond, test_reg, body_bc, cond_bc, outer_code, outer_pc} - dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, steps) + dispatch(cond_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, instruction_count) end # `:repeat_loop`: condition just finished. test_reg truthy = exit (Lua's @@ -1451,16 +1472,16 @@ defmodule Lua.VM.Dispatcher do {:loop_exit, _, _} = loop_exit | rest_cont ], frames, - steps + instruction_count ) do case :erlang.element(test_reg + 1, regs) do v when v === nil or v === false -> - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) cps = {:cps_repeat_body, test_reg, body_bc, cond_bc, outer_code, outer_pc} - dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, steps) + dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, instruction_count) _ -> - dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, steps) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, instruction_count) end end @@ -1475,33 +1496,33 @@ defmodule Lua.VM.Dispatcher do {:loop_exit, _, _} = loop_exit | rest_cont ], frames, - steps + instruction_count ) do iter_func = :erlang.element(base + 1, regs) invariant_state = :erlang.element(base + 2, regs) control = :erlang.element(base + 3, regs) - state = %{state | steps: steps} + state = %{state | instruction_count: instruction_count} {results, state} = Executor.dispatcher_call_value(iter_func, [invariant_state, control], proto, state, line) - steps = state.steps + instruction_count = state.instruction_count case results do [nil | _] -> - dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, steps) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, instruction_count) [] -> - dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, steps) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, instruction_count) [first | _] -> - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) regs = :erlang.setelement(base + 3, regs, first) regs = assign_iter_results(regs, var_regs, results, 0) first_var_reg = :erlang.element(1, var_regs) state = Executor.dispatcher_close_open_upvalues_at_or_above(state, first_var_reg) - dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | rest_cont], frames, steps) + dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | rest_cont], frames, instruction_count) end end @@ -1509,8 +1530,8 @@ defmodule Lua.VM.Dispatcher do # body ran past its last instruction with the loop_exit still on top). # Drop the loop_exit and let the next iteration of finish_body see the # cont below it. - defp finish_body(regs, upvalues, proto, state, [{:loop_exit, _, _} | rest_cont], frames, steps) do - finish_body(regs, upvalues, proto, state, rest_cont, frames, steps) + defp finish_body(regs, upvalues, proto, state, [{:loop_exit, _, _} | rest_cont], frames, instruction_count) do + finish_body(regs, upvalues, proto, state, rest_cont, frames, instruction_count) end # Body exhausted with no continuation: prototype ran off the end. Lua @@ -1518,8 +1539,8 @@ defmodule Lua.VM.Dispatcher do # values when control falls off the end, not a single `nil` — the # caller's `result_count` decides how that's projected (nil for a # single-value site, empty slot for a multi-return one). - defp finish_body(_regs, _upvalues, _proto, state, [], frames, steps) do - return_multi([], state, frames, steps) + defp finish_body(_regs, _upvalues, _proto, state, [], frames, instruction_count) do + return_multi([], state, frames, instruction_count) end # ── Return propagation through frames ─────────────────────────────────── @@ -1538,13 +1559,13 @@ defmodule Lua.VM.Dispatcher do # {:multi, B, -2} → expand all into regs[B..], set multi_return_count. # {:multi, B, n>1} → write n results into regs[B..], pad nil. - defp return_one(value, state, [], steps) do + defp return_one(value, state, [], instruction_count) do # Top of this dispatcher sub-evaluation: stamp the tally back into the # state so a caller in the other engine can resume the same budget. - {[value], %{state | steps: steps}} + {[value], %{state | instruction_count: instruction_count}} end - defp return_one(value, state, [frame | rest_frames], steps) do + defp return_one(value, state, [frame | rest_frames], instruction_count) do {code, pc, regs, upvalues, proto, cont, dest, saved_open} = frame # Every dispatcher frame corresponds to a Lua-level call that pushed a # call_stack entry. Pop it on the way out — the interpreter's @@ -1553,14 +1574,14 @@ defmodule Lua.VM.Dispatcher do case dest do :discard -> - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, instruction_count) n when is_integer(n) -> regs = :erlang.setelement(n + 1, regs, value) - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, instruction_count) {:multi, _, -1} -> - return_one(value, state, rest_frames, steps) + return_one(value, state, rest_frames, instruction_count) {:multi, base, -2} -> # The expansion dest may sit past the statically reserved register @@ -1570,13 +1591,13 @@ defmodule Lua.VM.Dispatcher do regs = grow_regs(regs, base + 1) regs = :erlang.setelement(base + 1, regs, value) state = %{state | multi_return_count: 1} - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, instruction_count) {:multi, base, n} when is_integer(n) and n > 1 -> regs = grow_regs(regs, base + n) regs = :erlang.setelement(base + 1, regs, value) regs = pad_nils(regs, base + 1, n - 1) - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, instruction_count) end end @@ -1584,17 +1605,17 @@ defmodule Lua.VM.Dispatcher do # `:return_proto_varargs`, and the non-compiled-callee branch of # `:call_multi`. Mirrors `return_one/3`'s frame-variant handling. - defp return_multi(results, state, [], steps) do - {results, %{state | steps: steps}} + defp return_multi(results, state, [], instruction_count) do + {results, %{state | instruction_count: instruction_count}} end - defp return_multi(results, state, [frame | rest_frames], steps) do + defp return_multi(results, state, [frame | rest_frames], instruction_count) do {code, pc, regs, upvalues, proto, cont, dest, saved_open} = frame state = %{state | open_upvalues: saved_open, call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} case dest do :discard -> - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, instruction_count) n when is_integer(n) -> v = @@ -1604,19 +1625,19 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(n + 1, regs, v) - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, instruction_count) {:multi, _, -1} -> - return_multi(results, state, rest_frames, steps) + return_multi(results, state, rest_frames, instruction_count) {:multi, base, -2} -> regs = write_results(regs, base, results) state = %{state | multi_return_count: length(results)} - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, instruction_count) {:multi, base, n} when is_integer(n) and n > 1 -> regs = write_results_n(regs, base, results, n) - dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, steps) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, instruction_count) end end @@ -1858,11 +1879,24 @@ defmodule Lua.VM.Dispatcher do # and unwinds via `return_multi/3`; this helper is for the synchronous # post-call shape (native, __call metamethod, lua_closure via # call_function). Mirrors `Executor.continue_after_call/11` shape. - defp apply_multi_call_result(0, _base, _results, code, pc, regs, upvalues, proto, state, cont, frames, steps) do - dispatch(code, pc, regs, upvalues, proto, state, cont, frames, steps) + defp apply_multi_call_result( + 0, + _base, + _results, + code, + pc, + regs, + upvalues, + proto, + state, + cont, + frames, + instruction_count + ) do + dispatch(code, pc, regs, upvalues, proto, state, cont, frames, instruction_count) end - defp apply_multi_call_result(1, base, results, code, pc, regs, upvalues, proto, state, cont, frames, steps) do + defp apply_multi_call_result(1, base, results, code, pc, regs, upvalues, proto, state, cont, frames, instruction_count) do first = case results do [v | _] -> v @@ -1870,23 +1904,36 @@ defmodule Lua.VM.Dispatcher do end regs = :erlang.setelement(base + 1, regs, first) - dispatch(code, pc, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc, regs, upvalues, proto, state, cont, frames, instruction_count) end - defp apply_multi_call_result(-1, _base, results, _code, _pc, _regs, _upvalues, _proto, state, _cont, frames, steps) do - return_multi(results, state, frames, steps) + defp apply_multi_call_result( + -1, + _base, + results, + _code, + _pc, + _regs, + _upvalues, + _proto, + state, + _cont, + frames, + instruction_count + ) do + return_multi(results, state, frames, instruction_count) end - defp apply_multi_call_result(-2, base, results, code, pc, regs, upvalues, proto, state, cont, frames, steps) do + defp apply_multi_call_result(-2, base, results, code, pc, regs, upvalues, proto, state, cont, frames, instruction_count) do regs = write_results(regs, base, results) state = %{state | multi_return_count: length(results)} - dispatch(code, pc, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc, regs, upvalues, proto, state, cont, frames, instruction_count) end - defp apply_multi_call_result(n, base, results, code, pc, regs, upvalues, proto, state, cont, frames, steps) + defp apply_multi_call_result(n, base, results, code, pc, regs, upvalues, proto, state, cont, frames, instruction_count) when is_integer(n) and n > 1 do regs = write_results_n(regs, base, results, n) - dispatch(code, pc, regs, upvalues, proto, state, cont, frames, steps) + dispatch(code, pc, regs, upvalues, proto, state, cont, frames, instruction_count) end # Lazy regs-tuple growth. Used at the points where multi-return diff --git a/lib/lua/vm/executor.ex b/lib/lua/vm/executor.ex index 2a41fe33..00f32fab 100644 --- a/lib/lua/vm/executor.ex +++ b/lib/lua/vm/executor.ex @@ -5,12 +5,12 @@ defmodule Lua.VM.Executor do 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, steps) + Signature: do_execute(instructions, registers, upvalues, proto, state, cont, frames, line, instruction_count) 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) - steps — running instruction tally for the `:max_steps` budget, threaded + instruction_count — running instruction tally for the `:max_instructions` budget, threaded as a parameter (not stored in `%State{}`) for the same reason `line` is: it would otherwise force a struct rebuild on the hot path. Incremented only at loop back-edges and call boundaries — @@ -91,7 +91,7 @@ defmodule Lua.VM.Executor do state = %{state | open_upvalues: %{}} {results, regs, state} = - do_execute(instructions, registers, upvalues, proto, state, [], [], 0, state.steps) + do_execute(instructions, registers, upvalues, proto, state, [], [], 0, state.instruction_count) {results, regs, %{state | open_upvalues: saved_open_upvalues}} rescue @@ -161,9 +161,19 @@ defmodule Lua.VM.Executor do # Seed the interpreter tally from the budget carried across the boundary # so an alternating-engine call chain accumulates against one budget # rather than resetting here. The terminal writes the final tally back - # into `state.steps` (see `finish_steps/2`). + # into `state.instruction_count` (see `finish_instructions/2`). {results, _callee_regs, state} = - do_execute(callee_proto.instructions, callee_regs, callee_upvalues, callee_proto, state, [], [], 0, state.steps) + do_execute( + callee_proto.instructions, + callee_regs, + callee_upvalues, + callee_proto, + state, + [], + [], + 0, + state.instruction_count + ) state = %{state | open_upvalues: saved_open_upvalues} {results, state} @@ -655,9 +665,9 @@ defmodule Lua.VM.Executor do # ── Break ────────────────────────────────────────────────────────────────── - defp do_execute([:break | _rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([:break | _rest], regs, upvalues, proto, state, cont, frames, line, instruction_count) do {exit_is, rest_cont} = find_loop_exit(cont) - do_execute(exit_is, regs, upvalues, proto, state, rest_cont, frames, line, steps) + do_execute(exit_is, regs, upvalues, proto, state, rest_cont, frames, line, instruction_count) end # ── Goto ─────────────────────────────────────────────────────────────────── @@ -667,7 +677,7 @@ defmodule Lua.VM.Executor do # `cont` entries leaves the blocks between here and the target; `target_tail` # is the destination block's instruction suffix after the `::label::`. - defp do_execute([{:goto, id} | _rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([{:goto, id} | _rest], regs, upvalues, proto, state, cont, frames, line, instruction_count) do case proto.goto_targets do %{^id => {depth, level, target_tail}} -> # Close upvalue cells for locals allocated beyond the target's scope: @@ -675,7 +685,7 @@ defmodule Lua.VM.Executor do # their captured locals with them, so the next pass through gets fresh # cells — matching Lua 5.3 §3.3.4 block-exit semantics. state = close_open_upvalues_at_or_above(state, level) - do_execute(target_tail, regs, upvalues, proto, state, Enum.drop(cont, depth), frames, line, steps) + do_execute(target_tail, regs, upvalues, proto, state, Enum.drop(cont, depth), frames, line, instruction_count) _ -> raise InternalError, value: "goto target not found" @@ -684,21 +694,31 @@ defmodule Lua.VM.Executor do # ── Label ────────────────────────────────────────────────────────────────── - defp do_execute([{:label, _name, _level, _block_path} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + defp do_execute( + [{:label, _name, _level, _block_path} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── Instructions exhausted — handle continuations and frames ─────────────── - defp do_execute([], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([], regs, upvalues, proto, state, cont, frames, line, instruction_count) 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, steps) + do_execute(next_is, regs, upvalues, proto, state, rest_cont, frames, line, instruction_count) # 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, steps) + do_execute([], regs, upvalues, proto, state, rest_cont, frames, line, instruction_count) # After while condition body — check test_reg; enter body or exit loop [{:cps_while_test, test_reg, loop_body, cond_body, rest, outer_cont} | _] -> @@ -706,35 +726,79 @@ defmodule Lua.VM.Executor do 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, steps) + + do_execute( + loop_body, + regs, + upvalues, + proto, + state, + [body_done | loop_exit_cont], + frames, + line, + instruction_count + ) else - do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, instruction_count) end # After while loop body — restart condition [{:cps_while_body, test_reg, loop_body, cond_body, rest, outer_cont} | _] -> - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) 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, steps) + + do_execute( + cond_body, + regs, + upvalues, + proto, + state, + [cond_check | loop_exit_cont], + frames, + line, + instruction_count + ) # 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, steps) + + do_execute( + cond_body, + regs, + upvalues, + proto, + state, + [cond_check | loop_exit_cont], + frames, + line, + instruction_count + ) # 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, steps) + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, instruction_count) else # Condition false = repeat body - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) 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, steps) + + do_execute( + loop_body, + regs, + upvalues, + proto, + state, + [body_done | loop_exit_cont], + frames, + line, + instruction_count + ) end # After numeric_for body — increment counter and re-check @@ -747,16 +811,16 @@ defmodule Lua.VM.Executor do should_continue = if step > 0, do: new_counter <= limit, else: new_counter >= limit if should_continue do - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) regs = put_elem(regs, loop_var, new_counter) state = close_open_upvalues_at_or_above(state, loop_var) 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, steps) + do_execute(body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line, instruction_count) else - do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, instruction_count) end # After generic_for body — call iterator and re-check @@ -765,15 +829,15 @@ defmodule Lua.VM.Executor do invariant_state = elem(regs, base + 1) control = elem(regs, base + 2) - state = %{state | steps: steps} + state = %{state | instruction_count: instruction_count} {results, state} = call_value(iter_func, [invariant_state, control], proto, state, line) - steps = state.steps + instruction_count = state.instruction_count first_result = List.first(results) if first_result == nil do - do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, instruction_count) else - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) regs = put_elem(regs, base + 2, first_result) regs = @@ -786,41 +850,61 @@ defmodule Lua.VM.Executor do 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, steps) + do_execute(body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line, instruction_count) end # Continuation stack exhausted — check frames for pending function return [] -> case frames do [] -> - {[], regs, finish_steps(state, steps)} + {[], regs, finish_instructions(state, instruction_count)} [frame | rest_frames] -> - do_frame_return([], regs, state, frame, rest_frames, line, steps) + do_frame_return([], regs, state, frame, rest_frames, line, instruction_count) end end end # ── load_constant ────────────────────────────────────────────────────────── - defp do_execute([{:load_constant, dest, value} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:load_constant, dest, value} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── load_boolean ─────────────────────────────────────────────────────────── - defp do_execute([{:load_boolean, dest, value} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:load_boolean, dest, value} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── get_global ───────────────────────────────────────────────────────────── - defp do_execute([{:get_global, dest, name} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([{:get_global, dest, name} | rest], regs, upvalues, proto, state, cont, frames, line, instruction_count) do value = State.get_global(state, name) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── load_env ─────────────────────────────────────────────────────────────── @@ -830,9 +914,9 @@ defmodule Lua.VM.Executor do # environment carries it in upvalue slot 0 (see `Stdlib.compile_loaded_chunk`); # otherwise `_ENV` defaults to the global table `_G`. - defp do_execute([{:load_env, dest} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([{:load_env, dest} | rest], regs, upvalues, proto, state, cont, frames, line, instruction_count) do regs = put_elem(regs, dest, load_env_value(upvalues, state)) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── get_upvalue / set_upvalue ────────────────────────────────────────────── @@ -853,20 +937,40 @@ defmodule Lua.VM.Executor do # same cell. `Map.get/2` keeps the nil-for-dangling-cell semantics the # dispatcher mirrors. - defp do_execute([{:get_upvalue, dest, index} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:get_upvalue, dest, index} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) 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, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── set_upvalue ──────────────────────────────────────────────────────────── - defp do_execute([{:set_upvalue, index, source} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:set_upvalue, index, source} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) 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, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── get_open_upvalue ─────────────────────────────────────────────────────── @@ -875,7 +979,17 @@ defmodule Lua.VM.Executor do # If no cell has been created yet (no closure has captured this register), # the register itself is the source of truth -- the next closure that # captures the register will create a cell from the current register value. - defp do_execute([{:get_open_upvalue, dest, reg} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:get_open_upvalue, dest, reg} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do value = case Map.get(state.open_upvalues, reg) do nil -> elem(regs, reg) @@ -883,7 +997,7 @@ defmodule Lua.VM.Executor do end regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── close_upvalues ───────────────────────────────────────────────────────── @@ -895,9 +1009,19 @@ defmodule Lua.VM.Executor do # slots does not read or overwrite the stale cell. Loop bodies do this on # each iteration boundary in the continuation handlers above; this is the # same operation for non-loop block exits. - defp do_execute([{:close_upvalues, threshold} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:close_upvalues, threshold} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do state = close_open_upvalues_at_or_above(state, threshold) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── set_open_upvalue ─────────────────────────────────────────────────────── @@ -907,7 +1031,17 @@ defmodule Lua.VM.Executor do # holds the value (codegen always emits a move into the register before # set_open_upvalue), and the next closure that captures the register will # create a cell from the current register value. - defp do_execute([{:set_open_upvalue, reg, source} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:set_open_upvalue, reg, source} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do state = case Map.get(state.open_upvalues, reg) do nil -> @@ -918,53 +1052,93 @@ defmodule Lua.VM.Executor do %{state | upvalue_cells: Map.put(state.upvalue_cells, cell_ref, value)} end - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── 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, steps) do - do_execute(rest, regs, upvalues, proto, state, cont, frames, new_line, steps) + defp do_execute( + [{:source_line, new_line, _file} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + _line, + instruction_count + ) do + do_execute(rest, regs, upvalues, proto, state, cont, frames, new_line, instruction_count) end # ── move ─────────────────────────────────────────────────────────────────── - defp do_execute([{:move, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([{:move, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line, instruction_count) do value = elem(regs, source) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── test — push rest as continuation, tail-call body ────────────────────── - defp do_execute([{:test, reg, then_body, else_body} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:test, reg, then_body, else_body} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) 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, steps) + do_execute(body, regs, upvalues, proto, state, [rest | cont], frames, line, instruction_count) end # ── 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, steps) do + defp do_execute( + [{:test_and, dest, source, rest_body} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do value = elem(regs, source) if Value.truthy?(value) do - do_execute(rest_body, regs, upvalues, proto, state, [rest | cont], frames, line, steps) + do_execute(rest_body, regs, upvalues, proto, state, [rest | cont], frames, line, instruction_count) else regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end end # ── 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, steps) do + defp do_execute( + [{:test_or, dest, source, rest_body} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do value = elem(regs, source) if Value.truthy?(value) do regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) else - do_execute(rest_body, regs, upvalues, proto, state, [rest | cont], frames, line, steps) + do_execute(rest_body, regs, upvalues, proto, state, [rest | cont], frames, line, instruction_count) end end @@ -979,11 +1153,11 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) 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, steps) + do_execute(cond_body, regs, upvalues, proto, state, [cond_check | loop_exit_cont], frames, line, instruction_count) end # ── repeat_loop — CPS: body → condition → check → restart ──────────────── @@ -997,16 +1171,26 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) 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, steps) + do_execute(loop_body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line, instruction_count) end # ── numeric_for — CPS ───────────────────────────────────────────────────── - defp do_execute([{:numeric_for, base, loop_var, body} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:numeric_for, base, loop_var, body} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do # Lua 5.3 §3.3.5: the three control values are coerced to numbers using # the same rules as arithmetic operators. If both initial value and step # are integers (after coercion), the loop is done with integers; if @@ -1036,26 +1220,36 @@ defmodule Lua.VM.Executor do 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, steps) + do_execute(body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line, instruction_count) else - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end end # ── generic_for — CPS ───────────────────────────────────────────────────── - defp do_execute([{:generic_for, base, var_regs, body} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:generic_for, base, var_regs, body} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do iter_func = elem(regs, base) invariant_state = elem(regs, base + 1) control = elem(regs, base + 2) - state = %{state | steps: steps} + state = %{state | instruction_count: instruction_count} {results, state} = call_value(iter_func, [invariant_state, control], proto, state, line) - steps = state.steps + instruction_count = state.instruction_count first_result = List.first(results) if first_result == nil do - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) else regs = put_elem(regs, base + 2, first_result) @@ -1071,13 +1265,23 @@ defmodule Lua.VM.Executor do 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, steps) + do_execute(body, regs, upvalues, proto, state, [body_done | loop_exit_cont], frames, line, instruction_count) end end # ── closure ──────────────────────────────────────────────────────────────── - defp do_execute([{:closure, dest, proto_index} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:closure, dest, proto_index} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do nested_proto = Enum.at(proto.prototypes, proto_index) {captured_upvalues_reversed, state} = @@ -1119,7 +1323,7 @@ defmodule Lua.VM.Executor do end regs = put_elem(regs, dest, closure) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── call — Lua closures via CPS frames; native functions inline ──────────── @@ -1133,7 +1337,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do func_value = elem(regs, base) @@ -1161,16 +1365,36 @@ defmodule Lua.VM.Executor do # without going through this branch. args = collect_args(regs, base + 1, total_args) call_info = {proto.source, line, name_hint} - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) State.check_call_depth!(state) # Carry the tally into the dispatcher so the budget spans the - # boundary; `Dispatcher.execute/4` seeds from `state.steps` and + # boundary; `Dispatcher.execute/4` seeds from `state.instruction_count` and # writes its final tally back there. - state = %{state | call_stack: [call_info | state.call_stack], call_depth: state.call_depth + 1, steps: steps} + state = %{ + state + | call_stack: [call_info | state.call_stack], + call_depth: state.call_depth + 1, + instruction_count: instruction_count + } + {results, state} = Dispatcher.execute(callee_proto, args, callee_upvalues, state) - steps = state.steps + instruction_count = state.instruction_count state = %{state | call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} - continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, steps, base, result_count) + + continue_after_call( + results, + regs, + rest, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count, + base, + result_count + ) {:lua_closure, callee_proto, callee_upvalues} -> param_count = callee_proto.param_count @@ -1209,7 +1433,7 @@ defmodule Lua.VM.Executor do call_info = {proto.source, line, name_hint} - steps = State.tick!(state, steps) + instruction_count = State.tick!(state, instruction_count) State.check_call_depth!(state) state = %{ @@ -1229,7 +1453,7 @@ defmodule Lua.VM.Executor do [], [frame | frames], line, - steps + instruction_count ) {:native_func, fun} -> @@ -1248,7 +1472,7 @@ defmodule Lua.VM.Executor do # Carry the tally into the native callback so a callback that # re-enters Lua (pcall, a sort comparator, a gsub function) keeps # accumulating against the same budget instead of restarting it. - state = %{state | steps: steps} + state = %{state | instruction_count: instruction_count} {results, state} = try do @@ -1269,9 +1493,22 @@ defmodule Lua.VM.Executor do restore_position(prev_pos) end - steps = state.steps + instruction_count = state.instruction_count - continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, steps, base, result_count) + continue_after_call( + results, + regs, + rest, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count, + base, + result_count + ) nil -> raise TypeError, @@ -1311,9 +1548,9 @@ defmodule Lua.VM.Executor do call_mm -> args = collect_args(regs, base + 1, total_args) - state = %{state | steps: steps} + state = %{state | instruction_count: instruction_count} {results, state} = call_function(call_mm, [other | args], state) - steps = state.steps + instruction_count = state.instruction_count continue_after_call( results, @@ -1325,7 +1562,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps, + instruction_count, base, result_count ) @@ -1336,7 +1573,7 @@ defmodule Lua.VM.Executor do # ── vararg ───────────────────────────────────────────────────────────────── - defp do_execute([{:vararg, base, count} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([{:vararg, base, count} | rest], regs, upvalues, proto, state, cont, frames, line, instruction_count) do varargs = Map.get(proto, :varargs, []) {regs, state} = @@ -1363,17 +1600,17 @@ defmodule Lua.VM.Executor do {regs, state} end - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── return_vararg ────────────────────────────────────────────────────────── - defp do_execute([{:return_vararg} | _rest], regs, _upvalues, proto, state, _cont, frames, line, steps) do + defp do_execute([{:return_vararg} | _rest], regs, _upvalues, proto, state, _cont, frames, line, instruction_count) do varargs = Map.get(proto, :varargs, []) case frames do - [] -> {varargs, regs, finish_steps(state, steps)} - [frame | rest_frames] -> do_frame_return(varargs, regs, state, frame, rest_frames, line, steps) + [] -> {varargs, regs, finish_instructions(state, instruction_count)} + [frame | rest_frames] -> do_frame_return(varargs, regs, state, frame, rest_frames, line, instruction_count) end end @@ -1388,14 +1625,14 @@ defmodule Lua.VM.Executor do _cont, frames, line, - steps + instruction_count ) do total = fixed_count + state.multi_return_count results = if total > 0, do: for(i <- 0..(total - 1), do: elem(regs, base + i)), else: [] case frames do - [] -> {results, regs, finish_steps(state, steps)} - [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, steps) + [] -> {results, regs, finish_instructions(state, instruction_count)} + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, instruction_count) end end @@ -1404,16 +1641,26 @@ defmodule Lua.VM.Executor do # Fast path: single-value return is by far the most common return shape # (every fib/factorial style recursion hits this every call). Avoid the # comprehension and Range allocation; just read one element. - defp do_execute([{:return, base, 1} | _rest], regs, _upvalues, _proto, state, _cont, frames, line, steps) do + defp do_execute([{:return, base, 1} | _rest], regs, _upvalues, _proto, state, _cont, frames, line, instruction_count) do results = [elem(regs, base)] case frames do - [] -> {results, regs, finish_steps(state, steps)} - [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, steps) + [] -> {results, regs, finish_instructions(state, instruction_count)} + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, instruction_count) end end - defp do_execute([{:return, base, count} | _rest], regs, _upvalues, _proto, state, _cont, frames, line, steps) do + defp do_execute( + [{:return, base, count} | _rest], + regs, + _upvalues, + _proto, + state, + _cont, + frames, + line, + instruction_count + ) do results = cond do count == 0 -> @@ -1429,8 +1676,8 @@ defmodule Lua.VM.Executor do end case frames do - [] -> {results, regs, finish_steps(state, steps)} - [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, steps) + [] -> {results, regs, finish_instructions(state, instruction_count)} + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, instruction_count) end end @@ -1443,20 +1690,40 @@ defmodule Lua.VM.Executor do # for Lua 5.3 §3.4.1 wrap-around; mixed and float-only fall through to # native `+`/`-`/`*`. - defp do_execute([{:add, dest, a, b, _hint_a, _hint_b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) + defp do_execute( + [{:add, dest, a, b, _hint_a, _hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) when is_integer(:erlang.element(a + 1, regs)) and is_integer(:erlang.element(b + 1, regs)) do sum = :erlang.element(a + 1, regs) + :erlang.element(b + 1, regs) regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(sum)) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end - defp do_execute([{:add, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:add, dest, a, b, hint_a, hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do val_a = elem(regs, a) val_b = elem(regs, b) if is_number(val_a) and is_number(val_b) do regs = put_elem(regs, dest, val_a + val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) else src = proto.source @@ -1466,7 +1733,7 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end end @@ -1479,12 +1746,12 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) when is_integer(:erlang.element(a + 1, regs)) and is_integer(:erlang.element(b + 1, regs)) do diff = :erlang.element(a + 1, regs) - :erlang.element(b + 1, regs) regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(diff)) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end defp do_execute( @@ -1496,14 +1763,14 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do val_a = elem(regs, a) val_b = elem(regs, b) if is_number(val_a) and is_number(val_b) do regs = put_elem(regs, dest, val_a - val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) else src = proto.source @@ -1513,7 +1780,7 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end end @@ -1526,12 +1793,12 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) when is_integer(:erlang.element(a + 1, regs)) and is_integer(:erlang.element(b + 1, regs)) do prod = :erlang.element(a + 1, regs) * :erlang.element(b + 1, regs) regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(prod)) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end defp do_execute( @@ -1543,14 +1810,14 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do val_a = elem(regs, a) val_b = elem(regs, b) if is_number(val_a) and is_number(val_b) do regs = put_elem(regs, dest, val_a * val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) else src = proto.source @@ -1560,11 +1827,21 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end end - defp do_execute([{:divide, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:divide, dest, a, b, hint_a, hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1575,7 +1852,7 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end defp do_execute( @@ -1587,7 +1864,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -1599,10 +1876,20 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end - defp do_execute([{:modulo, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:modulo, dest, a, b, hint_a, hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1613,10 +1900,20 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end - defp do_execute([{:power, dest, a, b, hint_a, hint_b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:power, dest, a, b, hint_a, hint_b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1627,12 +1924,22 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end # ── String concatenation ─────────────────────────────────────────────────── - defp do_execute([{:concatenate, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:concatenate, dest, a, b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do left = elem(regs, a) right = elem(regs, b) @@ -1644,13 +1951,13 @@ defmodule Lua.VM.Executor do cond do is_binary(left) and is_binary(right) -> regs = put_elem(regs, dest, concat_checked(left, right, state)) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) (is_binary(left) or is_number(left)) and (is_binary(right) or is_number(right)) -> src = proto.source result = concat_checked(concat_coerce(left, line, src, state), concat_coerce(right, line, src, state), state) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) true -> src = proto.source @@ -1661,7 +1968,7 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end end @@ -1676,7 +1983,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -1690,7 +1997,7 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end defp do_execute( @@ -1702,7 +2009,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -1716,7 +2023,7 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end defp do_execute( @@ -1728,7 +2035,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -1742,7 +2049,7 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end defp do_execute( @@ -1754,7 +2061,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -1766,7 +2073,7 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end defp do_execute( @@ -1778,7 +2085,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do val_a = elem(regs, a) val_b = elem(regs, b) @@ -1790,10 +2097,20 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end - defp do_execute([{:bitwise_not, dest, source, hint} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:bitwise_not, dest, source, hint} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do val = elem(regs, source) src = proto.source @@ -1803,7 +2120,7 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end # ── Comparison operations ────────────────────────────────────────────────── @@ -1811,40 +2128,40 @@ defmodule Lua.VM.Executor do # Comparison fast paths: number-vs-number and string-vs-string skip the # metamethod machinery — neither primitive type can carry __eq/__lt/__le. - defp do_execute([{:equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([{:equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, instruction_count) do val_a = elem(regs, a) val_b = elem(regs, b) cond do is_number(val_a) and is_number(val_b) -> regs = put_elem(regs, dest, val_a == val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) is_binary(val_a) and is_binary(val_b) -> regs = put_elem(regs, dest, val_a == val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) true -> {result, new_state} = try_equality_metamethod(val_a, val_b, state, fn -> lua_equal(val_a, val_b) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end end - defp do_execute([{:less_than, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([{:less_than, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, instruction_count) do val_a = elem(regs, a) val_b = elem(regs, b) cond do is_number(val_a) and is_number(val_b) -> regs = put_elem(regs, dest, val_a < val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) is_binary(val_a) and is_binary(val_b) -> regs = put_elem(regs, dest, val_a < val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) true -> src = proto.source @@ -1853,43 +2170,53 @@ defmodule Lua.VM.Executor do try_binary_metamethod("__lt", val_a, val_b, state, fn -> safe_compare_lt(val_a, val_b, line, src, state) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end end - defp do_execute([{:less_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([{:less_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, instruction_count) do val_a = elem(regs, a) val_b = elem(regs, b) cond do is_number(val_a) and is_number(val_b) -> regs = put_elem(regs, dest, val_a <= val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) is_binary(val_a) and is_binary(val_b) -> regs = put_elem(regs, dest, val_a <= val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) true -> {result, new_state} = compare_le(val_a, val_b, state, line, proto.source) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end end - defp do_execute([{:greater_than, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:greater_than, dest, a, b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do val_a = elem(regs, a) val_b = elem(regs, b) cond do is_number(val_a) and is_number(val_b) -> regs = put_elem(regs, dest, val_a > val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) is_binary(val_a) and is_binary(val_b) -> regs = put_elem(regs, dest, val_a > val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) true -> src = proto.source @@ -1899,71 +2226,91 @@ defmodule Lua.VM.Executor do try_binary_metamethod("__lt", val_b, val_a, state, fn -> safe_compare_lt(val_b, val_a, line, src, state) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end end - defp do_execute([{:greater_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:greater_equal, dest, a, b} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do val_a = elem(regs, a) val_b = elem(regs, b) cond do is_number(val_a) and is_number(val_b) -> regs = put_elem(regs, dest, val_a >= val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) is_binary(val_a) and is_binary(val_b) -> regs = put_elem(regs, dest, val_a >= val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) true -> # Lua 5.3 §3.4.4: a >= b is translated to b <= a. {result, new_state} = compare_le(val_b, val_a, state, line, proto.source) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end end - defp do_execute([{:not_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([{:not_equal, dest, a, b} | rest], regs, upvalues, proto, state, cont, frames, line, instruction_count) do val_a = elem(regs, a) val_b = elem(regs, b) cond do is_number(val_a) and is_number(val_b) -> regs = put_elem(regs, dest, val_a != val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) is_binary(val_a) and is_binary(val_b) -> regs = put_elem(regs, dest, val_a != val_b) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) true -> {eq_result, new_state} = try_equality_metamethod(val_a, val_b, state, fn -> lua_equal(val_a, val_b) end) regs = put_elem(regs, dest, not eq_result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end end # ── Unary operations ─────────────────────────────────────────────────────── - defp do_execute([{:negate, dest, source, hint} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute( + [{:negate, dest, source, hint} | rest], + regs, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count + ) do val = elem(regs, source) src = proto.source {result, new_state} = try_unary_metamethod("__unm", val, state, fn -> safe_negate(val, line, src, hint, state) end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end - defp do_execute([{:not, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([{:not, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line, instruction_count) do result = not Value.truthy?(elem(regs, source)) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end - defp do_execute([{:length, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line, steps) do + defp do_execute([{:length, dest, source} | rest], regs, upvalues, proto, state, cont, frames, line, instruction_count) do value = elem(regs, source) {result, new_state} = @@ -1985,7 +2332,7 @@ defmodule Lua.VM.Executor do end) regs = put_elem(regs, dest, result) - do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end # ── new_table ────────────────────────────────────────────────────────────── @@ -1999,11 +2346,11 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do {tref, state} = State.alloc_table(state) regs = put_elem(regs, dest, tref) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── get_table ────────────────────────────────────────────────────────────── @@ -2017,7 +2364,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do table_val = elem(regs, table_reg) key = elem(regs, key_reg) @@ -2032,17 +2379,17 @@ defmodule Lua.VM.Executor do case :erlang.map_get(:metatable, table) do nil -> regs = put_elem(regs, dest, nil) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) _ -> {value, state} = index_value(table_val, key, state, line, proto.source, name_hint) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end value -> regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end {:tref, id} when is_integer(key) or is_binary(key) -> @@ -2052,25 +2399,25 @@ defmodule Lua.VM.Executor do case :erlang.map_get(:data, table) do %{^key => value} -> regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) _data -> case :erlang.map_get(:metatable, table) do nil -> regs = put_elem(regs, dest, nil) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) _ -> {value, state} = index_value(table_val, key, state, line, proto.source, name_hint) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end end _ -> {value, state} = index_value(table_val, key, state, line, proto.source, name_hint) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end end @@ -2085,7 +2432,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do table_val = elem(regs, table_reg) @@ -2094,7 +2441,7 @@ defmodule Lua.VM.Executor do key = elem(regs, key_reg) value = elem(regs, value_reg) state = table_newindex(table_val, key, value, state) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) _ -> raise_index_type_error(table_val, line, proto.source, name_hint, state) @@ -2112,7 +2459,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do table_val = elem(regs, table_reg) @@ -2128,25 +2475,25 @@ defmodule Lua.VM.Executor do case :erlang.map_get(:data, table) do %{^name => value} -> regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) _data -> case :erlang.map_get(:metatable, table) do nil -> regs = put_elem(regs, dest, nil) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) _ -> {value, state} = index_value(table_val, name, state, line, proto.source, name_hint) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end end _ -> {value, state} = index_value(table_val, name, state, line, proto.source, name_hint) regs = put_elem(regs, dest, value) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end end @@ -2161,7 +2508,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do table_val = elem(regs, table_reg) @@ -2169,7 +2516,7 @@ defmodule Lua.VM.Executor do {:tref, _} -> value = elem(regs, value_reg) state = table_newindex(table_val, name, value, state) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) _ -> raise_index_type_error(table_val, line, proto.source, name_hint, state) @@ -2187,7 +2534,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do {:tref, id} = elem(regs, table_reg) total = init_count + state.multi_return_count @@ -2197,7 +2544,7 @@ defmodule Lua.VM.Executor do Table.put_many(table, set_list_pairs(regs, start, total, offset)) end) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── set_list ─────────────────────────────────────────────────────────────── @@ -2211,7 +2558,7 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do {:tref, id} = elem(regs, table_reg) total = if count == 0, do: state.multi_return_count, else: count @@ -2221,7 +2568,7 @@ defmodule Lua.VM.Executor do Table.put_many(table, set_list_pairs(regs, start, total, offset)) end) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── self ─────────────────────────────────────────────────────────────────── @@ -2235,19 +2582,19 @@ defmodule Lua.VM.Executor do cont, frames, line, - steps + instruction_count ) do obj = elem(regs, obj_reg) {func, state} = index_value(obj, method_name, state, line, proto.source, name_hint) regs = put_elem(regs, base + 1, obj) regs = put_elem(regs, base, func) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── Catch-all for unimplemented instructions ─────────────────────────────── - defp do_execute([instr | _rest], _regs, _upvalues, _proto, _state, _cont, _frames, _line, _steps) do + defp do_execute([instr | _rest], _regs, _upvalues, _proto, _state, _cont, _frames, _line, _instruction_count) do raise InternalError, value: "unimplemented instruction: #{inspect(instr)}" end @@ -2276,9 +2623,9 @@ defmodule Lua.VM.Executor do # is unwinding — never per intra-evaluation return, so the default # `:infinity` path pays only at boundaries, exactly like the call-frame # bookkeeping it sits beside. - defp finish_steps(state, steps), do: %{state | steps: steps} + defp finish_instructions(state, instruction_count), do: %{state | instruction_count: instruction_count} - defp do_frame_return(results, _callee_regs, state, frame, rest_frames, line, steps) do + defp do_frame_return(results, _callee_regs, state, frame, rest_frames, line, instruction_count) do %{ rest: rest, cont: caller_cont, @@ -2302,10 +2649,10 @@ defmodule Lua.VM.Executor do # Return-position call (return f()): pass results to the caller's caller case rest_frames do [] -> - {results, caller_regs, finish_steps(state, steps)} + {results, caller_regs, finish_instructions(state, instruction_count)} [outer_frame | outer_rest_frames] -> - do_frame_return(results, caller_regs, state, outer_frame, outer_rest_frames, line, steps) + do_frame_return(results, caller_regs, state, outer_frame, outer_rest_frames, line, instruction_count) end -2 -> @@ -2316,11 +2663,32 @@ defmodule Lua.VM.Executor do caller_regs = write_list_to_regs(caller_regs, base, results_list) state = %{state | multi_return_count: count} - do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line, steps) + + do_execute( + rest, + caller_regs, + caller_upvalues, + caller_proto, + state, + caller_cont, + rest_frames, + line, + instruction_count + ) 0 -> # No results captured - do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line, steps) + do_execute( + rest, + caller_regs, + caller_upvalues, + caller_proto, + state, + caller_cont, + rest_frames, + line, + instruction_count + ) 1 -> # Fast path: single-result return (the overwhelmingly common case, @@ -2336,7 +2704,18 @@ defmodule Lua.VM.Executor do caller_regs = ensure_regs_capacity(caller_regs, base + 1) caller_regs = :erlang.setelement(base + 1, caller_regs, first) - do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line, steps) + + do_execute( + rest, + caller_regs, + caller_upvalues, + caller_proto, + state, + caller_cont, + rest_frames, + line, + instruction_count + ) n when n > 0 -> # Fixed count: place first n results into caller regs from base @@ -2344,19 +2723,42 @@ defmodule Lua.VM.Executor do caller_regs = ensure_regs_capacity(caller_regs, base + n) caller_regs = write_list_to_regs_n(caller_regs, base, results_list, n) - do_execute(rest, caller_regs, caller_upvalues, caller_proto, state, caller_cont, rest_frames, line, steps) + do_execute( + rest, + caller_regs, + caller_upvalues, + caller_proto, + state, + caller_cont, + rest_frames, + line, + instruction_count + ) end end # ── continue_after_call — place results for native/metamethod calls ───────── - defp continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, steps, base, result_count) do + defp continue_after_call( + results, + regs, + rest, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count, + 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, finish_steps(state, steps)} - [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, steps) + [] -> {results, regs, finish_instructions(state, instruction_count)} + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, instruction_count) end -2 -> @@ -2366,10 +2768,10 @@ defmodule Lua.VM.Executor do regs = write_list_to_regs(regs, base, results_list) state = %{state | multi_return_count: count} - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) 0 -> - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) 1 -> first = @@ -2381,14 +2783,14 @@ defmodule Lua.VM.Executor do regs = ensure_regs_capacity(regs, base + 1) regs = :erlang.setelement(base + 1, regs, first) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) n when n > 0 -> results_list = List.wrap(results) regs = ensure_regs_capacity(regs, base + n) regs = write_list_to_regs_n(regs, base, results_list, n) - do_execute(rest, regs, upvalues, proto, state, cont, frames, line, steps) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end end @@ -2450,7 +2852,17 @@ defmodule Lua.VM.Executor do # Seed/recover the cross-boundary budget tally (see `call_function/3`). {results, _callee_regs, state} = - do_execute(callee_proto.instructions, callee_regs, callee_upvalues, callee_proto, state, [], [], 0, state.steps) + do_execute( + callee_proto.instructions, + callee_regs, + callee_upvalues, + callee_proto, + state, + [], + [], + 0, + state.instruction_count + ) state = %{state | open_upvalues: saved_open_upvalues} {results, state} diff --git a/lib/lua/vm/state.ex b/lib/lua/vm/state.ex index 8717cce9..3562dbb1 100644 --- a/lib/lua/vm/state.ex +++ b/lib/lua/vm/state.ex @@ -21,21 +21,21 @@ defmodule Lua.VM.State do # cap so an allocation bomb is refused deterministically instead # of racing the GC-time heap check. max_string_bytes: Limits.max_string_bytes(), - # `max_steps` is the configured instruction ceiling; `:infinity` + # `max_instructions` is the configured instruction ceiling; `:infinity` # (the default) means no limit. The running tally is NOT stored # here on the per-opcode hot path — it is threaded as a parameter # through the executor / dispatcher loops, mirroring the # `line`-off-State discipline so the default `:infinity` path # carries no per-instruction struct rebuild. See `tick!/2`. - max_steps: :infinity, - # `steps` carries the running tally ACROSS engine boundaries + max_instructions: :infinity, + # `instruction_count` carries the running tally ACROSS engine boundaries # only. The interpreter and dispatcher each thread their tally as # a loop parameter; at an `Executor`↔`Dispatcher` hand-off (where # the struct is already rebuilt to push a call frame) the crossing # engine writes its tally here and the entered engine seeds from # it, so the budget spans a chain that alternates engines instead # of resetting at each boundary. Never written per opcode. - steps: 0, + instruction_count: 0, metatables: %{}, upvalue_cells: %{}, open_upvalues: %{}, @@ -56,8 +56,8 @@ defmodule Lua.VM.State do call_depth: non_neg_integer(), max_call_depth: pos_integer() | :infinity, max_string_bytes: pos_integer(), - max_steps: pos_integer() | :infinity, - steps: non_neg_integer(), + max_instructions: pos_integer() | :infinity, + instruction_count: non_neg_integer(), metatables: map(), upvalue_cells: map(), tables: %{optional(non_neg_integer()) => Table.t()}, @@ -109,11 +109,11 @@ defmodule Lua.VM.State do default `:infinity` path stays free of per-instruction cost. The tally is threaded as a parameter, not stored in `%State{}`. When - `max_steps` is `:infinity` (the default) this is a true no-op: it returns + `max_instructions` is `:infinity` (the default) this is a true no-op: it returns the tally unchanged in a single function-head match, doing no arithmetic and rebuilding no struct, so the default path's only per-boundary cost is this one call. When a finite budget is set it increments the tally and, - once the new tally reaches `max_steps`, raises a catchable Lua + once the new tally reaches `max_instructions`, raises a catchable Lua `"instruction budget exceeded"` runtime error. The clauses are ordered so both the `:infinity` and under-budget cases resolve in a single function-head match. @@ -121,22 +121,23 @@ defmodule Lua.VM.State do The raise reuses the same `Lua.VM.RuntimeError` used by `"stack overflow"`, carrying the raise-time `state:` so `pcall`/`xpcall` recover heap effects for free. The live tally is stamped into that `state:` (the - threaded `steps` is not otherwise in `%State{}`), so `unwind_to/2` can + threaded `instruction_count` is not otherwise in `%State{}`), so `unwind_to/2` can carry it forward and a caught budget error stays spent — a protected call cannot refund the work it burned. """ @spec tick!(t(), non_neg_integer()) :: non_neg_integer() - def tick!(%__MODULE__{max_steps: :infinity}, steps), do: steps + def tick!(%__MODULE__{max_instructions: :infinity}, instruction_count), do: instruction_count - def tick!(%__MODULE__{max_steps: max}, steps) when steps + 1 < max, do: steps + 1 + def tick!(%__MODULE__{max_instructions: max}, instruction_count) when instruction_count + 1 < max, + do: instruction_count + 1 - def tick!(%__MODULE__{call_stack: call_stack} = state, steps) do - steps = steps + 1 + def tick!(%__MODULE__{call_stack: call_stack} = state, instruction_count) do + instruction_count = instruction_count + 1 raise RuntimeError, value: "instruction budget exceeded", call_stack: call_stack, - state: %{state | steps: steps} + state: %{state | instruction_count: instruction_count} end @doc """ @@ -156,9 +157,9 @@ defmodule Lua.VM.State do | `userdata`, `userdata_next_id` | `open_upvalues` | | `metatables`, `upvalue_cells`, `private` | `multi_return_count` | - The instruction tally `steps` is also carried forward (monotonic max), not + The instruction tally `instruction_count` is also carried forward (monotonic max), not reset to the entry value: the work a protected call burned must still count - against the one per-evaluation `:max_steps` budget, so wrapping heavy work in + against the one per-evaluation `:max_instructions` budget, so wrapping heavy work in `pcall` (or looping over `pcall`) cannot escape the cap. Keeping `upvalue_cells` while restoring `open_upvalues` matches reference @@ -182,7 +183,7 @@ defmodule Lua.VM.State do metatables: raised.metatables, upvalue_cells: raised.upvalue_cells, private: raised.private, - steps: max(entry.steps, raised.steps) + instruction_count: max(entry.instruction_count, raised.instruction_count) } end diff --git a/lib/lua/vm/stdlib.ex b/lib/lua/vm/stdlib.ex index 0db8c77a..a3657a5a 100644 --- a/lib/lua/vm/stdlib.ex +++ b/lib/lua/vm/stdlib.ex @@ -340,7 +340,7 @@ defmodule Lua.VM.Stdlib do table = Map.fetch!(state.tables, id) # Eagerly flush any deferred-append `order_tail` and build the O(1) - # iteration memo so subsequent steps are index lookups rather than + # iteration memo so subsequent instruction_count are index lookups rather than # linear scans. The first call to `lua_next` for a given iteration # pays the cost once; the rest see a clean `order` and a live memo. {table, state} = @@ -764,8 +764,8 @@ defmodule Lua.VM.Stdlib do {:ok, proto} <- Lua.Compiler.compile(ast), # `require` runs mid-evaluation: inherit the caller's instruction # budget instead of resetting it, so a looping module body counts - # against the same `:max_steps` and the pre-require work is preserved. - {:ok, results, state} <- Lua.VM.execute(proto, state, reset_steps: false) do + # against the same `:max_instructions` and the pre-require work is preserved. + {:ok, results, state} <- Lua.VM.execute(proto, state, reset_instructions: false) do # Get the return value (or true if no return value) result = case results do diff --git a/test/fixtures/budget_small_module.lua b/test/fixtures/budget_small_module.lua index 71e21681..09040d04 100644 --- a/test/fixtures/budget_small_module.lua +++ b/test/fixtures/budget_small_module.lua @@ -1,4 +1,4 @@ -- A trivial module: requiring it does negligible instruction work. Used to --- prove that `require` runs against the caller's `:max_steps` budget rather +-- prove that `require` runs against the caller's `:max_instructions` budget rather -- than resetting it (the pre-require work must still count). return 1 diff --git a/test/lua/vm/max_steps_test.exs b/test/lua/vm/max_instructions_test.exs similarity index 79% rename from test/lua/vm/max_steps_test.exs rename to test/lua/vm/max_instructions_test.exs index e0e8c540..ea9095df 100644 --- a/test/lua/vm/max_steps_test.exs +++ b/test/lua/vm/max_instructions_test.exs @@ -1,6 +1,6 @@ -defmodule Lua.VM.MaxStepsTest do +defmodule Lua.VM.MaxInstructionsTest do @moduledoc """ - Pins the `:max_steps` instruction budget: a finite budget aborts a + Pins the `:max_instructions` instruction budget: a finite budget aborts a non-terminating script with a catchable `"instruction budget exceeded"` runtime error, the budget is recoverable via `pcall`, it bounds both the interpreter and the compiled-dispatcher path, the budget is fresh per @@ -18,9 +18,9 @@ defmodule Lua.VM.MaxStepsTest do defp eval!(lua, code), do: Lua.eval!(lua, code) - describe ":max_steps enforcement (interpreter path)" do + describe ":max_instructions enforcement (interpreter path)" do test "a finite budget aborts a non-terminating while loop" do - lua = Lua.new(max_steps: 1000) + lua = Lua.new(max_instructions: 1000) assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> eval!(lua, "while true do end") @@ -28,7 +28,7 @@ defmodule Lua.VM.MaxStepsTest do end test "a finite budget aborts a tight numeric-for loop" do - lua = Lua.new(max_steps: 1000) + lua = Lua.new(max_instructions: 1000) assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> eval!(lua, "local s = 0 for i = 1, 1000000000 do s = s + i end return s") @@ -40,7 +40,7 @@ defmodule Lua.VM.MaxStepsTest do # call-boundary increment trips before the recursion exhausts itself, # and the message is the budget error — distinct from the # `:max_call_depth` "stack overflow". - lua = Lua.new(max_steps: 100) + lua = Lua.new(max_instructions: 100) assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> eval!(lua, "local function f() return f() end f()") @@ -48,9 +48,9 @@ defmodule Lua.VM.MaxStepsTest do end end - describe ":max_steps catchability and recovery" do + describe ":max_instructions catchability and recovery" do test "pcall catches the budget error and the VM keeps working afterward" do - lua = Lua.new(max_steps: 1000) + lua = Lua.new(max_instructions: 1000) {[false, msg], lua} = eval!(lua, "return pcall(function() while true do end end)") @@ -64,14 +64,14 @@ defmodule Lua.VM.MaxStepsTest do describe "budget scoping" do test "a loop under the budget returns normally" do - lua = Lua.new(max_steps: 10_000) + lua = Lua.new(max_instructions: 10_000) assert {[5050], _lua} = eval!(lua, "local s = 0 for i = 1, 100 do s = s + i end return s") end test "the budget is fresh per evaluation (no cross-eval leak)" do - lua = Lua.new(max_steps: 5000) + lua = Lua.new(max_instructions: 5000) # First eval consumes ~100 iterations of budget. {[5050], lua} = eval!(lua, "local s = 0 for i = 1, 100 do s = s + i end return s") @@ -93,7 +93,7 @@ defmodule Lua.VM.MaxStepsTest do # Establish a budget that comfortably clears one eval but is far below # the cumulative cost of running it 100 times. - lua = Lua.new(max_steps: 2000) + lua = Lua.new(max_instructions: 2000) final = Enum.reduce(1..100, lua, fn _i, acc -> @@ -110,7 +110,7 @@ defmodule Lua.VM.MaxStepsTest do # its instructions and nested calls. A tight loop calling a helper on # every iteration must still trip the budget — the reset is a # top-level boundary, not a per-call one. - lua = Lua.new(max_steps: 1000) + lua = Lua.new(max_instructions: 1000) code = """ local function step(x) return x + 1 end @@ -137,7 +137,7 @@ defmodule Lua.VM.MaxStepsTest do # A `function` body compiles to a `:compiled_closure`; calling it routes # the loop through `Lua.VM.Dispatcher`, exercising the dispatcher's # back-edge counting rather than the interpreter's. - lua = Lua.new(max_steps: 1000) + lua = Lua.new(max_instructions: 1000) assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> eval!(lua, "local function spin() while true do end end spin()") @@ -147,7 +147,7 @@ defmodule Lua.VM.MaxStepsTest do test "the dispatcher enforces the budget when driven directly" do {:ok, ast} = Parser.parse("local function spin() while true do end end return spin") {:ok, proto} = Compiler.compile(ast, source: "test.lua") - state = %{Stdlib.install(State.new()) | max_steps: 1000} + state = %{Stdlib.install(State.new()) | max_instructions: 1000} # Run the chunk to obtain the compiled closure it returns, then drive # the dispatcher with the closure's prototype directly. @@ -162,23 +162,21 @@ defmodule Lua.VM.MaxStepsTest do describe "cross-engine mutual recursion" do test "the budget bounds recursion that alternates execution engines" do - # A function whose body contains a `goto` cannot be bytecode-encoded, - # so it stays an interpreted `:lua_closure`; a plain arithmetic body - # compiles to a `:compiled_closure`. Pairing them in unbounded mutual - # recursion with no loop on either side forces a hand-off between the - # interpreter and the dispatcher on every call. The budget must span + # A function whose body contains a short-circuit `and`/`or` cannot be + # bytecode-encoded, so it stays an interpreted `:lua_closure`; a plain + # body compiles to a `:compiled_closure`. Pairing them in unbounded + # mutual recursion with no loop on either side forces a hand-off between + # the interpreter and the dispatcher on every call. The budget must span # those hand-offs rather than resetting at each boundary, so this # raises the budget error rather than recursing until `max_call_depth` # (which defaults to `:infinity`) or forever. - lua = Lua.new(max_steps: 1000) + lua = Lua.new(max_instructions: 1000) code = """ local pong - -- `goto` keeps this body off the bytecode path: interpreted closure. + -- short-circuit `and` keeps this body off the bytecode path: interpreted closure. local function ping(n) - ::again:: - if n < 0 then goto again end - return pong(n) + return n and pong(n) end -- Plain body: compiles to a dispatcher closure. pong = function(n) return ping(n) end @@ -198,7 +196,7 @@ defmodule Lua.VM.MaxStepsTest do {:ok, ast} = Parser.parse(""" local pong - local function ping(n) ::again:: if n < 0 then goto again end return pong end + local function ping(n) return n and pong end pong = function(n) return ping end return ping, pong """) @@ -216,8 +214,8 @@ defmodule Lua.VM.MaxStepsTest do describe "budget integrity (non-conservative-accounting regressions)" do test "an interpreted `return ...` terminal stamps the step tally back into state" do # The bottom-frame `{:return_vararg}` terminal must stamp the accumulated - # tally into `state.steps` like every other terminal. If it returns a - # bare state, a compiled caller that reads `steps = state.steps` after the + # tally into `state.instruction_count` like every other terminal. If it returns a + # bare state, a compiled caller that reads `instruction_count = state.instruction_count` after the # interpreted callee returns under-counts, so work done in `return ...` # passthroughs vanishes from the budget. A finite budget keeps the tally # live (the default `:infinity` path charges nothing, so accrual is only @@ -225,9 +223,9 @@ defmodule Lua.VM.MaxStepsTest do run = fn src -> {:ok, ast} = Parser.parse(src) {:ok, proto} = Compiler.compile(ast, source: "test.lua") - state = %{Stdlib.install(State.new()) | max_steps: 1_000_000} + state = %{Stdlib.install(State.new()) | max_instructions: 1_000_000} {:ok, _results, state} = Lua.VM.execute(proto, state) - state.steps + state.instruction_count end work = "local s = 0 for i = 1, 5 do s = s + i end " @@ -243,8 +241,8 @@ defmodule Lua.VM.MaxStepsTest do # exhausted tally forward (monotonic) rather than rewinding it to the # pcall-entry value. Otherwise a `pcall`-in-a-loop pattern re-funds the # inner work every iteration and the single evaluation runs far beyond - # `:max_steps` total instructions before tripping. - lua = Lua.new(max_steps: 2000) + # `:max_instructions` total instructions before tripping. + lua = Lua.new(max_instructions: 2000) assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> eval!(lua, "for i = 1, 1000000 do pcall(function() while true do end end) end") @@ -254,10 +252,10 @@ defmodule Lua.VM.MaxStepsTest do test "require runs the module body against the same budget (no mid-eval reset)" do # `require` re-enters `Lua.VM.execute`, which resets the per-eval tally at # genuine top-level entry only. The pre-require work must still count: with - # a mid-eval reset the 700 pre-require steps would be forgiven, leaving the - # 700 post-require steps under the 1000 budget so the eval would wrongly + # a mid-eval reset the 700 pre-require instruction_count would be forgiven, leaving the + # 700 post-require instruction_count under the 1000 budget so the eval would wrongly # succeed. With the budget preserved, pre + post exceed it and it trips. - lua = Lua.new(sandboxed: [], max_steps: 1000) + lua = Lua.new(sandboxed: [], max_instructions: 1000) code = ~S""" package.path = "./test/fixtures/?.lua" @@ -277,7 +275,7 @@ defmodule Lua.VM.MaxStepsTest do # Sanity companion: requiring the trivial module under a comfortable # budget, with light surrounding work, must succeed (the fix must not # over-count and spuriously trip a legitimate require). - lua = Lua.new(sandboxed: [], max_steps: 100_000) + lua = Lua.new(sandboxed: [], max_instructions: 100_000) code = ~S""" package.path = "./test/fixtures/?.lua" @@ -289,16 +287,16 @@ defmodule Lua.VM.MaxStepsTest do end end - describe ":max_steps validation" do + describe ":max_instructions validation" do test "rejects non-positive integers and non-integers" do - assert_raise ArgumentError, ~r/:max_steps/, fn -> Lua.new(max_steps: 0) end - assert_raise ArgumentError, ~r/:max_steps/, fn -> Lua.new(max_steps: -1) end - assert_raise ArgumentError, ~r/:max_steps/, fn -> Lua.new(max_steps: :nope) end + assert_raise ArgumentError, ~r/:max_instructions/, fn -> Lua.new(max_instructions: 0) end + assert_raise ArgumentError, ~r/:max_instructions/, fn -> Lua.new(max_instructions: -1) end + assert_raise ArgumentError, ~r/:max_instructions/, fn -> Lua.new(max_instructions: :nope) end end test "accepts :infinity and positive integers" do - assert %Lua{} = Lua.new(max_steps: :infinity) - assert %Lua{} = Lua.new(max_steps: 1) + assert %Lua{} = Lua.new(max_instructions: :infinity) + assert %Lua{} = Lua.new(max_instructions: 1) end end end