From e29ffb2a9c41c6594c455936a49eb38f029631ed Mon Sep 17 00:00:00 2001 From: dkanus Date: Fri, 1 May 2026 18:43:55 +0700 Subject: [PATCH] Improve selectors' diagnostics --- dev_tests/src/verify_expr.rs | 59 +- rottlib/src/ast/expressions.rs | 12 +- .../parse_error_diagnostics/mod.rs | 19 + .../primary_expressions.rs | 11 +- .../selector_expressions.rs | 354 ++++++++ rottlib/src/parser/errors.rs | 15 +- .../src/parser/grammar/expression/block.rs | 7 +- .../src/parser/grammar/expression/pratt.rs | 26 +- .../parser/grammar/expression/primary/new.rs | 26 +- .../parser/grammar/expression/selectors.rs | 330 +++++--- rottlib/src/parser/recovery.rs | 10 +- rottlib/tests/parser_diagnostics/mod.rs | 1 + .../parser_diagnostics/primary_expressions.rs | 28 +- .../selector_expressions.rs | 778 ++++++++++++++++++ 14 files changed, 1477 insertions(+), 199 deletions(-) create mode 100644 rottlib/src/diagnostics/parse_error_diagnostics/selector_expressions.rs create mode 100644 rottlib/tests/parser_diagnostics/selector_expressions.rs diff --git a/dev_tests/src/verify_expr.rs b/dev_tests/src/verify_expr.rs index 978b618..56168e3 100644 --- a/dev_tests/src/verify_expr.rs +++ b/dev_tests/src/verify_expr.rs @@ -16,40 +16,43 @@ use rottlib::parser::Parser; /// Keep these small: the goal is to inspect lexer diagnostics and delimiter /// recovery behavior, not full parser behavior. const TEST_CASES: &[(&str, &str)] = &[ - // P0027: `else` without a matching `if` - // - // `else` cannot start a standalone block item. The parser should report - // P0027 at `else`, then recover by bailing out of the current block. + // P0031 - FunctionCallArgumentMissingComma ( - "files/P0027_01.uc", - "{\n local bool bReady;\n bReady = CheckReady();\n else { StartMatch(); }\n NotifyReady();\n}\n", + "files/P0031_01.uc", + "{\n Func(A B);\n Log(\"after\");\n}\n", ), - - // P0027: `case` outside of a `switch` - // - // `case` is a switch-arm boundary, not a valid statement or expression - // starter in an ordinary block. ( - "files/P0027_02.uc", - "{ local int Count; Count = 3; case 3: Count++; UpdateHud();}", + "files/P0031_02.uc", + "{\n Func\n (A 123);\n Log(\"after\");\n}\n", ), - - // P0027: standalone `until` without a preceding `do` - // - // `until` is only meaningful as the tail of `do ... until`, so it should - // not be accepted as a normal block item. ( - "files/P0027_03.uc", - "{\n local bool bDone;\n bDone = false;\n until (bDone)\n TickWork();\n}\n", + "files/P0031_03.uc", + "{\n Func(\n A\n new SomeClass\n );\n Log(\"after\");\n}\n", ), - - // P0027: preprocessor/exec directive inside a statement block - // - // `#exec` is declaration/top-level-like syntax, not a valid statement or - // expression inside a braced statement block. + // P0032 - FunctionCallMissingClosingParenthesis + ("files/P0032_01.uc", "Func("), + ("files/P0032_02.uc", "Func\n(\n A,"), + ("files/P0032_03.uc", "Func(A,\n B,"), ( - "files/P0027_04.uc", - "{\n local int Count;\n Count = 0;\n #exec TEXTURE IMPORT NAME=Bad FILE=Bad.bmp\n Count++;\n}\n", + "files/P0032_04.uc", + "{\n Func\n (\n A,\n B,\n", + ), + // P0033 - FunctionCallUnexpectedTokenInArgumentList + ( + "files/P0033_01.uc", + "{\n Func(A #, B);\n Log(\"after\");\n}\n", + ), + ( + "files/P0033_02.uc", + "{\n Func\n (A\n :,\n B);\n Log(\"after\");\n}\n", + ), + ( + "files/P0033_03.uc", + "{\n Func(\n A ?\n , B\n );\n Log(\"after\");\n}\n", + ), + ( + "files/P0033_04.uc", + "{\n Func\n (\n A\n #,\n B\n );\n Log(\"after\");\n}\n", ), ]; @@ -135,4 +138,4 @@ fn render_diagnostic( _colors: bool, ) { diag.render(file, file_name.unwrap_or("")); -} \ No newline at end of file +} diff --git a/rottlib/src/ast/expressions.rs b/rottlib/src/ast/expressions.rs index d79cdd7..ff1dfb5 100644 --- a/rottlib/src/ast/expressions.rs +++ b/rottlib/src/ast/expressions.rs @@ -2,11 +2,12 @@ //! //! This module defines ordinary expressions together with expression-shaped //! control-flow and block forms parsed by the language. -use super::{IdentifierToken, InfixOperator, PostfixOperator, PrefixOperator, - QualifiedIdentifierRef, StatementRef, +use super::{ + IdentifierToken, InfixOperator, PostfixOperator, PrefixOperator, QualifiedIdentifierRef, + StatementRef, }; -use crate::lexer::TokenSpan; use crate::arena::ArenaVec; +use crate::lexer::TokenSpan; use super::super::lexer::TokenPosition; @@ -83,7 +84,7 @@ pub enum Expression<'src, 'arena> { /// arguments in syntaxes that allow empty slots. Call { callee: ExpressionRef<'src, 'arena>, - arguments: ArenaVec<'arena, Option>>, + arguments: ArgumentList<'src, 'arena>, }, /// Prefix unary operator application: `op rhs`. PrefixUnary(PrefixOperator, ExpressionRef<'src, 'arena>), @@ -179,6 +180,9 @@ pub enum Expression<'src, 'arena> { Error, } +/// Arguments in any comma-separated list. +pub type ArgumentList<'src, 'arena> = ArenaVec<'arena, OptionalExpression<'src, 'arena>>; + /// Statements contained in a `{ ... }` block. pub type StatementList<'src, 'arena> = ArenaVec<'arena, StatementRef<'src, 'arena>>; diff --git a/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs b/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs index 9e13ecd..32d7537 100644 --- a/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs +++ b/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs @@ -20,6 +20,7 @@ use crate::parser::{ParseError, ParseErrorKind}; mod block_items; mod control_flow_expressions; mod primary_expressions; +mod selector_expressions; #[derive(Clone, Copy)] enum FoundAt<'src> { @@ -76,6 +77,7 @@ pub(crate) fn diagnostic_from_parse_error<'src>( ) -> Diagnostic { use control_flow_expressions::*; use primary_expressions::*; + use selector_expressions::*; match error.kind { // primary_expressions.rs ParseErrorKind::ParenthesizedExpressionInvalidStart => { @@ -146,6 +148,23 @@ pub(crate) fn diagnostic_from_parse_error<'src>( diagnostic_block_missing_closing_brace(error, file) } ParseErrorKind::BlockExpectedItem => diagnostic_block_expected_item(error, file), + // selector_expression.rs + ParseErrorKind::MemberAccessMissingMemberName => { + diagnostic_member_access_missing_member_name(error, file) + } + ParseErrorKind::IndexMissingExpression => diagnostic_index_missing_expression(error, file), + ParseErrorKind::IndexMissingClosingBracket => { + diagnostic_index_missing_closing_bracket(error, file) + } + ParseErrorKind::FunctionCallArgumentMissingComma => { + diagnostic_function_call_argument_missing_comma(error, file) + } + ParseErrorKind::FunctionCallMissingClosingParenthesis => { + diagnostic_function_call_missing_closing_parenthesis(error, file) + } + ParseErrorKind::FunctionCallUnexpectedTokenInArgumentList => { + diagnostic_function_call_unexpected_token_in_argument_list(error, file) + } _ => DiagnosticBuilder::error(format!("error {:?} while parsing", error.kind)) .primary_label(error.covered_span, "happened here") diff --git a/rottlib/src/diagnostics/parse_error_diagnostics/primary_expressions.rs b/rottlib/src/diagnostics/parse_error_diagnostics/primary_expressions.rs index 8c07233..ab4a28d 100644 --- a/rottlib/src/diagnostics/parse_error_diagnostics/primary_expressions.rs +++ b/rottlib/src/diagnostics/parse_error_diagnostics/primary_expressions.rs @@ -167,7 +167,9 @@ pub(super) fn diagnostic_class_type_expected_qualified_type_name<'src>( let qualifier_dot_span = error.related_spans.get("qualifier_dot").copied(); let class_span = error.related_spans.get("class_keyword").copied(); - let (header_text, primary_text) = match found_at(file, error.blame_span.end) { + let blame_pos = error.blame_span.end; + + let (header_text, primary_text) = match found_at(file, blame_pos) { FoundAt::Token(token_text) => ( format!( "expected another type segment after `.`, found `{}`", @@ -188,7 +190,7 @@ pub(super) fn diagnostic_class_type_expected_qualified_type_name<'src>( let mut builder = DiagnosticBuilder::error(header_text); if let Some(dot_span) = qualifier_dot_span { - if !file.same_line(dot_span.start, error.blame_span.end) { + if !file.same_line(dot_span.start, blame_pos) { builder = builder.secondary_label( dot_span, "after this `.`, another type segment was expected", @@ -205,7 +207,10 @@ pub(super) fn diagnostic_class_type_expected_qualified_type_name<'src>( } } - let primary_span = collapse_span_to_end_on_same_line(file, error.blame_span); + let primary_span = TokenSpan { + start: blame_pos, + end: blame_pos, + }; builder .primary_label(primary_span, primary_text) diff --git a/rottlib/src/diagnostics/parse_error_diagnostics/selector_expressions.rs b/rottlib/src/diagnostics/parse_error_diagnostics/selector_expressions.rs new file mode 100644 index 0000000..dd65d9a --- /dev/null +++ b/rottlib/src/diagnostics/parse_error_diagnostics/selector_expressions.rs @@ -0,0 +1,354 @@ +use super::{Diagnostic, DiagnosticBuilder, FoundAt, found_at}; +use crate::lexer::{TokenSpan, TokenizedFile}; +use crate::parser::{ParseError, diagnostic_labels}; + +const PERIOD: &str = "period"; +const LEFT_BRACKET: &str = "left_bracket"; +const CALLEE: &str = "callee"; +const LEFT_PARENTHESIS: &str = "left_parenthesis"; +const PREVIOUS_ARGUMENT: &str = "previous_argument"; +const ARGUMENT: &str = "argument"; + +/// P0028 +pub(super) fn diagnostic_member_access_missing_member_name<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let period_span = error.related_spans.get(PERIOD).copied(); + let blame_span = error.blame_span; + + let period_context_span = match period_span { + Some(period_span) if !file.same_line(period_span.start, blame_span.end) => { + Some(period_span) + } + _ => None, + }; + + let found = found_at(file, blame_span.start); + + let title = match found { + FoundAt::Token(token_text) => { + format!("expected member name after `.`, found `{}`", token_text) + } + FoundAt::EndOfFile => "expected member name after `.`, found end of file".to_string(), + FoundAt::Unknown => "expected member name after `.`".to_string(), + }; + + let primary_text = match found { + FoundAt::Token(token_text) => format!("unexpected `{}`", token_text), + FoundAt::EndOfFile => "reached end of file here".to_string(), + FoundAt::Unknown => "expected member name here".to_string(), + }; + + let mut builder = DiagnosticBuilder::error(title); + + if let Some(period_context_span) = period_context_span { + builder = builder.secondary_label( + period_context_span, + "after this `.`, a member name was expected", + ); + } + + builder + .primary_label(blame_span, primary_text) + .code("P0028") + .build() +} + +/// P0029 +pub(super) fn diagnostic_index_missing_expression<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let left_bracket_span = error + .related_spans + .get(diagnostic_labels::EXPRESSION_EXPECTED_AFTER) + .copied(); + + let blame_span = error.blame_span; + + let left_bracket_context_span = match left_bracket_span { + Some(left_bracket_span) if !file.same_line(left_bracket_span.start, blame_span.end) => { + Some(left_bracket_span) + } + _ => None, + }; + + let primary_span = match left_bracket_context_span { + Some(left_bracket_context_span) => TokenSpan { + start: left_bracket_context_span.start, + end: blame_span.end, + }, + None => blame_span, + }; + + let found = found_at(file, blame_span.start); + + let title = match found { + FoundAt::Token(token_text) => { + format!("expected index expression after `[`, found `{}`", token_text) + } + FoundAt::EndOfFile => "expected index expression after `[`, found end of file".to_string(), + FoundAt::Unknown => "expected index expression after `[`".to_string(), + }; + + let primary_text = match found { + FoundAt::Token("]") => "expected expression before `]`".to_string(), + FoundAt::Token(token_text) => format!("unexpected `{}`", token_text), + FoundAt::EndOfFile => "reached end of file here".to_string(), + FoundAt::Unknown => "expected index expression here".to_string(), + }; + + let mut builder = DiagnosticBuilder::error(title); + + if let Some(left_bracket_context_span) = left_bracket_context_span { + builder = builder.secondary_label( + left_bracket_context_span, + "after this `[`, an index expression was expected", + ); + } + + builder + .primary_label(primary_span, primary_text) + .code("P0029") + .build() +} + +/// P0030 +pub(super) fn diagnostic_index_missing_closing_bracket<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let left_bracket_span = error.related_spans.get(LEFT_BRACKET).copied(); + let blame_span = error.blame_span; + + let left_bracket_context_span = match left_bracket_span { + Some(left_bracket_span) if !file.same_line(left_bracket_span.start, blame_span.end) => { + Some(left_bracket_span) + } + _ => None, + }; + + let primary_span = match left_bracket_context_span { + Some(left_bracket_context_span) => TokenSpan { + start: left_bracket_context_span.start, + end: blame_span.end, + }, + None => blame_span, + }; + + let primary_text = match found_at(file, blame_span.start) { + FoundAt::Token(token_text) => format!("expected `]` before `{}`", token_text), + FoundAt::EndOfFile => "expected `]` before end of file".to_string(), + FoundAt::Unknown => "expected `]` here".to_string(), + }; + + let mut builder = DiagnosticBuilder::error("missing `]` to close index selector"); + + if let Some(left_bracket_context_span) = left_bracket_context_span { + builder = builder.secondary_label(left_bracket_context_span, "index selector starts here"); + } + + builder + .primary_label(primary_span, primary_text) + .code("P0030") + .build() +} + +/// P0031 +pub(super) fn diagnostic_function_call_argument_missing_comma<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let callee_span = error.related_spans.get(CALLEE).copied(); + let left_parenthesis_span = error.related_spans.get(LEFT_PARENTHESIS).copied(); + let previous_argument_span = error.related_spans.get(PREVIOUS_ARGUMENT).copied(); + let blame_span = error.blame_span; + + let argument_list_context_span = match left_parenthesis_span { + Some(left_parenthesis_span) + if !file.same_line(left_parenthesis_span.start, blame_span.end) => + { + Some(left_parenthesis_span) + } + _ => None, + }; + + let callee_context_span = match (callee_span, left_parenthesis_span) { + (Some(callee_span), Some(left_parenthesis_span)) + if !file.same_line(callee_span.end, left_parenthesis_span.start) + && !file.same_line(callee_span.end, blame_span.end) => + { + Some(callee_span) + } + _ => None, + }; + + let primary_text = match found_at(file, blame_span.start) { + FoundAt::Token(token_text) => format!("expected `,` before `{}`", token_text), + FoundAt::EndOfFile => "expected `,` before end of file".to_string(), + FoundAt::Unknown => "expected `,` here".to_string(), + }; + + let mut builder = DiagnosticBuilder::error("missing `,` between function call arguments"); + + if let Some(callee_context_span) = callee_context_span { + builder = builder.secondary_label(callee_context_span, "function called here"); + } + + if let Some(argument_list_context_span) = argument_list_context_span { + builder = builder.secondary_label( + argument_list_context_span, + "function call argument list starts here", + ); + } + + if let Some(previous_argument_span) = previous_argument_span { + builder = builder.secondary_label(previous_argument_span, "previous argument ends here"); + } + + builder + .primary_label(blame_span, primary_text) + .code("P0031") + .build() +} + +/// P0032 +pub(super) fn diagnostic_function_call_missing_closing_parenthesis<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let callee_span = error.related_spans.get(CALLEE).copied(); + let left_parenthesis_span = error.related_spans.get(LEFT_PARENTHESIS).copied(); + let blame_span = error.blame_span; + + let argument_list_context_span = match left_parenthesis_span { + Some(left_parenthesis_span) + if !file.same_line(left_parenthesis_span.start, blame_span.end) => + { + Some(left_parenthesis_span) + } + _ => None, + }; + + let callee_context_span = match (callee_span, left_parenthesis_span) { + (Some(callee_span), Some(left_parenthesis_span)) + if !file.same_line(callee_span.end, left_parenthesis_span.start) + && !file.same_line(callee_span.end, blame_span.end) => + { + Some(callee_span) + } + _ => None, + }; + + let primary_span = match argument_list_context_span { + Some(argument_list_context_span) => TokenSpan { + start: argument_list_context_span.start, + end: blame_span.end, + }, + None => blame_span, + }; + + let primary_text = match found_at(file, blame_span.start) { + FoundAt::Token(token_text) => format!("expected `)` before `{}`", token_text), + FoundAt::EndOfFile => "expected `)` before end of file".to_string(), + FoundAt::Unknown => "expected `)` here".to_string(), + }; + + let mut builder = DiagnosticBuilder::error("missing `)` to close function call argument list"); + + if let Some(callee_context_span) = callee_context_span { + builder = builder.secondary_label(callee_context_span, "function called here"); + } + + if let Some(argument_list_context_span) = argument_list_context_span { + builder = builder.secondary_label( + argument_list_context_span, + "function call argument list starts here", + ); + } + + builder + .primary_label(primary_span, primary_text) + .code("P0032") + .build() +} + +/// P0033 +pub(super) fn diagnostic_function_call_unexpected_token_in_argument_list<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let callee_span = error.related_spans.get(CALLEE).copied(); + let left_parenthesis_span = error.related_spans.get(LEFT_PARENTHESIS).copied(); + let argument_span = error + .related_spans + .get(ARGUMENT) + .or_else(|| error.related_spans.get(PREVIOUS_ARGUMENT)) + .copied(); + let blame_span = error.blame_span; + + let argument_or_blame_span = match argument_span { + Some(argument_span) => argument_span, + None => blame_span, + }; + + let argument_list_context_span = match left_parenthesis_span { + Some(left_parenthesis_span) + if !file.same_line(left_parenthesis_span.start, argument_or_blame_span.end) => + { + Some(left_parenthesis_span) + } + _ => None, + }; + + let callee_context_span = match (callee_span, left_parenthesis_span) { + (Some(callee_span), Some(left_parenthesis_span)) + if !file.same_line(callee_span.end, left_parenthesis_span.start) + && !file.same_line(callee_span.end, argument_or_blame_span.end) => + { + Some(callee_span) + } + _ => None, + }; + + let found = found_at(file, blame_span.start); + + let title = match found { + FoundAt::Token(token_text) => { + format!("expected `,` or `)` after argument, found `{}`", token_text) + } + FoundAt::EndOfFile => { + "expected `,` or `)` after argument, found end of file".to_string() + } + FoundAt::Unknown => "expected `,` or `)` after argument".to_string(), + }; + + let primary_text = match found { + FoundAt::Token(token_text) => format!("unexpected `{}`", token_text), + FoundAt::EndOfFile => "reached end of file here".to_string(), + FoundAt::Unknown => "expected `,` or `)` here".to_string(), + }; + + let mut builder = DiagnosticBuilder::error(title); + + if let Some(callee_context_span) = callee_context_span { + builder = builder.secondary_label(callee_context_span, "function called here"); + } + + if let Some(argument_list_context_span) = argument_list_context_span { + builder = builder.secondary_label( + argument_list_context_span, + "function call argument list starts here", + ); + } + + if let Some(argument_span) = argument_span { + builder = builder.secondary_label(argument_span, "argument ends here"); + } + + builder + .primary_label(blame_span, primary_text) + .code("P0033") + .build() +} \ No newline at end of file diff --git a/rottlib/src/parser/errors.rs b/rottlib/src/parser/errors.rs index 9eec540..9a355fd 100644 --- a/rottlib/src/parser/errors.rs +++ b/rottlib/src/parser/errors.rs @@ -70,10 +70,19 @@ pub enum ParseErrorKind { BlockMissingClosingBrace, /// P0027 BlockExpectedItem, - // ================== Old errors to be thrown away! ================== - /// Expression inside `(...)` could not be parsed and no closing `)` - /// was found. + /// P0028 + MemberAccessMissingMemberName, + /// P0029 + IndexMissingExpression, + /// P0030 + IndexMissingClosingBracket, + /// P0031 + FunctionCallArgumentMissingComma, + /// P0032 FunctionCallMissingClosingParenthesis, + /// P0033 + FunctionCallUnexpectedTokenInArgumentList, + // ================== Old errors to be thrown away! ================== /// Found an unexpected token while parsing an expression. ExpressionUnexpectedToken, DeclEmptyVariableDeclarations, diff --git a/rottlib/src/parser/grammar/expression/block.rs b/rottlib/src/parser/grammar/expression/block.rs index 361b195..ef54273 100644 --- a/rottlib/src/parser/grammar/expression/block.rs +++ b/rottlib/src/parser/grammar/expression/block.rs @@ -1,4 +1,4 @@ -//! Block-body parsing for Fermented `UnrealScript`. +//! Block-body parsing for Fermented UnrealScript. //! //! Provides shared routines for parsing `{ ... }`-delimited bodies used in //! function, loop, state, and similar constructs after the opening `{` @@ -115,6 +115,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { ParseErrorKind::BlockMissingSemicolonAfterExpression, ) .widen_error_span_from(statement.span().start) + .sync_error_until(self, SyncLevel::StatementStart) .blame_token(unexpected_token_position) .related("expression_span", *statement.span()) .report(self); @@ -145,7 +146,9 @@ impl<'src, 'arena> Parser<'src, 'arena> { if expression_recovery_made_no_progress { return self.recover_bad_block_item_start_as_error_statement(error); } - error.fallback(self) + error + .sync_error_until(self, SyncLevel::StatementStart) + .fallback(self) } }; let expression_span = *expression.span(); diff --git a/rottlib/src/parser/grammar/expression/pratt.rs b/rottlib/src/parser/grammar/expression/pratt.rs index 60a8d26..570eb5d 100644 --- a/rottlib/src/parser/grammar/expression/pratt.rs +++ b/rottlib/src/parser/grammar/expression/pratt.rs @@ -34,7 +34,9 @@ use crate::ast::{self, Expression, ExpressionRef}; use crate::lexer::TokenPosition; -use crate::parser::{self, ParseExpressionResult, Parser, ResultRecoveryExt, diagnostic_labels}; +use crate::parser::{ + self, ParseErrorKind, ParseExpressionResult, Parser, ResultRecoveryExt, diagnostic_labels, +}; pub use super::precedence::PrecedenceRank; @@ -57,6 +59,7 @@ fn forbids_postfix_operators(expression: &ExpressionRef<'_, '_>) -> bool { } impl<'src, 'arena> Parser<'src, 'arena> { + // TODO: success here guaranees progress /// Parses an expression. /// /// Always returns some expression node; any syntax errors are reported @@ -85,7 +88,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { /// the [`diagnostic_labels::EXPRESSION_EXPECTED_AFTER`] label. pub(super) fn parse_expression_with_start_error( &mut self, - bad_start_error_kind: crate::parser::ParseErrorKind, + bad_start_error_kind: ParseErrorKind, required_by_position: crate::lexer::TokenPosition, expression_context_position: crate::lexer::TokenPosition, ) -> ParseExpressionResult<'src, 'arena> { @@ -127,7 +130,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { min_precedence_rank: PrecedenceRank, ) -> parser::ParseExpressionResult<'src, 'arena> { let mut left_hand_side = self.parse_prefix_or_primary()?; - left_hand_side = self.parse_selectors_into(left_hand_side)?; + left_hand_side = self.parse_selectors_after(left_hand_side)?; // We disallow only postfix operators after expression forms that // represent control-flow or block constructs. Selectors are still // parsed normally. @@ -143,16 +146,23 @@ impl<'src, 'arena> Parser<'src, 'arena> { // because neither `--` or `++` (the only existing default postfix // operators) make any sense after such expressions anyway. if !forbids_postfix_operators(&left_hand_side) { - left_hand_side = self.parse_postfix_into(left_hand_side); + left_hand_side = self.parse_postfix_after(left_hand_side); } - self.parse_infix_into(left_hand_side, min_precedence_rank) + self.parse_infix_after(left_hand_side, min_precedence_rank) } /// Parses a prefix or primary expression (Pratt parser's "nud" or /// null denotation). fn parse_prefix_or_primary(&mut self) -> parser::ParseExpressionResult<'src, 'arena> { let (token, token_lexeme, token_position) = - self.require_token_lexeme_and_position(parser::ParseErrorKind::ExpressionExpected)?; + self.require_token_lexeme_and_position(ParseErrorKind::ExpressionExpected)?; + // Avoid advancing over an obviously wrong token; + // this prevents error cases like `new(Outer, Name, 7 +) SomeClass`. + if token.is_definitely_not_expression_start() { + return Err( + self.make_error_at(ParseErrorKind::ExpressionExpected, token_position) + ); + } self.advance(); if let Ok(operator) = ast::PrefixOperator::try_from(token) { // In UnrealScript, prefix and postfix operators bind tighter than @@ -174,7 +184,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { /// Parses all postfix operators it can, creating a tree with /// `left_hand_side` as a child. - fn parse_postfix_into( + fn parse_postfix_after( &mut self, mut left_hand_side: ExpressionRef<'src, 'arena>, ) -> ExpressionRef<'src, 'arena> { @@ -193,7 +203,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { /// [`super::precedence::infix_precedence_ranks`]. /// /// Stops when the next operator is looser than `min_precedence_rank`. - fn parse_infix_into( + fn parse_infix_after( &mut self, mut left_hand_side: ExpressionRef<'src, 'arena>, min_precedence_rank: PrecedenceRank, diff --git a/rottlib/src/parser/grammar/expression/primary/new.rs b/rottlib/src/parser/grammar/expression/primary/new.rs index c9c3717..503e95f 100644 --- a/rottlib/src/parser/grammar/expression/primary/new.rs +++ b/rottlib/src/parser/grammar/expression/primary/new.rs @@ -1,6 +1,6 @@ //! Parser for `new` expressions in Fermented UnrealScript. -use super::super::selectors::{CallArgumentListParseState, ParsedCallArgumentSlot}; +use super::super::selectors::{CallArgumentListParseState, ParsedArgumentSlot}; use crate::ast::{Expression, ExpressionRef, OptionalExpression}; use crate::lexer::{Token, TokenPosition, TokenSpan}; use crate::parser::{ParseErrorKind, Parser, ResultRecoveryExt, SyncLevel}; @@ -51,7 +51,7 @@ struct NewArgumentListParseState<'src, 'arena> { impl<'src, 'arena> NewArgumentListParseState<'src, 'arena> { /// Stores an argument in the current `new` argument slot. fn store_argument_in_current_slot(&mut self, argument: OptionalExpression<'src, 'arena>) { - match self.call_argument_list_parse_state.parsed_slot_count { + match self.call_argument_list_parse_state.parsed_argument_slot_count { 1 => self.outer_argument = argument, 2 => self.name_argument = argument, 3 => self.flags_argument = argument, @@ -65,10 +65,10 @@ impl<'src, 'arena> NewArgumentListParseState<'src, 'arena> { #[must_use] fn current_argument_span(&self) -> Option { debug_assert!( - (1..=3).contains(&self.call_argument_list_parse_state.parsed_slot_count), + (1..=3).contains(&self.call_argument_list_parse_state.parsed_argument_slot_count), "parsed_slot_count out of range in new-argument parser" ); - match self.call_argument_list_parse_state.parsed_slot_count { + match self.call_argument_list_parse_state.parsed_argument_slot_count { 1 => &self.outer_argument, 2 => &self.name_argument, 3 => &self.flags_argument, @@ -190,9 +190,9 @@ impl<'src, 'arena> Parser<'src, 'arena> { ) -> Option { // Only successful slot parses continue the loop, // so each iteration makes progress. - while state.call_argument_list_parse_state.parsed_slot_count < 3 - && let ParsedCallArgumentSlot::Argument(argument) = - self.parse_call_argument_slot(&mut state.call_argument_list_parse_state) + while state.call_argument_list_parse_state.parsed_argument_slot_count < 3 + && let ParsedArgumentSlot::Argument(argument) = + self.parse_next_call_argument_slot(&mut state.call_argument_list_parse_state) { // On `ParsedCallArgumentSlot::Argument(_)`, // `parse_call_argument_slot` increases `parsed_slot_count` by 1, @@ -202,7 +202,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { if state .call_argument_list_parse_state - .last_slot_missing_boundary + .last_slot_missing_separator { if let Some(class_specifier_parse_action) = self.recover_from_missing_new_argument_separator(state) @@ -223,7 +223,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { state: &mut NewArgumentListParseState<'src, 'arena>, ) -> Option { let has_parsed_all_allowed_arguments = - state.call_argument_list_parse_state.parsed_slot_count >= 3; + state.call_argument_list_parse_state.parsed_argument_slot_count >= 3; let likely_missing_comma = !self.next_token_definitely_cannot_start_expression() && !has_parsed_all_allowed_arguments; if likely_missing_comma { @@ -259,10 +259,10 @@ impl<'src, 'arena> Parser<'src, 'arena> { // Preserve the first extra argument span for a more precise // diagnostic before we do any syncing. let first_extra_argument_span = - match self.parse_call_argument_slot(&mut state.call_argument_list_parse_state) { - ParsedCallArgumentSlot::Argument(Some(argument)) => Some(*argument.span()), - ParsedCallArgumentSlot::Argument(None) => None, - ParsedCallArgumentSlot::NoMoreArguments => None, + match self.parse_next_call_argument_slot(&mut state.call_argument_list_parse_state) { + ParsedArgumentSlot::Argument(Some(argument)) => Some(*argument.span()), + ParsedArgumentSlot::Argument(None) => None, + ParsedArgumentSlot::NoMoreArguments => None, }; let mut error = self .make_error_at_last_consumed(ParseErrorKind::NewTooManyArguments) diff --git a/rottlib/src/parser/grammar/expression/selectors.rs b/rottlib/src/parser/grammar/expression/selectors.rs index 9d49441..0e8febd 100644 --- a/rottlib/src/parser/grammar/expression/selectors.rs +++ b/rottlib/src/parser/grammar/expression/selectors.rs @@ -1,129 +1,126 @@ -//! Parser for expression selectors in Fermented `UnrealScript`. +//! Parser support for expression selectors. //! -//! Selectors are suffix forms that extend an already parsed expression, +//! Selectors are suffix forms that require an already parsed left-hand side, //! such as member access, indexing, and calls. -//! -//! Unlike primaries, selectors cannot be parsed on their own from the -//! current token. They always require a left-hand side expression. -use crate::arena::ArenaVec; -use crate::ast::{Expression, ExpressionRef, OptionalExpression}; -use crate::lexer::{Token, TokenPosition, TokenSpan}; +use crate::ast::{self, ExpressionRef}; +use crate::lexer::{self, Token, TokenPosition}; use crate::parser::{ParseErrorKind, ParseExpressionResult, Parser, ResultRecoveryExt, SyncLevel}; -// TODO: think about importing/moving out these fucking structs a level up. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct CallArgumentListParseState { - /// Number of argument slots already returned as `Argument(...)`. - /// - /// This counts omitted slots too, for example in `f(,x)` or `f(x,,z)`. - pub parsed_slot_count: usize, +// Lack of `Copy` is deliberate to avoid accidental reuse of parser state. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) struct CallArgumentListParseState { + /// Number of argument slots already yielded, including omitted slots. + pub(super) parsed_argument_slot_count: usize, - /// Whether the most recently returned argument slot was not followed by - /// a valid argument boundary such as `,` or `)`. - /// - /// This flag is reset at the start of each call, so after - /// `NoMoreArguments` it is always `false`. - pub last_slot_missing_boundary: bool, + /// Whether the last yielded argument expression lacked a following + /// separator (',' or ')' or end-of-file). + pub(super) last_slot_missing_separator: bool, } impl CallArgumentListParseState { #[must_use] - pub(crate) fn new() -> Self { + pub(super) fn new() -> Self { Self { - parsed_slot_count: 0, - last_slot_missing_boundary: false, + parsed_argument_slot_count: 0, + last_slot_missing_separator: false, } } #[must_use] - pub(crate) fn is_first_slot(&self) -> bool { - self.parsed_slot_count == 0 + fn has_parsed_any_argument_slots(&self) -> bool { + self.parsed_argument_slot_count > 0 } } /// Represents the result of parsing one call argument slot. -/// -/// This distinguishes between the end of the argument list and a parsed -/// argument slot, including an omitted one. #[must_use] #[derive(Debug, PartialEq)] -pub enum ParsedCallArgumentSlot<'src, 'arena> { - /// Indicates that the argument list has ended. +pub(super) enum ParsedArgumentSlot<'src, 'arena> { + /// No further slots should be parsed. NoMoreArguments, - /// The parsed argument for this slot. - /// - /// `None` represents an omitted argument between commas. - Argument(OptionalExpression<'src, 'arena>), + /// A parsed slot. `None` represents an omitted argument. + Argument(ast::OptionalExpression<'src, 'arena>), } impl<'src, 'arena> Parser<'src, 'arena> { /// Parses zero or more postfix selectors after `left_hand_side`. /// /// Returns the resulting expression after all contiguous selectors. - pub(crate) fn parse_selectors_into( + pub(super) fn parse_selectors_after( &mut self, - left_hand_side: ExpressionRef<'src, 'arena>, + mut left_hand_side: ExpressionRef<'src, 'arena>, ) -> ParseExpressionResult<'src, 'arena> { - let mut left_hand_side = left_hand_side; - // `next_position` is used only to widen diagnostic spans. - while let Some((next_token, next_position)) = self.peek_token_and_position() { + while let Some((next_token, next_token_position)) = self.peek_token_and_position() { left_hand_side = match next_token { - Token::Period => self.parse_selector_member_access_into(left_hand_side)?, + Token::Period => { + self.advance(); // '.' + self.parse_member_access_selector_after(left_hand_side, next_token_position)? + } Token::LeftBracket => { - self.parse_selector_index_into(left_hand_side, next_position)? + self.advance(); // '[' + self.parse_index_selector_after(left_hand_side, next_token_position)? } Token::LeftParenthesis => { - self.parse_selector_call_into(left_hand_side, next_position) + self.advance(); // '(' + self.parse_call_selector_after(left_hand_side, next_token_position) } _ => break, }; + self.ensure_forward_progress(next_token_position); } Ok(left_hand_side) } /// Parses a member access selector after `left_hand_side`. /// - /// Expects the leading `.` to be the next token and returns the resulting - /// member access expression. - fn parse_selector_member_access_into( + /// Expects the leading `.` to have already been consumed. + fn parse_member_access_selector_after( &mut self, left_hand_side: ExpressionRef<'src, 'arena>, + period_position: TokenPosition, ) -> ParseExpressionResult<'src, 'arena> { - self.advance(); // `.` let member_access_start = left_hand_side.span().start; - let member_identifier = self.parse_identifier(ParseErrorKind::ExpressionUnexpectedToken)?; - let member_access_end = member_identifier.0; + let member_name_position = self.peek_position_or_eof(); + let member_name = self + .parse_identifier(ParseErrorKind::MemberAccessMissingMemberName) + .blame_token(member_name_position) + .related_token("period", period_position)?; + let member_access_end = member_name.0; Ok(self.arena.alloc_node( - Expression::Member { + ast::Expression::Member { target: left_hand_side, - name: member_identifier, + name: member_name, }, - TokenSpan::range(member_access_start, member_access_end), + lexer::TokenSpan::range(member_access_start, member_access_end), )) } /// Parses an index selector after `left_hand_side`. /// - /// Expects the leading `[` to be the next token and returns the resulting - /// indexing expression. - fn parse_selector_index_into( + /// Expects the leading `[` to have already been consumed. + fn parse_index_selector_after( &mut self, left_hand_side: ExpressionRef<'src, 'arena>, left_bracket_position: TokenPosition, ) -> ParseExpressionResult<'src, 'arena> { - self.advance(); // '[' - let index_expression = self.parse_expression(); + let index_expression = self + .parse_expression_with_start_error( + ParseErrorKind::IndexMissingExpression, + left_hand_side.span().end, + left_bracket_position, + ) + .sync_error_at_matching_delimiter(self, left_bracket_position)?; let right_bracket_position = self .expect( Token::RightBracket, - ParseErrorKind::ExpressionUnexpectedToken, + ParseErrorKind::IndexMissingClosingBracket, ) .widen_error_span_from(left_bracket_position) - .sync_error_at(self, SyncLevel::CloseBracket)?; - + .sync_error_at_matching_delimiter(self, left_bracket_position) + .related_token("left_bracket", left_bracket_position)?; let expression_start = left_hand_side.span().start; Ok(self.arena.alloc_node_between( - Expression::Index { + ast::Expression::Index { target: left_hand_side, index: index_expression, }, @@ -134,27 +131,31 @@ impl<'src, 'arena> Parser<'src, 'arena> { /// Parses a call selector after `left_hand_side`. /// - /// Expects the leading `(` to be the next token and returns the resulting - /// call expression. - fn parse_selector_call_into( + /// Expects the leading `(` to have already been consumed. + /// Reports malformed argument lists internally and still returns + /// a call expression. + fn parse_call_selector_after( &mut self, left_hand_side: ExpressionRef<'src, 'arena>, left_parenthesis_position: TokenPosition, ) -> ExpressionRef<'src, 'arena> { - self.advance(); // '(' - let argument_list = self.parse_call_argument_list(left_parenthesis_position); + let callee_end_position = left_hand_side.span().end; + let argument_list = + self.parse_call_argument_list(callee_end_position, left_parenthesis_position); let right_parenthesis_position = self .expect( Token::RightParenthesis, ParseErrorKind::FunctionCallMissingClosingParenthesis, ) .widen_error_span_from(left_parenthesis_position) - .sync_error_at(self, SyncLevel::CloseParenthesis) + .sync_error_at_matching_delimiter(self, left_parenthesis_position) + .related_token("callee", callee_end_position) + .related_token("left_parenthesis", left_parenthesis_position) .unwrap_or_fallback(self); let expression_start = left_hand_side.span().start; self.arena.alloc_node_between( - Expression::Call { + ast::Expression::Call { callee: left_hand_side, arguments: argument_list, }, @@ -163,96 +164,157 @@ impl<'src, 'arena> Parser<'src, 'arena> { ) } - // TODO: add note that `parsed_slot_count` is guaranteed to be incremented - // by 1 at most (and when). - // TODO: say that errors must be handled by caller. - /// Parses one call argument slot after an already consumed `(`. + /// Parses a call argument list after an already-consumed `(`. /// - /// In `UnrealScript`, every comma introduces a follow-up argument slot, so a - /// trailing comma immediately before `)` denotes an omitted final argument. + /// Returns all parsed argument slots, preserving omitted arguments + /// as `None`. Does not consume the closing `)`. + fn parse_call_argument_list( + &mut self, + callee_end_position: TokenPosition, + left_parenthesis_position: TokenPosition, + ) -> ast::ArgumentList<'src, 'arena> { + let mut argument_list = crate::arena::ArenaVec::new_in(self.arena); + let mut argument_list_state = CallArgumentListParseState::new(); + + let mut progress_checkpoint = None; + while let ParsedArgumentSlot::Argument(argument) = + self.parse_next_call_argument_slot(&mut argument_list_state) + { + if let Some(progress_checkpoint) = progress_checkpoint { + self.ensure_forward_progress(progress_checkpoint); + } + let parsed_argument_span = argument.as_ref().map(|argument| *argument.span()); + argument_list.push(argument); + if argument_list_state.last_slot_missing_separator { + if !self.recover_after_missing_function_call_argument_separator( + callee_end_position, + left_parenthesis_position, + parsed_argument_span, + ) { + break; + } + } + progress_checkpoint = self.peek_position(); + } + argument_list + } + + /// Parses the next logical call-argument slot. /// - /// Returns [`ParsedCallArgumentSlot::NoMoreArguments`] when the argument list - /// ends, and `Argument(None)` for an omitted argument slot. + /// In UnrealScript, commas introduce follow-up argument slots, so `f(x,)` + /// means `f(x, )`, not a call with a tolerated trailing separator. /// - /// Per-call status is recorded into `state`. - pub(crate) fn parse_call_argument_slot( + /// Returns [`ParsedArgumentSlot::NoMoreArguments`] when the argument list + /// has ended or no safe recovery can continue it. + /// Returns [`ParsedArgumentSlot::Argument`] for a parsed slot, including + /// omitted slots. + /// + /// Repeated calls with the same `state` are guaranteed to eventually return + /// [`ParsedArgumentSlot::NoMoreArguments`], even for malformed input. + /// + /// Records per-slot status in `state`. + pub(super) fn parse_next_call_argument_slot( &mut self, state: &mut CallArgumentListParseState, - ) -> ParsedCallArgumentSlot<'src, 'arena> { - state.last_slot_missing_boundary = false; + ) -> ParsedArgumentSlot<'src, 'arena> { + state.last_slot_missing_separator = false; - // This function consumes arguments one at a time and the way we chose - // to handle this is by consuming a comma *before* each new argument, - // not *after*. - // Normal (non-empty) case of special argument will simply skip this - // `match`. But first *empty* argument must be handled as - // a special case. + // A comma belongs to the next slot because a final comma represents an + // omitted final argument, not a tolerated trailing separator. match self.peek_token() { None | Some(Token::RightParenthesis) => { - return ParsedCallArgumentSlot::NoMoreArguments; + return ParsedArgumentSlot::NoMoreArguments; } Some(Token::Comma) => { - // We handle special case of first empty argument by *not* - // consuming first comma (it will be consumed together with - // the second argument). - // - // We do change parsing state by incrementing - // `state.parsed_slot_count`, which ensures that - // `is_first_slot()` will return `false` from now on. - if !state.is_first_slot() { + // In `f(,x)`, the leading comma both creates the omitted first + // slot and separates it from `x`, so the first slot must not + // consume it. + if state.has_parsed_any_argument_slots() { self.advance(); } - // This `if`'s body is guaranteed to run if we've skipped - // `advance()` above. - if self.at_call_argument_boundary() { - state.parsed_slot_count += 1; - return ParsedCallArgumentSlot::Argument(None); + if self.is_at_call_argument_boundary() { + state.parsed_argument_slot_count += 1; + return ParsedArgumentSlot::Argument(None); } } _ => (), } - let argument = self.parse_expression(); - state.parsed_slot_count += 1; - state.last_slot_missing_boundary = !self.at_call_argument_boundary(); + let position_before_argument = self.peek_position_or_eof(); + let mut argument = self.parse_expression(); - ParsedCallArgumentSlot::Argument(Some(argument)) - } - - /// Parses a call argument list after an already-consumed `(`. - /// - /// Returns all parsed argument slots, preserving omitted arguments - /// as `None`. - fn parse_call_argument_list( - &mut self, - left_parenthesis_position: TokenPosition, - ) -> ArenaVec<'arena, Option>> { - let mut argument_list = ArenaVec::new_in(self.arena); - - let mut call_state = CallArgumentListParseState::new(); - //let mut old_position = self.peek_position_or_eof(); - // This caused infinite loop? (on eof?) what? - while let ParsedCallArgumentSlot::Argument(argument) = - self.parse_call_argument_slot(&mut call_state) - { - argument_list.push(argument); - // TODO: ensure progress here shouldn't be necessary actually - //self.ensure_forward_progress(old_position); - //old_position = self.peek_position_or_eof(); + let expression_recovery_made_no_progress = + self.peek_position_or_eof() == position_before_argument; + if expression_recovery_made_no_progress { + self.recover_until(SyncLevel::ListSeparator); + let list_level_recovery_made_no_progress = + self.peek_position_or_eof() == position_before_argument; + if list_level_recovery_made_no_progress { + return ParsedArgumentSlot::NoMoreArguments; + } else { + argument + .span_mut() + .extend_to(self.last_consumed_position_or_start()); + } } - - argument_list + state.parsed_argument_slot_count += 1; + state.last_slot_missing_separator = !self.is_at_call_argument_boundary(); + ParsedArgumentSlot::Argument(Some(argument)) } - /// Returns whether the current lookahead token ends the current call - /// argument slot. + /// Reports and recovers from a missing call-argument separator. /// - /// This is true for `,`, which starts the next slot, and for `)`, which - /// ends the argument list. - fn at_call_argument_boundary(&mut self) -> bool { + /// Returns whether argument-list parsing can continue at + /// the recovered position. + #[must_use] + fn recover_after_missing_function_call_argument_separator( + &mut self, + callee_end_position: TokenPosition, + left_parenthesis_position: TokenPosition, + previous_argument_span: Option, + ) -> bool { + if self.next_token_definitely_cannot_start_expression() { + let unexpected_token_position = self.peek_position_or_eof(); + let mut error = self + .make_error_at( + ParseErrorKind::FunctionCallUnexpectedTokenInArgumentList, + unexpected_token_position, + ) + .widen_error_span_from(left_parenthesis_position) + .sync_error_until(self, SyncLevel::ListSeparator) + .blame_token(unexpected_token_position) + .related_token("callee", callee_end_position) + .related_token("left_parenthesis", left_parenthesis_position); + if let Some(previous_argument_span) = previous_argument_span { + error = error.related("argument", previous_argument_span); + } + error.report(self); + self.is_at_call_argument_boundary() + } else { + let next_argument_position = self.peek_position_or_eof(); + let mut error = self + .make_error_at( + ParseErrorKind::FunctionCallArgumentMissingComma, + next_argument_position, + ) + .blame_token(next_argument_position) + .related_token("callee", callee_end_position) + .related_token("left_parenthesis", left_parenthesis_position); + debug_assert!(previous_argument_span.is_some()); + if let Some(previous_argument_span) = previous_argument_span { + error = error.related("previous_argument", previous_argument_span); + } + error.report(self); + true + } + } + + /// Returns whether the current token is a call-argument boundary. + #[must_use] + fn is_at_call_argument_boundary(&mut self) -> bool { matches!( self.peek_token(), - Some(Token::Comma | Token::RightParenthesis) + None | Some(Token::Comma | Token::RightParenthesis) ) } } diff --git a/rottlib/src/parser/recovery.rs b/rottlib/src/parser/recovery.rs index cbf1e2a..ee5b87f 100644 --- a/rottlib/src/parser/recovery.rs +++ b/rottlib/src/parser/recovery.rs @@ -243,7 +243,10 @@ impl<'src, 'arena> Parser<'src, 'arena> { return; } - while self.peek_position_or_eof() < target { + while let Some(position) = self.peek_position() { + if position >= target { + break; + } self.advance(); } } @@ -279,7 +282,10 @@ impl<'src, 'arena> Parser<'src, 'arena> { return; } - while self.peek_position_or_eof() < target { + while let Some(position) = self.peek_position() { + if position >= target { + break; + } self.advance(); } diff --git a/rottlib/tests/parser_diagnostics/mod.rs b/rottlib/tests/parser_diagnostics/mod.rs index 426a3ef..55d1686 100644 --- a/rottlib/tests/parser_diagnostics/mod.rs +++ b/rottlib/tests/parser_diagnostics/mod.rs @@ -8,6 +8,7 @@ use rottlib::parser::Parser; mod block_items; mod control_flow_expressions; mod primary_expressions; +mod selector_expressions; #[derive(Debug)] pub(super) struct ExpectedLabel { diff --git a/rottlib/tests/parser_diagnostics/primary_expressions.rs b/rottlib/tests/parser_diagnostics/primary_expressions.rs index 2d161cb..1dec025 100644 --- a/rottlib/tests/parser_diagnostics/primary_expressions.rs +++ b/rottlib/tests/parser_diagnostics/primary_expressions.rs @@ -35,6 +35,10 @@ pub(super) const P0002_FIXTURES: &[Fixture] = &[ label: "files/P0002_04.uc", source: "a * * *", }, + Fixture { + label: "files/P0002_05.uc", + source: "new(Outer, Name, 7 +) SomeClass" + } ]; pub(super) const P0003_FIXTURES: &[Fixture] = &[ @@ -322,6 +326,7 @@ fn check_p0002_fixtures() { assert_eq!(runs.get("files/P0002_02.uc").unwrap().len(), 1); assert_eq!(runs.get("files/P0002_03.uc").unwrap().len(), 1); assert_eq!(runs.get("files/P0002_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0002_05.uc").unwrap().len(), 1); assert_diagnostic( &runs.get_any("files/P0002_01.uc"), @@ -404,6 +409,25 @@ fn check_p0002_fixtures() { notes: &[], }, ); + + assert_diagnostic( + &runs.get_any("files/P0002_05.uc"), + &ExpectedDiagnostic { + headline: "expected expression after `+`, found `)`", + severity: Severity::Error, + code: Some("P0002"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(11), + end: TokenPosition(11), + }, + message: "unexpected `)`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); } #[test] @@ -665,7 +689,7 @@ fn check_p0005_fixtures() { code: Some("P0005"), primary_label: Some(ExpectedLabel { span: TokenSpan { - start: TokenPosition(9), + start: TokenPosition(14), end: TokenPosition(14), }, message: "unexpected `>`", @@ -699,7 +723,7 @@ fn check_p0005_fixtures() { code: Some("P0005"), primary_label: Some(ExpectedLabel { span: TokenSpan { - start: TokenPosition(5), + start: TokenPosition(10), end: TokenPosition(10), }, message: "unexpected `>`", diff --git a/rottlib/tests/parser_diagnostics/selector_expressions.rs b/rottlib/tests/parser_diagnostics/selector_expressions.rs new file mode 100644 index 0000000..767e24a --- /dev/null +++ b/rottlib/tests/parser_diagnostics/selector_expressions.rs @@ -0,0 +1,778 @@ +use super::*; + +pub(super) const P0028_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0028_01.uc", + source: "{\n local Actor A;\n A.\n}\n", + }, + Fixture { + label: "files/P0028_02.uc", + source: "{\n local Actor A;\n A.;\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0028_03.uc", + source: "{\n local Actor A;\n A.\n ;\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0028_04.uc", + source: "{\n local Actor A;\n Log(A.\n );\n Log(\"after\");\n}\n", + }, +]; + +pub(super) const P0029_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0029_01.uc", + source: "{\n local array Values;\n Values[];\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0029_02.uc", + source: "{\n local array Values;\n Values[\n ];\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0029_03.uc", + source: "{\n local array Values;\n Values[, 1];\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0029_04.uc", + source: "{\n local array Values;\n Log(Values[\n ]);\n Log(\"after\");\n}\n", + }, +]; + +pub(super) const P0030_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0030_01.uc", + source: "{\n local array Values;\n Values[0;\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0030_02.uc", + source: "{\n local array Values;\n Values[\n 0\n ;\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0030_03.uc", + source: "{\n local array Values;\n Log(Values[0));\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0030_04.uc", + source: "{\n local array Values;\n Values[GetIndex()\n Values[1] = 7;\n}\n", + }, +]; + +#[test] +fn check_p0028_fixtures() { + let runs = run_fixtures(P0028_FIXTURES); + + assert_eq!(runs.get("files/P0028_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0028_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0028_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0028_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0028_01.uc"), + &ExpectedDiagnostic { + headline: "expected member name after `.`, found `}`", + severity: Severity::Error, + code: Some("P0028"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(14), + end: TokenPosition(14), + }, + message: "unexpected `}`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "after this `.`, a member name was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0028_02.uc"), + &ExpectedDiagnostic { + headline: "expected member name after `.`, found `;`", + severity: Severity::Error, + code: Some("P0028"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(13), + end: TokenPosition(13), + }, + message: "unexpected `;`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0028_03.uc"), + &ExpectedDiagnostic { + headline: "expected member name after `.`, found `;`", + severity: Severity::Error, + code: Some("P0028"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "unexpected `;`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "after this `.`, a member name was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0028_04.uc"), + &ExpectedDiagnostic { + headline: "expected member name after `.`, found `)`", + severity: Severity::Error, + code: Some("P0028"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(17), + end: TokenPosition(17), + }, + message: "unexpected `)`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(14), + end: TokenPosition(14), + }, + message: "after this `.`, a member name was expected", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0029_fixtures() { + let runs = run_fixtures(P0029_FIXTURES); + + assert_eq!(runs.get("files/P0029_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0029_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0029_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0029_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0029_01.uc"), + &ExpectedDiagnostic { + headline: "expected index expression after `[`, found `]`", + severity: Severity::Error, + code: Some("P0029"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(16), + end: TokenPosition(16), + }, + message: "expected expression before `]`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0029_02.uc"), + &ExpectedDiagnostic { + headline: "expected index expression after `[`, found `]`", + severity: Severity::Error, + code: Some("P0029"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(18), + }, + message: "expected expression before `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "after this `[`, an index expression was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0029_03.uc"), + &ExpectedDiagnostic { + headline: "expected index expression after `[`, found `,`", + severity: Severity::Error, + code: Some("P0029"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(16), + end: TokenPosition(16), + }, + message: "unexpected `,`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0029_04.uc"), + &ExpectedDiagnostic { + headline: "expected index expression after `[`, found `]`", + severity: Severity::Error, + code: Some("P0029"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(17), + end: TokenPosition(20), + }, + message: "expected expression before `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(17), + end: TokenPosition(17), + }, + message: "after this `[`, an index expression was expected", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0030_fixtures() { + let runs = run_fixtures(P0030_FIXTURES); + + assert_eq!(runs.get("files/P0030_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0030_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0030_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0030_01.uc"), + &ExpectedDiagnostic { + headline: "missing `]` to close index selector", + severity: Severity::Error, + code: Some("P0030"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(17), + end: TokenPosition(17), + }, + message: "expected `]` before `;`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0030_02.uc"), + &ExpectedDiagnostic { + headline: "missing `]` to close index selector", + severity: Severity::Error, + code: Some("P0030"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(21), + }, + message: "expected `]` before `;`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "index selector starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0030_03.uc", "P0030"), + &ExpectedDiagnostic { + headline: "missing `]` to close index selector", + severity: Severity::Error, + code: Some("P0030"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(19), + end: TokenPosition(19), + }, + message: "expected `]` before `)`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0030_04.uc"), + &ExpectedDiagnostic { + headline: "missing `]` to close index selector", + severity: Severity::Error, + code: Some("P0030"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(21), + }, + message: "expected `]` before `Values`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "index selector starts here", + }], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0031_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0031_01.uc", + source: "{\n Func(A B);\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0031_02.uc", + source: "{\n Func\n (A 123);\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0031_03.uc", + source: "{\n Func(\n A\n new SomeClass\n );\n Log(\"after\");\n}\n", + }, +]; + +pub(super) const P0032_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0032_01.uc", + source: "Func(", + }, + Fixture { + label: "files/P0032_02.uc", + source: "Func\n(\n A,", + }, + Fixture { + label: "files/P0032_03.uc", + source: "Func(A,\n B,", + }, + Fixture { + label: "files/P0032_04.uc", + source: "{\n Func\n (\n A,\n B,\n", + }, +]; + +pub(super) const P0033_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0033_01.uc", + source: "{\n Func(A #, B);\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0033_02.uc", + source: "{\n Func\n (A\n :,\n B);\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0033_03.uc", + source: "{\n Func(\n A ?\n , B\n );\n Log(\"after\");\n}\n", + }, + Fixture { + label: "files/P0033_04.uc", + source: "{\n Func\n (\n A\n #,\n B\n );\n Log(\"after\");\n}\n", + }, +]; + +#[test] +fn check_p0031_fixtures() { + let runs = run_fixtures(P0031_FIXTURES); + + assert_eq!(runs.get("files/P0031_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0031_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0031_03.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0031_01.uc"), + &ExpectedDiagnostic { + headline: "missing `,` between function call arguments", + severity: Severity::Error, + code: Some("P0031"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "expected `,` before `B`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "previous argument ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0031_02.uc"), + &ExpectedDiagnostic { + headline: "missing `,` between function call arguments", + severity: Severity::Error, + code: Some("P0031"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "expected `,` before `123`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "function called here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "previous argument ends here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0031_03.uc"), + &ExpectedDiagnostic { + headline: "missing `,` between function call arguments", + severity: Severity::Error, + code: Some("P0031"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(10), + end: TokenPosition(10), + }, + message: "expected `,` before `new`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "function call argument list starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "previous argument ends here", + }, + ], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0032_fixtures() { + let runs = run_fixtures(P0032_FIXTURES); + + assert_eq!(runs.get("files/P0032_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0032_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0032_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0032_04.uc").unwrap().len(), 2); + + assert_diagnostic( + &runs.get_any("files/P0032_01.uc"), + &ExpectedDiagnostic { + headline: "missing `)` to close function call argument list", + severity: Severity::Error, + code: Some("P0032"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "expected `)` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0032_02.uc"), + &ExpectedDiagnostic { + headline: "missing `)` to close function call argument list", + severity: Severity::Error, + code: Some("P0032"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(7), + }, + message: "expected `)` before end of file", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "function called here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "function call argument list starts here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0032_03.uc"), + &ExpectedDiagnostic { + headline: "missing `)` to close function call argument list", + severity: Severity::Error, + code: Some("P0032"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(1), + end: TokenPosition(8), + }, + message: "expected `)` before end of file", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(1), + end: TokenPosition(1), + }, + message: "function call argument list starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0032_04.uc", "P0032"), + &ExpectedDiagnostic { + headline: "missing `)` to close function call argument list", + severity: Severity::Error, + code: Some("P0032"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(16), + }, + message: "expected `)` before end of file", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "function called here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(6), + }, + message: "function call argument list starts here", + }, + ], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0033_fixtures() { + let runs = run_fixtures(P0033_FIXTURES); + + assert_eq!(runs.get("files/P0033_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0033_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0033_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0033_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0033_01.uc"), + &ExpectedDiagnostic { + headline: "expected `,` or `)` after argument, found `#`", + severity: Severity::Error, + code: Some("P0033"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "unexpected `#`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "argument ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0033_02.uc"), + &ExpectedDiagnostic { + headline: "expected `,` or `)` after argument, found `:`", + severity: Severity::Error, + code: Some("P0033"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(10), + end: TokenPosition(10), + }, + message: "unexpected `:`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "function called here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "argument ends here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0033_03.uc"), + &ExpectedDiagnostic { + headline: "expected `,` or `)` after argument, found `?`", + severity: Severity::Error, + code: Some("P0033"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "unexpected `?`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "function call argument list starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "argument ends here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0033_04.uc"), + &ExpectedDiagnostic { + headline: "expected `,` or `)` after argument, found `#`", + severity: Severity::Error, + code: Some("P0033"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "unexpected `#`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "function called here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(6), + }, + message: "function call argument list starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "argument ends here", + }, + ], + help: None, + notes: &[], + }, + ); +} \ No newline at end of file