Skip to main content

candela/
build.rs

1//! The `candela build` path: compile a `.cdl` source to a `.cdlb` bytecode
2//! artifact.
3//!
4//! The artifact format and its load/run half live in the VM-only `candela-vm`
5//! crate ([`candela_vm::artifact`]); this module is the compiler-side half that
6//! turns a fresh [`compile_checked_profile`] result into a [`ProgramImage`] and
7//! serializes it.
8//!
9//! The VM carries no compiler, so it cannot build a call trampoline when a host
10//! asks for a function by name. The trampolines are compiled ahead of time by
11//! [`compile_checked_profile`] and recorded in the artifact's export table.
12
13use crate::compiler::CompileOutput;
14use crate::compiler::function_table::function_table;
15use crate::compiler::imports::ImportResolver;
16use crate::trampoline::compile_checked_profile;
17use candela_vm::artifact::DynLibFnImage;
18use candela_vm::artifact::EnumImage;
19use candela_vm::artifact::EnumVariantImage;
20use candela_vm::artifact::ExportImage;
21use candela_vm::artifact::FunctionTable;
22use candela_vm::artifact::HostFnImage;
23use candela_vm::artifact::InstrSrcImage;
24use candela_vm::artifact::NativeImage;
25use candela_vm::artifact::ProgramImage;
26use candela_vm::artifact::SourceImage;
27use candela_vm::artifact::StructImage;
28use candela_vm::artifact::serialize_image;
29
30/// Compiles a `.cdl` source string to a `.cdlb` bytecode artifact.
31///
32/// The artifact captures the whole program: every imported workspace `.cdl`
33/// module is linked into the single serialized image, so the resulting `.cdlb`
34/// runs under `candela-vm` with no source tree present. Dynamic-library `import`s
35/// and `host` blocks are captured as recipes (logical name + symbol +
36/// signature) that the VM re-binds by name at load time, never as embedded
37/// binary bytes.
38///
39/// Every fully annotated function is compiled at its declared parameter types
40/// before the image is built, so a body error fails the build even in a
41/// function `main` never calls, instead of dropping that function silently.
42///
43/// `resolver` says where a library import reads from, the same as it does for a
44/// run from source, so a program that depends on packages builds into an
45/// artifact that carries them.
46///
47/// # Errors
48///
49/// Returns an error string if serialization fails. A compile diagnostic
50/// travels by the error funnel, not through this `Result`.
51pub fn build_bytecode(
52    source: String,
53    filename: &str,
54    resolver: &ImportResolver,
55) -> Result<Vec<u8>, String> {
56    build_artifact(source, filename, resolver, &BuildOptions::default())
57}
58
59/// How a program is built into an artifact.
60#[derive(Clone, Debug)]
61pub struct BuildOptions {
62    /// The release profile: the passes that make the program faster to run,
63    /// and machine code. `false` builds the program exactly as a run from
64    /// source compiles it.
65    pub release: bool,
66    /// The machine code the artifact carries.
67    pub native: NativeCode,
68}
69
70impl Default for BuildOptions {
71    /// The release profile, with machine code for the machine building.
72    fn default() -> Self {
73        Self {
74            release: true,
75            native: NativeCode::Host,
76        }
77    }
78}
79
80/// Which machine code a release build puts in the artifact.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub enum NativeCode {
83    /// None: the program runs on bytecode everywhere.
84    None,
85    /// Code for the machine the build runs on, when candela generates code
86    /// for it.
87    Host,
88    /// Code for the machine a target triple such as `aarch64-apple-darwin`
89    /// names.
90    Target(String),
91}
92
93/// Compiles a `.cdl` source string to a `.cdlb` artifact the way `options`
94/// asks. [`build_bytecode`] is this with the defaults `candela build` uses.
95///
96/// Machine code covers the functions the code generator handles, and the VM
97/// runs the bytecode for everything else, and for the whole program on a
98/// machine the code was not generated for. A candela built without its
99/// `native` feature generates none.
100///
101/// # Errors
102///
103/// Returns an error string if serialization fails, or if `options` names a
104/// target candela does not generate code for. A compile diagnostic travels by
105/// the error funnel, not through this `Result`.
106pub fn build_artifact(
107    source: String,
108    filename: &str,
109    resolver: &ImportResolver,
110    options: &BuildOptions,
111) -> Result<Vec<u8>, String> {
112    let (out, exports) = compile_checked_profile(source, filename, resolver, options.release);
113    // An artifact runs from `main`, so packaging a file without one would write
114    // a program that cannot start.
115    out.require_main();
116    let table = function_table(&out);
117    let mut image = image_from_output(out, exports);
118    if options.release {
119        image.native = native_sections(&image, &table, &options.native)?;
120    }
121    serialize_image(&image, Some(&table))
122}
123
124/// The machine code sections for `image`.
125#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
126fn native_sections(
127    image: &ProgramImage,
128    table: &FunctionTable,
129    native: &NativeCode,
130) -> Result<Vec<NativeImage>, String> {
131    use crate::native::Target;
132    let target = match native {
133        NativeCode::None => return Ok(Vec::new()),
134        NativeCode::Host => match Target::host() {
135            Some(target) => target,
136            None => return Ok(Vec::new()),
137        },
138        NativeCode::Target(triple) => Target::parse(triple)?,
139    };
140    match crate::native::generate(image, table, &target) {
141        Ok(section) => Ok(section.into_iter().collect()),
142        // The program is sound; the code generator is what refused. A test
143        // build says so, and a release build carries on with bytecode, which
144        // runs the program all the same.
145        Err(e) if cfg!(debug_assertions) => Err(format!("machine code generation failed: {e}")),
146        Err(_) => Ok(Vec::new()),
147    }
148}
149
150/// Without the code generator there is no machine code to add.
151#[cfg(not(all(feature = "native", not(target_arch = "wasm32"))))]
152fn native_sections(
153    _image: &ProgramImage,
154    _table: &FunctionTable,
155    native: &NativeCode,
156) -> Result<Vec<NativeImage>, String> {
157    match native {
158        NativeCode::Target(triple) => Err(format!(
159            "this candela generates no machine code, so it cannot build for {triple}; it was built without the native feature"
160        )),
161        NativeCode::None | NativeCode::Host => Ok(Vec::new()),
162    }
163}
164
165fn image_from_output(out: CompileOutput, exports: Vec<ExportImage>) -> ProgramImage {
166    // Dynamic-library bindings become referenced-by-name recipes: the logical
167    // library name, the symbol, and the marshalling signature, never the
168    // shared object's bytes. The VM re-opens the library and rebuilds the libffi
169    // CIF from these at load time.
170    let dyn_lib_fns = out
171        .dyn_lib_fns
172        .iter()
173        .map(|d| DynLibFnImage {
174            library: d.library.to_string(),
175            symbol: d.symbol.to_string(),
176            types: d.types.to_vec(),
177            origin: d.origin,
178        })
179        .collect();
180    // `host` functions are captured as name + signature so an embedding runtime
181    // can re-bind them; standalone `candela-vm` reports a clear error naming the
182    // function it cannot provide.
183    let host_fns = out
184        .host_fns
185        .iter()
186        .map(|h| HostFnImage {
187            namespace: h.namespace.to_string(),
188            name: h.name.to_string(),
189            types: h.types.to_vec(),
190            variadic: h.variadic,
191        })
192        .collect();
193
194    ProgramImage {
195        instructions: out.instructions,
196        registers: out.registers.iter().map(|d| d.into_words()).collect(),
197        objs: out
198            .pools
199            .objs
200            .0
201            .iter()
202            .map(|v| v.iter().map(|d| d.into_words()).collect())
203            .collect(),
204        maps: out
205            .pools
206            .maps
207            .0
208            .iter()
209            .map(|m| {
210                m.iter()
211                    .map(|(k, v)| (k.into_words(), v.into_words()))
212                    .collect()
213            })
214            .collect(),
215        strings: out.pools.strings.iter().cloned().collect(),
216        instr_src: out
217            .instr_src
218            .iter()
219            .map(|s| InstrSrcImage {
220                instr: s.instr,
221                span: s.span,
222                file_id: s.file_id,
223            })
224            .collect(),
225        callsite_registers: out.callsite_registers,
226        structs: out
227            .structs
228            .iter()
229            .map(|s| StructImage {
230                name: s.name.to_string(),
231                fields: s
232                    .fields
233                    .iter()
234                    .map(|(n, t, sp)| (n.to_string(), t.clone(), *sp))
235                    .collect(),
236                id: s.id,
237                name_span: s.name_span,
238            })
239            .collect(),
240        enums: out
241            .enums
242            .iter()
243            .map(|e| EnumImage {
244                name: e.name.to_string(),
245                variants: e
246                    .variants
247                    .iter()
248                    .map(|vt| EnumVariantImage {
249                        name: vt.name.to_string(),
250                        payload: vt.payload.to_vec(),
251                        name_span: vt.name_span,
252                    })
253                    .collect(),
254                id: e.id,
255                name_span: e.name_span,
256            })
257            .collect(),
258        // A runtime error shows the source of the instruction that raised it,
259        // so a file no instruction came from needs no text in the artifact.
260        // The prelude is loaded into every program, and most of it is never
261        // called.
262        sources: out
263            .sources
264            .iter()
265            .enumerate()
266            .map(|(file, s)| SourceImage {
267                filename: s.filename.to_string(),
268                contents: if out
269                    .instr_src
270                    .iter()
271                    .any(|src| usize::from(src.file_id) == file)
272                {
273                    s.contents.clone()
274                } else {
275                    String::new()
276                },
277            })
278            .collect(),
279        allocated_arg_count: out.allocated_arg_count as u64,
280        allocated_call_depth: out.allocated_call_depth as u64,
281        dyn_lib_fns,
282        host_fns,
283        exports,
284        native: Vec::new(),
285    }
286}