1use 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
48pub const MAX_EXPR_NODES: usize = 512;
52pub const MAX_EXPR_DEPTH: usize = 64;
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub enum Var {
62 ScopeX,
64 ScopeY,
66 ScopeZ,
68 SplitI,
71 SplitN,
74 Depth,
76 Named(String),
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83pub enum UnaryOp {
84 Neg,
86 Not,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92pub enum BinOp {
93 Add,
94 Sub,
95 Mul,
96 Div,
97 Rem,
99 Eq,
100 Ne,
101 Lt,
102 Le,
103 Gt,
104 Ge,
105 And,
107 Or,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113pub enum Func {
114 Rand,
117 Floor,
118 Ceil,
119 Rint,
121 Abs,
122 Sqrt,
123 Pow,
124 Clamp,
126 Min,
127 Max,
128}
129
130impl Func {
131 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub enum Expr {
164 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 pub fn lit(v: f64) -> Self {
175 Expr::Lit(v)
176 }
177
178 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 pub fn as_lit(&self) -> Option<f64> {
193 match self {
194 Expr::Lit(v) => Some(*v),
195 _ => None,
196 }
197 }
198
199 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 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
236pub struct EvalCtx<'a> {
244 pub scope_size: Vec3,
246 pub split_i: f64,
248 pub split_n: f64,
250 pub depth: f64,
252 pub params: &'a [(String, f64)],
255 pub globals: &'a HashMap<String, f64>,
257 pub rng: &'a mut Pcg64,
259}
260
261impl Expr {
262 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 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 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
408impl 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 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
469fn 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
490fn 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 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 if let Ok((rest, v)) = ews(unsigned_double).parse(input) {
518 return Ok((rest, Expr::Lit(v)));
519 }
520 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 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 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 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 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 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 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
682pub 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
692pub 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, ¶ms, &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); 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, ¶ms, &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 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, ¶ms, &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 assert_eq!(eval_str("rand(3, 3)").unwrap(), 3.0);
833 }
834
835 #[test]
836 fn short_circuit_skips_rhs_rand_draw() {
837 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, ¶ms, &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 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 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 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}