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(§ions);
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}