Skip to main content

symbios_shape/
expr.rs

1//! Arithmetic / logical expression language for grammar arguments.
2//!
3//! Every numeric argument position in the grammar (extrusion heights, split
4//! sizes, scale factors, roof parameters, …) accepts an expression instead of
5//! a bare literal. Expressions are evaluated at derivation time against the
6//! current shape's [`EvalCtx`] — its scope extents, split position, depth,
7//! bound rule parameters, and grammar attributes — so one rule adapts to
8//! every scope it is applied to.
9//!
10//! ```text
11//! Extrude(rand(8, 14))
12//! Split(Y) { FloorH: Ground | ~1: Upper }          // FloorH = const/attr
13//! Scale(scope.x * 0.5, 1, 1)
14//! when(split.i == 0): CornerBay | else: MidBay
15//! ```
16//!
17//! Design notes:
18//! - **Floats only.** Booleans are CGA-style floats: comparisons yield `1.0` /
19//!   `0.0`, and any non-zero value is truthy. This keeps one value type
20//!   through parameters, attributes, and genetics.
21//! - **Deterministic randomness.** `rand(..)` draws from the per-shape RNG
22//!   stream supplied by the interpreter, so the same seed derives the same
23//!   model. Short-circuit `&&` / `||` skip RHS draws by design — divergence
24//!   is data-driven and reproducible.
25//! - **Hard failure over silent nonsense.** Division by zero, non-finite
26//!   results, unknown identifiers, and bad function domains all return
27//!   [`ShapeError`] rather than propagating NaNs into scope math.
28
29use std::collections::HashMap;
30use std::fmt;
31
32use nom::{
33    IResult, Parser,
34    bytes::complete::tag,
35    character::complete::char as c_char,
36    combinator::verify,
37    error::{Error, ErrorKind},
38    number::complete::double,
39};
40use rand::Rng;
41use rand_pcg::Pcg64;
42use serde::{Deserialize, Serialize};
43
44use crate::error::ShapeError;
45use crate::grammar::{identifier, space_or_comment};
46use crate::scope::Vec3;
47
48/// Maximum number of AST nodes in a single expression (DoS hardening).
49/// Checked after parsing via [`Expr::node_count`]; grammar-level integration
50/// rejects any argument expression exceeding this.
51pub const MAX_EXPR_NODES: usize = 512;
52/// Maximum parenthesis/recursion depth inside one expression. Bounds nom's
53/// recursion during parsing (a `((((…))))` bomb would otherwise overflow the
54/// stack long before node counting runs).
55pub const MAX_EXPR_DEPTH: usize = 64;
56
57// ── AST ──────────────────────────────────────────────────────────────────────
58
59/// Built-in read-only variables.
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub enum Var {
62    /// Current scope extent along local X (`scope.x`).
63    ScopeX,
64    /// Current scope extent along local Y (`scope.y`).
65    ScopeY,
66    /// Current scope extent along local Z (`scope.z`).
67    ScopeZ,
68    /// Zero-based index of this shape within the last `Split` / `Repeat`
69    /// (`split.i`). `0` outside any split.
70    SplitI,
71    /// Total sibling count of the last `Split` / `Repeat` (`split.n`).
72    /// `1` outside any split.
73    SplitN,
74    /// Current derivation depth (`depth`); the root rule runs at `0`.
75    Depth,
76    /// A named binding: rule parameter, grammar attribute, or constant —
77    /// resolved in that order at evaluation time.
78    Named(String),
79}
80
81/// Unary operators.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83pub enum UnaryOp {
84    /// Arithmetic negation `-x`.
85    Neg,
86    /// Logical not `!x` (`1.0` if `x == 0.0`, else `0.0`).
87    Not,
88}
89
90/// Binary operators, lowest section = lowest precedence.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92pub enum BinOp {
93    Add,
94    Sub,
95    Mul,
96    Div,
97    /// Remainder (`%`). Follows Rust `f64::rem` semantics (sign of dividend).
98    Rem,
99    Eq,
100    Ne,
101    Lt,
102    Le,
103    Gt,
104    Ge,
105    /// Logical and — short-circuits: RHS is not evaluated when LHS is falsy.
106    And,
107    /// Logical or — short-circuits: RHS is not evaluated when LHS is truthy.
108    Or,
109}
110
111/// Built-in functions.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113pub enum Func {
114    /// `rand()` → `[0,1)`, `rand(max)` → `[0,max)`, `rand(min,max)` → `[min,max)`.
115    /// Draws from the per-shape RNG stream (deterministic per seed).
116    Rand,
117    Floor,
118    Ceil,
119    /// Round half-to-even (banker's rounding), CGA `rint` parity.
120    Rint,
121    Abs,
122    Sqrt,
123    Pow,
124    /// `clamp(v, lo, hi)`; errors when `lo > hi`.
125    Clamp,
126    Min,
127    Max,
128}
129
130impl Func {
131    /// `(name, func, min_arity, max_arity)` table shared by parser and eval.
132    const TABLE: [(&'static str, Func, usize, usize); 10] = [
133        ("rand", Func::Rand, 0, 2),
134        ("floor", Func::Floor, 1, 1),
135        ("ceil", Func::Ceil, 1, 1),
136        ("rint", Func::Rint, 1, 1),
137        ("abs", Func::Abs, 1, 1),
138        ("sqrt", Func::Sqrt, 1, 1),
139        ("pow", Func::Pow, 2, 2),
140        ("clamp", Func::Clamp, 3, 3),
141        ("min", Func::Min, 2, 2),
142        ("max", Func::Max, 2, 2),
143    ];
144
145    fn by_name(name: &str) -> Option<(Func, usize, usize)> {
146        Self::TABLE
147            .iter()
148            .find(|(n, ..)| *n == name)
149            .map(|&(_, f, lo, hi)| (f, lo, hi))
150    }
151
152    fn name(self) -> &'static str {
153        Self::TABLE
154            .iter()
155            .find(|&&(_, f, ..)| f == self)
156            .map(|&(n, ..)| n)
157            .unwrap_or("?")
158    }
159}
160
161/// An argument expression, evaluated per shape at derivation time.
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub enum Expr {
164    /// Literal constant. The only node genetics mutates.
165    Lit(f64),
166    Var(Var),
167    Unary(UnaryOp, Box<Expr>),
168    Binary(BinOp, Box<Expr>, Box<Expr>),
169    Call(Func, Vec<Expr>),
170}
171
172impl Expr {
173    /// Convenience literal constructor.
174    pub fn lit(v: f64) -> Self {
175        Expr::Lit(v)
176    }
177
178    /// Total AST node count (self included). Used for the
179    /// [`MAX_EXPR_NODES`] DoS cap and as a cheap complexity metric.
180    pub fn node_count(&self) -> usize {
181        match self {
182            Expr::Lit(_) | Expr::Var(_) => 1,
183            Expr::Unary(_, e) => 1 + e.node_count(),
184            Expr::Binary(_, a, b) => 1 + a.node_count() + b.node_count(),
185            Expr::Call(_, args) => 1 + args.iter().map(Expr::node_count).sum::<usize>(),
186        }
187    }
188
189    /// `Some(v)` when the expression is a bare literal. Parse-time range
190    /// checks apply only to bare literals; everything else re-validates at
191    /// evaluation time.
192    pub fn as_lit(&self) -> Option<f64> {
193        match self {
194            Expr::Lit(v) => Some(*v),
195            _ => None,
196        }
197    }
198
199    /// Visits every literal leaf mutably — the genetics mutation hook.
200    pub fn visit_literals_mut(&mut self, f: &mut impl FnMut(&mut f64)) {
201        match self {
202            Expr::Lit(v) => f(v),
203            Expr::Var(_) => {}
204            Expr::Unary(_, e) => e.visit_literals_mut(f),
205            Expr::Binary(_, a, b) => {
206                a.visit_literals_mut(f);
207                b.visit_literals_mut(f);
208            }
209            Expr::Call(_, args) => {
210                for a in args {
211                    a.visit_literals_mut(f);
212                }
213            }
214        }
215    }
216
217    /// Structural equality that ignores literal *values* — two expressions
218    /// have the same shape when they differ only in their `Lit` payloads.
219    /// Genetics uses this to decide whether two ops are crossover-compatible.
220    pub fn shape_eq(&self, other: &Expr) -> bool {
221        match (self, other) {
222            (Expr::Lit(_), Expr::Lit(_)) => true,
223            (Expr::Var(a), Expr::Var(b)) => a == b,
224            (Expr::Unary(oa, ea), Expr::Unary(ob, eb)) => oa == ob && ea.shape_eq(eb),
225            (Expr::Binary(oa, la, ra), Expr::Binary(ob, lb, rb)) => {
226                oa == ob && la.shape_eq(lb) && ra.shape_eq(rb)
227            }
228            (Expr::Call(fa, aa), Expr::Call(fb, ab)) => {
229                fa == fb && aa.len() == ab.len() && aa.iter().zip(ab).all(|(x, y)| x.shape_eq(y))
230            }
231            _ => false,
232        }
233    }
234}
235
236// ── Evaluation ───────────────────────────────────────────────────────────────
237
238/// Per-shape evaluation context.
239///
240/// Built by the interpreter for each work item; `params` are the innermost
241/// rule-call arguments (linear scan, shadowing `globals`), `globals` the
242/// merged attribute/constant table (style and host overrides already applied).
243pub struct EvalCtx<'a> {
244    /// Current scope extents (`scope.x` / `scope.y` / `scope.z`).
245    pub scope_size: Vec3,
246    /// Zero-based index within the last split/repeat; `0.0` at the root.
247    pub split_i: f64,
248    /// Sibling count of the last split/repeat; `1.0` at the root.
249    pub split_n: f64,
250    /// Derivation depth of the current rule invocation.
251    pub depth: f64,
252    /// Innermost rule-call parameter bindings; searched *in reverse* so the
253    /// latest binding of a repeated name wins.
254    pub params: &'a [(String, f64)],
255    /// Grammar attributes + constants (merged, overrides applied).
256    pub globals: &'a HashMap<String, f64>,
257    /// Per-shape RNG stream (`rand(..)` draws).
258    pub rng: &'a mut Pcg64,
259}
260
261impl Expr {
262    /// Evaluates the expression. Any non-finite intermediate or final result
263    /// is an error — scope math must stay finite.
264    pub fn eval(&self, ctx: &mut EvalCtx<'_>) -> Result<f64, ShapeError> {
265        let v = self.eval_inner(ctx)?;
266        if !v.is_finite() {
267            return Err(ShapeError::ExprEval(format!(
268                "expression produced a non-finite value: {self}"
269            )));
270        }
271        Ok(v)
272    }
273
274    fn eval_inner(&self, ctx: &mut EvalCtx<'_>) -> Result<f64, ShapeError> {
275        Ok(match self {
276            Expr::Lit(v) => *v,
277            Expr::Var(var) => match var {
278                Var::ScopeX => ctx.scope_size.x,
279                Var::ScopeY => ctx.scope_size.y,
280                Var::ScopeZ => ctx.scope_size.z,
281                Var::SplitI => ctx.split_i,
282                Var::SplitN => ctx.split_n,
283                Var::Depth => ctx.depth,
284                Var::Named(name) => {
285                    if let Some((_, v)) = ctx.params.iter().rev().find(|(n, _)| n == name) {
286                        *v
287                    } else if let Some(v) = ctx.globals.get(name) {
288                        *v
289                    } else {
290                        return Err(ShapeError::UnknownIdentifier(name.clone()));
291                    }
292                }
293            },
294            Expr::Unary(op, e) => {
295                let v = e.eval(ctx)?;
296                match op {
297                    UnaryOp::Neg => -v,
298                    UnaryOp::Not => {
299                        if v == 0.0 {
300                            1.0
301                        } else {
302                            0.0
303                        }
304                    }
305                }
306            }
307            Expr::Binary(op, a, b) => {
308                // Short-circuit logicals first — RHS must not evaluate (and
309                // must not draw from the RNG) when the LHS already decides.
310                match op {
311                    BinOp::And => {
312                        let l = a.eval(ctx)?;
313                        if l == 0.0 {
314                            return Ok(0.0);
315                        }
316                        return Ok(if b.eval(ctx)? != 0.0 { 1.0 } else { 0.0 });
317                    }
318                    BinOp::Or => {
319                        let l = a.eval(ctx)?;
320                        if l != 0.0 {
321                            return Ok(1.0);
322                        }
323                        return Ok(if b.eval(ctx)? != 0.0 { 1.0 } else { 0.0 });
324                    }
325                    _ => {}
326                }
327                let l = a.eval(ctx)?;
328                let r = b.eval(ctx)?;
329                let bool_to_f = |b: bool| if b { 1.0 } else { 0.0 };
330                match op {
331                    BinOp::Add => l + r,
332                    BinOp::Sub => l - r,
333                    BinOp::Mul => l * r,
334                    BinOp::Div => {
335                        if r == 0.0 {
336                            return Err(ShapeError::ExprEval(format!("division by zero: {self}")));
337                        }
338                        l / r
339                    }
340                    BinOp::Rem => {
341                        if r == 0.0 {
342                            return Err(ShapeError::ExprEval(format!("remainder by zero: {self}")));
343                        }
344                        l % r
345                    }
346                    BinOp::Eq => bool_to_f(l == r),
347                    BinOp::Ne => bool_to_f(l != r),
348                    BinOp::Lt => bool_to_f(l < r),
349                    BinOp::Le => bool_to_f(l <= r),
350                    BinOp::Gt => bool_to_f(l > r),
351                    BinOp::Ge => bool_to_f(l >= r),
352                    BinOp::And | BinOp::Or => unreachable!("handled above"),
353                }
354            }
355            Expr::Call(func, args) => {
356                match func {
357                    Func::Rand => {
358                        // Arity fixed at parse time: 0, 1, or 2 args.
359                        let (lo, hi) = match args.len() {
360                            0 => (0.0, 1.0),
361                            1 => (0.0, args[0].eval(ctx)?),
362                            _ => (args[0].eval(ctx)?, args[1].eval(ctx)?),
363                        };
364                        if lo > hi {
365                            return Err(ShapeError::ExprEval(format!(
366                                "rand range is inverted ({lo} > {hi}): {self}"
367                            )));
368                        }
369                        if lo == hi {
370                            lo
371                        } else {
372                            ctx.rng.random::<f64>() * (hi - lo) + lo
373                        }
374                    }
375                    Func::Floor => args[0].eval(ctx)?.floor(),
376                    Func::Ceil => args[0].eval(ctx)?.ceil(),
377                    Func::Rint => args[0].eval(ctx)?.round_ties_even(),
378                    Func::Abs => args[0].eval(ctx)?.abs(),
379                    Func::Sqrt => {
380                        let v = args[0].eval(ctx)?;
381                        if v < 0.0 {
382                            return Err(ShapeError::ExprEval(format!(
383                                "sqrt of negative value {v}: {self}"
384                            )));
385                        }
386                        v.sqrt()
387                    }
388                    Func::Pow => args[0].eval(ctx)?.powf(args[1].eval(ctx)?),
389                    Func::Clamp => {
390                        let v = args[0].eval(ctx)?;
391                        let lo = args[1].eval(ctx)?;
392                        let hi = args[2].eval(ctx)?;
393                        if lo > hi {
394                            return Err(ShapeError::ExprEval(format!(
395                                "clamp bounds are inverted ({lo} > {hi}): {self}"
396                            )));
397                        }
398                        v.clamp(lo, hi)
399                    }
400                    Func::Min => args[0].eval(ctx)?.min(args[1].eval(ctx)?),
401                    Func::Max => args[0].eval(ctx)?.max(args[1].eval(ctx)?),
402                }
403            }
404        })
405    }
406}
407
408// ── Display (diagnostics + round-trip tests) ─────────────────────────────────
409
410impl fmt::Display for Var {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        match self {
413            Var::ScopeX => write!(f, "scope.x"),
414            Var::ScopeY => write!(f, "scope.y"),
415            Var::ScopeZ => write!(f, "scope.z"),
416            Var::SplitI => write!(f, "split.i"),
417            Var::SplitN => write!(f, "split.n"),
418            Var::Depth => write!(f, "depth"),
419            Var::Named(n) => write!(f, "{n}"),
420        }
421    }
422}
423
424impl BinOp {
425    fn symbol(self) -> &'static str {
426        match self {
427            BinOp::Add => "+",
428            BinOp::Sub => "-",
429            BinOp::Mul => "*",
430            BinOp::Div => "/",
431            BinOp::Rem => "%",
432            BinOp::Eq => "==",
433            BinOp::Ne => "!=",
434            BinOp::Lt => "<",
435            BinOp::Le => "<=",
436            BinOp::Gt => ">",
437            BinOp::Ge => ">=",
438            BinOp::And => "&&",
439            BinOp::Or => "||",
440        }
441    }
442}
443
444impl fmt::Display for Expr {
445    /// Renders with explicit parentheses around every compound node —
446    /// unambiguous by construction, at the cost of elegance. Guarantees
447    /// `parse(display(e))` reproduces `e` (asserted in tests).
448    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449        match self {
450            Expr::Lit(v) => write!(f, "{v}"),
451            Expr::Var(v) => write!(f, "{v}"),
452            Expr::Unary(UnaryOp::Neg, e) => write!(f, "(-{e})"),
453            Expr::Unary(UnaryOp::Not, e) => write!(f, "(!{e})"),
454            Expr::Binary(op, a, b) => write!(f, "({a} {} {b})", op.symbol()),
455            Expr::Call(func, args) => {
456                write!(f, "{}(", func.name())?;
457                for (i, a) in args.iter().enumerate() {
458                    if i > 0 {
459                        write!(f, ", ")?;
460                    }
461                    write!(f, "{a}")?;
462                }
463                write!(f, ")")
464            }
465        }
466    }
467}
468
469// ── Parser ───────────────────────────────────────────────────────────────────
470//
471// Precedence (loosest → tightest):
472//   or  :=  and ( "||" and )*
473//   and :=  cmp ( "&&" cmp )*
474//   cmp :=  add ( ("==" | "!=" | "<=" | ">=" | "<" | ">") add )?   — single, non-associative
475//   add :=  mul ( ("+" | "-") mul )*
476//   mul :=  una ( ("*" | "/" | "%") una )*
477//   una :=  "-" una | "!" una | atom
478//   atom := number | call | var | "(" or ")"
479//
480// A comparison chain (`a < b < c`) is rejected: the second `<` is left
481// unconsumed and surfaces as trailing input at the integration site.
482
483fn ews<'a, F, O>(inner: F) -> impl Parser<&'a str, Output = O, Error = Error<&'a str>>
484where
485    F: Parser<&'a str, Output = O, Error = Error<&'a str>>,
486{
487    nom::sequence::delimited(space_or_comment, inner, space_or_comment)
488}
489
490/// A float literal that does NOT consume a leading sign — unary minus is
491/// handled at the `una` level so `Scale(3 - 2, 1, 1)` parses as a binary
492/// subtraction instead of two adjacent literals.
493fn unsigned_double(input: &str) -> IResult<&str, f64> {
494    if input.starts_with('-') || input.starts_with('+') {
495        return Err(nom::Err::Error(Error::new(input, ErrorKind::Digit)));
496    }
497    verify(double, |x: &f64| x.is_finite()).parse(input)
498}
499
500fn depth_guard(input: &str, depth: usize) -> Result<(), nom::Err<Error<&str>>> {
501    if depth > MAX_EXPR_DEPTH {
502        Err(nom::Err::Failure(Error::new(input, ErrorKind::TooLarge)))
503    } else {
504        Ok(())
505    }
506}
507
508fn parse_atom(input: &str, depth: usize) -> IResult<&str, Expr> {
509    depth_guard(input, depth)?;
510    // Parenthesised sub-expression.
511    if let Ok((rest, _)) = ews(c_char::<_, Error<&str>>('(')).parse(input) {
512        let (rest, e) = parse_or(rest, depth + 1)?;
513        let (rest, _) = ews(c_char(')')).parse(rest)?;
514        return Ok((rest, e));
515    }
516    // Number literal (sign handled by `parse_unary`).
517    if let Ok((rest, v)) = ews(unsigned_double).parse(input) {
518        return Ok((rest, Expr::Lit(v)));
519    }
520    // `scope.x` / `split.i` / `depth` / call / named binding.
521    let (rest, name) = ews(identifier).parse(input)?;
522    match name {
523        "scope" | "split" => {
524            let (rest, _) = c_char('.').parse(rest)?;
525            let (rest, field) = identifier.parse(rest)?;
526            let var = match (name, field) {
527                ("scope", "x") => Var::ScopeX,
528                ("scope", "y") => Var::ScopeY,
529                ("scope", "z") => Var::ScopeZ,
530                ("split", "i") => Var::SplitI,
531                ("split", "n") => Var::SplitN,
532                _ => return Err(nom::Err::Failure(Error::new(rest, ErrorKind::Tag))),
533            };
534            Ok((rest, Expr::Var(var)))
535        }
536        "depth" => Ok((rest, Expr::Var(Var::Depth))),
537        _ => {
538            // Function call when a builtin name is followed by `(`.
539            if let Some((func, min_ar, max_ar)) = Func::by_name(name)
540                && let Ok((mut rem, _)) = ews(c_char::<_, Error<&str>>('(')).parse(rest)
541            {
542                let mut args = Vec::new();
543                if let Ok((after, _)) = ews(c_char::<_, Error<&str>>(')')).parse(rem) {
544                    rem = after;
545                } else {
546                    loop {
547                        let (after_arg, arg) = parse_or(rem, depth + 1)?;
548                        args.push(arg);
549                        if args.len() > max_ar {
550                            return Err(nom::Err::Failure(Error::new(
551                                after_arg,
552                                ErrorKind::TooLarge,
553                            )));
554                        }
555                        if let Ok((after, _)) = ews(c_char::<_, Error<&str>>(',')).parse(after_arg)
556                        {
557                            rem = after;
558                            continue;
559                        }
560                        let (after, _) = ews(c_char(')')).parse(after_arg)?;
561                        rem = after;
562                        break;
563                    }
564                }
565                if args.len() < min_ar || args.len() > max_ar {
566                    return Err(nom::Err::Failure(Error::new(rem, ErrorKind::Verify)));
567                }
568                return Ok((rem, Expr::Call(func, args)));
569            }
570            Ok((rest, Expr::Var(Var::Named(name.to_string()))))
571        }
572    }
573}
574
575fn parse_unary(input: &str, depth: usize) -> IResult<&str, Expr> {
576    depth_guard(input, depth)?;
577    if let Ok((rest, _)) = ews(c_char::<_, Error<&str>>('-')).parse(input) {
578        let (rest, e) = parse_unary(rest, depth + 1)?;
579        // Constant-fold negated literals so `-0.2` IS a literal — parse-time
580        // range checks (`as_lit`) and genetics mutation see the signed value.
581        if let Expr::Lit(v) = e {
582            return Ok((rest, Expr::Lit(-v)));
583        }
584        return Ok((rest, Expr::Unary(UnaryOp::Neg, Box::new(e))));
585    }
586    if let Ok((rest, _)) = ews(c_char::<_, Error<&str>>('!')).parse(input) {
587        // `!=` must not be half-eaten as unary-not on the RHS of nothing:
588        // at this position an operator cannot start, so a following `=` is
589        // simply a parse error downstream — no special case needed.
590        let (rest, e) = parse_unary(rest, depth + 1)?;
591        return Ok((rest, Expr::Unary(UnaryOp::Not, Box::new(e))));
592    }
593    parse_atom(input, depth)
594}
595
596fn parse_mul(input: &str, depth: usize) -> IResult<&str, Expr> {
597    let (mut rest, mut acc) = parse_unary(input, depth)?;
598    loop {
599        let op = if let Ok((r, _)) = ews(c_char::<_, Error<&str>>('*')).parse(rest) {
600            (r, BinOp::Mul)
601        } else if let Ok((r, _)) = ews(c_char::<_, Error<&str>>('/')).parse(rest) {
602            // Guard: `//` and `/*` are comments, not division. space_or_comment
603            // inside `ews` already consumed well-formed comments, so a raw `/`
604            // followed by `/` or `*` here is a malformed comment — stop.
605            if r.starts_with('/') || r.starts_with('*') {
606                break;
607            }
608            (r, BinOp::Div)
609        } else if let Ok((r, _)) = ews(c_char::<_, Error<&str>>('%')).parse(rest) {
610            (r, BinOp::Rem)
611        } else {
612            break;
613        };
614        let (r2, rhs) = parse_unary(op.0, depth + 1)?;
615        acc = Expr::Binary(op.1, Box::new(acc), Box::new(rhs));
616        rest = r2;
617    }
618    Ok((rest, acc))
619}
620
621fn parse_add(input: &str, depth: usize) -> IResult<&str, Expr> {
622    let (mut rest, mut acc) = parse_mul(input, depth)?;
623    loop {
624        let op = if let Ok((r, _)) = ews(c_char::<_, Error<&str>>('+')).parse(rest) {
625            (r, BinOp::Add)
626        } else if let Ok((r, _)) = ews(c_char::<_, Error<&str>>('-')).parse(rest) {
627            // `-->` is the rule arrow; never treat it as subtraction. This
628            // matters once guards parse expressions directly after a rule head.
629            if r.starts_with('-') || r.starts_with('>') {
630                break;
631            }
632            (r, BinOp::Sub)
633        } else {
634            break;
635        };
636        let (r2, rhs) = parse_mul(op.0, depth + 1)?;
637        acc = Expr::Binary(op.1, Box::new(acc), Box::new(rhs));
638        rest = r2;
639    }
640    Ok((rest, acc))
641}
642
643fn parse_cmp(input: &str, depth: usize) -> IResult<&str, Expr> {
644    let (rest, lhs) = parse_add(input, depth)?;
645    // Single optional comparison — non-associative by design.
646    for (sym, op) in [
647        ("==", BinOp::Eq),
648        ("!=", BinOp::Ne),
649        ("<=", BinOp::Le),
650        (">=", BinOp::Ge),
651        ("<", BinOp::Lt),
652        (">", BinOp::Gt),
653    ] {
654        if let Ok((r, _)) = ews(tag::<_, _, Error<&str>>(sym)).parse(rest) {
655            let (r2, rhs) = parse_add(r, depth + 1)?;
656            return Ok((r2, Expr::Binary(op, Box::new(lhs), Box::new(rhs))));
657        }
658    }
659    Ok((rest, lhs))
660}
661
662fn parse_and(input: &str, depth: usize) -> IResult<&str, Expr> {
663    let (mut rest, mut acc) = parse_cmp(input, depth)?;
664    while let Ok((r, _)) = ews(tag::<_, _, Error<&str>>("&&")).parse(rest) {
665        let (r2, rhs) = parse_cmp(r, depth + 1)?;
666        acc = Expr::Binary(BinOp::And, Box::new(acc), Box::new(rhs));
667        rest = r2;
668    }
669    Ok((rest, acc))
670}
671
672fn parse_or(input: &str, depth: usize) -> IResult<&str, Expr> {
673    let (mut rest, mut acc) = parse_and(input, depth)?;
674    while let Ok((r, _)) = ews(tag::<_, _, Error<&str>>("||")).parse(rest) {
675        let (r2, rhs) = parse_and(r, depth + 1)?;
676        acc = Expr::Binary(BinOp::Or, Box::new(acc), Box::new(rhs));
677        rest = r2;
678    }
679    Ok((rest, acc))
680}
681
682/// nom-style entry point: parses one expression, leaving trailing input.
683/// Enforces [`MAX_EXPR_NODES`] before returning.
684pub fn parse_expr(input: &str) -> IResult<&str, Expr> {
685    let (rest, e) = parse_or(input, 0)?;
686    if e.node_count() > MAX_EXPR_NODES {
687        return Err(nom::Err::Failure(Error::new(input, ErrorKind::TooLarge)));
688    }
689    Ok((rest, e))
690}
691
692/// Whole-string entry point: the entire input must be one expression.
693pub fn parse_expr_str(input: &str) -> Result<Expr, ShapeError> {
694    let (rest, e) = parse_expr(input).map_err(|e| ShapeError::ParseError(e.to_string()))?;
695    let (rest, _) =
696        space_or_comment::<Error<&str>>(rest).map_err(|e| ShapeError::ParseError(e.to_string()))?;
697    if !rest.is_empty() {
698        return Err(ShapeError::ParseError(format!(
699            "trailing input after expression: {rest:?}"
700        )));
701    }
702    Ok(e)
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708    use rand::SeedableRng;
709
710    fn ctx_fixture<'a>(
711        globals: &'a HashMap<String, f64>,
712        params: &'a [(String, f64)],
713        rng: &'a mut Pcg64,
714    ) -> EvalCtx<'a> {
715        EvalCtx {
716            scope_size: Vec3::new(10.0, 4.0, 8.0),
717            split_i: 2.0,
718            split_n: 5.0,
719            depth: 3.0,
720            params,
721            globals,
722            rng,
723        }
724    }
725
726    fn eval_str(s: &str) -> Result<f64, ShapeError> {
727        let globals = HashMap::from([("FloorH".to_string(), 3.2)]);
728        let params = [("w".to_string(), 1.5)];
729        let mut rng = Pcg64::seed_from_u64(7);
730        let mut ctx = ctx_fixture(&globals, &params, &mut rng);
731        parse_expr_str(s)?.eval(&mut ctx)
732    }
733
734    #[test]
735    fn precedence_and_parens() {
736        assert_eq!(eval_str("1 + 2 * 3").unwrap(), 7.0);
737        assert_eq!(eval_str("(1 + 2) * 3").unwrap(), 9.0);
738        assert_eq!(eval_str("10 - 4 - 3").unwrap(), 3.0); // left assoc
739        assert_eq!(eval_str("7 % 4").unwrap(), 3.0);
740        assert_eq!(eval_str("-2 * 3").unwrap(), -6.0);
741        assert_eq!(eval_str("--2").unwrap(), 2.0);
742    }
743
744    #[test]
745    fn comparisons_and_logic() {
746        assert_eq!(eval_str("3 < 4").unwrap(), 1.0);
747        assert_eq!(eval_str("3 >= 4").unwrap(), 0.0);
748        assert_eq!(eval_str("1 && 0").unwrap(), 0.0);
749        assert_eq!(eval_str("1 || 0").unwrap(), 1.0);
750        assert_eq!(eval_str("!0").unwrap(), 1.0);
751        assert_eq!(eval_str("!3").unwrap(), 0.0);
752        assert_eq!(eval_str("1 + 1 == 2 && 3 > 1").unwrap(), 1.0);
753    }
754
755    #[test]
756    fn chained_comparison_is_rejected() {
757        assert!(matches!(
758            parse_expr_str("1 < 2 < 3"),
759            Err(ShapeError::ParseError(_))
760        ));
761    }
762
763    #[test]
764    fn builtin_vars() {
765        assert_eq!(eval_str("scope.x").unwrap(), 10.0);
766        assert_eq!(eval_str("scope.y + scope.z").unwrap(), 12.0);
767        assert_eq!(eval_str("split.i").unwrap(), 2.0);
768        assert_eq!(eval_str("split.n - 1").unwrap(), 4.0);
769        assert_eq!(eval_str("depth").unwrap(), 3.0);
770        assert_eq!(eval_str("split.i == split.n - 1 - 2").unwrap(), 1.0);
771    }
772
773    #[test]
774    fn named_bindings_param_shadows_global() {
775        assert_eq!(eval_str("FloorH").unwrap(), 3.2);
776        assert_eq!(eval_str("w * 2").unwrap(), 3.0);
777        let globals = HashMap::from([("w".to_string(), 100.0)]);
778        let params = [("w".to_string(), 1.0)];
779        let mut rng = Pcg64::seed_from_u64(1);
780        let mut ctx = ctx_fixture(&globals, &params, &mut rng);
781        assert_eq!(parse_expr_str("w").unwrap().eval(&mut ctx).unwrap(), 1.0);
782    }
783
784    #[test]
785    fn unknown_identifier_errors() {
786        assert!(matches!(
787            eval_str("NoSuchThing"),
788            Err(ShapeError::UnknownIdentifier(n)) if n == "NoSuchThing"
789        ));
790    }
791
792    #[test]
793    fn functions() {
794        assert_eq!(eval_str("floor(3.7)").unwrap(), 3.0);
795        assert_eq!(eval_str("ceil(3.2)").unwrap(), 4.0);
796        assert_eq!(eval_str("abs(-5)").unwrap(), 5.0);
797        assert_eq!(eval_str("sqrt(16)").unwrap(), 4.0);
798        assert_eq!(eval_str("pow(2, 10)").unwrap(), 1024.0);
799        assert_eq!(eval_str("clamp(15, 0, 10)").unwrap(), 10.0);
800        assert_eq!(eval_str("min(3, 4) + max(3, 4)").unwrap(), 7.0);
801        // Banker's rounding: both 2.5 and 3.5 round to even neighbours.
802        assert_eq!(eval_str("rint(2.5)").unwrap(), 2.0);
803        assert_eq!(eval_str("rint(3.5)").unwrap(), 4.0);
804    }
805
806    #[test]
807    fn function_arity_is_enforced() {
808        assert!(parse_expr_str("floor()").is_err());
809        assert!(parse_expr_str("floor(1, 2)").is_err());
810        assert!(parse_expr_str("pow(2)").is_err());
811        assert!(parse_expr_str("rand(1, 2, 3)").is_err());
812        assert!(parse_expr_str("clamp(1, 2)").is_err());
813    }
814
815    #[test]
816    fn rand_is_seed_deterministic_and_in_range() {
817        let expr = parse_expr_str("rand(2, 6)").unwrap();
818        let globals = HashMap::new();
819        let params: [(String, f64); 0] = [];
820        let draw = |seed: u64| {
821            let mut rng = Pcg64::seed_from_u64(seed);
822            let mut ctx = ctx_fixture(&globals, &params, &mut rng);
823            expr.eval(&mut ctx).unwrap()
824        };
825        let a = draw(42);
826        let b = draw(42);
827        let c = draw(43);
828        assert_eq!(a, b, "same seed must reproduce the same value");
829        assert_ne!(a, c, "different seeds should diverge");
830        assert!((2.0..6.0).contains(&a));
831        // Zero-width range degenerates to the bound without an RNG draw.
832        assert_eq!(eval_str("rand(3, 3)").unwrap(), 3.0);
833    }
834
835    #[test]
836    fn short_circuit_skips_rhs_rand_draw() {
837        // `0 && rand()`: the RHS draw must NOT happen — verify by comparing
838        // the RNG position via a subsequent draw.
839        let globals = HashMap::new();
840        let params: [(String, f64); 0] = [];
841        let run = |src: &str| {
842            let mut rng = Pcg64::seed_from_u64(9);
843            let mut ctx = ctx_fixture(&globals, &params, &mut rng);
844            parse_expr_str(src).unwrap().eval(&mut ctx).unwrap();
845            rng.random::<f64>()
846        };
847        let after_short = run("0 && rand()");
848        let after_no_rand = run("0 * 1");
849        let after_draw = run("1 && rand()");
850        assert_eq!(
851            after_short, after_no_rand,
852            "short-circuit must leave the stream untouched"
853        );
854        assert_ne!(
855            after_draw, after_no_rand,
856            "taken RHS must advance the stream"
857        );
858    }
859
860    #[test]
861    fn error_paths() {
862        assert!(matches!(eval_str("1 / 0"), Err(ShapeError::ExprEval(_))));
863        assert!(matches!(eval_str("1 % 0"), Err(ShapeError::ExprEval(_))));
864        assert!(matches!(eval_str("sqrt(-1)"), Err(ShapeError::ExprEval(_))));
865        assert!(matches!(
866            eval_str("clamp(1, 5, 0)"),
867            Err(ShapeError::ExprEval(_))
868        ));
869        assert!(matches!(
870            eval_str("rand(6, 2)"),
871            Err(ShapeError::ExprEval(_))
872        ));
873        // Overflow to infinity is caught, not propagated.
874        assert!(matches!(
875            eval_str("pow(10, 400)"),
876            Err(ShapeError::ExprEval(_))
877        ));
878    }
879
880    #[test]
881    fn comments_inside_expressions() {
882        assert_eq!(eval_str("1 + /* two */ 2").unwrap(), 3.0);
883        assert_eq!(eval_str("scope.x /* width */ * 0.5").unwrap(), 5.0);
884    }
885
886    #[test]
887    fn division_is_not_mistaken_for_comments() {
888        assert_eq!(eval_str("10 / 2").unwrap(), 5.0);
889    }
890
891    #[test]
892    fn depth_cap_rejects_paren_bombs() {
893        let bomb = format!("{}1{}", "(".repeat(200), ")".repeat(200));
894        assert!(parse_expr_str(&bomb).is_err());
895    }
896
897    #[test]
898    fn node_cap_rejects_huge_expressions() {
899        let huge = (0..400).map(|_| "1").collect::<Vec<_>>().join(" + ");
900        assert!(matches!(
901            parse_expr_str(&huge),
902            Err(ShapeError::ParseError(_))
903        ));
904    }
905
906    #[test]
907    fn display_round_trips() {
908        for src in [
909            "1 + 2 * 3",
910            "(scope.x - 1.5) / split.n",
911            "rand(2, 6) + FloorH",
912            "!(a && b) || c > 3",
913            "clamp(scope.y, 0, pow(2, depth))",
914            "-w * -2",
915        ] {
916            let e = parse_expr_str(src).unwrap();
917            let rendered = e.to_string();
918            let reparsed = parse_expr_str(&rendered)
919                .unwrap_or_else(|err| panic!("re-parse of {rendered:?} failed: {err}"));
920            assert_eq!(e, reparsed, "round-trip mismatch for {src:?}");
921        }
922    }
923
924    #[test]
925    fn shape_eq_ignores_literal_values_only() {
926        let a = parse_expr_str("scope.x * 2 + 1").unwrap();
927        let b = parse_expr_str("scope.x * 9 + 7").unwrap();
928        let c = parse_expr_str("scope.y * 2 + 1").unwrap();
929        assert!(a.shape_eq(&b));
930        assert!(!a.shape_eq(&c));
931    }
932
933    #[test]
934    fn visit_literals_mut_reaches_every_leaf() {
935        // `-4` constant-folds to the literal -4.0 (signed value visible).
936        let mut e = parse_expr_str("1 + rand(2, 3) * -4").unwrap();
937        let mut seen = Vec::new();
938        e.visit_literals_mut(&mut |v| {
939            seen.push(*v);
940            *v += 10.0;
941        });
942        seen.sort_by(f64::total_cmp);
943        assert_eq!(seen, vec![-4.0, 1.0, 2.0, 3.0]);
944        let mut seen2 = Vec::new();
945        e.visit_literals_mut(&mut |v| seen2.push(*v));
946        seen2.sort_by(f64::total_cmp);
947        assert_eq!(seen2, vec![6.0, 11.0, 12.0, 13.0]);
948    }
949
950    #[test]
951    fn negative_literals_constant_fold() {
952        assert_eq!(parse_expr_str("-0.2").unwrap(), Expr::Lit(-0.2));
953        assert_eq!(parse_expr_str("-0.2").unwrap().as_lit(), Some(-0.2));
954        // Folding only applies to literals; other operands keep the node.
955        assert!(matches!(
956            parse_expr_str("-scope.x").unwrap(),
957            Expr::Unary(UnaryOp::Neg, _)
958        ));
959    }
960
961    #[test]
962    fn serde_round_trip() {
963        let e = parse_expr_str("clamp(scope.x * w, 0, 10)").unwrap();
964        let json = serde_json::to_string(&e).unwrap();
965        let back: Expr = serde_json::from_str(&json).unwrap();
966        assert_eq!(e, back);
967    }
968}