Skip to main content

candela/
lib.rs

1// This file is derived from keel (https://github.com/horacehoff/keel),
2// Copyright 2026 Horace Hoff, licensed under the Apache License, Version 2.0.
3// It has been modified by the candela authors. See the NOTICE file.
4//! `candela` is the full toolchain: lexer, parser, type-checker, compiler,
5//! REPL, the `Engine`/`Program` embedding API, and the `candela build`
6//! subcommand.
7//!
8//! The runtime core (the VM executor, bytecode/data types, GC, value
9//! marshalling, and the `.cdlb` load/run API) lives in the self-contained
10//! `candela-vm` crate. This crate depends on it (strictly `candela ->
11//! candela-vm`) and aliases its modules under `crate::` so the compiler keeps
12//! its `crate::data` / `crate::instr` / `crate::rt` / `crate::errors` /
13//! `crate::vm` paths.
14
15#[cfg(all(feature = "compiler", any(target_arch = "wasm32", feature = "embed")))]
16use crate::compiler::compile;
17// The colour constants and the cold-path hint are named through `crate::` by
18// the parser, the compiler, and the macro scanner, which are descendants of
19// this module.
20#[cfg(feature = "compiler")]
21use crate::errors::BOLD;
22#[cfg(feature = "compiler")]
23use crate::errors::ErrorCtx;
24#[cfg(feature = "compiler")]
25use crate::errors::RED;
26#[cfg(feature = "compiler")]
27use crate::errors::RESET;
28use crate::vm::RegisterFile;
29#[cfg(all(feature = "embed", feature = "compiler"))]
30use std::ffi::{CStr, CString, c_char};
31#[cfg(feature = "compiler")]
32use std::hint::cold_path;
33#[cfg(all(feature = "embed", feature = "compiler"))]
34use std::panic::catch_unwind;
35#[cfg(target_arch = "wasm32")]
36use wasm_bindgen::prelude::*;
37
38// The runtime core is the `candela-vm` crate. Alias its modules under `crate::`
39// so the compiler/parser/REPL keep their existing paths, and re-export its
40// public runtime API from this crate's surface.
41pub(crate) use candela_vm::data;
42pub(crate) use candela_vm::errors;
43pub(crate) use candela_vm::instr;
44pub(crate) use candela_vm::rt;
45pub(crate) use candela_vm::vm;
46
47// `pub` so an out-of-tree frontend (candela-lsp) can reuse the lexer, parser,
48// and type-checker directly instead of reimplementing them. Gated behind the
49// `compiler` feature.
50#[cfg(feature = "compiler")]
51#[path = "./compiler/compiler.rs"]
52pub mod compiler;
53// The embedding API (`Engine`/`Program`), built on top of the compiler and the
54// `candela-vm` marshalling types.
55#[cfg(feature = "compiler")]
56mod engine;
57// The `candela build` path: compile a `.cdl` source into a `.cdlb` artifact.
58#[cfg(feature = "compiler")]
59mod build;
60// Machine code for a built program.
61#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
62pub mod native;
63// The command line: the verbs, and the project they read.
64#[cfg(all(feature = "compiler", not(target_arch = "wasm32")))]
65mod cli;
66// Talking to `lpm`, the registry client that resolves dependencies.
67#[cfg(all(feature = "compiler", not(target_arch = "wasm32")))]
68mod lpm;
69// `candela.toml`, the project manifest.
70#[cfg(all(feature = "compiler", not(target_arch = "wasm32")))]
71mod manifest;
72// The flags `@cfg(...)` attributes test, which the embedder supplies.
73#[cfg(feature = "compiler")]
74pub mod cfg;
75// Macro registration and the region scanner. `pub` because both sides of a
76// macro are the embedder's: it registers the expanders, and it can scan a file
77// for regions without compiling it.
78#[cfg(feature = "compiler")]
79pub mod macros;
80// `pub` for the same reason as `compiler`: exported so tooling can lex/parse
81// standalone without going through a full `compiler::compile`.
82#[cfg(feature = "compiler")]
83#[path = "./parser/parser.rs"]
84pub mod parser;
85#[cfg(feature = "compiler")]
86mod repl;
87#[path = "./tests.rs"]
88#[cfg(all(test, feature = "compiler"))]
89mod tests;
90// Call trampolines, shared by the `Engine`/`Program` embedding API and the
91// export table `candela build` records in an artifact.
92#[cfg(feature = "compiler")]
93mod trampoline;
94// Tells a person at a terminal when a newer release is out. It is only reached
95// from the REPL and `--help`, never while a program is running.
96#[cfg(feature = "compiler")]
97mod update;
98#[path = "./util/util.rs"]
99mod util;
100// Compile warnings, which leave the build running, and the collector an
101// embedder or an editor reads them through.
102#[cfg(feature = "compiler")]
103mod warnings;
104
105pub use candela_vm::Diagnostic;
106pub use candela_vm::collect_diagnostic;
107
108pub use candela_vm::FromHostValue;
109pub use candela_vm::HostError;
110pub use candela_vm::HostType;
111pub use candela_vm::IntoHostFn;
112pub use candela_vm::IntoHostResult;
113pub use candela_vm::IntoHostValue;
114pub use candela_vm::Value;
115#[cfg(feature = "compiler")]
116pub use cfg::Cfg;
117#[cfg(feature = "compiler")]
118pub use engine::Engine;
119#[cfg(feature = "compiler")]
120pub use engine::Program;
121// Where an import reads from: the standard library, and the root of every
122// package a program depends on.
123#[cfg(feature = "compiler")]
124pub use compiler::imports::ImportResolver;
125
126// The VM-only surface: load a pre-compiled `.cdlb`, run it, and call into it.
127pub use candela_vm::CallError;
128pub use candela_vm::HostBindError;
129pub use candela_vm::HostRegistry;
130pub use candela_vm::LoadError;
131pub use candela_vm::RuntimeProgram;
132pub use candela_vm::load_program;
133// What the collector has done, read through `gc_stats` on either program type.
134pub use candela_vm::GcStats;
135pub use candela_vm::PoolStats;
136// Where a `dylib` import looks for its library file. Read by a compile and by
137// an artifact load alike, so a host that keeps its libraries in directories of
138// its own sets them once.
139pub use candela_vm::dylib_dirs;
140pub use candela_vm::set_dylib_dirs;
141// Compile a `.cdl` source string straight to `.cdlb` bytes (the `candela build`
142// path). Needs the compiler.
143#[cfg(feature = "compiler")]
144pub use build::build_bytecode;
145// The same with the profile and the machine code chosen.
146#[cfg(feature = "compiler")]
147pub use build::BuildOptions;
148#[cfg(feature = "compiler")]
149pub use build::NativeCode;
150#[cfg(feature = "compiler")]
151pub use build::build_artifact;
152// The compile `candela check` and `candela build` both run: the program, and
153// then an entry point for every fully annotated function in it. An out-of-tree
154// frontend that wants to report what the command line reports compiles through
155// here, not through `compiler::compile`, which stops at the program.
156#[cfg(feature = "compiler")]
157pub use trampoline::compile_checked;
158// The same compile as a check only, which opens no `dylib` library: what
159// `candela check` and the language server run.
160#[cfg(feature = "compiler")]
161pub use trampoline::check_only;
162// Gathers the warnings a compile raises instead of printing them.
163#[cfg(feature = "compiler")]
164pub use warnings::collect_warnings;
165// How a type is written out for a person to read: paired with the struct and
166// enum tables a compile produces, so a user type is named the way it was
167// declared. Every diagnostic prints through this, and so does a frontend that
168// shows a type in a tooltip.
169#[cfg(feature = "compiler")]
170pub use compiler::compiler_data::TypeNames;
171
172/// Runs a freshly compiled program's `main` to completion on the CLI/REPL path.
173/// The embedding API (`Engine`/`Program`) drives the VM directly instead, with
174/// the host-function tables the CLI never has.
175#[cfg(feature = "compiler")]
176pub(crate) fn execute_compiled(out: compiler::CompileOutput) {
177    // Every run starts at the entry file's `main`.
178    out.require_main();
179    let compiler::CompileOutput {
180        instructions,
181        registers,
182        mut pools,
183        instr_src,
184        callsite_registers,
185        dyn_lib_fns,
186        structs,
187        enums,
188        allocated_arg_count,
189        allocated_call_depth,
190        sources,
191        ..
192    } = out;
193    let err_ctx = ErrorCtx {
194        instr_src: &instr_src,
195        sources: &sources,
196    };
197    if let Err(error) = vm::execute(
198        &instructions,
199        &mut RegisterFile(registers),
200        &mut pools,
201        &err_ctx,
202        &callsite_registers,
203        &dyn_lib_fns,
204        &structs,
205        &enums,
206        allocated_arg_count,
207        allocated_call_depth,
208        &[],
209        &[],
210        0,
211        None,
212    ) {
213        error.report(&err_ctx);
214    }
215}
216
217#[cfg(target_arch = "wasm32")]
218#[wasm_bindgen]
219pub fn get_output() -> String {
220    candela_vm::captured_output::CAPTURED_OUTPUT.with(|o| o.take())
221}
222
223#[cfg(all(target_arch = "wasm32", feature = "compiler"))]
224#[wasm_bindgen]
225pub fn run(code: String) {
226    candela_vm::captured_output::CAPTURED_OUTPUT.with(|o| o.borrow_mut().clear());
227    execute_compiled(compile(
228        code,
229        "playground.cdl",
230        false,
231        &ImportResolver::new(),
232    ));
233}
234
235#[cfg(all(feature = "embed", feature = "compiler"))]
236#[unsafe(no_mangle)]
237#[allow(clippy::missing_safety_doc)] // WIP
238pub unsafe extern "C" fn candela_run(code: *const c_char) -> *mut c_char {
239    std::panic::set_hook(Box::new(|_| {}));
240    let code = unsafe { CStr::from_ptr(code) }
241        .to_string_lossy()
242        .to_string();
243    candela_vm::captured_output::CAPTURED_OUTPUT.with(|o| o.borrow_mut().clear());
244    // The caller gets the program's output and any error report back as the
245    // returned string, so redirect both for the duration of the run.
246    let was_capturing = candela_vm::captured_output::set_capturing(true);
247    let _ = catch_unwind(|| {
248        execute_compiled(compile(code, "embedded.cdl", false, &ImportResolver::new()));
249    });
250    candela_vm::captured_output::set_capturing(was_capturing);
251    let output = candela_vm::captured_output::CAPTURED_OUTPUT.with(|o| o.take());
252    CString::new(output).unwrap_or_default().into_raw()
253}
254
255#[cfg(all(feature = "embed", feature = "compiler"))]
256#[unsafe(no_mangle)]
257#[allow(clippy::missing_safety_doc)] // WIP
258pub unsafe extern "C" fn candela_free_output(output: *mut c_char) {
259    if !output.is_null() {
260        #[allow(unused_must_use)]
261        unsafe {
262            CString::from_raw(output)
263        };
264    }
265}
266
267/// The `candela` command. The command line itself is read in [`cli`].
268#[cfg(all(feature = "compiler", not(target_arch = "wasm32")))]
269#[must_use]
270pub fn main() -> std::process::ExitCode {
271    #[cfg(not(debug_assertions))]
272    std::panic::set_hook(Box::new(|info| {
273        eprintln!("{RED}CANDELA ERROR{RESET}\n{info}");
274    }));
275
276    cli::main()
277}