Improve block body's diagnostics

This commit is contained in:
dkanus 2026-04-29 20:13:58 +07:00
commit b1f0714483
18 changed files with 783 additions and 119 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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`.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<StatementRef<'src, 'arena>> {
let Some((token, lexeme, position)) = self.peek_token_lexeme_and_position() else {

View file

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