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, (®ister_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}