diff --git a/app/gui/language/parser/src/lib.rs b/app/gui/language/parser/src/lib.rs index bc503db03ec..230a12b608e 100644 --- a/app/gui/language/parser/src/lib.rs +++ b/app/gui/language/parser/src/lib.rs @@ -5,6 +5,7 @@ #![feature(extend_one)] #![feature(let_chains)] #![feature(if_let_guard)] +#![feature(assert_matches)] // === Standard Linter Configuration === #![deny(non_ascii_idents)] #![warn(unsafe_code)] @@ -160,6 +161,7 @@ impl Parser { mod tests { use super::*; use ast::HasRepr; + use std::assert_matches::assert_matches; #[test] fn test_group_repr() { @@ -220,4 +222,10 @@ main = let ast = Parser::new().parse_line_ast("a->4").unwrap(); assert!(ast::macros::as_lambda(&ast).is_some(), "{ast:?}"); } + + #[test] + fn test_negative_number() { + let ast = Parser::new().parse_line_ast("-23").unwrap(); + assert_matches!(ast.shape(), ast::Shape::Number(_)); + } } diff --git a/app/gui/language/parser/src/translation.rs b/app/gui/language/parser/src/translation.rs index 5b11e50acf7..9847124501b 100644 --- a/app/gui/language/parser/src/translation.rs +++ b/app/gui/language/parser/src/translation.rs @@ -174,8 +174,12 @@ impl Translate { if let Some(arg) = rhs { let non_block_operand = "Unary operator cannot be applied to an (empty) block."; let arg = self.translate(arg).expect(non_block_operand); - let section = section_right(opr, arg).expect_unspaced(); - self.finish_ast(section, builder) + let value = match arg.body.shape() { + ast::Shape::Number(ast::Number { base, int }) => + (ast::Number { base: base.clone(), int: format!("-{int}") }).into(), + _ => prefix(opr, arg).expect_unspaced(), + }; + self.finish_ast(value, builder) } else { let opr = opr.expect_unspaced(); self.finish_ast(ast::SectionSides { opr }, builder) diff --git a/engine/runtime/src/main/java/org/enso/compiler/TreeToIr.java b/engine/runtime/src/main/java/org/enso/compiler/TreeToIr.java index ee28a5d39f3..6a8e580ac87 100644 --- a/engine/runtime/src/main/java/org/enso/compiler/TreeToIr.java +++ b/engine/runtime/src/main/java/org/enso/compiler/TreeToIr.java @@ -936,9 +936,7 @@ final class TreeToIr { case Tree.OprSectionBoundary bound -> translateExpression(bound.getAst(), false); case Tree.UnaryOprApp un when "-".equals(un.getOpr().codeRepr()) -> switch (translateExpression(un.getRhs(), false)) { - // AstToIr doesn't construct negative floating-point literals. - // Match that behavior for testing during the transition. - case IR$Literal$Number n when !n.copy$default$2().contains(".") -> n.copy( + case IR$Literal$Number n -> n.copy( n.copy$default$1(), "-" + n.copy$default$2(), n.copy$default$3(), diff --git a/engine/runtime/src/test/java/org/enso/compiler/EnsoCompilerTest.java b/engine/runtime/src/test/java/org/enso/compiler/EnsoCompilerTest.java index cf95714bc68..1990f368a28 100644 --- a/engine/runtime/src/test/java/org/enso/compiler/EnsoCompilerTest.java +++ b/engine/runtime/src/test/java/org/enso/compiler/EnsoCompilerTest.java @@ -1208,6 +1208,11 @@ public class EnsoCompilerTest { """); } + @Test + public void testDotPrecedence() throws Exception { + equivalenceTest("x = -1.up_to 100", "x = (-1).up_to 100"); + } + @Test public void testFreeze() throws Exception { equivalenceTest("a = x", "a = FREEZE x"); diff --git a/lib/rust/parser/debug/tests/parse.rs b/lib/rust/parser/debug/tests/parse.rs index 1bc04ea797a..4a22c36cc50 100644 --- a/lib/rust/parser/debug/tests/parse.rs +++ b/lib/rust/parser/debug/tests/parse.rs @@ -748,8 +748,25 @@ fn minus_unary() { test!("x=-x", (Assignment (Ident x) "=" (UnaryOprApp "-" (Ident x)))); test!("-x+x", (OprApp (UnaryOprApp "-" (Ident x)) (Ok "+") (Ident x))); test!("-x*x", (OprApp (UnaryOprApp "-" (Ident x)) (Ok "*") (Ident x))); +} + +#[test] +fn minus_unary_decimal() { test!("-2.1", (UnaryOprApp "-" (Number () "2" ("." "1")))); - //test!("-1.x", (OprApp (UnaryOprApp "-" (Number () "1" ())) (Ok ".") (Ident x))); +} + +#[test] +fn minus_unary_in_method_app() { + test!("-1.x", (OprApp (UnaryOprApp "-" (Number () "1" ())) (Ok ".") (Ident x))); + test!("-1.up_to 100", + (App (OprApp (UnaryOprApp "-" (Number () "1" ())) (Ok ".") (Ident up_to)) + (Number () "100" ()))); +} + +#[test] +fn method_app_in_minus_unary() { + test!("-Number.positive_infinity", + (UnaryOprApp "-" (OprApp (Ident Number) (Ok ".") (Ident positive_infinity)))); } @@ -1197,6 +1214,7 @@ fn numbers() { test!("0o122137", (Number "0o" "122137" ())); test!("0xAE2F14", (Number "0x" "AE2F14" ())); test!("pi = 3.14", (Assignment (Ident pi) "=" (Number () "3" ("." "14")))); + test!("0.0.x", (OprApp (Number () "0" ("." "0")) (Ok ".") (Ident x))); } #[test] @@ -1211,6 +1229,7 @@ fn new_delimited_numbers() { #[test] fn old_nondecimal_numbers() { test!("2_01101101", (Number "2_" "01101101" ())); + test!("-2_01101101", (UnaryOprApp "-" (Number "2_" "01101101" ()))); test!("16_17ffffffffffffffa", (Number "16_" "17ffffffffffffffa" ())); } diff --git a/lib/rust/parser/src/lexer.rs b/lib/rust/parser/src/lexer.rs index 00c8b9ae1b4..b53a87d16b5 100644 --- a/lib/rust/parser/src/lexer.rs +++ b/lib/rust/parser/src/lexer.rs @@ -619,22 +619,48 @@ impl<'s> Lexer<'s> { } }); if let Some(token) = token { - if token.code == "+-" { - let (left, right) = token.split_at_(Bytes(1)); - let lhs = analyze_operator(&left.code); - self.submit_token(left.with_variant(token::Variant::operator(lhs))); - let rhs = analyze_operator(&right.code); - self.submit_token(right.with_variant(token::Variant::operator(rhs))); - return; + match token.code.as_ref() { + // Special-case: Split into multiple operators. + "+-" => { + let (left, right) = token.split_at_(Bytes(1)); + let lhs = analyze_operator(&left.code); + self.submit_token(left.with_variant(token::Variant::operator(lhs))); + let rhs = analyze_operator(&right.code); + self.submit_token(right.with_variant(token::Variant::operator(rhs))); + } + // Composed of operator characters, but not an operator node. + "..." => { + let token = token.with_variant(token::Variant::auto_scope()); + self.submit_token(token); + } + // Decimal vs. method-application must be distinguished before parsing because they + // have different precedences; this is a special case here because the distinction + // requires lookahead. + "." if self.last_spaces_visible_offset.width_in_spaces == 0 + && let Some(char) = self.current_char && char.is_ascii_digit() => { + let opr = token::OperatorProperties::new() + .with_binary_infix_precedence(81) + .as_decimal(); + let token = token.with_variant(token::Variant::operator(opr)); + self.submit_token(token); + } + // The unary-negation operator binds tighter to numeric literals than other + // expressions. + "-" if self.last_spaces_visible_offset.width_in_spaces == 0 + && let Some(char) = self.current_char && char.is_ascii_digit() => { + let opr = token::OperatorProperties::new() + .with_unary_prefix_mode(token::Precedence::unary_minus_numeric_literal()) + .with_binary_infix_precedence(15); + let token = token.with_variant(token::Variant::operator(opr)); + self.submit_token(token); + } + // Normally-structured operator. + _ => { + let tp = token::Variant::operator(analyze_operator(&token.code)); + let token = token.with_variant(tp); + self.submit_token(token); + } } - if token.code == "..." { - let token = token.with_variant(token::Variant::auto_scope()); - self.submit_token(token); - return; - } - let tp = token::Variant::operator(analyze_operator(&token.code)); - let token = token.with_variant(tp); - self.submit_token(token); } } } @@ -703,8 +729,7 @@ fn analyze_operator(token: &str) -> token::OperatorProperties { .as_compile_time_operation() .as_special() .as_sequence(), - "." => - return operator.with_binary_infix_precedence(80).with_decimal_interpretation().as_dot(), + "." => return operator.with_binary_infix_precedence(80).as_dot(), _ => (), } // "The precedence of all other operators is determined by the operator's Precedence Character:" @@ -821,9 +846,13 @@ impl<'s> Lexer<'s> { token::Base::Hexadecimal => self.token(|this| this.take_while(is_hexadecimal_digit)), }; - if let Some(token) = token { - self.submit_token(token.with_variant(token::Variant::digits(Some(base)))); - } + let joiner = token::OperatorProperties::new() + .with_binary_infix_precedence(usize::MAX) + .as_token_joiner(); + self.submit_token(Token("", "", token::Variant::operator(joiner))); + // Every number has a digits-token, even if it's zero-length. + let token = token.unwrap_or_default(); + self.submit_token(token.with_variant(token::Variant::digits(Some(base)))); } else { self.submit_token(token.with_variant(token::Variant::digits(None))); } diff --git a/lib/rust/parser/src/syntax/operator.rs b/lib/rust/parser/src/syntax/operator.rs index 4850b310c79..c686d5c6c8b 100644 --- a/lib/rust/parser/src/syntax/operator.rs +++ b/lib/rust/parser/src/syntax/operator.rs @@ -299,7 +299,7 @@ impl<'s> ExpressionBuilder<'s> { SectionTermination::Unwrap => lhs.map(|op| op.value), }; let rhs = rhs_.map(syntax::Tree::from); - let ast = syntax::tree::apply_operator(lhs, tokens, rhs, self.nospace); + let ast = syntax::tree::apply_operator(lhs, tokens, rhs); Operand::from(ast) } else { let rhs = rhs_.map(syntax::Tree::from); @@ -307,9 +307,8 @@ impl<'s> ExpressionBuilder<'s> { if tokens.len() != 1 || tokens[0].properties.can_form_section() { elided += lhs.is_none() as u32 + rhs.is_none() as u32; } - let mut operand = Operand::from(lhs).map(|lhs| { - syntax::tree::apply_operator(lhs, tokens, rhs, self.nospace) - }); + let mut operand = Operand::from(lhs) + .map(|lhs| syntax::tree::apply_operator(lhs, tokens, rhs)); operand.elided += elided; operand } diff --git a/lib/rust/parser/src/syntax/token.rs b/lib/rust/parser/src/syntax/token.rs index b044fc37c9d..ff090d7b5f4 100644 --- a/lib/rust/parser/src/syntax/token.rs +++ b/lib/rust/parser/src/syntax/token.rs @@ -334,7 +334,7 @@ pub struct OperatorProperties { is_compile_time_operation: bool, is_right_associative: bool, // Unique operators - can_be_decimal_operator: bool, + is_decimal: bool, is_type_annotation: bool, is_assignment: bool, is_arrow: bool, @@ -343,6 +343,7 @@ pub struct OperatorProperties { is_annotation: bool, is_dot: bool, is_special: bool, + is_token_joiner: bool, } impl OperatorProperties { @@ -379,6 +380,11 @@ impl OperatorProperties { Self { is_special: true, ..self } } + /// Return a copy of this operator, modified to be flagged as the token-joiner operator. + pub fn as_token_joiner(self) -> Self { + Self { is_token_joiner: true, ..self } + } + /// Return a copy of this operator, modified to have the specified LHS operator-section/ /// template-function behavior. pub fn with_lhs_section_termination(self, lhs_section_termination: T) -> Self @@ -421,9 +427,9 @@ impl OperatorProperties { Self { is_dot: true, ..self } } - /// Return a copy of this operator, modified to allow an interpretion as a decmial point. - pub fn with_decimal_interpretation(self) -> Self { - Self { can_be_decimal_operator: true, ..self } + /// Return a copy of this operator, modified to be interpreted as a decimal point. + pub fn as_decimal(self) -> Self { + Self { is_decimal: true, ..self } } /// Return this operator's binary infix precedence, if it has one. @@ -486,6 +492,11 @@ impl OperatorProperties { self.is_dot } + /// Return whether this operator is the token-joiner operator. + pub fn is_token_joiner(&self) -> bool { + self.is_token_joiner + } + /// Return this operator's associativity. pub fn associativity(&self) -> Associativity { match self.is_right_associative { @@ -494,9 +505,9 @@ impl OperatorProperties { } } - /// Return whether this operator can be interpreted as a decimal point. - pub fn can_be_decimal_operator(&self) -> bool { - self.can_be_decimal_operator + /// Return whether this operator is a decimal point. + pub fn is_decimal(&self) -> bool { + self.is_decimal } } @@ -533,6 +544,11 @@ impl Precedence { pub fn unary_minus() -> Self { Precedence { value: 79 } } + + /// Return the precedence of unary minus when applied to a numeric literal. + pub fn unary_minus_numeric_literal() -> Self { + Precedence { value: 80 } + } } /// Associativity (left or right). diff --git a/lib/rust/parser/src/syntax/tree.rs b/lib/rust/parser/src/syntax/tree.rs index 33aa1864167..001bd55e2cd 100644 --- a/lib/rust/parser/src/syntax/tree.rs +++ b/lib/rust/parser/src/syntax/tree.rs @@ -762,12 +762,6 @@ impl<'s> span::Builder<'s> for OperatorDelimitedTree<'s> { /// application has special semantics. pub fn apply<'s>(mut func: Tree<'s>, mut arg: Tree<'s>) -> Tree<'s> { match (&mut *func.variant, &mut *arg.variant) { - (Variant::Number(func_ @ Number { base: _, integer: None, fractional_digits: None }), - Variant::Number(Number { base: None, integer, fractional_digits })) => { - func_.integer = mem::take(integer); - func_.fractional_digits = mem::take(fractional_digits); - func - } (Variant::Annotated(func_ @ Annotated { argument: None, .. }), _) => { func_.argument = maybe_apply(mem::take(&mut func_.argument), arg).into(); func @@ -862,13 +856,29 @@ pub fn apply_operator<'s>( mut lhs: Option>, opr: Vec>, mut rhs: Option>, - nospace: bool, ) -> Tree<'s> { let opr = match opr.len() { 0 => return apply(lhs.unwrap(), rhs.unwrap()), 1 => Ok(opr.into_iter().next().unwrap()), _ => Err(MultipleOperatorError { operators: NonEmptyVec::try_from(opr).unwrap() }), }; + if let Ok(opr_) = &opr + && opr_.properties.is_token_joiner() + && let Some(lhs_) = lhs.as_mut() + && let Some(rhs_) = rhs.as_mut() { + return match (&mut *lhs_.variant, &mut *rhs_.variant) { + (Variant::Number(func_ @ Number { base: _, integer: None, fractional_digits: None }), + Variant::Number(Number { base: None, integer, fractional_digits })) => { + func_.integer = mem::take(integer); + func_.fractional_digits = mem::take(fractional_digits); + lhs.take().unwrap() + } + _ => { + debug_assert!(false, "Unexpected use of token-joiner operator!"); + apply(lhs.take().unwrap(), rhs.take().unwrap()) + }, + } + } if let Ok(opr_) = &opr && opr_.properties.is_special() { let tree = Tree::opr_app(lhs, opr, rhs); return tree.with_error("Invalid use of special operator."); @@ -890,8 +900,7 @@ pub fn apply_operator<'s>( let invalid = Tree::opr_app(lhs, opr, rhs); return invalid.with_error(error); } - if nospace - && let Ok(opr) = &opr && opr.properties.can_be_decimal_operator() + if let Ok(opr) = &opr && opr.properties.is_decimal() && let Some(lhs) = lhs.as_mut() && let box Variant::Number(lhs_) = &mut lhs.variant && lhs_.fractional_digits.is_none()