nomiscript/compiler/
mod.rs1mod 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 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}