diff --git a/libraries/math-parser/src/lexer.rs b/libraries/math-parser/src/lexer.rs index 2715a622b9..544ab3cfb9 100644 --- a/libraries/math-parser/src/lexer.rs +++ b/libraries/math-parser/src/lexer.rs @@ -186,8 +186,22 @@ impl<'a> Lexer<'a> { (digits, value) } - // A numeric literal cannot follow another operand across whitespace (`10 000`, `sqrt(4).5`), only constants/calls/parens may juxtapose - fn juxtaposes_with_preceding_operand(&self, literal_start: usize) -> bool { + // Two number literals never juxtapose, so digit grouping like `10 000` can't silently multiply + fn follows_number_literal(&self, literal_start: usize) -> bool { + let preceding = self.input[..literal_start].trim_end(); + + // The preceding token begins within its run of name and number characters (`2pi`, `1e5`), whose start is a token boundary to lex from + let run_start = preceding + .char_indices() + .rev() + .take_while(|&(_, c)| unicode_ident::is_xid_continue(c) || c == '.') + .last() + .map_or(preceding.len(), |(index, _)| index); + Lexer::new(&preceding[run_start..]).last().is_some_and(|token| matches!(token, Token::Float(_))) + } + + // A `.`-led literal can't follow an operand (`sqrt(4).5`), which must write its leading zero instead + fn follows_operand(&self, literal_start: usize) -> bool { let mut preceding = self.input[..literal_start].trim_end(); // A `!` run is postfix factorial only when an operand precedes it, otherwise it's a prefix logical not @@ -226,7 +240,8 @@ impl<'a> Lexer<'a> { } // A numeric literal cannot be glued directly to another by a stray decimal point or digit (e.g. `1..5`, `1.5.5`), so reject rather than letting it parse as implicit multiplication - if !got_digit || self.peek().is_some_and(|c| c == '.' || c.is_ascii_digit()) || self.juxtaposes_with_preceding_operand(start_pos) { + let leading_dot = self.input[start_pos..].starts_with('.'); + if !got_digit || self.peek().is_some_and(|c| c == '.' || c.is_ascii_digit()) || self.follows_number_literal(start_pos) || (leading_dot && self.follows_operand(start_pos)) { self.pos = start_pos; return None; } diff --git a/libraries/math-parser/src/lib.rs b/libraries/math-parser/src/lib.rs index 3ddc274e4d..70365db27b 100644 --- a/libraries/math-parser/src/lib.rs +++ b/libraries/math-parser/src/lib.rs @@ -50,15 +50,15 @@ mod tests { #[test] fn juxtaposed_numbers_fail_to_parse() { - // Adjacent number literals like digit-grouped `10 000` must not silently multiply - for input in ["2 3", "10 000", "1 .5", "sqrt(4).5", "2 3 + 1"] { + // Adjacent number literals like digit-grouped `10 000` must not silently multiply, and a `.`-led literal after any operand needs its leading zero + for input in ["2 3", "10 000", "1 .5", "2 3 + 1", "1e5 3", "2. 3", "sqrt(4).5", "sqrt(4) .5", "pi.5", "5!.5"] { assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error"); } } #[test] fn dot_led_function_suffixes_fail_to_parse() { - // A `.`-led base suffix must stay an error, keeping dot-after-identifier free for possible future accessor syntax (the supported spelling is `log0.5`) + // A `.`-led base suffix is an error (the supported spelling is `log0.5`) for input in ["log.5(8)", "log.5", "root.5(9)", "log_.5(8)"] { assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error"); } @@ -194,6 +194,13 @@ mod tests { implicit_multiplication_power_operand: "2pi^2" => 2. * std::f64::consts::PI.powi(2), implicit_multiplication_function: "2sqrt(4)" => 4., implicit_multiplication_excludes_unary_minus: "2 -3" => -1., + implicit_multiplication_trailing_number: "pi 2" => 2. * std::f64::consts::PI, + implicit_multiplication_trailing_number_after_call: "sqrt(4) 3" => 6., + implicit_multiplication_trailing_number_unspaced: "sqrt(4)3" => 6., + implicit_multiplication_trailing_leading_zero: "sqrt(4) 0.5" => 1., + implicit_multiplication_trailing_number_after_name_run: "2pi 3" => 6. * std::f64::consts::PI, + implicit_multiplication_trailing_number_after_factorial: "3! 2" => 12., + implicit_multiplication_trailing_number_after_infinity: "∞ 2" => f64::INFINITY, // Factorial (postfix !) factorial_simple: "5!" => 120., @@ -448,9 +455,9 @@ mod tests { assert!(ast::Node::try_parse_from_str(&input).is_err(), "expected `{input}` to be a parse error"); } - // A name ending in a combining mark is still an operand, so a spaced number after it doesn't silently multiply + // A name ending in a combining mark is an operand like any other, so a spaced number after it multiplies for input in ["x 2".to_string(), format!("{decomposed_e_acute} 2")] { - assert!(ast::Node::try_parse_from_str(&input).is_err(), "expected `{input}` to be a parse error"); + assert!(ast::Node::try_parse_from_str(&input).is_ok(), "expected `{input}` to parse"); } } diff --git a/libraries/math-parser/src/parser.rs b/libraries/math-parser/src/parser.rs index 825e2742b8..c70365eb0b 100644 --- a/libraries/math-parser/src/parser.rs +++ b/libraries/math-parser/src/parser.rs @@ -115,7 +115,7 @@ where let unary = unary_op.clone().repeated().foldr(pow.clone(), |op, expr| Node::UnaryOp { op, expr: Box::new(expr) }); // Juxtaposed factors like `2pi` or `2sqrt(4)` multiply implicitly at the same precedence as `*` and `/`. - // The implicit operand is a `pow`, not a full unary, so `2 -3` stays a subtraction; the lexer rejects a bare number as the right operand (`10 000` is not `10*000`). + // The implicit operand is a `pow`, not a full unary, so `2 -3` stays a subtraction; the lexer rejects a number right after another number (`10 000` is not `10*000`). let implicit_mul = pow.map(|rhs| (BinaryOp::Mul, rhs)); let product = unary.clone().foldl(choice((mul_op.then(unary), implicit_mul)).repeated(), |lhs, (op, rhs)| Node::BinOp { lhs: Box::new(lhs),