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}