Skip to main content

ocas_eval/
compile.rs

1//! AST-to-instruction compiler.
2//!
3//! Transforms an [`EvalTree`](crate::EvalTree) into an
4//! [`ExpressionEvaluator`](crate::ExpressionEvaluator) by generating
5//! a sequence of [`Instr`](crate::Instr)s.
6//!
7//! The compiler walks the tree in post-order, assigning stack slots
8//! and emitting instructions for each node.
9
10use std::collections::{HashMap, HashSet};
11
12use ocas_atom::Atom;
13
14use crate::domain::{EvaluationDomain, PowfExtension};
15use crate::error::{EvaluationError, Result};
16use crate::evaluator::ExpressionEvaluator;
17use crate::function_map::FunctionMap;
18use crate::instruction::Instr;
19use crate::optimize;
20use crate::tree::EvalTree;
21
22/// Compile an [`Atom`] into an [`ExpressionEvaluator`].
23pub fn compile_atom<T: EvaluationDomain + PowfExtension>(
24    atom: Atom<'_>,
25) -> Result<ExpressionEvaluator<T>> {
26    compile_atom_with(atom, None)
27}
28
29/// Compile an [`Atom`] into an [`ExpressionEvaluator`] with a function map.
30pub fn compile_atom_with<T: EvaluationDomain + PowfExtension>(
31    atom: Atom<'_>,
32    function_map: Option<FunctionMap<T>>,
33) -> Result<ExpressionEvaluator<T>> {
34    let tree = EvalTree::from_atom(atom);
35    compile_tree_with(&tree, function_map)
36}
37
38/// Compile an [`EvalTree`] into an [`ExpressionEvaluator`].
39#[allow(dead_code)]
40pub fn compile_tree<T: EvaluationDomain + PowfExtension>(
41    tree: &EvalTree,
42) -> Result<ExpressionEvaluator<T>> {
43    compile_tree_with(tree, None)
44}
45
46/// Compile an [`EvalTree`] with an optional function map.
47pub fn compile_tree_with<T: EvaluationDomain + PowfExtension>(
48    tree: &EvalTree,
49    function_map: Option<FunctionMap<T>>,
50) -> Result<ExpressionEvaluator<T>> {
51    // Pass 1: collect all variable names and count constants
52    let mut var_names = HashSet::new();
53    let mut const_count = 0usize;
54    scan_tree(tree, &mut var_names, &mut const_count);
55    let param_count = var_names.len();
56
57    // Assign parameter slots: sort for deterministic ordering
58    let mut sorted_vars: Vec<String> = var_names.into_iter().collect();
59    sorted_vars.sort();
60    let var_to_param: HashMap<String, usize> = sorted_vars
61        .iter()
62        .enumerate()
63        .map(|(i, v)| (v.clone(), i))
64        .collect();
65
66    // Pass 2: compile with known param_count and const_count
67    let temp_base = param_count + const_count;
68    let (instructions, next_temp, constants, result_slot) = {
69        let mut ctx =
70            CompileContext::<T>::new(param_count, temp_base, var_to_param, function_map.as_ref());
71        let result_slot = ctx.compile_node(tree)?;
72        (ctx.instructions, ctx.next_temp, ctx.constants, result_slot)
73    };
74
75    let actual_const_count = constants.len();
76    let (instructions, _next_temp) = optimize::optimize(instructions, next_temp);
77    let stack_size = temp_base + next_temp;
78
79    match function_map {
80        Some(fm) => Ok(ExpressionEvaluator::new_with_functions(
81            instructions,
82            param_count,
83            actual_const_count,
84            stack_size,
85            vec![result_slot],
86            constants,
87            fm,
88        )),
89        None => Ok(ExpressionEvaluator::new(
90            instructions,
91            param_count,
92            actual_const_count,
93            stack_size,
94            vec![result_slot],
95            constants,
96        )),
97    }
98}
99
100/// Pre-scan tree to count variables and constants.
101fn scan_tree(tree: &EvalTree, vars: &mut HashSet<String>, const_count: &mut usize) {
102    match tree {
103        EvalTree::Num(_) => {
104            *const_count += 1;
105        }
106        EvalTree::Var(name) => {
107            vars.insert(name.clone());
108        }
109        EvalTree::Add(terms) | EvalTree::Mul(terms) => {
110            for t in terms {
111                scan_tree(t, vars, const_count);
112            }
113        }
114        EvalTree::Pow(base, exp) => {
115            scan_tree(base, vars, const_count);
116            scan_tree(exp, vars, const_count);
117        }
118        EvalTree::Fun(_, args) => {
119            for a in args {
120                scan_tree(a, vars, const_count);
121            }
122        }
123    }
124}
125
126struct CompileContext<'a, T: EvaluationDomain> {
127    instructions: Vec<Instr>,
128    /// Next available temp slot index. Temps start at `temp_base` in the actual stack.
129    next_temp: usize,
130    /// Parameter slots occupy stack[0..param_count].
131    param_count: usize,
132    /// Base index for temp slots in the actual stack (= param_count + estimated_const_count).
133    temp_base: usize,
134    /// variable name → stack slot index (0..param_count-1)
135    variables: HashMap<String, usize>,
136    /// constant values in order
137    constants: Vec<T>,
138    /// Optional function map for resolving external functions
139    function_map: Option<&'a FunctionMap<T>>,
140}
141
142impl<'a, T: EvaluationDomain> CompileContext<'a, T> {
143    fn new(
144        param_count: usize,
145        temp_base: usize,
146        variables: HashMap<String, usize>,
147        function_map: Option<&'a FunctionMap<T>>,
148    ) -> Self {
149        Self {
150            instructions: Vec::new(),
151            next_temp: 0,
152            param_count,
153            temp_base,
154            variables,
155            constants: Vec::new(),
156            function_map,
157        }
158    }
159
160    fn alloc_temp(&mut self) -> usize {
161        let slot = self.next_temp + self.temp_base;
162        self.next_temp += 1;
163        slot
164    }
165
166    fn param_slot(&self, name: &str) -> usize {
167        self.variables[name]
168    }
169
170    fn const_slot(&mut self, value: T) -> usize {
171        let idx = self.constants.len();
172        self.constants.push(value);
173        self.param_count + idx
174    }
175
176    fn compile_node(&mut self, node: &EvalTree) -> Result<usize> {
177        match node {
178            EvalTree::Num(n) => {
179                let dst = self.alloc_temp();
180                let const_slot = self.const_slot(T::from_f64(*n));
181                self.instructions.push(Instr::Copy {
182                    dst,
183                    src: const_slot,
184                });
185                Ok(dst)
186            }
187            EvalTree::Var(name) => {
188                let dst = self.alloc_temp();
189                let param_slot = self.param_slot(name);
190                self.instructions.push(Instr::Copy {
191                    dst,
192                    src: param_slot,
193                });
194                Ok(dst)
195            }
196            EvalTree::Add(terms) => {
197                let dst = self.alloc_temp();
198                let mut srcs = Vec::with_capacity(terms.len());
199                for term in terms {
200                    srcs.push(self.compile_node(term)?);
201                }
202                self.instructions.push(Instr::Add { dst, srcs });
203                Ok(dst)
204            }
205            EvalTree::Mul(factors) => {
206                let dst = self.alloc_temp();
207                let mut srcs = Vec::with_capacity(factors.len());
208                for factor in factors {
209                    srcs.push(self.compile_node(factor)?);
210                }
211                self.instructions.push(Instr::Mul { dst, srcs });
212                Ok(dst)
213            }
214            EvalTree::Pow(base, exp) => {
215                let base_slot = self.compile_node(base)?;
216                let dst = self.alloc_temp();
217                if let EvalTree::Num(n) = exp.as_ref()
218                    && n.fract() == 0.0
219                    && *n >= i64::MIN as f64
220                    && *n <= i64::MAX as f64
221                {
222                    self.instructions.push(Instr::Pow {
223                        dst,
224                        base: base_slot,
225                        exp: *n as i64,
226                    });
227                    return Ok(dst);
228                }
229                let exp_slot = self.compile_node(exp)?;
230                self.instructions.push(Instr::Powf {
231                    dst,
232                    base: base_slot,
233                    exp: exp_slot,
234                });
235                Ok(dst)
236            }
237            EvalTree::Fun(name, args) => {
238                if is_builtin(name) && args.len() == 1 {
239                    let arg_slot = self.compile_node(&args[0])?;
240                    let dst = self.alloc_temp();
241                    let op = crate::instruction::BuiltinOp::from_name(name)
242                        .expect("is_builtin guarantees known name");
243                    self.instructions.push(Instr::BuiltinOp {
244                        dst,
245                        op,
246                        src: arg_slot,
247                    });
248                    Ok(dst)
249                } else if let Some(fm) = self.function_map {
250                    // Look up in function map
251                    if let Some(_entry) = fm.resolve(name) {
252                        let mut srcs = Vec::with_capacity(args.len());
253                        for arg in args {
254                            srcs.push(self.compile_node(arg)?);
255                        }
256                        let dst = self.alloc_temp();
257                        // Find the function index in the map
258                        let fn_idx =
259                            fm.index_of(name)
260                                .ok_or_else(|| EvaluationError::FunctionNotFound {
261                                    name: name.clone(),
262                                })?;
263                        self.instructions
264                            .push(Instr::ExternalFun { dst, fn_idx, srcs });
265                        Ok(dst)
266                    } else {
267                        Err(EvaluationError::FunctionNotFound { name: name.clone() })
268                    }
269                } else {
270                    Err(EvaluationError::FunctionNotFound { name: name.clone() })
271                }
272            }
273        }
274    }
275}
276
277fn is_builtin(name: &str) -> bool {
278    matches!(
279        name.to_lowercase().as_str(),
280        "sin" | "cos" | "tan" | "sec" | "csc" | "cot" | "exp" | "log" | "sqrt" | "abs"
281    )
282}
283
284// ---------------------------------------------------------------------------
285// ExpressionEvaluator::compile
286// ---------------------------------------------------------------------------
287
288impl<T: EvaluationDomain + PowfExtension> ExpressionEvaluator<T> {
289    /// Compile an [`Atom`] into an executable evaluator.
290    pub fn compile(atom: Atom<'_>) -> Result<Self> {
291        compile_atom(atom)
292    }
293
294    /// Compile an [`Atom`] with a [`FunctionMap`] for user-defined functions.
295    pub fn compile_with(atom: Atom<'_>, map: FunctionMap<T>) -> Result<Self> {
296        compile_atom_with(atom, Some(map))
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use ocas_atom::AtomArena;
304    use ocas_core::arena::Arena;
305
306    #[test]
307    fn compile_constant() {
308        let arena = Arena::new();
309        let ctx = AtomArena::new(&arena);
310        let expr = ctx.num(42);
311        let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
312        let result = eval.evaluate(&[]).unwrap();
313        assert!((result[0] - 42.0).abs() < 1e-10);
314    }
315
316    #[test]
317    fn compile_single_var() {
318        let arena = Arena::new();
319        let ctx = AtomArena::new(&arena);
320        let expr = ctx.var("x");
321        let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
322        assert_eq!(eval.param_count(), 1);
323        let result = eval.evaluate(&[7.0]).unwrap();
324        assert!((result[0] - 7.0).abs() < 1e-10);
325    }
326
327    #[test]
328    fn compile_add_two_vars() {
329        let arena = Arena::new();
330        let ctx = AtomArena::new(&arena);
331        let expr = ctx.add(&[ctx.var("x"), ctx.var("y")]);
332        let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
333        let result = eval.evaluate(&[2.0, 3.0]).unwrap();
334        assert!((result[0] - 5.0).abs() < 1e-10);
335    }
336
337    #[test]
338    fn compile_mul_var_const() {
339        let arena = Arena::new();
340        let ctx = AtomArena::new(&arena);
341        let expr = ctx.mul(&[ctx.var("x"), ctx.num(3)]);
342        let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
343        let result = eval.evaluate(&[4.0]).unwrap();
344        assert!((result[0] - 12.0).abs() < 1e-10);
345    }
346
347    #[test]
348    fn compile_pow_integer_exp() {
349        let arena = Arena::new();
350        let ctx = AtomArena::new(&arena);
351        let expr = ctx.pow(ctx.var("x"), ctx.num(3));
352        let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
353        let result = eval.evaluate(&[2.0]).unwrap();
354        assert!((result[0] - 8.0).abs() < 1e-10);
355    }
356
357    #[test]
358    fn compile_sin() {
359        let arena = Arena::new();
360        let ctx = AtomArena::new(&arena);
361        let expr = ctx.fun("sin", &[ctx.var("x")]);
362        let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
363        let result = eval.evaluate(&[std::f64::consts::FRAC_PI_2]).unwrap();
364        assert!((result[0] - 1.0).abs() < 1e-10);
365    }
366
367    #[test]
368    fn compile_cos() {
369        let arena = Arena::new();
370        let ctx = AtomArena::new(&arena);
371        let expr = ctx.fun("cos", &[ctx.var("x")]);
372        let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
373        let result = eval.evaluate(&[std::f64::consts::PI]).unwrap();
374        assert!((result[0] + 1.0).abs() < 1e-10);
375    }
376
377    #[test]
378    fn compile_exp_log_roundtrip() {
379        let arena = Arena::new();
380        let ctx = AtomArena::new(&arena);
381        let exp_x = ctx.fun("exp", &[ctx.var("x")]);
382        let expr = ctx.fun("log", &[exp_x]);
383        let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
384        let result = eval.evaluate(&[2.0]).unwrap();
385        assert!((result[0] - 2.0).abs() < 1e-10);
386    }
387
388    #[test]
389    fn compile_nested_expression() {
390        // (x + 1) * (x - 1) = x^2 - 1
391        let arena = Arena::new();
392        let ctx = AtomArena::new(&arena);
393        let x = ctx.var("x");
394        let x_plus_1 = ctx.add(&[x, ctx.num(1)]);
395        let x_minus_1 = ctx.add(&[x, ctx.num(-1)]);
396        let expr = ctx.mul(&[x_plus_1, x_minus_1]);
397        let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
398        let result = eval.evaluate(&[3.0]).unwrap();
399        assert!((result[0] - 8.0).abs() < 1e-10);
400    }
401
402    #[test]
403    fn compile_sqrt() {
404        let arena = Arena::new();
405        let ctx = AtomArena::new(&arena);
406        let expr = ctx.fun("sqrt", &[ctx.num(16)]);
407        let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
408        let result = eval.evaluate(&[]).unwrap();
409        assert!((result[0] - 4.0).abs() < 1e-10);
410    }
411
412    #[test]
413    fn compile_zero_params() {
414        let arena = Arena::new();
415        let ctx = AtomArena::new(&arena);
416        let expr = ctx.fun("sin", &[ctx.num(1)]); // sin(1 rad)
417        let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
418        assert_eq!(eval.param_count(), 0);
419        let result = eval.evaluate(&[]).unwrap();
420        assert!((result[0] - 1.0f64.sin()).abs() < 1e-10);
421    }
422
423    #[test]
424    fn compile_with_external_function() {
425        let arena = Arena::new();
426        let ctx = AtomArena::new(&arena);
427        let expr = ctx.fun("square", &[ctx.var("x")]);
428
429        let mut map = FunctionMap::<f64>::new();
430        map.register("square", 1, Box::new(|args| args[0] * args[0]));
431
432        let eval = ExpressionEvaluator::compile_with(expr, map).unwrap();
433        let result = eval.evaluate(&[3.0]).unwrap();
434        assert!((result[0] - 9.0).abs() < 1e-10);
435    }
436
437    #[test]
438    fn compile_external_function_not_registered() {
439        let arena = Arena::new();
440        let ctx = AtomArena::new(&arena);
441        let expr = ctx.fun("missing_fn", &[ctx.var("x")]);
442
443        let result: Result<ExpressionEvaluator<f64>> = ExpressionEvaluator::compile(expr);
444        assert!(result.is_err());
445    }
446
447    #[test]
448    fn compile_with_case_insensitive_external() {
449        let arena = Arena::new();
450        let ctx = AtomArena::new(&arena);
451        let expr = ctx.fun("Square", &[ctx.num(4)]);
452
453        let mut map = FunctionMap::<f64>::new();
454        map.register("square", 1, Box::new(|args| args[0] * args[0]));
455
456        let eval = ExpressionEvaluator::compile_with(expr, map).unwrap();
457        let result = eval.evaluate(&[]).unwrap();
458        assert!((result[0] - 16.0).abs() < 1e-10);
459    }
460}