Skip to main content

candela_vm/
artifact.rs

1//! Bytecode artifact format (`.cdlb`) and the VM-only load/run API.
2//!
3//! This mirrors an AOT model: the full `candela` toolchain (compiler + VM)
4//! compiles a `.cdl` source to a compact, self-contained bytecode artifact
5//! ([`ProgramImage`] serialized via [`serialize_image`]), and the VM-only
6//! `candela-vm` binary loads it ([`load_program`]) and runs it
7//! ([`RuntimeProgram::run`]) WITHOUT the parser, compiler, or REPL.
8//!
9//! An embedding host gets the same two halves the `Engine`/`Program` pair
10//! gives it, minus the compiler: it supplies a [`HostRegistry`] at load so the
11//! artifact's `host` functions bind to Rust closures, and it invokes script
12//! functions by name through [`RuntimeProgram::call`], which runs a trampoline
13//! the compiler emitted at build time.
14//!
15//! The on-disk format is: a 4-byte magic (`CDLB`), a 1-byte format version, then
16//! a `postcard`-encoded [`ProgramImage`]. `postcard` was chosen over `bincode`
17//! because it is `no_std`/`alloc`-friendly and its varint encoding keeps the
18//! artifact small; it also pulls in the least code, which matters for the
19//! `candela-vm` binary-size budget.
20//!
21//! [`ProgramImage`] carries `pub` fields so the `candela` crate's `build`
22//! subcommand can populate it from a fresh compile; everything needed to LOAD
23//! and RUN it lives here.
24
25use crate::data::Data;
26use crate::embed::HostBindError;
27use crate::embed::HostDispatch;
28use crate::embed::HostRegistry;
29use crate::embed::Value;
30use crate::embed::describe_value;
31use crate::embed::marshal_args;
32use crate::embed::unmarshal_value;
33use crate::embed::value_matches_type;
34use crate::errors::Diagnostic;
35use crate::errors::ErrorCtx;
36use crate::instr::Instr;
37use crate::rt::DataType;
38use crate::rt::DynamicLibFn;
39use crate::rt::EnumType;
40use crate::rt::EnumVariant;
41use crate::rt::GcStats;
42use crate::rt::HostFnSig;
43use crate::rt::InstrSrc;
44use crate::rt::LibraryOrigin;
45use crate::rt::Pools;
46use crate::rt::Source;
47use crate::rt::Span;
48use crate::rt::Struct;
49#[cfg(not(target_arch = "wasm32"))]
50use crate::rt::TargetOs;
51#[cfg(not(target_arch = "wasm32"))]
52use crate::rt::resolve_library_filename;
53use crate::vm;
54use crate::vm::CandelaMap;
55use crate::vm::MapPool;
56use crate::vm::ObjectPool;
57use crate::vm::Pool;
58use crate::vm::RegisterFile;
59use crate::vm::StringPool;
60use serde::Deserialize;
61use serde::Serialize;
62use smol_strc::SmolStr;
63use std::collections::HashMap;
64
65/// The pair of words a value is recorded as. See [`Data::into_words`].
66pub type DataWords = (u64, i64);
67
68/// 4-byte artifact magic, so a non-`.cdlb` file fails cleanly.
69const MAGIC: [u8; 4] = *b"CDLB";
70/// Bumped whenever the serialized shape changes, so a mismatched artifact is
71/// rejected instead of mis-decoded. Version 2 added the dynamic-library and
72/// host-function recipe tables (`dyn_lib_fns`/`host_fns`). Version 3 added the
73/// native-enum type table (`enums`) and the `CloneEnum` instruction. Version 4
74/// added the map/json/any library functions (`Keys`/`Values`/`JsonParse`/
75/// `JsonStringify` and the `is_*`/`as_*` value ops). Version 5 added the export
76/// table (`exports`), the call trampolines that back
77/// [`RuntimeProgram::call`]. Version 6 added the `MapRemove` instruction and
78/// the origin of each dynamic-library recipe, which says whether its spec is
79/// the program's own or a path into the standard library's `libs` directory.
80/// Version 7 widened `int` to 64 bits, so every recorded value carries a
81/// second word. Version 8 added the cell instructions a captured variable is
82/// read and written through. Version 9 added the indirect call, which dispatches
83/// on the function a value carries. Version 10 added the instruction that
84/// builds a function value, which tells a function from the list it is built
85/// as wherever one becomes text. Version 11 added the string comparison and
86/// bitwise instructions, which sit among the existing ones and so renumber the
87/// instructions after them, and gave the order of each map's recorded pairs a
88/// meaning: a map is walked in the order its keys went in, so the pair list is
89/// the program's order and an artifact from before the change cannot supply it.
90/// Version 12 added the loop step and the add-a-constant instructions, which
91/// sit among the existing ones and so renumber the instructions after them.
92/// Version 13 added the move chain and the add-to-a-float-field instructions,
93/// which renumber the instructions after them the same way. Version 14 added
94/// the machine code sections ([`ProgramImage::native`]) and the function table
95/// that follows the image ([`FunctionTable`]). Version 15 added the type test
96/// `is` compiles to for a struct, an enum, a function, a union or a collection,
97/// and the nested type codes that check what a list or a map holds.
98const FORMAT_VERSION: u8 = 15;
99
100/// Serializable mirror of a compiled program's runtime state.
101///
102/// A dedicated DTO keeps `serde` off the hot runtime types: `Data` becomes its
103/// pair of raw words, `SmolStr` becomes `String`, maps become key/value pairs.
104/// Only [`Instr`], [`Span`], and [`DataType`] carry `serde` derives directly.
105/// Fields are `pub` so the compiler front-end can build an image from a fresh
106/// compile.
107#[derive(Serialize, Deserialize)]
108pub struct ProgramImage {
109    pub instructions: Vec<Instr>,
110    pub registers: Vec<DataWords>,
111    pub objs: Vec<Vec<DataWords>>,
112    pub maps: Vec<Vec<(DataWords, DataWords)>>,
113    pub strings: Vec<String>,
114    pub instr_src: Vec<InstrSrcImage>,
115    /// The registers each recursive call site saves across its call, indexed by
116    /// the call-site id its `SaveFrame` carries.
117    pub callsite_registers: Vec<Vec<u16>>,
118    pub structs: Vec<StructImage>,
119    pub enums: Vec<EnumImage>,
120    pub sources: Vec<SourceImage>,
121    pub allocated_arg_count: u64,
122    pub allocated_call_depth: u64,
123    /// Recipe for each dynamic-library binding, in `DynamicLibFn` id order. The
124    /// artifact stores only the logical library name, symbol, and marshalling
125    /// signature, never the shared object's bytes, so the VM re-opens the
126    /// library and re-binds the symbol by name at load time (see
127    /// [`resolve_library_filename`]).
128    pub dyn_lib_fns: Vec<DynLibFnImage>,
129    /// Recipe for each `host` function declared by the program, in host-fn id
130    /// order. The [`HostRegistry`] handed to [`load_program`] supplies the
131    /// closure behind each one; an unregistered name is a load error.
132    pub host_fns: Vec<HostFnImage>,
133    /// One entry per host-callable function, each with the call trampoline the
134    /// compiler emitted for it at build time. This is what lets the VM invoke a
135    /// script function by name with no compiler present.
136    pub exports: Vec<ExportImage>,
137    /// Machine code for some of the program's functions, one section per
138    /// machine it was built for. The VM runs the first section this machine
139    /// can run and the bytecode for everything else; a program whose sections
140    /// all name another machine runs on bytecode alone. The bytecode stays
141    /// complete either way.
142    pub native: Vec<NativeImage>,
143}
144
145/// Machine code `candela build` generated for one kind of machine, and what
146/// the VM needs to know to run it.
147///
148/// The code has no relocations left: calls between its functions were
149/// resolved when it was laid out, and it reaches the VM only through the
150/// context the VM passes in. Loading it is a copy into memory the VM then
151/// makes executable, and a rewrite of the calls it covers.
152#[derive(Serialize, Deserialize, Clone, Debug)]
153pub struct NativeImage {
154    /// The version of the contract between the code and the VM: the context
155    /// layout, the helper functions, and the layout of a value. A VM that
156    /// speaks another version runs bytecode.
157    pub abi: u32,
158    pub arch: NativeArch,
159    /// The operating system the calling convention is that of.
160    pub os: NativeOs,
161    /// Instruction set extensions the code uses beyond the architecture's
162    /// baseline, by the names Rust's feature detection uses. The VM checks
163    /// each one on the machine it runs on.
164    pub cpu_features: Vec<String>,
165    /// Where the object pool's per-object vector keeps its size, buffer
166    /// pointer and length, as the build measured it. The code reads objects
167    /// through that layout, so the VM checks it against its own.
168    pub object_layout: [u8; 3],
169    /// The most stack one of the functions takes per call, in bytes. The VM
170    /// sizes how deep native recursion may go from it before a call carries
171    /// on in the interpreter.
172    pub frame_bytes: u32,
173    /// The functions the section holds.
174    pub functions: Vec<NativeFunctionImage>,
175    /// The function that stands in for `main`, when `main` itself was
176    /// compiled.
177    pub main: Option<u32>,
178    /// The call instructions the VM rewrites to call into the section.
179    pub sites: Vec<NativeSiteImage>,
180    pub code: Vec<u8>,
181}
182
183/// An architecture machine code is built for.
184#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
185pub enum NativeArch {
186    X86_64,
187    Aarch64,
188}
189
190/// An operating system whose calling convention machine code follows.
191#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
192pub enum NativeOs {
193    Linux,
194    MacOs,
195    Windows,
196}
197
198/// One function in a [`NativeImage`].
199#[derive(Serialize, Deserialize, Clone, Debug)]
200pub struct NativeFunctionImage {
201    /// The instruction its bytecode starts at.
202    pub loc: u32,
203    /// Where its entry point starts in the section's code.
204    pub offset: u32,
205    /// Whether it returns a value.
206    pub returns: bool,
207}
208
209/// A call instruction the VM points at a native function once it loads.
210#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
211pub struct NativeSiteImage {
212    /// The instruction, a `CallFunc` or a `CallFuncRecursive`.
213    pub at: u32,
214    /// Its callee, by index into [`NativeImage::functions`].
215    pub function: u32,
216}
217
218/// What the compiler knows about each function of a program, written after
219/// the [`ProgramImage`] in a `.cdlb`.
220///
221/// It is what code generation reads instead of working types out of the
222/// bytecode: where each function starts, which registers its parameters
223/// arrive in, and the type of every value each instruction writes. The VM
224/// never reads it.
225#[derive(Serialize, Deserialize, Clone, Debug, Default)]
226pub struct FunctionTable {
227    pub functions: Vec<FunctionImage>,
228}
229
230/// One compiled function in a [`FunctionTable`]: `main`, or one
231/// specialisation of a declared function or closure.
232#[derive(Serialize, Deserialize, Clone, Debug)]
233pub struct FunctionImage {
234    /// The name the function was declared with; `main` for the program's
235    /// entry.
236    pub name: String,
237    /// The instruction the body starts at.
238    pub entry: u32,
239    /// The register each parameter arrives in, in declaration order.
240    pub params: Vec<u16>,
241    /// The type of each parameter.
242    pub param_types: Vec<DataType>,
243    /// What the function returns: `Null` for nothing.
244    pub returns: DataType,
245    /// The register a closure reads its captured environment from.
246    pub env: Option<u16>,
247    /// Whether a call through a function value can reach it.
248    pub indirect: bool,
249    /// The type of the value each instruction of the body writes into each
250    /// register it writes, in instruction order.
251    pub defs: Vec<DefImage>,
252}
253
254/// The type of one value an instruction writes.
255#[derive(Serialize, Deserialize, Clone, Debug)]
256pub struct DefImage {
257    /// The instruction.
258    pub at: u32,
259    /// The register it writes.
260    pub reg: u16,
261    /// The type of what it writes there. `Unknown` where the compiler cannot
262    /// tell, such as an element of a list built from a literal.
263    pub ty: DataType,
264}
265
266/// Serializable recipe for one dynamic-library binding: enough to re-open the
267/// library and re-resolve the symbol by name at load, with no embedded bytes.
268#[derive(Serialize, Deserialize)]
269pub struct DynLibFnImage {
270    /// Logical library name (`z`, `sqlite3`) or path. A program's own binding
271    /// records it exactly as the source `dylib "..."` block wrote it; a
272    /// standard-library one records it relative to the `libs` directory.
273    pub library: String,
274    /// The C symbol resolved within `library`.
275    pub symbol: String,
276    /// `[ return_type, arg_types... ]`, driving the rebuilt libffi CIF.
277    pub types: Vec<DataType>,
278    /// Which search `library` is resolved through at load.
279    pub origin: LibraryOrigin,
280}
281
282/// Serializable recipe for one `host` function: its fully-qualified name and
283/// marshalling signature, so an embedding runtime can re-bind it by name.
284#[derive(Serialize, Deserialize)]
285pub struct HostFnImage {
286    pub namespace: String,
287    pub name: String,
288    /// `[ return_type, arg_types... ]`.
289    pub types: Vec<DataType>,
290    pub variadic: bool,
291}
292
293impl From<&HostFnImage> for HostFnSig {
294    fn from(image: &HostFnImage) -> Self {
295        Self {
296            types: image.types.clone().into_boxed_slice(),
297            namespace: SmolStr::from(image.namespace.as_str()),
298            name: SmolStr::from(image.name.as_str()),
299            variadic: image.variadic,
300        }
301    }
302}
303
304/// One host-callable function, with the call trampoline `candela build`
305/// compiled for it.
306///
307/// The compiler specialises the function for its declared parameter types and
308/// lays down a short instruction run that moves the parameter registers into
309/// place, calls the specialisation, and halts. Invoking it is then a matter of
310/// writing marshalled arguments into `arg_registers` and executing from
311/// `entry`; the result is left in `ret_register`.
312#[derive(Serialize, Deserialize)]
313pub struct ExportImage {
314    /// The name the function is called by, as written in the source.
315    pub name: String,
316    /// Instruction index the trampoline starts at.
317    pub entry: u64,
318    /// The register holding each parameter, in declaration order.
319    pub arg_registers: Vec<u16>,
320    /// The declared type of each parameter, checked against the host's
321    /// arguments before the call runs.
322    pub arg_types: Vec<DataType>,
323    /// The register the trampoline leaves the result in. Register 0 is the null
324    /// sink, which is what a function returning nothing lands on.
325    pub ret_register: u16,
326}
327
328#[derive(Serialize, Deserialize)]
329pub struct InstrSrcImage {
330    pub instr: Instr,
331    pub span: Span,
332    pub file_id: u16,
333}
334
335#[derive(Serialize, Deserialize)]
336pub struct StructImage {
337    pub name: String,
338    pub fields: Vec<(String, DataType, Span)>,
339    pub id: u16,
340    pub name_span: Span,
341}
342
343#[derive(Serialize, Deserialize)]
344pub struct EnumImage {
345    pub name: String,
346    pub variants: Vec<EnumVariantImage>,
347    pub id: u16,
348    pub name_span: Span,
349}
350
351#[derive(Serialize, Deserialize)]
352pub struct EnumVariantImage {
353    pub name: String,
354    pub payload: Vec<DataType>,
355    pub name_span: Span,
356}
357
358#[derive(Serialize, Deserialize)]
359pub struct SourceImage {
360    pub filename: String,
361    pub contents: String,
362}
363
364/// One host-callable function in a loaded program: where its trampoline starts,
365/// which registers its parameters live in, and what it leaves the result in.
366struct Export {
367    entry: usize,
368    arg_registers: Box<[u16]>,
369    arg_types: Box<[DataType]>,
370    ret_register: u16,
371}
372
373/// A loaded, ready-to-run program image with owned runtime state.
374///
375/// Produced by [`load_program`] from `.cdlb` bytes. [`Self::run`] executes it
376/// exactly as the full `candela` binary runs a freshly compiled program, and
377/// [`Self::call`] invokes an exported function by name. Registers and heap
378/// pools stay resident between calls, so state one call establishes is visible
379/// to the next.
380pub struct RuntimeProgram {
381    instructions: Vec<Instr>,
382    registers: Vec<Data>,
383    pools: Pools,
384    instr_src: Vec<InstrSrc>,
385    callsite_registers: Vec<Vec<u16>>,
386    dyn_lib_fns: Vec<DynamicLibFn>,
387    host_sigs: Vec<HostFnSig>,
388    host_dispatch: Vec<HostDispatch>,
389    exports: HashMap<String, Export>,
390    structs: Vec<Struct>,
391    enums: Vec<EnumType>,
392    sources: Vec<Source>,
393    allocated_arg_count: usize,
394    allocated_call_depth: usize,
395    /// The program's machine code, when its artifact carries some this
396    /// machine runs.
397    native: Option<crate::native::NativeCode>,
398}
399
400impl RuntimeProgram {
401    /// Loads the first of `sections` this machine can run, and points the
402    /// calls it covers at it. Without one the program runs on bytecode.
403    fn load_native(&mut self, sections: &[NativeImage]) {
404        if sections.is_empty() {
405            return;
406        }
407        #[cfg(not(target_arch = "wasm32"))]
408        let dylibs = self
409            .dyn_lib_fns
410            .iter()
411            .map(|f| f.ptr.as_ptr().cast::<u8>())
412            .collect();
413        #[cfg(target_arch = "wasm32")]
414        let dylibs = Vec::new();
415        self.native = crate::native::load(sections, &mut self.instructions, dylibs);
416    }
417
418    /// How many of the program's functions run as machine code on this
419    /// machine: zero when its artifact carries none this machine runs.
420    #[must_use]
421    pub fn native_functions(&self) -> usize {
422        self.native.as_ref().map_or(0, |n| n.functions.len())
423    }
424
425    /// Runs the program's `main` to completion.
426    ///
427    /// A runtime error prints its report and ends the process, matching the
428    /// full `candela <file.cdl>` path exactly. A
429    /// host that wants the error as a value instead runs this inside
430    /// [`collect_diagnostic`](crate::collect_diagnostic).
431    pub fn run(&mut self) {
432        let err_ctx = ErrorCtx {
433            instr_src: &self.instr_src,
434            sources: &self.sources,
435        };
436        let mut register_file = RegisterFile(std::mem::take(&mut self.registers));
437        let result = vm::execute(
438            &self.instructions,
439            &mut register_file,
440            &mut self.pools,
441            &err_ctx,
442            &self.callsite_registers,
443            &self.dyn_lib_fns,
444            &self.structs,
445            &self.enums,
446            self.allocated_arg_count,
447            self.allocated_call_depth,
448            &self.host_sigs,
449            &self.host_dispatch,
450            0,
451            self.native.as_ref(),
452        );
453        self.registers = std::mem::take(&mut register_file.0);
454        if let Err(error) = result {
455            error.report(&err_ctx);
456        }
457    }
458
459    /// Invokes the exported function `name` with `args`, returning its value
460    /// (or [`Value::Null`] for a function that returns nothing).
461    ///
462    /// The call runs the trampoline the compiler emitted for the function at
463    /// build time, against the resident register and heap state, so globals a
464    /// previous call or `main` established remain visible. Arguments are
465    /// checked against the declared parameter types before anything runs.
466    ///
467    /// # Errors
468    ///
469    /// Returns a [`CallError`] if the artifact exports no such function, if the
470    /// arguments disagree with the declared signature, or if the call raises a
471    /// runtime error.
472    pub fn call(&mut self, name: &str, args: &[Value]) -> Result<Value, CallError> {
473        let export = self
474            .exports
475            .get(name)
476            .ok_or_else(|| CallError::UnknownFunction(name.to_owned()))?;
477
478        if export.arg_registers.len() != args.len() {
479            return Err(CallError::ArgCount {
480                function: name.to_owned(),
481                expected: export.arg_registers.len(),
482                found: args.len(),
483            });
484        }
485        for (idx, (arg, declared)) in args.iter().zip(export.arg_types.iter()).enumerate() {
486            if !value_matches_type(arg, declared) {
487                return Err(CallError::ArgType {
488                    function: name.to_owned(),
489                    index: idx + 1,
490                    expected: declared.to_string(),
491                    found: describe_value(arg),
492                });
493            }
494        }
495
496        let entry = export.entry;
497        let ret_register = export.ret_register as usize;
498        marshal_args(
499            args,
500            &export.arg_registers,
501            &mut self.registers,
502            &mut self.pools,
503        );
504
505        self.execute_from(entry)?;
506
507        Ok(unmarshal_value(
508            self.registers[ret_register],
509            &self.pools.objs,
510            &self.pools.maps,
511            &self.pools.strings,
512            &self.structs,
513            &self.enums,
514        ))
515    }
516
517    /// Does up to about `budget` units of garbage collection while the
518    /// program is idle, and answers whether no collection work is left.
519    ///
520    /// Call it between calls into the program, where the host would otherwise
521    /// wait, such as between frames. It carries on a collection already under
522    /// way, or begins one when the heap has grown enough since the last; on a
523    /// heap that has not, it does nothing and answers `true`. A unit is about
524    /// one object traced or one freed slot, never a length of time, so the
525    /// same calls do the same work on every machine. `collect(u32::MAX)` runs
526    /// a whole collection. Beginning a collection reads every root at once
527    /// whatever the budget.
528    ///
529    /// Once a host has called this, allocations leave more of the work to it
530    /// and begin a collection themselves only when the heap has grown further.
531    pub fn collect(&mut self, budget: u32) -> bool {
532        self.pools.collect(&self.registers, budget)
533    }
534
535    /// What the collector has done over this program's heap, and how large
536    /// the heap is. See [`GcStats`].
537    #[must_use]
538    pub fn gc_stats(&self) -> GcStats {
539        self.pools.gc_stats()
540    }
541
542    /// The names of every function this artifact exports, in no particular
543    /// order.
544    pub fn exports(&self) -> impl Iterator<Item = &str> {
545        self.exports.keys().map(String::as_str)
546    }
547
548    /// Runs the VM against the resident state starting at instruction `start`,
549    /// capturing any runtime error as a [`Diagnostic`].
550    fn execute_from(&mut self, start: usize) -> Result<(), Diagnostic> {
551        let err_ctx = ErrorCtx {
552            instr_src: &self.instr_src,
553            sources: &self.sources,
554        };
555
556        // Move the register file out so the VM can borrow it mutably, then
557        // reclaim it (register state must persist across calls).
558        let mut register_file = RegisterFile(std::mem::take(&mut self.registers));
559
560        let instructions = &self.instructions;
561        let pools = &mut self.pools;
562        let callsite_registers = &self.callsite_registers;
563        let dyn_lib_fns = &self.dyn_lib_fns;
564        let structs = &self.structs;
565        let enums = &self.enums;
566        let host_sigs = &self.host_sigs;
567        let host_dispatch = &self.host_dispatch;
568        let allocated_arg_count = self.allocated_arg_count;
569        let allocated_call_depth = self.allocated_call_depth;
570
571        // An error comes back as a value, so a call sets up no unwind and
572        // leaves the process panic hook alone.
573        let result = vm::execute(
574            instructions,
575            &mut register_file,
576            pools,
577            &err_ctx,
578            callsite_registers,
579            dyn_lib_fns,
580            structs,
581            enums,
582            allocated_arg_count,
583            allocated_call_depth,
584            host_sigs,
585            host_dispatch,
586            start,
587            self.native.as_ref(),
588        );
589
590        self.registers = std::mem::take(&mut register_file.0);
591        result.map_err(|error| error.diagnostic(&err_ctx))
592    }
593}
594
595/// Why a [`RuntimeProgram::call`] did not produce a value.
596#[derive(Debug)]
597pub enum CallError {
598    /// The artifact exports no function under this name.
599    UnknownFunction(String),
600    /// The call passed a different number of arguments than the function
601    /// declares.
602    ArgCount {
603        function: String,
604        expected: usize,
605        found: usize,
606    },
607    /// An argument does not match the parameter type it was passed for.
608    /// `index` is 1-based.
609    ArgType {
610        function: String,
611        index: usize,
612        expected: String,
613        found: String,
614    },
615    /// The call ran and raised a runtime error.
616    Runtime(Diagnostic),
617}
618
619impl From<Diagnostic> for CallError {
620    fn from(diagnostic: Diagnostic) -> Self {
621        Self::Runtime(diagnostic)
622    }
623}
624
625impl std::fmt::Display for CallError {
626    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
627        match self {
628            Self::UnknownFunction(name) => write!(
629                f,
630                "this artifact exports no function named '{name}'; only functions defined in the file it was built from, with every parameter annotated, are callable by name"
631            ),
632            Self::ArgCount {
633                function,
634                expected,
635                found,
636            } => write!(
637                f,
638                "'{function}' takes {expected} argument(s) but the call passed {found}"
639            ),
640            Self::ArgType {
641                function,
642                index,
643                expected,
644                found,
645            } => write!(
646                f,
647                "argument {index} of '{function}' is declared '{expected}' but the call passed a {found}"
648            ),
649            Self::Runtime(diagnostic) => f.write_str(&diagnostic.message),
650        }
651    }
652}
653
654impl std::error::Error for CallError {}
655
656impl RuntimeProgram {
657    /// Reconstructs a runnable program from a decoded [`ProgramImage`].
658    ///
659    /// This is where a `.cdlb`'s dynamic-library and host recipes are turned
660    /// back into live bindings: each `dylib` symbol is re-resolved through the
661    /// OS loader by logical name (never from embedded bytes), and each `host`
662    /// function is bound to the closure `hosts` registered under the same name,
663    /// with its signature checked against the recorded declaration.
664    ///
665    /// # Errors
666    ///
667    /// Returns [`LoadError`] if a referenced library cannot be opened, a symbol
668    /// cannot be resolved, or a declared `host` function is unregistered or
669    /// bound to a closure of a different shape.
670    fn from_image(img: ProgramImage, hosts: &HostRegistry) -> Result<Self, LoadError> {
671        let objs: ObjectPool = Pool(
672            img.objs
673                .into_iter()
674                .map(|v| {
675                    v.into_iter()
676                        .map(|(boxed, int)| Data::from_words(boxed, int))
677                        .collect()
678                })
679                .collect(),
680        );
681        let maps: MapPool = Pool(
682            img.maps
683                .into_iter()
684                .map(|pairs| {
685                    let mut m = CandelaMap::default();
686                    for ((k_boxed, k_int), (v_boxed, v_int)) in pairs {
687                        m.insert(
688                            Data::from_words(k_boxed, k_int),
689                            Data::from_words(v_boxed, v_int),
690                        );
691                    }
692                    m
693                })
694                .collect(),
695        );
696        let strings = StringPool::from_strings(img.strings);
697
698        let structs: Vec<Struct> = img
699            .structs
700            .into_iter()
701            .map(|s| Struct {
702                name: SmolStr::from(s.name),
703                fields: s
704                    .fields
705                    .into_iter()
706                    .map(|(n, t, sp)| (SmolStr::from(n), t, sp))
707                    .collect::<Vec<_>>()
708                    .into_boxed_slice(),
709                id: s.id,
710                name_span: s.name_span,
711            })
712            .collect();
713
714        let enums: Vec<EnumType> = img
715            .enums
716            .into_iter()
717            .map(|e| EnumType {
718                name: SmolStr::from(e.name),
719                variants: e
720                    .variants
721                    .into_iter()
722                    .map(|v| EnumVariant {
723                        name: SmolStr::from(v.name),
724                        payload: v.payload.into_boxed_slice(),
725                        name_span: v.name_span,
726                    })
727                    .collect::<Vec<_>>()
728                    .into_boxed_slice(),
729                id: e.id,
730                name_span: e.name_span,
731            })
732            .collect();
733
734        // `host` functions need a host-supplied closure to dispatch to. Bind
735        // them all before anything runs, so an unbound call is impossible once
736        // the program is loaded.
737        let host_sigs: Vec<HostFnSig> = img.host_fns.iter().map(HostFnSig::from).collect();
738        let host_dispatch = hosts.bind(&host_sigs, &structs)?;
739
740        let dyn_lib_fns = resolve_dyn_lib_fns(&img.dyn_lib_fns, &structs)?;
741
742        let exports = img
743            .exports
744            .into_iter()
745            .map(|e| {
746                (
747                    e.name,
748                    Export {
749                        entry: e.entry as usize,
750                        arg_registers: e.arg_registers.into_boxed_slice(),
751                        arg_types: e.arg_types.into_boxed_slice(),
752                        ret_register: e.ret_register,
753                    },
754                )
755            })
756            .collect();
757
758        Ok(Self {
759            instructions: img.instructions,
760            registers: img
761                .registers
762                .into_iter()
763                .map(|(boxed, int)| Data::from_words(boxed, int))
764                .collect(),
765            pools: Pools::new(objs, maps, strings),
766            instr_src: img
767                .instr_src
768                .into_iter()
769                .map(|s| InstrSrc {
770                    instr: s.instr,
771                    span: s.span,
772                    file_id: s.file_id,
773                })
774                .collect(),
775            callsite_registers: img.callsite_registers,
776            dyn_lib_fns,
777            host_sigs,
778            host_dispatch,
779            exports,
780            structs,
781            enums,
782            sources: img
783                .sources
784                .into_iter()
785                .map(|s| Source {
786                    filename: SmolStr::from(s.filename),
787                    contents: s.contents,
788                })
789                .collect(),
790            allocated_arg_count: img.allocated_arg_count as usize,
791            allocated_call_depth: img.allocated_call_depth as usize,
792            native: None,
793        })
794    }
795}
796
797/// Re-opens each referenced dynamic library through the OS loader and rebuilds a
798/// live [`DynamicLibFn`] (libffi CIF + code pointer) for every recorded symbol.
799///
800/// Libraries are opened once and shared across their symbols. The logical name
801/// is mapped to this platform's filename convention via
802/// [`resolve_library_filename`], so an artifact built on one OS resolves the
803/// right file on another. The directories a host named with
804/// [`set_dylib_dirs`](crate::rt::set_dylib_dirs) are searched before the
805/// loader's own paths, which is how an application ships its libraries in a
806/// directory of its own. A recipe the standard library owns is searched for
807/// under the toolchain's `libs` directory as well, so an artifact that imports
808/// `std/math` runs from any working directory.
809#[cfg(not(target_arch = "wasm32"))]
810fn resolve_dyn_lib_fns(
811    recipes: &[DynLibFnImage],
812    structs: &[Struct],
813) -> Result<Vec<DynamicLibFn>, LoadError> {
814    use crate::rt::open_library;
815    use libloading::Library;
816    use std::rc::Rc;
817
818    // Keyed by origin as well as filename: the same relative path means one
819    // file under the toolchain's `libs` and another beside the program.
820    let mut libs: HashMap<(LibraryOrigin, String), Rc<Library>> = HashMap::default();
821    let mut out: Vec<DynamicLibFn> = Vec::with_capacity(recipes.len());
822
823    for recipe in recipes {
824        let filename = resolve_library_filename(&recipe.library, TargetOs::CURRENT);
825        let key = (recipe.origin, filename.clone());
826        let lib = if let Some(lib) = libs.get(&key) {
827            Rc::clone(lib)
828        } else {
829            let lib = Rc::new(open_library(&filename, recipe.origin).map_err(|e| {
830                LoadError::LibraryOpen {
831                    spec: recipe.library.clone(),
832                    filename: filename.clone(),
833                    message: e.to_string(),
834                }
835            })?);
836            libs.insert(key, Rc::clone(&lib));
837            lib
838        };
839
840        let ptr = unsafe {
841            let sym = lib
842                .get::<*const ()>(recipe.symbol.as_bytes())
843                .map_err(|_| LoadError::SymbolNotFound {
844                    library: recipe.library.clone(),
845                    symbol: recipe.symbol.clone(),
846                })?;
847            libffi::middle::CodePtr(sym.try_as_raw_ptr().ok_or_else(|| {
848                LoadError::SymbolNotFound {
849                    library: recipe.library.clone(),
850                    symbol: recipe.symbol.clone(),
851                }
852            })?)
853        };
854
855        // types[0] is the return type; types[1..] are the argument types.
856        let (return_type, arg_types) = recipe.types.split_first().unwrap_or((&DataType::Null, &[]));
857        let cif = libffi::middle::Cif::new(
858            arg_types.iter().map(|t| t.to_c_type(structs)),
859            return_type.to_c_type(structs),
860        );
861
862        out.push(DynamicLibFn {
863            types: recipe.types.clone().into_boxed_slice(),
864            library: SmolStr::from(recipe.library.as_str()),
865            origin: recipe.origin,
866            symbol: SmolStr::from(recipe.symbol.as_str()),
867            _lib: lib,
868            ptr,
869            cif,
870        });
871    }
872
873    Ok(out)
874}
875
876/// On targets without the FFI backend (wasm) no library loads. A binding the
877/// standard library owns resolves to the runtime's own function for it; any
878/// other refuses the load rather than silently dropping the binding.
879#[cfg(target_arch = "wasm32")]
880fn resolve_dyn_lib_fns(
881    recipes: &[DynLibFnImage],
882    _structs: &[Struct],
883) -> Result<Vec<DynamicLibFn>, LoadError> {
884    recipes
885        .iter()
886        .map(|recipe| {
887            let intrinsic = match recipe.origin {
888                LibraryOrigin::StandardLibrary => {
889                    crate::intrinsics::lookup(&recipe.library, &recipe.symbol)
890                }
891                LibraryOrigin::Program => None,
892            };
893            let Some(intrinsic) = intrinsic else {
894                return Err(LoadError::LibraryOpen {
895                    spec: recipe.library.clone(),
896                    filename: recipe.library.clone(),
897                    message: String::from("dynamic libraries are not supported on this target"),
898                });
899            };
900            Ok(DynamicLibFn {
901                types: recipe.types.clone().into_boxed_slice(),
902                library: SmolStr::from(recipe.library.as_str()),
903                origin: recipe.origin,
904                symbol: SmolStr::from(recipe.symbol.as_str()),
905                intrinsic,
906            })
907        })
908        .collect()
909}
910
911/// Why a `.cdlb` artifact could not be loaded.
912#[derive(Debug)]
913pub enum LoadError {
914    /// The magic header is missing (not a `.cdlb` file).
915    BadMagic,
916    /// The file is shorter than the fixed header.
917    Truncated,
918    /// The artifact's format version is not understood by this runtime.
919    UnsupportedVersion(u8),
920    /// The `postcard` body failed to decode.
921    Decode(postcard::Error),
922    /// A referenced dynamic library could not be opened by the OS loader.
923    LibraryOpen {
924        /// Logical name/path as written in the source.
925        spec: String,
926        /// The platform filename it mapped to at load.
927        filename: String,
928        /// The loader's error text.
929        message: String,
930    },
931    /// A symbol declared by a `dylib` block was not found in its library.
932    SymbolNotFound { library: String, symbol: String },
933    /// The artifact's `host` functions could not be bound to the registry the
934    /// load was given.
935    HostBinding(HostBindError),
936}
937
938impl From<HostBindError> for LoadError {
939    fn from(error: HostBindError) -> Self {
940        Self::HostBinding(error)
941    }
942}
943
944impl std::fmt::Display for LoadError {
945    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
946        match self {
947            Self::BadMagic => write!(f, "not a candela bytecode artifact (bad magic header)"),
948            Self::Truncated => write!(f, "truncated candela bytecode artifact"),
949            Self::UnsupportedVersion(v) => write!(
950                f,
951                "unsupported .cdlb format version {v} (this runtime understands version {FORMAT_VERSION})"
952            ),
953            Self::Decode(e) => write!(f, "failed to decode .cdlb body: {e}"),
954            Self::LibraryOpen {
955                spec,
956                filename,
957                message,
958            } => write!(
959                f,
960                "cannot open dynamic library '{spec}' (resolved to '{filename}') referenced by this artifact: {message}"
961            ),
962            Self::SymbolNotFound { library, symbol } => write!(
963                f,
964                "dynamic library '{library}' does not export the symbol '{symbol}' this artifact needs"
965            ),
966            Self::HostBinding(error) => write!(f, "{error}"),
967        }
968    }
969}
970
971impl std::error::Error for LoadError {}
972
973/// Loads a `.cdlb` bytecode artifact into a runnable [`RuntimeProgram`].
974///
975/// `hosts` supplies the Rust closures the artifact's `host` blocks bind to.
976/// Pass an empty [`HostRegistry`] for a program that declares none; a program
977/// that declares one it does not cover fails to load, naming what is missing.
978///
979/// # Errors
980///
981/// Returns a [`LoadError`] if the magic/version header is wrong, the body fails
982/// to decode, a dynamic library cannot be re-bound, or a `host` function cannot
983/// be bound to the registry.
984pub fn load_program(bytes: &[u8], hosts: &HostRegistry) -> Result<RuntimeProgram, LoadError> {
985    if bytes.len() < 5 {
986        return Err(LoadError::Truncated);
987    }
988    if bytes[0..4] != MAGIC {
989        return Err(LoadError::BadMagic);
990    }
991    let version = bytes[4];
992    if version != FORMAT_VERSION {
993        return Err(LoadError::UnsupportedVersion(version));
994    }
995    // The function table follows the image, and the VM has no use for it.
996    let (mut img, _table): (ProgramImage, &[u8]) =
997        postcard::take_from_bytes(&bytes[5..]).map_err(LoadError::Decode)?;
998    let sections = std::mem::take(&mut img.native);
999    #[allow(unused_mut)]
1000    let mut program = RuntimeProgram::from_image(img, hosts)?;
1001    program.load_native(&sections);
1002    Ok(program)
1003}
1004
1005/// Serializes a [`ProgramImage`] to `.cdlb` bytes (magic + version + body),
1006/// followed by the function table when there is one.
1007///
1008/// # Errors
1009///
1010/// Returns the serialization error as a string if the `postcard` body cannot be
1011/// encoded.
1012pub fn serialize_image(
1013    image: &ProgramImage,
1014    functions: Option<&FunctionTable>,
1015) -> Result<Vec<u8>, String> {
1016    let mut bytes = Vec::new();
1017    bytes.extend_from_slice(&MAGIC);
1018    bytes.push(FORMAT_VERSION);
1019    let body = postcard::to_allocvec(image).map_err(|e| e.to_string())?;
1020    bytes.extend_from_slice(&body);
1021    if let Some(functions) = functions {
1022        let table = postcard::to_allocvec(functions).map_err(|e| e.to_string())?;
1023        bytes.extend_from_slice(&table);
1024    }
1025    Ok(bytes)
1026}
1027
1028/// Reads a `.cdlb` back into its image and the function table that follows
1029/// it, for a tool that works on the program rather than runs it. The table
1030/// is `None` for an artifact written without one.
1031///
1032/// # Errors
1033///
1034/// Returns a [`LoadError`] if the header is wrong or either part fails to
1035/// decode.
1036pub fn read_artifact(bytes: &[u8]) -> Result<(ProgramImage, Option<FunctionTable>), LoadError> {
1037    if bytes.len() < 5 {
1038        return Err(LoadError::Truncated);
1039    }
1040    if bytes[0..4] != MAGIC {
1041        return Err(LoadError::BadMagic);
1042    }
1043    if bytes[4] != FORMAT_VERSION {
1044        return Err(LoadError::UnsupportedVersion(bytes[4]));
1045    }
1046    let (image, rest): (ProgramImage, &[u8]) =
1047        postcard::take_from_bytes(&bytes[5..]).map_err(LoadError::Decode)?;
1048    if rest.is_empty() {
1049        return Ok((image, None));
1050    }
1051    let table: FunctionTable = postcard::from_bytes(rest).map_err(LoadError::Decode)?;
1052    Ok((image, Some(table)))
1053}