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::Result;
12use crate::runtime::SymbolTable;
13
14use context::CompileContext;
15use emit::FunctionEmitter;
16
17pub struct Compiler;
18
19impl Compiler {
20    #[must_use]
21    pub fn new() -> Self {
22        Self
23    }
24
25    pub fn compile(&mut self, program: &Program, symbols: &mut SymbolTable) -> Result<Vec<u8>> {
26        debug!(expr_count = program.exprs.len(), "compilation start");
27        let mut ctx = CompileContext::new();
28
29        let mut process = FunctionEmitter::new();
30
31        // output_base = get_output_offset()
32        process.call(ctx.func("get_output_offset"));
33        process.local_set(expr::LOCAL_OUTPUT_BASE);
34
35        expr::compile_program(&mut ctx, &mut process, symbols, program)?;
36        process.end();
37
38        let process_locals = ctx.build_locals_declaration();
39        ctx.reset_locals();
40
41        let should_apply = self.build_should_apply(&mut ctx, symbols)?;
42        ctx.add_should_apply(should_apply);
43        ctx.add_process(process.finish(&process_locals));
44
45        let wasm = ctx.finish();
46        debug!(wasm_size = wasm.len(), "compilation complete");
47        Ok(wasm)
48    }
49
50    fn build_should_apply(
51        &self,
52        ctx: &mut CompileContext,
53        symbols: &mut SymbolTable,
54    ) -> Result<wasm_encoder::Function> {
55        let body = symbols
56            .lookup("SHOULD-APPLY")
57            .and_then(|s| s.function().cloned());
58
59        let Some(Expr::Lambda(params, body)) = body else {
60            return Ok(CompileContext::default_should_apply());
61        };
62
63        if !params.required.is_empty() {
64            return Err(crate::error::Error::Compile(
65                "SHOULD-APPLY must take no parameters".to_string(),
66            ));
67        }
68
69        debug!("compiling custom should-apply");
70        let mut emit = FunctionEmitter::new();
71
72        let ty = expr::compile_for_stack(ctx, &mut emit, symbols, &body)?;
73        match ty {
74            WasmType::I32 => {}
75            WasmType::Ratio => {
76                emit.struct_get(ctx.type_idx("ratio"), 0);
77                emit.i64_const(0);
78                emit.i64_ne();
79            }
80            _ => {
81                return Err(crate::error::Error::Compile(
82                    "SHOULD-APPLY must return a boolean or numeric value".to_string(),
83                ));
84            }
85        }
86        emit.end();
87
88        let locals = ctx.build_locals_declaration();
89        ctx.reset_locals();
90        Ok(emit.finish(&locals))
91    }
92}
93
94impl Default for Compiler {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::ast::Expr;
104    use crate::runtime::{Symbol, SymbolKind};
105
106    #[test]
107    fn test_compile_empty_program() {
108        let program = Program::default();
109        let mut compiler = Compiler::new();
110        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
111        assert!(!wasm.is_empty());
112        assert_eq!(&wasm[0..4], b"\0asm");
113    }
114
115    #[test]
116    fn test_compile_nil() {
117        let program = Program::new(vec![Expr::Nil]);
118        let mut compiler = Compiler::new();
119        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
120        assert!(!wasm.is_empty());
121        assert_eq!(&wasm[0..4], b"\0asm");
122    }
123
124    #[test]
125    fn test_compile_bool() {
126        let program = Program::new(vec![Expr::Bool(true)]);
127        let mut compiler = Compiler::new();
128        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
129        assert!(!wasm.is_empty());
130    }
131
132    #[test]
133    fn test_compile_number() {
134        use num_rational::Ratio;
135        let program = Program::new(vec![Expr::Number(Ratio::new(1, 2))]);
136        let mut compiler = Compiler::new();
137        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
138        assert!(!wasm.is_empty());
139    }
140
141    #[test]
142    fn test_compile_string() {
143        let program = Program::new(vec![Expr::String("hello".into())]);
144        let mut compiler = Compiler::new();
145        let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
146        assert!(!wasm.is_empty());
147        assert_eq!(&wasm[0..4], b"\0asm");
148    }
149
150    #[test]
151    fn test_compile_symbol_with_value() {
152        let mut symbols = SymbolTable::new();
153        symbols.define(Symbol::new("REVISION", SymbolKind::Variable).with_value(Expr::Bool(true)));
154        let program = Program::new(vec![Expr::Symbol("REVISION".into())]);
155        let mut compiler = Compiler::new();
156        let wasm = compiler.compile(&program, &mut symbols).unwrap();
157        assert!(!wasm.is_empty());
158    }
159
160    #[test]
161    fn test_compile_undefined_symbol() {
162        let program = Program::new(vec![Expr::Symbol("UNKNOWN".into())]);
163        let mut compiler = Compiler::new();
164        let result = compiler.compile(&program, &mut SymbolTable::new());
165        assert!(result.is_err());
166        let err = result.unwrap_err();
167        assert!(matches!(err, crate::error::Error::UndefinedSymbol(_)));
168    }
169
170    #[test]
171    fn test_defun_populates_function_cell() {
172        let program = Program::new(vec![Expr::List(vec![
173            Expr::Symbol("DEFUN".into()),
174            Expr::Symbol("SUM".into()),
175            Expr::List(vec![
176                Expr::Symbol("A".into()),
177                Expr::Symbol("B".into()),
178                Expr::Symbol("C".into()),
179            ]),
180            Expr::String("Sums A, B, C".into()),
181            Expr::List(vec![
182                Expr::Symbol("+".into()),
183                Expr::Symbol("A".into()),
184                Expr::Symbol("B".into()),
185                Expr::Symbol("C".into()),
186            ]),
187        ])]);
188        let mut compiler = Compiler::new();
189        let mut symbols = SymbolTable::with_builtins();
190        compiler.compile(&program, &mut symbols).unwrap();
191
192        let sym = symbols.lookup("SUM").expect("SUM should be defined");
193        assert!(sym.function().is_some());
194        assert!(matches!(sym.function(), Some(Expr::Lambda(_, _))));
195        assert_eq!(sym.doc(), Some("Sums A, B, C"));
196    }
197
198    #[test]
199    fn test_defun_no_doc_populates_function_cell() {
200        let program = Program::new(vec![Expr::List(vec![
201            Expr::Symbol("DEFUN".into()),
202            Expr::Symbol("ADD".into()),
203            Expr::List(vec![Expr::Symbol("A".into()), Expr::Symbol("B".into())]),
204            Expr::List(vec![
205                Expr::Symbol("+".into()),
206                Expr::Symbol("A".into()),
207                Expr::Symbol("B".into()),
208            ]),
209        ])]);
210        let mut compiler = Compiler::new();
211        let mut symbols = SymbolTable::with_builtins();
212        compiler.compile(&program, &mut symbols).unwrap();
213
214        let sym = symbols.lookup("ADD").expect("ADD should be defined");
215        assert!(sym.function().is_some());
216        assert!(sym.doc().is_none());
217    }
218}