Skip to main content

candela/
engine.rs

1//! Embedding / library API for candela.
2//!
3//! This is the persistent, in-process entry point a Rust host (such as Lumen)
4//! uses to embed candela the way it embeds `rhai`/`mlua`: register typed host
5//! functions, compile a script to a reusable [`Program`], and invoke
6//! script-defined functions by name with marshalled arguments, all while
7//! keeping interpreter state (registers + heap pools) alive between calls.
8//!
9//! ```
10//! let mut engine = candela::Engine::new();
11//! engine.register_host_fn("app", "rows", |id: &str| id.len() as i64);
12//! let mut program = engine.compile("host \"app\" { int rows(string); }\nfn count(id: string) { return app::rows(id); }\nfn main() {}", "main.cdl")?;
13//! let rows = program.call("count", &["board".into()])?;
14//! assert_eq!(rows, candela::Value::Int(5));
15//! # Ok::<(), candela::Diagnostic>(())
16//! ```
17//!
18//! Unlike the one-shot [`crate::candela_run`] C-ABI entry point (which compiles a
19//! script, runs `main`, and returns captured stdout), the [`Engine`]/[`Program`]
20//! pair keeps the compiler and VM state resident so the host can drive the
21//! script incrementally. Errors are returned as structured [`Diagnostic`]
22//! values (reusing the `structured-errors` funnel) instead of printing and
23//! aborting the process. The value-marshalling types it uses ([`Value`],
24//! [`HostType`], ...) live in the VM-only `candela-vm` crate.
25
26use crate::Cfg;
27use crate::compiler::CompileOutput;
28use crate::compiler::FileNamespaces;
29use crate::compiler::compiler_data::Dynamiclib;
30use crate::compiler::compiler_data::Function;
31use crate::compiler::compiler_data::IndirectRegisters;
32use crate::compiler::compiler_data::State;
33use crate::compiler::compiler_data::Variable;
34use crate::compiler::expr::Expr;
35use crate::compiler::imports::DEFAULT_PACKAGE_ENTRY;
36use crate::compiler::imports::ImportResolver;
37use crate::compiler::type_system::Generics;
38use crate::compiler::use_immediates;
39use crate::macros::MacroEnv;
40use crate::macros::MacroError;
41#[cfg(not(target_arch = "wasm32"))]
42use crate::manifest::MANIFEST_NAME;
43#[cfg(not(target_arch = "wasm32"))]
44use crate::manifest::Manifest;
45use crate::trampoline::check_only;
46use crate::trampoline::compile_checked;
47use crate::trampoline::compile_trampoline;
48use crate::warnings::collect_warnings;
49use candela_vm::data::Data;
50use candela_vm::data::NULL;
51use candela_vm::embed::HostDispatch;
52use candela_vm::embed::HostError;
53use candela_vm::embed::HostRegistry;
54use candela_vm::embed::HostType;
55use candela_vm::embed::IntoHostFn;
56use candela_vm::embed::Value;
57use candela_vm::embed::describe_value;
58use candela_vm::embed::holds_enum;
59use candela_vm::embed::marshal_args;
60use candela_vm::embed::unmarshal_value;
61use candela_vm::errors::Diagnostic;
62use candela_vm::errors::ErrorCtx;
63use candela_vm::errors::collect_diagnostic;
64use candela_vm::instr::Instr;
65use candela_vm::rt::DataType;
66use candela_vm::rt::DynamicLibFn;
67use candela_vm::rt::EnumType;
68use candela_vm::rt::GcStats;
69use candela_vm::rt::HostFnSig;
70use candela_vm::rt::InstrSrc;
71use candela_vm::rt::Pools;
72use candela_vm::rt::Source;
73use candela_vm::rt::Span;
74use candela_vm::rt::Struct;
75use candela_vm::vm;
76use candela_vm::vm::RegisterFile;
77use rustc_hash::FxHashMap;
78use smol_strc::SmolStr;
79use std::path::Path;
80use std::path::PathBuf;
81
82// `Engine::compile` and `Program::call` recover from a compile error by
83// catching the unwind the error funnel raises, so this API exists only under an
84// unwinding panic strategy. Built with `panic = "abort"` the first error in a
85// script would abort the process instead of coming back as a `Diagnostic`,
86// which is what the LSP and every embedding host read.
87//
88// wasm32 aborts on panic whatever the profile says, so the check is for the
89// profiles that build the toolchain.
90#[cfg(not(target_arch = "wasm32"))]
91const _: () = assert!(
92    cfg!(panic = "unwind"),
93    "the candela embedding API needs an unwinding panic strategy; see [profile.release]"
94);
95
96/// The persistent embedding entry point. Holds the table of registered host
97/// functions and compiles scripts into reusable [`Program`]s.
98///
99/// This is the library analogue of the one-shot [`crate::candela_run`]: it does
100/// not run a script and hand back stdout, it keeps compiler + VM state resident
101/// so the host can call into the script repeatedly.
102#[derive(Default)]
103pub struct Engine {
104    registry: HostRegistry,
105    macros: MacroEnv,
106    resolver: ImportResolver,
107    cfg: Cfg,
108}
109
110impl Engine {
111    #[must_use]
112    pub fn new() -> Self {
113        Self::default()
114    }
115
116    /// Points library imports at `dir` instead of the directory the toolchain
117    /// installed.
118    ///
119    /// `dir` is the one that holds `std/`, so `import "std/string";` reads
120    /// `dir/std/string.cdl`. A host that ships the standard library inside its
121    /// own application directory names it here, and the scripts it compiles
122    /// find it wherever the application was installed.
123    ///
124    /// ```no_run
125    /// let engine = candela::Engine::new().with_lib_dir("/opt/myapp/libs");
126    /// ```
127    #[must_use]
128    pub fn with_lib_dir(mut self, dir: impl Into<PathBuf>) -> Self {
129        self.resolver.set_lib_dir(dir.into());
130        self
131    }
132
133    /// Sets the flags `@cfg(...)` attributes test in the scripts this engine
134    /// compiles.
135    ///
136    /// Code under a `@cfg` whose condition is false is dropped before names
137    /// resolve, so it may import a module or call a function that does not
138    /// exist here. A flag the configuration does not enable is false.
139    ///
140    /// ```no_run
141    /// let engine = candela::Engine::new().with_cfg(candela::Cfg::from_iter(["web"]));
142    /// ```
143    #[must_use]
144    pub fn with_cfg(mut self, cfg: Cfg) -> Self {
145        self.cfg = cfg;
146        self
147    }
148
149    /// Makes `dir` the root of the package a script imports as `name`.
150    ///
151    /// The package is entered through the file its `candela.toml` names as
152    /// `entry`, or `src/main.cdl` when the manifest names none or there is no
153    /// manifest. `import "shapes";` then reads that file, and
154    /// `import "shapes/circle";` reads `circle.cdl` from the entry's
155    /// directory. `dir` is searched for native libraries too, so a package
156    /// that ships a shared library under its root binds a `dylib` import with
157    /// no further setup.
158    ///
159    /// ```no_run
160    /// let engine = candela::Engine::new().with_import_root("shapes", "/cache/shapes/1.2.3");
161    /// ```
162    #[must_use]
163    pub fn with_import_root(mut self, name: &str, dir: impl Into<PathBuf>) -> Self {
164        let dir = dir.into();
165        let entry = package_entry(&dir);
166        self.resolver.add_root(name, dir, entry);
167        self
168    }
169
170    /// Registers a typed host function under `namespace::name`.
171    ///
172    /// The closure may take any combination of `i64`/`i32`, `f64`, `bool`,
173    /// `String` (or a single `&str`) arguments and return one of those or `()`.
174    /// The declared types are checked against the script's `host` block when
175    /// [`Engine::compile`] runs; a mismatch is a clean [`Diagnostic`], never a
176    /// panic.
177    ///
178    /// A closure that can fail returns `Result<T, HostError>` instead of `T`,
179    /// and the error is raised at the call site in the script:
180    ///
181    /// ```no_run
182    /// use candela::HostError;
183    ///
184    /// let mut engine = candela::Engine::new();
185    /// engine.register_host_fn("fs", "read", |path: &str| {
186    ///     std::fs::read_to_string(path).map_err(HostError::new)
187    /// });
188    /// ```
189    ///
190    /// The type checked against the declaration is the `T` inside the `Result`,
191    /// so both spellings bind to the same `host` signature.
192    pub fn register_host_fn<Marker, F>(&mut self, namespace: &str, name: &str, f: F)
193    where
194        F: IntoHostFn<Marker>,
195    {
196        self.registry.register_host_fn(namespace, name, f);
197    }
198
199    /// Registers a host function whose signature is given as data rather than
200    /// derived from a Rust closure's types.
201    ///
202    /// The closure takes the arguments as a `&[Value]` slice, and `arg_types` /
203    /// `ret_type` say what the script may pass and expect. The binding is
204    /// checked against the `host` block exactly as [`Engine::register_host_fn`]
205    /// is, so a declaration that disagrees is a [`Diagnostic`] at
206    /// [`Engine::compile`] time, and the declaration must not use `...`.
207    ///
208    /// ```no_run
209    /// use candela::{HostType, Value};
210    ///
211    /// let mut engine = candela::Engine::new();
212    /// engine.register_host_fn_typed(
213    ///     "gpio",
214    ///     "read",
215    ///     vec![HostType::Int],
216    ///     HostType::Int,
217    ///     |args: &[Value]| Ok(Value::Int(args[0].as_i64().unwrap_or(0))),
218    /// );
219    /// ```
220    ///
221    /// Use it when the signature is only known at run time: a plugin table, a
222    /// generated binding, anything with no Rust signature to read the types
223    /// from.
224    pub fn register_host_fn_typed<F>(
225        &mut self,
226        namespace: &str,
227        name: &str,
228        arg_types: Vec<HostType>,
229        ret_type: HostType,
230        f: F,
231    ) where
232        F: Fn(&[Value]) -> Result<Value, HostError> + 'static,
233    {
234        self.registry
235            .register_host_fn_typed(namespace, name, arg_types, ret_type, f);
236    }
237
238    /// Registers a variadic host function under `namespace::name`.
239    ///
240    /// Unlike [`Engine::register_host_fn`], the closure receives every argument
241    /// as a `&[Value]` slice of any length and returns a single [`Value`] (or a
242    /// [`HostError`] to raise in the script), so arguments of mixed /
243    /// dynamically-typed shape can cross the boundary without a fixed Rust
244    /// signature. The `host` block must declare the function with a `...`
245    /// argument list:
246    ///
247    /// ```candela
248    /// host "app" {
249    ///     log(...);
250    /// }
251    /// ```
252    ///
253    /// No arity or per-argument type checking is performed at the call site;
254    /// the closure interprets the slice it is handed. A non-variadic
255    /// declaration bound to a variadic closure (or vice versa) is a clean
256    /// [`Diagnostic`] at [`Engine::compile`] time.
257    pub fn register_host_fn_variadic<F>(&mut self, namespace: &str, name: &str, f: F)
258    where
259        F: Fn(&[Value]) -> Result<Value, HostError> + 'static,
260    {
261        self.registry.register_host_fn_variadic(namespace, name, f);
262    }
263
264    /// Registers the expander for `name!( ... )` invocations in the scripts this
265    /// engine compiles.
266    ///
267    /// The expander receives the raw text between the parentheses, which
268    /// candela does not interpret, and returns candela source for one
269    /// expression that is parsed in its place. A [`MacroError`] it returns
270    /// instead becomes a compile error at the macro, at the byte offset into
271    /// the region the error names.
272    ///
273    /// ```no_run
274    /// use candela::macros::MacroError;
275    ///
276    /// let mut engine = candela::Engine::new();
277    /// engine.register_macro("rows", |body: &str| {
278    ///     Ok::<String, MacroError>(body.lines().count().to_string())
279    /// });
280    /// ```
281    pub fn register_macro<F>(&mut self, name: &str, expander: F)
282    where
283        F: Fn(&str) -> Result<String, MacroError> + 'static,
284    {
285        self.macros.register(name, expander);
286    }
287
288    /// Sets what a macro with no registered expander does: fail the compile
289    /// naming it (the default), or, when `allow` is true, compile as `null`.
290    ///
291    /// Tooling that reads scripts written for a host it is not part of turns
292    /// this on, so the host's macros do not read as errors.
293    pub const fn allow_unknown_macros(&mut self, allow: bool) {
294        self.macros.allow_unknown(allow);
295    }
296
297    /// Type-checks `src` the way [`Engine::compile`] does, without producing a
298    /// program, and returns the warnings the compile raised.
299    ///
300    /// Every function whose parameters are all annotated is checked at those
301    /// types, and a function a host would call with a bare parameter is
302    /// checked with it typed `any`, as `compile` does. What this leaves out is
303    /// everything a check does not need: a `dylib` block binds the signatures
304    /// it declares without opening its library, so a script whose C library a
305    /// build step has not produced yet still checks; `host` blocks are not
306    /// matched against registered closures; `main` is not required and
307    /// nothing runs.
308    ///
309    /// # Errors
310    ///
311    /// Returns the [`Diagnostic`] of the first error: a parse or type error, a
312    /// body that does not compile at its declared parameter types, or a `dylib`
313    /// signature naming a type C cannot represent.
314    pub fn check(&self, src: &str, filename: &str) -> Result<Vec<Diagnostic>, Diagnostic> {
315        let resolver = &self.resolver;
316        self.cfg.scope(|| {
317            self.macros.scope(|| {
318                collect_diagnostic(|| {
319                    collect_warnings(|| {
320                        let _ = check_only(src.to_owned(), filename, resolver);
321                    })
322                    .1
323                })
324            })
325        })
326    }
327
328    /// Compiles `src` into a reusable [`Program`], binding every `host` function
329    /// it declares to the matching registered closure.
330    ///
331    /// Every function in `src` whose parameters are all annotated is compiled at
332    /// those declared types, whether or not anything calls it, so an error in
333    /// the body of a function `main` never calls comes back from here rather
334    /// than from the first [`Program::call`]. A function with a bare parameter
335    /// that nothing in the script calls is checked with that parameter typed
336    /// `any`, and warned about; the warnings come back on
337    /// [`Program::warnings`]. A function an import brought in stays lazy.
338    ///
339    /// `main` is executed once here (module instantiation), so any top-level
340    /// setup runs before the host makes its first [`Program::call`]. The
341    /// entry-point check runs first, so a broken body is reported before any
342    /// top-level side effect.
343    ///
344    /// # Errors
345    ///
346    /// Returns a [`Diagnostic`] if the script fails to parse/type-check, if the
347    /// body of an annotated function does not compile at its declared parameter
348    /// types, if the script declares no `main`, if a declared `host` function
349    /// has no registered closure, if a registered closure's arity/types
350    /// disagree with the `host` block, or if running `main` raises a runtime
351    /// error.
352    pub fn compile(&self, src: &str, filename: &str) -> Result<Program, Diagnostic> {
353        let filename_owned = filename.to_owned();
354        let resolver = &self.resolver;
355        let (out, warnings): (CompileOutput, Vec<Diagnostic>) = self.cfg.scope(|| {
356            self.macros.scope(|| {
357                collect_diagnostic(|| {
358                    collect_warnings(|| {
359                        // `compile_checked` compiles an entry point for every
360                        // annotated function, which is what makes this a check
361                        // step: a body error in a function `main` never calls
362                        // is reported now, not on the first `Program::call`
363                        // that reaches it. It also leaves those
364                        // specialisations warm for that call.
365                        let (out, _) = compile_checked(src.to_owned(), &filename_owned, resolver);
366                        // A `Program` runs `main` as soon as it is built, so an
367                        // embedded program needs one the way a run from the CLI
368                        // does.
369                        out.require_main();
370                        out
371                    })
372                })
373            })
374        })?;
375
376        // Bind each declared host function to a registered closure, validating
377        // arity + types against the closure's derived signature.
378        let host_dispatch: Vec<HostDispatch> = self
379            .registry
380            .bind(&out.host_fns, &out.structs)
381            .map_err(|e| Diagnostic {
382                filename: filename.to_owned(),
383                span: 0..0,
384                message: e.to_string(),
385                code: e.code().to_owned(),
386            })?;
387
388        let mut program = Program {
389            instructions: out.instructions,
390            registers: out.registers,
391            pools: out.pools,
392            instr_src: out.instr_src,
393            callsite_registers: out.callsite_registers,
394            dyn_lib_fns: out.dyn_lib_fns,
395            host_sigs: out.host_fns,
396            host_dispatch,
397            allocated_arg_count: out.allocated_arg_count,
398            allocated_call_depth: out.allocated_call_depth,
399            sources: out.sources,
400            structs: out.structs,
401            enums: out.enums,
402            functions: out.functions,
403            dyn_libs: out.dyn_libs,
404            namespaces: out.namespaces,
405            const_registers: out.const_registers,
406            free_registers: out.free_registers,
407            indirect_registers: out.indirect_registers,
408            generics: out.generics,
409            warnings,
410            calls: FxHashMap::default(),
411        };
412
413        // Instantiate: run `main` once so top-level state is established before
414        // the first host-driven call.
415        program.execute_from(0)?;
416        Ok(program)
417    }
418}
419
420/// A compiled candela program with resident interpreter state.
421///
422/// Registers and heap pools persist between [`Program::call`] invocations, so
423/// state established by one call (including anything a host function mutates on
424/// the Rust side) is visible to the next.
425///
426/// `Program` is single-threaded (`!Send`/`!Sync`): it holds `Rc` dispatchers
427/// and reflects candela's single-threaded VM.
428pub struct Program {
429    // ---- VM state (persists across calls) ----
430    instructions: Vec<Instr>,
431    registers: Vec<Data>,
432    pools: Pools,
433    instr_src: Vec<InstrSrc>,
434    callsite_registers: Vec<Vec<u16>>,
435    dyn_lib_fns: Vec<DynamicLibFn>,
436    host_sigs: Vec<HostFnSig>,
437    host_dispatch: Vec<HostDispatch>,
438    allocated_arg_count: usize,
439    allocated_call_depth: usize,
440    sources: Vec<Source>,
441    structs: Vec<Struct>,
442    enums: Vec<EnumType>,
443    // ---- compiler state (drives on-demand call trampolines) ----
444    functions: Vec<Function>,
445    dyn_libs: Vec<Dynamiclib>,
446    namespaces: FileNamespaces,
447    const_registers: FxHashMap<Data, u16>,
448    free_registers: Vec<u16>,
449    generics: Generics,
450    indirect_registers: IndirectRegisters,
451    /// The warnings the compile raised, in the order it raised them.
452    warnings: Vec<Diagnostic>,
453    /// The trampoline compiled for each function and argument types a host
454    /// has called with.
455    calls: FxHashMap<(String, String), CompiledCall>,
456}
457
458/// A host call compiled onto the resident instruction stream.
459#[derive(Clone)]
460struct CompiledCall {
461    /// Where the trampoline starts.
462    start: usize,
463    /// The registers it reads its arguments from, in order.
464    arg_registers: Box<[u16]>,
465    /// The register the call's value lands in.
466    ret_register: u16,
467}
468
469impl Program {
470    /// The warnings compiling this program raised, in the order they were
471    /// raised. A warning does not stop the compile; see the warnings section
472    /// of the error reference for what each one means.
473    #[must_use]
474    pub fn warnings(&self) -> &[Diagnostic] {
475        &self.warnings
476    }
477
478    /// Invokes the script-defined function `fn_name` with `args`, returning its
479    /// value (or [`Value::Null`] for a void function).
480    ///
481    /// The first call with a given set of argument types compiles a small
482    /// trampoline (which specializes `fn_name` for those types if it hasn't
483    /// been already) onto the resident instruction stream; later calls with
484    /// the same types run it again. It runs against the persistent
485    /// register/heap state, so globals mutated by a previous call remain
486    /// visible.
487    ///
488    /// A diagnostic raised while compiling the trampoline leaves the resident
489    /// tables exactly as they stood before this call began, and the `Program`
490    /// stays callable afterward.
491    ///
492    /// # Errors
493    ///
494    /// Returns a [`Diagnostic`] if `fn_name` is unknown, if the arguments don't
495    /// type-check against its signature, or if the call raises a runtime error.
496    pub fn call(&mut self, fn_name: &str, args: &[Value]) -> Result<Value, Diagnostic> {
497        // An enum travels outward only, and no parameter accepts one, not even
498        // an `any` one that accepts everything else. Refusing it here, before
499        // anything is marshalled, is what keeps the script from reading a null
500        // the host never passed.
501        if let Some((index, value)) = args.iter().enumerate().find(|(_, v)| holds_enum(v)) {
502            return Err(Diagnostic {
503                filename: self
504                    .sources
505                    .first()
506                    .map(|source| source.filename.to_string())
507                    .unwrap_or_default(),
508                span: 0..0,
509                message: format!(
510                    "argument {} of '{fn_name}' cannot be a {}: an enum value crosses the boundary outward only",
511                    index + 1,
512                    describe_value(value)
513                ),
514                code: String::from("argument_type_mismatch"),
515            });
516        }
517
518        // A call is compiled once per function and argument types, into a
519        // trampoline that reads its arguments from registers of its own, and
520        // every later call with the same types writes them and runs it again.
521        // The key renders the types, since `DataType` equality lets an empty
522        // list stand for a list of anything.
523        let types: Vec<DataType> = args.iter().map(value_datatype).collect();
524        let key = (fn_name.to_owned(), format!("{types:?}"));
525        let call = if let Some(call) = self.calls.get(&key) {
526            marshal_args(
527                args,
528                &call.arg_registers,
529                &mut self.registers,
530                &mut self.pools,
531            );
532            call.clone()
533        } else {
534            let call = self.compile_call(fn_name, args, types)?;
535            self.calls.insert(key, call.clone());
536            call
537        };
538
539        self.execute_from(call.start)?;
540        let ret_id = call.ret_register;
541
542        Ok(unmarshal_value(
543            self.registers[ret_id as usize],
544            &self.pools.objs,
545            &self.pools.maps,
546            &self.pools.strings,
547            &self.structs,
548            &self.enums,
549        ))
550    }
551
552    /// Compiles the trampoline for a call of `fn_name` with arguments of
553    /// `types`, and leaves `args` in the registers it reads them from.
554    ///
555    /// A diagnostic raised while compiling it (an undeclared name at the call
556    /// site, a type mismatch, a nested call that needed its own
557    /// specialization first) leaves the resident tables exactly as they stood
558    /// before; see [`State::rollback_to`].
559    fn compile_call(
560        &mut self,
561        fn_name: &str,
562        args: &[Value],
563        types: Vec<DataType>,
564    ) -> Result<CompiledCall, Diagnostic> {
565        // Every table a trampoline compile can grow is snapshotted up front, so
566        // a diagnostic raised while compiling it can be undone rather than
567        // leaving a half-compiled specialization for a later call to trip on.
568        let checkpoint = self.compiler_state().checkpoint();
569
570        // Each argument is a variable seeded into a register of its own, which
571        // holds this call's value while the trampoline compiles.
572        let arg_registers: Box<[u16]> = args
573            .iter()
574            .map(|_| {
575                self.registers.push(NULL);
576                (self.registers.len() - 1) as u16
577            })
578            .collect();
579        marshal_args(args, &arg_registers, &mut self.registers, &mut self.pools);
580
581        let dummy = Span { start: 0, end: 0 };
582        let mut arg_exprs: Vec<Expr> = Vec::with_capacity(args.len());
583        let mut seed_vars: Vec<Variable> = Vec::with_capacity(args.len());
584        for (i, (&register_id, var_type)) in arg_registers.iter().zip(types).enumerate() {
585            let name = SmolStr::from(format!("__host_arg{i}"));
586            seed_vars.push(Variable {
587                declared: None,
588                name: name.clone(),
589                register_id,
590                cell: false,
591                var_type,
592            });
593            arg_exprs.push(Expr::Var(name, dummy));
594        }
595
596        let arg_spans: Box<[Span]> = args.iter().map(|_| dummy).collect();
597        let call_expr = Expr::FunctionCall(
598            arg_exprs.into_boxed_slice(),
599            Box::from([SmolStr::from(fn_name)]),
600            dummy,
601            arg_spans,
602            Box::from([]),
603        );
604
605        // Compile the trampoline (type-checks the call) under a diagnostic sink.
606        let offset = self.instructions.len() as u16;
607        let (mut output, ret_id) = match collect_diagnostic(|| {
608            let mut state = self.compiler_state();
609            compile_trampoline(&mut state, offset, &call_expr, seed_vars)
610        }) {
611            Ok(compiled) => compiled,
612            Err(diagnostic) => {
613                self.compiler_state().rollback_to(&checkpoint);
614                return Err(diagnostic);
615            }
616        };
617
618        output.push(Instr::Halt(0));
619        use_immediates(
620            &mut output,
621            &self.instructions,
622            &self.registers,
623            &self.const_registers,
624        );
625        let start = self.instructions.len();
626        self.instructions.extend(output);
627        Ok(CompiledCall {
628            start,
629            arg_registers,
630            ret_register: ret_id.unwrap_or(0),
631        })
632    }
633
634    /// Does up to about `budget` units of garbage collection while the
635    /// program is idle, and answers whether no collection work is left.
636    ///
637    /// Call it between calls into the program, where the host would otherwise
638    /// wait, such as between frames. It carries on a collection already under
639    /// way, or begins one when the heap has grown enough since the last; on a
640    /// heap that has not, it does nothing and answers `true`. A unit is about
641    /// one object traced or one freed slot, never a length of time, so the
642    /// same calls do the same work on every machine. `collect(u32::MAX)` runs
643    /// a whole collection. Beginning a collection reads every root at once
644    /// whatever the budget.
645    ///
646    /// Once a host has called this, allocations leave more of the work to it
647    /// and begin a collection themselves only when the heap has grown further.
648    pub fn collect(&mut self, budget: u32) -> bool {
649        self.pools.collect(&self.registers, budget)
650    }
651
652    /// What the collector has done over this program's heap, and how large
653    /// the heap is. See [`GcStats`].
654    #[must_use]
655    pub fn gc_stats(&self) -> GcStats {
656        self.pools.gc_stats()
657    }
658
659    /// Borrows the resident compiler state a trampoline compile writes into.
660    fn compiler_state(&mut self) -> State<'_> {
661        State {
662            registers: &mut self.registers,
663            fns: &mut self.functions,
664            structs: &mut self.structs,
665            enums: &mut self.enums,
666            pools: &mut self.pools,
667            instr_src: &mut self.instr_src,
668            callsite_registers: &mut self.callsite_registers,
669            dyn_libs: &mut self.dyn_libs,
670            allocated_arg_count: &mut self.allocated_arg_count,
671            allocated_call_depth: &mut self.allocated_call_depth,
672            const_registers: &mut self.const_registers,
673            free_registers: &mut self.free_registers,
674            sources: &mut self.sources,
675            reserved_registers: rustc_hash::FxHashSet::default(),
676            value_callsites: rustc_hash::FxHashSet::default(),
677            optimize: false,
678            namespaces: &mut self.namespaces,
679            generics: &mut self.generics,
680            indirect_registers: &mut self.indirect_registers,
681            propagations: Vec::new(),
682            fn_returns: Vec::new(),
683            captured_binders: Vec::new(),
684        }
685    }
686
687    /// Runs the VM against the resident state starting at instruction `start`,
688    /// capturing any error as a [`Diagnostic`].
689    fn execute_from(&mut self, start: usize) -> Result<(), Diagnostic> {
690        let err_ctx = ErrorCtx {
691            instr_src: &self.instr_src,
692            sources: &self.sources,
693        };
694
695        // Move the register file out so the VM can borrow it mutably, then
696        // reclaim it (register state must persist across calls).
697        let mut register_file = RegisterFile(std::mem::take(&mut self.registers));
698
699        let instructions = &self.instructions;
700        let pools = &mut self.pools;
701        let callsite_registers = &self.callsite_registers;
702        let dyn_lib_fns = &self.dyn_lib_fns;
703        let structs = &self.structs;
704        let enums = &self.enums;
705        let host_sigs = &self.host_sigs;
706        let host_dispatch = &self.host_dispatch;
707        let allocated_arg_count = self.allocated_arg_count;
708        let allocated_call_depth = self.allocated_call_depth;
709
710        // An error comes back as a value, so a call sets up no unwind and
711        // leaves the process panic hook alone.
712        let result = vm::execute(
713            instructions,
714            &mut register_file,
715            pools,
716            &err_ctx,
717            callsite_registers,
718            dyn_lib_fns,
719            structs,
720            enums,
721            allocated_arg_count,
722            allocated_call_depth,
723            host_sigs,
724            host_dispatch,
725            start,
726            None,
727        );
728
729        self.registers = std::mem::take(&mut register_file.0);
730        result.map_err(|error| error.diagnostic(&err_ctx))
731    }
732}
733
734/// Infers the candela [`DataType`] of a [`Value`] so a host-provided array/map
735/// argument can be given a type the call site type-checks against. Homogeneous
736/// element/value types are assumed (matching candela's static collection typing);
737/// the first element is sampled, empty collections yield an unknown element type.
738/// An enum only travels outward, and [`Program::call`] refuses an argument
739/// holding one before it gets here, so that arm is what keeps the match
740/// exhaustive rather than a type anything is given.
741fn value_datatype(v: &Value) -> DataType {
742    match v {
743        Value::Null | Value::Enum { .. } => DataType::Null,
744        Value::Int(_) => DataType::Int,
745        Value::Float(_) => DataType::Float,
746        Value::Bool(_) => DataType::Bool,
747        Value::String(_) => DataType::String,
748        Value::Array(items) => DataType::Array(items.first().map(|e| Box::new(value_datatype(e)))),
749        Value::Map(entries) => {
750            let value = entries.values().next().map(value_datatype);
751            DataType::Map(Box::new((Some(DataType::String), value)))
752        }
753    }
754}
755
756/// The entry a package at `dir` is imported through, relative to `dir`: what
757/// its manifest names, or the default when there is no manifest to ask or it
758/// cannot be read. A manifest the `candela` command would refuse is not this
759/// API's to judge; the import simply lands on the default.
760#[cfg(not(target_arch = "wasm32"))]
761fn package_entry(dir: &Path) -> String {
762    Manifest::load(&dir.join(MANIFEST_NAME)).map_or_else(
763        |_| String::from(DEFAULT_PACKAGE_ENTRY),
764        |m| m.entry().to_owned(),
765    )
766}
767
768/// The wasm build carries no manifest reader, so every package enters at the
769/// default.
770#[cfg(target_arch = "wasm32")]
771fn package_entry(_dir: &Path) -> String {
772    String::from(DEFAULT_PACKAGE_ENTRY)
773}