diff --git a/dev_tests/src/verify_expr.rs b/dev_tests/src/verify_expr.rs index e37fc12..978b618 100644 --- a/dev_tests/src/verify_expr.rs +++ b/dev_tests/src/verify_expr.rs @@ -16,45 +16,40 @@ 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)] = &[ - // L0001: invalid or unknown token - ( - "files/L0001_01.uc", - "`", - ), - - // L0002: unexpected closing delimiter - ( - "files/L0002_01.uc", - "]", - ), - - // L0003: unclosed delimiter before later closing delimiter + // P0027: `else` without a matching `if` // - // The `}` can still recover by matching the earlier `{`. + // `else` cannot start a standalone block item. The parser should report + // P0027 at `else`, then recover by bailing out of the current block. ( - "files/L0003_01.uc", - "{\n foo(\n}\n", + "files/P0027_01.uc", + "{\n local bool bReady;\n bReady = CheckReady();\n else { StartMatch(); }\n NotifyReady();\n}\n", ), - // L0004: mismatched closing delimiter - ( - "files/L0004_01.uc", - "(]", - ), - - // L0005: unclosed delimiter at end of file - ( - "files/L0005_01.uc", - "foo(", - ), - - // Mixed recovery case: + // P0027: `case` outside of a `switch` // - // `)` recovers by matching `(` after treating `[` as unclosed; - // the following `]` is then unexpected. + // `case` is a switch-arm boundary, not a valid statement or expression + // starter in an ordinary block. ( - "files/L_mixed_01.uc", - "([)]", + "files/P0027_02.uc", + "{ local int Count; Count = 3; case 3: Count++; UpdateHud();}", + ), + + // 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", + ), + + // 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. + ( + "files/P0027_04.uc", + "{\n local int Count;\n Count = 0;\n #exec TEXTURE IMPORT NAME=Bad FILE=Bad.bmp\n Count++;\n}\n", ), ]; @@ -62,12 +57,12 @@ const TEST_CASES: &[(&str, &str)] = &[ /// /// For lexer-focused fixtures this is usually noisy, so keep it off unless you /// want to inspect how parser recovery behaves after lexer diagnostics. -const RUN_PARSER: bool = false; +const RUN_PARSER: bool = true; /// If true, print the parsed expression using Debug formatting. const PRINT_PARSED_EXPR: bool = false; -/// If true, print diagnostics even when parsing returned a value. +const PRINT_LEXER_DIAGNOSTICS: bool = false; const ALWAYS_PRINT_DIAGNOSTICS: bool = true; fn main() { @@ -89,7 +84,7 @@ fn main() { } else { had_any_problem = true; - if ALWAYS_PRINT_DIAGNOSTICS { + if PRINT_LEXER_DIAGNOSTICS { println!("Lexer diagnostics:"); for diag in lexer_diagnostics { render_diagnostic(diag, &tf, Some(label), false); diff --git a/rottlib/src/diagnostics/parse_error_diagnostics/block_items.rs b/rottlib/src/diagnostics/parse_error_diagnostics/block_items.rs new file mode 100644 index 0000000..d3fcc35 --- /dev/null +++ b/rottlib/src/diagnostics/parse_error_diagnostics/block_items.rs @@ -0,0 +1,111 @@ +use super::{Diagnostic, DiagnosticBuilder, FoundAt, found_at}; +use crate::lexer::{Token, TokenSpan, TokenizedFile}; +use crate::parser::ParseError; + +/// P0025 +pub(super) fn diagnostic_block_missing_semicolon_after_expression<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let expression_span = error.related_spans.get("expression_span").copied(); + + let primary_span = TokenSpan::new(error.blame_span.end); + + let primary_text = match found_at(file, primary_span.end) { + 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 `;` after expression statement"); + + if let Some(expression_span) = expression_span { + if file.same_line(expression_span.start, primary_span.end) { + builder = builder.secondary_label(expression_span, "expression statement"); + } else { + builder = builder.secondary_label( + TokenSpan::new(expression_span.end), + "expression statement ends here", + ); + } + } + + builder + .primary_label(primary_span, primary_text) + .code("P0025") + .build() +} + +/// P0026 +pub(super) fn diagnostic_block_missing_closing_brace<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let left_brace_span = error.related_spans.get("left_brace").copied(); + let unexpected_token_span = error.related_spans.get("unexpected_token").copied(); + + let (mut primary_span, primary_text) = + if let Some(unexpected_token_span) = unexpected_token_span { + let primary_span = TokenSpan::new(unexpected_token_span.end); + + let primary_text = match found_at(file, primary_span.end) { + FoundAt::Token(token_text) => format!("expected `}}` before `{}`", token_text), + FoundAt::EndOfFile => "expected `}` before end of file".to_string(), + FoundAt::Unknown => "expected `}` here".to_string(), + }; + + (primary_span, primary_text) + } else { + ( + TokenSpan::new(error.blame_span.end), + "expected `}` before end of file".to_string(), + ) + }; + + let builder = DiagnosticBuilder::error("missing `}` to close block"); + + if let Some(left_brace_span) = left_brace_span + && !file.same_line(left_brace_span.start, primary_span.end) + { + primary_span.start = left_brace_span.start; + } + + builder + .primary_label(primary_span, primary_text) + .code("P0026") + .build() +} + +/// P0027 +pub(super) fn diagnostic_block_expected_item<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let primary_span = error.blame_span; + + let (title, primary_text) = match file.token_at(primary_span.start).map(|data| data.token) { + Some(Token::ExecDirective) => ( + "expected statement or expression, found `#exec` directive".to_string(), + "`#exec` directives are not allowed in a statement block".to_string(), + ), + _ => match found_at(file, primary_span.start) { + FoundAt::Token(token_text) => ( + format!("expected statement or expression, found `{}`", token_text), + format!("unexpected `{}`", token_text), + ), + FoundAt::EndOfFile => ( + "expected statement or expression, found end of file".to_string(), + "reached end of file here".to_string(), + ), + FoundAt::Unknown => ( + "expected statement or expression".to_string(), + "expected statement or expression here".to_string(), + ), + }, + }; + + DiagnosticBuilder::error(title) + .primary_label(primary_span, primary_text) + .code("P0027") + .build() +} diff --git a/rottlib/src/diagnostics/parse_error_diagnostics/control_flow_expressions.rs b/rottlib/src/diagnostics/parse_error_diagnostics/control_flow_expressions.rs index cb6fe57..c9518b6 100644 --- a/rottlib/src/diagnostics/parse_error_diagnostics/control_flow_expressions.rs +++ b/rottlib/src/diagnostics/parse_error_diagnostics/control_flow_expressions.rs @@ -263,7 +263,7 @@ pub(super) fn diagnostic_for_each_iterator_expression_expected<'src>( let control_keyword_text = control_keyword_span.and_then(|span| file.token_text(span.end)); let found = found_at(file, error.blame_span.end); - let found_body_block_start = matches!(found, FoundAt::Token("{")); + let _ = matches!(found, FoundAt::Token("{")); let (header_text, primary_text) = match (control_keyword_text, found) { (Some(keyword_text), FoundAt::Token(token_text)) => { diff --git a/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs b/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs index 3fc5f19..9e13ecd 100644 --- a/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs +++ b/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs @@ -10,9 +10,14 @@ //! parser areas or grammar families. use super::{Diagnostic, DiagnosticBuilder}; +use crate::diagnostics::parse_error_diagnostics::block_items::{ + diagnostic_block_expected_item, diagnostic_block_missing_closing_brace, + diagnostic_block_missing_semicolon_after_expression, +}; use crate::lexer::{TokenPosition, TokenSpan, TokenizedFile}; use crate::parser::{ParseError, ParseErrorKind}; +mod block_items; mod control_flow_expressions; mod primary_expressions; @@ -133,6 +138,14 @@ pub(crate) fn diagnostic_from_parse_error<'src>( } ParseErrorKind::BreakValueInvalidStart => diagnostic_break_value_invalid_start(error, file), ParseErrorKind::GotoMissingLabel => diagnostic_goto_missing_label(error, file), + // block_items.rs + ParseErrorKind::BlockMissingSemicolonAfterExpression => { + diagnostic_block_missing_semicolon_after_expression(error, file) + } + ParseErrorKind::BlockMissingClosingBrace => { + diagnostic_block_missing_closing_brace(error, file) + } + ParseErrorKind::BlockExpectedItem => diagnostic_block_expected_item(error, file), _ => DiagnosticBuilder::error(format!("error {:?} while parsing", error.kind)) .primary_label(error.covered_span, "happened here") diff --git a/rottlib/src/lexer/raw_lexer.rs b/rottlib/src/lexer/raw_lexer.rs index 892f1d7..0cc441b 100644 --- a/rottlib/src/lexer/raw_lexer.rs +++ b/rottlib/src/lexer/raw_lexer.rs @@ -59,7 +59,7 @@ pub enum BraceKind { #[logos(extras = LexerState)] pub enum RawToken { // # Compiler/directive keywords - #[regex(r"(?i)#exec[^\r\n]*(?:\r\n|\n|\r)?")] + #[regex(r"(?i)#exec[^\r\n]*")] ExecDirective, #[regex("(?i)cpptext", |lex| { if is_next_nontrivia_left_brace(lex) { diff --git a/rottlib/src/parser/cursor.rs b/rottlib/src/parser/cursor.rs index 812a9ab..10fab51 100644 --- a/rottlib/src/parser/cursor.rs +++ b/rottlib/src/parser/cursor.rs @@ -363,7 +363,8 @@ impl<'src, 'arena> Parser<'src, 'arena> { .unwrap_or(TokenPosition(0)) } - /// Ensures that parsing has advanced past `old_position`. + /// Ensures that parsing has advanced past `old_position`. + // TODO: must be given peeked value! /// /// This is intended as a safeguard against infinite-loop bugs while /// recovering from invalid input. In debug builds it asserts that progress diff --git a/rottlib/src/parser/errors.rs b/rottlib/src/parser/errors.rs index 3931d3e..9eec540 100644 --- a/rottlib/src/parser/errors.rs +++ b/rottlib/src/parser/errors.rs @@ -64,6 +64,12 @@ pub enum ParseErrorKind { BreakValueInvalidStart, /// P0024 GotoMissingLabel, + /// P0025 + BlockMissingSemicolonAfterExpression, + /// P0026 + BlockMissingClosingBrace, + /// P0027 + BlockExpectedItem, // ================== Old errors to be thrown away! ================== /// Expression inside `(...)` could not be parsed and no closing `)` /// was found. @@ -83,11 +89,7 @@ pub enum ParseErrorKind { TypeSpecClassMissingInnerType, TypeSpecClassMissingClosingAngle, - /// An expression inside a block is not terminated with `;`. - BlockMissingSemicolonAfterExpression, - /// A statement inside a block is not terminated with `;`. BlockMissingSemicolonAfterStatement, - BlockMissingClosingBrace, /// `switch` has no body (missing matching braces). SwitchMissingBody, /// The first top-level item in a `switch` body is not a `case`. diff --git a/rottlib/src/parser/grammar/class.rs b/rottlib/src/parser/grammar/class.rs index 76b49d0..57be640 100644 --- a/rottlib/src/parser/grammar/class.rs +++ b/rottlib/src/parser/grammar/class.rs @@ -357,7 +357,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { Ok(rule) => rules.push(rule), Err(error) => { self.report_error(error); - self.recover_until(SyncLevel::Statement); + self.recover_until(SyncLevel::StatementStart); let _ = self.eat(Token::Semicolon); if !self.ensure_progress_or_break(loop_start) { break; @@ -899,7 +899,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { } Some((_, _)) if declarators.is_empty() => { self.report_error_here(ParseErrorKind::DeclBadVariableIdentifier); - self.recover_until(SyncLevel::Statement); + self.recover_until(SyncLevel::StatementStart); let _ = self.eat(Token::Semicolon); break; } diff --git a/rottlib/src/parser/grammar/declarations/enum_definition.rs b/rottlib/src/parser/grammar/declarations/enum_definition.rs index 76d7286..46597d5 100644 --- a/rottlib/src/parser/grammar/declarations/enum_definition.rs +++ b/rottlib/src/parser/grammar/declarations/enum_definition.rs @@ -104,7 +104,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { variants: &mut ArenaVec<'arena, IdentifierToken>, ) -> ControlFlow<()> { self.parse_identifier(ParseErrorKind::EnumBadVariant) - .sync_error_until(self, SyncLevel::Statement) + .sync_error_until(self, SyncLevel::StatementStart) .ok_or_report(self) .map_or(ControlFlow::Break(()), |variant| { variants.push(variant); @@ -122,7 +122,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { let Some(variant) = self .parse_identifier(ParseErrorKind::EnumBadVariant) .widen_error_span_from(error_start_position) - .sync_error_until(self, SyncLevel::Statement) + .sync_error_until(self, SyncLevel::StatementStart) .ok_or_report(self) else { // If we don't even get a good identifier - error is different diff --git a/rottlib/src/parser/grammar/declarations/struct_definition.rs b/rottlib/src/parser/grammar/declarations/struct_definition.rs index 235f06d..5c22c11 100644 --- a/rottlib/src/parser/grammar/declarations/struct_definition.rs +++ b/rottlib/src/parser/grammar/declarations/struct_definition.rs @@ -93,7 +93,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { self.advance(); if !self.eat(Token::CppBlock) { self.report_error_here(ParseErrorKind::CppDirectiveMissingCppBlock); - self.recover_until(SyncLevel::Statement); + self.recover_until(SyncLevel::StatementStart); } StructBodyItemParseOutcome::Skip } diff --git a/rottlib/src/parser/grammar/declarations/variable_declarators.rs b/rottlib/src/parser/grammar/declarations/variable_declarators.rs index 22762c2..254d4a3 100644 --- a/rottlib/src/parser/grammar/declarations/variable_declarators.rs +++ b/rottlib/src/parser/grammar/declarations/variable_declarators.rs @@ -104,7 +104,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { ) -> ControlFlow<()> { if let Some(parsed_declarator) = self .parse_variable_declarator() - .sync_error_until(self, SyncLevel::Statement) + .sync_error_until(self, SyncLevel::StatementStart) .ok_or_report(self) { declarators.push(parsed_declarator); @@ -122,7 +122,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { if let Some(parsed_declarator) = self .parse_variable_declarator() .widen_error_span_from(error_start_position) - .sync_error_until(self, SyncLevel::Statement) + .sync_error_until(self, SyncLevel::StatementStart) .ok_or_report(self) { self.make_error_at_last_consumed(ParseErrorKind::DeclNoSeparatorBetweenVariableDeclarations) diff --git a/rottlib/src/parser/grammar/expression/block.rs b/rottlib/src/parser/grammar/expression/block.rs index 7b7a5c2..361b195 100644 --- a/rottlib/src/parser/grammar/expression/block.rs +++ b/rottlib/src/parser/grammar/expression/block.rs @@ -4,106 +4,187 @@ //! function, loop, state, and similar constructs after the opening `{` //! has been consumed. -use crate::arena::ArenaVec; -use crate::ast::{BlockBody, Expression, ExpressionRef, Statement, StatementRef}; +use crate::ast::{BlockBody, Expression, ExpressionRef, Statement, StatementList, StatementRef}; use crate::lexer::{Token, TokenPosition, TokenSpan}; -use crate::parser::{ParseErrorKind, Parser}; +use crate::parser::{ParseErrorKind, ParseResult, Parser, ResultRecoveryExt, SyncLevel}; impl<'src, 'arena> Parser<'src, 'arena> { - /// Parses a `{ ... }` block after the opening `{` has been consumed. + /// Parses a braced block body into an [`Expression::Block`]. /// - /// Consumes tokens until the matching `}` and returns an - /// [`Expression::Block`] whose span covers the entire block, from - /// `opening_brace_position` to the closing `}`. + /// The opening `{` must already have been consumed. The returned block's + /// span covers the whole block, from `left_brace_position` through + /// the closing `}`. /// - /// On premature end-of-file, returns a best-effort block. + /// On premature end-of-file, reports the missing `}` and returns + /// a best-effort block. #[must_use] - pub(crate) fn parse_block_tail( + pub(crate) fn parse_block_body_tail( &mut self, - opening_brace_position: TokenPosition, + left_brace_position: TokenPosition, ) -> ExpressionRef<'src, 'arena> { let BlockBody { statements, span } = - self.parse_braced_block_statements_tail(opening_brace_position); + self.parse_braced_block_statements_tail(left_brace_position); self.arena.alloc_node(Expression::Block(statements), span) } - /// Parses a `{ ... }` block after the opening `{` has been consumed. + /// Parses the statements in a braced block body. /// - /// Consumes tokens until the matching `}` and returns the contained - /// statements together with a span that covers the entire block, from - /// `opening_brace_position` to the closing `}`. + /// The opening `{` must already have been consumed. Returns the parsed + /// statements and a span covering the whole block, from + /// `left_brace_position` through the closing `}`. /// - /// On premature end-of-file, returns a best-effort statement list and span. + /// On premature end-of-file, reports the missing `}` and returns + /// a best-effort body. #[must_use] pub(crate) fn parse_braced_block_statements_tail( &mut self, - opening_brace_position: TokenPosition, + left_brace_position: TokenPosition, ) -> BlockBody<'src, 'arena> { let mut statements = self.arena.vec(); while let Some((token, token_position)) = self.peek_token_and_position() { if token == Token::RightBrace { self.advance(); // '}' - let span = TokenSpan::range(opening_brace_position, token_position); + let span = TokenSpan::range(left_brace_position, token_position); return BlockBody { statements, span }; } - self.parse_next_block_item_into(&mut statements); - self.ensure_forward_progress(token_position); + match self.parse_and_append_next_block_item(&mut statements) { + Ok(()) => { + // Guard against parser bugs that would otherwise leave + // block parsing stuck on the same token. + self.ensure_forward_progress(token_position); + } + Err(error) => { + // Item-level recovery failed, + // so escalate to the block boundary. + let error = error.sync_error_at_matching_delimiter(self, left_brace_position); + let error_statement = error.fallback(self); + statements.push(error_statement); + let span = TokenSpan::range( + left_brace_position, + self.last_consumed_position_or_start(), + ); + return BlockBody { statements, span }; + } + } } - // Reached EOF without a closing `}` - self.report_error_here(ParseErrorKind::BlockMissingClosingBrace); - let span = TokenSpan::range( - opening_brace_position, - self.last_consumed_position_or_start(), - ); + let eof_position = self.peek_position_or_eof(); + self.make_error_at(ParseErrorKind::BlockMissingClosingBrace, eof_position) + .related_token("left_brace", left_brace_position) + .report(self); + let span = TokenSpan::range(left_brace_position, self.last_consumed_position_or_start()); + BlockBody { statements, span } } - /// Parses one statement inside a `{ ... }` block and appends it to - /// `statements`. + /// Parses one statement-like item inside a `{ ... }` block and appends it + /// to `statements`. /// /// This method never consumes the closing `}` and is only meant to be - /// called while parsing inside a block. It always appends at least one - /// statement, even in the presence of syntax errors. - pub(crate) fn parse_next_block_item_into( + /// called while parsing inside a block. + /// + /// On success, it appends exactly one statement. If neither a statement nor + /// an expression statement can be recovered locally, it returns + /// an unreported error so the enclosing block parser can recover at + /// block level. + pub(crate) fn parse_and_append_next_block_item( &mut self, - statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>, - ) { - let mut next_statement = self.parse_statement().unwrap_or_else(|| { - let next_expression = self.parse_expression(); - let next_expression_span = *next_expression.span(); - self.arena - .alloc_node(Statement::Expression(next_expression), next_expression_span) - }); - if statement_needs_semicolon(&next_statement) - && let Some((Token::Semicolon, semicolon_position)) = self.peek_token_and_position() - { - next_statement.span_mut().extend_to(semicolon_position); - self.advance(); // ';' + statements: &mut StatementList<'src, 'arena>, + ) -> ParseResult<'src, 'arena, ()> { + let mut statement = match self.parse_statement() { + Some(statement) => statement, + None => { + // Non-statement starters are parsed as expression statements + self.parse_expression_statement_in_block()? + } + }; + if block_item_requires_semicolon(&statement) { + match self.peek_token_and_position() { + Some((Token::Semicolon, semicolon_position)) => { + statement.span_mut().extend_to(semicolon_position); + self.advance(); // ';' + } + // A final expression before `}` may omit `;`; this makes it + // the block's tail value. + // + // On end-of-file, suppress the missing-`;` diagnostic as well. + // The block parser will report the missing `}`, + // and an extra semicolon error would just cascade. + None | Some((Token::RightBrace, _)) => (), + Some((_, unexpected_token_position)) => { + self.make_error_at_last_consumed( + ParseErrorKind::BlockMissingSemicolonAfterExpression, + ) + .widen_error_span_from(statement.span().start) + .blame_token(unexpected_token_position) + .related("expression_span", *statement.span()) + .report(self); + } + } } - statements.push(next_statement); + statements.push(statement); + Ok(()) + } + + fn parse_expression_statement_in_block( + &mut self, + ) -> ParseResult<'src, 'arena, StatementRef<'src, 'arena>> { + let expression_start_position = self.peek_position_or_eof(); + let expected_block_item_after_position = self.last_consumed_position_or_start(); + let expression_result = self.parse_expression_with_start_error( + ParseErrorKind::BlockExpectedItem, + expected_block_item_after_position, + expected_block_item_after_position, + ); + let expression = match expression_result { + Ok(expression) => expression, + Err(error) => { + let expression_recovery_made_no_progress = + self.peek_position_or_eof() == expression_start_position; + // Without progress, a fallback statement could leave + // the block loop stuck. + if expression_recovery_made_no_progress { + return self.recover_bad_block_item_start_as_error_statement(error); + } + error.fallback(self) + } + }; + let expression_span = *expression.span(); + Ok(self + .arena + .alloc_node(Statement::Expression(expression), expression_span)) + } + + fn recover_bad_block_item_start_as_error_statement( + &mut self, + error: crate::parser::ParseError, + ) -> ParseResult<'src, 'arena, StatementRef<'src, 'arena>> { + let position_before_statement_recovery = self.peek_position_or_eof(); + // Recover one damaged block item if possible; + // otherwise let the enclosing block recover at its own boundary. + let error = error.sync_error_at(self, SyncLevel::StatementTerminator); + if self.peek_position_or_eof() != position_before_statement_recovery { + return Ok(error.fallback(self)); + } + Err(error) } } -fn statement_needs_semicolon(statement: &Statement) -> bool { - use Statement::{Empty, Error, Expression, Function, Label, LocalVariableDeclaration}; - match statement { - Empty | Label(_) | Error | Function(_) => false, - Expression(expression) => expression_needs_semicolon(expression), - LocalVariableDeclaration { .. } => true, +fn block_item_requires_semicolon(statement: &Statement) -> bool { + // Control-flow and block expressions do not require a trailing semicolon + // when used as block items. + if let Statement::Expression(expression) = statement { + !matches!( + **expression, + Expression::Block { .. } + | Expression::If { .. } + | Expression::While { .. } + | Expression::DoUntil { .. } + | Expression::ForEach { .. } + | Expression::For { .. } + | Expression::Switch { .. } + | Expression::Error + ) + } else { + false } } - -const fn expression_needs_semicolon(expression: &Expression) -> bool { - use Expression::{Block, DoUntil, Error, For, ForEach, If, Switch, While}; - matches!( - expression, - Block { .. } - | If { .. } - | While { .. } - | DoUntil { .. } - | ForEach { .. } - | For { .. } - | Switch { .. } - | Error - ) -} diff --git a/rottlib/src/parser/grammar/expression/primary/mod.rs b/rottlib/src/parser/grammar/expression/primary/mod.rs index 38b8ced..71afce9 100644 --- a/rottlib/src/parser/grammar/expression/primary/mod.rs +++ b/rottlib/src/parser/grammar/expression/primary/mod.rs @@ -78,7 +78,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { token_position, ), Token::LeftParenthesis => self.parse_parenthesized_expression_tail(token_position), - Token::LeftBrace => self.parse_block_tail(token_position), + Token::LeftBrace => self.parse_block_body_tail(token_position), Token::Keyword(keyword) => { match self.try_parse_keyword_primary(keyword, token_position) { Some(keyword_expression) => keyword_expression, diff --git a/rottlib/src/parser/grammar/expression/switch.rs b/rottlib/src/parser/grammar/expression/switch.rs index be948b4..0ca0e54 100644 --- a/rottlib/src/parser/grammar/expression/switch.rs +++ b/rottlib/src/parser/grammar/expression/switch.rs @@ -96,7 +96,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { // at statement sync level. self.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon) .widen_error_span_from(case_position) - .sync_error_until(self, crate::parser::SyncLevel::Statement) + .sync_error_until(self, crate::parser::SyncLevel::StatementStart) .report_error(self); } let mut body = self.arena.vec(); @@ -115,7 +115,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { self.advance(); // 'default' self.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon) .widen_error_span_from(default_position) - .sync_error_until(self, crate::parser::SyncLevel::Statement) + .sync_error_until(self, crate::parser::SyncLevel::StatementStart) .report_error(self); self.parse_switch_arm_body(statements); } @@ -129,7 +129,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { match token { Token::Keyword(Keyword::Case | Keyword::Default) | Token::RightBrace => break, _ => { - self.parse_next_block_item_into(statements); + self.parse_and_append_next_block_item(statements); self.ensure_forward_progress(token_position); } } diff --git a/rottlib/src/parser/grammar/statement.rs b/rottlib/src/parser/grammar/statement.rs index 97a4d39..0e2ec59 100644 --- a/rottlib/src/parser/grammar/statement.rs +++ b/rottlib/src/parser/grammar/statement.rs @@ -11,8 +11,9 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { /// Parses a single statement. /// /// Does not consume a trailing `;` except for [`Statement::Empty`]. - /// The caller handles semicolons. Returns [`Some`] if a statement is + /// The caller handles semicolons (WRONG NOW - WE MUST HANDLE THEM). Returns [`Some`] if a statement is /// recognized; otherwise [`None`]. + /// ALSO WE SPECIFICALLY DONT HANDLE EXPRESSION TYPE STATEMENTS #[must_use] pub(crate) fn parse_statement(&mut self) -> Option> { let Some((token, lexeme, position)) = self.peek_token_lexeme_and_position() else { diff --git a/rottlib/src/parser/recovery.rs b/rottlib/src/parser/recovery.rs index 18df7e7..cbf1e2a 100644 --- a/rottlib/src/parser/recovery.rs +++ b/rottlib/src/parser/recovery.rs @@ -50,7 +50,10 @@ pub enum SyncLevel { /// /// Includes `;` and keywords that begin standalone statements / /// statement-like control-flow forms. - Statement, + StatementStart, + + /// Statement terminator `;`. + StatementTerminator, /// Start of a `switch` arm. /// @@ -77,7 +80,7 @@ impl SyncLevel { use crate::lexer::Keyword; use SyncLevel::{ BlockBoundary, CloseAngleBracket, CloseBracket, CloseParenthesis, DeclarationStart, - ExpressionStart, ListSeparator, Statement, SwitchArmStart, + ExpressionStart, ListSeparator, StatementStart, StatementTerminator, SwitchArmStart, }; match token { @@ -88,8 +91,7 @@ impl SyncLevel { Token::RightBracket => Some(CloseBracket), // Statement-level boundaries - Token::Semicolon - | Token::Keyword( + Token::Keyword( Keyword::If | Keyword::Else | Keyword::Switch @@ -102,7 +104,9 @@ impl SyncLevel { | Keyword::Break | Keyword::Continue | Keyword::Local, - ) => Some(Statement), + ) => Some(StatementStart), + + Token::Semicolon => Some(StatementTerminator), // Switch-specific stronger boundary Token::Keyword(Keyword::Case | Keyword::Default) => Some(SwitchArmStart), diff --git a/rottlib/tests/parser_diagnostics/block_items.rs b/rottlib/tests/parser_diagnostics/block_items.rs new file mode 100644 index 0000000..56f434b --- /dev/null +++ b/rottlib/tests/parser_diagnostics/block_items.rs @@ -0,0 +1,455 @@ +use super::*; + +pub(super) const P0025_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0025_01.uc", + source: "{\n local int Count;\n Count = Count + 1 UpdateHud();\n DrawHud(CanvasRef);\n}\n", + }, + Fixture { + label: "files/P0025_02.uc", + source: "{\n local float XL;\n C.TextSize(LevelTitle, XL, YL)\n C.SetPos(0, 0);\n C.DrawText(LevelTitle);\n}\n", + }, + Fixture { + label: "files/P0025_03.uc", + source: "{\n local bool bReady;\n bReady = CheckReady()\n if (bReady) { StartMatch(); }\n NotifyReady();\n}\n", + }, + Fixture { + label: "files/P0025_04.uc", + source: "{\n local int I;\n Scores[I] = Scores[I] + 1\n I++;\n RefreshScores();\n}\n", + }, +]; + +pub(super) const P0026_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0026_01.uc", + source: "{\n local int Count;\n Count = 0;\n Count++;\n UpdateHud();\n", + }, + Fixture { + label: "files/P0026_02.uc", + source: "{\n local bool bReady;\n bReady = CheckReady();\n if (bReady) { StartMatch(); }\n NotifyReady();\n", + }, + Fixture { + label: "files/P0026_03.uc", + source: "{ local float XL; do { C.TextSize(LevelTitle, XL, YL); } until (XL < C.ClipX) C.SetPos(0, 0); C.DrawText(LevelTitle);", + }, + Fixture { + label: "files/P0026_04.uc", + source: "{\n local int Count;\n Count = Count + 1;\n UpdateHud();\n Count\n", + }, +]; + +pub(super) const P0025_P0026_MIXED_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P_mixed_01.uc", + source: "{ local int Count; Count = Count + 1 UpdateHud(); DrawHud(CanvasRef);", + }, + Fixture { + label: "files/P_mixed_02.uc", + source: "{\n local bool bReady;\n bReady = CheckReady()\n if (bReady) { StartMatch(); }\n NotifyReady();\n", + }, +]; + +#[test] +fn check_p0025_fixtures() { + let runs = run_fixtures(P0025_FIXTURES); + + assert_eq!(runs.get("files/P0025_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0025_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0025_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0025_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0025_01.uc"), + &ExpectedDiagnostic { + headline: "missing `;` after expression statement", + severity: Severity::Error, + code: Some("P0025"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(21), + end: TokenPosition(21), + }, + message: "expected `;` before `UpdateHud`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(11), + end: TokenPosition(19), + }, + message: "expression statement", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0025_02.uc"), + &ExpectedDiagnostic { + headline: "missing `;` after expression statement", + severity: Severity::Error, + code: Some("P0025"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(25), + end: TokenPosition(25), + }, + message: "expected `;` before `C`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(22), + end: TokenPosition(22), + }, + message: "expression statement ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0025_03.uc"), + &ExpectedDiagnostic { + headline: "missing `;` after expression statement", + severity: Severity::Error, + code: Some("P0025"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(20), + end: TokenPosition(20), + }, + message: "expected `;` before `if`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(17), + end: TokenPosition(17), + }, + message: "expression statement ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0025_04.uc"), + &ExpectedDiagnostic { + headline: "missing `;` after expression statement", + severity: Severity::Error, + code: Some("P0025"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(28), + end: TokenPosition(28), + }, + message: "expected `;` before `I`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(25), + end: TokenPosition(25), + }, + message: "expression statement ends here", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0026_fixtures() { + let runs = run_fixtures(P0026_FIXTURES); + + assert_eq!(runs.get("files/P0026_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0026_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0026_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0026_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0026_01.uc"), + &ExpectedDiagnostic { + headline: "missing `}` to close block", + severity: Severity::Error, + code: Some("P0026"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(29), + }, + message: "expected `}` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0026_02.uc"), + &ExpectedDiagnostic { + headline: "missing `}` to close block", + severity: Severity::Error, + code: Some("P0026"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(42), + }, + message: "expected `}` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0026_03.uc"), + &ExpectedDiagnostic { + headline: "missing `}` to close block", + severity: Severity::Error, + code: Some("P0026"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(59), + end: TokenPosition(59), + }, + message: "expected `}` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0026_04.uc"), + &ExpectedDiagnostic { + headline: "missing `}` to close block", + severity: Severity::Error, + code: Some("P0026"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(31), + }, + message: "expected `}` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0025_mixed_fixtures() { + let runs = run_fixtures(P0025_P0026_MIXED_FIXTURES); + + assert_eq!(runs.get("files/P_mixed_01.uc").unwrap().len(), 2); + assert_eq!(runs.get("files/P_mixed_02.uc").unwrap().len(), 2); + + assert_diagnostic( + &runs.get_by_code("files/P_mixed_01.uc", "P0025"), + &ExpectedDiagnostic { + headline: "missing `;` after expression statement", + severity: Severity::Error, + code: Some("P0025"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(19), + end: TokenPosition(19), + }, + message: "expected `;` before `UpdateHud`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(17), + }, + message: "expression statement", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P_mixed_02.uc", "P0025"), + &ExpectedDiagnostic { + headline: "missing `;` after expression statement", + severity: Severity::Error, + code: Some("P0025"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(20), + end: TokenPosition(20), + }, + message: "expected `;` before `if`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(17), + end: TokenPosition(17), + }, + message: "expression statement ends here", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0026_mixed_fixtures() { + let runs = run_fixtures(P0025_P0026_MIXED_FIXTURES); + + assert_eq!(runs.get("files/P_mixed_01.uc").unwrap().len(), 2); + assert_eq!(runs.get("files/P_mixed_02.uc").unwrap().len(), 2); + + assert_diagnostic( + &runs.get_by_code("files/P_mixed_01.uc", "P0026"), + &ExpectedDiagnostic { + headline: "missing `}` to close block", + severity: Severity::Error, + code: Some("P0026"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(29), + end: TokenPosition(29), + }, + message: "expected `}` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P_mixed_02.uc", "P0026"), + &ExpectedDiagnostic { + headline: "missing `}` to close block", + severity: Severity::Error, + code: Some("P0026"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(41), + }, + message: "expected `}` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0027_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0027_01.uc", + source: "{\n local bool bReady;\n bReady = CheckReady();\n else { StartMatch(); }\n NotifyReady();\n}\n", + }, + Fixture { + label: "files/P0027_02.uc", + source: "{ local int Count; Count = 3; case 3: Count++; UpdateHud();}", + }, + Fixture { + label: "files/P0027_03.uc", + source: "{\n local bool bDone;\n bDone = false;\n until (bDone)\n TickWork();\n}\n", + }, + Fixture { + label: "files/P0027_04.uc", + source: "{\n local int Count;\n Count = 0;\n #exec TEXTURE IMPORT NAME=Bad FILE=Bad.bmp\n Count++;\n}\n", + }, +]; + +#[test] +fn check_p0027_fixtures() { + let runs = run_fixtures(P0027_FIXTURES); + + assert_eq!(runs.get("files/P0027_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0027_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0027_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0027_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0027_01.uc"), + &ExpectedDiagnostic { + headline: "expected statement or expression, found `else`", + severity: Severity::Error, + code: Some("P0027"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(21), + end: TokenPosition(21), + }, + message: "unexpected `else`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0027_02.uc"), + &ExpectedDiagnostic { + headline: "expected statement or expression, found `case`", + severity: Severity::Error, + code: Some("P0027"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(16), + end: TokenPosition(16), + }, + message: "unexpected `case`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0027_03.uc"), + &ExpectedDiagnostic { + headline: "expected statement or expression, found `until`", + severity: Severity::Error, + code: Some("P0027"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(19), + end: TokenPosition(19), + }, + message: "unexpected `until`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0027_04.uc"), + &ExpectedDiagnostic { + headline: "expected statement or expression, found `#exec` directive", + severity: Severity::Error, + code: Some("P0027"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(19), + end: TokenPosition(19), + }, + message: "`#exec` directives are not allowed in a statement block", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); +} \ No newline at end of file diff --git a/rottlib/tests/parser_diagnostics/mod.rs b/rottlib/tests/parser_diagnostics/mod.rs index c61c0bb..426a3ef 100644 --- a/rottlib/tests/parser_diagnostics/mod.rs +++ b/rottlib/tests/parser_diagnostics/mod.rs @@ -5,6 +5,7 @@ use rottlib::diagnostics::{Diagnostic, Severity}; use rottlib::lexer::{TokenPosition, TokenSpan, TokenizedFile}; use rottlib::parser::Parser; +mod block_items; mod control_flow_expressions; mod primary_expressions;