Skip to main content

candela/
lib.rs

1//! `candela` is the full toolchain: lexer, parser, type-checker, compiler,
2//! REPL, the `Engine`/`Program` embedding API, and the `candela build`
3//! subcommand.
4//!
5//! The runtime core (the VM executor, bytecode/data types, GC, value
6//! marshalling, and the `.cdlb` load/run API) lives in the self-contained
7//! `candela-vm` crate. This crate depends on it (strictly `candela ->
8//! candela-vm`) and aliases its modules under `crate::` so the compiler keeps
9//! its `crate::data` / `crate::instr` / `crate::rt` / `crate::errors` /
10//! `crate::vm` paths.
11
12#[cfg(feature = "compiler")]
13use crate::compiler::compile;
14#[cfg(feature = "compiler")]
15use crate::errors::BOLD;
16#[cfg(feature = "compiler")]
17use crate::errors::ErrorCtx;
18#[cfg(feature = "compiler")]
19use crate::errors::RED;
20#[cfg(feature = "compiler")]
21use crate::errors::RESET;
22#[cfg(feature = "compiler")]
23use crate::repl::repl;
24use crate::vm::RegisterFile;
25#[cfg(all(feature = "embed", feature = "compiler"))]
26use std::ffi::{CStr, CString, c_char};
27use std::fs;
28#[cfg(feature = "compiler")]
29use std::hint::cold_path;
30#[cfg(all(feature = "embed", feature = "compiler"))]
31use std::panic::catch_unwind;
32#[cfg(target_arch = "wasm32")]
33use wasm_bindgen::prelude::*;
34
35// The runtime core is the `candela-vm` crate. Alias its modules under `crate::`
36// so the compiler/parser/REPL keep their existing paths, and re-export its
37// public runtime API from this crate's surface.
38pub(crate) use candela_vm::data;
39pub(crate) use candela_vm::errors;
40pub(crate) use candela_vm::instr;
41pub(crate) use candela_vm::rt;
42pub(crate) use candela_vm::vm;
43
44// `pub` so an out-of-tree frontend (candela-lsp) can reuse the lexer, parser,
45// and type-checker directly instead of reimplementing them. Gated behind the
46// `compiler` feature.
47#[cfg(feature = "compiler")]
48#[path = "./compiler/compiler.rs"]
49pub mod compiler;
50// The embedding API (`Engine`/`Program`), built on top of the compiler and the
51// `candela-vm` marshalling types.
52#[cfg(feature = "compiler")]
53mod engine;
54// The `candela build` path: compile a `.cdl` source into a `.cdlb` artifact.
55#[cfg(feature = "compiler")]
56mod build;
57// `pub` for the same reason as `compiler`: exported so tooling can lex/parse
58// standalone without going through a full `compiler::compile`.
59#[cfg(feature = "compiler")]
60#[path = "./parser/parser.rs"]
61pub mod parser;
62#[cfg(feature = "compiler")]
63mod repl;
64#[path = "./tests.rs"]
65#[cfg(all(test, feature = "compiler"))]
66mod tests;
67// Tells a person at a terminal when a newer release is out. It is only reached
68// from the REPL and `--help`, never while a program is running.
69#[cfg(feature = "compiler")]
70mod update;
71#[path = "./util/util.rs"]
72mod util;
73
74pub use candela_vm::Diagnostic;
75pub use candela_vm::collect_diagnostic;
76
77pub use candela_vm::FromHostValue;
78pub use candela_vm::HostType;
79pub use candela_vm::IntoHostFn;
80pub use candela_vm::IntoHostValue;
81pub use candela_vm::Value;
82#[cfg(feature = "compiler")]
83pub use engine::Engine;
84#[cfg(feature = "compiler")]
85pub use engine::Program;
86
87// The VM-only surface: load a pre-compiled `.cdlb` and run it.
88pub use candela_vm::LoadError;
89pub use candela_vm::RuntimeProgram;
90pub use candela_vm::load_program;
91// Compile a `.cdl` source string straight to `.cdlb` bytes (the `candela build`
92// path). Needs the compiler.
93#[cfg(feature = "compiler")]
94pub use build::build_bytecode;
95
96/// Runs a freshly compiled program's `main` to completion on the CLI/REPL path.
97/// The embedding API (`Engine`/`Program`) drives the VM directly instead, with
98/// the host-function tables the CLI never has.
99#[cfg(feature = "compiler")]
100fn execute_compiled(out: compiler::CompileOutput) {
101    let compiler::CompileOutput {
102        instructions,
103        registers,
104        mut pools,
105        instr_src,
106        fn_registers,
107        dyn_lib_fns,
108        structs,
109        enums,
110        allocated_arg_count,
111        allocated_call_depth,
112        sources,
113        ..
114    } = out;
115    vm::execute(
116        &instructions,
117        &mut RegisterFile(registers),
118        &mut pools,
119        &ErrorCtx { instr_src, sources },
120        &fn_registers,
121        &dyn_lib_fns,
122        &structs,
123        &enums,
124        allocated_arg_count,
125        allocated_call_depth,
126        &[],
127        &[],
128        0,
129    );
130}
131
132#[cfg(target_arch = "wasm32")]
133#[wasm_bindgen]
134pub fn get_output() -> String {
135    candela_vm::captured_output::CAPTURED_OUTPUT.with(|o| o.take())
136}
137
138#[cfg(all(target_arch = "wasm32", feature = "compiler"))]
139#[wasm_bindgen]
140pub fn run(code: String) {
141    candela_vm::captured_output::CAPTURED_OUTPUT.with(|o| o.borrow_mut().clear());
142    execute_compiled(compile(code, "playground.cdl", false));
143}
144
145#[cfg(all(feature = "embed", feature = "compiler"))]
146#[unsafe(no_mangle)]
147#[allow(clippy::missing_safety_doc)] // WIP
148pub unsafe extern "C" fn candela_run(code: *const c_char) -> *mut c_char {
149    std::panic::set_hook(Box::new(|_| {}));
150    let code = unsafe { CStr::from_ptr(code) }
151        .to_string_lossy()
152        .to_string();
153    candela_vm::captured_output::CAPTURED_OUTPUT.with(|o| o.borrow_mut().clear());
154    // The caller gets the program's output and any error report back as the
155    // returned string, so redirect both for the duration of the run.
156    let was_capturing = candela_vm::captured_output::set_capturing(true);
157    let _ = catch_unwind(|| {
158        execute_compiled(compile(code, "embedded.cdl", false));
159    });
160    candela_vm::captured_output::set_capturing(was_capturing);
161    let output = candela_vm::captured_output::CAPTURED_OUTPUT.with(|o| o.take());
162    CString::new(output).unwrap_or_default().into_raw()
163}
164
165#[cfg(all(feature = "embed", feature = "compiler"))]
166#[unsafe(no_mangle)]
167#[allow(clippy::missing_safety_doc)] // WIP
168pub unsafe extern "C" fn candela_free_output(output: *mut c_char) {
169    if !output.is_null() {
170        #[allow(unused_must_use)]
171        unsafe {
172            CString::from_raw(output)
173        };
174    }
175}
176
177/// Compiles a `.cdl` source file to a `.cdlb` bytecode artifact.
178///
179/// `candela build <file.cdl> [-o out.cdlb]`. The emitted artifact is run by the
180/// VM-only `candela-vm` binary, which links no parser/compiler/REPL.
181#[cfg(feature = "compiler")]
182fn build_subcommand(args: &mut impl Iterator<Item = String>) {
183    let Some(input) = args.next() else {
184        eprintln!("{RED}CANDELA ERROR{RESET}\nUsage:\n  candela build <file.cdl> [-o out.cdlb]");
185        std::process::exit(1);
186    };
187
188    // The output path is only ever named by `-o`/`--output`. A second bare
189    // path is rejected instead of taken as the output, so a mistyped
190    // `candela build a.cdl b.cdlb` says so rather than quietly writing
191    // `b.cdlb`.
192    let mut output: Option<String> = None;
193    while let Some(a) = args.next() {
194        if a == "-o" || a == "--output" {
195            let Some(path) = args.next() else {
196                eprintln!(
197                    "{RED}CANDELA ERROR{RESET}\n{a} needs an output path\nUsage:\n  candela build <file.cdl> [-o out.cdlb]"
198                );
199                std::process::exit(1);
200            };
201            output = Some(path);
202        } else {
203            eprintln!(
204                "{RED}CANDELA ERROR{RESET}\nUnexpected argument {RED}{BOLD}{a}{RESET}\nName the output file with -o or --output\nUsage:\n  candela build <file.cdl> [-o out.cdlb]"
205            );
206            std::process::exit(1);
207        }
208    }
209    // A `-o`/`--output` argument is honored verbatim. Otherwise the default
210    // output name replaces the `.cdl` extension with `.cdlb` (so
211    // `program.cdl` -> `program.cdlb`); it never appends a second extension
212    // (never `program.cdl.cdlb`). A path without a `.cdl` suffix just gets
213    // `.cdlb` added.
214    let output = output.unwrap_or_else(|| {
215        let stem = input.strip_suffix(".cdl").unwrap_or(&input);
216        format!("{stem}.cdlb")
217    });
218
219    let contents = fs::read_to_string(&input).unwrap_or_else(|_| {
220        cold_path();
221        eprintln!(
222            "--------------\n{RED}CANDELA RUNTIME ERROR:{RESET}\nCannot read {RED}{BOLD}{input}{RESET}\n--------------",
223        );
224        std::process::exit(1);
225    });
226
227    let bytes = match build::build_bytecode(contents, &input) {
228        Ok(b) => b,
229        Err(e) => {
230            eprintln!("{RED}CANDELA ERROR{RESET}\nCannot build bytecode: {e}");
231            std::process::exit(1);
232        }
233    };
234
235    if let Err(e) = fs::write(&output, &bytes) {
236        eprintln!("{RED}CANDELA ERROR{RESET}\nCannot write {output}: {e}");
237        std::process::exit(1);
238    }
239    println!("Wrote {} ({} bytes)", output, bytes.len());
240}
241
242/// Rejects anything trailing `--help` or `--version`.
243///
244/// Both flags answer a question that takes no further input, so a trailing
245/// argument is a mistake; saying so beats printing the answer to a question
246/// nobody asked.
247#[cfg(feature = "compiler")]
248fn reject_extra_args(args: &mut impl Iterator<Item = String>, flag: &str) {
249    if let Some(extra) = args.next() {
250        cold_path();
251        eprintln!(
252            "{RED}CANDELA ERROR{RESET}\n{flag} takes no other arguments, got {RED}{BOLD}{extra}{RESET}\nUsage:\n  candela myfile.cdl\n  candela build <file.cdl> [-o out.cdlb]\n  candela [-h | --help]\n  candela [-v | --version]"
253        );
254        std::process::exit(1);
255    }
256}
257
258#[cfg(feature = "compiler")]
259pub fn main() {
260    #[cfg(not(debug_assertions))]
261    std::panic::set_hook(Box::new(|info| {
262        eprintln!("{RED}CANDELA ERROR{RESET}\n{info}");
263    }));
264
265    let mut args = std::env::args().skip(1);
266
267    if args.len() == 0 {
268        cold_path();
269        repl();
270        return;
271    }
272
273    let next_arg = unsafe { args.next().unwrap_unchecked() };
274
275    if next_arg == "build" || next_arg == "compile" {
276        cold_path();
277        build_subcommand(&mut args);
278        return;
279    }
280
281    if next_arg == "--help" || next_arg == "-h" {
282        cold_path();
283        reject_extra_args(&mut args, &next_arg);
284        let update = update::start();
285        println!(
286            "{}\nCandela is a fast, statically-typed interpreted language that aims to combine Rust-like syntax with Python's ease-of-use.\n\nUsage:\n  candela myfile.cdl\n  candela build <file.cdl> [-o out.cdlb]   (compile to bytecode; run with candela-vm)\n  candela [-v | --version]",
287            util::CANDELA_LOGO
288        );
289        update::finish(update);
290        return;
291    }
292
293    if next_arg == "--version" || next_arg == "-v" {
294        cold_path();
295        reject_extra_args(&mut args, &next_arg);
296        println!("Candela {}", env!("CARGO_PKG_VERSION"));
297        return;
298    }
299
300    let filename = &next_arg;
301
302    let contents = fs::read_to_string(filename).unwrap_or_else(|_| {
303        cold_path();
304        eprintln!(
305            "--------------\n{RED}CANDELA RUNTIME ERROR:{RESET}\nCannot read {RED}{BOLD}{filename}{RESET}\n--------------",
306        );
307        std::process::exit(1);
308    });
309
310    #[cfg(debug_assertions)]
311    {
312        let next = args.next();
313        if next == Some(String::from("--debug")) {
314            let now = std::time::Instant::now();
315            let out = compile(contents, filename, true);
316            println!("COMPILATION TIME: {:.2?}", now.elapsed());
317            let now = std::time::Instant::now();
318            execute_compiled(out);
319            println!(
320                "EXECUTION TIME: {:.3}ms",
321                now.elapsed().as_nanos() / 1_000_000
322            );
323            return;
324        } else if next == Some(String::from("--debug-parser")) {
325            let _ = compile(contents, filename, false);
326            return;
327        }
328    }
329
330    execute_compiled(compile(contents, filename, false));
331}