Fix precedence of -1.x (#5830)

Fixes #5826.

# Important Notes
- Change frontend representation of negation.
- Fix a precedence issue: The `.` operators in -1.x and -1.2 must have different precedences.
- Remove a no-longer-needed special case from backend translation.
- Add tests for this case after all translations.
This commit is contained in:
Kaz Wesley 2023-03-17 11:53:34 -07:00 committed by GitHub
parent c696bf1d87
commit 75fda33837
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 133 additions and 46 deletions

View File

@ -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(_));
}
}

View File

@ -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)

View File

@ -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(),

View File

@ -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");

View File

@ -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" ()));
}

View File

@ -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)));
}

View File

@ -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
}

View File

@ -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<T>(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).

View File

@ -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<Tree<'s>>,
opr: Vec<token::Operator<'s>>,
mut rhs: Option<Tree<'s>>,
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()