Skip to main content

qualia_core_db/specialized_libs/
symbolic_algebra.rs

1//! Symbolic algebra — a small computer-algebra system (CAS).
2//!
3//! Expression trees over rationals/reals + variables, with `simplify`,
4//! `differentiate`, numeric `eval`, and symbolic equation solving. This is the
5//! ALGEBRA_MANIFOLD_PLAN.md Phase 3 module and is DELIBERATELY distinct from
6//! `solvers/symbolic_logic` (which is SAT / defeasible LOGIC, not computer algebra).
7//!
8//! The CAS is an authoring / tooling path and may allocate (`Box`, `String`); it must
9//! NOT be used on an NQuin/SlgArena hot path. Results can be bridged back into the
10//! graph via [`expr_citation_hash`] for provenance.
11
12use crate::NQuin;
13use std::collections::HashMap;
14
15/// A symbolic expression over real constants and named variables.
16#[derive(Debug, Clone, PartialEq)]
17pub enum Expr {
18    Const(f64),
19    Var(String),
20    Add(Box<Expr>, Box<Expr>),
21    Sub(Box<Expr>, Box<Expr>),
22    Mul(Box<Expr>, Box<Expr>),
23    Div(Box<Expr>, Box<Expr>),
24    /// Integer power `base^exp`.
25    Pow(Box<Expr>, i32),
26    Neg(Box<Expr>),
27    /// Principal square root.
28    Sqrt(Box<Expr>),
29    /// Natural exponential `e^u`.
30    Exp(Box<Expr>),
31    /// Natural logarithm `ln(u)` (domain `u > 0`).
32    Ln(Box<Expr>),
33    Sin(Box<Expr>),
34    Cos(Box<Expr>),
35    /// Tangent `tan(u)` (undefined where `cos(u) = 0`).
36    Tan(Box<Expr>),
37}
38
39// ── ergonomic constructors ──────────────────────────────────────────────────────
40pub fn c(v: f64) -> Expr {
41    Expr::Const(v)
42}
43pub fn var(name: &str) -> Expr {
44    Expr::Var(name.to_string())
45}
46pub fn add(a: Expr, b: Expr) -> Expr {
47    Expr::Add(Box::new(a), Box::new(b))
48}
49pub fn sub(a: Expr, b: Expr) -> Expr {
50    Expr::Sub(Box::new(a), Box::new(b))
51}
52pub fn mul(a: Expr, b: Expr) -> Expr {
53    Expr::Mul(Box::new(a), Box::new(b))
54}
55pub fn div(a: Expr, b: Expr) -> Expr {
56    Expr::Div(Box::new(a), Box::new(b))
57}
58pub fn pow(a: Expr, e: i32) -> Expr {
59    Expr::Pow(Box::new(a), e)
60}
61pub fn neg(a: Expr) -> Expr {
62    Expr::Neg(Box::new(a))
63}
64pub fn sqrt(a: Expr) -> Expr {
65    Expr::Sqrt(Box::new(a))
66}
67pub fn exp(a: Expr) -> Expr {
68    Expr::Exp(Box::new(a))
69}
70pub fn ln(a: Expr) -> Expr {
71    Expr::Ln(Box::new(a))
72}
73pub fn sin(a: Expr) -> Expr {
74    Expr::Sin(Box::new(a))
75}
76pub fn cos(a: Expr) -> Expr {
77    Expr::Cos(Box::new(a))
78}
79pub fn tan(a: Expr) -> Expr {
80    Expr::Tan(Box::new(a))
81}
82
83impl Expr {
84    /// Numerically evaluate, given variable bindings. Returns `None` if a variable is
85    /// unbound or a non-finite value is produced (e.g. division by zero, √negative).
86    pub fn eval(&self, env: &HashMap<String, f64>) -> Option<f64> {
87        let v = match self {
88            Expr::Const(k) => *k,
89            Expr::Var(name) => *env.get(name)?,
90            Expr::Add(a, b) => a.eval(env)? + b.eval(env)?,
91            Expr::Sub(a, b) => a.eval(env)? - b.eval(env)?,
92            Expr::Mul(a, b) => a.eval(env)? * b.eval(env)?,
93            Expr::Div(a, b) => {
94                let d = b.eval(env)?;
95                if d == 0.0 {
96                    return None;
97                }
98                a.eval(env)? / d
99            }
100            Expr::Pow(a, e) => a.eval(env)?.powi(*e),
101            Expr::Neg(a) => -a.eval(env)?,
102            Expr::Sqrt(a) => {
103                let x = a.eval(env)?;
104                if x < 0.0 {
105                    return None;
106                }
107                x.sqrt()
108            }
109            Expr::Exp(a) => a.eval(env)?.exp(),
110            Expr::Ln(a) => {
111                let x = a.eval(env)?;
112                if x <= 0.0 {
113                    return None;
114                }
115                x.ln()
116            }
117            Expr::Sin(a) => a.eval(env)?.sin(),
118            Expr::Cos(a) => a.eval(env)?.cos(),
119            Expr::Tan(a) => a.eval(env)?.tan(),
120        };
121        if v.is_finite() {
122            Some(v)
123        } else {
124            None
125        }
126    }
127}
128
129/// Symbolic derivative of `expr` with respect to variable `wrt`. The result is NOT
130/// auto-simplified — call [`simplify`] on it for a compact form.
131pub fn differentiate(expr: &Expr, wrt: &str) -> Expr {
132    match expr {
133        Expr::Const(_) => c(0.0),
134        Expr::Var(name) => {
135            if name == wrt {
136                c(1.0)
137            } else {
138                c(0.0)
139            }
140        }
141        Expr::Add(a, b) => add(differentiate(a, wrt), differentiate(b, wrt)),
142        Expr::Sub(a, b) => sub(differentiate(a, wrt), differentiate(b, wrt)),
143        // (f·g)' = f'·g + f·g'
144        Expr::Mul(a, b) => add(
145            mul(differentiate(a, wrt), (**b).clone()),
146            mul((**a).clone(), differentiate(b, wrt)),
147        ),
148        // (f/g)' = (f'·g − f·g') / g²
149        Expr::Div(a, b) => div(
150            sub(
151                mul(differentiate(a, wrt), (**b).clone()),
152                mul((**a).clone(), differentiate(b, wrt)),
153            ),
154            pow((**b).clone(), 2),
155        ),
156        // (fⁿ)' = n·fⁿ⁻¹·f'
157        Expr::Pow(a, e) => mul(
158            mul(c(*e as f64), pow((**a).clone(), e - 1)),
159            differentiate(a, wrt),
160        ),
161        Expr::Neg(a) => neg(differentiate(a, wrt)),
162        // (√f)' = f' / (2·√f)
163        Expr::Sqrt(a) => div(differentiate(a, wrt), mul(c(2.0), sqrt((**a).clone()))),
164        // (e^f)' = e^f · f'
165        Expr::Exp(a) => mul(exp((**a).clone()), differentiate(a, wrt)),
166        // (ln f)' = f' / f
167        Expr::Ln(a) => div(differentiate(a, wrt), (**a).clone()),
168        // (sin f)' = cos(f) · f'
169        Expr::Sin(a) => mul(cos((**a).clone()), differentiate(a, wrt)),
170        // (cos f)' = −sin(f) · f'
171        Expr::Cos(a) => mul(neg(sin((**a).clone())), differentiate(a, wrt)),
172        // (tan f)' = f' / cos²(f)   (sec²)
173        Expr::Tan(a) => div(differentiate(a, wrt), pow(cos((**a).clone()), 2)),
174    }
175}
176
177/// Simplify an expression: constant folding, identity elimination (`x+0`, `x·1`,
178/// `x·0`, `x⁰`, `x¹`, `−(−x)`, `x/1`, `x−x`, `x/x`) and collection of an identical
179/// `x+x → 2·x`. Applied to a fixpoint (bounded).
180pub fn simplify(expr: &Expr) -> Expr {
181    let mut cur = expr.clone();
182    for _ in 0..16 {
183        let next = simplify_once(&cur);
184        if next == cur {
185            break;
186        }
187        cur = next;
188    }
189    cur
190}
191
192fn simplify_once(expr: &Expr) -> Expr {
193    match expr {
194        Expr::Const(_) | Expr::Var(_) => expr.clone(),
195        Expr::Add(a, b) => {
196            let (a, b) = (simplify_once(a), simplify_once(b));
197            match (&a, &b) {
198                (Expr::Const(x), Expr::Const(y)) => c(x + y),
199                (Expr::Const(z), _) if *z == 0.0 => b,
200                (_, Expr::Const(z)) if *z == 0.0 => a,
201                _ if a == b => mul(c(2.0), a), // x + x → 2·x
202                _ => add(a, b),
203            }
204        }
205        Expr::Sub(a, b) => {
206            let (a, b) = (simplify_once(a), simplify_once(b));
207            match (&a, &b) {
208                (Expr::Const(x), Expr::Const(y)) => c(x - y),
209                (_, Expr::Const(z)) if *z == 0.0 => a,
210                _ if a == b => c(0.0), // x − x → 0
211                _ => sub(a, b),
212            }
213        }
214        Expr::Mul(a, b) => {
215            let (a, b) = (simplify_once(a), simplify_once(b));
216            match (&a, &b) {
217                (Expr::Const(x), Expr::Const(y)) => c(x * y),
218                (Expr::Const(z), _) | (_, Expr::Const(z)) if *z == 0.0 => c(0.0),
219                (Expr::Const(o), _) if *o == 1.0 => b,
220                (_, Expr::Const(o)) if *o == 1.0 => a,
221                _ => mul(a, b),
222            }
223        }
224        Expr::Div(a, b) => {
225            let (a, b) = (simplify_once(a), simplify_once(b));
226            match (&a, &b) {
227                (Expr::Const(x), Expr::Const(y)) if *y != 0.0 => c(x / y),
228                (Expr::Const(z), _) if *z == 0.0 => c(0.0),
229                (_, Expr::Const(o)) if *o == 1.0 => a,
230                _ if a == b => c(1.0), // x / x → 1 (assumes x ≠ 0)
231                _ => div(a, b),
232            }
233        }
234        Expr::Pow(a, e) => {
235            let a = simplify_once(a);
236            match (&a, e) {
237                (_, 0) => c(1.0),
238                (_, 1) => a,
239                (Expr::Const(x), _) => c(x.powi(*e)),
240                _ => pow(a, *e),
241            }
242        }
243        Expr::Neg(a) => {
244            let a = simplify_once(a);
245            match &a {
246                Expr::Const(x) => c(-x),
247                Expr::Neg(inner) => (**inner).clone(), // −(−x) → x
248                _ => neg(a),
249            }
250        }
251        Expr::Sqrt(a) => {
252            let a = simplify_once(a);
253            match &a {
254                Expr::Const(x) if *x >= 0.0 => {
255                    let r = x.sqrt();
256                    // fold only when exact (avoids hiding irrationality)
257                    if r.fract() == 0.0 {
258                        c(r)
259                    } else {
260                        sqrt(a)
261                    }
262                }
263                _ => sqrt(a),
264            }
265        }
266        Expr::Exp(a) => {
267            let a = simplify_once(a);
268            match &a {
269                Expr::Const(z) if *z == 0.0 => c(1.0), // e⁰ = 1
270                Expr::Ln(inner) => (**inner).clone(),  // e^{ln u} = u
271                _ => exp(a),
272            }
273        }
274        Expr::Ln(a) => {
275            let a = simplify_once(a);
276            match &a {
277                Expr::Const(o) if *o == 1.0 => c(0.0), // ln 1 = 0
278                Expr::Exp(inner) => (**inner).clone(), // ln(e^u) = u
279                _ => ln(a),
280            }
281        }
282        Expr::Sin(a) => {
283            let a = simplify_once(a);
284            match &a {
285                Expr::Const(z) if *z == 0.0 => c(0.0), // sin 0 = 0
286                _ => sin(a),
287            }
288        }
289        Expr::Cos(a) => {
290            let a = simplify_once(a);
291            match &a {
292                Expr::Const(z) if *z == 0.0 => c(1.0), // cos 0 = 1
293                _ => cos(a),
294            }
295        }
296        Expr::Tan(a) => {
297            let a = simplify_once(a);
298            match &a {
299                Expr::Const(z) if *z == 0.0 => c(0.0), // tan 0 = 0
300                _ => tan(a),
301            }
302        }
303    }
304}
305
306/// Symbolic roots of `a·x² + b·x + c = 0` with real coefficients, as exact `Expr`s
307/// `(-b ± √(b²−4ac)) / (2a)`. Returns the two root expressions (simplified). For
308/// `a = 0` returns the single linear root `-c/b`.
309pub fn solve_quadratic_symbolic(a: f64, b: f64, cc: f64) -> Vec<Expr> {
310    if a == 0.0 {
311        if b == 0.0 {
312            return Vec::new();
313        }
314        return vec![simplify(&div(neg(c(cc)), c(b)))];
315    }
316    let disc = sub(pow(c(b), 2), mul(c(4.0), mul(c(a), c(cc)))); // b² − 4ac
317    let root_plus = div(add(neg(c(b)), sqrt(disc.clone())), mul(c(2.0), c(a)));
318    let root_minus = div(sub(neg(c(b)), sqrt(disc)), mul(c(2.0), c(a)));
319    vec![simplify(&root_plus), simplify(&root_minus)]
320}
321
322/// Distribute every product over sums and expand small positive integer powers, so the
323/// result contains no `Mul`/`Pow` node with an additive child. Semantically equal to the
324/// input (verify by evaluation). Powers above 8 are left unexpanded to bound blow-up.
325pub fn expand(expr: &Expr) -> Expr {
326    match expr {
327        Expr::Const(_) | Expr::Var(_) => expr.clone(),
328        Expr::Add(a, b) => add(expand(a), expand(b)),
329        Expr::Sub(a, b) => sub(expand(a), expand(b)),
330        Expr::Neg(a) => neg(expand(a)),
331        Expr::Sqrt(a) => sqrt(expand(a)),
332        Expr::Exp(a) => exp(expand(a)),
333        Expr::Ln(a) => ln(expand(a)),
334        Expr::Sin(a) => sin(expand(a)),
335        Expr::Cos(a) => cos(expand(a)),
336        Expr::Tan(a) => tan(expand(a)),
337        Expr::Div(a, b) => div(expand(a), expand(b)),
338        Expr::Mul(a, b) => expand_mul(&expand(a), &expand(b)),
339        Expr::Pow(a, e) => expand_pow(&expand(a), *e),
340    }
341}
342
343fn expand_mul(a: &Expr, b: &Expr) -> Expr {
344    match (a, b) {
345        (Expr::Add(a1, a2), _) => add(expand_mul(a1, b), expand_mul(a2, b)),
346        (Expr::Sub(a1, a2), _) => sub(expand_mul(a1, b), expand_mul(a2, b)),
347        (_, Expr::Add(b1, b2)) => add(expand_mul(a, b1), expand_mul(a, b2)),
348        (_, Expr::Sub(b1, b2)) => sub(expand_mul(a, b1), expand_mul(a, b2)),
349        (Expr::Neg(a1), _) => neg(expand_mul(a1, b)),
350        (_, Expr::Neg(b1)) => neg(expand_mul(a, b1)),
351        _ => mul(a.clone(), b.clone()),
352    }
353}
354
355fn expand_pow(base: &Expr, e: i32) -> Expr {
356    if e <= 1 || e > 8 {
357        return pow(base.clone(), e);
358    }
359    let mut acc = base.clone();
360    for _ in 1..e {
361        acc = expand_mul(&acc, base);
362    }
363    acc
364}
365
366/// Factor a real quadratic `a·x² + b·x + c` into `a·(x − r₁)·(x − r₂)` when it has real
367/// roots. Returns `None` when the discriminant is negative (no real factorisation) or
368/// `a = 0`. Root constants are snapped to integers/halves when numerically close, so the
369/// common rational case factors cleanly.
370pub fn factor_quadratic(a: f64, b: f64, cc: f64, varname: &str) -> Option<Expr> {
371    if a == 0.0 {
372        return None;
373    }
374    let disc = b * b - 4.0 * a * cc;
375    if disc < 0.0 {
376        return None;
377    }
378    let sq = disc.sqrt();
379    let clean = |r: f64| {
380        let halves = (r * 2.0).round() / 2.0;
381        if (halves - r).abs() < 1e-9 {
382            halves
383        } else {
384            r
385        }
386    };
387    let r1 = clean((-b + sq) / (2.0 * a));
388    let r2 = clean((-b - sq) / (2.0 * a));
389    let prod = mul(sub(var(varname), c(r1)), sub(var(varname), c(r2)));
390    Some(if (a - 1.0).abs() < 1e-12 {
391        prod
392    } else {
393        mul(c(a), prod)
394    })
395}
396
397/// A stable provenance hash of an expression's canonical form, for citing symbolic
398/// results back into the graph (Phase 3.8 bridge). Two structurally-equal expressions
399/// hash equally; this is `q_hash` over the canonical `Display` string.
400pub fn expr_citation_hash(expr: &Expr) -> u64 {
401    crate::q_hash(&format!("{expr}"))
402}
403
404// ── Expr ↔ NQuin tree encoding (Phase 3.8) ──────────────────────────────────────
405// A symbolic expression is serialised into a post-order `Vec<NQuin>`: each node is one
406// quin that references its children by their index in the vec (the root is the last
407// element). This lets symbolic results be STORED in the graph and CITED, not just hashed.
408//
409// Per-node quin layout (predicate = node-kind tag via q_hash):
410//   const   object = f64 bits
411//   var     object = name packed LE (≤ 8 bytes), metadata = byte length
412//   add/sub/mul/div  object = left child index, context = right child index
413//   pow     object = base child index, metadata = exponent (i32 as u64)
414//   neg/sqrt object = child index
415
416fn name_tag(kind: &str) -> u64 {
417    crate::q_hash(kind)
418}
419
420fn pack_name(name: &str) -> (u64, u64) {
421    let bytes = name.as_bytes();
422    let len = bytes.len().min(8);
423    let mut v = 0u64;
424    for (i, &b) in bytes.iter().take(8).enumerate() {
425        v |= (b as u64) << (i * 8);
426    }
427    (v, len as u64)
428}
429
430fn unpack_name(v: u64, len: u64) -> String {
431    let len = (len as usize).min(8);
432    let mut s = String::with_capacity(len);
433    for i in 0..len {
434        s.push(((v >> (i * 8)) & 0xFF) as u8 as char);
435    }
436    s
437}
438
439fn push_node(
440    out: &mut Vec<NQuin>,
441    predicate: u64,
442    object: u64,
443    context: u64,
444    metadata: u64,
445) -> usize {
446    let idx = out.len();
447    let subject = idx as u64;
448    out.push(NQuin {
449        subject,
450        predicate,
451        object,
452        context,
453        metadata,
454        parity: NQuin::calculate_parity(subject, predicate, object, context, metadata),
455    });
456    idx
457}
458
459fn encode(e: &Expr, out: &mut Vec<NQuin>) -> usize {
460    match e {
461        Expr::Const(k) => push_node(out, name_tag("cas:const"), k.to_bits(), 0, 0),
462        Expr::Var(name) => {
463            let (packed, len) = pack_name(name);
464            push_node(out, name_tag("cas:var"), packed, 0, len)
465        }
466        Expr::Add(a, b) => {
467            let (l, r) = (encode(a, out), encode(b, out));
468            push_node(out, name_tag("cas:add"), l as u64, r as u64, 0)
469        }
470        Expr::Sub(a, b) => {
471            let (l, r) = (encode(a, out), encode(b, out));
472            push_node(out, name_tag("cas:sub"), l as u64, r as u64, 0)
473        }
474        Expr::Mul(a, b) => {
475            let (l, r) = (encode(a, out), encode(b, out));
476            push_node(out, name_tag("cas:mul"), l as u64, r as u64, 0)
477        }
478        Expr::Div(a, b) => {
479            let (l, r) = (encode(a, out), encode(b, out));
480            push_node(out, name_tag("cas:div"), l as u64, r as u64, 0)
481        }
482        Expr::Pow(a, exp) => {
483            let l = encode(a, out);
484            push_node(out, name_tag("cas:pow"), l as u64, 0, (*exp as i64) as u64)
485        }
486        Expr::Neg(a) => {
487            let l = encode(a, out);
488            push_node(out, name_tag("cas:neg"), l as u64, 0, 0)
489        }
490        Expr::Sqrt(a) => {
491            let l = encode(a, out);
492            push_node(out, name_tag("cas:sqrt"), l as u64, 0, 0)
493        }
494        Expr::Exp(a) => {
495            let l = encode(a, out);
496            push_node(out, name_tag("cas:exp"), l as u64, 0, 0)
497        }
498        Expr::Ln(a) => {
499            let l = encode(a, out);
500            push_node(out, name_tag("cas:ln"), l as u64, 0, 0)
501        }
502        Expr::Sin(a) => {
503            let l = encode(a, out);
504            push_node(out, name_tag("cas:sin"), l as u64, 0, 0)
505        }
506        Expr::Cos(a) => {
507            let l = encode(a, out);
508            push_node(out, name_tag("cas:cos"), l as u64, 0, 0)
509        }
510        Expr::Tan(a) => {
511            let l = encode(a, out);
512            push_node(out, name_tag("cas:tan"), l as u64, 0, 0)
513        }
514    }
515}
516
517/// Serialise an expression into a post-order `Vec<NQuin>` (the root is the last element).
518pub fn to_quins(expr: &Expr) -> Vec<NQuin> {
519    let mut out = Vec::new();
520    encode(expr, &mut out);
521    out
522}
523
524fn decode(quins: &[NQuin], idx: usize) -> Result<Expr, String> {
525    let node = quins
526        .get(idx)
527        .ok_or_else(|| format!("child index {idx} out of range"))?;
528    let p = node.predicate;
529    let child = |i: u64| decode(quins, i as usize);
530    if p == name_tag("cas:const") {
531        Ok(c(f64::from_bits(node.object)))
532    } else if p == name_tag("cas:var") {
533        Ok(var(&unpack_name(node.object, node.metadata)))
534    } else if p == name_tag("cas:add") {
535        Ok(add(child(node.object)?, child(node.context)?))
536    } else if p == name_tag("cas:sub") {
537        Ok(sub(child(node.object)?, child(node.context)?))
538    } else if p == name_tag("cas:mul") {
539        Ok(mul(child(node.object)?, child(node.context)?))
540    } else if p == name_tag("cas:div") {
541        Ok(div(child(node.object)?, child(node.context)?))
542    } else if p == name_tag("cas:pow") {
543        Ok(pow(child(node.object)?, node.metadata as i64 as i32))
544    } else if p == name_tag("cas:neg") {
545        Ok(neg(child(node.object)?))
546    } else if p == name_tag("cas:sqrt") {
547        Ok(sqrt(child(node.object)?))
548    } else if p == name_tag("cas:exp") {
549        Ok(exp(child(node.object)?))
550    } else if p == name_tag("cas:ln") {
551        Ok(ln(child(node.object)?))
552    } else if p == name_tag("cas:sin") {
553        Ok(sin(child(node.object)?))
554    } else if p == name_tag("cas:cos") {
555        Ok(cos(child(node.object)?))
556    } else if p == name_tag("cas:tan") {
557        Ok(tan(child(node.object)?))
558    } else {
559        Err(format!("unknown CAS node tag in quin {idx}"))
560    }
561}
562
563/// Reconstruct an expression from a post-order `Vec<NQuin>` produced by [`to_quins`].
564pub fn from_quins(quins: &[NQuin]) -> Result<Expr, String> {
565    if quins.is_empty() {
566        return Err("empty quin sequence".to_string());
567    }
568    decode(quins, quins.len() - 1)
569}
570
571// ── parser: text → Expr (recursive descent) ─────────────────────────────────────
572// Grammar:  expr = term (('+'|'-') term)*
573//           term = factor (('*'|'/') factor)*
574//           factor = unary ('^' integer)?
575//           unary = '-' unary | base
576//           base = number | ident | 'sqrt' '(' expr ')' | '(' expr ')'
577
578/// Parse a textual expression like `"x^3 - 2*x^2 + 5"` or `"sqrt(b^2 - 4*a*c)"` into an
579/// [`Expr`]. Supports `+ - * / ^`, parentheses, `sqrt(...)`, numbers and identifiers.
580pub fn parse(input: &str) -> Result<Expr, String> {
581    let tokens = tokenize(input)?;
582    let mut p = Parser { tokens, pos: 0 };
583    let e = p.parse_expr()?;
584    if p.pos != p.tokens.len() {
585        return Err(format!("unexpected trailing tokens at {}", p.pos));
586    }
587    Ok(e)
588}
589
590#[derive(Debug, Clone, PartialEq)]
591enum Tok {
592    Num(f64),
593    Ident(String),
594    Plus,
595    Minus,
596    Star,
597    Slash,
598    Caret,
599    LParen,
600    RParen,
601}
602
603fn tokenize(s: &str) -> Result<Vec<Tok>, String> {
604    let mut out = Vec::new();
605    let chars: Vec<char> = s.chars().collect();
606    let mut i = 0;
607    while i < chars.len() {
608        let ch = chars[i];
609        match ch {
610            ' ' | '\t' | '\n' | '\r' => i += 1,
611            '+' => {
612                out.push(Tok::Plus);
613                i += 1;
614            }
615            '-' => {
616                out.push(Tok::Minus);
617                i += 1;
618            }
619            '*' => {
620                out.push(Tok::Star);
621                i += 1;
622            }
623            '/' => {
624                out.push(Tok::Slash);
625                i += 1;
626            }
627            '^' => {
628                out.push(Tok::Caret);
629                i += 1;
630            }
631            '(' => {
632                out.push(Tok::LParen);
633                i += 1;
634            }
635            ')' => {
636                out.push(Tok::RParen);
637                i += 1;
638            }
639            c if c.is_ascii_digit() || c == '.' => {
640                let start = i;
641                while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
642                    i += 1;
643                }
644                let num: String = chars[start..i].iter().collect();
645                out.push(Tok::Num(
646                    num.parse().map_err(|_| format!("bad number '{num}'"))?,
647                ));
648            }
649            c if c.is_ascii_alphabetic() || c == '_' => {
650                let start = i;
651                while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
652                    i += 1;
653                }
654                out.push(Tok::Ident(chars[start..i].iter().collect()));
655            }
656            other => return Err(format!("unexpected character '{other}'")),
657        }
658    }
659    Ok(out)
660}
661
662struct Parser {
663    tokens: Vec<Tok>,
664    pos: usize,
665}
666
667impl Parser {
668    fn peek(&self) -> Option<&Tok> {
669        self.tokens.get(self.pos)
670    }
671    fn next(&mut self) -> Option<Tok> {
672        let t = self.tokens.get(self.pos).cloned();
673        if t.is_some() {
674            self.pos += 1;
675        }
676        t
677    }
678
679    fn parse_expr(&mut self) -> Result<Expr, String> {
680        let mut left = self.parse_term()?;
681        while let Some(op) = self.peek() {
682            match op {
683                Tok::Plus => {
684                    self.next();
685                    left = add(left, self.parse_term()?);
686                }
687                Tok::Minus => {
688                    self.next();
689                    left = sub(left, self.parse_term()?);
690                }
691                _ => break,
692            }
693        }
694        Ok(left)
695    }
696
697    fn parse_term(&mut self) -> Result<Expr, String> {
698        let mut left = self.parse_factor()?;
699        while let Some(op) = self.peek() {
700            match op {
701                Tok::Star => {
702                    self.next();
703                    left = mul(left, self.parse_factor()?);
704                }
705                Tok::Slash => {
706                    self.next();
707                    left = div(left, self.parse_factor()?);
708                }
709                _ => break,
710            }
711        }
712        Ok(left)
713    }
714
715    fn parse_factor(&mut self) -> Result<Expr, String> {
716        let base = self.parse_unary()?;
717        if let Some(Tok::Caret) = self.peek() {
718            self.next();
719            // exponent must be an integer literal (optionally negated)
720            let neg_exp = matches!(self.peek(), Some(Tok::Minus));
721            if neg_exp {
722                self.next();
723            }
724            match self.next() {
725                Some(Tok::Num(n)) if n.fract() == 0.0 => {
726                    let e = n as i32 * if neg_exp { -1 } else { 1 };
727                    Ok(pow(base, e))
728                }
729                _ => Err("'^' requires an integer exponent".to_string()),
730            }
731        } else {
732            Ok(base)
733        }
734    }
735
736    fn parse_unary(&mut self) -> Result<Expr, String> {
737        if let Some(Tok::Minus) = self.peek() {
738            self.next();
739            return Ok(neg(self.parse_unary()?));
740        }
741        self.parse_base()
742    }
743
744    fn parse_base(&mut self) -> Result<Expr, String> {
745        match self.next() {
746            Some(Tok::Num(n)) => Ok(c(n)),
747            Some(Tok::Ident(name)) => {
748                // Unary functions: name '(' expr ')'.
749                let unary: Option<fn(Expr) -> Expr> = match name.as_str() {
750                    "sqrt" => Some(sqrt),
751                    "exp" => Some(exp),
752                    "ln" => Some(ln),
753                    "sin" => Some(sin),
754                    "cos" => Some(cos),
755                    "tan" => Some(tan),
756                    _ => None,
757                };
758                if let Some(ctor) = unary {
759                    self.expect(Tok::LParen)?;
760                    let inner = self.parse_expr()?;
761                    self.expect(Tok::RParen)?;
762                    Ok(ctor(inner))
763                } else {
764                    Ok(var(&name))
765                }
766            }
767            Some(Tok::LParen) => {
768                let inner = self.parse_expr()?;
769                self.expect(Tok::RParen)?;
770                Ok(inner)
771            }
772            other => Err(format!("unexpected token: {other:?}")),
773        }
774    }
775
776    fn expect(&mut self, t: Tok) -> Result<(), String> {
777        if self.next().as_ref() == Some(&t) {
778            Ok(())
779        } else {
780            Err(format!("expected {t:?}"))
781        }
782    }
783}
784
785impl std::fmt::Display for Expr {
786    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
787        match self {
788            Expr::Const(k) => write!(f, "{k}"),
789            Expr::Var(name) => write!(f, "{name}"),
790            Expr::Add(a, b) => write!(f, "({a} + {b})"),
791            Expr::Sub(a, b) => write!(f, "({a} - {b})"),
792            Expr::Mul(a, b) => write!(f, "({a} * {b})"),
793            Expr::Div(a, b) => write!(f, "({a} / {b})"),
794            Expr::Pow(a, e) => write!(f, "({a}^{e})"),
795            Expr::Neg(a) => write!(f, "(-{a})"),
796            Expr::Sqrt(a) => write!(f, "sqrt({a})"),
797            Expr::Exp(a) => write!(f, "exp({a})"),
798            Expr::Ln(a) => write!(f, "ln({a})"),
799            Expr::Sin(a) => write!(f, "sin({a})"),
800            Expr::Cos(a) => write!(f, "cos({a})"),
801            Expr::Tan(a) => write!(f, "tan({a})"),
802        }
803    }
804}
805
806#[cfg(test)]
807mod tests {
808    use super::*;
809
810    fn env1(name: &str, v: f64) -> HashMap<String, f64> {
811        let mut m = HashMap::new();
812        m.insert(name.to_string(), v);
813        m
814    }
815
816    #[test]
817    fn differentiate_matches_finite_difference() {
818        // f(x) = x³ − 2x² + 5  → f'(x) = 3x² − 4x. Check at several points against a
819        // central finite difference of the ORIGINAL expression (strong correctness test).
820        let f = add(sub(pow(var("x"), 3), mul(c(2.0), pow(var("x"), 2))), c(5.0));
821        let df = simplify(&differentiate(&f, "x"));
822        for &x in &[-2.0, -0.5, 1.0, 3.7] {
823            let h = 1e-6;
824            let fd = (f.eval(&env1("x", x + h)).unwrap() - f.eval(&env1("x", x - h)).unwrap())
825                / (2.0 * h);
826            let sym = df.eval(&env1("x", x)).unwrap();
827            assert!((sym - fd).abs() < 1e-4, "x={x}: symbolic {sym} vs fd {fd}");
828        }
829    }
830
831    #[test]
832    fn simplify_identities() {
833        assert_eq!(simplify(&add(var("x"), c(0.0))), var("x"));
834        assert_eq!(simplify(&mul(var("x"), c(1.0))), var("x"));
835        assert_eq!(simplify(&mul(var("x"), c(0.0))), c(0.0));
836        assert_eq!(simplify(&pow(var("x"), 0)), c(1.0));
837        assert_eq!(simplify(&neg(neg(var("x")))), var("x"));
838        assert_eq!(simplify(&sub(var("x"), var("x"))), c(0.0));
839        assert_eq!(simplify(&add(var("x"), var("x"))), mul(c(2.0), var("x")));
840        assert_eq!(simplify(&add(c(2.0), c(3.0))), c(5.0));
841    }
842
843    #[test]
844    fn symbolic_quadratic_agrees_with_numeric() {
845        // x² − 5x + 6 → roots {3, 2}; evaluate the symbolic root expressions.
846        let roots = solve_quadratic_symbolic(1.0, -5.0, 6.0);
847        assert_eq!(roots.len(), 2);
848        let empty = HashMap::new();
849        let mut vals: Vec<f64> = roots.iter().map(|r| r.eval(&empty).unwrap()).collect();
850        vals.sort_by(|a, b| a.partial_cmp(b).unwrap());
851        assert!((vals[0] - 2.0).abs() < 1e-12 && (vals[1] - 3.0).abs() < 1e-12);
852    }
853
854    #[test]
855    fn parse_and_differentiate_roundtrip() {
856        // Parse a textual polynomial, differentiate symbolically, and check the
857        // derivative numerically (d/dx of x^3 - 2x^2 + 5 is 3x^2 - 4x → at x=2: 4).
858        let f = parse("x^3 - 2*x^2 + 5").unwrap();
859        let df = simplify(&differentiate(&f, "x"));
860        assert!((df.eval(&env1("x", 2.0)).unwrap() - 4.0).abs() < 1e-9);
861        // precedence: 2 + 3 * 4 = 14, not 20
862        assert_eq!(
863            parse("2 + 3 * 4").unwrap().eval(&HashMap::new()).unwrap(),
864            14.0
865        );
866        // sqrt + parens
867        let g = parse("sqrt((a + 3))").unwrap();
868        assert!((g.eval(&env1("a", 1.0)).unwrap() - 2.0).abs() < 1e-12);
869        // bad input errors, not panics
870        assert!(parse("2 +* 3").is_err());
871    }
872
873    #[test]
874    fn expand_distributes_and_preserves_value() {
875        // (x + 1)·(x + 2) expands to a sum with no Mul-over-sum; value matches at samples.
876        let e = mul(add(var("x"), c(1.0)), add(var("x"), c(2.0)));
877        let ex = expand(&e);
878        for &x in &[-3.0, 0.0, 2.5, 7.0] {
879            let want = e.eval(&env1("x", x)).unwrap();
880            let got = ex.eval(&env1("x", x)).unwrap();
881            assert!((want - got).abs() < 1e-9, "expand changed value at x={x}");
882        }
883        // (x + 1)^3 expands and still evaluates correctly.
884        let cube = pow(add(var("x"), c(1.0)), 3);
885        let cube_x = expand(&cube);
886        assert!((cube_x.eval(&env1("x", 2.0)).unwrap() - 27.0).abs() < 1e-9);
887    }
888
889    #[test]
890    fn factor_quadratic_inverts_expand() {
891        // x² − 5x + 6 factors to (x−2)(x−3); expanding the factors recovers the value.
892        let f = factor_quadratic(1.0, -5.0, 6.0, "x").unwrap();
893        let original = add(sub(pow(var("x"), 2), mul(c(5.0), var("x"))), c(6.0));
894        for &x in &[-1.0, 0.0, 2.0, 3.0, 5.5] {
895            let a = f.eval(&env1("x", x)).unwrap();
896            let b = original.eval(&env1("x", x)).unwrap();
897            assert!((a - b).abs() < 1e-9, "factored != original at x={x}");
898        }
899        // Negative discriminant → no real factorisation.
900        assert!(factor_quadratic(1.0, 0.0, 1.0, "x").is_none());
901    }
902
903    #[test]
904    fn expr_quin_roundtrip() {
905        // Encode an expression to NQuins and decode it back unchanged.
906        let e = parse("x^2 + 3*x + 2").unwrap();
907        let quins = to_quins(&e);
908        assert!(!quins.is_empty());
909        assert!(
910            quins.iter().all(|q| q.verify_ecc_parity()),
911            "CAS Quins must carry five-field ECC so a v3 volume verify can accept them"
912        );
913        let back = from_quins(&quins).unwrap();
914        assert_eq!(e, back);
915
916        // Multi-char variable names (≤ 8 bytes) and sqrt/neg survive the round-trip.
917        let e2 = sqrt(neg(sub(var("price"), c(4.0))));
918        assert_eq!(from_quins(&to_quins(&e2)).unwrap(), e2);
919    }
920
921    #[test]
922    fn transcendental_diff_eval_parse_and_quins() {
923        // d/dx[sin x] = cos x, d/dx[e^x] = e^x, d/dx[ln x] = 1/x, d/dx[tan x] = 1/cos²x —
924        // checked against a central finite difference of the original (strong test).
925        for f in [
926            sin(var("x")),
927            cos(var("x")),
928            exp(var("x")),
929            ln(var("x")),
930            tan(var("x")),
931        ] {
932            let df = simplify(&differentiate(&f, "x"));
933            for &x in &[0.4, 1.1, 2.3] {
934                let h = 1e-6;
935                let fd = (f.eval(&env1("x", x + h)).unwrap() - f.eval(&env1("x", x - h)).unwrap())
936                    / (2.0 * h);
937                let sym = df.eval(&env1("x", x)).unwrap();
938                assert!(
939                    (sym - fd).abs() < 1e-4,
940                    "{f}: symbolic {sym} vs fd {fd} at x={x}"
941                );
942            }
943        }
944        // Inverse-pair simplifications.
945        assert_eq!(simplify(&ln(exp(var("x")))), var("x"));
946        assert_eq!(simplify(&exp(ln(var("x")))), var("x"));
947        assert_eq!(simplify(&sin(c(0.0))), c(0.0));
948        assert_eq!(simplify(&cos(c(0.0))), c(1.0));
949        // Parser + Display + quin round-trip on a transcendental expression.
950        let e = parse("sin(x) + exp(2*x) - ln(x)").unwrap();
951        assert!(
952            (e.eval(&env1("x", 1.0)).unwrap() - (1.0_f64.sin() + 2.0_f64.exp() - 1.0_f64.ln()))
953                .abs()
954                < 1e-9
955        );
956        assert_eq!(from_quins(&to_quins(&e)).unwrap(), e);
957    }
958
959    #[test]
960    fn citation_hash_is_structural() {
961        let a = add(var("x"), c(1.0));
962        let b = add(var("x"), c(1.0));
963        assert_eq!(expr_citation_hash(&a), expr_citation_hash(&b));
964        assert_ne!(expr_citation_hash(&a), expr_citation_hash(&var("x")));
965    }
966}