diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 5708c57867..2d298132f9 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -3431,10 +3431,12 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> TaggedValue::String({ let mut expression = x.value.trim().to_string(); - if ["+", "-", "*", "/", "^", "%"].iter().any(|&infix| infix == expression) { + if ["+", "-", "*", "/", "^"].iter().any(|&infix| infix == expression) { expression = format!("A {expression} B"); } else if expression == "^" { expression = String::from("A^B"); + } else if expression == "%" || expression == "mod" { + expression = String::from("mod(a, b)"); } expression diff --git a/libraries/math-parser/src/ast.rs b/libraries/math-parser/src/ast.rs index 51dd09d51b..a63c60493a 100644 --- a/libraries/math-parser/src/ast.rs +++ b/libraries/math-parser/src/ast.rs @@ -22,7 +22,6 @@ pub enum BinaryOp { Div, /// Logical OR over operands that must each be exactly 0 or 1, returning 0 or 1. Or, - Modulo, Pow, Leq, Lt, diff --git a/libraries/math-parser/src/constants.rs b/libraries/math-parser/src/constants.rs index 9ddb5ec59c..ca058ac435 100644 --- a/libraries/math-parser/src/constants.rs +++ b/libraries/math-parser/src/constants.rs @@ -400,6 +400,15 @@ pub fn builtin_function(name: &str) -> Option { _ => None, }, + "mod" => |values| match values { + [Value::Number(Number::Real(x)), Value::Number(Number::Real(modulus))] => { + // Floored, so a truncated remainder with the opposite sign from the modulus moves over by one modulus + let remainder = x % modulus; + Some(Value::from_f64(if remainder != 0. && (remainder < 0.) != (*modulus < 0.) { remainder + modulus } else { remainder })) + } + _ => None, + }, + "gcd" => |values| { let reduced = real_operands(values)? .into_iter() diff --git a/libraries/math-parser/src/lexer.rs b/libraries/math-parser/src/lexer.rs index 4d8d1c60ae..b3736f2f25 100644 --- a/libraries/math-parser/src/lexer.rs +++ b/libraries/math-parser/src/lexer.rs @@ -24,7 +24,8 @@ pub enum Token<'src> { Comma, Plus, Minus, - Modulo, + /// Reserved for percentages, so the parser never matches it and its error points a C-style remainder to `mod(a, b)`. + Percent, Star, Slash, Caret, @@ -59,7 +60,7 @@ impl<'src> fmt::Display for Token<'src> { Token::Comma => f.write_str(","), Token::Plus => f.write_str("+"), Token::Minus => f.write_str("-"), - Token::Modulo => f.write_str("%"), + Token::Percent => f.write_str("%"), Token::Star => f.write_str("*"), Token::Slash => f.write_str("/"), Token::Caret => f.write_str("^"), @@ -375,7 +376,7 @@ impl<'a> Lexer<'a> { '+' => Plus, '-' => Minus, '*' => Star, - '%' => Modulo, + '%' => Percent, '/' => Slash, '^' => Caret, '≠' => Neq, diff --git a/libraries/math-parser/src/lib.rs b/libraries/math-parser/src/lib.rs index 32d4f9652f..edcaf15878 100644 --- a/libraries/math-parser/src/lib.rs +++ b/libraries/math-parser/src/lib.rs @@ -40,6 +40,15 @@ mod tests { } } + #[test] + fn percent_is_not_the_remainder() { + // `%` is reserved for percentages, so a C-style remainder is a parse error naming the function that computes it + for input in ["5 % 3", "x % 2", "%", "50%"] { + let error = evaluate(input).unwrap_err().to_string(); + assert!(error.contains("`mod(a, b)`"), "`{input}` gave the error `{error}`"); + } + } + #[test] fn not_sign_is_prefix_only() { // `¬` spells only the prefix logical not, so it must not stand in for `!` in its postfix factorial role @@ -136,6 +145,7 @@ mod tests { "sin(inf)", "gcd(inf, 6)", "gcd(10000000000000000000, 2)", + "mod(5, 0)", "(-1)!", "2.5!", "i!", @@ -359,10 +369,13 @@ mod tests { infix_subtraction: "5 - 3" => 2., infix_multiplication: "4 * 4" => 16., infix_division: "8/2" => 4., - modulo_pos_pos: "3.2 % 2" => 1.2, - modulo_pos_neg: "3.2 % -2" => 1.2, - modulo_neg_neg: "(-3.2) % -2" => -1.2, - modulo_neg_pos: "(-3.2) % 2" => -1.2, + modulo_pos_pos: "mod(3.2, 2)" => 1.2, + modulo_pos_neg: "mod(3.2, -2)" => -0.8, + modulo_neg_neg: "mod(-3.2, -2)" => -1.2, + modulo_neg_pos: "mod(-3.2, 2)" => 0.8, + modulo_neg_multiple: "mod(-4, 2)" => 0., + modulo_integer_wrap: "mod(-7, 3)" => 2., + modulo_angle_wrap: "mod(-pi/2, tau)" => 1.5 * std::f64::consts::PI, exp_pos_pos: "3.2 ^ 2" => 256. / 25., exp_pos_neg: "3.2 ^ -2" => 25. / 256., exp_neg_neg: "-3.2 ^ -2" => -25. / 256., @@ -686,7 +699,7 @@ mod tests { magnitude_difference_spaced: "|-2| - |-3|" => -1., magnitude_difference_unspaced: "|-2|-|-3|" => -1., magnitude_quotient: "|-2| / |-4|" => 0.5, - magnitude_modulo: "|-7| % |-4|" => 3., + magnitude_modulo: "mod(|-7|, |-4|)" => 3., magnitude_factorial_inside: "|3!|" => 6., magnitude_factorial_then_sum: "|-3|! + 1" => 7., magnitude_less_than_spaced: "|-3| < |-5|" => 1., diff --git a/libraries/math-parser/src/parser.rs b/libraries/math-parser/src/parser.rs index 59ceed6e30..309921a0c2 100644 --- a/libraries/math-parser/src/parser.rs +++ b/libraries/math-parser/src/parser.rs @@ -49,7 +49,15 @@ impl Node { match parser::>>().parse(Lexer::new(src)).into_result() { Ok(ast) => Ok(ast), - Err(parse_errs) => Err(ParseError(parse_errs.into_iter().map(|e| format!("{e} at {}", e.span())).collect())), + Err(parse_errs) => Err(ParseError( + parse_errs + .into_iter() + .map(|e| match e.found() { + Some(Token::Percent) => format!("`%` is reserved for percentages, so the remainder is written `mod(a, b)`, at {}", e.span()), + _ => format!("{e} at {}", e.span()), + }) + .collect(), + )), } } } @@ -92,7 +100,7 @@ where let atom = choice((constant, if_expr, call_or_var, parens, magnitude)).labelled("atom"); let add_op = choice((just(Token::Plus).to(BinaryOp::Add), just(Token::Minus).to(BinaryOp::Sub))); - let mul_op = choice((just(Token::Star).to(BinaryOp::Mul), just(Token::Slash).to(BinaryOp::Div), just(Token::Modulo).to(BinaryOp::Modulo))); + let mul_op = choice((just(Token::Star).to(BinaryOp::Mul), just(Token::Slash).to(BinaryOp::Div))); let pow_op = just(Token::Caret).to(BinaryOp::Pow); let unary_op = choice(( just(Token::Minus).to(UnaryOp::Neg), diff --git a/libraries/math-parser/src/value.rs b/libraries/math-parser/src/value.rs index 0664f5d94c..b600faa555 100644 --- a/libraries/math-parser/src/value.rs +++ b/libraries/math-parser/src/value.rs @@ -155,7 +155,6 @@ impl Number { BinaryOp::Sub => lhs - rhs, BinaryOp::Mul => lhs * rhs, BinaryOp::Div => lhs / rhs, - BinaryOp::Modulo => lhs % rhs, BinaryOp::Pow => { // A negative base under a fractional exponent has no real power, so it climbs to the principal complex one let power = lhs.powf(rhs); @@ -180,7 +179,6 @@ impl Number { BinaryOp::Sub => lhs - rhs, BinaryOp::Mul => lhs * rhs, BinaryOp::Div => lhs / rhs, - BinaryOp::Modulo => lhs % rhs, BinaryOp::Pow => lhs.powc(rhs), BinaryOp::Leq | BinaryOp::Lt | BinaryOp::Geq | BinaryOp::Gt => { return None;