Skip to main content

luna_core/vm/
ergo.rs

1//! Embedder ergonomics (B2, B7 — Phase 2 P2-A).
2//!
3//! `vm.eval` / `vm.eval_chunk` collapse the
4//! `load(src.as_bytes(), name.as_bytes())? → call_value(Value::Closure(cl), &[])`
5//! sequence into a single call. `vm.intern_str` exposes the heap-side
6//! string interner for embedders that need a `Gc<LuaStr>` handle
7//! (table key, set comparison, etc.).
8//!
9//! ```
10//! use luna_core::vm::Vm;
11//! use luna_core::version::LuaVersion;
12//! let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().open_math().build();
13//! let r = vm.eval("return 1 + 2").unwrap();
14//! assert_eq!(r.len(), 1);
15//! ```
16
17use crate::runtime::heap::Gc;
18use crate::runtime::string::LuaStr;
19use crate::runtime::value::Value;
20use crate::vm::error::LuaError;
21use crate::vm::exec::Vm;
22
23impl Vm {
24    /// Same as [`Vm::eval`] but with a user-supplied chunk name
25    /// (appears in tracebacks for debugging).
26    pub fn eval_chunk(&mut self, src: &str, name: &str) -> Result<Vec<Value>, LuaError> {
27        self.clear_error_metadata();
28        let cl = match self.load(src.as_bytes(), name.as_bytes()) {
29            Ok(c) => c,
30            Err(syntax) => {
31                // B6: classify + record source position.
32                self.set_error_kind(crate::vm::error::LuaErrorKind::Syntax);
33                self.set_error_source(name.to_string(), syntax.line);
34                // Surface SyntaxError as a LuaError carrying the
35                // formatted PUC-style message (`<line>: <msg>`).
36                let msg = format!("{}", syntax);
37                let s = self.heap.intern(msg.as_bytes());
38                return Err(LuaError(Value::Str(s)));
39            }
40        };
41        self.call_value(Value::Closure(cl), &[])
42    }
43
44    /// Intern a UTF-8 string into the heap's string table.
45    /// Idempotent — interning the same bytes twice returns the same
46    /// [`Gc<LuaStr>`] handle.
47    ///
48    /// Useful for embedders constructing table keys or comparing Lua
49    /// strings without going through `Value::Str` wrapping each time.
50    pub fn intern_str(&mut self, s: &str) -> Gc<LuaStr> {
51        self.heap.intern(s.as_bytes())
52    }
53
54    // ─── B12 host-root pool — moved to `crate::vm::host_roots` ───
55    //
56    // v1.3 Phase SR migrated the append-only `Vec<Value>` to a
57    // slot-recycling pool keyed by `HostRootTicket { idx, generation }`.
58    // The new API surface (`pin_host` / `read_host` / `write_host` /
59    // `unpin` / `unpin_all` / `host_root_count`) lives in
60    // [`crate::vm::host_roots`]; the type re-exports are in
61    // [`crate::vm`] (`HostRootTicket`, `HostRootStale`).
62    //
63    // Breaking change vs v1.2 / v1.1: `pin_host` returns
64    // `HostRootTicket` (was `usize`); `host_root_at` / `host_root_set`
65    // are removed in favor of `read_host` / `write_host` which
66    // validate the ticket's generation. See CHANGELOG `[1.3.0]`
67    // Phase SR section for the migration recipe.
68
69    // ─── B6 LuaError classification ──────────────────────────────
70    //
71    // The error value itself (`LuaError(pub Value)`) stays `Copy` so
72    // the 379 existing references / 34 construction sites compile
73    // unchanged. Richer context lives on the Vm; embedders read it
74    // via these accessors after observing a `Result::Err(LuaError)`.
75
76    /// Classification of the most recently raised error on this Vm.
77    /// Returns [`crate::vm::error::LuaErrorKind::Runtime`] before any error fires.
78    pub fn error_kind(&self) -> crate::vm::error::LuaErrorKind {
79        self.last_error_kind
80    }
81
82    /// `(source_name, line)` of the most recently raised error, or
83    /// `None` if the dispatcher could not locate one. Source names
84    /// match Lua's chunk-name convention (`"=eval"`, `"=stdin"`,
85    /// user-supplied via `Vm::load`).
86    pub fn error_source(&self) -> Option<(&str, u32)> {
87        self.last_error_source
88            .as_ref()
89            .map(|(s, l)| (s.as_str(), *l))
90    }
91
92    /// Set the classification for the next error to be raised — used
93    /// by the dispatcher at well-known sites. Embedders writing
94    /// native callbacks may call this before returning `Err(LuaError)`
95    /// to flag a specific kind (e.g. `LuaErrorKind::Type` for a bad
96    /// arg).
97    pub fn set_error_kind(&mut self, kind: crate::vm::error::LuaErrorKind) {
98        self.last_error_kind = kind;
99    }
100
101    /// Set the `(source_name, line)` for the next error to be raised.
102    /// The dispatcher uses this at the syntax-error / parser
103    /// boundary.
104    pub fn set_error_source(&mut self, name: String, line: u32) {
105        self.last_error_source = Some((name, line));
106    }
107
108    /// Clear error classification — called on a clean `call_value`
109    /// entry so old error metadata doesn't leak into the next call.
110    pub fn clear_error_metadata(&mut self) {
111        self.last_error_kind = crate::vm::error::LuaErrorKind::default();
112        self.last_error_source = None;
113    }
114
115    // ─── B8 LuaUserdata host payloads ────────────────────────────
116    //
117    // The closed-world userdata GC infrastructure (`Gc<Userdata>` +
118    // metatable + `__gc`) is already in place; B8 just unlocks the
119    // `Host { type_id, data: Box<dyn Any> }` payload variant for
120    // embedders to stash arbitrary `T: 'static` Rust values.
121    //
122    // v1.1 restricts host types to `'static` (typically heap-only
123    // `Box<...>` or `Rc<...>` to non-Gc objects). Trace-bearing host
124    // payloads land in Phase 4+ alongside the userdata Trace ripple.
125    //
126    // v1.2 Track B: bounds tightened from `T: Any + 'static` to
127    // `T: LuaUserdata` so the metatable produced by `T::add_methods`
128    // is auto-installed at `create_userdata` time. Source-compatible
129    // for B8 users via a one-line `impl LuaUserdata for T {}`.
130
131    /// Allocate a host userdata wrapping `value`. Returns the
132    /// `Value::Userdata` you can `set_global` / pin / pass to scripts.
133    ///
134    /// The metatable produced by [`crate::vm::LuaUserdata::add_methods`]
135    /// is auto-installed on the userdata (cached per `Vm` keyed by
136    /// `TypeId::of::<T>()`). For a type that only needs identity +
137    /// raw host-side access (no Lua-callable methods), provide an
138    /// empty impl:
139    ///
140    /// ```
141    /// # use luna_core::vm::LuaUserdata;
142    /// struct Counter(i64);
143    /// impl LuaUserdata for Counter {}
144    /// ```
145    ///
146    /// ```
147    /// use luna_core::vm::{LuaUserdata, Vm};
148    /// use luna_core::version::LuaVersion;
149    /// use luna_core::runtime::Value;
150    ///
151    /// #[derive(Debug)]
152    /// struct Counter(i64);
153    /// impl LuaUserdata for Counter {}
154    ///
155    /// let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
156    /// let ud = vm.create_userdata(Counter(42));
157    /// vm.set_global("counter", ud).unwrap();
158    ///
159    /// match ud {
160    ///     Value::Userdata(g) => {
161    ///         // SAFETY: single-threaded heap; pointer is live.
162    ///         let r = unsafe { &*g.as_ptr() };
163    ///         assert_eq!(r.downcast::<Counter>().unwrap().0, 42);
164    ///     }
165    ///     _ => unreachable!(),
166    /// }
167    /// ```
168    pub fn create_userdata<T: crate::vm::LuaUserdata>(&mut self, value: T) -> Value {
169        // Phase TB (v1.3): capture a monomorphic trace adapter for `T`.
170        // The fn item `trace_fn_for::<T>` is a distinct code address
171        // per `T` (LLVM monomorphization); the downcast cannot fail
172        // because `register_userdata::<T>` pairs the adapter with
173        // `TypeId::of::<T>()` here, and `Userdata::trace` always reads
174        // the adapter back through the same `Host` instance.
175        fn trace_fn_for<T: crate::vm::LuaUserdata>(
176            any: &(dyn std::any::Any + 'static),
177            m: &mut crate::vm::UserdataMarker<'_>,
178        ) {
179            let typed = any
180                .downcast_ref::<T>()
181                .expect("LuaUserdata trace adapter / TypeId mismatch");
182            typed.trace(m);
183        }
184        let payload = crate::runtime::userdata::UserdataPayload::Host {
185            type_id: std::any::TypeId::of::<T>(),
186            data: Box::new(value),
187            trace_fn: Some(trace_fn_for::<T>),
188        };
189        let g = self.heap.new_userdata(payload, /* writable */ true);
190        // v1.2 Track B — install the trait-derived metatable (or
191        // fetch the cached one). Build only fails if the metatable's
192        // table set overflows MAX_ASIZE, which is impossible with
193        // <100 entries; expect-on-fail is appropriate here.
194        let mt = self
195            .register_userdata::<T>()
196            .expect("LuaUserdata metatable build overflowed");
197        // SAFETY: g is a freshly allocated Gc<Userdata>; the heap is
198        // single-threaded and the pointer is live.
199        unsafe { g.as_mut() }.set_metatable(Some(mt));
200        self.heap
201            .barrier_back(g.as_ptr() as *mut crate::runtime::heap::GcHeader);
202        // PUC contract: __gc is registered for finalization at
203        // metatable-set time, not at later mutation of the metatable.
204        self.check_finalizer_userdata(g);
205        Value::Userdata(g)
206    }
207
208    /// Convenience: [`Self::create_userdata`] + [`Self::set_global`].
209    pub fn set_userdata<T: crate::vm::LuaUserdata>(
210        &mut self,
211        name: &str,
212        value: T,
213    ) -> Result<(), LuaError> {
214        let ud = self.create_userdata(value);
215        self.set_global(name, ud)
216    }
217
218    /// Borrow the host payload of a global userdata as `&T`. Returns
219    /// `None` if the global doesn't exist, isn't a userdata, isn't a
220    /// host userdata, or holds a different type than `T`.
221    ///
222    /// Takes `&mut self` because the lookup interns the key string;
223    /// returning a borrow tied to `&mut Vm` mirrors `vm.set_global`
224    /// ergonomics.
225    pub fn userdata_borrow<T: std::any::Any + 'static>(&mut self, name: &str) -> Option<&T> {
226        let key = Value::Str(self.heap.intern(name.as_bytes()));
227        // SAFETY: Gc<T> = NonNull<T> over the single-threaded GC heap.
228        let v = unsafe { (*self.globals().as_ptr()).get(key) };
229        match v {
230            Value::Userdata(g) => {
231                // SAFETY: single-threaded GC heap; the Gc<Userdata>
232                // stays live as long as it's reachable from globals.
233                let ud = unsafe { &*g.as_ptr() };
234                ud.downcast::<T>()
235            }
236            _ => None,
237        }
238    }
239
240    // ─── B9 Rust-side coroutine drive ────────────────────────────
241
242    /// Create a new coroutine carrying `body` (a Lua function or
243    /// any callable Value). Returns the `Value::Coro` handle ready
244    /// to be passed to [`Self::resume_coroutine`].
245    ///
246    /// Equivalent to `coroutine.create(body)` from a Rust embedder.
247    pub fn create_coroutine(&mut self, body: Value) -> Value {
248        let co = self.new_coro(body);
249        Value::Coro(co)
250    }
251
252    /// Resume a coroutine with the given arguments. Returns the
253    /// yielded values on `yield`, the return values on the body's
254    /// terminal `return`, or an error if the body raised.
255    ///
256    /// Equivalent to `coroutine.resume(co, args...)`. Returns
257    /// `Err(LuaError)` if `co` is not a `Value::Coro`.
258    pub fn resume_coroutine(
259        &mut self,
260        co: Value,
261        args: Vec<Value>,
262    ) -> Result<Vec<Value>, LuaError> {
263        let coro = match co {
264            Value::Coro(c) => c,
265            _ => return Err(LuaError(Value::Nil)),
266        };
267        self.resume_coro(coro, args)
268    }
269
270    // ─── B11 Rust-side debug hook ────────────────────────────────
271
272    /// Install a Rust-side debug hook (see [`crate::vm::exec::RustDebugHook`]). The
273    /// `mask` is a bitwise OR of `HOOK_MASK_CALL` / `HOOK_MASK_RETURN`
274    /// / `HOOK_MASK_LINE` / `HOOK_MASK_COUNT` exported from
275    /// [`crate::vm::exec`]. The `count` arg sets the instruction
276    /// granularity for `Count` events (ignored unless `HOOK_MASK_COUNT`
277    /// is set).
278    ///
279    /// Passing `hook = None` clears the Rust hook; the Lua-side hook
280    /// installed via `debug.sethook` is unaffected.
281    pub fn set_rust_debug_hook(
282        &mut self,
283        hook: Option<crate::vm::exec::RustDebugHook>,
284        mask: u32,
285        count: i64,
286    ) {
287        self.hook.rust_func = hook;
288        // Update event mask flags. Other categories of the Lua hook
289        // stay as they were so a Lua-side debug.sethook + Rust hook
290        // can coexist with independent event subscriptions.
291        if hook.is_some() {
292            self.hook.call |= mask & crate::vm::exec::HOOK_MASK_CALL != 0;
293            self.hook.ret |= mask & crate::vm::exec::HOOK_MASK_RETURN != 0;
294            self.hook.line |= mask & crate::vm::exec::HOOK_MASK_LINE != 0;
295            if mask & crate::vm::exec::HOOK_MASK_COUNT != 0 {
296                self.hook.count = true;
297                self.hook.count_base = count;
298                self.hook.count_left = count;
299            }
300        }
301    }
302
303    /// Clear the Rust-side debug hook (sugar over
304    /// `set_rust_debug_hook(None, 0, 0)`).
305    pub fn clear_rust_debug_hook(&mut self) {
306        self.hook.rust_func = None;
307    }
308
309    /// Read the most recently dispatched Lua opcode, if the Vm is currently
310    /// executing inside a Lua frame. Intended for use from a Count hook
311    /// (installed via [`Self::set_rust_debug_hook`] with `HOOK_MASK_COUNT`)
312    /// to tally per-opcode distribution against a workload — the v1.2
313    /// methodology gate (`perf-decomposition-vs-polish.md` §2 Phase A,
314    /// in `~/.claude-shared/global/methodology/`) requires runtime-counter
315    /// validation of per-iter op mix before any stage decomposition is
316    /// acted on.
317    ///
318    /// Returns `None` outside a Lua frame (top-level setup, while a
319    /// native callback or Cont guard is on top of the call stack, etc.).
320    /// Reads `self.frames.last() → CallFrame::Lua(f) → f.closure.proto.code[f.pc - 1]`
321    /// — the just-dispatched opcode (PC has already advanced past it).
322    pub fn current_op(&self) -> Option<crate::vm::isa::Op> {
323        let f = self.jit_last_lua_frame()?;
324        let pc = (f.pc as usize).checked_sub(1)?;
325        let inst = f.closure.proto.code.get(pc)?;
326        Some(inst.op())
327    }
328
329    /// Mutable variant of [`Self::userdata_borrow`].
330    pub fn userdata_borrow_mut<T: std::any::Any + 'static>(
331        &mut self,
332        name: &str,
333    ) -> Option<&mut T> {
334        let key = Value::Str(self.heap.intern(name.as_bytes()));
335        // SAFETY: see userdata_borrow.
336        let v = unsafe { (*self.globals().as_ptr()).get(key) };
337        match v {
338            Value::Userdata(g) => {
339                // SAFETY: see userdata_borrow; the returned &mut is
340                // exclusive within the &mut self window.
341                let ud = unsafe { &mut *g.as_ptr() };
342                ud.downcast_mut::<T>()
343            }
344            _ => None,
345        }
346    }
347}