Reject @ as binary operator (#4021)

`@` should not be legal to use as a binary operator. I accepted it in the parser because it occurred in the .enso sources, but it was actually used to create a syntax error to test error recovery.

See: https://www.pivotaltracker.com/story/show/184054024
This commit is contained in:
Kaz Wesley 2023-01-19 12:31:14 -08:00 committed by GitHub
parent f39070abef
commit 591cacb79a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 40 additions and 47 deletions

View File

@ -3360,7 +3360,7 @@ class RuntimeServerTest
"""from Standard.Base import all
|
|main =
| x = Panic.catch_primitive @ .convert_to_dataflow_error
| x = Panic.catch_primitive ` .convert_to_dataflow_error
| IO.println x
| IO.println (x.catch Any .to_text)
|""".stripMargin.linesIterator.mkString("\n")
@ -3402,7 +3402,7 @@ class RuntimeServerTest
contextId,
Seq(
Api.ExecutionResult.Diagnostic.error(
"Unrecognized token.",
"Unexpected expression.",
Some(mainFile),
Some(model.Range(model.Position(3, 30), model.Position(3, 31)))
)
@ -3412,8 +3412,8 @@ class RuntimeServerTest
context.executionComplete(contextId)
)
context.consumeOut shouldEqual List(
"(Error: (Syntax_Error.Error 'Unrecognized token.'))",
"(Syntax_Error.Error 'Unrecognized token.')"
"(Error: (Syntax_Error.Error 'Unexpected expression.'))",
"(Syntax_Error.Error 'Unexpected expression.')"
)
}

View File

@ -713,13 +713,6 @@ final class TreeToIr {
default -> {
var lhs = unnamedCallArgument(app.getLhs());
var rhs = unnamedCallArgument(app.getRhs());
if ("@".equals(op.codeRepr()) && lhs.value() instanceof IR$Application$Prefix fn) {
final Option<IdentifiedLocation> where = getIdentifiedLocation(op);
var err = translateSyntaxError(where.get(), IR$Error$Syntax$UnrecognizedToken$.MODULE$);
var errArg = new IR$CallArgument$Specified(Option.empty(), err, where, meta(), diag());
var args = cons(rhs, cons(errArg, fn.arguments()));
yield new IR$Application$Prefix(fn.function(), args.reverse(), false, getIdentifiedLocation(app), meta(), diag());
}
var name = new IR$Name$Literal(
op.codeRepr(), true, getIdentifiedLocation(op), meta(), diag()
);

View File

@ -582,12 +582,11 @@ public class EnsoCompilerTest {
}
@Test
public void testEmptyGroup2AndAtSymbol() throws Exception {
public void testEmptyGroup2() throws Exception {
parseTest("""
main =
x = ()
x = 5
y = @
""");
}
@ -601,15 +600,6 @@ public class EnsoCompilerTest {
""");
}
@Test
public void testNotAnOperator() throws Exception {
parseTest("""
main =
x = Panic.catch_primitive @ caught_panic-> caught_panic.payload
x.to_text
""");
}
@Test
public void testWildcardLeftHandSide() throws Exception {
parseTest("""

View File

@ -31,10 +31,10 @@ class CompileDiagnosticsTest extends InterpreterTest {
|import Standard.Base.Panic.Panic
|
|main =
| x = Panic.catch_primitive @ caught_panic-> caught_panic.payload
| x = Panic.catch_primitive ` caught_panic-> caught_panic.payload
| x.to_text
|""".stripMargin
eval(code) shouldEqual "(Syntax_Error.Error 'Unrecognized token.')"
eval(code) shouldEqual "(Syntax_Error.Error 'Unexpected expression.')"
}
"surface redefinition errors in the language" in {

View File

@ -198,14 +198,14 @@ class DataflowErrorsTest extends InterpreterTest {
"""from Standard.Base import all
|
|main =
| x = Panic.catch_primitive @ .convert_to_dataflow_error
| x = Panic.catch_primitive ` .convert_to_dataflow_error
| IO.println x
| IO.println (x.catch Any .to_text)
|""".stripMargin
eval(code)
consumeOut shouldEqual List(
"(Error: (Syntax_Error.Error 'Unrecognized token.'))",
"(Syntax_Error.Error 'Unrecognized token.')"
"(Error: (Syntax_Error.Error 'Unexpected expression.'))",
"(Syntax_Error.Error 'Unexpected expression.')"
)
}
}

View File

@ -23,7 +23,7 @@ class StrictCompileDiagnosticsTest extends InterpreterTest {
"""main =
| x = ()
| x = 5
| y = @
| y = `
|""".stripMargin.linesIterator.mkString("\n")
the[InterpreterException] thrownBy eval(code) should have message
"Compilation aborted due to errors."
@ -35,7 +35,7 @@ class StrictCompileDiagnosticsTest extends InterpreterTest {
.toSet shouldEqual Set(
"Test[2:9-2:10]: Parentheses can't be empty.",
"Test[3:5-3:9]: Variable x is being redefined.",
"Test[4:9-4:9]: Unrecognized token.",
"Test[4:9-4:9]: Unexpected expression.",
"Test[4:5-4:5]: Unused variable y.",
"Test[2:5-2:5]: Unused variable x."
)

View File

@ -688,20 +688,12 @@ fn unevaluated_argument() {
#[test]
fn unary_operator_missing_operand() {
let code = ["main ~ = x"];
let expected = block![
(Function (Ident main) #((() (UnaryOprApp "~" ()) () ())) "=" (Ident x))
];
test(&code.join("\n"), expected);
test_invalid("main ~ = x");
}
#[test]
fn unary_operator_at_end_of_expression() {
let code = ["foo ~"];
let expected = block![
(App (Ident foo) (UnaryOprApp "~" ()))
];
test(&code.join("\n"), expected);
test_invalid("foo ~");
}
#[test]
@ -1227,8 +1219,8 @@ fn trailing_whitespace() {
#[test]
fn at_operator() {
test!("foo@bar", (OprApp (Ident foo) (Ok "@") (Ident bar)));
test!("foo @ bar", (OprApp (Ident foo) (Ok "@") (Ident bar)));
test_invalid("foo@bar");
test_invalid("foo @ bar");
}
#[test]

View File

@ -661,7 +661,6 @@ fn analyze_operator(token: &str) -> token::OperatorProperties {
"@" =>
return operator
.with_unary_prefix_mode(token::Precedence::max())
.with_binary_infix_precedence(20)
.as_compile_time_operation()
.as_annotation(),
"-" =>

View File

@ -209,10 +209,16 @@ impl<'s> ExpressionBuilder<'s> {
&mut self,
prec: token::Precedence,
assoc: token::Associativity,
arity: Unary<'s>,
mut arity: Unary<'s>,
) {
if self.prev_type == Some(ItemType::Ast) {
self.application();
if self.nospace {
if let Unary::Simple(token) = arity {
let error = "Space required between term and unary-operator expression.".into();
arity = Unary::Invalid { token, error };
}
}
}
self.push_operator(prec, assoc, Arity::Unary(arity));
}
@ -277,6 +283,8 @@ impl<'s> ExpressionBuilder<'s> {
let ast = match opr.opr {
Arity::Unary(Unary::Simple(opr)) =>
Operand::from(rhs_).map(|item| syntax::tree::apply_unary_operator(opr, item)),
Arity::Unary(Unary::Invalid { token, error }) => Operand::from(rhs_)
.map(|item| syntax::tree::apply_unary_operator(token, item).with_error(error)),
Arity::Unary(Unary::Fragment { mut fragment }) => {
if let Some(rhs_) = rhs_ {
fragment.operand(rhs_);
@ -331,6 +339,7 @@ impl<'s> ExpressionBuilder<'s> {
if child.output.is_empty() && let Some(op) = child.operator_stack.pop() {
match op.opr {
Arity::Unary(Unary::Simple(un)) => self.operator(un),
Arity::Unary(Unary::Invalid{ .. }) => unreachable!(),
Arity::Unary(Unary::Fragment{ .. }) => unreachable!(),
Arity::Binary { tokens, .. } => tokens.into_iter().for_each(|op| self.operator(op)),
};
@ -401,6 +410,7 @@ impl<'s> Arity<'s> {
#[derive(Debug, PartialEq, Eq)]
enum Unary<'s> {
Simple(token::Operator<'s>),
Invalid { token: token::Operator<'s>, error: Cow<'static, str> },
Fragment { fragment: ExpressionBuilder<'s> },
}

View File

@ -863,6 +863,11 @@ pub fn apply_operator<'s>(
}
};
}
if let Ok(opr_) = &opr && !opr_.properties.can_form_section() && lhs.is_none() && rhs.is_none() {
let error = format!("Operator `{opr:?}` must be applied to two operands.");
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()
&& let Some(lhs) = lhs.as_mut()
@ -898,13 +903,17 @@ pub fn apply_operator<'s>(
pub fn apply_unary_operator<'s>(opr: token::Operator<'s>, rhs: Option<Tree<'s>>) -> Tree<'s> {
if opr.properties.is_annotation()
&& let Some(Tree { variant: box Variant::Ident(Ident { token }), .. }) = rhs {
match token.is_type {
return match token.is_type {
true => Tree::annotated_builtin(opr, token, vec![], None),
false => Tree::annotated(opr, token, None, vec![], None),
}
} else {
Tree::unary_opr_app(opr, rhs)
};
}
if !opr.properties.can_form_section() && rhs.is_none() {
let error = format!("Operator `{opr:?}` must be applied to an operand.");
let invalid = Tree::unary_opr_app(opr, rhs);
return invalid.with_error(error);
}
Tree::unary_opr_app(opr, rhs)
}
/// Create an AST node for a token.