1
mod context;
2
mod emit;
3
pub(crate) mod expr;
4
mod layout;
5
mod native;
6
pub mod special;
7

            
8
use tracing::debug;
9

            
10
use crate::ast::{Expr, Program, WasmType};
11
use crate::error::{Error, Result};
12
use crate::host_fn::HostFnSpec;
13
use crate::runtime::SymbolTable;
14

            
15
use context::CompileContext;
16
use 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)]
26
pub enum CompileMode {
27
    Script,
28
    Eval,
29
}
30

            
31
pub struct Compiler {
32
    host_fns: Vec<HostFnSpec>,
33
}
34

            
35
impl Compiler {
36
    #[must_use]
37
72723
    pub fn new() -> Self {
38
72723
        Self {
39
72723
            host_fns: Vec::new(),
40
72723
        }
41
72723
    }
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
34114
    pub fn with_host_fns(host_fns: Vec<HostFnSpec>) -> Self {
49
34114
        Self { host_fns }
50
34114
    }
51

            
52
100192
    pub fn compile(&mut self, program: &Program, symbols: &mut SymbolTable) -> Result<Vec<u8>> {
53
100192
        self.compile_with_mode(program, symbols, CompileMode::Script)
54
100192
    }
55

            
56
100281
    pub fn compile_with_mode(
57
100281
        &mut self,
58
100281
        program: &Program,
59
100281
        symbols: &mut SymbolTable,
60
100281
        mode: CompileMode,
61
100281
    ) -> Result<Vec<u8>> {
62
100281
        debug!(expr_count = program.exprs.len(), ?mode, "compilation start");
63
100281
        match mode {
64
100277
            CompileMode::Script => self.compile_script(program, symbols),
65
4
            CompileMode::Eval => self.compile_eval(program, symbols),
66
        }
67
100281
    }
68

            
69
100277
    fn compile_script(&mut self, program: &Program, symbols: &mut SymbolTable) -> Result<Vec<u8>> {
70
100277
        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
100277
        let bootstrap_helper_count = ctx.pending_helper_count();
81

            
82
100277
        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
100277
        ctx.emit_boundary_wrapper(&mut process, None, |ctx, emit| {
88
            // output_base = get_output_offset()
89
100277
            emit.call(ctx.ids.get_output_offset()?);
90
100277
            emit.local_set(expr::LOCAL_OUTPUT_BASE);
91
100277
            expr::compile_program(ctx, emit, symbols, program)
92
100277
        })?;
93
87908
        process.end();
94

            
95
87908
        let process_locals = ctx.build_locals_declaration();
96
87908
        ctx.reset_locals();
97

            
98
87908
        let should_apply = self.build_should_apply(&mut ctx, symbols)?;
99
87908
        ctx.add_should_apply(should_apply, bootstrap_helper_count);
100
87908
        ctx.add_process(process.finish(&process_locals));
101

            
102
87908
        let wasm = ctx.finish();
103
87908
        debug!(wasm_size = wasm.len(), "compilation complete");
104
87908
        Ok(wasm)
105
100277
    }
106

            
107
4
    fn compile_eval(&mut self, program: &Program, symbols: &mut SymbolTable) -> Result<Vec<u8>> {
108
4
        let (wasm, _ty) = self.compile_eval_with_type(program, symbols)?;
109
4
        Ok(wasm)
110
4
    }
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
376908
    pub fn compile_eval_with_type(
118
376908
        &mut self,
119
376908
        program: &Program,
120
376908
        symbols: &mut SymbolTable,
121
376908
    ) -> Result<(Vec<u8>, Option<WasmType>)> {
122
376908
        let mut ctx = CompileContext::new_eval_with_host_fns(&self.host_fns)?;
123
376908
        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
376908
        let mut result_ty: Option<WasmType> = None;
130
376908
        ctx.emit_boundary_wrapper(&mut emit, Some(WasmType::AnyRef), |ctx, emit| {
131
376908
            if program.exprs.is_empty() {
132
84
                emit.ref_null_any();
133
84
                return Ok(());
134
376824
            }
135
376824
            let last_idx = program.exprs.len() - 1;
136
376824
            for expr in &program.exprs[..last_idx] {
137
664
                expr::compile_for_effect(ctx, emit, symbols, expr)?;
138
            }
139
376824
            let last = &program.exprs[last_idx];
140
376824
            if is_definition_form(last) {
141
581
                expr::compile_for_effect(ctx, emit, symbols, last)?;
142
581
                emit.ref_null_any();
143
            } else {
144
376243
                let ty = expr::compile_for_stack(ctx, emit, symbols, last)?;
145
374084
                emit_to_anyref(ctx, emit, ty);
146
374084
                result_ty = Some(ty);
147
            }
148
374665
            Ok(())
149
376908
        })?;
150
374749
        emit.end();
151

            
152
374749
        let locals = ctx.build_locals_declaration();
153
374749
        ctx.reset_locals();
154
374749
        ctx.add_nomi_eval(emit.finish(&locals));
155

            
156
374749
        let wasm = ctx.finish();
157
374749
        debug!(wasm_size = wasm.len(), "eval compilation complete");
158
374749
        Ok((wasm, result_ty))
159
376908
    }
160

            
161
87908
    fn build_should_apply(
162
87908
        &self,
163
87908
        ctx: &mut CompileContext,
164
87908
        symbols: &mut SymbolTable,
165
87908
    ) -> Result<wasm_encoder::Function> {
166
87908
        let body = symbols
167
87908
            .lookup("SHOULD-APPLY")
168
87908
            .and_then(|s| s.function().cloned());
169

            
170
1411
        let Some(Expr::Lambda(params, body)) = body else {
171
86497
            return Ok(CompileContext::default_should_apply());
172
        };
173

            
174
1411
        if !params.required.is_empty() {
175
            return Err(Error::Compile(
176
                "SHOULD-APPLY must take no parameters".to_string(),
177
            ));
178
1411
        }
179

            
180
1411
        debug!("compiling custom should-apply");
181
1411
        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
1411
        ctx.emit_boundary_wrapper(&mut emit, Some(WasmType::I32), |ctx, emit| {
186
1411
            let ty = expr::compile_for_stack(ctx, emit, symbols, &body)?;
187
1411
            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
1411
                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
1411
        })?;
203
1411
        emit.end();
204

            
205
1411
        let locals = ctx.build_locals_declaration();
206
1411
        ctx.reset_locals();
207
1411
        Ok(emit.finish(&locals))
208
87908
    }
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.
218
374084
fn emit_to_anyref(ctx: &CompileContext, emit: &mut FunctionEmitter, ty: WasmType) {
219
374084
    match ty {
220
102259
        WasmType::I32 => {
221
102259
            emit.i64_extend_i32_s();
222
102259
            emit.call(ctx.ids.ratio_from_i64);
223
102259
        }
224
5146
        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
266679
        | WasmType::AnyRef => {}
232
    }
233
374084
}
234

            
235
376824
fn is_definition_form(expr: &Expr) -> bool {
236
376824
    let Expr::List(elems) = expr else {
237
3737
        return false;
238
    };
239
373087
    let Some(Expr::Symbol(name)) = elems.first() else {
240
        return false;
241
    };
242
581
    matches!(
243
373087
        name.as_str(),
244
373087
        "DEFUN" | "DEFVAR" | "DEFMACRO" | "DEFPARAMETER" | "DEFSTRUCT"
245
    )
246
376824
}
247

            
248
impl Default for Compiler {
249
    fn default() -> Self {
250
        Self::new()
251
    }
252
}
253

            
254
#[cfg(test)]
255
mod tests {
256
    use super::*;
257
    use crate::ast::Expr;
258
    use crate::runtime::{Symbol, SymbolKind};
259

            
260
    #[test]
261
1
    fn test_compile_empty_program() {
262
1
        let program = Program::default();
263
1
        let mut compiler = Compiler::new();
264
1
        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
265
1
        assert!(!wasm.is_empty());
266
1
        assert_eq!(&wasm[0..4], b"\0asm");
267
1
    }
268

            
269
    #[test]
270
1
    fn test_compile_eval_empty_program_emits_capture_nil() {
271
1
        let program = Program::default();
272
1
        let mut compiler = Compiler::new();
273
1
        let wasm = compiler
274
1
            .compile_with_mode(&program, &mut SymbolTable::new(), CompileMode::Eval)
275
1
            .unwrap();
276
1
        assert!(!wasm.is_empty());
277
1
        assert_eq!(&wasm[0..4], b"\0asm");
278
1
    }
279

            
280
    #[test]
281
1
    fn test_compile_eval_integer_literal() {
282
1
        let program = Program::new(vec![Expr::Number(num_rational::Ratio::from_integer(7))]);
283
1
        let mut compiler = Compiler::new();
284
1
        let wasm = compiler
285
1
            .compile_with_mode(&program, &mut SymbolTable::new(), CompileMode::Eval)
286
1
            .unwrap();
287
1
        assert!(!wasm.is_empty());
288
1
    }
289

            
290
    #[test]
291
1
    fn test_compile_eval_arithmetic() {
292
1
        let program = Program::new(vec![Expr::List(vec![
293
1
            Expr::Symbol("+".into()),
294
1
            Expr::Number(num_rational::Ratio::from_integer(1)),
295
1
            Expr::Number(num_rational::Ratio::from_integer(2)),
296
1
        ])]);
297
1
        let mut compiler = Compiler::new();
298
1
        let mut symbols = SymbolTable::with_builtins();
299
1
        let wasm = compiler
300
1
            .compile_with_mode(&program, &mut symbols, CompileMode::Eval)
301
1
            .unwrap();
302
1
        assert!(!wasm.is_empty());
303
1
    }
304

            
305
    #[test]
306
1
    fn test_compile_default_uses_script_mode() {
307
1
        let program = Program::new(vec![Expr::T]);
308
1
        let mut compiler = Compiler::new();
309
1
        let mut symbols = SymbolTable::new();
310
1
        let default_bytes = compiler.compile(&program, &mut symbols).unwrap();
311
1
        let mut symbols = SymbolTable::new();
312
1
        let explicit_bytes = compiler
313
1
            .compile_with_mode(&program, &mut symbols, CompileMode::Script)
314
1
            .unwrap();
315
1
        assert_eq!(default_bytes, explicit_bytes);
316
1
    }
317

            
318
    #[test]
319
1
    fn test_compile_eval_and_script_produce_distinct_bytes() {
320
1
        let program = Program::new(vec![Expr::Number(num_rational::Ratio::from_integer(1))]);
321
1
        let mut compiler = Compiler::new();
322
1
        let mut s1 = SymbolTable::new();
323
1
        let mut s2 = SymbolTable::new();
324
1
        let script_bytes = compiler
325
1
            .compile_with_mode(&program, &mut s1, CompileMode::Script)
326
1
            .unwrap();
327
1
        let eval_bytes = compiler
328
1
            .compile_with_mode(&program, &mut s2, CompileMode::Eval)
329
1
            .unwrap();
330
1
        assert_ne!(script_bytes, eval_bytes);
331
1
    }
332

            
333
    #[test]
334
1
    fn test_compile_nil() {
335
1
        let program = Program::new(vec![Expr::Nil]);
336
1
        let mut compiler = Compiler::new();
337
1
        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
338
1
        assert!(!wasm.is_empty());
339
1
        assert_eq!(&wasm[0..4], b"\0asm");
340
1
    }
341

            
342
    #[test]
343
1
    fn test_compile_bool() {
344
1
        let program = Program::new(vec![Expr::T]);
345
1
        let mut compiler = Compiler::new();
346
1
        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
347
1
        assert!(!wasm.is_empty());
348
1
    }
349

            
350
    #[test]
351
1
    fn test_compile_number() {
352
        use num_rational::Ratio;
353
1
        let program = Program::new(vec![Expr::Number(Ratio::new(1, 2))]);
354
1
        let mut compiler = Compiler::new();
355
1
        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
356
1
        assert!(!wasm.is_empty());
357
1
    }
358

            
359
    #[test]
360
1
    fn test_compile_string() {
361
1
        let program = Program::new(vec![Expr::String("hello".into())]);
362
1
        let mut compiler = Compiler::new();
363
1
        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
364
1
        assert!(!wasm.is_empty());
365
1
        assert_eq!(&wasm[0..4], b"\0asm");
366
1
    }
367

            
368
    #[test]
369
1
    fn test_compile_symbol_with_value() {
370
1
        let mut symbols = SymbolTable::new();
371
1
        symbols.define(Symbol::new("REVISION", SymbolKind::Variable).with_value(Expr::T));
372
1
        let program = Program::new(vec![Expr::Symbol("REVISION".into())]);
373
1
        let mut compiler = Compiler::new();
374
1
        let wasm = compiler.compile(&program, &mut symbols).unwrap();
375
1
        assert!(!wasm.is_empty());
376
1
    }
377

            
378
    #[test]
379
1
    fn test_compile_undefined_symbol() {
380
1
        let program = Program::new(vec![Expr::Symbol("UNKNOWN".into())]);
381
1
        let mut compiler = Compiler::new();
382
1
        let result = compiler.compile(&program, &mut SymbolTable::new());
383
1
        assert!(result.is_err());
384
1
        let err = result.unwrap_err();
385
1
        assert!(matches!(err, crate::error::Error::UndefinedSymbol(_)));
386
1
    }
387

            
388
    #[test]
389
1
    fn test_defun_populates_function_cell() {
390
1
        let program = Program::new(vec![Expr::List(vec![
391
1
            Expr::Symbol("DEFUN".into()),
392
1
            Expr::Symbol("SUM".into()),
393
1
            Expr::List(vec![
394
1
                Expr::Symbol("A".into()),
395
1
                Expr::Symbol("B".into()),
396
1
                Expr::Symbol("C".into()),
397
1
            ]),
398
1
            Expr::String("Sums A, B, C".into()),
399
1
            Expr::List(vec![
400
1
                Expr::Symbol("+".into()),
401
1
                Expr::Symbol("A".into()),
402
1
                Expr::Symbol("B".into()),
403
1
                Expr::Symbol("C".into()),
404
1
            ]),
405
1
        ])]);
406
1
        let mut compiler = Compiler::new();
407
1
        let mut symbols = SymbolTable::with_builtins();
408
1
        compiler.compile(&program, &mut symbols).unwrap();
409

            
410
1
        let sym = symbols.lookup("SUM").expect("SUM should be defined");
411
1
        assert!(sym.function().is_some());
412
1
        assert!(matches!(sym.function(), Some(Expr::Lambda(_, _))));
413
1
        assert_eq!(sym.doc(), Some("Sums A, B, C"));
414
1
    }
415

            
416
    #[test]
417
1
    fn test_defun_no_doc_populates_function_cell() {
418
1
        let program = Program::new(vec![Expr::List(vec![
419
1
            Expr::Symbol("DEFUN".into()),
420
1
            Expr::Symbol("ADD".into()),
421
1
            Expr::List(vec![Expr::Symbol("A".into()), Expr::Symbol("B".into())]),
422
1
            Expr::List(vec![
423
1
                Expr::Symbol("+".into()),
424
1
                Expr::Symbol("A".into()),
425
1
                Expr::Symbol("B".into()),
426
1
            ]),
427
1
        ])]);
428
1
        let mut compiler = Compiler::new();
429
1
        let mut symbols = SymbolTable::with_builtins();
430
1
        compiler.compile(&program, &mut symbols).unwrap();
431

            
432
1
        let sym = symbols.lookup("ADD").expect("ADD should be defined");
433
1
        assert!(sym.function().is_some());
434
1
        assert!(sym.doc().is_none());
435
1
    }
436
}