Skip to main content

candela/
trampoline.rs

1//! Call trampolines: the short instruction run that lets a Rust host invoke a
2//! script function by name.
3//!
4//! A trampoline moves the call's arguments into the registers the target
5//! specialisation expects, calls it, and leaves the result in one register. It
6//! is compiled onto the end of the program's instruction stream, so running it
7//! means executing from its entry index against the resident register and heap
8//! state.
9//!
10//! Both halves of the toolchain build them here. `Engine`/`Program` compiles
11//! one per call, with the arguments known; the compile-time pass
12//! ([`compile_entry_points`]) compiles one per fully annotated function ahead
13//! of time, and one per function a host would call that leaves a parameter
14//! bare, with the arguments still to come, and `candela build` records where
15//! each host-callable one starts in the `.cdlb` export table.
16//!
17//! Compiling an entry point ahead of time is also what type-checks the body of
18//! a function nothing in the program calls: its declared parameter types stand
19//! in for the call site it never gets.
20
21use crate::compiler::CompileOutput;
22use crate::compiler::SymbolKind;
23use crate::compiler::check_only_scope;
24use crate::compiler::compile_profile;
25use crate::compiler::compiler_data::Ctx;
26use crate::compiler::compiler_data::State;
27use crate::compiler::compiler_data::Variable;
28use crate::compiler::expr::Expr;
29use crate::compiler::expr::METHOD_SEP;
30use crate::compiler::imports::ImportResolver;
31use crate::compiler::type_system::ANON_FN_PREFIX;
32use crate::compiler::use_immediates;
33use crate::instr::Instr;
34use crate::warnings::emit_warning;
35use crate::warnings::warn_unraisable_catch_kinds;
36use candela_vm::Diagnostic;
37use candela_vm::artifact::ExportImage;
38use candela_vm::collect_diagnostic;
39use candela_vm::data::NULL;
40use candela_vm::embed::is_host_callable_type;
41use candela_vm::rt::DataType;
42use candela_vm::rt::Span;
43use rustc_hash::FxHashSet;
44use smol_strc::SmolStr;
45
46/// Compiles a call trampoline for `call_expr`, specialising the target function
47/// for the argument types if that has not happened yet.
48///
49/// `offset` is the index the returned run will sit at once appended, which the
50/// emitted jumps and call targets are absolute against. `seed_vars` are
51/// pre-allocated registers standing in for arguments the expression refers to
52/// by name. Returns the instruction run and the register holding the result,
53/// which is `None` for a function that returns nothing.
54pub fn compile_trampoline(
55    state: &mut State<'_>,
56    offset: u16,
57    call_expr: &Expr,
58    seed_vars: Vec<Variable>,
59) -> (Vec<Instr>, Option<u16>) {
60    // A prior trampoline that aborted mid-inference (error unwind) may have
61    // left stale entries in the return-type inference thread-local; clear it so
62    // this compile starts clean, exactly as `compile()` does.
63    crate::compiler::type_system::reset_inference_state();
64
65    let ctx = Ctx {
66        block_id: 0,
67        is_compiling_recursive: false,
68        single_run: false,
69        in_function: false,
70        file_idx: 0,
71        offset,
72        return_downcast: None,
73    };
74    let mut variables = seed_vars;
75    let mut output = Vec::new();
76    let ret = call_expr.compile(&mut variables, ctx, state, &mut output, None, false, true);
77    (output, ret)
78}
79
80/// Compiles `source` the way `candela build` does: the whole program, and then
81/// an entry point for every fully annotated function in it.
82///
83/// `candela check` and [`Engine::compile`](crate::Engine::compile) are check
84/// steps for that build, so they compile through here too and refuse what the
85/// build would refuse. The export table comes back with the compile result for
86/// the caller that writes an artifact; a caller that only wanted the check
87/// drops both.
88#[must_use]
89pub fn compile_checked(
90    source: String,
91    filename: &str,
92    resolver: &ImportResolver,
93) -> (CompileOutput, Vec<ExportImage>) {
94    compile_checked_profile(source, filename, resolver, false)
95}
96
97/// [`compile_checked`] as a check only, which opens no library.
98///
99/// A `dylib` block binds the signatures it declares, so a script whose C
100/// library has not been built yet checks the same as one whose library is in
101/// place.
102///
103/// The result type-checks like a full compile but cannot call into a library,
104/// so it is for reporting on, not for running or packaging. `candela check`,
105/// [`Engine::check`](crate::Engine::check) and the language server compile
106/// through here.
107#[must_use]
108pub fn check_only(source: String, filename: &str, resolver: &ImportResolver) -> CompileOutput {
109    check_only_scope(|| compile_checked(source, filename, resolver).0)
110}
111
112/// [`compile_checked`] in the profile `optimize` names; see
113/// [`crate::compiler::compile_profile`].
114pub fn compile_checked_profile(
115    source: String,
116    filename: &str,
117    resolver: &ImportResolver,
118    optimize: bool,
119) -> (CompileOutput, Vec<ExportImage>) {
120    let mut out = compile_profile(source, filename, false, resolver, optimize);
121    let exports = compile_entry_points(&mut out);
122    let std_files: Vec<bool> = (0..out.sources.len())
123        .map(|file| out.namespaces.is_std(file as u16))
124        .collect();
125    warn_unraisable_catch_kinds(&out.sources, &std_files);
126    (out, exports)
127}
128
129/// Compiles the entry point of every fully annotated function in the file being
130/// built, and returns the export table describing the host-callable ones.
131///
132/// A function a host would call that leaves a parameter bare gets a warning per
133/// bare parameter and an entry point with those parameters typed `any`. A body
134/// that does not compile that way is undone and reported as a warning, not an
135/// error, so a program that built before still builds. A file with no `main`
136/// is a library its importers call, so it gets the entry points and none of
137/// the warnings.
138///
139/// A function's declared parameter types are everything the compiler needs to
140/// check its body, so this pass is where a body that only a host would have
141/// reached is type-checked. A diagnostic raised here travels by the error
142/// funnel and ends the compile.
143///
144/// Being checked and being exported are separate questions. The export table
145/// only lists the functions a host `Value` can be marshalled into, so a
146/// function with a struct- or enum-typed parameter is compiled here and gets no
147/// entry.
148pub fn compile_entry_points(out: &mut CompileOutput) -> Vec<ExportImage> {
149    // Register 0 is candela's void-return / null sink: a call whose result is
150    // discarded writes `null` there. A normal program has register 0 occupied by
151    // a constant, but an empty `main` can leave it free, which would let a
152    // trampoline give it to a parameter and then have a void host call clobber
153    // that parameter. Reserve it.
154    if out.registers.is_empty() {
155        out.registers.push(NULL);
156        out.const_registers.entry(NULL).or_insert(0);
157    }
158
159    let mut exports = Vec::new();
160    for (name, arg_types) in annotated_functions(out) {
161        let host_callable = arg_types.iter().all(is_host_callable_type);
162        let export = compile_entry_point(out, &name, arg_types);
163        if host_callable {
164            exports.push(export);
165        }
166    }
167
168    // A file with no `main` is a library: its functions are called by the
169    // files that import it, and those calls specialise the bare parameters,
170    // so it is not told what a host would pass. The `any` entry point is still
171    // tried for a host that loads it anyway. A file the one being built imports
172    // is never looked at here, since only the built file's functions are.
173    let warn = declares_main(out);
174
175    // What nothing compiled so far calls is chosen once, before any attempt
176    // below compiles a body that calls another candidate, so the choice is the
177    // same in both profiles and does not depend on declaration order.
178    for (name, fn_id) in host_called_bare_functions(out) {
179        let function = &out.functions[fn_id];
180        let name_span = function.name_span;
181        let filename = out.sources[function.src_file as usize].filename.to_string();
182        let arg_types: Vec<DataType> = function
183            .args
184            .iter()
185            .map(|(_, ty)| ty.clone().unwrap_or(DataType::Unknown))
186            .collect();
187        let bare: Vec<SmolStr> = function
188            .args
189            .iter()
190            .filter(|(_, ty)| warn && ty.is_none())
191            .map(|(param, _)| param.clone())
192            .collect();
193        for param in bare {
194            emit_warning(
195                &out.sources,
196                "Unannotated parameter",
197                Diagnostic {
198                    filename: filename.clone(),
199                    span: (name_span.start as usize)..(name_span.end as usize),
200                    message: format!(
201                        "Parameter {param} of {name} has no type, so a host calling {name} passes it as any. Annotate it with the type the host passes"
202                    ),
203                    code: String::from("unannotated_host_parameter"),
204                },
205            );
206        }
207        // Trying the body at `any` and carrying on when it does not compile
208        // means catching the error it raises, which needs an unwinding panic.
209        if !cfg!(panic = "unwind") {
210            continue;
211        }
212        let host_callable = arg_types.iter().all(is_host_callable_type);
213        let checkpoint = compiler_state(out).checkpoint();
214        match collect_diagnostic(|| compile_entry_point(out, &name, arg_types)) {
215            Ok(export) => {
216                if host_callable {
217                    exports.push(export);
218                }
219            }
220            Err(error) => {
221                compiler_state(out).rollback_to(&checkpoint);
222                if !warn {
223                    continue;
224                }
225                emit_warning(
226                    &out.sources,
227                    "No host entry point",
228                    Diagnostic {
229                        filename: error.filename,
230                        span: error.span,
231                        message: format!(
232                            "{name} gets no entry point a host can call: with its unannotated parameters as any, its body does not compile. {}",
233                            error.message
234                        ),
235                        code: String::from("no_host_entry_point"),
236                    },
237                );
238            }
239        }
240    }
241    exports
242}
243
244/// Whether the file being built declares `main`, which is what makes it a
245/// program a host loads rather than a library other files import.
246fn declares_main(out: &CompileOutput) -> bool {
247    out.namespaces.root().fns().any(|(name, kind)| {
248        name.as_str() == "main"
249            && matches!(kind, SymbolKind::Fn(fn_id) if out.functions[*fn_id as usize].src_file == 0)
250    })
251}
252
253/// The functions a host would call by name that have a bare parameter, in
254/// declaration order, by name and index.
255///
256/// A function qualifies when it is defined in the file being built, reachable
257/// by its bare name (so not a method or a closure), is not `main`, declares no
258/// type parameters, leaves at
259/// least one parameter unannotated, and nothing compiled so far calls it. A
260/// function the program calls is compiled by that call, at the types it
261/// passes; one nothing calls is left for a host, which has no compiler to do
262/// that once the program is packaged.
263fn host_called_bare_functions(out: &CompileOutput) -> Vec<(SmolStr, usize)> {
264    let mut seen: FxHashSet<SmolStr> = FxHashSet::default();
265    let mut bare = Vec::new();
266    for (name, kind) in out.namespaces.root().fns() {
267        let SymbolKind::Fn(fn_id) = kind else {
268            continue;
269        };
270        // A method is reached through a value of its type and a closure
271        // through the value it was stored in, never by a bare name.
272        if name.as_str() == "main"
273            || name.contains(METHOD_SEP)
274            || name.starts_with(ANON_FN_PREFIX)
275            || !seen.insert(name.clone())
276        {
277            continue;
278        }
279        let function = &out.functions[*fn_id as usize];
280        if function.src_file != 0
281            || function.generics.is_some()
282            || !function.impls.is_empty()
283            || function.args.iter().all(|(_, ty)| ty.is_some())
284        {
285            continue;
286        }
287        bare.push((name.clone(), *fn_id as usize));
288    }
289    bare
290}
291
292/// The functions an entry point is compiled for, in declaration order, paired
293/// with the parameter types it is compiled against.
294///
295/// A function qualifies when it is defined in the file being built, reachable
296/// by its bare name, is not `main`, and annotates every parameter. A bare
297/// parameter has no declared type to specialise on; such a function is first
298/// checked by the call that reaches it, or, when nothing in the program calls
299/// it, by the `any` entry point [`compile_entry_points`] tries for a host.
300fn annotated_functions(out: &CompileOutput) -> Vec<(SmolStr, Vec<DataType>)> {
301    let mut seen: FxHashSet<SmolStr> = FxHashSet::default();
302    let mut annotated = Vec::new();
303    for (name, kind) in out.namespaces.root().fns() {
304        let SymbolKind::Fn(fn_id) = kind else {
305            continue;
306        };
307        // A name resolves to the first symbol carrying it, so a later shadowed
308        // definition is not the one a call would reach.
309        if name.as_str() == "main" || !seen.insert(name.clone()) {
310            continue;
311        }
312        let function = &out.functions[*fn_id as usize];
313        if function.src_file != 0 {
314            continue;
315        }
316        let arg_types: Option<Vec<DataType>> =
317            function.args.iter().map(|(_, ty)| ty.clone()).collect();
318        if let Some(arg_types) = arg_types {
319            annotated.push((name.clone(), arg_types));
320        }
321    }
322    annotated
323}
324
325/// Compiles one function's entry point onto the end of the instruction stream.
326///
327/// The run is appended whether or not the function ends up in the export table:
328/// a specialisation compiled during the attempt caches the bytecode address of
329/// its body, so dropping the run would leave that cache pointing at
330/// instructions that were never emitted.
331fn compile_entry_point(
332    out: &mut CompileOutput,
333    name: &str,
334    arg_types: Vec<DataType>,
335) -> ExportImage {
336    // Each parameter gets a register of its own. The host writes its marshalled
337    // argument there before running the trampoline, which is why the register
338    // ids travel in the export table.
339    let dummy = Span { start: 0, end: 0 };
340    let mut arg_registers: Vec<u16> = Vec::with_capacity(arg_types.len());
341    let mut seed_vars: Vec<Variable> = Vec::with_capacity(arg_types.len());
342    let mut arg_exprs: Vec<Expr> = Vec::with_capacity(arg_types.len());
343    for (idx, var_type) in arg_types.iter().enumerate() {
344        let register_id = out.registers.len() as u16;
345        out.registers.push(NULL);
346        arg_registers.push(register_id);
347        let var_name = SmolStr::from(format!("__export_arg{idx}"));
348        seed_vars.push(Variable {
349            declared: None,
350            name: var_name.clone(),
351            register_id,
352            cell: false,
353            var_type: var_type.clone(),
354        });
355        arg_exprs.push(Expr::Var(var_name, dummy));
356    }
357
358    let call_expr = Expr::FunctionCall(
359        arg_exprs.into_boxed_slice(),
360        Box::from([SmolStr::from(name)]),
361        dummy,
362        arg_types.iter().map(|_| dummy).collect(),
363        Box::from([]),
364    );
365
366    let entry = out.instructions.len() as u16;
367    let (mut trampoline, ret_id) = {
368        let mut state = compiler_state(out);
369        compile_trampoline(&mut state, entry, &call_expr, seed_vars)
370    };
371
372    trampoline.push(Instr::Halt(0));
373    use_immediates(
374        &mut trampoline,
375        &out.instructions,
376        &out.registers,
377        &out.const_registers,
378    );
379    out.instructions.extend(trampoline);
380
381    ExportImage {
382        name: name.to_owned(),
383        entry: u64::from(entry),
384        arg_registers,
385        arg_types,
386        // A function that returns nothing lands on register 0, the null sink.
387        ret_register: ret_id.unwrap_or(0),
388    }
389}
390
391/// Borrows the compile result as the mutable state a trampoline compile writes
392/// into.
393fn compiler_state(out: &mut CompileOutput) -> State<'_> {
394    State {
395        registers: &mut out.registers,
396        fns: &mut out.functions,
397        structs: &mut out.structs,
398        enums: &mut out.enums,
399        pools: &mut out.pools,
400        instr_src: &mut out.instr_src,
401        callsite_registers: &mut out.callsite_registers,
402        dyn_libs: &mut out.dyn_libs,
403        allocated_arg_count: &mut out.allocated_arg_count,
404        allocated_call_depth: &mut out.allocated_call_depth,
405        const_registers: &mut out.const_registers,
406        free_registers: &mut out.free_registers,
407        generics: &mut out.generics,
408        indirect_registers: &mut out.indirect_registers,
409        sources: &mut out.sources,
410        reserved_registers: FxHashSet::default(),
411        value_callsites: FxHashSet::default(),
412        optimize: out.optimize,
413        namespaces: &mut out.namespaces,
414        propagations: Vec::new(),
415        fn_returns: Vec::new(),
416        captured_binders: Vec::new(),
417    }
418}