Skip to main content

ocas_eval/
instruction.rs

1//! Instruction set for the stack-based evaluation VM.
2//!
3//! The instruction set has two representations:
4//!
5//! - [`Instr`]: Internal, index-based instructions used by the evaluator.
6//!   All slot references are absolute stack indices.
7//! - [`Instruction`]: Public, [`Slot`]-based instructions for inspection
8//!   and serialization.
9//!
10//! # Stack layout
11//!
12//! ```text
13//! [params (param_count)]
14//! [constants (const_count)]
15//! [temporaries (temp_count)]
16//! [outputs (result_count)]
17//! ```
18//!
19//! The evaluator pre-fills params from user input and constants from the
20//! compiled expression, then executes instructions to compute temporaries
21//! and results.
22
23use ocas_atom::Symbol;
24
25/// Pre-resolved builtin function operation.
26///
27/// Used by the SIMD evaluator to avoid string matching on the hot path.
28/// The compiler converts `Symbol` names to `BuiltinOp` variants once at
29/// compile time.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum BuiltinOp {
32    /// $\sin(x)$
33    Sin,
34    /// $\cos(x)$
35    Cos,
36    /// $\tan(x)$
37    Tan,
38    /// $\sec(x) = 1/\cos(x)$
39    Sec,
40    /// $\csc(x) = 1/\sin(x)$
41    Csc,
42    /// $\cot(x) = 1/\tan(x)$
43    Cot,
44    /// $e^x$
45    Exp,
46    /// $\ln(x)$
47    Log,
48    /// $\sqrt{x}$
49    Sqrt,
50    /// $|x|$
51    Abs,
52}
53
54impl BuiltinOp {
55    /// Try to parse a builtin operation name (case-insensitive).
56    pub fn from_name(name: &str) -> Option<Self> {
57        match name.to_lowercase().as_str() {
58            "sin" => Some(Self::Sin),
59            "cos" => Some(Self::Cos),
60            "tan" => Some(Self::Tan),
61            "sec" => Some(Self::Sec),
62            "csc" => Some(Self::Csc),
63            "cot" => Some(Self::Cot),
64            "exp" => Some(Self::Exp),
65            "log" | "ln" => Some(Self::Log),
66            "sqrt" => Some(Self::Sqrt),
67            "abs" => Some(Self::Abs),
68            _ => None,
69        }
70    }
71}
72
73/// A named slot in the evaluator stack.
74///
75/// Used by the public [`Instruction`] type to refer to stack positions
76/// semantically rather than by raw index.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78pub enum Slot {
79    /// A parameter slot. Index is 0-based within the params area.
80    Param(usize),
81    /// A constant value slot. Index is 0-based within the constants area.
82    Const(usize),
83    /// A temporary value slot. Index is 0-based within the temporaries area.
84    Temp(usize),
85}
86
87/// A public, self-documenting instruction.
88///
89/// Unlike [`Instr`] (which uses raw stack indices), `Instruction` uses
90/// [`Slot`] to make the instruction stream readable and serializable.
91#[derive(Debug, Clone, PartialEq)]
92pub enum Instruction {
93    /// `dst = sum of sources`
94    Add(Slot, Vec<Slot>),
95    /// `dst = product of sources`
96    Mul(Slot, Vec<Slot>),
97    /// `dst = base^exp` where exp is an integer exponent
98    Pow(Slot, Slot, i64),
99    /// `dst = base^exp` where exp is another slot
100    Powf(Slot, Slot, Slot),
101    /// `dst = builtin_function(src)`
102    Fun(Slot, Symbol, Slot),
103    /// `dst = external_function(srcs...)`
104    ExternalFun(Slot, usize, Vec<Slot>),
105    /// `dst = src` (copy)
106    Assign(Slot, Slot),
107}
108
109/// An internal, index-based instruction executed by [`super::ExpressionEvaluator`].
110///
111/// All indices are absolute positions in the evaluator's flat stack:
112/// indices `0..param_count` are parameters, `param_count..param_count+const_count`
113/// are constants, and the remainder are temporaries and outputs.
114#[derive(Debug, Clone)]
115pub enum Instr {
116    /// `stack[dst] = sum(stack[srcs[0]], stack[srcs[1]], ...)`
117    Add { dst: usize, srcs: Vec<usize> },
118    /// `stack[dst] = product(stack[srcs[0]], stack[srcs[1]], ...)`
119    Mul { dst: usize, srcs: Vec<usize> },
120    /// `stack[dst] = stack[base]^exp` where exp is an integer
121    Pow { dst: usize, base: usize, exp: i64 },
122    /// `stack[dst] = stack[base]^stack[exp]` (floating-point exponent)
123    Powf { dst: usize, base: usize, exp: usize },
124    /// `stack[dst] = builtin(stack[src])`
125    BuiltinOp {
126        dst: usize,
127        op: BuiltinOp,
128        src: usize,
129    },
130    /// `stack[dst] = fns[fn_idx](&stack[srcs[0]..])`
131    ExternalFun {
132        dst: usize,
133        fn_idx: usize,
134        srcs: Vec<usize>,
135    },
136    /// `stack[dst] = stack[src]`
137    Copy { dst: usize, src: usize },
138}
139
140impl Instr {
141    /// Return the destination stack index of this instruction.
142    pub fn dst(&self) -> usize {
143        match self {
144            Instr::Add { dst, .. }
145            | Instr::Mul { dst, .. }
146            | Instr::Pow { dst, .. }
147            | Instr::Powf { dst, .. }
148            | Instr::BuiltinOp { dst, .. }
149            | Instr::ExternalFun { dst, .. }
150            | Instr::Copy { dst, .. } => *dst,
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn slot_equality() {
161        assert_eq!(Slot::Param(0), Slot::Param(0));
162        assert_ne!(Slot::Param(0), Slot::Const(0));
163        assert_eq!(Slot::Temp(3), Slot::Temp(3));
164    }
165
166    #[test]
167    fn instr_dst() {
168        let add = Instr::Add {
169            dst: 5,
170            srcs: vec![1, 2],
171        };
172        assert_eq!(add.dst(), 5);
173
174        let fun = Instr::BuiltinOp {
175            dst: 3,
176            op: BuiltinOp::Sin,
177            src: 2,
178        };
179        assert_eq!(fun.dst(), 3);
180    }
181}