Skip to main content

symplex/output/
lambdify.rs

1//! Compile symbolic expressions to callable numerical functions.
2//!
3//! [`Ex::compile`](crate::api::expr::Ex::compile) lowers one or more expressions into a compact stack-VM
4//! program after a shared common-subexpression-elimination
5//! pass.  The program is wrapped in [`CompiledFn`] (scalar) or
6//! [`CompiledFnVec`] (vector-valued) handles that are `Clone + Send + Sync`
7//! and evaluate without touching the arena or taking any locks.
8//!
9//! Every numerically evaluable [`ExprNode`] is supported, including the
10//! special functions (Γ, ln Γ, ψ, erf/erfc, Lambert W, Beta, factorials,
11//! binomials), Bessel functions and orthogonal polynomials with integer
12//! order, integer sequences, piecewise expressions and boolean/relational
13//! nodes (represented as `0.0`/`1.0`).  The special-function algorithms live
14//! in the crate-internal `numeric_rt` module and are shared with
15//! the Rust code generator, so `compile()` and `to_rust_fn()` agree bit for
16//! bit on those routines.
17//!
18//! The compiler never recurses over the expression tree — it uses an
19//! explicit work stack — so arbitrarily deep expressions are safe.
20
21use crate::base::arena::Arena;
22use crate::base::errors::SymplexError;
23use crate::base::node::{ExprId, ExprNode, SymbolId};
24use crate::output::codegen::numeric_rt as rt;
25use num_traits::{One, ToPrimitive, Zero};
26use rustc_hash::FxHashMap;
27use smallvec::SmallVec;
28use std::fmt;
29use std::ops::Deref;
30use std::sync::Arc;
31
32/// Boxed, thread-safe closure type behind [`CompiledFn`]'s `Deref`.
33type SharedFn = Arc<dyn Fn(&[f64]) -> f64 + Send + Sync>;
34
35// ═══════════════════════════════════════════════════════════════════════════
36// Public handle types
37// ═══════════════════════════════════════════════════════════════════════════
38
39/// A compiled scalar function `f(x₁, …, xₙ) → f64`.
40///
41/// Produced by [`Ex::compile`](crate::api::expr::Expr::compile).  The handle
42/// is cheap to clone (it shares the underlying program), is `Send + Sync`,
43/// and can be called directly like a closure thanks to a `Deref` to
44/// `dyn Fn(&[f64]) -> f64`:
45///
46/// ```
47/// use symplex::prelude::*;
48///
49/// let ctx = Context::new();
50/// let x = ctx.symbol("x");
51/// let f = (x.powi(2) + 1).compile(&["x"]).unwrap();
52/// assert_eq!(f.arity(), 1);
53/// assert_eq!(f(&[3.0]), 10.0);        // closure-style call
54/// assert_eq!(f.call(&[3.0]), 10.0);   // explicit call
55/// assert!(f.call(&[1.0, 2.0]).is_nan()); // wrong arity → NaN
56/// assert!(f.try_call(&[1.0, 2.0]).is_err());
57/// ```
58///
59/// Calling with the wrong number of arguments returns `NaN` from
60/// [`call`](Self::call) (and the closure form); use
61/// [`try_call`](Self::try_call) to get an error instead.
62#[derive(Clone)]
63pub struct CompiledFn {
64    program: Arc<Program>,
65    func: SharedFn,
66}
67
68impl CompiledFn {
69    fn from_program(program: Program) -> Self {
70        let program = Arc::new(program);
71        let p = Arc::clone(&program);
72        let func: SharedFn = Arc::new(move |args: &[f64]| p.run_scalar(args));
73        Self { program, func }
74    }
75
76    /// Number of arguments the function expects.
77    #[must_use]
78    pub fn arity(&self) -> usize {
79        self.program.arity
80    }
81
82    /// Evaluate the function.  Returns `NaN` if `args.len() != self.arity()`.
83    #[must_use]
84    pub fn call(&self, args: &[f64]) -> f64 {
85        self.program.run_scalar(args)
86    }
87
88    /// Evaluate the function, returning an error on arity mismatch.
89    pub fn try_call(&self, args: &[f64]) -> Result<f64, SymplexError> {
90        self.program.check_arity("CompiledFn::try_call", args)?;
91        Ok(self.program.run_scalar(args))
92    }
93
94    /// Number of VM instructions in the compiled program (a rough cost measure).
95    #[must_use]
96    pub fn instruction_count(&self) -> usize {
97        self.program.code.len()
98    }
99}
100
101impl Deref for CompiledFn {
102    type Target = dyn Fn(&[f64]) -> f64 + Send + Sync;
103    fn deref(&self) -> &Self::Target {
104        &*self.func
105    }
106}
107
108impl fmt::Debug for CompiledFn {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.debug_struct("CompiledFn")
111            .field("arity", &self.program.arity)
112            .field("instructions", &self.program.code.len())
113            .field("locals", &self.program.n_locals)
114            .finish()
115    }
116}
117
118/// A compiled vector-valued function `F(x₁, …, xₙ) → (f₁, …, fₖ)`.
119///
120/// Produced by [`Ex::compile_many`](crate::api::expr::Expr::compile_many);
121/// all outputs share one common-subexpression-elimination pass, which makes
122/// this the right tool for gradients, Jacobians and other expression
123/// families with heavy overlap.
124///
125/// ```
126/// use symplex::prelude::*;
127///
128/// let ctx = Context::new();
129/// let x = ctx.symbol("x");
130/// let y = ctx.symbol("y");
131/// let f = &x.sin() * &y;
132/// let grad = Ex::compile_many(&[&f.diff(&x), &f.diff(&y)], &["x", "y"]).unwrap();
133/// assert_eq!(grad.len(), 2);
134/// let g = grad.call_vec(&[0.0, 2.0]);
135/// assert_eq!(g, vec![2.0, 0.0]);
136/// ```
137#[derive(Clone)]
138pub struct CompiledFnVec {
139    program: Arc<Program>,
140}
141
142impl CompiledFnVec {
143    /// Number of arguments the function expects.
144    #[must_use]
145    pub fn arity(&self) -> usize {
146        self.program.arity
147    }
148
149    /// Number of output values.
150    #[must_use]
151    pub fn len(&self) -> usize {
152        self.program.n_outputs
153    }
154
155    /// True when the function produces no outputs.
156    #[must_use]
157    pub fn is_empty(&self) -> bool {
158        self.program.n_outputs == 0
159    }
160
161    /// Evaluate into `out`.
162    ///
163    /// On arity mismatch, or if `out.len() != self.len()`, every element of
164    /// `out` is set to `NaN` and no other work is done.
165    pub fn call(&self, args: &[f64], out: &mut [f64]) {
166        if args.len() != self.program.arity || out.len() != self.program.n_outputs {
167            out.fill(f64::NAN);
168            return;
169        }
170        self.program.run(args, out);
171    }
172
173    /// Evaluate into `out`, returning an error on arity or length mismatch.
174    pub fn try_call(&self, args: &[f64], out: &mut [f64]) -> Result<(), SymplexError> {
175        self.program.check_arity("CompiledFnVec::try_call", args)?;
176        if out.len() != self.program.n_outputs {
177            return Err(SymplexError::InvalidArgument {
178                operation: "CompiledFnVec::try_call",
179                reason: format!(
180                    "output slice has length {}, expected {}",
181                    out.len(),
182                    self.program.n_outputs
183                ),
184            });
185        }
186        self.program.run(args, out);
187        Ok(())
188    }
189
190    /// Evaluate and return the outputs as a freshly allocated vector
191    /// (all `NaN` on arity mismatch).
192    #[must_use]
193    pub fn call_vec(&self, args: &[f64]) -> Vec<f64> {
194        let mut out = vec![f64::NAN; self.program.n_outputs];
195        self.call(args, &mut out);
196        out
197    }
198
199    /// Number of VM instructions in the compiled program.
200    #[must_use]
201    pub fn instruction_count(&self) -> usize {
202        self.program.code.len()
203    }
204}
205
206impl fmt::Debug for CompiledFnVec {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        f.debug_struct("CompiledFnVec")
209            .field("arity", &self.program.arity)
210            .field("outputs", &self.program.n_outputs)
211            .field("instructions", &self.program.code.len())
212            .field("locals", &self.program.n_locals)
213            .finish()
214    }
215}
216
217// ═══════════════════════════════════════════════════════════════════════════
218// Entry points (crate-internal)
219// ═══════════════════════════════════════════════════════════════════════════
220
221/// Compile a single expression into a [`CompiledFn`].
222///
223/// Runs `eval()` (cheap constant folding) and CSE before lowering.
224pub(crate) fn compile(
225    arena: &mut Arena,
226    expr: ExprId,
227    var_names: &[&str],
228) -> Result<CompiledFn, SymplexError> {
229    let program = compile_program(arena, &[expr], var_names)?;
230    Ok(CompiledFn::from_program(program))
231}
232
233/// Compile a single expression **without** the `eval()` / CSE pre-passes.
234///
235/// Works on an immutable arena, so it can be used from contexts that only
236/// hold `&Arena` (numeric evaluation of a `DefiniteIntegral` body inside
237/// `evalf`).  The caller is expected to have constant-folded `expr`
238/// already; nothing is interned.
239pub(crate) fn compile_raw(
240    arena: &Arena,
241    expr: ExprId,
242    var_names: &[&str],
243) -> Result<CompiledFn, SymplexError> {
244    check_params(var_names)?;
245    let mut em = Emitter::new(arena, var_names, FxHashMap::default());
246    em.lower(expr)?;
247    em.emit(Instruction::StoreOut(0));
248    let code = em.finish()?;
249    Ok(CompiledFn::from_program(Program {
250        code,
251        arity: var_names.len(),
252        n_locals: 0,
253        n_outputs: 1,
254    }))
255}
256
257/// Compile several expressions into one [`CompiledFnVec`] with a shared CSE pass.
258pub(crate) fn compile_many(
259    arena: &mut Arena,
260    exprs: &[ExprId],
261    var_names: &[&str],
262) -> Result<CompiledFnVec, SymplexError> {
263    let program = compile_program(arena, exprs, var_names)?;
264    Ok(CompiledFnVec {
265        program: Arc::new(program),
266    })
267}
268
269/// A [`CompiledFnVec`] with no outputs (no arena required).
270pub(crate) fn compile_many_empty(var_names: &[&str]) -> Result<CompiledFnVec, SymplexError> {
271    check_params(var_names)?;
272    Ok(CompiledFnVec {
273        program: Arc::new(Program {
274            code: Vec::new(),
275            arity: var_names.len(),
276            n_locals: 0,
277            n_outputs: 0,
278        }),
279    })
280}
281
282fn check_params(var_names: &[&str]) -> Result<(), SymplexError> {
283    for (i, a) in var_names.iter().enumerate() {
284        if var_names[..i].contains(a) {
285            return Err(SymplexError::InvalidArgument {
286                operation: "compile",
287                reason: format!("duplicate parameter name '{a}'"),
288            });
289        }
290    }
291    Ok(())
292}
293
294/// Shared lowering pipeline: eval → CSE → bytecode.
295fn compile_program(
296    arena: &mut Arena,
297    exprs: &[ExprId],
298    var_names: &[&str],
299) -> Result<Program, SymplexError> {
300    check_params(var_names)?;
301
302    // Pre-pass: fold exact constants (sin(0)→0, cos(π)→−1, 4^(1/2)→2, …).
303    //
304    // Two families are lowered as-is instead:
305    // * `Piecewise` — `eval()` currently mis-simplifies piecewise nodes whose
306    //   earlier conditions are symbolic (it selects the first literal-`True`
307    //   branch);
308    // * orthogonal polynomials — `eval()` expands them into monomials, which
309    //   is numerically much worse than the three-term recurrence used by the
310    //   runtime (1e-13 vs 1e-16 absolute error for P₁₁ on [−1, 1]).
311    let evaled: Vec<ExprId> = exprs
312        .iter()
313        .map(|&e| {
314            if needs_raw_lowering(arena, e) {
315                e
316            } else {
317                crate::transforms::eval::eval(arena, e)
318            }
319        })
320        .collect();
321
322    // Shared CSE across all outputs.
323    let cse = crate::output::cse::cse_multi(arena, &evaled);
324
325    let mut locals: FxHashMap<SymbolId, usize> = FxHashMap::default();
326    for (i, (name_id, _)) in cse.bindings.iter().enumerate() {
327        if let ExprNode::Symbol(sid) = arena.node(*name_id) {
328            locals.insert(*sid, i);
329        }
330    }
331
332    let mut em = Emitter::new(arena, var_names, locals);
333    for (i, (_, value)) in cse.bindings.iter().enumerate() {
334        em.lower(*value)?;
335        em.emit(Instruction::StoreLocal(i));
336    }
337    for (i, &e) in cse.exprs.iter().enumerate() {
338        em.lower(e)?;
339        em.emit(Instruction::StoreOut(i));
340    }
341    let code = em.finish()?;
342
343    Ok(Program {
344        code,
345        arity: var_names.len(),
346        n_locals: cse.bindings.len(),
347        n_outputs: exprs.len(),
348    })
349}
350
351// ═══════════════════════════════════════════════════════════════════════════
352// Program / VM
353// ═══════════════════════════════════════════════════════════════════════════
354
355/// A lowered stack-machine program.
356struct Program {
357    code: Vec<Instruction>,
358    arity: usize,
359    n_locals: usize,
360    n_outputs: usize,
361}
362
363impl Program {
364    fn check_arity(&self, operation: &'static str, args: &[f64]) -> Result<(), SymplexError> {
365        if args.len() != self.arity {
366            return Err(SymplexError::InvalidArgument {
367                operation,
368                reason: format!("expected {} argument(s), got {}", self.arity, args.len()),
369            });
370        }
371        Ok(())
372    }
373
374    fn run_scalar(&self, args: &[f64]) -> f64 {
375        if args.len() != self.arity {
376            return f64::NAN;
377        }
378        let mut out = [f64::NAN];
379        self.run(args, &mut out);
380        out[0]
381    }
382
383    /// Execute the program.  Callers must have validated arities.
384    fn run(&self, args: &[f64], out: &mut [f64]) {
385        let mut stack: SmallVec<[f64; 32]> = SmallVec::new();
386        let mut locals: SmallVec<[f64; 16]> = SmallVec::new();
387        locals.resize(self.n_locals, 0.0);
388        let code = &self.code;
389        let mut pc = 0usize;
390
391        macro_rules! pop {
392            () => {
393                stack.pop().unwrap_or(f64::NAN)
394            };
395        }
396        macro_rules! un {
397            ($f:expr) => {{
398                let a = pop!();
399                stack.push($f(a));
400            }};
401        }
402        macro_rules! bin {
403            ($f:expr) => {{
404                let b = pop!();
405                let a = pop!();
406                stack.push($f(a, b));
407            }};
408        }
409
410        while pc < code.len() {
411            match code[pc] {
412                Instruction::PushConst(v) => stack.push(v),
413                Instruction::PushVar(i) => stack.push(args.get(i).copied().unwrap_or(f64::NAN)),
414                Instruction::LoadLocal(i) => stack.push(locals.get(i).copied().unwrap_or(f64::NAN)),
415                Instruction::StoreLocal(i) => {
416                    let v = pop!();
417                    if let Some(slot) = locals.get_mut(i) {
418                        *slot = v;
419                    }
420                }
421                Instruction::StoreOut(i) => {
422                    let v = pop!();
423                    if let Some(slot) = out.get_mut(i) {
424                        *slot = v;
425                    }
426                }
427                Instruction::Add => bin!(|a: f64, b: f64| a + b),
428                Instruction::Mul => bin!(|a: f64, b: f64| a * b),
429                Instruction::Div => bin!(|a: f64, b: f64| a / b),
430                Instruction::Neg => un!(|a: f64| -a),
431                Instruction::Pow => bin!(|a: f64, b: f64| a.powf(b)),
432                Instruction::Powi(n) => un!(|a: f64| a.powi(n)),
433                Instruction::Sqrt => un!(|a: f64| a.sqrt()),
434                Instruction::Cbrt => un!(|a: f64| a.cbrt()),
435                Instruction::ExpM1 => un!(|a: f64| a.exp_m1()),
436                Instruction::Ln1p => un!(|a: f64| a.ln_1p()),
437                Instruction::Sin => un!(|a: f64| a.sin()),
438                Instruction::Cos => un!(|a: f64| a.cos()),
439                Instruction::Tan => un!(|a: f64| a.tan()),
440                Instruction::Exp => un!(|a: f64| a.exp()),
441                Instruction::Ln => un!(|a: f64| a.ln()),
442                Instruction::Abs => un!(|a: f64| a.abs()),
443                Instruction::Asin => un!(|a: f64| a.asin()),
444                Instruction::Acos => un!(|a: f64| a.acos()),
445                Instruction::Atan => un!(|a: f64| a.atan()),
446                Instruction::Sinh => un!(|a: f64| a.sinh()),
447                Instruction::Cosh => un!(|a: f64| a.cosh()),
448                Instruction::Tanh => un!(|a: f64| a.tanh()),
449                Instruction::Asinh => un!(|a: f64| a.asinh()),
450                Instruction::Acosh => un!(|a: f64| a.acosh()),
451                Instruction::Atanh => un!(|a: f64| a.atanh()),
452                Instruction::Sign => un!(|a: f64| if a > 0.0 {
453                    1.0
454                } else if a < 0.0 {
455                    -1.0
456                } else {
457                    0.0
458                }),
459                Instruction::Heaviside => un!(|a: f64| if a > 0.0 {
460                    1.0
461                } else if a < 0.0 {
462                    0.0
463                } else {
464                    0.5
465                }),
466                // Distributional: pointwise evaluation is 0 everywhere (see docs).
467                Instruction::DiracDelta => un!(|a: f64| if a.is_nan() { f64::NAN } else { 0.0 }),
468                Instruction::Atan2 => bin!(|y: f64, x: f64| y.atan2(x)),
469                Instruction::Floor => un!(|a: f64| a.floor()),
470                Instruction::Ceiling => un!(|a: f64| a.ceil()),
471                Instruction::Min2 => bin!(|a: f64, b: f64| a.min(b)),
472                Instruction::Max2 => bin!(|a: f64, b: f64| a.max(b)),
473                Instruction::Gamma => un!(rt::gamma),
474                Instruction::LogGamma => un!(rt::lgamma),
475                Instruction::Digamma => un!(rt::digamma),
476                Instruction::Erf => un!(rt::erf),
477                Instruction::Erfc => un!(rt::erfc),
478                Instruction::LambertW => un!(rt::lambert_w0),
479                Instruction::Factorial => un!(rt::factorial),
480                Instruction::Binomial => bin!(rt::binomial),
481                Instruction::Beta => bin!(rt::beta),
482                Instruction::BesselJ(n) => un!(|a: f64| rt::bessel_j(n, a)),
483                Instruction::BesselY(n) => un!(|a: f64| rt::bessel_y(n, a)),
484                Instruction::BesselI(n) => un!(|a: f64| rt::bessel_i(n, a)),
485                Instruction::BesselK(n) => un!(|a: f64| rt::bessel_k(n, a)),
486                Instruction::LegendreP(n) => un!(|a: f64| rt::legendre_p(n, a)),
487                Instruction::ChebyshevT(n) => un!(|a: f64| rt::chebyshev_t(n, a)),
488                Instruction::ChebyshevU(n) => un!(|a: f64| rt::chebyshev_u(n, a)),
489                Instruction::HermiteH(n) => un!(|a: f64| rt::hermite_h(n, a)),
490                Instruction::LaguerreL(n) => un!(|a: f64| rt::laguerre_l(n, a)),
491                Instruction::Fibonacci => un!(rt::fibonacci),
492                Instruction::Lucas => un!(rt::lucas),
493                Instruction::Harmonic => un!(rt::harmonic),
494                Instruction::Factorial2 => un!(rt::factorial2),
495                Instruction::RisingFactorial => bin!(rt::rising_factorial),
496                Instruction::FallingFactorial => bin!(rt::falling_factorial),
497                Instruction::Gt => bin!(|a: f64, b: f64| bool_f64(a > b)),
498                Instruction::Ge => bin!(|a: f64, b: f64| bool_f64(a >= b)),
499                Instruction::Eq => bin!(|a: f64, b: f64| bool_f64(a == b)),
500                Instruction::Ne => bin!(|a: f64, b: f64| bool_f64(a != b)),
501                Instruction::And => bin!(|a: f64, b: f64| bool_f64(a != 0.0 && b != 0.0)),
502                Instruction::Or => bin!(|a: f64, b: f64| bool_f64(a != 0.0 || b != 0.0)),
503                Instruction::Not => un!(|a: f64| bool_f64(a == 0.0)),
504                Instruction::JumpIfZero(target) => {
505                    let c = pop!();
506                    if c == 0.0 || c.is_nan() {
507                        pc = target;
508                        continue;
509                    }
510                }
511                Instruction::Jump(target) => {
512                    pc = target;
513                    continue;
514                }
515            }
516            pc += 1;
517        }
518    }
519}
520
521#[inline(always)]
522fn bool_f64(b: bool) -> f64 {
523    if b { 1.0 } else { 0.0 }
524}
525
526/// A single stack-machine instruction.
527#[derive(Clone, Copy, Debug, PartialEq)]
528enum Instruction {
529    PushConst(f64),
530    /// Push argument `i`.
531    PushVar(usize),
532    /// Push CSE temporary `i`.
533    LoadLocal(usize),
534    /// Pop into CSE temporary `i`.
535    StoreLocal(usize),
536    /// Pop into output slot `i`.
537    StoreOut(usize),
538    Add,
539    Mul,
540    /// `second / top` (produced by a peephole on `Powi(-1); Mul`).
541    Div,
542    Neg,
543    /// `second ^ top`.
544    Pow,
545    Powi(i32),
546    Sqrt,
547    Cbrt,
548    ExpM1,
549    Ln1p,
550    Sin,
551    Cos,
552    Tan,
553    Exp,
554    Ln,
555    Abs,
556    Asin,
557    Acos,
558    Atan,
559    Sinh,
560    Cosh,
561    Tanh,
562    Asinh,
563    Acosh,
564    Atanh,
565    Sign,
566    Heaviside,
567    DiracDelta,
568    /// `atan2(second, top)`.
569    Atan2,
570    Floor,
571    Ceiling,
572    Min2,
573    Max2,
574    Gamma,
575    LogGamma,
576    Digamma,
577    Erf,
578    Erfc,
579    LambertW,
580    Factorial,
581    /// `binomial(second, top)`.
582    Binomial,
583    /// `beta(second, top)`.
584    Beta,
585    BesselJ(i32),
586    BesselY(i32),
587    BesselI(i32),
588    BesselK(i32),
589    LegendreP(i32),
590    ChebyshevT(i32),
591    ChebyshevU(i32),
592    HermiteH(i32),
593    LaguerreL(i32),
594    Fibonacci,
595    Lucas,
596    Harmonic,
597    Factorial2,
598    RisingFactorial,
599    FallingFactorial,
600    Gt,
601    Ge,
602    Eq,
603    Ne,
604    And,
605    Or,
606    Not,
607    /// Pop a boolean (0/1); jump to the absolute index if it is zero (or NaN).
608    JumpIfZero(usize),
609    Jump(usize),
610}
611
612// ═══════════════════════════════════════════════════════════════════════════
613// Emitter (iterative lowering with an explicit work stack)
614// ═══════════════════════════════════════════════════════════════════════════
615
616/// Work item for the explicit-stack lowering loop.
617#[derive(Clone, Copy)]
618enum Task {
619    /// Lower this expression (its value ends up on the VM stack).
620    Node(ExprId),
621    /// Emit one instruction.
622    Emit(Instruction),
623    /// Bind label `l` to the current code position.
624    Label(usize),
625    /// Emit a `JumpIfZero` to label `l` (patched at the end).
626    JumpIfZero(usize),
627    /// Emit a `Jump` to label `l` (patched at the end).
628    Jump(usize),
629    /// Lower a boolean-valued expression (pushes 0.0/1.0).
630    Bool(ExprId),
631}
632
633struct Emitter<'a> {
634    arena: &'a Arena,
635    vars: FxHashMap<&'a str, usize>,
636    locals: FxHashMap<SymbolId, usize>,
637    code: Vec<Instruction>,
638    labels: Vec<Option<usize>>,
639    fixups: Vec<(usize, usize)>,
640    /// Peephole optimisations must not reach back across this position
641    /// (a label may point here).
642    barrier: usize,
643    work: Vec<Task>,
644}
645
646impl<'a> Emitter<'a> {
647    fn new(arena: &'a Arena, var_names: &[&'a str], locals: FxHashMap<SymbolId, usize>) -> Self {
648        let vars = var_names.iter().enumerate().map(|(i, &n)| (n, i)).collect();
649        Self {
650            arena,
651            vars,
652            locals,
653            code: Vec::new(),
654            labels: Vec::new(),
655            fixups: Vec::new(),
656            barrier: 0,
657            work: Vec::new(),
658        }
659    }
660
661    fn new_label(&mut self) -> usize {
662        self.labels.push(None);
663        self.labels.len() - 1
664    }
665
666    fn emit(&mut self, inst: Instruction) {
667        // Peephole: `x; y; Powi(-1); Mul` → `x; y; Div` (one rounding instead of two).
668        if inst == Instruction::Mul
669            && self.code.len() > self.barrier
670            && self.code.last() == Some(&Instruction::Powi(-1))
671        {
672            self.code.pop();
673            self.code.push(Instruction::Div);
674            return;
675        }
676        self.code.push(inst);
677    }
678
679    /// Push tasks so that they execute in the given (forward) order.
680    fn push_seq(&mut self, tasks: &[Task]) {
681        for t in tasks.iter().rev() {
682            self.work.push(*t);
683        }
684    }
685
686    /// Lower `root`, appending code that leaves its value on the VM stack.
687    fn lower(&mut self, root: ExprId) -> Result<(), SymplexError> {
688        self.work.push(Task::Node(root));
689        while let Some(task) = self.work.pop() {
690            match task {
691                Task::Emit(inst) => self.emit(inst),
692                Task::Label(l) => {
693                    self.labels[l] = Some(self.code.len());
694                    self.barrier = self.code.len();
695                }
696                Task::JumpIfZero(l) => {
697                    self.fixups.push((self.code.len(), l));
698                    self.code.push(Instruction::JumpIfZero(usize::MAX));
699                }
700                Task::Jump(l) => {
701                    self.fixups.push((self.code.len(), l));
702                    self.code.push(Instruction::Jump(usize::MAX));
703                }
704                Task::Node(id) => self.lower_node(id)?,
705                Task::Bool(id) => self.lower_bool(id)?,
706            }
707        }
708        Ok(())
709    }
710
711    fn finish(mut self) -> Result<Vec<Instruction>, SymplexError> {
712        for (pos, label) in self.fixups.drain(..) {
713            let target = self.labels[label].ok_or_else(|| SymplexError::ComputationFailed {
714                operation: "compile",
715                reason: "internal error: unbound jump label".to_string(),
716            })?;
717            match &mut self.code[pos] {
718                Instruction::JumpIfZero(t) | Instruction::Jump(t) => *t = target,
719                _ => {}
720            }
721        }
722        Ok(self.code)
723    }
724
725    fn unsupported(&self, what: &str) -> SymplexError {
726        SymplexError::NotImplemented(format!("cannot compile `{what}` to a numerical function"))
727    }
728
729    /// Resolve an expression to a compile-time integer (for orders of Bessel
730    /// functions and degrees of orthogonal polynomials).
731    fn const_i32(&self, id: ExprId, what: &str) -> Result<i32, SymplexError> {
732        if let Some(r) = self.arena.as_num(id)
733            && r.is_integer()
734            && let Some(n) = r.to_integer().to_i32()
735        {
736            return Ok(n);
737        }
738        if let ExprNode::Neg(inner) = self.arena.node(id)
739            && let Some(r) = self.arena.as_num(*inner)
740            && r.is_integer()
741            && let Some(n) = r.to_integer().to_i32()
742        {
743            return Ok(-n);
744        }
745        Err(SymplexError::NotImplemented(format!(
746            "{what} requires a constant integer order/degree, got `{}`",
747            self.arena.display(id)
748        )))
749    }
750
751    fn unary(&mut self, child: ExprId, inst: Instruction) {
752        self.push_seq(&[Task::Node(child), Task::Emit(inst)]);
753    }
754
755    fn binary(&mut self, a: ExprId, b: ExprId, inst: Instruction) {
756        self.push_seq(&[Task::Node(a), Task::Node(b), Task::Emit(inst)]);
757    }
758
759    /// Left fold: `c₀ op c₁ op c₂ …` with `empty` for zero children.
760    fn nary(&mut self, children: &[ExprId], inst: Instruction, empty: f64) {
761        if children.is_empty() {
762            self.emit(Instruction::PushConst(empty));
763            return;
764        }
765        let mut tasks: Vec<Task> = Vec::with_capacity(children.len() * 2);
766        tasks.push(Task::Node(children[0]));
767        for &c in &children[1..] {
768            tasks.push(Task::Node(c));
769            tasks.push(Task::Emit(inst));
770        }
771        self.push_seq(&tasks);
772    }
773
774    fn lower_node(&mut self, id: ExprId) -> Result<(), SymplexError> {
775        let arena = self.arena;
776        match arena.node(id).clone() {
777            ExprNode::Num(nid) => {
778                let r = arena.num(nid);
779                let n = r.numer().to_f64().unwrap_or(f64::NAN);
780                let d = r.denom().to_f64().unwrap_or(f64::NAN);
781                self.emit(Instruction::PushConst(n / d));
782            }
783            ExprNode::Symbol(sid) => {
784                if let Some(&slot) = self.locals.get(&sid) {
785                    self.emit(Instruction::LoadLocal(slot));
786                } else {
787                    let name = arena.symbol_name(sid);
788                    match self.vars.get(name) {
789                        Some(&idx) => self.emit(Instruction::PushVar(idx)),
790                        None => {
791                            return Err(SymplexError::FreeSymbol {
792                                name: name.to_string(),
793                            });
794                        }
795                    }
796                }
797            }
798            ExprNode::Pi => self.emit(Instruction::PushConst(std::f64::consts::PI)),
799            ExprNode::E => self.emit(Instruction::PushConst(std::f64::consts::E)),
800            ExprNode::EulerGamma => self.emit(Instruction::PushConst(
801                crate::output::codegen::numeric_rt::EULER_GAMMA_F64,
802            )),
803            ExprNode::Catalan => self.emit(Instruction::PushConst(
804                crate::output::codegen::numeric_rt::CATALAN_F64,
805            )),
806            ExprNode::GoldenRatio => self.emit(Instruction::PushConst(
807                crate::output::codegen::numeric_rt::GOLDEN_RATIO_F64,
808            )),
809            ExprNode::PhysicalConstant(_, value_id) => self.work.push(Task::Node(value_id)),
810            ExprNode::Infinity => self.emit(Instruction::PushConst(f64::INFINITY)),
811            ExprNode::NegInfinity => self.emit(Instruction::PushConst(f64::NEG_INFINITY)),
812            ExprNode::NaN | ExprNode::ComplexInfinity => {
813                self.emit(Instruction::PushConst(f64::NAN))
814            }
815            // This VM is real-valued: intrinsically complex nodes have no f64
816            // value.  Use `eval_complex64` for complex evaluation.
817            ExprNode::ImaginaryUnit => return Err(self.unsupported("ImaginaryUnit")),
818            ExprNode::Re(_) => return Err(self.unsupported("re")),
819            ExprNode::Im(_) => return Err(self.unsupported("im")),
820            ExprNode::Conjugate(_) => return Err(self.unsupported("conjugate")),
821            ExprNode::Arg(_) => return Err(self.unsupported("arg")),
822            ExprNode::Si(_) => return Err(self.unsupported("Si")),
823            ExprNode::Ci(_) => return Err(self.unsupported("Ci")),
824            ExprNode::Ei(_) => return Err(self.unsupported("Ei")),
825            ExprNode::Li(_) => return Err(self.unsupported("li")),
826            ExprNode::Zeta(_) => return Err(self.unsupported("zeta")),
827            ExprNode::Polygamma(_, _) => return Err(self.unsupported("polygamma")),
828            ExprNode::KroneckerDelta(_, _) => return Err(self.unsupported("KroneckerDelta")),
829
830            ExprNode::Add(ref children) => {
831                // exp(x) + (-1) [+ rest] → exp_m1(x) [+ rest]
832                if children.len() >= 2 {
833                    let exp_idx = children
834                        .iter()
835                        .position(|&c| matches!(arena.node(c), ExprNode::Exp(_)));
836                    let neg_one_idx = children.iter().position(|&c| is_neg_one(arena, c));
837                    if let (Some(ei), Some(ni)) = (exp_idx, neg_one_idx)
838                        && ei != ni
839                        && let ExprNode::Exp(inner) = arena.node(children[ei]).clone()
840                    {
841                        let mut tasks = vec![Task::Node(inner), Task::Emit(Instruction::ExpM1)];
842                        for (i, &c) in children.iter().enumerate() {
843                            if i != ei && i != ni {
844                                tasks.push(Task::Node(c));
845                                tasks.push(Task::Emit(Instruction::Add));
846                            }
847                        }
848                        self.push_seq(&tasks);
849                        return Ok(());
850                    }
851                }
852                self.nary(children, Instruction::Add, 0.0);
853            }
854            ExprNode::Mul(ref children) => self.nary(children, Instruction::Mul, 1.0),
855            ExprNode::Min(ref children) => self.nary(children, Instruction::Min2, f64::INFINITY),
856            ExprNode::Max(ref children) => {
857                self.nary(children, Instruction::Max2, f64::NEG_INFINITY)
858            }
859            ExprNode::Pow(base, exp) => {
860                if let Some(r) = arena.as_num(exp) {
861                    if r.is_integer()
862                        && let Some(n) = r.to_integer().to_i32()
863                    {
864                        self.unary(base, Instruction::Powi(n));
865                        return Ok(());
866                    }
867                    let one: num_bigint::BigInt = One::one();
868                    if *r.numer() == one {
869                        if *r.denom() == num_bigint::BigInt::from(2) {
870                            self.unary(base, Instruction::Sqrt);
871                            return Ok(());
872                        }
873                        if *r.denom() == num_bigint::BigInt::from(3) {
874                            self.unary(base, Instruction::Cbrt);
875                            return Ok(());
876                        }
877                    }
878                    // Odd denominator q: the real root b^(p/q) = (sign(b)|b|^(1/q))^p,
879                    // i.e. |b|^e with the sign of b restored only for odd p.
880                    let two = num_bigint::BigInt::from(2);
881                    if (r.denom() % &two) != Zero::zero() {
882                        let odd_numer = (r.numer() % &two) != Zero::zero();
883                        if odd_numer {
884                            self.push_seq(&[
885                                Task::Node(base),
886                                Task::Emit(Instruction::Sign),
887                                Task::Node(base),
888                                Task::Emit(Instruction::Abs),
889                                Task::Node(exp),
890                                Task::Emit(Instruction::Pow),
891                                Task::Emit(Instruction::Mul),
892                            ]);
893                        } else {
894                            self.push_seq(&[
895                                Task::Node(base),
896                                Task::Emit(Instruction::Abs),
897                                Task::Node(exp),
898                                Task::Emit(Instruction::Pow),
899                            ]);
900                        }
901                        return Ok(());
902                    }
903                }
904                self.binary(base, exp, Instruction::Pow);
905            }
906            ExprNode::Neg(x) => self.unary(x, Instruction::Neg),
907            ExprNode::Floor(x) => self.unary(x, Instruction::Floor),
908            ExprNode::Ceiling(x) => self.unary(x, Instruction::Ceiling),
909            ExprNode::Sin(x) => self.unary(x, Instruction::Sin),
910            ExprNode::Cos(x) => self.unary(x, Instruction::Cos),
911            ExprNode::Tan(x) => self.unary(x, Instruction::Tan),
912            ExprNode::Exp(x) => self.unary(x, Instruction::Exp),
913            ExprNode::Ln(x) => {
914                // ln(1 + y) → ln_1p(y)
915                if let ExprNode::Add(ref ch) = arena.node(x).clone()
916                    && ch.len() == 2
917                {
918                    if is_one(arena, ch[0]) {
919                        self.unary(ch[1], Instruction::Ln1p);
920                        return Ok(());
921                    }
922                    if is_one(arena, ch[1]) {
923                        self.unary(ch[0], Instruction::Ln1p);
924                        return Ok(());
925                    }
926                }
927                self.unary(x, Instruction::Ln);
928            }
929            ExprNode::Abs(x) => self.unary(x, Instruction::Abs),
930            ExprNode::Asin(x) => self.unary(x, Instruction::Asin),
931            ExprNode::Acos(x) => self.unary(x, Instruction::Acos),
932            ExprNode::Atan(x) => self.unary(x, Instruction::Atan),
933            ExprNode::Atan2(y, x) => self.binary(y, x, Instruction::Atan2),
934            ExprNode::Sinh(x) => self.unary(x, Instruction::Sinh),
935            ExprNode::Cosh(x) => self.unary(x, Instruction::Cosh),
936            ExprNode::Tanh(x) => self.unary(x, Instruction::Tanh),
937            ExprNode::Asinh(x) => self.unary(x, Instruction::Asinh),
938            ExprNode::Acosh(x) => self.unary(x, Instruction::Acosh),
939            ExprNode::Atanh(x) => self.unary(x, Instruction::Atanh),
940            ExprNode::Sign(x) => self.unary(x, Instruction::Sign),
941            ExprNode::Heaviside(x) => self.unary(x, Instruction::Heaviside),
942            ExprNode::DiracDelta(x) => self.unary(x, Instruction::DiracDelta),
943
944            ExprNode::Gamma(x) => self.unary(x, Instruction::Gamma),
945            ExprNode::LogGamma(x) => self.unary(x, Instruction::LogGamma),
946            ExprNode::Digamma(x) => self.unary(x, Instruction::Digamma),
947            ExprNode::Erf(x) => self.unary(x, Instruction::Erf),
948            ExprNode::Erfc(x) => self.unary(x, Instruction::Erfc),
949            ExprNode::LambertW(x) => self.unary(x, Instruction::LambertW),
950            ExprNode::Beta(a, b) => self.binary(a, b, Instruction::Beta),
951            ExprNode::Factorial(x) => self.unary(x, Instruction::Factorial),
952            ExprNode::Binomial(n, k) => self.binary(n, k, Instruction::Binomial),
953
954            ExprNode::Piecewise(ref branches) => {
955                let end = self.new_label();
956                let mut tasks: Vec<Task> = Vec::with_capacity(branches.len() * 5 + 2);
957                for &(value, cond) in branches.iter() {
958                    let next = self.new_label();
959                    tasks.push(Task::Bool(cond));
960                    tasks.push(Task::JumpIfZero(next));
961                    tasks.push(Task::Node(value));
962                    tasks.push(Task::Jump(end));
963                    tasks.push(Task::Label(next));
964                }
965                tasks.push(Task::Emit(Instruction::PushConst(f64::NAN)));
966                tasks.push(Task::Label(end));
967                self.push_seq(&tasks);
968            }
969
970            ExprNode::BoolTrue
971            | ExprNode::BoolFalse
972            | ExprNode::Gt(_, _)
973            | ExprNode::Ge(_, _)
974            | ExprNode::Eq_(_, _)
975            | ExprNode::Ne(_, _)
976            | ExprNode::And(_)
977            | ExprNode::Or(_)
978            | ExprNode::Not(_) => self.work.push(Task::Bool(id)),
979
980            ExprNode::Apply(sid, ref args) => {
981                let name = arena.symbol_name(sid).to_string();
982                self.lower_apply(&name, args)?;
983            }
984
985            ExprNode::Derivative(_, _) => return Err(self.unsupported("Derivative")),
986            ExprNode::Integral(_, _) => return Err(self.unsupported("Integral")),
987            ExprNode::DefiniteIntegral(_, _, _, _) => {
988                return Err(self.unsupported("DefiniteIntegral"));
989            }
990            ExprNode::Sum(_, _, _, _) => return Err(self.unsupported("Sum")),
991            ExprNode::Product_(_, _, _, _) => return Err(self.unsupported("Product")),
992            ExprNode::Limit(_, _, _) => return Err(self.unsupported("Limit")),
993            ExprNode::Series(_, _, _, _) => return Err(self.unsupported("Series")),
994            ExprNode::LaplaceTransform(_, _, _) => {
995                return Err(self.unsupported("LaplaceTransform"));
996            }
997            ExprNode::InverseLaplaceTransform(_, _, _) => {
998                return Err(self.unsupported("InverseLaplaceTransform"));
999            }
1000            ExprNode::Residue(_, _, _) => return Err(self.unsupported("Residue")),
1001            ExprNode::RootOf(_, _) => return Err(self.unsupported("RootOf")),
1002            ExprNode::RootSum(_, _, _) => return Err(self.unsupported("RootSum")),
1003            ExprNode::DSolve(_, _, _) => return Err(self.unsupported("DSolve")),
1004            ExprNode::ConditionSet(_, _) => return Err(self.unsupported("ConditionSet")),
1005            ExprNode::EmptySet => return Err(self.unsupported("EmptySet")),
1006            ExprNode::UniversalSet => return Err(self.unsupported("UniversalSet")),
1007            ExprNode::Interval(_, _, _) => return Err(self.unsupported("Interval")),
1008            ExprNode::FiniteSet(_) => return Err(self.unsupported("FiniteSet")),
1009            ExprNode::SetUnion(_) => return Err(self.unsupported("SetUnion")),
1010            ExprNode::SetIntersection(_) => return Err(self.unsupported("SetIntersection")),
1011            ExprNode::SetComplement(_, _) => return Err(self.unsupported("SetComplement")),
1012        }
1013        Ok(())
1014    }
1015
1016    /// Lower a boolean-valued node to 0.0/1.0.
1017    fn lower_bool(&mut self, id: ExprId) -> Result<(), SymplexError> {
1018        let arena = self.arena;
1019        match arena.node(id).clone() {
1020            ExprNode::BoolTrue => self.emit(Instruction::PushConst(1.0)),
1021            ExprNode::BoolFalse => self.emit(Instruction::PushConst(0.0)),
1022            ExprNode::Gt(a, b) => self.binary(a, b, Instruction::Gt),
1023            ExprNode::Ge(a, b) => self.binary(a, b, Instruction::Ge),
1024            ExprNode::Eq_(a, b) => self.binary(a, b, Instruction::Eq),
1025            ExprNode::Ne(a, b) => self.binary(a, b, Instruction::Ne),
1026            ExprNode::And(ref ch) => self.nary_bool(ch, Instruction::And, 1.0),
1027            ExprNode::Or(ref ch) => self.nary_bool(ch, Instruction::Or, 0.0),
1028            ExprNode::Not(x) => self.push_seq(&[Task::Bool(x), Task::Emit(Instruction::Not)]),
1029            // A numeric expression in boolean position: nonzero ⇒ true.
1030            _ => self.work.push(Task::Node(id)),
1031        }
1032        Ok(())
1033    }
1034
1035    fn nary_bool(&mut self, children: &[ExprId], inst: Instruction, empty: f64) {
1036        if children.is_empty() {
1037            self.emit(Instruction::PushConst(empty));
1038            return;
1039        }
1040        let mut tasks: Vec<Task> = Vec::with_capacity(children.len() * 2);
1041        tasks.push(Task::Bool(children[0]));
1042        for &c in &children[1..] {
1043            tasks.push(Task::Bool(c));
1044            tasks.push(Task::Emit(inst));
1045        }
1046        self.push_seq(&tasks);
1047    }
1048
1049    /// Lower library `Apply` nodes (Bessel functions, orthogonal polynomials,
1050    /// integer sequences, factorial variants).
1051    fn lower_apply(&mut self, name: &str, args: &[ExprId]) -> Result<(), SymplexError> {
1052        use crate::base::arena as names;
1053        let arity_err = |n: usize| {
1054            SymplexError::NotImplemented(format!(
1055                "cannot compile `{name}` with {} argument(s) (expected {n})",
1056                args.len()
1057            ))
1058        };
1059        // (order, x) families with compile-time integer order.
1060        let ordered: Option<fn(i32) -> Instruction> = match name {
1061            n if n == names::FN_BESSELJ => Some(Instruction::BesselJ),
1062            n if n == names::FN_BESSELY => Some(Instruction::BesselY),
1063            n if n == names::FN_BESSELI => Some(Instruction::BesselI),
1064            n if n == names::FN_BESSELK => Some(Instruction::BesselK),
1065            n if n == names::FN_LEGENDRE => Some(Instruction::LegendreP),
1066            n if n == names::FN_CHEBYSHEV_T => Some(Instruction::ChebyshevT),
1067            n if n == names::FN_CHEBYSHEV_U => Some(Instruction::ChebyshevU),
1068            n if n == names::FN_HERMITE => Some(Instruction::HermiteH),
1069            n if n == names::FN_LAGUERRE => Some(Instruction::LaguerreL),
1070            _ => None,
1071        };
1072        if let Some(make) = ordered {
1073            if args.len() != 2 {
1074                return Err(arity_err(2));
1075            }
1076            let order = self.const_i32(args[0], name)?;
1077            self.unary(args[1], make(order));
1078            return Ok(());
1079        }
1080        let unary: Option<Instruction> = match name {
1081            n if n == names::FN_FIBONACCI => Some(Instruction::Fibonacci),
1082            n if n == names::FN_LUCAS => Some(Instruction::Lucas),
1083            n if n == names::FN_HARMONIC => Some(Instruction::Harmonic),
1084            n if n == names::FN_FACTORIAL2 => Some(Instruction::Factorial2),
1085            _ => None,
1086        };
1087        if let Some(inst) = unary {
1088            if args.len() != 1 {
1089                return Err(arity_err(1));
1090            }
1091            self.unary(args[0], inst);
1092            return Ok(());
1093        }
1094        let binary: Option<Instruction> = match name {
1095            n if n == names::FN_RISING_FACTORIAL => Some(Instruction::RisingFactorial),
1096            n if n == names::FN_FALLING_FACTORIAL => Some(Instruction::FallingFactorial),
1097            _ => None,
1098        };
1099        if let Some(inst) = binary {
1100            if args.len() != 2 {
1101                return Err(arity_err(2));
1102            }
1103            self.binary(args[0], args[1], inst);
1104            return Ok(());
1105        }
1106        Err(SymplexError::NotImplemented(format!(
1107            "cannot compile `Apply({name}, …)` to a numerical function"
1108        )))
1109    }
1110}
1111
1112/// Iterative check for nodes that must bypass the `eval()` pre-pass
1113/// (`Piecewise` and orthogonal-polynomial `Apply` nodes).
1114fn needs_raw_lowering(arena: &Arena, root: ExprId) -> bool {
1115    use crate::base::arena as names;
1116    let mut stack = vec![root];
1117    let mut seen = rustc_hash::FxHashSet::default();
1118    while let Some(id) = stack.pop() {
1119        if !seen.insert(id) {
1120            continue;
1121        }
1122        let node = arena.node(id);
1123        match node {
1124            ExprNode::Piecewise(_) => return true,
1125            ExprNode::Apply(sid, _) => {
1126                let name = arena.symbol_name(*sid);
1127                if name == names::FN_LEGENDRE
1128                    || name == names::FN_CHEBYSHEV_T
1129                    || name == names::FN_CHEBYSHEV_U
1130                    || name == names::FN_HERMITE
1131                    || name == names::FN_LAGUERRE
1132                {
1133                    return true;
1134                }
1135            }
1136            _ => {}
1137        }
1138        node.for_each_child(|c| stack.push(c));
1139    }
1140    false
1141}
1142
1143/// Check if a node is the constant −1 (either `Num(-1)` or `Neg(Num(1))`).
1144fn is_neg_one(arena: &Arena, id: ExprId) -> bool {
1145    if let Some(r) = arena.as_num(id) {
1146        return *r == num_rational::Ratio::from_integer((-1).into());
1147    }
1148    if let ExprNode::Neg(inner) = arena.node(id)
1149        && let Some(r) = arena.as_num(*inner)
1150    {
1151        return r.is_one();
1152    }
1153    false
1154}
1155
1156/// Check if a node is the constant 1.
1157fn is_one(arena: &Arena, id: ExprId) -> bool {
1158    arena.as_num(id).is_some_and(|r| r.is_one())
1159}
1160
1161#[cfg(test)]
1162#[allow(clippy::excessive_precision)]
1163mod tests {
1164    use super::*;
1165
1166    fn close(a: f64, b: f64, tol: f64) -> bool {
1167        (a - b).abs() <= tol * (1.0 + b.abs())
1168    }
1169
1170    #[test]
1171    fn lambdify_polynomial() {
1172        let mut a = Arena::new();
1173        let x = a.symbol("x");
1174        let two = a.int(2);
1175        let three = a.int(3);
1176        let x2 = a.pow(x, two);
1177        let three_x = a.mul(&[three, x]);
1178        let one = a.int(1);
1179        let expr = a.add(&[x2, three_x, one]);
1180        // f(x) = x^2 + 3x + 1
1181        let f = compile(&mut a, expr, &["x"]).unwrap();
1182        assert!((f(&[2.0]) - 11.0).abs() < 1e-10); // 4 + 6 + 1 = 11
1183        assert!((f(&[0.0]) - 1.0).abs() < 1e-10);
1184        assert!((f(&[-1.0]) - (-1.0)).abs() < 1e-10); // 1 - 3 + 1 = -1
1185        assert_eq!(f.arity(), 1);
1186    }
1187
1188    #[test]
1189    fn lambdify_trig() {
1190        let mut a = Arena::new();
1191        let x = a.symbol("x");
1192        let expr = a.sin(x);
1193        let f = compile(&mut a, expr, &["x"]).unwrap();
1194        assert!((f(&[0.0]) - 0.0).abs() < 1e-10);
1195        assert!((f(&[std::f64::consts::FRAC_PI_2]) - 1.0).abs() < 1e-10);
1196    }
1197
1198    #[test]
1199    fn lambdify_two_vars() {
1200        let mut a = Arena::new();
1201        let x = a.symbol("x");
1202        let y = a.symbol("y");
1203        let xy = a.mul(&[x, y]);
1204        let one = a.int(1);
1205        let expr = a.add(&[xy, one]);
1206        let f = compile(&mut a, expr, &["x", "y"]).unwrap();
1207        assert!((f(&[3.0, 4.0]) - 13.0).abs() < 1e-10);
1208    }
1209
1210    #[test]
1211    fn lambdify_exp_sin() {
1212        let mut a = Arena::new();
1213        let x = a.symbol("x");
1214        let sin_x = a.sin(x);
1215        let expr = a.exp(sin_x);
1216        let f = compile(&mut a, expr, &["x"]).unwrap();
1217        let expected = 0.5f64.sin().exp();
1218        assert!((f(&[0.5]) - expected).abs() < 1e-14);
1219    }
1220
1221    #[test]
1222    fn lambdify_pi_e() {
1223        let mut a = Arena::new();
1224        let pi = a.pi;
1225        let f = compile(&mut a, pi, &[]).unwrap();
1226        assert!((f(&[]) - std::f64::consts::PI).abs() < 1e-10);
1227        let e = a.e_const;
1228        let f2 = compile(&mut a, e, &[]).unwrap();
1229        assert!((f2(&[]) - std::f64::consts::E).abs() < 1e-10);
1230    }
1231
1232    #[test]
1233    fn lambdify_complex_fails() {
1234        let mut a = Arena::new();
1235        let i = a.i_unit;
1236        let result = compile(&mut a, i, &[]);
1237        assert!(matches!(result, Err(SymplexError::NotImplemented(_))));
1238    }
1239
1240    #[test]
1241    fn lambdify_free_symbol_is_error() {
1242        let mut a = Arena::new();
1243        let x = a.symbol("x");
1244        let y = a.symbol("y");
1245        let expr = a.add(&[x, y]);
1246        match compile(&mut a, expr, &["x"]) {
1247            Err(SymplexError::FreeSymbol { name }) => assert_eq!(name, "y"),
1248            other => panic!("expected FreeSymbol, got {other:?}"),
1249        }
1250    }
1251
1252    #[test]
1253    fn arity_mismatch_is_nan_or_error() {
1254        let mut a = Arena::new();
1255        let x = a.symbol("x");
1256        let f = compile(&mut a, x, &["x"]).unwrap();
1257        assert!(f.call(&[]).is_nan());
1258        assert!(f(&[1.0, 2.0]).is_nan());
1259        assert!(f.try_call(&[]).is_err());
1260        assert_eq!(f.try_call(&[7.0]).unwrap(), 7.0);
1261    }
1262
1263    #[test]
1264    fn duplicate_param_is_error() {
1265        let mut a = Arena::new();
1266        let x = a.symbol("x");
1267        assert!(matches!(
1268            compile(&mut a, x, &["x", "x"]),
1269            Err(SymplexError::InvalidArgument { .. })
1270        ));
1271    }
1272
1273    #[test]
1274    fn division_peephole() {
1275        let mut a = Arena::new();
1276        let x = a.symbol("x");
1277        let y = a.symbol("y");
1278        let m1 = a.int(-1);
1279        let inv_y = a.pow(y, m1);
1280        let expr = a.mul(&[x, inv_y]);
1281        let f = compile(&mut a, expr, &["x", "y"]).unwrap();
1282        assert_eq!(f(&[1.0, 3.0]), 1.0 / 3.0);
1283    }
1284
1285    #[test]
1286    fn piecewise_and_booleans() {
1287        let mut a = Arena::new();
1288        let x = a.symbol("x");
1289        let zero = a.int(0);
1290        let one = a.int(1);
1291        let two = a.int(2);
1292        let gt = a.gt(x, zero);
1293        let neg_x = a.neg(x);
1294        let eq1 = a.eq_(x, one);
1295        let t = a.bool_true;
1296        // piecewise: x==1 → 2 ; x>0 → x ; else → -x
1297        let pw = a.piecewise(&[(two, eq1), (x, gt), (neg_x, t)]);
1298        let f = compile(&mut a, pw, &["x"]).unwrap();
1299        assert_eq!(f(&[1.0]), 2.0);
1300        assert_eq!(f(&[3.0]), 3.0);
1301        assert_eq!(f(&[-4.0]), 4.0);
1302        // no matching branch → NaN
1303        let pw2 = a.piecewise(&[(x, gt)]);
1304        let g = compile(&mut a, pw2, &["x"]).unwrap();
1305        assert_eq!(g(&[2.0]), 2.0);
1306        assert!(g(&[-2.0]).is_nan());
1307        // nested boolean logic
1308        let five = a.int(5);
1309        let lt5 = a.gt(five, x);
1310        let both = a.and(&[gt, lt5]);
1311        let not_both = a.not(both);
1312        let pw3 = a.piecewise(&[(one, not_both), (zero, t)]);
1313        let h = compile(&mut a, pw3, &["x"]).unwrap();
1314        assert_eq!(h(&[3.0]), 0.0);
1315        assert_eq!(h(&[7.0]), 1.0);
1316        assert_eq!(h(&[-1.0]), 1.0);
1317    }
1318
1319    #[test]
1320    fn special_functions_compile() {
1321        let mut a = Arena::new();
1322        let x = a.symbol("x");
1323        let g = a.gamma(x);
1324        let f = compile(&mut a, g, &["x"]).unwrap();
1325        assert!(close(f(&[0.5]), std::f64::consts::PI.sqrt(), 1e-15));
1326        let e = a.erf(x);
1327        let f = compile(&mut a, e, &["x"]).unwrap();
1328        assert!(close(f(&[1.0]), 0.84270079294971486934, 1e-15));
1329        let w = a.lambertw(x);
1330        let f = compile(&mut a, w, &["x"]).unwrap();
1331        assert!(close(f(&[1.0]), 0.56714329040978387300, 1e-15));
1332        let two = a.int(2);
1333        let j2 = a.besselj(two, x);
1334        let f = compile(&mut a, j2, &["x"]).unwrap();
1335        assert!(close(f(&[10.0]), 0.25463031368512062253, 1e-14));
1336        let sym_order = a.symbol("n");
1337        let jn = a.besselj(sym_order, x);
1338        assert!(matches!(
1339            compile(&mut a, jn, &["x", "n"]),
1340            Err(SymplexError::NotImplemented(_))
1341        ));
1342        let fac = a.factorial(x);
1343        let f = compile(&mut a, fac, &["x"]).unwrap();
1344        assert_eq!(f(&[5.0]), 120.0);
1345        let fib = a.fibonacci(x);
1346        let f = compile(&mut a, fib, &["x"]).unwrap();
1347        assert_eq!(f(&[20.0]), 6765.0);
1348    }
1349
1350    #[test]
1351    fn compile_many_shares_cse() {
1352        let mut a = Arena::new();
1353        let x = a.symbol("x");
1354        let s = a.sin(x);
1355        let c = a.cos(x);
1356        let two = a.int(2);
1357        let s2 = a.pow(s, two);
1358        let e1 = a.add(&[s2, c]);
1359        let e2 = a.mul(&[s, c]);
1360        let f = compile_many(&mut a, &[e1, e2], &["x"]).unwrap();
1361        assert_eq!(f.len(), 2);
1362        assert_eq!(f.arity(), 1);
1363        let out = f.call_vec(&[0.3]);
1364        let (s, c) = (0.3f64.sin(), 0.3f64.cos());
1365        assert!(close(out[0], s * s + c, 1e-15));
1366        assert!(close(out[1], s * c, 1e-15));
1367        let mut buf = [0.0; 1];
1368        f.call(&[0.3], &mut buf);
1369        assert!(buf[0].is_nan());
1370        assert!(f.try_call(&[0.3], &mut buf).is_err());
1371        let empty = compile_many(&mut a, &[], &["x"]).unwrap();
1372        assert!(empty.is_empty());
1373        assert_eq!(empty.call_vec(&[1.0]).len(), 0);
1374        let empty2 = compile_many_empty(&["x", "y"]).unwrap();
1375        assert_eq!(empty2.arity(), 2);
1376        assert!(empty2.is_empty());
1377    }
1378
1379    #[test]
1380    fn deep_expression_does_not_overflow_stack() {
1381        let mut a = Arena::new();
1382        let x = a.symbol("x");
1383        let mut e = x;
1384        for _ in 0..20_000 {
1385            e = a.sin(e);
1386        }
1387        let f = compile(&mut a, e, &["x"]).unwrap();
1388        assert!(f(&[1.0]).is_finite());
1389    }
1390}