Skip to main content

hara_native/vm/
machine.rs

1//! The synchronous stack machine.
2//!
3//! Validated programs reject impossible indexes and avoid per-instruction allocation.
4//!
5//! VM closures and static calls stay inside one machine. Call frames and
6//! compact scalar slots avoid native callback recursion and boxed integer
7//! traffic on the hot path; closures are converted to shared runtime values
8//! only when they escape through the public value boundary.
9//!
10//! Exceptions (milestone 3): every failure routes through
11//! [`Machine::raise`], which unwinds to the innermost covering try-table
12//! entry. Catch dispatch and binding identity come from the shared
13//! `core::catch_matches`/`core::caught_error` boundary, so thrown values
14//! and runtime-error strings behave exactly as in the tree evaluator.
15
16use std::cell::RefCell;
17use std::collections::{HashMap, VecDeque};
18use std::rc::{Rc, Weak};
19
20use super::error::VmError;
21use super::fiber::{VmFiber, VmFiberState};
22use super::frame::Frame;
23use super::opcode::Instruction;
24use super::program::{FunctionPrototype, Program};
25use super::slot::{VmClosure, VmMultiArity, VmSlot};
26use crate::core::{
27    call_value, native_fiber_function, with_namespace_registry, Cont, Promise, PromiseState, Step,
28    Value,
29};
30use crate::task::promise::settle_result;
31
32#[path = "machine/async_runtime.rs"]
33mod async_runtime;
34#[path = "machine/constants.rs"]
35mod constants;
36#[path = "machine/coroutine_runtime.rs"]
37mod coroutine_runtime;
38use constants::constant_string;
39#[path = "machine/dispatch.rs"]
40mod dispatch;
41use dispatch::Dispatch;
42#[path = "machine/globals.rs"]
43mod globals;
44use async_runtime::{async_result, async_result_from_outcome};
45#[cfg(feature = "bytecode-instrumentation")]
46#[path = "machine/instrumentation.rs"]
47pub mod instrumentation;
48#[cfg(feature = "bytecode-observation")]
49#[path = "machine/observation.rs"]
50pub mod observation;
51
52/// Terminal state of a machine run. Suspension variants belong to the
53/// later async milestone; adding them does not change instruction
54/// dispatch, only the set of exit points.
55pub enum VmOutcome {
56    Returned(Value),
57    Failed(VmError),
58    Suspended(Promise),
59    Yielded(Value),
60}
61
62/// A synchronous interpreter for one function of a validated [`Program`].
63pub struct Machine {
64    program: Rc<Program>,
65    function: usize,
66    frame: Frame,
67    stack: Vec<VmSlot>,
68    scratch: Vec<Value>,
69    calls: Vec<SavedFrame>,
70    free_locals: Vec<Vec<VmSlot>>,
71    free_args: Vec<Vec<VmSlot>>,
72    vm_globals: HashMap<usize, VmSlot>,
73    next_closure_identity: u64,
74    scheduler: Weak<RefCell<AsyncScheduler>>,
75    scheduler_owner: Option<Rc<RefCell<AsyncScheduler>>>,
76    ip: usize,
77    #[cfg(feature = "tracing-jit")]
78    jit: crate::jit::runtime::JitRuntime,
79    #[cfg(feature = "tracing-jit")]
80    jit_path: Vec<(usize, u32)>,
81    #[cfg(feature = "tracing-jit")]
82    jit_suppressed_range: Option<(usize, u32, u32)>,
83    #[cfg(feature = "tracing-jit")]
84    jit_loop_entries: HashMap<(usize, u32), Vec<crate::jit::TraceValue>>,
85    #[cfg(feature = "tracing-jit")]
86    jit_status_function: usize,
87    #[cfg(feature = "tracing-jit")]
88    jit_function_disabled: bool,
89}
90
91#[cfg(feature = "tracing-jit")]
92struct CachedJit {
93    program: Weak<Program>,
94    runtime: crate::jit::runtime::JitRuntime,
95}
96
97#[cfg(feature = "tracing-jit")]
98thread_local! {
99    static PROGRAM_JITS: RefCell<HashMap<usize, CachedJit>> = RefCell::new(HashMap::new());
100}
101
102#[cfg(feature = "tracing-jit")]
103const MAX_PROGRAM_JITS: usize = 128;
104
105#[cfg(feature = "tracing-jit")]
106fn program_key(program: &Rc<Program>) -> usize {
107    Rc::as_ptr(program) as usize
108}
109
110#[cfg(feature = "tracing-jit")]
111fn take_program_jit(program: &Rc<Program>) -> crate::jit::runtime::JitRuntime {
112    PROGRAM_JITS.with(|cache| {
113        cache
114            .borrow_mut()
115            .remove(&program_key(program))
116            .filter(|cached| {
117                cached
118                    .program
119                    .upgrade()
120                    .is_some_and(|owner| Rc::ptr_eq(&owner, program))
121            })
122            .map(|cached| cached.runtime)
123            .unwrap_or_default()
124    })
125}
126
127#[cfg(feature = "tracing-jit")]
128fn store_program_jit(program: &Rc<Program>, runtime: crate::jit::runtime::JitRuntime) {
129    PROGRAM_JITS.with(|cache| {
130        let mut cache = cache.borrow_mut();
131        cache.retain(|_, cached| cached.program.strong_count() > 0);
132        if cache.len() >= MAX_PROGRAM_JITS && !cache.contains_key(&program_key(program)) {
133            // Program keys are pointer identities. Bound this TLS cache so a
134            // long-lived embedder compiling many retained programs cannot grow
135            // it indefinitely; all entries are merely optimization state.
136            cache.clear();
137        }
138        cache.insert(
139            program_key(program),
140            CachedJit {
141                program: Rc::downgrade(program),
142                runtime,
143            },
144        );
145    });
146}
147
148struct SavedFrame {
149    function: usize,
150    frame: Frame,
151    call_ip: usize,
152}
153
154struct AsyncChild {
155    machine: Machine,
156    result: crate::task::promise::WeakPromise,
157    pending: Promise,
158}
159
160#[derive(Default)]
161struct AsyncScheduler {
162    next_id: u64,
163    children: HashMap<u64, AsyncChild>,
164    ready: VecDeque<(u64, PromiseState)>,
165    polling: bool,
166}
167
168impl Machine {
169    #[cfg(feature = "tracing-jit")]
170    pub(super) fn attach_cached_jit(&mut self) {
171        self.jit = take_program_jit(&self.program);
172    }
173
174    #[cfg(feature = "tracing-jit")]
175    pub(super) fn detach_cached_jit(&mut self) {
176        store_program_jit(&self.program.clone(), std::mem::take(&mut self.jit));
177    }
178
179    /// The machine for the program's entry function.
180    pub fn entry(program: Rc<Program>) -> Machine {
181        let index = usize::from(program.entry);
182        let local_count = usize::from(program.functions[index].local_count);
183        let max_stack = usize::from(program.functions[index].max_stack);
184        let scheduler = Rc::new(RefCell::new(AsyncScheduler::default()));
185        Machine {
186            program,
187            function: index,
188            frame: Frame::entry(local_count),
189            stack: Vec::with_capacity(max_stack),
190            scratch: Vec::new(),
191            calls: Vec::new(),
192            free_locals: Vec::new(),
193            free_args: Vec::new(),
194            vm_globals: HashMap::new(),
195            next_closure_identity: 0,
196            scheduler: Rc::downgrade(&scheduler),
197            scheduler_owner: Some(scheduler),
198            ip: 0,
199            #[cfg(feature = "tracing-jit")]
200            jit: crate::jit::runtime::JitRuntime::default(),
201            #[cfg(feature = "tracing-jit")]
202            jit_path: Vec::new(),
203            #[cfg(feature = "tracing-jit")]
204            jit_suppressed_range: None,
205            #[cfg(feature = "tracing-jit")]
206            jit_loop_entries: HashMap::new(),
207            #[cfg(feature = "tracing-jit")]
208            jit_status_function: usize::MAX,
209            #[cfg(feature = "tracing-jit")]
210            jit_function_disabled: false,
211        }
212    }
213
214    /// The machine for a function call: `args` fill the parameter slots,
215    /// `captures` the capture slots directly above them.
216    pub fn call(
217        program: Rc<Program>,
218        prototype: u16,
219        args: Vec<Value>,
220        captures: Vec<Value>,
221    ) -> Machine {
222        Machine::call_slots(
223            program,
224            prototype,
225            args.into_iter().map(VmSlot::from).collect(),
226            captures.into_iter().map(VmSlot::from).collect(),
227        )
228    }
229
230    fn call_slots(
231        program: Rc<Program>,
232        prototype: u16,
233        args: Vec<VmSlot>,
234        captures: Vec<VmSlot>,
235    ) -> Machine {
236        let scheduler = Rc::new(RefCell::new(AsyncScheduler::default()));
237        Self::call_slots_with_scheduler(
238            program,
239            prototype,
240            args,
241            captures,
242            Rc::downgrade(&scheduler),
243            Some(scheduler),
244        )
245    }
246
247    fn call_slots_with_scheduler(
248        program: Rc<Program>,
249        prototype: u16,
250        mut args: Vec<VmSlot>,
251        captures: Vec<VmSlot>,
252        scheduler: Weak<RefCell<AsyncScheduler>>,
253        scheduler_owner: Option<Rc<RefCell<AsyncScheduler>>>,
254    ) -> Machine {
255        let index = usize::from(prototype);
256        let proto = &program.functions[index];
257        let mut arity = usize::from(proto.arity);
258        if proto.variadic {
259            // The rest parameter occupies the slot directly above the
260            // fixed parameters (captures sit above it): pack the
261            // remaining arguments into a list there, exactly like
262            // `call_function` binds `& rest`.
263            let fixed = arity.min(args.len());
264            let rest = args
265                .split_off(fixed)
266                .into_iter()
267                .map(|value| Machine::into_value(program.clone(), value))
268                .collect();
269            args.push(Value::List(rest).into());
270            arity = fixed + 1;
271        }
272        Machine {
273            frame: Frame::call(usize::from(proto.local_count), arity, args, captures, 0),
274            stack: Vec::with_capacity(usize::from(proto.max_stack)),
275            program,
276            function: index,
277            scratch: Vec::new(),
278            calls: Vec::new(),
279            free_locals: Vec::new(),
280            free_args: Vec::new(),
281            vm_globals: HashMap::new(),
282            next_closure_identity: 0,
283            scheduler,
284            scheduler_owner,
285            ip: 0,
286            #[cfg(feature = "tracing-jit")]
287            jit: crate::jit::runtime::JitRuntime::default(),
288            #[cfg(feature = "tracing-jit")]
289            jit_path: Vec::new(),
290            #[cfg(feature = "tracing-jit")]
291            jit_suppressed_range: None,
292            #[cfg(feature = "tracing-jit")]
293            jit_loop_entries: HashMap::new(),
294            #[cfg(feature = "tracing-jit")]
295            jit_status_function: usize::MAX,
296            #[cfg(feature = "tracing-jit")]
297            jit_function_disabled: false,
298        }
299    }
300
301    fn into_value(program: Rc<Program>, slot: VmSlot) -> Value {
302        match slot {
303            VmSlot::Number(value) => Value::Number(value),
304            VmSlot::Bool(value) => Value::Bool(value),
305            VmSlot::Nil => Value::Nil,
306            VmSlot::Value(value) => Rc::try_unwrap(value).unwrap_or_else(|value| (*value).clone()),
307            VmSlot::InlineClosure { prototype, .. } => Self::closure_value(
308                program,
309                Rc::new(VmClosure {
310                    prototype,
311                    captures: Vec::new(),
312                }),
313            ),
314            VmSlot::Closure(closure) => Self::closure_value(program, closure),
315            VmSlot::MultiArity(dispatch) => {
316                let functions = dispatch
317                    .clauses
318                    .iter()
319                    .cloned()
320                    .map(
321                        |closure| match Self::closure_value(program.clone(), closure) {
322                            Value::Function(function) => function,
323                            _ => unreachable!(),
324                        },
325                    )
326                    .collect();
327                crate::core::arity_dispatcher(&dispatch.name, functions, false)
328            }
329        }
330    }
331
332    fn callable_key(value: &Value) -> Option<usize> {
333        match value {
334            Value::Function(function) => Some(Rc::as_ptr(function) as usize),
335            _ => None,
336        }
337    }
338
339    fn remember_vm_global(&mut self, value: &Value, slot: VmSlot) {
340        if let Some(key) = Self::callable_key(value) {
341            self.vm_globals.insert(key, slot);
342        }
343    }
344
345    fn enter_callable(
346        &mut self,
347        program: &Rc<Program>,
348        callee: VmSlot,
349        mut args: Vec<VmSlot>,
350    ) -> Result<(), String> {
351        match callee {
352            VmSlot::InlineClosure { prototype, .. } => {
353                self.check_arity(program, prototype, args.len())?;
354                self.enter_or_spawn(program, prototype, args, Vec::new());
355                Ok(())
356            }
357            VmSlot::Closure(closure) => {
358                self.check_arity(program, closure.prototype, args.len())?;
359                self.enter_or_spawn(program, closure.prototype, args, closure.captures.clone());
360                Ok(())
361            }
362            VmSlot::MultiArity(dispatch) => {
363                let closure = dispatch
364                    .clauses
365                    .iter()
366                    .find(|closure| {
367                        let proto = &program.functions[usize::from(closure.prototype)];
368                        (!proto.variadic && usize::from(proto.arity) == args.len())
369                            || (proto.variadic && args.len() >= usize::from(proto.arity))
370                    })
371                    .cloned()
372                    .ok_or_else(|| format!("{} has no arity {}", dispatch.name, args.len()))?;
373                self.enter_or_spawn(program, closure.prototype, args, closure.captures.clone());
374                Ok(())
375            }
376            value => {
377                let callee = Self::into_value(program.clone(), value);
378                let runtime_args = args
379                    .drain(..)
380                    .map(|value| Self::into_value(program.clone(), value))
381                    .collect();
382                self.free_args.push(args);
383                let value = if let Some(position) = program.functions[self.function]
384                    .source_map
385                    .position(self.ip)
386                {
387                    crate::core::with_exception_site(
388                        crate::core::ExceptionSite {
389                            namespace: program.namespace.clone(),
390                            resource: None,
391                            line: position.line,
392                            column: position.column,
393                        },
394                        || call_value(callee, runtime_args),
395                    )?
396                } else {
397                    call_value(callee, runtime_args)?
398                };
399                self.stack.push(value.into());
400                self.ip += 1;
401                Ok(())
402            }
403        }
404    }
405
406    fn enter_or_spawn(
407        &mut self,
408        program: &Rc<Program>,
409        prototype: u16,
410        args: Vec<VmSlot>,
411        captures: Vec<VmSlot>,
412    ) {
413        if program.functions[usize::from(prototype)].async_function {
414            let mut child = Machine::call_slots(program.clone(), prototype, args, captures);
415            child.vm_globals = self.vm_globals.clone();
416            child.next_closure_identity = self.next_closure_identity;
417            self.stack
418                .push(Value::Promise(self.spawn_async(child)).into());
419            self.ip += 1;
420        } else {
421            self.enter_prototype(program, prototype, args, captures);
422        }
423    }
424
425    fn check_arity(&self, program: &Program, prototype: u16, argc: usize) -> Result<(), String> {
426        let proto = &program.functions[usize::from(prototype)];
427        let arity = usize::from(proto.arity);
428        if (!proto.variadic && argc != arity) || (proto.variadic && argc < arity) {
429            let expectation = if proto.variadic {
430                format!("at least {arity}")
431            } else {
432                arity.to_string()
433            };
434            return Err(format!("function expects {expectation} arguments"));
435        }
436        Ok(())
437    }
438
439    fn enter_prototype(
440        &mut self,
441        program: &Program,
442        prototype: u16,
443        mut args: Vec<VmSlot>,
444        captures: Vec<VmSlot>,
445    ) {
446        let proto = &program.functions[usize::from(prototype)];
447        let mut frame_arity = usize::from(proto.arity);
448        if proto.variadic {
449            let fixed = frame_arity.min(args.len());
450            let rest = args
451                .split_off(fixed)
452                .into_iter()
453                .map(|value| Self::into_value(self.program.clone(), value))
454                .collect();
455            args.push(Value::List(rest).into());
456            frame_arity = fixed + 1;
457        }
458        let locals = self.free_locals.pop().unwrap_or_default();
459        let frame = Frame::call_reusing(
460            locals,
461            usize::from(proto.local_count),
462            frame_arity,
463            &mut args,
464            captures,
465            self.stack.len(),
466        );
467        self.free_args.push(args);
468        let caller = std::mem::replace(&mut self.frame, frame);
469        self.calls.push(SavedFrame {
470            function: self.function,
471            frame: caller,
472            call_ip: self.ip,
473        });
474        self.function = usize::from(prototype);
475        self.ip = 0;
476    }
477
478    /// Enters a synchronous, capture-free, fixed-arity static target by
479    /// transferring its operands straight into a recycled local frame.
480    fn enter_static_direct(&mut self, program: &Program, prototype: u16, argc: u8) {
481        let proto = &program.functions[usize::from(prototype)];
482        debug_assert_eq!(proto.capture_count, 0);
483        debug_assert!(!proto.async_function);
484        debug_assert!(!proto.variadic);
485        debug_assert_eq!(usize::from(proto.arity), usize::from(argc));
486        let locals = self.free_locals.pop().unwrap_or_default();
487        let frame = Frame::call_static_reusing(
488            locals,
489            usize::from(proto.local_count),
490            &mut self.stack,
491            usize::from(argc),
492        );
493        let caller = std::mem::replace(&mut self.frame, frame);
494        self.calls.push(SavedFrame {
495            function: self.function,
496            frame: caller,
497            call_ip: self.ip,
498        });
499        self.function = usize::from(prototype);
500        self.ip = 0;
501    }
502
503    /// Runs the function to completion or failure.
504    pub fn run(&mut self) -> VmOutcome {
505        let program = self.program.clone();
506        // Guest calls use the explicit frame stack below, so instruction
507        // dispatch may be inlined without increasing native recursion depth.
508        loop {
509            let Some(function) = program.functions.get(self.function) else {
510                return VmOutcome::Failed(VmError::new("function index out of range", 0, None));
511            };
512            let Some(instruction) = function.code.get(self.ip) else {
513                return VmOutcome::Failed(self.error(function, "instruction pointer out of range"));
514            };
515            #[cfg(feature = "tracing-jit")]
516            {
517                // A cached fully-disabled function takes the shortest fallback
518                // path: one predictable flag check per instruction. Recompute
519                // only if control moved to another function.
520                if !self.jit_function_disabled || self.jit_status_function != self.function {
521                    if self.jit_status_function != self.function {
522                        self.jit_function_disabled = self
523                            .jit
524                            .function_is_fully_disabled(&program, self.function as u16);
525                        self.jit_status_function = self.function;
526                    }
527                    if !self.jit_function_disabled {
528                        let instruction = self.ip as u32;
529                        let suppressed = self.jit_suppressed_range.is_some_and(
530                            |(function, header, backedge)| {
531                                function == self.function
532                                    && instruction >= header
533                                    && instruction <= backedge
534                            },
535                        );
536                        if !suppressed {
537                            self.jit_suppressed_range = None;
538                            self.jit_path.push((self.function, instruction));
539                        }
540                    }
541                }
542            }
543            match self.dispatch(&program, function, instruction) {
544                Dispatch::Next(ip) | Dispatch::Unwound(ip) => {
545                    #[cfg(feature = "tracing-jit")]
546                    let mut next_ip = ip;
547                    #[cfg(not(feature = "tracing-jit"))]
548                    let next_ip = ip;
549                    #[cfg(feature = "tracing-jit")]
550                    if !self.jit_function_disabled && ip <= self.ip {
551                        let header = ip as u32;
552                        if self.jit.is_disabled(self.function as u16, header) {
553                            self.jit_suppressed_range =
554                                Some((self.function, header, self.ip as u32));
555                        } else {
556                            let (mut locals, writable) = self.frame.trace_locals();
557                            let recording_locals = self
558                                .jit_loop_entries
559                                .get(&(self.function, header))
560                                .cloned()
561                                .unwrap_or_else(|| locals.clone());
562                            let path_start = self
563                                .jit_path
564                                .iter()
565                                .rposition(|entry| *entry == (self.function, header));
566                            let path = path_start.map_or_else(Vec::new, |start| {
567                                self.jit_path[start..]
568                                    .iter()
569                                    .map(|(_, instruction)| *instruction)
570                                    .collect()
571                            });
572                            if let Some(snapshot) = self.jit.backedge(
573                                &program,
574                                self.function as u16,
575                                self.ip as u32,
576                                header,
577                                &path,
578                                &recording_locals,
579                                &mut locals,
580                            ) {
581                                self.frame.apply_trace_locals(&snapshot.locals, &writable);
582                                locals = snapshot.locals;
583                                next_ip = snapshot.instruction as usize;
584                            }
585                            self.jit_loop_entries
586                                .insert((self.function, header), locals);
587                            if self.jit.is_disabled(self.function as u16, header) {
588                                self.jit_suppressed_range =
589                                    Some((self.function, header, self.ip as u32));
590                                self.jit_status_function = usize::MAX;
591                            }
592                        }
593                        self.jit_path.clear();
594                    }
595                    self.ip = next_ip;
596                }
597                Dispatch::Call { callee, args } => {
598                    #[cfg(feature = "tracing-jit")]
599                    {
600                        self.jit_path.clear();
601                        self.jit_loop_entries.clear();
602                    }
603                    if let Err(message) = self.enter_callable(&program, callee, args) {
604                        match self.raise(function, message) {
605                            Ok(target) => self.ip = target,
606                            Err(error) => return VmOutcome::Failed(error),
607                        }
608                    }
609                }
610                Dispatch::CallStatic {
611                    prototype,
612                    args,
613                    captures,
614                } => {
615                    #[cfg(feature = "tracing-jit")]
616                    {
617                        self.jit_path.clear();
618                        self.jit_loop_entries.clear();
619                    }
620                    self.enter_or_spawn(&program, prototype, args, captures)
621                }
622                Dispatch::CallStaticDirect { prototype, argc } => {
623                    #[cfg(feature = "tracing-jit")]
624                    {
625                        self.jit_path.clear();
626                        self.jit_loop_entries.clear();
627                    }
628                    self.enter_static_direct(&program, prototype, argc)
629                }
630                Dispatch::Returned(value) => {
631                    #[cfg(feature = "tracing-jit")]
632                    {
633                        self.jit_path.clear();
634                        self.jit_loop_entries.clear();
635                    }
636                    self.stack.truncate(self.frame.base());
637                    if let Some(caller) = self.calls.pop() {
638                        self.function = caller.function;
639                        let completed = std::mem::replace(&mut self.frame, caller.frame);
640                        self.free_locals.push(completed.into_locals());
641                        self.ip = caller.call_ip + 1;
642                        self.stack.push(value);
643                    } else {
644                        return VmOutcome::Returned(Self::into_value(program.clone(), value));
645                    }
646                }
647                Dispatch::Suspended(promise) => return VmOutcome::Suspended(promise),
648                Dispatch::Yielded(value) => return VmOutcome::Yielded(value),
649                Dispatch::Failed(error) => return VmOutcome::Failed(error),
650            }
651        }
652    }
653
654    /// Pops the callee and arguments for a Call instruction.
655    fn collect_call(&mut self, argc: u8) -> Result<(VmSlot, Vec<VmSlot>), String> {
656        let argc = usize::from(argc);
657        if self.stack.len() < argc + 1 {
658            return Err("stack underflow".to_string());
659        }
660        let mut args = self.free_args.pop().unwrap_or_default();
661        args.extend(self.stack.drain(self.stack.len() - argc..));
662        let callee = self.stack.pop().expect("callee checked above");
663        Ok((callee, args))
664    }
665
666    /// Collects the arguments and capture slots for a CallStatic
667    /// instruction; the nested machine runs from the thin `run` loop.
668    fn collect_call_static(
669        &mut self,
670        program: &Program,
671        function: &FunctionPrototype,
672        prototype: u16,
673        argc: u8,
674    ) -> Result<(u16, Vec<VmSlot>, Vec<VmSlot>), String> {
675        let argc = usize::from(argc);
676        if self.stack.len() < argc {
677            return Err("stack underflow".to_string());
678        }
679        let Some(proto) = program.functions.get(usize::from(prototype)) else {
680            return Err(format!("callstatic target {prototype} out of range"));
681        };
682        let capture_count = usize::from(proto.capture_count);
683        let mut args = self.free_args.pop().unwrap_or_default();
684        args.extend(self.stack.drain(self.stack.len() - argc..));
685        let capture_base = usize::from(function.arity) + usize::from(function.variadic);
686        let Some(captures) = self.frame.slot_range(capture_base, capture_count) else {
687            return Err("capture slots out of range".to_string());
688        };
689        Ok((prototype, args, captures))
690    }
691
692    /// Executes the Closure instruction: builds a plain
693    /// `core::Value::Function` whose native callback re-enters
694    /// [`Machine::call`]. Kept out of the dispatch loop
695    /// (`#[inline(never)]`) so the hot `run` frame stays small — guest
696    /// recursion maps onto native stack depth through Call/CallStatic,
697    /// and this arm carries large transient locals.
698    #[inline(never)]
699    fn exec_closure(
700        &mut self,
701        program: &Rc<Program>,
702        prototype: u16,
703        captures: u8,
704    ) -> Result<(), String> {
705        let captures = usize::from(captures);
706        if self.stack.len() < captures {
707            return Err("stack underflow".to_string());
708        }
709        let Some(_proto) = program.functions.get(usize::from(prototype)) else {
710            return Err(format!("closure prototype {prototype} out of range"));
711        };
712        if captures == 0 {
713            let identity = self.next_closure_identity;
714            self.next_closure_identity = self.next_closure_identity.wrapping_add(1);
715            self.stack.push(VmSlot::InlineClosure {
716                prototype,
717                identity,
718            });
719        } else {
720            let captured = self.stack.split_off(self.stack.len() - captures);
721            self.stack.push(VmSlot::Closure(Rc::new(VmClosure {
722                prototype,
723                captures: captured,
724            })));
725        }
726        Ok(())
727    }
728
729    /// Routes a failure through the static handler table: the innermost
730    /// entry covering the failing instruction gets first catch dispatch,
731    /// then outer entries. Returns the instruction to continue at, or the
732    /// terminal error when no entry handles it.
733    #[cold]
734    #[inline(never)]
735    fn raise(
736        &mut self,
737        _function: &FunctionPrototype,
738        message: impl Into<String>,
739    ) -> Result<usize, VmError> {
740        let message = message.into();
741        loop {
742            let function = &self.program.functions[self.function];
743            let error_ip = self.ip;
744            for entry in function.handlers.iter().rev() {
745                let (start, end) = (entry.start as usize, entry.end as usize);
746                if error_ip < start || error_ip >= end {
747                    continue;
748                }
749                let depth = self.frame.base() + usize::from(entry.depth);
750                if self.stack.len() < depth {
751                    return Err(self.error(function, "handler stack depth out of range"));
752                }
753                for catch in &entry.catches {
754                    if crate::core::catch_matches(&message, &catch.class) {
755                        self.stack.truncate(depth);
756                        let value = crate::core::caught_error(&message);
757                        if !self.frame.store(catch.binding, value.into()) {
758                            return Err(self.error(function, "catch binding slot out of range"));
759                        }
760                        return Ok(catch.target as usize);
761                    }
762                }
763                if let Some(finally) = entry.finally {
764                    let (Some(value_slot), Some(flag_slot)) =
765                        (entry.pending_value, entry.pending_error)
766                    else {
767                        return Err(self.error(function, "handler pending slots missing"));
768                    };
769                    self.stack.truncate(depth);
770                    if !self
771                        .frame
772                        .store(value_slot, crate::core::caught_error(&message).into())
773                        || !self.frame.store(flag_slot, Value::Bool(true).into())
774                    {
775                        return Err(self.error(function, "pending slot out of range"));
776                    }
777                    return Ok(finally as usize);
778                }
779            }
780
781            let Some(caller) = self.calls.pop() else {
782                return Err(VmError::new(
783                    message,
784                    error_ip as u32,
785                    function.source_map.position(error_ip),
786                ));
787            };
788            self.stack.truncate(self.frame.base());
789            self.function = caller.function;
790            let completed = std::mem::replace(&mut self.frame, caller.frame);
791            self.free_locals.push(completed.into_locals());
792            self.ip = caller.call_ip;
793        }
794    }
795
796    fn error(&self, function: &FunctionPrototype, message: impl Into<String>) -> VmError {
797        VmError::new(
798            message,
799            self.ip as u32,
800            function.source_map.position(self.ip),
801        )
802    }
803}
804
805impl Machine {
806    /// Continues a machine stopped at `Await`. The settlement is applied at
807    /// the suspended instruction so ordinary exception handlers see a
808    /// rejected promise exactly like a guest throw.
809    pub fn resume(&mut self, state: PromiseState) -> VmOutcome {
810        let Some(function) = self.program.functions.get(self.function).cloned() else {
811            return VmOutcome::Failed(VmError::new("function index out of range", 0, None));
812        };
813        let protocol_deref = match function.code.get(self.ip) {
814            Some(Instruction::ProtocolCall { target, argc }) if *argc == 1 => {
815                constant_string(&self.program, *target)
816                    .is_some_and(|name| name == "std.protocol.ideref.IDeref/deref")
817            }
818            _ => false,
819        };
820        if !protocol_deref && !matches!(function.code.get(self.ip), Some(Instruction::Await)) {
821            return VmOutcome::Failed(
822                self.error(&function, "VM is not suspended at await or deref"),
823            );
824        }
825        match state {
826            PromiseState::Pending => {
827                let promise = match self.stack.last().and_then(VmSlot::runtime_value) {
828                    Some(Value::Promise(promise)) => promise,
829                    _ => {
830                        return VmOutcome::Failed(self.error(
831                            &function,
832                            if protocol_deref {
833                                "deref expects a promise"
834                            } else {
835                                "await expects a promise"
836                            },
837                        ))
838                    }
839                };
840                return VmOutcome::Suspended(promise);
841            }
842            PromiseState::Fulfilled(value) => {
843                self.stack.pop();
844                self.stack.push(value.into());
845                self.ip += 1;
846            }
847            PromiseState::Rejected(error) => {
848                self.stack.pop();
849                let message = if protocol_deref {
850                    crate::core::promise_rejection_error(error)
851                } else {
852                    error.message()
853                };
854                match self.raise(&function, message) {
855                    Ok(target) => self.ip = target,
856                    Err(error) => return VmOutcome::Failed(error),
857                }
858            }
859        }
860        self.run()
861    }
862
863    pub fn resume_yield(&mut self, value: Value) -> VmOutcome {
864        let Some(function) = self.program.functions.get(self.function).cloned() else {
865            return VmOutcome::Failed(VmError::new("function index out of range", 0, None));
866        };
867        if !matches!(function.code.get(self.ip), Some(Instruction::Yield)) {
868            return VmOutcome::Failed(self.error(&function, "VM is not suspended at yield"));
869        }
870        self.stack.push(value.into());
871        self.ip += 1;
872        self.run()
873    }
874}
875
876fn run_entry(program: Rc<Program>) -> Result<Value, VmError> {
877    let mut machine = Machine::entry(program.clone());
878    #[cfg(feature = "tracing-jit")]
879    {
880        machine.jit = take_program_jit(&program);
881    }
882    let outcome = machine.run();
883    #[cfg(feature = "tracing-jit")]
884    store_program_jit(&program, machine.jit);
885    match outcome {
886        VmOutcome::Returned(value) => Ok(value),
887        VmOutcome::Failed(error) => Err(error),
888        VmOutcome::Suspended(_) => Err(VmError::new(
889            "VM fiber suspended on an unresolved promise",
890            0,
891            None,
892        )),
893        VmOutcome::Yielded(_) => Err(VmError::new(
894            "coroutine/yield used outside of a coroutine",
895            0,
896            None,
897        )),
898    }
899}
900
901#[cfg(feature = "tracing-jit")]
902fn cached_jit_runtime<R>(
903    program: &Rc<Program>,
904    access: impl FnOnce(&crate::jit::runtime::JitRuntime) -> R,
905) -> Option<R> {
906    PROGRAM_JITS.with(|cache| {
907        cache
908            .borrow()
909            .get(&program_key(program))
910            .and_then(|cached| {
911                cached
912                    .program
913                    .upgrade()
914                    .map(|owner| (owner, &cached.runtime))
915            })
916            .filter(|(owner, _)| Rc::ptr_eq(owner, program))
917            .map(|(_, runtime)| access(runtime))
918    })
919}
920
921#[cfg(all(test, feature = "tracing-jit"))]
922pub(crate) fn cached_trace_count(program: &Rc<Program>) -> usize {
923    cached_jit_runtime(program, crate::jit::runtime::JitRuntime::compiled_count).unwrap_or(0)
924}
925
926#[cfg(all(test, feature = "tracing-jit"))]
927pub(crate) fn active_compiled_trace_count() -> usize {
928    PROGRAM_JITS.with(|cache| {
929        cache
930            .borrow()
931            .values()
932            .filter(|cached| cached.program.strong_count() > 0)
933            .map(|cached| cached.runtime.compiled_count())
934            .sum()
935    })
936}
937
938#[cfg(all(test, feature = "tracing-jit"))]
939pub(crate) fn active_jit_telemetry() -> Vec<crate::jit::JitTelemetry> {
940    PROGRAM_JITS.with(|cache| {
941        cache
942            .borrow()
943            .values()
944            .filter(|cached| cached.program.strong_count() > 0)
945            .map(|cached| cached.runtime.telemetry())
946            .collect()
947    })
948}
949
950#[cfg(feature = "tracing-jit")]
951pub(crate) fn cached_jit_telemetry(program: &Rc<Program>) -> crate::jit::JitTelemetry {
952    cached_jit_runtime(program, crate::jit::runtime::JitRuntime::telemetry).unwrap_or_default()
953}
954
955/// Executes a validated program's entry function.
956///
957/// Programs produced by [`crate::vm::compile_source`] are already
958/// validated; callers constructing programs by hand must run
959/// [`crate::vm::validate`] first. Either way the machine reports
960/// [`VmError`] rather than panicking on malformed state. When no
961/// namespace registry is active the program runs against a throwaway
962/// `user` registry, so same-program `def`/`defn`/`defstruct` can intern
963/// without touching caller state (issue #223).
964pub fn execute_program(program: Rc<Program>) -> Result<Value, VmError> {
965    if crate::core::namespace_registry().is_ok() {
966        return run_entry(program);
967    }
968    let registry = crate::kernel::NamespaceRegistry::new("user");
969    with_namespace_registry(&registry, || run_entry(program))
970}
971
972/// Executes a program against a caller's namespace registry: globals
973/// intern into it and resolve from it, with no env bridge, snapshot, or
974/// refresh (issue #223).
975pub fn execute_program_with_globals(
976    program: Rc<Program>,
977    globals: &crate::kernel::NamespaceRegistry<Value>,
978) -> Result<Value, VmError> {
979    with_namespace_registry(globals, || run_entry(program))
980}