Skip to main content

nomiscript/compiler/
mod.rs

1mod context;
2mod emit;
3pub(crate) mod expr;
4mod layout;
5mod native;
6pub mod special;
7
8use tracing::debug;
9
10use crate::ast::{Expr, Program, WasmType};
11use crate::error::{Error, Result};
12use crate::host_fn::HostFnSpec;
13use crate::runtime::SymbolTable;
14
15use context::CompileContext;
16use emit::FunctionEmitter;
17
18/// Selects which export shape the compiler emits.
19///
20/// `Script` keeps the entity-script surface (`should_apply` + `process`
21/// exports, env.* imports) the script executor consumes. `Eval` emits a
22/// `nomi-eval` export that runs the program and returns the final value
23/// via the function's `(ref null any)` return slot; the host decodes it
24/// with `scripting::runtime::decode_eval_result`.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum CompileMode {
27    Script,
28    Eval,
29}
30
31pub struct Compiler {
32    host_fns: Vec<HostFnSpec>,
33}
34
35impl Compiler {
36    #[must_use]
37    pub fn new() -> Self {
38        Self {
39            host_fns: Vec::new(),
40        }
41    }
42
43    /// Builds a compiler that recognises the given host fn names in eval-mode
44    /// programs. Each spec gets a wasm import declared in the module and an
45    /// entry the native dispatcher consults when emitting calls. Has no effect
46    /// in `Script` mode (entity-script bytecode imports `env.*`, not `nomi.*`).
47    #[must_use]
48    pub fn with_host_fns(host_fns: Vec<HostFnSpec>) -> Self {
49        Self { host_fns }
50    }
51
52    pub fn compile(&mut self, program: &Program, symbols: &mut SymbolTable) -> Result<Vec<u8>> {
53        self.compile_with_mode(program, symbols, CompileMode::Script)
54    }
55
56    pub fn compile_with_mode(
57        &mut self,
58        program: &Program,
59        symbols: &mut SymbolTable,
60        mode: CompileMode,
61    ) -> Result<Vec<u8>> {
62        debug!(expr_count = program.exprs.len(), ?mode, "compilation start");
63        match mode {
64            CompileMode::Script => self.compile_script(program, symbols),
65            CompileMode::Eval => self.compile_eval(program, symbols),
66        }
67    }
68
69    fn compile_script(&mut self, program: &Program, symbols: &mut SymbolTable) -> Result<Vec<u8>> {
70        let mut ctx = CompileContext::new()?;
71
72        // Snapshot how many helpers (gcd / ratio_* / commodity_* / pair_*)
73        // were queued during context bootstrap; these correspond to the
74        // function-section slots between `should_apply` and `process`,
75        // so their bodies must drain *with* `should_apply`'s emit. User
76        // code inside `process` may queue more helpers (e.g. a real
77        // wasm fn for a `(lambda ...)` value); those slots land *after*
78        // `process` and have to drain *with* `process`'s emit so the
79        // code-section ordering matches the function-section.
80        let bootstrap_helper_count = ctx.pending_helper_count();
81
82        let mut process = FunctionEmitter::new();
83
84        // Wrap the body in the Tier 3 boundary `try_table` so an uncaught
85        // `(error)` throw bridges to `__nomi_raise` (ADR-0026). `process`
86        // returns void.
87        ctx.emit_boundary_wrapper(&mut process, None, |ctx, emit| {
88            // output_base = get_output_offset()
89            emit.call(ctx.ids.get_output_offset()?);
90            emit.local_set(expr::LOCAL_OUTPUT_BASE);
91            expr::compile_program(ctx, emit, symbols, program)
92        })?;
93        process.end();
94
95        let process_locals = ctx.build_locals_declaration();
96        ctx.reset_locals();
97
98        let should_apply = self.build_should_apply(&mut ctx, symbols)?;
99        ctx.add_should_apply(should_apply, bootstrap_helper_count);
100        ctx.add_process(process.finish(&process_locals));
101
102        let wasm = ctx.finish();
103        debug!(wasm_size = wasm.len(), "compilation complete");
104        Ok(wasm)
105    }
106
107    fn compile_eval(&mut self, program: &Program, symbols: &mut SymbolTable) -> Result<Vec<u8>> {
108        let (wasm, _ty) = self.compile_eval_with_type(program, symbols)?;
109        Ok(wasm)
110    }
111
112    /// Compiles `program` in eval mode and returns the wasm bytes paired
113    /// with the form's final result type. `None` for empty programs /
114    /// definition-only forms (nomi-eval returns `ref.null any`); `Some(ty)`
115    /// for any other terminator. Hosts decode the returned anyref via the
116    /// type hint — see `scripting::runtime::decode_eval_result`.
117    pub fn compile_eval_with_type(
118        &mut self,
119        program: &Program,
120        symbols: &mut SymbolTable,
121    ) -> Result<(Vec<u8>, Option<WasmType>)> {
122        let mut ctx = CompileContext::new_eval_with_host_fns(&self.host_fns)?;
123        let mut emit = FunctionEmitter::new();
124
125        // `nomi-eval` returns anyref. The boundary wrapper bridges an
126        // uncaught `(error)` to `__nomi_raise` (ADR-0026). The body's
127        // final result type is computed inside the closure and surfaced
128        // out for the host's `decode_eval_result` hint.
129        let mut result_ty: Option<WasmType> = None;
130        ctx.emit_boundary_wrapper(&mut emit, Some(WasmType::AnyRef), |ctx, emit| {
131            if program.exprs.is_empty() {
132                emit.ref_null_any();
133                return Ok(());
134            }
135            let last_idx = program.exprs.len() - 1;
136            for expr in &program.exprs[..last_idx] {
137                expr::compile_for_effect(ctx, emit, symbols, expr)?;
138            }
139            let last = &program.exprs[last_idx];
140            if is_definition_form(last) {
141                expr::compile_for_effect(ctx, emit, symbols, last)?;
142                emit.ref_null_any();
143            } else {
144                let ty = expr::compile_for_stack(ctx, emit, symbols, last)?;
145                emit_to_anyref(ctx, emit, ty);
146                result_ty = Some(ty);
147            }
148            Ok(())
149        })?;
150        emit.end();
151
152        let locals = ctx.build_locals_declaration();
153        ctx.reset_locals();
154        ctx.add_nomi_eval(emit.finish(&locals));
155
156        let wasm = ctx.finish();
157        debug!(wasm_size = wasm.len(), "eval compilation complete");
158        Ok((wasm, result_ty))
159    }
160
161    fn build_should_apply(
162        &self,
163        ctx: &mut CompileContext,
164        symbols: &mut SymbolTable,
165    ) -> Result<wasm_encoder::Function> {
166        let body = symbols
167            .lookup("SHOULD-APPLY")
168            .and_then(|s| s.function().cloned());
169
170        let Some(Expr::Lambda(params, body)) = body else {
171            return Ok(CompileContext::default_should_apply());
172        };
173
174        if !params.required.is_empty() {
175            return Err(Error::Compile(
176                "SHOULD-APPLY must take no parameters".to_string(),
177            ));
178        }
179
180        debug!("compiling custom should-apply");
181        let mut emit = FunctionEmitter::new();
182
183        // Boundary wrapper bridges an uncaught `(error)` in the body to
184        // `__nomi_raise` (ADR-0026). `should_apply` returns i32.
185        ctx.emit_boundary_wrapper(&mut emit, Some(WasmType::I32), |ctx, emit| {
186            let ty = expr::compile_for_stack(ctx, emit, symbols, &body)?;
187            match ty {
188                // Bool is the natural should-apply result (a predicate); I32
189                // (a raw count used as truthy) is also accepted. Both are
190                // i32-repr, so the i32 return passes through unchanged.
191                WasmType::I32 | WasmType::Bool => Ok(()),
192                WasmType::Ratio => {
193                    emit.struct_get(ctx.ids.ty_ratio, 0);
194                    emit.i64_const(0);
195                    emit.i64_ne();
196                    Ok(())
197                }
198                _ => Err(Error::Compile(
199                    "SHOULD-APPLY must return a boolean or numeric value".to_string(),
200                )),
201            }
202        })?;
203        emit.end();
204
205        let locals = ctx.build_locals_declaration();
206        ctx.reset_locals();
207        Ok(emit.finish(&locals))
208    }
209}
210
211/// Promotes the form's final value (already on the wasm stack as its
212/// declared `WasmType`) to an anyref-subtype so it satisfies nomi-eval's
213/// `(ref null any)` return slot. Reference-typed values (ratio_ref,
214/// commodity_ref, i8_array, pair_ref, entity_ref) are anyref subtypes
215/// already. An index crosses exactly as a `(n, 1)` ratio struct the host
216/// decodes by the result type — every i32 survives, and no script handler
217/// could catch a boxing failure this late. A bool is nil-null / i31 1.
218fn emit_to_anyref(ctx: &CompileContext, emit: &mut FunctionEmitter, ty: WasmType) {
219    match ty {
220        WasmType::I32 => {
221            emit.i64_extend_i32_s();
222            emit.call(ctx.ids.ratio_from_i64);
223        }
224        WasmType::Bool => expr::emit_box_anyref(ctx, emit, ty),
225        WasmType::Ratio
226        | WasmType::Commodity
227        | WasmType::StringRef
228        | WasmType::PairRef(_)
229        | WasmType::EntityRef(_)
230        | WasmType::Closure(_)
231        | WasmType::AnyRef => {}
232    }
233}
234
235fn is_definition_form(expr: &Expr) -> bool {
236    let Expr::List(elems) = expr else {
237        return false;
238    };
239    let Some(Expr::Symbol(name)) = elems.first() else {
240        return false;
241    };
242    matches!(
243        name.as_str(),
244        "DEFUN" | "DEFVAR" | "DEFMACRO" | "DEFPARAMETER" | "DEFSTRUCT"
245    )
246}
247
248impl Default for Compiler {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use crate::ast::Expr;
258    use crate::runtime::{Symbol, SymbolKind};
259
260    #[test]
261    fn test_compile_empty_program() {
262        let program = Program::default();
263        let mut compiler = Compiler::new();
264        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
265        assert!(!wasm.is_empty());
266        assert_eq!(&wasm[0..4], b"\0asm");
267    }
268
269    #[test]
270    fn test_compile_eval_empty_program_emits_capture_nil() {
271        let program = Program::default();
272        let mut compiler = Compiler::new();
273        let wasm = compiler
274            .compile_with_mode(&program, &mut SymbolTable::new(), CompileMode::Eval)
275            .unwrap();
276        assert!(!wasm.is_empty());
277        assert_eq!(&wasm[0..4], b"\0asm");
278    }
279
280    #[test]
281    fn test_compile_eval_integer_literal() {
282        let program = Program::new(vec![Expr::Number(num_rational::Ratio::from_integer(7))]);
283        let mut compiler = Compiler::new();
284        let wasm = compiler
285            .compile_with_mode(&program, &mut SymbolTable::new(), CompileMode::Eval)
286            .unwrap();
287        assert!(!wasm.is_empty());
288    }
289
290    #[test]
291    fn test_compile_eval_arithmetic() {
292        let program = Program::new(vec![Expr::List(vec![
293            Expr::Symbol("+".into()),
294            Expr::Number(num_rational::Ratio::from_integer(1)),
295            Expr::Number(num_rational::Ratio::from_integer(2)),
296        ])]);
297        let mut compiler = Compiler::new();
298        let mut symbols = SymbolTable::with_builtins();
299        let wasm = compiler
300            .compile_with_mode(&program, &mut symbols, CompileMode::Eval)
301            .unwrap();
302        assert!(!wasm.is_empty());
303    }
304
305    #[test]
306    fn test_compile_default_uses_script_mode() {
307        let program = Program::new(vec![Expr::T]);
308        let mut compiler = Compiler::new();
309        let mut symbols = SymbolTable::new();
310        let default_bytes = compiler.compile(&program, &mut symbols).unwrap();
311        let mut symbols = SymbolTable::new();
312        let explicit_bytes = compiler
313            .compile_with_mode(&program, &mut symbols, CompileMode::Script)
314            .unwrap();
315        assert_eq!(default_bytes, explicit_bytes);
316    }
317
318    #[test]
319    fn test_compile_eval_and_script_produce_distinct_bytes() {
320        let program = Program::new(vec![Expr::Number(num_rational::Ratio::from_integer(1))]);
321        let mut compiler = Compiler::new();
322        let mut s1 = SymbolTable::new();
323        let mut s2 = SymbolTable::new();
324        let script_bytes = compiler
325            .compile_with_mode(&program, &mut s1, CompileMode::Script)
326            .unwrap();
327        let eval_bytes = compiler
328            .compile_with_mode(&program, &mut s2, CompileMode::Eval)
329            .unwrap();
330        assert_ne!(script_bytes, eval_bytes);
331    }
332
333    #[test]
334    fn test_compile_nil() {
335        let program = Program::new(vec![Expr::Nil]);
336        let mut compiler = Compiler::new();
337        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
338        assert!(!wasm.is_empty());
339        assert_eq!(&wasm[0..4], b"\0asm");
340    }
341
342    #[test]
343    fn test_compile_bool() {
344        let program = Program::new(vec![Expr::T]);
345        let mut compiler = Compiler::new();
346        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
347        assert!(!wasm.is_empty());
348    }
349
350    #[test]
351    fn test_compile_number() {
352        use num_rational::Ratio;
353        let program = Program::new(vec![Expr::Number(Ratio::new(1, 2))]);
354        let mut compiler = Compiler::new();
355        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
356        assert!(!wasm.is_empty());
357    }
358
359    #[test]
360    fn test_compile_string() {
361        let program = Program::new(vec![Expr::String("hello".into())]);
362        let mut compiler = Compiler::new();
363        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
364        assert!(!wasm.is_empty());
365        assert_eq!(&wasm[0..4], b"\0asm");
366    }
367
368    #[test]
369    fn test_compile_symbol_with_value() {
370        let mut symbols = SymbolTable::new();
371        symbols.define(Symbol::new("REVISION", SymbolKind::Variable).with_value(Expr::T));
372        let program = Program::new(vec![Expr::Symbol("REVISION".into())]);
373        let mut compiler = Compiler::new();
374        let wasm = compiler.compile(&program, &mut symbols).unwrap();
375        assert!(!wasm.is_empty());
376    }
377
378    #[test]
379    fn test_compile_undefined_symbol() {
380        let program = Program::new(vec![Expr::Symbol("UNKNOWN".into())]);
381        let mut compiler = Compiler::new();
382        let result = compiler.compile(&program, &mut SymbolTable::new());
383        assert!(result.is_err());
384        let err = result.unwrap_err();
385        assert!(matches!(err, crate::error::Error::UndefinedSymbol(_)));
386    }
387
388    #[test]
389    fn test_defun_populates_function_cell() {
390        let program = Program::new(vec![Expr::List(vec![
391            Expr::Symbol("DEFUN".into()),
392            Expr::Symbol("SUM".into()),
393            Expr::List(vec![
394                Expr::Symbol("A".into()),
395                Expr::Symbol("B".into()),
396                Expr::Symbol("C".into()),
397            ]),
398            Expr::String("Sums A, B, C".into()),
399            Expr::List(vec![
400                Expr::Symbol("+".into()),
401                Expr::Symbol("A".into()),
402                Expr::Symbol("B".into()),
403                Expr::Symbol("C".into()),
404            ]),
405        ])]);
406        let mut compiler = Compiler::new();
407        let mut symbols = SymbolTable::with_builtins();
408        compiler.compile(&program, &mut symbols).unwrap();
409
410        let sym = symbols.lookup("SUM").expect("SUM should be defined");
411        assert!(sym.function().is_some());
412        assert!(matches!(sym.function(), Some(Expr::Lambda(_, _))));
413        assert_eq!(sym.doc(), Some("Sums A, B, C"));
414    }
415
416    #[test]
417    fn test_defun_no_doc_populates_function_cell() {
418        let program = Program::new(vec![Expr::List(vec![
419            Expr::Symbol("DEFUN".into()),
420            Expr::Symbol("ADD".into()),
421            Expr::List(vec![Expr::Symbol("A".into()), Expr::Symbol("B".into())]),
422            Expr::List(vec![
423                Expr::Symbol("+".into()),
424                Expr::Symbol("A".into()),
425                Expr::Symbol("B".into()),
426            ]),
427        ])]);
428        let mut compiler = Compiler::new();
429        let mut symbols = SymbolTable::with_builtins();
430        compiler.compile(&program, &mut symbols).unwrap();
431
432        let sym = symbols.lookup("ADD").expect("ADD should be defined");
433        assert!(sym.function().is_some());
434        assert!(sym.doc().is_none());
435    }
436}