luna_core/runtime/function.rs
1//! Function objects: compiled prototypes, Lua closures, upvalues.
2
3use crate::runtime::heap::{Gc, GcHeader, Marker};
4use crate::runtime::string::LuaStr;
5use crate::runtime::value::Value;
6use crate::vm::isa::Inst;
7
8/// An activation record on a thread's call stack. Pure data (closure handle +
9/// stack offsets), so it lives in `runtime` where the GC can trace a suspended
10/// coroutine's frames.
11#[derive(Clone, Copy)]
12pub struct Frame {
13 /// Currently executing closure.
14 pub closure: Gc<LuaClosure>,
15 /// stack index of register 0
16 pub base: u32,
17 /// Program counter (index into `closure.proto.code`).
18 pub pc: u32,
19 /// stack slot of the function (results land here)
20 pub func_slot: u32,
21 /// number of extra (vararg) arguments, living on the stack just below `base`
22 /// at `func_slot+1 .. func_slot+1+n_varargs` (PUC `CallInfo.u.l.nextraargs`).
23 /// `OP_VARARG`/`OP_VARGIDX` read them there; a named vararg only materializes
24 /// a heap table when it is written / escapes / is `_ENV`.
25 pub n_varargs: u32,
26 /// results expected by the caller (-1 = all)
27 pub nresults: i32,
28 /// pc the line hook last observed in this frame (PUC CallInfo `oldpc`);
29 /// `u32::MAX` on a fresh frame so its first instruction fires a line event
30 pub hook_oldpc: u32,
31 /// true if this Lua frame was entered across a C boundary (call_value: a
32 /// metamethod, pcall, __close handler, or a coroutine body). The debug
33 /// interface does not read it: it places the running natives themselves
34 /// among the frames.
35 pub from_c: bool,
36 /// the metamethod event this frame is handling (e.g. "close" for a
37 /// `__close` handler call); the debug interface reads `"gc"` to recognize
38 /// a finalizer (PUC `CIST_FIN`), and names other handlers from the
39 /// instruction that called them.
40 pub tm: Option<&'static str>,
41 /// true when this frame is the hook function itself (PUC sets
42 /// `CIST_HOOKED`). `debug.getinfo(1).namewhat` returns `"hook"` for it.
43 pub is_hook: bool,
44 /// PUC `ci->u.l.tailcalls` — how many tail calls have collapsed into
45 /// this activation slot. Each `OP_TailCall` chain adds one. 5.1
46 /// `lua_getstack` reports a synthetic `CIST_TAIL` level per count
47 /// (so a deeply tail-recursive function shows `tailcalls` extra
48 /// levels between itself and its real caller — 5.1 db.lua :372 walks
49 /// `getinfo(2..lim)` and expects each to be `"tail"`). The 5.2+
50 /// `istailcall` boolean is `tailcalls > 0`.
51 pub tailcalls: u32,
52}
53
54/// An entry on a thread's call stack: either a Lua activation record or a
55/// continuation frame standing in for a *yieldable native* (pcall/xpcall).
56///
57/// A `Cont` sits just below the call it protects. When that call returns,
58/// yields-to-completion, or errors, the interpreter consumes the `Cont` to wrap
59/// the outcome — the analogue of PUC `lua_pcallk`'s continuation `k`. Keeping it
60/// on the same stack as Lua frames means a `coroutine.yield` crossing it is
61/// preserved and restored automatically with the thread's saved context.
62#[derive(Clone, Copy)]
63pub enum CallFrame {
64 /// A Lua activation record.
65 Lua(
66 /// The activation record.
67 Frame,
68 ),
69 /// A continuation guarding a yieldable native call (pcall / xpcall /
70 /// metamethod / `__close` / `__pairs`).
71 Cont(
72 /// The continuation record.
73 NativeCont,
74 ),
75}
76
77impl CallFrame {
78 /// Borrow the inner Lua frame if this is a `Lua` variant.
79 #[inline]
80 pub fn lua(&self) -> Option<&Frame> {
81 match self {
82 CallFrame::Lua(f) => Some(f),
83 CallFrame::Cont(_) => None,
84 }
85 }
86
87 /// Mutably borrow the inner Lua frame if this is a `Lua` variant.
88 #[inline]
89 pub fn lua_mut(&mut self) -> Option<&mut Frame> {
90 match self {
91 CallFrame::Lua(f) => Some(f),
92 CallFrame::Cont(_) => None,
93 }
94 }
95}
96
97/// A continuation frame for `pcall`/`xpcall`: where its wrapped result lands and
98/// how to wrap it. Lives on the call stack below the protected call (see
99/// [`CallFrame`]).
100#[derive(Clone, Copy)]
101pub struct NativeCont {
102 /// What kind of protection this continuation represents.
103 pub kind: ContKind,
104 /// the protecting native's own stack slot — the wrapped status + values
105 /// (`true, …` / `false, msg`) land here
106 pub func_slot: u32,
107 /// results the caller of pcall/xpcall expects (-1 = all)
108 pub nresults: i32,
109}
110
111/// Continuation kind for yieldable native dispatch.
112#[derive(Clone, Copy)]
113pub enum ContKind {
114 /// `pcall(f, ...)` — wraps the result as `(true, ...)` / `(false, msg)`.
115 Pcall,
116 /// xpcall: the message handler to run if the protected call errors
117 Xpcall {
118 /// Message handler function invoked on error.
119 handler: Value,
120 },
121 /// a yieldable metamethod call triggered by a VM instruction (PUC's
122 /// `luaV_finishOp`): on the metamethod's return the interrupted instruction
123 /// is completed per `MetaCont`. A `coroutine.yield` inside the metamethod is
124 /// preserved on the thread's frame stack like any other call.
125 Meta(
126 /// Continuation describing how to finish the interrupted op.
127 MetaCont,
128 ),
129 /// a yieldable `__pairs` metamethod call from `pairs()` (PUC luaB_pairs uses
130 /// lua_callk): on return, its (≤4, nil-padded) results are `pairs`'s own
131 /// results. A `coroutine.yield` inside `__pairs` is preserved like pcall's.
132 Pairs,
133 /// a yieldable `__close` handler call driven by `begin_close` (PUC's
134 /// `luaF_close` + `lua_callk` continuation). On the handler's return or
135 /// error, the close iteration resumes from `CloseCont`'s state and either
136 /// invokes the next handler (pushing a fresh Cont::Close) or executes the
137 /// recorded `AfterClose` action.
138 Close(
139 /// Per-iteration close state.
140 CloseCont,
141 ),
142}
143
144/// Per-iteration state for a chain of `__close` handlers driven through the
145/// interpreter loop. When a handler is pushed onto the call stack, this rides
146/// in a `Cont::Close` frame underneath it so a `coroutine.yield` from the
147/// handler preserves the close iteration with the rest of the thread.
148#[derive(Clone, Copy)]
149pub struct CloseCont {
150 /// the close threshold: keep closing tbc slots ≥ from until exhausted
151 pub from: u32,
152 /// the error object threaded through subsequent handlers, if any
153 pub pending: Option<Value>,
154 /// what to do once every slot ≥ from is closed
155 pub after: AfterClose,
156}
157
158/// What to run once `begin_close` has drained every tbc slot.
159#[derive(Clone, Copy)]
160pub enum AfterClose {
161 /// `OP_Close` (block-end close): nothing else; next instruction continues.
162 Block,
163 /// `OP_Return*`: pop the Lua frame whose `OP_Return` triggered the close
164 /// and deliver `nret` results from `[abs_a, abs_a + nret)` to the frame's
165 /// `func_slot`. `from_native` mirrors the original op's hook flag.
166 Return {
167 /// Absolute stack index of the first return value.
168 abs_a: u32,
169 /// Number of return values.
170 nret: u32,
171 /// Mirrors the original op's hook-fired flag.
172 from_native: bool,
173 },
174 /// Error unwind: the close runs while unwinding a Lua frame. When every
175 /// handler is done, pop the deferred Lua frame, truncate to `func_slot`,
176 /// and re-raise — preferring a handler-raised error over `err` (PUC
177 /// luaF_close).
178 ResumeUnwind {
179 /// Slot to truncate the value stack to before re-raising.
180 func_slot: u32,
181 /// Original error value to re-raise (or replaced by a handler raise).
182 err: Value,
183 },
184}
185
186/// How to complete a VM instruction once its metamethod returns.
187#[derive(Clone, Copy)]
188pub struct MetaCont {
189 /// What to do with the metamethod's return value.
190 pub action: MetaAction,
191 /// the interrupted frame's `top` to restore after the metamethod returns
192 pub saved_top: u32,
193}
194
195/// Per-op finishing action for a yielded metamethod call.
196#[derive(Clone, Copy)]
197pub enum MetaAction {
198 /// arithmetic / index / unary / length: store the single result at `dst`
199 Store {
200 /// Destination register receiving the metamethod's first result.
201 dst: u32,
202 },
203 /// `__newindex`: the metamethod has no result to keep
204 Discard,
205 /// comparison (`__eq`/`__lt`/`__le`): the truthiness of the result feeds the
206 /// conditional skip — the following JMP runs iff `result.truthy() == k`.
207 /// `negate=true` flips the truthiness first, for the ≤5.3 `__le` →
208 /// `not __lt(b, a)` synthesis path where the metamethod is `__lt` but
209 /// the operator was `<=`.
210 Compare {
211 /// Sense of the conditional skip the comparison op was emitted for.
212 k: bool,
213 /// True when the 5.3 `__le → not __lt(b,a)` synthesis is in effect.
214 negate: bool,
215 },
216 /// `__concat`: store the result at `dst`, set `top = dst + 1`, then continue
217 /// folding the operands still at `[base_a .. top)` (PUC finishOp re-runs).
218 Concat {
219 /// Destination register for the metamethod's result.
220 dst: u32,
221 /// First operand register of the original concat span.
222 base_a: u32,
223 },
224}
225
226/// Where a closure's upvalue is captured from, relative to the *enclosing*
227/// function (PUC Upvaldesc).
228#[derive(Clone, Debug)]
229pub struct UpvalDesc {
230 /// captured from the enclosing frame's registers (true) or from the
231 /// enclosing closure's own upvalues (false)
232 pub in_stack: bool,
233 /// Index in the enclosing frame's register file (when `in_stack`) or
234 /// in the enclosing closure's upvalue array (otherwise).
235 pub index: u8,
236 /// variable name, for error messages and debug info
237 pub name: Box<str>,
238 /// the captured variable is `<const>` (5.5): assignment through this
239 /// upvalue is a compile-time error
240 pub read_only: bool,
241}
242
243/// Debug record for a local variable: its name and the pc range over which it
244/// occupies register `reg`. Used to name registers in error messages and
245/// debug.getinfo (PUC LocVar).
246#[derive(Clone, Debug)]
247pub struct LocVar {
248 /// Local-variable name.
249 pub name: Box<str>,
250 /// Register holding the variable while in scope.
251 pub reg: u32,
252 /// First pc where the variable is live.
253 pub start_pc: u32,
254 /// Pc one past the last where the variable is live.
255 pub end_pc: u32,
256}
257
258/// A compiled function (PUC Proto). Immutable after compilation.
259#[repr(C)]
260pub struct Proto {
261 pub(crate) hdr: GcHeader,
262 /// Bytecode instructions, in execution order.
263 pub code: Box<[Inst]>,
264 /// Constant table referenced by `LoadK` / `*K` opcodes.
265 pub consts: Box<[Value]>,
266 /// Nested prototypes referenced by `Closure`.
267 pub protos: Box<[Gc<Proto>]>,
268 /// Upvalue descriptors (one per upvalue this function captures).
269 pub upvals: Box<[UpvalDesc]>,
270 /// Fixed parameter count.
271 pub num_params: u8,
272 /// Whether the function accepts `...`.
273 pub is_vararg: bool,
274 /// PUC `lparser.c` emits a hidden `(vararg table)` locvar for a function
275 /// declared with an explicit anonymous `(...)` (and NOT for a main chunk's
276 /// implicit vararg, nor for `(...t)` which becomes a named local). When
277 /// true, `debug.getlocal` exposes the pseudo at `num_params + 1`.
278 pub has_vararg_table_pseudo: bool,
279 /// PUC 5.1 `LUAI_COMPAT_VARARG`: the function declared `...` and so gets a
280 /// hidden local named `arg` at `num_params` populated at entry with the
281 /// extra args as `{n = count, [1] = e1, [2] = e2, …}`. The slot keeps the
282 /// shape across resumes; user code can reassign it. 5.1 db.lua :279 reads
283 /// `arg.n` from inside a `line` hook walking `debug.getlocal(2, i)`.
284 pub has_compat_vararg_arg: bool,
285 /// registers needed by a frame of this function
286 pub max_stack: u8,
287 /// line of each instruction (same length as `code`)
288 pub lines: Box<[u32]>,
289 /// chunk name, for error messages
290 pub source: Gc<LuaStr>,
291 /// Source line where the function was defined.
292 pub line_defined: u32,
293 /// line of the function's closing `end` (PUC `lastlinedefined`); 0 for the
294 /// main chunk
295 pub last_line_defined: u32,
296 /// local-variable debug records (name + live pc range)
297 pub locvars: Box<[LocVar]>,
298 /// PUC 5.2+ closure cache (`Proto.cache`): the last LClosure built from
299 /// this Proto. When OP_CLOSURE fires, the VM compares each candidate
300 /// upvalue to the cached closure's same-slot upvalue (`getcached`); on a
301 /// full match the cached closure is reused, so two `function() ... end`
302 /// literals reached from the same source compile but with identical
303 /// upvalue bindings compare equal. closure.lua's `for i=1,5 do
304 /// a[i]=function(x) return x+a+_ENV end end` asserts that subsequent
305 /// iterations reuse the closure; capturing `i` instead defeats the cache.
306 pub cache: std::cell::Cell<Option<Gc<LuaClosure>>>,
307 /// Index into `upvals` of the `_ENV` upvalue (5.1 per-function-env
308 /// model needs to clone-on-closure), or `u8::MAX` for "no _ENV
309 /// upval". Computed once at Proto construction so `Op::Closure`'s
310 /// 5.1 path doesn't string-compare across `upvals` per closure.
311 pub env_upval_idx: u8,
312 /// JIT cache slot. `Untried` on Proto creation; the first
313 /// `Vm::call_value` on a closure whose body fits the JIT whitelist
314 /// flips it to `Compiled(fn ptr)` and the `JitHandle` that backs
315 /// the mmap is parked on the `Vm.jit_handles` Vec for the Vm's
316 /// lifetime. `Failed` records the whitelist miss so subsequent
317 /// calls skip the compile attempt.
318 pub jit: std::cell::Cell<JitProtoState>,
319 /// Trace JIT hot-loop detector. Incremented by `Vm::run`
320 /// on each backward-jump dispatched within this Proto. Once the
321 /// counter passes `TRACE_HOT_THRESHOLD`, the next visit to the
322 /// backward-jump target promotes that PC to a trace head and
323 /// begins recording. `Cell<u32>` matches the interp's
324 /// single-threaded dispatch and pays no atomic cost. Cap at
325 /// `u32::MAX / 2` to leave headroom above the threshold.
326 pub trace_hot_count: std::cell::Cell<u32>,
327 /// Trace-on-call counter. Incremented by `begin_call` on
328 /// every Lua-callee push into this Proto. Once it passes
329 /// `CALL_HOT_THRESHOLD`, the next call into this Proto promotes
330 /// `pc=0` to a trace head and begins recording. Lets the trace
331 /// JIT cover self-recursive functions whose body holds no
332 /// negative `Op::Jmp` (`fib`, recursive `make`/`check` in
333 /// `binary_trees`), where the back-edge counter never triggers.
334 pub call_hot_count: std::cell::Cell<u32>,
335 /// Count of "partial-coverage" discards on
336 /// this Proto's call-triggered recordings. Each discard is a
337 /// new opportunity for the recorder to record a different
338 /// (hopefully longer) trace at a deeper recursion point; the
339 /// trigger condition re-uses `c >= THRESHOLD &&
340 /// !already_cached` so the next call retries. Without
341 /// a cap, pathologically-branchy workloads like binary_trees
342 /// (`make` body contains 2 nested self-recursive calls)
343 /// produce a 1500+ discard storm — the recorder never
344 /// captures a covered trace because every base / shallow-
345 /// depth entry caught yields a partial path. The cap
346 /// bounds the storm: after `MAX_DISCARDS = 5` discards, the
347 /// next close skips the coverage check and compiles + caches
348 /// whatever shape it has (length gate will likely refuse
349 /// dispatch but at least the trigger stops firing).
350 pub trace_discard_count: std::cell::Cell<u32>,
351 /// Once the discard cap forces a compile on
352 /// this Proto (the recorder gave up trying to capture a
353 /// covered trace and just compiled whatever shape it had), set
354 /// this flag to `true`. Both trigger gates (back-edge in
355 /// `Op::Jmp` and call in `begin_call`) short-circuit on
356 /// `gave_up` BEFORE doing the `proto.traces.borrow()` +
357 /// linear-scan `already_cached` check. Each post-cap call into
358 /// such a Proto avoids the RefCell borrow + Vec scan
359 /// (`binary_trees_pattern`'s 20k make + 20k check calls per
360 /// run = 40k RefCell borrows saved). The `gave_up` flag never
361 /// flips back to `false` within a Vm — gave-up is permanent
362 /// on the Proto, mirroring the `JitProtoState::Failed`
363 /// invariant.
364 pub trace_gave_up: std::cell::Cell<bool>,
365 /// Trace heads (pc) whose recordings failed to compile, with the
366 /// number of failures. The hot counters are not reset after a
367 /// recording, so without this every later call or back-edge would
368 /// record and compile the same failing trace again.
369 pub(crate) trace_compile_failures: crate::jit::send_compat::TRefLock<Vec<(u32, u8)>>,
370 /// Compiled trace cache for this Proto. A successful
371 /// `compile_trace(record)` parks its `CompiledTrace` here;
372 /// `Vm::run`'s trace dispatcher iterates this on each
373 /// back-edge target visit. `RefCell` because compile is invoked
374 /// from inside `Vm::run` and may need to push while another op
375 /// is mid-dispatch in the same Proto.
376 pub traces: crate::jit::send_compat::TRefLock<
377 Vec<crate::jit::send_compat::TArc<crate::jit::trace::CompiledTrace>>,
378 >,
379}
380
381/// Per-Proto JIT cache state. Copy so it fits a plain
382/// `Cell` on the dispatch hot path (no `RefCell` borrow check); the
383/// fn pointer's mmap is kept alive by `Vm.jit_handles`.
384#[derive(Clone, Copy, Debug)]
385pub enum JitProtoState {
386 /// Compilation hasn't been attempted yet.
387 Untried,
388 /// Compilation was attempted and the body fell outside the whitelist;
389 /// subsequent calls skip the attempt.
390 Failed,
391 /// Native code is installed and callable through the recorded entry.
392 Compiled {
393 /// Raw mmap'd code address. Transmute to the
394 /// `unsafe extern "C" fn(i64, …) -> i64` shape matching
395 /// `num_args` at the call site.
396 entry: *const u8,
397 /// 0..=MAX_JIT_ARITY. Picks the transmute target.
398 num_args: u8,
399 /// True when the Lua chunk terminates with `Return1` (single
400 /// observable return value). False means the chunk only
401 /// side-effects + `Return0` — host gets an empty `Vec<Value>`
402 /// from `Vm::call_value`, an interpreter `Op::Call` gets
403 /// zero results pushed (PUC nresults handling).
404 returns_one: bool,
405 /// Per-arg Float bit. Bit `i = 1` ↔ arg slot `i`
406 /// is f64 (passed as i64 bit-pattern across the ABI, bitcast
407 /// inside the JIT). Bit `i = 0` ↔ Int. Bits ≥ MAX_JIT_ARITY
408 /// are zero.
409 arg_float_mask: u8,
410 /// Per-arg Table bit. Bit `i = 1` ↔ arg slot `i`
411 /// is `Gc<Table>` raw ptr (passed as the i64 pointer value
412 /// directly, since `Gc<Table>` is `NonNull<Table>` =
413 /// pointer-shaped). Mutually exclusive with `arg_float_mask`
414 /// for the same bit. Required so `try_jit_call_op`'s arg
415 /// marshalling can accept `Value::Table(t)` and pack
416 /// `t.as_ptr() as i64`; without it a Table arg would fall
417 /// into the dispatcher's default-deny match arm and the
418 /// callee couldn't be reached via JIT.
419 arg_table_mask: u8,
420 /// True iff the chunk's `Return1` value is f64.
421 /// Dispatcher wraps `r` as `Value::Float(f64::from_bits(r))`
422 /// vs `Value::Int(r)` accordingly. Meaningful only when
423 /// `returns_one == true`.
424 ret_is_float: bool,
425 /// True iff the chunk's `Return1` value is a
426 /// `Gc<Table>` ptr. Mutually exclusive with `ret_is_float`.
427 /// Dispatcher wraps `r` as
428 /// `Value::Table(Gc::from_ptr(r as *mut Table))`.
429 ret_is_table: bool,
430 },
431}
432
433// Cell<JitProtoState> stores raw pointers; explicit Send + Sync
434// negative: keep these on a single-threaded runtime. The Vm itself
435// already is !Send (Heap holds raw GcHeader pointers), so we don't
436// need any auto-trait gymnastics — this comment exists so a future
437// audit doesn't try to flip the trait without thinking.
438
439/// Hand-rolled FNV-1a-128 state.
440/// Used by [`Proto::stable_hash`] to fingerprint a Proto without
441/// pulling a third-party hash crate (`luna-core` 0-dep contract).
442///
443/// FNV-1a is not cryptographic; collision-resistance suffices for the
444/// AOT proto-ID use case because a collision would surface as a
445/// trace-vs-proto mismatch and the dispatcher's existing tag/shape
446/// guards would deopt to interp rather than corrupt state.
447struct FnvHash128 {
448 state: u128,
449}
450
451impl FnvHash128 {
452 /// Standard FNV-1a-128 offset basis.
453 const OFFSET_BASIS: u128 = 0x6c62272e07bb014262b821756295c58d;
454 /// Standard FNV-1a-128 prime.
455 const PRIME: u128 = 0x0000000001000000000000000000013b;
456
457 fn new() -> Self {
458 FnvHash128 {
459 state: Self::OFFSET_BASIS,
460 }
461 }
462
463 /// Absorb `bytes` into the running hash. FNV-1a: per byte, XOR
464 /// into the low octet of state, then multiply by the prime (wrap).
465 fn update(&mut self, bytes: &[u8]) {
466 let mut s = self.state;
467 for &b in bytes {
468 s ^= b as u128;
469 s = s.wrapping_mul(Self::PRIME);
470 }
471 self.state = s;
472 }
473
474 /// Finalise to 16 big-endian bytes (network order — stable across
475 /// platforms; the LE/BE choice is cosmetic since the only consumer
476 /// is byte-equality, but BE matches the canonical FNV-1a-128
477 /// reference output if anyone cross-checks).
478 fn finish(self) -> [u8; 16] {
479 self.state.to_be_bytes()
480 }
481}
482
483impl Proto {
484 /// Stable 128-bit hash over a
485 /// Proto's identity-defining bytes. Two `Proto`s whose Lua source +
486 /// dialect compile to the same bytecode hash to the same digest;
487 /// distinct sources hash distinct. The digest is stable across
488 /// `dump` / `undump` round-trips and across separate process runs,
489 /// so an AOT pipeline (which fingerprints protos at compile time)
490 /// and the deploy `Vm` (which fingerprints the same protos after
491 /// undumping the embedded bytecode) agree on which `(Proto, pc)`
492 /// site a precompiled trace targets.
493 ///
494 /// # What's fed into the hash
495 ///
496 /// - `code`: the raw u32 packed words, in order.
497 /// - `consts`: per entry, a one-byte discriminant + payload bytes
498 /// (Int/Float as raw 8-byte LE; Str as `[len_u32_le | bytes]`;
499 /// Nil/Bool as discriminant alone). Heap-pointer variants in
500 /// `Value` (Table / Closure / Native / Coro / Userdata /
501 /// LightUserdata) never appear in a Proto's constant table —
502 /// constants are restricted to nil / bool / number / string by
503 /// the Lua compiler — so a `debug_assert!` catches the contract
504 /// if a future refactor changes that.
505 /// - `upvals`: per descriptor, `in_stack` byte + `index` byte +
506 /// `read_only` byte + name bytes (length-prefixed u32 LE).
507 /// - `num_params`, `is_vararg`, `max_stack`: single-byte each.
508 ///
509 /// # What's NOT fed in
510 ///
511 /// - Nested `protos`: each nested Proto has its own `stable_hash`;
512 /// parent identity is determined by its own immediate bytes only.
513 /// Callers that need a "whole tree" identity should hash the
514 /// roots they care about.
515 /// - `lines`, `locvars`, `source`, `line_defined`,
516 /// `last_line_defined`: debug metadata. A `.lua` source edited
517 /// to add a comment shouldn't invalidate AOT traces — bytecode
518 /// is the identity, not the editor cursor.
519 /// - JIT cache fields (`jit`, `traces`, `trace_hot_count`, …),
520 /// `cache`, `has_vararg_table_pseudo`, `has_compat_vararg_arg`,
521 /// `env_upval_idx`: runtime-only state derived from the
522 /// load-bearing fields above.
523 ///
524 /// # Algorithm
525 ///
526 /// Hand-rolled FNV-1a-128 (no third-party deps — `luna-core` 0-dep
527 /// contract is hard). The standard 128-bit constants:
528 ///
529 /// - offset basis = `0x6c62272e07bb014262b821756295c58d`
530 /// - prime = `0x0000000001000000000000000000013b`
531 ///
532 /// Collision resistance suffices for AOT proto ID — collisions
533 /// would manifest as a precompiled trace dispatched against the
534 /// wrong Proto, but the dispatcher's existing guards (entry_tags
535 /// match, head_pc match, register types match) would deopt to
536 /// interp on a mismatch rather than corrupt state.
537 pub fn stable_hash(&self) -> [u8; 16] {
538 let mut h = FnvHash128::new();
539 // 1. Bytecode words — `Inst` is `repr(transparent)` over u32;
540 // feed the raw little-endian bytes so the hash matches
541 // cross-platform (luna only targets little-endian platforms
542 // today, but the explicit LE serialization future-proofs).
543 for inst in self.code.iter() {
544 h.update(&inst.0.to_le_bytes());
545 }
546 // 2. Constants — discriminant + payload. Keep the discriminant
547 // byte values stable: bumping the `Value` enum order would
548 // invalidate AOT cache files, but that's the same constraint
549 // as `Value::tag_byte` already imposes.
550 for c in self.consts.iter() {
551 match c {
552 Value::Nil => h.update(&[0u8]),
553 Value::Bool(b) => {
554 h.update(&[1u8, *b as u8]);
555 }
556 Value::Int(i) => {
557 h.update(&[2u8]);
558 h.update(&i.to_le_bytes());
559 }
560 Value::Float(f) => {
561 // Hash the bit pattern so +0.0 / -0.0 don't
562 // collide and NaNs are stable across runs.
563 h.update(&[3u8]);
564 h.update(&f.to_bits().to_le_bytes());
565 }
566 Value::Str(s) => {
567 h.update(&[4u8]);
568 let bytes = s.as_bytes();
569 h.update(&(bytes.len() as u32).to_le_bytes());
570 h.update(bytes);
571 }
572 // Heap-pointer constants are not produced by the Lua
573 // compiler. A debug_assert keeps the contract honest
574 // without paying a runtime cost in release.
575 Value::Table(_)
576 | Value::Closure(_)
577 | Value::Native(_)
578 | Value::Coro(_)
579 | Value::Userdata(_)
580 | Value::LightUserdata(_) => {
581 debug_assert!(
582 false,
583 "Proto::stable_hash: unexpected heap-pointer constant \
584 (kind={}); luna's compiler only emits nil/bool/number/string \
585 constants",
586 c.type_name()
587 );
588 // Fall-through default: treat as a NUL byte. Won't
589 // happen in practice (compiler invariant), so the
590 // exact behaviour doesn't matter.
591 h.update(&[255u8]);
592 }
593 }
594 }
595 // 3. Upvalue descriptors — `name` bytes affect debug.getinfo
596 // only, but they're cheap and bytecode-equivalent compiles
597 // always produce equal names, so include them.
598 for u in self.upvals.iter() {
599 h.update(&[u.in_stack as u8, u.index, u.read_only as u8]);
600 let name_bytes = u.name.as_bytes();
601 h.update(&(name_bytes.len() as u32).to_le_bytes());
602 h.update(name_bytes);
603 }
604 // 4. Signature bytes.
605 h.update(&[self.num_params, self.is_vararg as u8, self.max_stack]);
606 h.finish()
607 }
608
609 pub(crate) fn trace(&self, m: &mut Marker) {
610 for &k in self.consts.iter() {
611 m.value(k);
612 }
613 for &p in self.protos.iter() {
614 m.header(p.as_ptr() as *mut GcHeader);
615 }
616 m.header(self.source.as_ptr() as *mut GcHeader);
617 // PUC `traverseproto`: the closure cache is a *weak* reference — if
618 // the cached LClosure is unmarked at sweep time, clear the slot
619 // instead of marking it. Queue self for the post-mark cleanup pass
620 // so a closure whose only remaining live reference is the cache
621 // becomes collectable (gc.lua's `__gc` finalisers inside `do ... end`
622 // blocks rely on this).
623 if self.cache.get().is_some() {
624 m.cached_protos.push(self as *const Proto as *mut Proto);
625 }
626 }
627}
628
629/// Closures with `≤ INLINE_UPVALS_N` upvalues skip the
630/// per-closure upvals Box. The `Op::Closure` handler builds upvals
631/// into a stack array and calls `Heap::new_closure_inline(&[Gc<…>])`,
632/// which writes them straight into `inline_storage` — no caller-side
633/// Vec/Box. `closure_alloc`-style benchmarks create 10k single-upval
634/// closures per iter; eliminating the 24-byte Vec alloc shaves ~300µs.
635pub const INLINE_UPVALS_N: usize = 2;
636
637/// A Lua closure: a `Proto` paired with its captured upvalues.
638#[repr(C)]
639pub struct LuaClosure {
640 /// read through raw casts by the GC, not by field access
641 #[allow(dead_code)]
642 pub(crate) hdr: GcHeader,
643 /// The compiled function body this closure binds.
644 pub proto: Gc<Proto>,
645 /// Single source of truth for "where are the upvals?". Points to
646 /// either `inline_storage` (when `upvals_len <= INLINE_UPVALS_N`)
647 /// or `overflow.as_mut_ptr()` (otherwise). Set up by
648 /// `Heap::new_closure*` after the LuaClosure reaches its stable
649 /// heap address.
650 pub(crate) upvals_ptr: *mut Gc<Upvalue>,
651 pub(crate) upvals_len: u32,
652 /// Inline storage for small closures. Only the first
653 /// `upvals_len.min(INLINE_UPVALS_N)` slots are initialised.
654 /// `Gc<Upvalue>` is `Copy` so no explicit `Drop` pass is needed.
655 ///
656 /// `UnsafeCell` for the same reason as `Table::inline_storage`:
657 /// `upvals_ptr` is a self-referential cached pointer into this
658 /// field, and a `&mut self` function-entry retag would otherwise
659 /// invalidate it under Stacked Borrows (Miri reports it). All
660 /// access goes through `upvals_ptr` / `.get()`.
661 pub(crate) inline_storage:
662 std::cell::UnsafeCell<[std::mem::MaybeUninit<Gc<Upvalue>>; INLINE_UPVALS_N]>,
663 /// Overflow box for closures with `> INLINE_UPVALS_N` upvalues.
664 /// Empty box (dangling, no allocation) otherwise.
665 pub(crate) overflow: Box<[Gc<Upvalue>]>,
666}
667
668// SAFETY: `upvals_ptr` always refers to memory the same LuaClosure
669// owns (its own inline_storage or its `overflow` Box). The closure is
670// heap-allocated and never moves post-adoption.
671unsafe impl Send for LuaClosure {}
672unsafe impl Sync for LuaClosure {}
673
674impl LuaClosure {
675 /// View of all upvalues as a `&[Gc<Upvalue>]`. Backed by inline
676 /// storage when `upvals_len <= INLINE_UPVALS_N`, else by overflow.
677 /// Freshly-derived base pointer for the upvalue storage — same
678 /// Stacked Borrows discipline as `Table::array_base`: a cached
679 /// pointer into `*self` dies on every `&mut self` entry retag, so
680 /// the inline case re-derives through
681 /// `UnsafeCell::get()` at each use; the overflow (heap Box) case
682 /// keeps the cached pointer whose tag lives outside `*self`.
683 /// `upvals_ptr` stays maintained for raw-field consumers.
684 #[inline(always)]
685 fn upvals_base(&self) -> *mut Gc<Upvalue> {
686 if self.upvals_len as usize <= INLINE_UPVALS_N {
687 self.inline_storage.get() as *mut Gc<Upvalue>
688 } else {
689 self.upvals_ptr
690 }
691 }
692
693 /// View of all upvalues as a `&[Gc<Upvalue>]`. Backed by inline
694 /// storage when `upvals_len <= INLINE_UPVALS_N`, else by overflow.
695 #[inline(always)]
696 pub fn upvals(&self) -> &[Gc<Upvalue>] {
697 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
698 unsafe { std::slice::from_raw_parts(self.upvals_base(), self.upvals_len as usize) }
699 }
700
701 #[inline(always)]
702 pub(crate) fn upvals_mut(&mut self) -> &mut [Gc<Upvalue>] {
703 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
704 unsafe { std::slice::from_raw_parts_mut(self.upvals_base(), self.upvals_len as usize) }
705 }
706
707 /// Wire `upvals_ptr` to the active backing storage. Called by the
708 /// Heap closure constructors once the LuaClosure is at its stable
709 /// heap address (inline_storage's address is only valid after the
710 /// Box::new move into the heap).
711 pub(crate) fn init_upvals_ptr(&mut self) {
712 if self.upvals_len as usize <= INLINE_UPVALS_N {
713 self.upvals_ptr = self.inline_storage.get() as *mut Gc<Upvalue>;
714 } else {
715 self.upvals_ptr = self.overflow.as_mut_ptr();
716 }
717 }
718
719 pub(crate) fn trace(&self, m: &mut Marker) {
720 m.header(self.proto.as_ptr() as *mut GcHeader);
721 for &uv in self.upvals().iter() {
722 m.header(uv.as_ptr() as *mut GcHeader);
723 }
724 }
725}
726
727/// A native (host) function with captured upvalues — the analogue of PUC C
728/// closures. Builtins are allocated once at registration so identity is
729/// stable; stateful iterators (gmatch) mutate their upvalues via `as_mut`.
730#[repr(C)]
731pub struct NativeClosure {
732 /// read through raw casts by the GC, not by field access
733 #[allow(dead_code)]
734 pub(crate) hdr: GcHeader,
735 /// The host function pointer this closure dispatches to.
736 pub f: crate::runtime::value::NativeFn,
737 /// Captured upvalues, visible inside `f` via the Vm's call API.
738 pub upvals: Box<[Value]>,
739 /// Marker bit for async natives. When `true`,
740 /// `f` is actually an `crate::vm::async_drive::AsyncNativeFn`
741 /// (same pointer width, transmuted at the call site) returning a
742 /// `Pin<Box<dyn Future>>`. The dispatcher's native-call path checks
743 /// this bit and routes through the cooperative-yield mechanism
744 /// instead of invoking `f` synchronously. Default `false` (sync
745 /// native) for every other construction site.
746 pub is_async: bool,
747}
748
749impl NativeClosure {
750 pub(crate) fn trace(&self, m: &mut Marker) {
751 for &v in self.upvals.iter() {
752 m.value(v);
753 }
754 }
755}
756
757/// An upvalue cell. Open: refers to a live VM stack slot (the stack is a GC
758/// root, so open cells trace nothing). Closed: owns the value inline.
759#[repr(C)]
760pub struct Upvalue {
761 /// read through raw casts by the GC, not by field access
762 #[allow(dead_code)]
763 pub(crate) hdr: GcHeader,
764 pub(crate) state: UpvalState,
765}
766
767/// Open / closed state of an upvalue cell.
768#[derive(Clone, Copy)]
769pub enum UpvalState {
770 /// references slot `slot` of `thread`'s value stack (`None` = the main
771 /// thread). The owning thread is tracked so the cell still resolves to the
772 /// right stack after a coroutine swap.
773 Open {
774 /// Stack slot of the captured local on the owning thread.
775 slot: u32,
776 /// Owning thread, or `None` for the main thread.
777 thread: Option<Gc<crate::runtime::coroutine::Coro>>,
778 },
779 /// Captured value has been hoisted into the cell.
780 Closed(
781 /// The closed-over value.
782 Value,
783 ),
784}
785
786impl Upvalue {
787 /// Return the upvalue's current state (open / closed).
788 pub fn state(&self) -> UpvalState {
789 self.state
790 }
791
792 pub(crate) fn set_closed(&mut self, v: Value) {
793 self.state = UpvalState::Closed(v);
794 }
795
796 pub(crate) fn trace(&self, m: &mut Marker) {
797 match self.state {
798 UpvalState::Closed(v) => {
799 m.value(v);
800 }
801 UpvalState::Open {
802 thread: Some(co), ..
803 } => {
804 m.header(co.as_ptr() as *mut GcHeader);
805 }
806 UpvalState::Open { thread: None, .. } => {}
807 }
808 }
809}