diff --git a/.agents/plans/B17-vm-max-steps.md b/.agents/plans/B17-vm-max-steps.md new file mode 100644 index 00000000..308c92ad --- /dev/null +++ b/.agents/plans/B17-vm-max-steps.md @@ -0,0 +1,259 @@ +--- +id: B17 +title: "VM instruction budget: configurable :max_steps with catchable exhaustion" +issue: 306 +pr: 320 +branch: feat/vm-max-steps +base: main +status: review +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 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` + +- 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. + +## 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"`); 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; + 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; 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, 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 → 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 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/CHANGELOG.md b/CHANGELOG.md index 210ef39c..466163c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- `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 + `Task` plus wall-clock timeout. Enforced at loop back-edges and call + 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 - **Register tuples are sized to an honest peak, with no slack buffer, on diff --git a/README.md b/README.md index 42477faf..6ffde9b4 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_instructions` caps the number of VM instructions a single evaluation may + execute; exceeding it raises `"instruction budget exceeded"`. + + 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 + +See the [Sandboxing guide](guides/examples/sandboxing.livemd) for details. + ### Metatables and metamethods Full metamethod dispatch is supported (`__index`, `__newindex`, `__call`, diff --git a/guides/examples/sandboxing.livemd b/guides/examples/sandboxing.livemd index 20db0b45..b55572d1 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_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_instructions: 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/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 4f06e634..eeca80d0 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_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. + 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_instructions: 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_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_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_string_bytes: max_string_bytes, + max_instructions: max_instructions } opts @@ -160,6 +176,16 @@ defmodule Lua do ":max_string_bytes must be a positive integer, got: #{inspect(other)}" end + defp validate_max_instructions!(:infinity), do: :infinity + + 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_instructions 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.ex b/lib/lua/vm.ex index e51f7840..b40ba2f5 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_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 + 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` @@ -23,6 +32,18 @@ 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_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.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_instructions: false` to keep the + # module body counting against the same budget. + state = if Keyword.get(opts, :reset_instructions, true), do: %{state | instruction_count: 0}, else: state + # Execute the prototype instructions {results, _final_regs, final_state} = Executor.execute(proto.instructions, registers, [], proto, state) diff --git a/lib/lua/vm/dispatcher.ex b/lib/lua/vm/dispatcher.ex index ff4dcf55..b0d375e3 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, [], []) + # 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.instruction_count`. + {results, state} = dispatch(proto.bytecode, 1, regs, upvalues, proto, state, [], [], state.instruction_count) state = %{state | open_upvalues: saved_open} {results, state} @@ -211,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) when pc > tuple_size(code) do - finish_body(regs, upvalues, proto, state, cont, frames) + 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) 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) + 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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_load_env, dest} -> env = @@ -243,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_get_upvalue, dest, index} -> cell_ref = :erlang.element(index + 1, upvalues) @@ -254,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) + 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) + 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) @@ -275,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end end @@ -297,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end # ── Arithmetic ────────────────────────────────────────────────── @@ -316,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_subtract, dest, a, b, hint_a, hint_b} -> @@ -337,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_multiply, dest, a, b, hint_a, hint_b} -> @@ -358,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_divide, dest, a, b, hint_a, hint_b} -> @@ -383,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_floor_divide, dest, a, b, hint_a, hint_b} -> {value, state} = @@ -398,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_modulo, dest, a, b, hint_a, hint_b} -> {value, state} = @@ -413,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_power, dest, a, b, hint_a, hint_b} -> {value, state} = @@ -428,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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Bitwise ───────────────────────────────────────────────────── # @@ -459,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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_bitwise_or, dest, a, b, hint_a, hint_b} -> @@ -472,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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_bitwise_xor, dest, a, b, hint_a, hint_b} -> @@ -485,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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_shift_left, dest, a, b, hint_a, hint_b} -> @@ -497,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Comparisons ───────────────────────────────────────────────── @@ -521,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_less_equal, dest, a, b} -> @@ -540,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_greater_than, dest, a, b} -> @@ -559,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_greater_equal, dest, a, b} -> @@ -578,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_equal, dest, a, b} -> @@ -597,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_not_equal, dest, a, b} -> @@ -616,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_not, dest, src} -> @@ -634,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Conditional branching ─────────────────────────────────────── # @@ -653,7 +657,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, instruction_count) # ── Calls ─────────────────────────────────────────────────────── # @@ -684,6 +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) + instruction_count = State.tick!(state, instruction_count) State.check_call_depth!(state) state = %{ @@ -701,25 +706,39 @@ defmodule Lua.VM.Dispatcher do callee_proto, state, [], - [frame | frames] + [frame | frames], + instruction_count ) {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, arg_count) call_info = Executor.dispatcher_call_info(proto, name_hint, 0) + 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} + + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) _ -> args = collect_args(regs, base + 1, arg_count) + state = %{state | instruction_count: instruction_count} + {_results, state} = Executor.dispatcher_call_function(func_value, args, state, proto, name_hint, line) - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + instruction_count = state.instruction_count + + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_call_one, base, arg_count, name_hint, line} -> @@ -737,6 +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) + instruction_count = State.tick!(state, instruction_count) State.check_call_depth!(state) state = %{ @@ -754,15 +774,25 @@ defmodule Lua.VM.Dispatcher do callee_proto, state, [], - [frame | frames] + [frame | frames], + instruction_count ) {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, arg_count) call_info = Executor.dispatcher_call_info(proto, name_hint, 0) + 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} + + 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) + instruction_count = state.instruction_count state = %{state | call_stack: tl(state.call_stack), call_depth: state.call_depth - 1} first = @@ -772,14 +802,18 @@ 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, instruction_count) _ -> args = collect_args(regs, base + 1, arg_count) + state = %{state | instruction_count: instruction_count} + {results, state} = Executor.dispatcher_call_function(func_value, args, state, proto, name_hint, line) + instruction_count = state.instruction_count + first = case results do [v | _] -> v @@ -787,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end # ── Returns ───────────────────────────────────────────────────── @@ -800,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) + return_one(:erlang.element(base + 1, regs), state, frames, instruction_count) {@op_return_zero} -> - return_one(nil, state, frames) + return_one(nil, state, frames, instruction_count) # ── Table opcodes ─────────────────────────────────────────────── # @@ -815,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) + 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) @@ -830,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {:tref, id} when is_integer(key) or is_binary(key) -> @@ -852,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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end end @@ -874,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end {@op_set_table, table_reg, key_reg, value_reg, name_hint} -> @@ -882,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) + 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) + 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 @@ -901,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) + 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 @@ -916,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_length, dest, source} -> value = :erlang.element(source + 1, regs) @@ -931,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) + 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) + 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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end # ── numeric_for ───────────────────────────────────────────────── @@ -978,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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) end # ── while_loop / repeat_loop / generic_for ───────────────────── @@ -993,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) + 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) + 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: @@ -1011,15 +1045,19 @@ defmodule Lua.VM.Dispatcher do invariant_state = :erlang.element(base + 2, regs) control = :erlang.element(base + 3, regs) + state = %{state | instruction_count: instruction_count} + {results, state} = Executor.dispatcher_call_value(iter_func, [invariant_state, control], proto, state, line) + instruction_count = state.instruction_count + case results do [nil | _] -> - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) [] -> - dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) [first | _] -> regs = :erlang.setelement(base + 3, regs, first) @@ -1028,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) + dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | cont], frames, instruction_count) end # ── break ───────────────────────────────────────────────────── @@ -1039,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) + dispatch(exit_code, exit_pc, regs, upvalues, proto, state, rest_cont, frames, instruction_count) # ── label / goto ────────────────────────────────────────────── # @@ -1051,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 ────────────────────────────────────── # @@ -1086,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Upvalue access ──────────────────────────────────────────── # @@ -1101,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_get_open_upvalue, dest, reg} -> value = @@ -1111,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) {@op_set_open_upvalue, reg, source} -> state = @@ -1124,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) + 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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Vararg ──────────────────────────────────────────────────── # @@ -1143,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) + 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) + 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) + 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) + return_multi(results, state, frames, instruction_count) {@op_return_multi, base, count} -> results = collect_args(regs, base, count) - return_multi(results, state, frames) + return_multi(results, state, frames, instruction_count) # ── Multi-return calls ──────────────────────────────────────── # @@ -1203,6 +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) + instruction_count = State.tick!(state, instruction_count) State.check_call_depth!(state) state = %{ @@ -1220,25 +1259,66 @@ defmodule Lua.VM.Dispatcher do callee_proto, state, [], - [frame | frames] + [frame | frames], + instruction_count ) {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, total_args) call_info = Executor.dispatcher_call_info(proto, name_hint, 0) + 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} + + 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) + instruction_count = state.instruction_count 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, + instruction_count + ) _ -> args = collect_args(regs, base + 1, total_args) + state = %{state | instruction_count: instruction_count} + {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) + instruction_count = state.instruction_count + + apply_multi_call_result( + result_count, + base, + results, + code, + pc + 1, + regs, + upvalues, + proto, + state, + cont, + frames, + instruction_count + ) end # ── self ────────────────────────────────────────────────────── @@ -1254,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) + dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames, instruction_count) # ── Concatenation ───────────────────────────────────────────── # @@ -1272,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) + 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) + 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) 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, 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, @@ -1300,7 +1380,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, + instruction_count ) do counter = :erlang.element(base + 1, regs) step = :erlang.element(base + 3, regs) @@ -1310,11 +1391,12 @@ defmodule Lua.VM.Dispatcher do should_continue = if step > 0, do: new_counter <= limit, else: new_counter >= limit if should_continue do + 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) + 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) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, instruction_count) end end @@ -1329,15 +1411,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, + 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) + 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) + dispatch(body_bc, 1, regs, upvalues, proto, state, [cps, loop_exit | rest_cont], frames, instruction_count) end end @@ -1352,10 +1435,12 @@ 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, + instruction_count ) do + 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) + 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. @@ -1368,10 +1453,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, + 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) + 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 @@ -1385,15 +1471,17 @@ 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, + instruction_count ) do case :erlang.element(test_reg + 1, regs) do v when v === nil or v === false -> + 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) + 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) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, instruction_count) end end @@ -1407,28 +1495,34 @@ 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, + 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 | instruction_count: instruction_count} + {results, state} = Executor.dispatcher_call_value(iter_func, [invariant_state, control], proto, state, line) + instruction_count = state.instruction_count + 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, instruction_count) [] -> - dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames) + dispatch(outer_code, outer_pc, regs, upvalues, proto, state, rest_cont, frames, instruction_count) [first | _] -> + 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) + dispatch(body_bc, 1, regs, upvalues, proto, state, [marker, loop_exit | rest_cont], frames, instruction_count) end end @@ -1436,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) do - finish_body(regs, upvalues, proto, state, rest_cont, frames) + 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 @@ -1445,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) do - return_multi([], state, frames) + defp finish_body(_regs, _upvalues, _proto, state, [], frames, instruction_count) do + return_multi([], state, frames, instruction_count) end # ── Return propagation through frames ─────────────────────────────────── @@ -1465,11 +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, []) do - {[value], state} + 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 | instruction_count: instruction_count}} end - defp return_one(value, state, [frame | rest_frames]) 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 @@ -1478,14 +1574,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, instruction_count) 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, instruction_count) {:multi, _, -1} -> - return_one(value, state, rest_frames) + return_one(value, state, rest_frames, instruction_count) {:multi, base, -2} -> # The expansion dest may sit past the statically reserved register @@ -1495,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) + 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) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, instruction_count) end end @@ -1509,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, []) do - {results, state} + defp return_multi(results, state, [], instruction_count) do + {results, %{state | instruction_count: instruction_count}} end - defp return_multi(results, state, [frame | rest_frames]) 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) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, instruction_count) n when is_integer(n) -> v = @@ -1529,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) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, instruction_count) {:multi, _, -1} -> - return_multi(results, state, rest_frames) + 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) + 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) + dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames, instruction_count) end end @@ -1783,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) 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, + 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) 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 @@ -1795,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) + 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) do - return_multi(results, state, frames) + 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) 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) + 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) + 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) + 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 71f14790..00f32fab 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, 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) + 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 — + 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, state.instruction_count) {results, regs, %{state | open_upvalues: saved_open_upvalues}} rescue @@ -153,8 +158,22 @@ 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.instruction_count` (see `finish_instructions/2`). {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, + state.instruction_count + ) state = %{state | open_upvalues: saved_open_upvalues} {results, state} @@ -646,9 +665,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, instruction_count) 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, instruction_count) end # ── Goto ─────────────────────────────────────────────────────────────────── @@ -658,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) 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: @@ -666,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) + do_execute(target_tail, regs, upvalues, proto, state, Enum.drop(cont, depth), frames, line, instruction_count) _ -> raise InternalError, value: "goto target not found" @@ -675,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) 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, + 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) 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) + 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) + 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} | _] -> @@ -697,33 +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) + + 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) + 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} | _] -> + 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) + + 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) + + 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) + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, instruction_count) else # Condition false = repeat body + 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) + + 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 @@ -736,15 +811,16 @@ defmodule Lua.VM.Executor do should_continue = if step > 0, do: new_counter <= limit, else: new_counter >= limit if should_continue do + 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) + 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) + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, instruction_count) end # After generic_for body — call iterator and re-check @@ -753,12 +829,15 @@ defmodule Lua.VM.Executor do invariant_state = elem(regs, base + 1) control = elem(regs, base + 2) + state = %{state | instruction_count: instruction_count} {results, state} = call_value(iter_func, [invariant_state, control], proto, state, line) + 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) + do_execute(rest, regs, upvalues, proto, state, outer_cont, frames, line, instruction_count) else + instruction_count = State.tick!(state, instruction_count) regs = put_elem(regs, base + 2, first_result) regs = @@ -771,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) + 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, state} + {[], regs, finish_instructions(state, instruction_count)} [frame | rest_frames] -> - do_frame_return([], regs, state, frame, rest_frames, line) + 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) 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) + 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) 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) + 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) 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── load_env ─────────────────────────────────────────────────────────────── @@ -815,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) 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── get_upvalue / set_upvalue ────────────────────────────────────────────── @@ -838,18 +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) 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) 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, + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── get_open_upvalue ─────────────────────────────────────────────────────── @@ -858,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) 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) @@ -866,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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── close_upvalues ───────────────────────────────────────────────────────── @@ -878,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) 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── set_open_upvalue ─────────────────────────────────────────────────────── @@ -890,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) 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 -> @@ -901,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) + 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) 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, + 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) 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) + 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) 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) + 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) 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) + 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) + 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) 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) + 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) + do_execute(rest_body, regs, upvalues, proto, state, [rest | cont], frames, line, instruction_count) end end @@ -961,11 +1152,12 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + 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) + 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 ──────────────── @@ -978,16 +1170,27 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + 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) + 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) 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 @@ -1017,24 +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) + 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) + 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) 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 | instruction_count: instruction_count} {results, state} = call_value(iter_func, [invariant_state, control], proto, state, line) + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) else regs = put_elem(regs, base + 2, first_result) @@ -1050,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) + 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) 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} = @@ -1098,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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── call — Lua closures via CPS frames; native functions inline ──────────── @@ -1111,7 +1336,8 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + instruction_count ) do func_value = elem(regs, base) @@ -1139,11 +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} + 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} + # Carry the tally into the dispatcher so the budget spans the + # 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, + instruction_count: instruction_count + } + {results, state} = Dispatcher.execute(callee_proto, args, callee_upvalues, state) + 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, 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 @@ -1182,6 +1433,7 @@ defmodule Lua.VM.Executor do call_info = {proto.source, line, name_hint} + instruction_count = State.tick!(state, instruction_count) State.check_call_depth!(state) state = %{ @@ -1200,7 +1452,8 @@ defmodule Lua.VM.Executor do state, [], [frame | frames], - line + line, + instruction_count ) {:native_func, fun} -> @@ -1216,6 +1469,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 | instruction_count: instruction_count} + {results, state} = try do case fun.(args, state) do @@ -1235,7 +1493,22 @@ 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) + instruction_count = state.instruction_count + + continue_after_call( + results, + regs, + rest, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count, + base, + result_count + ) nil -> raise TypeError, @@ -1275,8 +1548,24 @@ defmodule Lua.VM.Executor do call_mm -> args = collect_args(regs, base + 1, total_args) + state = %{state | instruction_count: instruction_count} {results, state} = call_function(call_mm, [other | args], state) - continue_after_call(results, regs, rest, upvalues, proto, state, cont, frames, line, base, result_count) + instruction_count = state.instruction_count + + continue_after_call( + results, + regs, + rest, + upvalues, + proto, + state, + cont, + frames, + line, + instruction_count, + base, + result_count + ) end end end @@ -1284,7 +1573,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, instruction_count) do varargs = Map.get(proto, :varargs, []) {regs, state} = @@ -1311,17 +1600,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, instruction_count) 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, instruction_count) 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) + [] -> {varargs, regs, finish_instructions(state, instruction_count)} + [frame | rest_frames] -> do_frame_return(varargs, regs, state, frame, rest_frames, line, instruction_count) end end @@ -1335,14 +1624,15 @@ defmodule Lua.VM.Executor do state, _cont, frames, - line + line, + 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, state} - [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line) + [] -> {results, regs, finish_instructions(state, instruction_count)} + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, instruction_count) end end @@ -1351,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) 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, state} - [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line) + [] -> {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) do + defp do_execute( + [{:return, base, count} | _rest], + regs, + _upvalues, + _proto, + state, + _cont, + frames, + line, + instruction_count + ) do results = cond do count == 0 -> @@ -1376,8 +1676,8 @@ defmodule Lua.VM.Executor do end case frames do - [] -> {results, regs, state} - [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line) + [] -> {results, regs, finish_instructions(state, instruction_count)} + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, instruction_count) end end @@ -1390,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) + 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) + 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) 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) else src = proto.source @@ -1413,24 +1733,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, instruction_count) 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, + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) 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, + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) else src = proto.source @@ -1440,24 +1780,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, instruction_count) 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, + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) 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, + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) else src = proto.source @@ -1467,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) + 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) 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 @@ -1482,10 +1852,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, instruction_count) 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, + instruction_count + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1496,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) + 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) 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 @@ -1510,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) + 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) 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 @@ -1524,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) + 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) 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) @@ -1541,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) + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) true -> src = proto.source @@ -1558,13 +1968,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, instruction_count) 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, + instruction_count + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1577,10 +1997,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, instruction_count) 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, + instruction_count + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1593,10 +2023,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, instruction_count) 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, + instruction_count + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1609,10 +2049,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, instruction_count) 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, + instruction_count + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1623,10 +2073,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, instruction_count) 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, + instruction_count + ) do val_a = elem(regs, a) val_b = elem(regs, b) src = proto.source @@ -1637,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) + 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) 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 @@ -1650,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) + do_execute(rest, regs, upvalues, proto, new_state, cont, frames, line, instruction_count) end # ── Comparison operations ────────────────────────────────────────────────── @@ -1658,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) 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) + 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) + 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) + 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) 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) + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) true -> src = proto.source @@ -1700,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) + 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) 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) + 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) + 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) + 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) 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) + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) true -> src = proto.source @@ -1746,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) + 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) 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) + 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) + 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) + 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) 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) + 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) + 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) + 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) 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) + 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) 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) + 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) 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} = @@ -1832,15 +2332,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, instruction_count) 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, + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── get_table ────────────────────────────────────────────────────────────── @@ -1853,7 +2363,8 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + instruction_count ) do table_val = elem(regs, table_reg) key = elem(regs, key_reg) @@ -1868,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) + 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) + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end {:tref, id} when is_integer(key) or is_binary(key) -> @@ -1888,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) + 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) + 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) + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end end @@ -1920,7 +2431,8 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + instruction_count ) do table_val = elem(regs, table_reg) @@ -1929,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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) _ -> raise_index_type_error(table_val, line, proto.source, name_hint, state) @@ -1946,7 +2458,8 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + instruction_count ) do table_val = elem(regs, table_reg) @@ -1962,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) + 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) + 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) + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end end @@ -1994,7 +2507,8 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + instruction_count ) do table_val = elem(regs, table_reg) @@ -2002,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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) _ -> raise_index_type_error(table_val, line, proto.source, name_hint, state) @@ -2019,7 +2533,8 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + instruction_count ) do {:tref, id} = elem(regs, table_reg) total = init_count + state.multi_return_count @@ -2029,12 +2544,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, instruction_count) 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, + instruction_count + ) do {:tref, id} = elem(regs, table_reg) total = if count == 0, do: state.multi_return_count, else: count @@ -2043,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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end # ── self ─────────────────────────────────────────────────────────────────── @@ -2056,19 +2581,20 @@ defmodule Lua.VM.Executor do state, cont, frames, - line + line, + 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) + 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) do + defp do_execute([instr | _rest], _regs, _upvalues, _proto, _state, _cont, _frames, _line, _instruction_count) do raise InternalError, value: "unimplemented instruction: #{inspect(instr)}" end @@ -2090,7 +2616,16 @@ 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 + # 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_instructions(state, instruction_count), do: %{state | instruction_count: instruction_count} + + defp do_frame_return(results, _callee_regs, state, frame, rest_frames, line, instruction_count) do %{ rest: rest, cont: caller_cont, @@ -2114,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, state} + {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) + do_frame_return(results, caller_regs, state, outer_frame, outer_rest_frames, line, instruction_count) end -2 -> @@ -2128,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) + + 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) + 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, @@ -2148,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) + + 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 @@ -2156,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) + 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, 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, state} - [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line) + [] -> {results, regs, finish_instructions(state, instruction_count)} + [frame | rest_frames] -> do_frame_return(results, regs, state, frame, rest_frames, line, instruction_count) end -2 -> @@ -2178,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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) 0 -> - do_execute(rest, regs, upvalues, proto, state, cont, frames, line) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) 1 -> first = @@ -2193,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) + 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) + do_execute(rest, regs, upvalues, proto, state, cont, frames, line, instruction_count) end end @@ -2260,8 +2850,19 @@ 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) + 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 74f416ad..3562dbb1 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,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_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_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. + instruction_count: 0, metatables: %{}, upvalue_cells: %{}, open_upvalues: %{}, @@ -40,6 +56,8 @@ defmodule Lua.VM.State do call_depth: non_neg_integer(), max_call_depth: pos_integer() | :infinity, max_string_bytes: pos_integer(), + max_instructions: pos_integer() | :infinity, + instruction_count: non_neg_integer(), metatables: map(), upvalue_cells: map(), tables: %{optional(non_neg_integer()) => Table.t()}, @@ -79,7 +97,47 @@ 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 """ + 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_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_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. + + 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 `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_instructions: :infinity}, instruction_count), do: instruction_count + + 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, instruction_count) do + instruction_count = instruction_count + 1 + + raise RuntimeError, + value: "instruction budget exceeded", + call_stack: call_stack, + state: %{state | instruction_count: instruction_count} end @doc """ @@ -99,6 +157,11 @@ defmodule Lua.VM.State do | `userdata`, `userdata_next_id` | `open_upvalues` | | `metatables`, `upvalue_cells`, `private` | `multi_return_count` | + 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_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 upvalue semantics: cells captured before the protected call keep their mutated values, while cells opened by the unwound frames become @@ -119,7 +182,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, + 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 875b942c..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} = @@ -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_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 new file mode 100644 index 00000000..09040d04 --- /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_instructions` budget rather +-- than resetting it (the pre-require work must still count). +return 1 diff --git a/test/lua/vm/max_instructions_test.exs b/test/lua/vm/max_instructions_test.exs new file mode 100644 index 00000000..ea9095df --- /dev/null +++ b/test/lua/vm/max_instructions_test.exs @@ -0,0 +1,302 @@ +defmodule Lua.VM.MaxInstructionsTest do + @moduledoc """ + 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 + 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_instructions enforcement (interpreter path)" do + test "a finite budget aborts a non-terminating while loop" do + lua = Lua.new(max_instructions: 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_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") + 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_instructions: 100) + + assert_raise RuntimeException, ~r/instruction budget exceeded/, fn -> + eval!(lua, "local function f() return f() end f()") + end + end + end + + describe ":max_instructions catchability and recovery" do + test "pcall catches the budget error and the VM keeps working afterward" do + lua = Lua.new(max_instructions: 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_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_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") + + # 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 + + 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_instructions: 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_instructions: 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 + 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_instructions: 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_instructions: 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 "cross-engine mutual recursion" do + test "the budget bounds recursion that alternates execution engines" do + # 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_instructions: 1000) + + code = """ + local pong + -- short-circuit `and` keeps this body off the bytecode path: interpreted closure. + local function ping(n) + return n and 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) return n and 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 "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.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 + # 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") + state = %{Stdlib.install(State.new()) | max_instructions: 1_000_000} + {:ok, _results, state} = Lua.VM.execute(proto, state) + state.instruction_count + 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_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") + 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 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_instructions: 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_instructions: 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_instructions validation" do + test "rejects non-positive integers and non-integers" do + 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_instructions: :infinity) + assert %Lua{} = Lua.new(max_instructions: 1) + end + end +end