Skip to main content

Vm

Struct Vm 

Source
pub struct Vm {
    pub heap: Heap,
    pub capi_stack: Vec<Value>,
    pub capi_cstr_pin: Option<CString>,
    /* private fields */
}
Expand description

A Lua virtual machine: one OS thread’s worth of Lua state.

§Threading model

Vm is !Send + !Sync. The GC uses Gc<T> = NonNull<T> over an intrusive mark-sweep heap (not Rc<RefCell<T>>), and the trace JIT side-table uses Rc<CompiledTrace> — both single-threaded by design. Embedders that want concurrency spawn one Vm per OS thread (or per single-thread Tokio worker) and exchange data via channels. See docs/threading.md for canonical embedding patterns including Tokio current_thread, LocalSet on multi-thread, and Vm-per-OS-thread + channels.

The constraint is enforced at compile time:

fn must_be_send<T: Send>() {}
must_be_send::<luna_core::Vm>(); // error[E0277]: `Vm` cannot be sent between threads safely

A future feature = "send" (post-v1.1 sprint) will gate an opt-in Arc<RwLock<T>> mode with a hard ≤8% perf regression budget. See .dev/rfcs/v1.1-rfc-vm-send-sync.md for the design.

Fields§

§heap: Heap

The GC heap owned by this VM. Embedders normally interact via the Vm methods (load / call_value / set_global / …) rather than the heap directly.

§capi_stack: Vec<Value>

C ABI scratch (capi module): the host-visible value stack that C callers operate on via lua_pushinteger / lua_tostring / etc. Kept here (instead of in a separate LuaState wrapper) so the trampoline that bridges to a LuaCFunction can safely cast the Vm pointer it already holds to the public *mut LuaState type without any aliasing of &mut Vm against &mut LuaState.vm.

§capi_cstr_pin: Option<CString>

Pinned CString backing the pointer last returned by lua_tostring; valid until the next lua_tostring on the same Vm.

Implementations§

Source§

impl Vm

Source

pub const DEFAULT_LOADER_INPUT_BUDGET: usize

Default loader input budget — 256 MiB.

Vm::load and the Lua-level load(reader, ...) both refuse sources whose byte length crosses this cap, returning the PUC-shaped not enough memory error rather than letting the host allocator try (and crash) to hold the next chunk.

Source

pub fn new(version: LuaVersion) -> Vm

Build a fully-loaded Vm — the default for embedders that want PUC’s standard library surface. Equivalent to Vm::new_minimal(version) followed by vm.open_all_libs().

Source

pub fn new_minimal(version: LuaVersion) -> Vm

P09 embedding: build a Vm with no standard libraries loaded. Embedders that want a sandbox (Redis-style scripts, in-game scripting with a curated API) call this and then open_base / open_math / etc. selectively. The Vm is otherwise fully initialized (main coroutine, RNG seed, GC) so eval and call_value are immediately usable.

Source

pub fn install_jit_backend<C, T>(&mut self, chunk: C, trace: T)
where C: IntChunkCompiler + 'static, T: TraceCompiler + 'static,

v1.1 A1 Session C — install a caller-supplied JIT backend. The luna crate uses this to swap in its CraneliftBackend; tests or third-party backends pass their own crate::jit::IntChunkCompiler / crate::jit::TraceCompiler implementations. Re-installing on a Vm whose closures already populated Proto.jit: JitProtoState::Compiled does NOT evict those cached entries — call right after construction for a clean swap.

Naming: install_jit_backend (not install_default_jit) because the “default” in luna-core is NullJitBackend; the “default JIT” lives in the luna crate.

Source

pub fn install_jit_storage<S>(&mut self, storage: S)
where S: JitStorage + 'static,

v2.0 Track J sub-step J-B — install a caller-supplied JIT storage holder. Default is crate::jit::NullJitStorage; the luna_jit crate’s install_default_jit pairs this with install_jit_backend(CraneliftBackend, CraneliftBackend) to also install a fresh CraneliftJitStorage. Storage holds the per-Vm JIT cache + handle collections that used to be thread_local!s in luna_jit::jit_backend.

Idempotency: re-installing storage on a Vm that already holds compiled-trace pointers WILL evict their owners (the old CraneliftJitStorage’s JITModules drop their mmap pages). Call right after construction for a clean swap.

Source

pub fn install_null_jit(&mut self)

v1.1 A1 Session A — install the no-op JIT backend. try_compile reports “skipped” so every closure stays on the interpreter path, and the trace recorder’s compile attempt always returns None. Intended for tests that want to verify the trait boundary works in a JIT-free configuration, and for the future luna-core build path that ships without Cranelift.

Calling this on a Vm whose closures already populated Proto.jit: JitProtoState::Compiled does NOT evict those cached entries — the dispatcher will still call into them. For a truly JIT-free run, call this immediately after construction.

Source

pub fn open_all_libs(&mut self)

Open the entire 5.5 standard library on a new_minimal-built Vm. Vm::new calls this; sandboxed embedders open libraries one at a time instead (open_base, open_math, open_table, …).

Source

pub fn open_base(&mut self)

Install the base library (print, type, pairs, tostring, pcall, error, assert, select, setmetatable, getmetatable, rawequal, rawget, rawset, rawlen, next, tonumber, collectgarbage, warn on 5.4+, _VERSION, _G, plus 5.1’s retired globals unpack, loadstring, setfenv, getfenv, newproxy, gcinfo when version == 5.1). Safe to call at most once per Vm.

Source

pub fn open_math(&mut self)

Install the math standard library.

Source

pub fn open_table(&mut self)

Install the table standard library.

Source

pub fn open_string(&mut self)

Install the string standard library (and the shared string metatable).

Source

pub fn open_utf8(&mut self)

Install the utf8 standard library (5.3+).

Source

pub fn open_os_io(&mut self)

os and io are merged because file userdata shares state with both (io.tmpname and os.tmpname are the same function, io.popen wraps os.execute’s shell).

Source

pub fn open_debug(&mut self)

Install the debug standard library (introspection / hooks). Off by default for sandbox embedders.

Source

pub fn open_coroutine(&mut self)

Install the coroutine standard library.

Source

pub fn open_package(&mut self)

package plus the 5.1-only module and package.seeall aliases.

Source

pub fn open_bit32(&mut self)

5.2-only bit32 library (5.3+ retired in favour of native bitwise ops on 64-bit integers).

Source

pub fn native(&mut self, f: NativeFn) -> Value

Allocate a native function object (no upvalues): builtin registration.

Source

pub fn native_with(&mut self, f: NativeFn, upvals: Box<[Value]>) -> Value

Allocate a native function object with captured upvalues.

Source

pub fn set_string_metatable(&mut self, mt: Option<Gc<Table>>)

Install the shared string metatable (string library, P04).

Source

pub fn globals(&self) -> Gc<Table>

The current globals table (_G / _ENV source for new chunks).

Source

pub fn version(&self) -> LuaVersion

The Lua dialect this VM was constructed for (5.1 / 5.2 / 5.3 / 5.4 / 5.5). Determines numeric semantics, available standard libraries, and metamethod behavior.

Source

pub fn set_global<V: IntoValue>( &mut self, name: &str, v: V, ) -> Result<(), LuaError>

Set a global by name. v may be any IntoValue: a primitive (i64, f64, bool, &str, String, Vec<u8>), a Value directly, an Option<T>, or a Gc<Table> / Gc<LuaClosure> / Gc<NativeClosure> handle.

Returns Err(LuaError) only if the globals table overflows (extremely unlikely in practice — MAX_ASIZE = 1 << 27). String interning + key construction cannot fail.

let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
vm.set_global("answer", 42).unwrap();
vm.set_global("ratio", 0.5_f64).unwrap();
vm.set_global("hello", "world").unwrap();
let r = vm.eval("return answer, ratio, hello").unwrap();
assert_eq!(r.len(), 3);
Source

pub fn define_macro(&mut self, name: &str, m: Box<dyn Macro>)

v1.3 Phase ML — register a MacroLua macro under name. Inert under non-MacroLua dialects (the macro is stored but the load path only consults the registry when self.version == LuaVersion::MacroLua).

name is stored without the leading @ — source code writes @double(x) to invoke a macro registered as "double".

Source

pub fn clear_macros(&mut self)

v1.3 Phase ML — drop all MacroLua macros (built-in + custom). Mostly useful for tests / dogfood resets.

Source

pub fn load( &mut self, src: &[u8], chunkname: &[u8], ) -> Result<Gc<LuaClosure>, SyntaxError>

Parse + compile a chunk and close it over the globals table.

Source

pub fn eval(&mut self, src: &str) -> Result<Vec<Value>, LuaError>

Compile and run src as an anonymous chunk; return its results. Source name in the traceback is "=eval". Syntax errors are surfaced as LuaError carrying the formatted PUC-style message (interned through the heap so the error value composes with pcall / error_text like any runtime error).

Source

pub fn error_text(&self, e: &LuaError) -> String

Render an error value for messages/tests. Non-string errors — error({code=…}), error(42), etc. — collapse to a type tag ("(error object is a table value)"); embedders that need structured payloads should inspect e.0 directly. Errors whose text starts with "native panic:" indicate a Rust panic crossed catch_unwind — the Vm may be inconsistent and should be dropped (do not reuse).

Source

pub fn call_value( &mut self, f: Value, args: &[Value], ) -> Result<Vec<Value>, LuaError>

Call any callable value from the host (or from natives like pcall).

Source

pub fn collect_garbage(&mut self) -> usize

Run a full collection with the VM’s roots, then run any __gc finalizers the collection scheduled. A no-op (returns 0) when already inside a finalizer — the collector is not reentrant (PUC).

Source

pub fn warn_log_take(&mut self) -> Vec<Vec<u8>>

Drain the in-process warning log (one entry per emitted message, sans "Lua warning: " prefix and newline). For test harnesses that want to assert on warn output without scraping stderr.

Source

pub fn set_instr_budget(&mut self, budget: Option<i64>)

Arm the cooperative instruction budget (P09 embedding). The run loop decrements this once per dispatch turn; on zero it raises a catchable "instruction budget exceeded" error and disarms itself so the host can resume with a fresh budget on the next call. None removes the cap. Pass Some(n) before eval/call_value for the embedder’s short-script semantics.

Source

pub fn instr_budget_remaining(&self) -> Option<i64>

Remaining instruction budget (None when unbounded).

Source

pub fn set_jit_enabled(&mut self, enabled: bool)

Toggle the cranelift JIT (P11). Default true. Sandbox embedders must disable JIT when relying on instr_budget — see the jit_enabled field doc for the rationale.

Source

pub fn jit_enabled(&self) -> bool

Current JIT enable state.

Source

pub fn set_trace_jit_enabled(&mut self, enabled: bool)

Toggle the trace JIT (P12). Off by default while the sprint develops. When enabled, hot back-edges are counted on Proto.trace_hot_count; once the counter passes TRACE_HOT_THRESHOLD, the dispatch loop enters recording mode at the back-edge target. Stays a no-op until S2’s trace lowerer and S3’s dispatcher land.

P16-A — opt-in flag for the self-link cycle catch. See field docs for the correctness blocker. Default false.

Current state of the P16-A self-link cycle catch.

Source

pub fn trace_jit_enabled(&self) -> bool

Current trace-JIT enable state.

Source

pub fn trace_closed_count(&self) -> u64

Number of traces that have closed cleanly (looped back to the head PC) since this Vm was constructed. Cumulative; used by tests + tuning. Will become the dominant signal once S2’s compile + cache lands.

Source

pub fn trace_aborted_count(&self) -> u64

Number of traces that have aborted (exceeded MAX_TRACE_LEN or hit an un-recordable op — the latter lands at S2).

Source

pub fn trace_inline_abort_count(&self) -> u64

P13-S13-G v2 — number of compiled traces whose close shape is TraceEnd::InlineAbort (depth>0 boundary). Such traces pin dispatchable=false because the dispatcher can’t resume at a depth>0 PC without the matching CallFrames. S4-step4b’s frame-mat helper could synthesise those, but the InlineAbort emit path isn’t wired up yet — fresh pickup work for S13-G v2-full.

Source

pub fn trace_dispatch_off_reasons(&self) -> &[&'static str]

P13-S13-G v2.5 — see JitCounters::dispatch_off_reasons.

Source

pub fn trace_compile_failed_reasons(&self) -> &[&'static str]

P13-S13-G v2.6 — see JitCounters::compile_failed_reasons.

Source

pub fn trace_closed_lens(&self) -> &[(bool, usize)]

P13-S13-H — see JitCounters::closed_lens. Returns (is_call_triggered, ops_len) for every trace that closed.

Source

pub fn trace_close_cause_counts(&self) -> &HashMap<&'static str, u64>

v2.0 Track-R R2 — see [crate::vm::jit_state::JitCounters::close_cause_counts]. Per-reason close-cause counts (recorder-side abort/discard + lowerer-side dispatch_off labels) keyed by &'static str.

v2.0 Track-R R3b — number of compiled traces whose CompiledTrace.downrec_link is Some(_) (lowerer’s downrec_idx_opt arm emitted the stitch sentinel + caller-pc guard scaffold). R3b regression pin checks >= 1 on a fib(3) hot loop with p16-on. R3b keeps dispatchable = false even when this count bumps; R3d will lift it.

Source

pub fn trace_downrec_dispatched_count(&self) -> u64

v2.0 Track-R R3c — see [crate::vm::jit_state::JitCounters::downrec_dispatched]. Number of times the dispatcher’s is_downrec_sentinel arm fired and classified the return as a caller-pc-guard HIT.

Source

pub fn trace_downrec_deopt_count(&self) -> u64

v2.0 Track-R R3c — see [crate::vm::jit_state::JitCounters::downrec_deopt]. Number of times the dispatcher entered a downrec_link-bearing trace and the trace returned via the lowerer’s deopt block (caller-pc guard MISS), or the dispatcher itself force-deopted via the stitch-cycle checkpoint.

Source

pub fn trace_multi_way_guard_emitted_count(&self) -> u64

v2.0 Track-R R3d — see [crate::vm::jit_state::JitCounters::multi_way_guard_emitted]. Number of compiled traces whose lowerer emitted a multi-way caller-pc guard chain (>= 2 distinct caller_pc candidates) at the TraceEnd::DownRec close + lifted dispatchable = true.

Source

pub fn trace_compiled_count(&self) -> u64

P12-S2.C — number of closed traces the lowerer compiled and parked on Proto.traces. Re-records of the same head_pc are deduped (the second close finds the head_pc already cached and skips compile), so this never exceeds trace_closed_count.

Source

pub fn trace_field_ic_snapshot_count(&self) -> u64

v2.1 Phase 1I.B — number of times the recorder captured a crate::jit::trace_types::FieldIcSnapshot under LUNA_JIT_FIELD_IC=1. Stays 0 on the env-default path. Used by the Phase 1I.B opt-in fire test to verify the env gate wiring round-trips end-to-end (env -> recorder -> snapshot -> counter -> getter -> assertion).

Source

pub fn trace_compile_failed_count(&self) -> u64

P12-S2.C — number of closed traces the lowerer rejected (any of the bail conditions in crate::jit::trace::try_compile_trace).

Source

pub fn trace_dispatched_count(&self) -> u64

P12-S3 — number of times the dispatcher jumped into a compiled trace. Bumps on every entry; trace_deopt_count counts the subset where the trace returned with a parked jit_pending_err.

Source

pub fn trace_deopt_count(&self) -> u64

P12-S3 — number of trace entries that came back with jit_pending_err set (typically a metatable shadowed an index inside a helper, forcing the dispatcher to fall back to the interpreter without committing the trace’s result).

Source

pub fn trace_side_trace_started_count(&self) -> u64

P15-A v1 — number of times the dispatcher started a side trace recording (an exit_hit_counts slot crossed crate::jit::trace::HOTEXIT_THRESHOLD while active_trace was None and trace JIT was enabled). Each unit is exactly one start_side_trace call; the actual compile success counts under Self::trace_compiled_count like any other trace. Probe use: distinguishes the “side-trace pipeline fired” signal from the “primary back-edge / call-trigger fired” signal so v0-v3 architectural progress is visible without reading per-counter histograms.

Source

pub fn trace_side_trace_compiled_count(&self) -> u64

P15-A v2-A — number of side-trace recordings that closed, compiled successfully, AND patched their parent’s exit_side_trace_ptrs[exit_idx]. The parent’s IR doesn’t dispatch through these ptrs yet (v2-B/C job), but the counter + ptr write proves the compile + link pipeline is complete end-to-end.

Source

pub fn trace_side_trace_shape_mismatch_count(&self) -> u64

P15-A v2-C-A5-C — number of side traces that compiled successfully but were SHEDDED by the close-handler shape- match gate (exit_tags_match_entry_tags). High ratios vs. trace_side_trace_compiled_count indicate the architecture is shedding lots of would-be side traces; useful as a tuning probe for future relaxation of the gate or for child-IR re-specialisation against parent’s exit shape.

Source

pub fn trace_sinkable_seen_count(&self) -> u64

P12-S5-A — sum of NewTable sites the pre-emit escape sweep classified as crate::jit::trace::EscapeState::Sinkable across every successfully compiled trace on this Vm. The count is post-demotion: sites pre-emit drops back to Escaped for not meeting v1 sunk-emit criteria are NOT counted. trace_sunk_alloc_count matches one-for-one today (every surviving Sinkable site goes through sunk emit).

Source

pub fn trace_accum_bufferable_seen_count(&self) -> u64

P14-S14-B v1 — see JitCounters::accum_bufferable_seen.

Source

pub fn trace_exit_hit_summary(&self, cl: Gc<LuaClosure>) -> Vec<(u32, Vec<u32>)>

P15-prep — total dispatch hits across all known traces, broken into hot-exit telemetry (max single-exit count, total dispatches, exit count). Used by probes to identify hot side-exits as side-trace candidates.

Walks cl.proto AND all nested protos in cl.proto.protos recursively, so inner functions’ traces are reported.

Source

pub fn hot_exit_iter(&self, cl: Gc<LuaClosure>) -> Vec<HotExitInfo>

P15-A v0 — surface every side-exit slot whose hit count is >= HOTEXIT_THRESHOLD across every trace reachable from cl.proto (recursively walking proto.protos). Returned entries are side-trace candidates: each carries the parent trace’s (head_proto, head_pc), the exit’s index in the parent’s exit_hit_counts, and the side trace’s natural entry shape (cont_pc + exit_tags).

Layout of exit_hit_counts (mirrored by the iter):

  • [0..per_exit_inline.len())InlineSideExit (cont_pc + window-sized exit_tags).
  • [per_exit_inline.len()..inline.len() + per_exit_tags.len())per_exit_tags[i] (per-cont_pc caller-window tags).
  • Last slot → global clean-tail (cont_pc = head_pc, exit_tags = ct.exit_tags).
Source

pub fn trace_sunk_alloc_count(&self) -> u64

P12-S5-B — sum of NewTable sites that actually took the sunk-emit path across every successfully compiled trace on this Vm. Each counted site skips its heap Gc<Table> allocation per dispatch; the array part lives as Cranelift Variables for the duration of the trace.

Source

pub fn trace_materialize_emit_count(&self) -> u64

P12-S5-C — sum of materialise-helper emit sites across every successfully compiled trace on this Vm. Each unit is a (site × cmp side-exit) pair whose IR reconstructs a heap Gc<Table> from the virt slots on deopt — proves S5-C emit is wiring materialise into the right side-exits.

Source

pub fn trace_closure_emit_count(&self) -> u64

P12-S7-A diagnostic — total Op::Closure ops the trace JIT lowered to the luna_jit_op_closure helper. Each emitted op replaces a Heap::new_closure_inline call on the dispatch path; the count is static (one per matching op per compiled trace), summed at compile success.

Source

pub fn trace_per_exit_inline_compiled_count(&self) -> u64

v2.0 Stage 7 polish 6 fire experiment — see [crate::vm::jit_state::JitCounters::per_exit_inline_compiled]. Number of compiled traces whose per_exit_inline.len() > 0 (depth>0 inlined cmp side-exits emitted).

Source

pub fn trace_per_exit_inline_dispatchable_count(&self) -> u64

v2.0 Stage 7 polish 6 fire experiment — see [crate::vm::jit_state::JitCounters::per_exit_inline_dispatchable]. Number of compiled traces with per_exit_inline.len() > 0 AND dispatchable == true — i.e. the count of compiled traces that would actually exercise the AOT polish 6 chain-reloc + deploy-resolver path.

Source

pub fn trace_max_depth_seen(&self) -> u8

P12-S4-step1 diagnostic — max inline_depth ever seen on any RecordedOp pushed by the recorder. Tells tests + tuning whether a self-recursive function actually walked the depth tracker past 0. Saturates at MAX_INLINE_DEPTH. Persists across traces and Vm activations; reset only on Vm::new.

Source

pub fn set_bytecode_loading(&mut self, enabled: bool)

Toggle precompiled-chunk loading. Default true. Sandbox embedders should set to false so load/loadstring reject bytecode input (which bypasses parser limits and could exploit verifier gaps).

Source

pub fn bytecode_loading(&self) -> bool

Current bytecode-loading gate state.

Source

pub fn set_puc_bytecode_loading(&mut self, enabled: bool)

Toggle PUC .luac bytecode loading. Default false — PUC bytecode is a strictly larger trust surface than luna’s own dump format (third-party toolchain bugs, malformed chunks, unknown opcode shapes). Enable only for trusted PUC chunks. Per-dialect translators (Phase LB Wave 2) live in crate::vm::dump::puc.

Source

pub fn puc_bytecode_loading(&self) -> bool

Current PUC bytecode-loading gate state.

Source

pub fn set_loader_input_budget(&mut self, bytes: usize)

Set the loader input byte budget (see Vm::DEFAULT_LOADER_INPUT_BUDGET). Pass usize::MAX to effectively disable. Smaller caps are honored verbatim — a 0 cap rejects every non-empty source.

Source

pub fn loader_input_budget(&self) -> usize

Current loader input byte budget.

Source

pub fn take_error_traceback(&mut self) -> Option<String>

Take the error traceback captured at the latest error point and reset it. Embedders should call this immediately after a failed call_value/eval/call/etc. — the next public call_value entry clears it. Returns None if no error was in flight.

Source

pub fn set_memory_cap(&mut self, cap: Option<usize>)

Arm the soft memory cap (P09 embedding). The run loop checks the heap’s tracked byte usage between dispatch turns; on overshoot it first runs a full collect, and if bytes still exceeds the cap it raises a catchable "memory cap exceeded" Lua error and disarms itself (fire-once: re-arm before the next call_value if reusing the Vm across requests). None removes the cap. The accounting is approximate — internal Vec/Box capacity overhead is not tracked, so embedders should size the cap with ~2× margin over the desired hard limit and additionally bound the Vm’s lifetime (drop after each request).

Source

pub fn memory_used(&self) -> usize

Approximate bytes the heap is currently holding. Object shells plus every table’s internal array/hash boxes (tracked via Heap::apply_bytes_delta in set/rehash/ensure_*). Proto bytecode and closure upvalue slices still go uncounted — this is a lower bound, not a precise malloc_stats-style total.

Source

pub fn running_native_upvalue(&self, i: usize) -> Value

Read upvalue slot i of the native function currently on top of the dispatch chain (the one whose body is executing). Returns Value::Nil when no native is running. Public so the C ABI trampoline can fetch the host C function pointer it stashed there at registration time.

Source

pub fn nat_upval(&self, func_slot: u32, i: usize) -> Value

A native function’s own captured upvalue (self lives at func_slot).

Public so native_typed trampolines and embedders authoring stateful natives via native_with(...) can read their upvals.

Source

pub fn nat_arg(&self, func_slot: u32, nargs: u32, i: u32) -> Value

Read the i-th positional argument inside a NativeFn body (analogous to lua_tovalue(L, i + 1)). i >= nargs yields Nil, matching PUC’s “missing arg is nil” contract. Public so embedders can author their own natives.

Source

pub fn nat_return(&mut self, func_slot: u32, vals: &[Value]) -> u32

Push the return values of a NativeFn and return their count (analogous to pushing N values then return N from a C function). Public so embedders can author their own natives.

Source§

impl Vm

Source

pub fn install_aot_trace(&mut self, proto: Gc<Proto>, trace: CompiledTrace)

v1.3 Phase AOT Stage 7 sub-piece 4 — install a precompiled CompiledTrace onto proto.traces so the interp dispatcher fires it at the trace’s head_pc. This is the runtime install API the deploy-side luna-runtime-helpers resolver calls once per AOT-emitted trace meta entry, after looking up proto by stable hash (see crate::runtime::function::Proto::stable_hash).

§What this does

Pushes trace onto proto.traces via the existing RefCell. The trace’s entry fn ptr must already point at runnable machine code (the AOT linker resolved the symbol at link time; the deploy resolver passes the address verbatim).

§What this does NOT do
  • No deduplication. Calling twice with the same head_pc pushes two entries; the dispatcher’s find will pick the first match. The deploy resolver is responsible for not double-installing.
  • No invalidation of the runtime JIT cache. If the runtime JIT later records + compiles a trace for the same (proto, head_pc), both coexist on proto.traces and the dispatcher’s find picks whichever appears first. AOT traces install before any runtime recording is possible (resolver runs before vm.load returns its first closure), so AOT traces win the race for the same site.
  • No coverage gating. AOT traces are trusted by construction — they were validated at compile time. Setting dispatchable: false on the input would silently disable dispatch; the caller controls that flag.
§Safety / soundness

trace.entry is an unsafe extern "C" fn (mmap’d or linked machine code). Soundness contract:

  • The fn pointer must remain valid for the Vm’s lifetime. In the AOT-binary deploy shape this is trivially satisfied — the fn lives in the binary’s .text.
  • trace.entry_tags / exit_tags / window_size must match what the trace’s IR actually compiled against; the dispatcher uses them to marshal reg_state in and out without further validation. A mismatch corrupts vm.stack.

The AOT pipeline (luna-aot) is responsible for ensuring these invariants hold; this fn is a plain push — no validation that would slow the dispatcher’s hot path either.

Source

pub fn collect_proto_hashes( &self, root: Gc<Proto>, ) -> Vec<(Gc<Proto>, [u8; 16])>

v1.3 Phase AOT Stage 7 sub-piece 4 — walk the proto tree reachable from root and return (proto, stable_hash) pairs for every Proto found. Used by the deploy-side resolver to match AOT-emitted proto_hash keys against the freshly undump’d chunk’s protos.

The walk is BFS over Proto.protos. Same-Proto deduplication is done via Gc::as_ptr identity — a Proto re-referenced from multiple nested closures (rare; the cache field would catch the closure-side dedup, not the Proto side) is reported once.

§Why on &Vm and not a free fn

Keeps the AOT install API discoverable on the Vm surface — vm.collect_proto_hashes(root) reads naturally next to vm.install_aot_trace(proto, trace). Doesn’t actually touch any Vm field, so &self (read-only) is enough.

Source§

impl Vm

Source

pub fn create_async_native(&mut self, f: AsyncNativeFn) -> Value

v1.1 B10 Stage 2 — allocate a Value::Native whose closure is tagged as async (NativeClosure.is_async = true). The underlying NativeFn pointer slot stores f transmuted from AsyncNativeFn — same pointer width, no provenance loss — and the marker bit is what tells the dispatcher to route it through the cooperative-yield path.

The returned Value can be installed under a Lua global via Vm::set_global, passed as a callback, stored in a table — whatever a sync vm.native(f) value supports. Calling it from a sync Vm::eval context raises LuaError (“async native called in sync context”); only Vm::eval_async (or another driver that sets async_mode = true) can drive it.

Source

pub fn set_async_native( &mut self, name: &str, f: AsyncNativeFn, ) -> Result<(), LuaError>

v1.1 B10 Stage 2 — convenience: install an async native under name as a Lua global. Equivalent to vm.set_global(name, vm.create_async_native(f)).

Source

pub fn eval_async<'vm>(&'vm mut self, src: &str) -> EvalFuture<'vm>

v1.1 B10 Stage 1 — convenience entry: compile + run src as an anonymous chunk via the cooperative-yield dispatcher. The returned EvalFuture borrows &mut self for its full lifetime, which (by Vm: !Send) keeps it pinned to a single OS thread.

Holding two EvalFutures on the same Vm is blocked by the borrow checker (&mut Vm exclusivity). Holding a sync eval/call_value call while an EvalFuture is in flight is likewise blocked.

The chunk source name in tracebacks is "=eval". Use Vm::eval_async_chunk to supply a custom name.

Source

pub fn eval_async_chunk<'vm>( &'vm mut self, src: &str, name: &str, ) -> EvalFuture<'vm>

v1.1 B10 Stage 1 — like Vm::eval_async but with a user-supplied chunk name (appears in tracebacks).

Source

pub fn set_async_slice(&mut self, n: i64)

v1.1 B10 Stage 1 — set the per-poll opcode quota loaded into instr_budget at the start of each EvalFuture poll slice. Default 10_000 opcodes. Smaller = finer-grained cooperative yield (lower per-task latency, more task-switch overhead); larger = closer to sync throughput per slice.

Source

pub fn async_slice(&self) -> i64

v1.1 B10 Stage 1 — current per-poll async slice size (default 10_000).

Source§

impl Vm

Source

pub fn eval_chunk( &mut self, src: &str, name: &str, ) -> Result<Vec<Value>, LuaError>

Same as Vm::eval but with a user-supplied chunk name (appears in tracebacks for debugging).

Source

pub fn intern_str(&mut self, s: &str) -> Gc<LuaStr>

Intern a UTF-8 string into the heap’s string table. Idempotent — interning the same bytes twice returns the same Gc<LuaStr> handle.

Useful for embedders constructing table keys or comparing Lua strings without going through Value::Str wrapping each time.

Source

pub fn error_kind(&self) -> LuaErrorKind

Classification of the most recently raised error on this Vm. Returns crate::vm::error::LuaErrorKind::Runtime before any error fires.

Source

pub fn error_source(&self) -> Option<(&str, u32)>

(source_name, line) of the most recently raised error, or None if the dispatcher could not locate one. Source names match Lua’s chunk-name convention ("=eval", "=stdin", user-supplied via Vm::load).

Source

pub fn set_error_kind(&mut self, kind: LuaErrorKind)

Set the classification for the next error to be raised — used by the dispatcher at well-known sites. Embedders writing native callbacks may call this before returning Err(LuaError) to flag a specific kind (e.g. LuaErrorKind::Type for a bad arg).

Source

pub fn set_error_source(&mut self, name: String, line: u32)

Set the (source_name, line) for the next error to be raised. The dispatcher uses this at the syntax-error / parser boundary.

Source

pub fn clear_error_metadata(&mut self)

Clear error classification — called on a clean call_value entry so old error metadata doesn’t leak into the next call.

Source

pub fn create_userdata<T: LuaUserdata>(&mut self, value: T) -> Value

Allocate a host userdata wrapping value. Returns the Value::Userdata you can set_global / pin / pass to scripts.

The metatable produced by crate::vm::LuaUserdata::add_methods is auto-installed on the userdata (cached per Vm keyed by TypeId::of::<T>()). For a type that only needs identity + raw host-side access (no Lua-callable methods), provide an empty impl:

struct Counter(i64);
impl LuaUserdata for Counter {}
use luna_core::vm::{LuaUserdata, Vm};
use luna_core::version::LuaVersion;
use luna_core::runtime::Value;

#[derive(Debug)]
struct Counter(i64);
impl LuaUserdata for Counter {}

let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
let ud = vm.create_userdata(Counter(42));
vm.set_global("counter", ud).unwrap();

match ud {
    Value::Userdata(g) => {
        // SAFETY: single-threaded heap; pointer is live.
        let r = unsafe { &*g.as_ptr() };
        assert_eq!(r.downcast::<Counter>().unwrap().0, 42);
    }
    _ => unreachable!(),
}
Source

pub fn set_userdata<T: LuaUserdata>( &mut self, name: &str, value: T, ) -> Result<(), LuaError>

Source

pub fn userdata_borrow<T: Any + 'static>(&mut self, name: &str) -> Option<&T>

Borrow the host payload of a global userdata as &T. Returns None if the global doesn’t exist, isn’t a userdata, isn’t a host userdata, or holds a different type than T.

Takes &mut self because the lookup interns the key string; returning a borrow tied to &mut Vm mirrors vm.set_global ergonomics.

Source

pub fn create_coroutine(&mut self, body: Value) -> Value

Create a new coroutine carrying body (a Lua function or any callable Value). Returns the Value::Coro handle ready to be passed to Self::resume_coroutine.

Equivalent to coroutine.create(body) from a Rust embedder.

Source

pub fn resume_coroutine( &mut self, co: Value, args: Vec<Value>, ) -> Result<Vec<Value>, LuaError>

Resume a coroutine with the given arguments. Returns the yielded values on yield, the return values on the body’s terminal return, or an error if the body raised.

Equivalent to coroutine.resume(co, args...). Returns Err(LuaError) if co is not a Value::Coro.

Source

pub fn set_rust_debug_hook( &mut self, hook: Option<RustDebugHook>, mask: u32, count: i64, )

Install a Rust-side debug hook (see crate::vm::exec::RustDebugHook). The mask is a bitwise OR of HOOK_MASK_CALL / HOOK_MASK_RETURN / HOOK_MASK_LINE / HOOK_MASK_COUNT exported from crate::vm::exec. The count arg sets the instruction granularity for Count events (ignored unless HOOK_MASK_COUNT is set).

Passing hook = None clears the Rust hook; the Lua-side hook installed via debug.sethook is unaffected.

Source

pub fn clear_rust_debug_hook(&mut self)

Clear the Rust-side debug hook (sugar over set_rust_debug_hook(None, 0, 0)).

Source

pub fn current_op(&self) -> Option<Op>

Read the most recently dispatched Lua opcode, if the Vm is currently executing inside a Lua frame. Intended for use from a Count hook (installed via Self::set_rust_debug_hook with HOOK_MASK_COUNT) to tally per-opcode distribution against a workload — the v1.2 methodology gate (perf-decomposition-vs-polish.md §2 Phase A, in ~/.claude-shared/global/methodology/) requires runtime-counter validation of per-iter op mix before any stage decomposition is acted on.

Returns None outside a Lua frame (top-level setup, while a native callback or Cont guard is on top of the call stack, etc.). Reads self.frames.last() → CallFrame::Lua(f) → f.closure.proto.code[f.pc - 1] — the just-dispatched opcode (PC has already advanced past it).

Source

pub fn userdata_borrow_mut<T: Any + 'static>( &mut self, name: &str, ) -> Option<&mut T>

Mutable variant of Self::userdata_borrow.

Source§

impl Vm

Source

pub fn pin_host(&mut self, v: Value) -> HostRootTicket

Pin v as a host root. Reuses a recycled slot if the free list is non-empty, else extends the pool. Bumps the slot’s generation; previously-issued tickets for that slot become stale (read_host returns None, write_host / unpin return Err(HostRootStale)).

Returns a HostRootTicket (Copy, 8 bytes). The value becomes an extra GC root until the ticket is released via Self::unpin or the whole pool via Self::unpin_all.

Source

pub fn read_host(&self, t: HostRootTicket) -> Option<Value>

Read a previously pinned host root. Returns None if the ticket is stale (slot was unpinned and possibly re-pinned to a different value) or if the ticket index is out of bounds.

Source

pub fn write_host( &mut self, t: HostRootTicket, v: Value, ) -> Result<(), HostRootStale>

Mutate a previously pinned host root in place. Returns Err(HostRootStale) on stale ticket; otherwise updates the slot’s value WITHOUT bumping generation (mutation does not invalidate other live aliases of the same ticket).

Source

pub fn unpin(&mut self, t: HostRootTicket) -> Result<(), HostRootStale>

Drop a single pinned root. Clears the slot’s value to Nil, bumps the slot’s generation, and pushes the index onto the free list for reuse. Returns Err(HostRootStale) if the ticket is stale (already-unpinned / re-pinned slot); the pool is unchanged in that case.

Generation overflow at u32::MAX retires the slot permanently — the index is NOT pushed to the free list, and future pin_host calls will allocate a fresh slot rather than reuse this one.

Source

pub fn host_root_count(&self) -> usize

Number of currently-pinned (live) host roots. Diagnostic only.

Computed as host_roots.len() - host_roots_free.len() — this over-counts retired-by-overflow slots as still-allocated. For a short-lived process the difference is sub-MB; long-running servers should treat this as an upper bound on live pins.

Source

pub fn unpin_all(&mut self)

Drop every pinned host root. Embedders driving the Lua facade in a request-per-script loop call this to release a batch of pins in one shot. Bumps every slot’s generation; every previously-issued ticket becomes stale uniformly.

Keeps the underlying Vec capacity to amortize future pin_host allocations. Slots that already reached generation == u32::MAX stay retired (not added back to the free list).

Source§

impl Vm

Source

pub fn sandbox(version: LuaVersion) -> SandboxBuilder

Start a SandboxBuilder for a fresh, sandboxed Vm. See SandboxBuilder for defaults.

Source§

impl Vm

Source

pub fn new_table(&mut self) -> TableBuilder<'_>

Allocate a fresh table and return a builder for in-place population.

Source

pub fn table_of<K, V, const N: usize>( &mut self, entries: [(K, V); N], ) -> Gc<Table>
where K: IntoValue, V: IntoValue,

Allocate a fresh table populated from a fixed-size slice of (key, value) pairs. Equivalent to chained new_table().with(...) calls, but more concise for static tables (stdlib registration, embedder-side constants).

Panics on table overflow (unreachable for N small).

Source§

impl Vm

Source

pub fn native_typed<F, Marker>(&mut self, f: F) -> Value
where F: NativeTypedSig<Marker>,

Register a typed Rust function as a Lua-callable Value. The callable must be a fn-pointer or a non-capturing closure (Copy + 'static + ZST or fn-pointer-sized). For capturing closures use vm.native_with(...) with explicit upvals.

let add = vm.native_typed(|a: i64, b: i64| -> i64 { a + b });
vm.set_global("add", add).unwrap();
let r = vm.eval("return add(40, 2)").unwrap();
assert!(matches!(r[0], Value::Int(42)));
Source§

impl Vm

Source

pub fn register_userdata<T: LuaUserdata>( &mut self, ) -> Result<Gc<Table>, LuaError>

Build (or fetch from cache) the metatable for T. Called lazily by Vm::create_userdata / Vm::set_userdata; embedders rarely need to invoke it directly. Returns the same Gc<Table> on every call within a given Vm (keyed by TypeId::of::<T>()).

The metatable is pinned as a host root so it survives GC even when no userdata of type T is currently reachable.

Trait Implementations§

Source§

impl Drop for Vm

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

§

impl !RefUnwindSafe for Vm

§

impl !Send for Vm

§

impl !Sync for Vm

§

impl !UnwindSafe for Vm

§

impl Freeze for Vm

§

impl Unpin for Vm

§

impl UnsafeUnpin for Vm

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.