Improve switch's diagnostics

This commit is contained in:
dkanus 2026-05-02 19:40:37 +07:00
parent e29ffb2a9c
commit f695f8a52e
17 changed files with 2715 additions and 244 deletions

View File

@ -16,43 +16,163 @@ 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)] = &[
// P0031 - FunctionCallArgumentMissingComma
// P0034 - SwitchMissingBody
("files/P0034_01.uc", "switch(A) local\n"),
("files/P0034_02.uc", "switch\n(A)\nvar"),
("files/P0034_03.uc", "switch(\n A\n)\n"),
("files/P0034_04.uc", "switch\n(\n A\n)\n"),
("files/P0034_05.uc", "switch(A)\ncase 1:\n"),
// P0035 - SwitchTopLevelItemNotCase
("files/P0035_01.uc", "switch(A) {\n Log(\"bad\");\n}\n"),
(
"files/P0031_01.uc",
"{\n Func(A B);\n Log(\"after\");\n}\n",
"files/P0035_02.uc",
"switch\n(A)\n{\n Log(\"bad\");\n Log(\"worse\");\n case 1:\n}\n",
),
("files/P0035_03.uc", "switch(A) {\n 123;\n default:\n}\n"),
(
"files/P0035_04.uc",
"switch\n(\n A\n)\n{\n if (A) {}\n case 1:\n}\n",
),
(
"files/P0031_02.uc",
"{\n Func\n (A 123);\n Log(\"after\");\n}\n",
"files/P0035_05.uc",
"switch(A) {\n {\n Log(\"nested\");\n }\n case 1:\n}\n",
),
// P0036 - SwitchCaseMissingColon
(
"files/P0036_01.uc",
"switch(A) {\n case 1\n case 2:\n}\n",
),
(
"files/P0031_03.uc",
"{\n Func(\n A\n new SomeClass\n );\n Log(\"after\");\n}\n",
),
// P0032 - FunctionCallMissingClosingParenthesis
("files/P0032_01.uc", "Func("),
("files/P0032_02.uc", "Func\n(\n A,"),
("files/P0032_03.uc", "Func(A,\n B,"),
(
"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/P0036_02.uc",
"switch\n(A)\n{\n case\n 1\n default:\n}\n",
),
(
"files/P0033_02.uc",
"{\n Func\n (A\n :,\n B);\n Log(\"after\");\n}\n",
"files/P0036_03.uc",
"switch(A) {\n case (A)\n case B:\n}\n",
),
(
"files/P0033_03.uc",
"{\n Func(\n A ?\n , B\n );\n Log(\"after\");\n}\n",
"files/P0036_04.uc",
"switch\n(\n A\n)\n{\n case\n A + B\n default\n :\n}\n",
),
(
"files/P0033_04.uc",
"{\n Func\n (\n A\n #,\n B\n );\n Log(\"after\");\n}\n",
"files/P0036_05.uc",
"switch(A) {\n case Foo.Bar(Baz)\n case Other:\n}\n",
),
// P0037 - SwitchDefaultMissingColon
(
"files/P0037_01.uc",
"switch(A) {\n default\n if (A) {}\n}\n",
),
(
"files/P0037_02.uc",
"switch\n(A)\n{\n default\n while (A) {}\n}\n",
),
(
"files/P0037_03.uc",
"switch(A) {\n default\n for (;;) {}\n}\n",
),
(
"files/P0037_04.uc",
"switch\n(\n A\n)\n{\n default\n switch(B) {\n case 1:\n }\n}\n",
),
(
"files/P0037_05.uc",
"switch(A) {\n default\n case 1:\n}\n",
),
// P0038 - SwitchDuplicateDefault
(
"files/P0038_01.uc",
"switch(A) {\n default:\n default:\n}\n",
),
(
"files/P0038_02.uc",
"switch\n(A)\n{\n default\n :\n default\n :\n}\n",
),
(
"files/P0038_03.uc",
"switch(A) {\n default:\n default:\n default:\n}\n",
),
(
"files/P0038_04.uc",
"switch\n(\n A\n)\n{\n default:\n Log(\"first\");\n default:\n Log(\"second\");\n}\n",
),
// P0039 - SwitchCasesAfterDefault
(
"files/P0039_01.uc",
"switch(A) {\n default:\n case 1:\n}\n",
),
(
"files/P0039_02.uc",
"switch\n(A)\n{\n default\n :\n case\n 1\n :\n}\n",
),
(
"files/P0039_03.uc",
"switch(A) {\n default:\n case 1:\n case 2:\n}\n",
),
(
"files/P0039_04.uc",
"switch\n(\n A\n)\n{\n default:\n case 1:\n Log(\"one\");\n case 2:\n Log(\"two\");\n}\n",
),
(
"files/P0039_05.uc",
"switch(A) {\n default:\n Log(\"done\");\n case 1:\n case 2:\n Log(\"stacked\");\n}\n",
),
// P0040 - SwitchMissingClosingBrace
("files/P0040_01.uc", "switch(A) {\n"),
("files/P0040_02.uc", "switch(A) {\n case 1:\n"),
("files/P0040_03.uc", "switch\n(A)\n{\n default:\n"),
(
"files/P0040_04.uc",
"switch\n(\n A\n)\n{\n case 1:\n case 2:\n",
),
(
"files/P0040_05.uc",
"switch(A) {\n case 1:\n Log(\"body\");\n",
),
// P0041 - SwitchCaseMissingExpression
("files/P0041_01.uc", "switch(A) {\n case:\n}\n"),
("files/P0041_02.uc", "switch\n(A)\n{\n case\n :\n}\n"),
("files/P0041_03.uc", "switch(A) {\n case:\n default:\n}\n"),
(
"files/P0041_04.uc",
"switch\n(\n A\n)\n{\n case\n :\n case 1:\n}\n",
),
(
"files/P0041_05.uc",
"switch(A) {\n case\n :\n default:\n}\n",
),
// P0042 - SwitchCaseExpressionInvalidStart
("files/P0042_01.uc", "switch(A) {\n case *:\n}\n"),
(
"files/P0042_02.uc",
"switch\n(A)\n{\n case\n =\n :\n}\n",
),
("files/P0042_03.uc", "switch(A) {\n case &&:\n default:\n}\n"),
(
"files/P0042_04.uc",
"switch\n(\n A\n)\n{\n case\n .\n :\n case 1:\n}\n",
),
(
"files/P0042_05.uc",
"switch(A) {\n case ]:\n}\n",
),
// Mixed / intentional cascades
(
"files/P0042_cascade_01.uc",
"switch(A) {\n case *\n case 1:\n}\n",
),
(
"files/P0042_cascade_02.uc",
"switch\n(\n A\n)\n{\n case\n =\n default:\n}\n",
),
];

View File

@ -21,6 +21,7 @@ mod block_items;
mod control_flow_expressions;
mod primary_expressions;
mod selector_expressions;
mod switch_expressions;
#[derive(Clone, Copy)]
enum FoundAt<'src> {
@ -78,6 +79,7 @@ pub(crate) fn diagnostic_from_parse_error<'src>(
use control_flow_expressions::*;
use primary_expressions::*;
use selector_expressions::*;
use switch_expressions::*;
match error.kind {
// primary_expressions.rs
ParseErrorKind::ParenthesizedExpressionInvalidStart => {
@ -165,6 +167,28 @@ pub(crate) fn diagnostic_from_parse_error<'src>(
ParseErrorKind::FunctionCallUnexpectedTokenInArgumentList => {
diagnostic_function_call_unexpected_token_in_argument_list(error, file)
}
// switch_expressions.rs
ParseErrorKind::SwitchMissingBody => diagnostic_switch_missing_body(error, file),
ParseErrorKind::SwitchTopLevelItemNotCase => {
diagnostic_switch_top_level_item_not_case(error, file)
}
ParseErrorKind::SwitchCaseMissingColon => diagnostic_switch_case_missing_colon(error, file),
ParseErrorKind::SwitchDefaultMissingColon => {
diagnostic_switch_default_missing_colon(error, file)
}
ParseErrorKind::SwitchDuplicateDefault => diagnostic_switch_duplicate_default(error, file),
ParseErrorKind::SwitchCasesAfterDefault => {
diagnostic_switch_cases_after_default(error, file)
}
ParseErrorKind::SwitchMissingClosingBrace => {
diagnostic_switch_missing_closing_brace(error, file)
}
ParseErrorKind::SwitchCaseMissingExpression => {
diagnostic_switch_case_missing_expression(error, file)
}
ParseErrorKind::SwitchCaseExpressionInvalidStart => {
diagnostic_switch_case_expression_invalid_start(error, file)
}
_ => DiagnosticBuilder::error(format!("error {:?} while parsing", error.kind))
.primary_label(error.covered_span, "happened here")

View File

@ -0,0 +1,417 @@
use super::{Diagnostic, DiagnosticBuilder, FoundAt, found_at, should_show_context_label};
use crate::lexer::{TokenSpan, TokenizedFile};
use crate::parser::ParseError;
const SWITCH_KEYWORD: &str = "switch_keyword";
const LEFT_BRACE: &str = "left_brace";
const CASE_KEYWORD: &str = "case_keyword";
const DEFAULT_KEYWORD: &str = "default_keyword";
const CASE_EXPRESSION: &str = "case_expression";
const FIRST_DEFAULT: &str = "first_default";
const DUPLICATE_DEFAULT_PREFIX: &str = "duplicate_default";
const CASE_AFTER_DEFAULT_PREFIX: &str = "case_after_default";
fn related_span(error: &ParseError, label: &str) -> Option<TokenSpan> {
error.related_spans.get(label).copied()
}
fn context_span_if_useful(
file: &TokenizedFile<'_>,
context_span: Option<TokenSpan>,
blame_span: TokenSpan,
) -> Option<TokenSpan> {
context_span.filter(|span| should_show_context_label(file, *span, blame_span))
}
fn primary_span_with_context(
context_span: Option<TokenSpan>,
blame_span: TokenSpan,
) -> TokenSpan {
match context_span {
Some(context_span) => TokenSpan {
start: context_span.start,
end: blame_span.end,
},
None => blame_span,
}
}
fn same_span(left: TokenSpan, right: TokenSpan) -> bool {
left.start == right.start && left.end == right.end
}
fn switch_keyword_context_span(
file: &TokenizedFile<'_>,
error: &ParseError,
blame_span: TokenSpan,
left_brace_span: Option<TokenSpan>,
) -> Option<TokenSpan> {
let switch_keyword_span = related_span(error, SWITCH_KEYWORD)?;
if file.same_line(switch_keyword_span.start, blame_span.end) {
return None;
}
if let Some(left_brace_span) = left_brace_span
&& file.same_line(switch_keyword_span.start, left_brace_span.start)
{
return None;
}
Some(switch_keyword_span)
}
fn switch_case_label_context_span(
file: &TokenizedFile<'_>,
error: &ParseError,
blame_span: TokenSpan,
) -> Option<TokenSpan> {
let case_keyword_span = related_span(error, CASE_KEYWORD)?;
let case_label_span = match related_span(error, CASE_EXPRESSION) {
Some(case_expression_span) => TokenSpan {
start: case_keyword_span.start,
end: case_expression_span.end,
},
None => case_keyword_span,
};
context_span_if_useful(file, Some(case_label_span), blame_span)
}
fn numbered_related_count(error: &ParseError, prefix: &str) -> usize {
let mut count = 0;
for index in 1.. {
let label = format!("{}_{}", prefix, index);
if related_span(error, &label).is_none() {
break;
}
count += 1;
}
count
}
fn add_numbered_related_labels(
mut builder: DiagnosticBuilder,
error: &ParseError,
prefix: &str,
primary_span: TokenSpan,
first_text: &'static str,
later_text: &'static str,
) -> DiagnosticBuilder {
for index in 1.. {
let label = format!("{}_{}", prefix, index);
let Some(span) = related_span(error, &label) else {
break;
};
if same_span(span, primary_span) {
continue;
}
let text = if index == 1 { first_text } else { later_text };
builder = builder.secondary_label(span, text);
}
builder
}
/// P0034
pub(super) fn diagnostic_switch_missing_body(
error: ParseError,
file: &TokenizedFile<'_>,
) -> Diagnostic {
let blame_span = error.blame_span;
let switch_context_span =
context_span_if_useful(file, related_span(&error, SWITCH_KEYWORD), blame_span);
let primary_span = primary_span_with_context(switch_context_span, blame_span);
let primary_text = match found_at(file, blame_span.start) {
FoundAt::Token(token_text) => format!("expected `{{` before `{}`", token_text),
FoundAt::EndOfFile => "expected `{` after the switch expression".to_string(),
FoundAt::Unknown => "expected `{` here".to_string(),
};
DiagnosticBuilder::error("missing `{` to start `switch` body")
.primary_label(primary_span, primary_text)
.code("P0034")
.build()
}
/// P0035
pub(super) fn diagnostic_switch_top_level_item_not_case(
error: ParseError,
file: &TokenizedFile<'_>,
) -> Diagnostic {
let blame_span = error.blame_span;
let left_brace_span = related_span(&error, LEFT_BRACE);
let left_brace_context_span = context_span_if_useful(file, left_brace_span, blame_span);
let switch_keyword_context_span =
switch_keyword_context_span(file, &error, blame_span, left_brace_span);
let primary_text = if matches!(found_at(file, blame_span.start), FoundAt::Token("{")) {
"this block must be inside a `case` or `default` section"
} else if related_span(&error, "multiple_items").is_some() {
"these statements must be inside a `case` or `default` section"
} else {
"this statement must be inside a `case` or `default` section"
};
let mut builder =
DiagnosticBuilder::error("expected `case` or `default` section label in switch body");
if let Some(switch_keyword_context_span) = switch_keyword_context_span {
builder = builder.secondary_label(switch_keyword_context_span, "`switch` starts here");
}
if let Some(left_brace_context_span) = left_brace_context_span {
builder = builder.secondary_label(left_brace_context_span, "switch body starts here");
}
builder
.primary_label(blame_span, primary_text)
.code("P0035")
.build()
}
/// P0036
pub(super) fn diagnostic_switch_case_missing_colon(
error: ParseError,
file: &TokenizedFile<'_>,
) -> Diagnostic {
let blame_span = error.blame_span;
let case_label_context_span = switch_case_label_context_span(file, &error, 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 `:` after `case` label");
if let Some(case_label_context_span) = case_label_context_span {
builder = builder.secondary_label(
case_label_context_span,
"this `case` label needs a trailing `:`",
);
}
builder
.primary_label(blame_span, primary_text)
.code("P0036")
.build()
}
/// P0037
pub(super) fn diagnostic_switch_default_missing_colon(
error: ParseError,
file: &TokenizedFile<'_>,
) -> Diagnostic {
let blame_span = error.blame_span;
let default_context_span =
context_span_if_useful(file, related_span(&error, DEFAULT_KEYWORD), 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 `:` after `default`");
if let Some(default_context_span) = default_context_span {
builder = builder.secondary_label(
default_context_span,
"this `default` label needs a trailing `:`",
);
}
builder
.primary_label(blame_span, primary_text)
.code("P0037")
.build()
}
/// P0038
pub(super) fn diagnostic_switch_duplicate_default(
error: ParseError,
_file: &TokenizedFile<'_>,
) -> Diagnostic {
let blame_span = error.blame_span;
let duplicate_count = numbered_related_count(&error, DUPLICATE_DEFAULT_PREFIX);
let title = if duplicate_count > 1 {
"multiple `default` sections in switch"
} else {
"duplicate `default` section in switch"
};
let mut builder = DiagnosticBuilder::error(title);
if let Some(first_default_span) = related_span(&error, FIRST_DEFAULT)
&& !same_span(first_default_span, blame_span)
{
builder = builder.secondary_label(first_default_span, "first `default` section is here");
}
builder = add_numbered_related_labels(
builder,
&error,
DUPLICATE_DEFAULT_PREFIX,
blame_span,
"duplicate `default` section",
"another duplicate `default` section",
);
builder
.primary_label(blame_span, "duplicate `default` section")
.code("P0038")
.build()
}
/// P0039
pub(super) fn diagnostic_switch_cases_after_default(
error: ParseError,
_file: &TokenizedFile<'_>,
) -> Diagnostic {
let blame_span = error.blame_span;
let case_after_default_count = numbered_related_count(&error, CASE_AFTER_DEFAULT_PREFIX);
let title = if case_after_default_count > 1 {
"multiple `case` sections appear after `default`"
} else {
"`case` section appears after `default`"
};
let mut builder = DiagnosticBuilder::error(title);
if let Some(first_default_span) = related_span(&error, FIRST_DEFAULT)
&& !same_span(first_default_span, blame_span)
{
builder = builder.secondary_label(
first_default_span,
"`default` must be the last section in this switch",
);
}
builder = add_numbered_related_labels(
builder,
&error,
CASE_AFTER_DEFAULT_PREFIX,
blame_span,
"`case` after `default`",
"another `case` after `default`",
);
builder
.primary_label(blame_span, "`case` after `default`")
.code("P0039")
.build()
}
/// P0040
pub(super) fn diagnostic_switch_missing_closing_brace(
error: ParseError,
file: &TokenizedFile<'_>,
) -> Diagnostic {
let blame_span = error.blame_span;
let left_brace_span = related_span(&error, LEFT_BRACE);
let left_brace_context_span = context_span_if_useful(file, left_brace_span, blame_span);
let switch_keyword_context_span =
switch_keyword_context_span(file, &error, blame_span, left_brace_span);
let primary_span = primary_span_with_context(left_brace_context_span, 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 switch body");
if let Some(switch_keyword_context_span) = switch_keyword_context_span {
builder = builder.secondary_label(switch_keyword_context_span, "`switch` starts here");
}
builder
.primary_label(primary_span, primary_text)
.code("P0040")
.build()
}
/// P0041
pub(super) fn diagnostic_switch_case_missing_expression(
error: ParseError,
file: &TokenizedFile<'_>,
) -> Diagnostic {
let blame_span = error.blame_span;
let case_context_span =
context_span_if_useful(file, related_span(&error, CASE_KEYWORD), blame_span);
let primary_text = match found_at(file, blame_span.start) {
FoundAt::Token(":") => "expected expression before `:`".to_string(),
FoundAt::Token(token_text) => format!("expected expression before `{}`", token_text),
FoundAt::EndOfFile => "expected expression before end of file".to_string(),
FoundAt::Unknown => "expected expression here".to_string(),
};
let mut builder = DiagnosticBuilder::error("missing expression after `case`");
if let Some(case_context_span) = case_context_span {
builder = builder.secondary_label(
case_context_span,
"after this `case`, an expression was expected",
);
}
builder
.primary_label(blame_span, primary_text)
.code("P0041")
.build()
}
/// P0042
pub(super) fn diagnostic_switch_case_expression_invalid_start(
error: ParseError,
file: &TokenizedFile<'_>,
) -> Diagnostic {
let blame_span = error.blame_span;
let case_context_span =
context_span_if_useful(file, related_span(&error, CASE_KEYWORD), blame_span);
let found = found_at(file, blame_span.start);
let title = match found {
FoundAt::Token(token_text) => {
format!("expected expression after `case`, found `{}`", token_text)
}
FoundAt::EndOfFile => "expected expression after `case`, found end of file".to_string(),
FoundAt::Unknown => "expected expression after `case`".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 expression here".to_string(),
};
let mut builder = DiagnosticBuilder::error(title);
if let Some(case_context_span) = case_context_span {
builder = builder.secondary_label(
case_context_span,
"after this `case`, an expression was expected",
);
}
builder
.primary_label(blame_span, primary_text)
.code("P0042")
.build()
}

View File

@ -8,7 +8,22 @@ use std::collections::HashMap;
use std::ops::RangeInclusive;
const INDENT: &str = " ";
/*
error[P0034]: missing `{` to start `switch` body
in file: files/P0034_04.uc
1 | switch
| ------ `switch` starts here
2 | (
3 | A
4 | )
| ^ expected `{` before end of file
| ^ switch selector is here
Outmost guideline isn't reaching ^!. And should it be `^` even?
*/
/*
error: expected one of `,`, `:`, or `}`, found `token_to`
--> rottlib/src/ast/mod.rs:80:13

View File

@ -2,7 +2,7 @@
use std::collections::HashMap;
use crate::{lexer::TokenSpan, lexer::TokenPosition};
use crate::{lexer::TokenPosition, lexer::TokenSpan};
/// Internal parse error kinds.
///
@ -82,6 +82,24 @@ pub enum ParseErrorKind {
FunctionCallMissingClosingParenthesis,
/// P0033
FunctionCallUnexpectedTokenInArgumentList,
/// P0034
SwitchMissingBody,
/// P0035
SwitchTopLevelItemNotCase,
/// P0036
SwitchCaseMissingColon,
/// P0037
SwitchDefaultMissingColon,
/// P0038
SwitchDuplicateDefault,
/// P0039
SwitchCasesAfterDefault,
/// P0040
SwitchMissingClosingBrace,
/// P0041
SwitchCaseMissingExpression,
/// P0042
SwitchCaseExpressionInvalidStart,
// ================== Old errors to be thrown away! ==================
/// Found an unexpected token while parsing an expression.
ExpressionUnexpectedToken,
@ -99,17 +117,6 @@ pub enum ParseErrorKind {
TypeSpecClassMissingInnerType,
TypeSpecClassMissingClosingAngle,
BlockMissingSemicolonAfterStatement,
/// `switch` has no body (missing matching braces).
SwitchMissingBody,
/// The first top-level item in a `switch` body is not a `case`.
SwitchTopLevelItemNotCase,
/// A `case` arm is missing the trailing `:`.
SwitchCaseMissingColon,
/// Found more than one `default` branch.
SwitchDuplicateDefault,
/// Found `case` arms after a `default` branch.
SwitchCasesAfterDefault,
SwitchMissingClosingBrace,
/// Unexpected end of input while parsing.
UnexpectedEndOfFile,
/// Token looked like a numeric literal but could not be parsed as one.

View File

@ -82,10 +82,10 @@ impl<'src, 'arena> Parser<'src, 'arena> {
/// This method never consumes the closing `}` and is only meant to be
/// 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.
/// On success, it appends exactly one statement and advances past at least
/// one token. If statement cannot 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 StatementList<'src, 'arena>,
@ -126,23 +126,30 @@ impl<'src, 'arena> Parser<'src, 'arena> {
Ok(())
}
/// Parses a non-statement starter as an expression statement inside
/// a block.
///
/// On success, returns an expression statement whose expression parser has
/// consumed at least one token. If expression parsing fails and recovery
/// cannot consume anything locally, returns the unreported error instead of
/// producing a zero-width fallback statement.
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(
let expression_result = self.parse_required_expression(
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.
// Without progress, a fallback statement would violate this
// function's success contract and could leave the enclosing
// block loop stuck.
if expression_recovery_made_no_progress {
return self.recover_bad_block_item_start_as_error_statement(error);
}

View File

@ -1,4 +1,28 @@
//! Parser for `for` loop expressions in Fermented UnrealScript.
//!
//! ## Disambiguation of `for` as loop vs expression
//!
//! Unlike other control-flow keywords, `for` is disambiguated from functions
//! and variables with the same name. This is done syntactically in
//! [`Parser::peek_for_loop_header_left_parenthesis_position`]: a `for` token
//! followed by a `(` whose contents contain a top-level `;` is unambiguously
//! a loop header.
//!
//! This rule is lightweight, local, and robust, and mirrors the fixed grammar
//! `for (init; condition; step)` without requiring name resolution.
//!
//! ### Why this is not done for `if` / `while` / `do`
//!
//! There is no similarly reliable way to discriminate `if`, `while`, or related
//! keywords at this stage of parsing: their parenthesized forms are
//! indistinguishable from single-argument function calls.
//!
//! Supporting these keywords as identifiers would complicate parsing
//! disproportionately and we always treat them as openers for conditional and
//! loop expressions. This matches common `UnrealScript` usage and
//! intentionally drops support for moronic design choices where such names were
//! reused as variables or functions (like what author did by declaring
//! a `For` function in Acedia).
use crate::ast::{Expression, ExpressionRef, OptionalExpression};
use crate::lexer::{self, Token, TokenPosition};
@ -115,7 +139,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
&& next_token != terminator_token
{
Some(
self.parse_expression_with_start_error(
self.parse_required_expression_with_context(
invalid_start_error_kind,
for_keyword_position,
component_start_anchor_position,

View File

@ -18,9 +18,9 @@
//! legacy cut-off rule:
//!
//! If the condition begins with a parenthesized expression, and the token after
//! the matching `)` is identifier-like, the parenthesized expression is treated
//! as the whole condition. The following identifier-like token is left for the
//! branch body.
//! the matching `)` is identifier-like, the parser treats only the
//! parenthesized expression as the condition. The following identifier-like
//! token is left for the branch body.
//!
//! This prevents the parser from accidentally consuming the following
//! statement/body as part of the condition in older code such as:
@ -30,8 +30,8 @@
//! ```
//!
//! Without the legacy cut-off, a permissive expression parser could interpret
//! `Cross` as a continuation of the condition in dialects where identifier-like
//! tokens may participate in operator syntax.
//! the identifier-like body opener, such as `Cross`, as an operator-like
//! continuation of the parenthesized condition.
//!
//! Operator tokens such as `*`, `+`, `<`, `==`, etc. do not trigger this
//! legacy cut-off. They allow the normal expression parser to continue the
@ -41,43 +41,6 @@
//! a custom/named operator, the parser prefers the legacy interpretation and
//! ends the condition at the closing `)`. Write the condition with additional
//! parentheses or use an unambiguous operator form.
//!
//! ## Disambiguation of `for` as loop vs expression
//!
//! Unlike other control-flow keywords, `for` is disambiguated from functions
//! and variables with the same name. This is done syntactically in
//! [`Parser::is_for_loop_header_ahead`]: a `for` token followed by
//! a `(` whose contents contain a top-level `;` is unambiguously a loop header.
//!
//! This rule is lightweight, local, and robust, and mirrors the fixed grammar
//! `for (init; condition; step)` without requiring name resolution.
//!
//! ### Why this is not done for `if` / `while` / `do`
//!
//! There is no similarly reliable way to discriminate `if`, `while`, or related
//! keywords at this stage of parsing: their parenthesized forms are
//! indistinguishable from single argument function calls.
//!
//! Supporting these keywords as identifiers would complicate parsing
//! disproportionately and we always treat them as openers for conditional and
//! loop expressions. This matches common `UnrealScript` usage and
//! intentionally drops support for moronic design choices where such names were
//! reused as variables or functions (like what author did by declaring
//! a `For` function in Acedia).
//!
//! ### But what about `switch`?
//!
//! `switch` is handled separately because, in existing `UnrealScript` code,
//! it may appear either as a keyword-led construct or as an identifier.
//!
//! Its disambiguation rule is simpler than for `for`: if the next token is
//! `(`, `switch` is parsed as a `switch` expression; otherwise it remains
//! available as an identifier.
//!
//! This rule is local and purely syntactic, matching the behavior expected by
//! the existing codebase we support. The actual parsing of `switch` expressions
//! lives in a separate module because the construct itself is more involved
//! than the control-flow forms handled here.
use crate::ast::{BranchBody, Expression, ExpressionRef};
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};
@ -210,7 +173,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
branch_owner_keyword_position: TokenPosition,
) -> BranchBody<'src, 'arena> {
let branch_expression = self
.parse_expression_with_start_error(
.parse_required_expression_with_context(
ParseErrorKind::ControlFlowBodyExpected,
branch_owner_keyword_position,
self.last_consumed_position_or_start(),
@ -415,10 +378,9 @@ impl<'src, 'arena> Parser<'src, 'arena> {
None | Some(Token::Semicolon) => (None, TokenSpan::new(return_keyword_position)),
_ => {
let return_value = self
.parse_expression_with_start_error(
.parse_required_expression(
ParseErrorKind::ReturnValueInvalidStart,
return_keyword_position,
return_keyword_position,
)
.unwrap_or_fallback(self);
let span = TokenSpan::range(return_keyword_position, return_value.span().end);
@ -444,10 +406,9 @@ impl<'src, 'arena> Parser<'src, 'arena> {
None | Some(Token::Semicolon) => (None, TokenSpan::new(break_keyword_position)),
_ => {
let break_value = self
.parse_expression_with_start_error(
.parse_required_expression(
ParseErrorKind::BreakValueInvalidStart,
break_keyword_position,
break_keyword_position,
)
.unwrap_or_fallback(self);
let span = TokenSpan::range(break_keyword_position, break_value.span().end);

View File

@ -86,15 +86,35 @@ impl<'src, 'arena> Parser<'src, 'arena> {
/// `expression_context_position` identifies the local syntactic anchor after
/// which the expression was expected. It is attached to the diagnostic with
/// the [`diagnostic_labels::EXPRESSION_EXPECTED_AFTER`] label.
pub(super) fn parse_expression_with_start_error(
pub(super) fn parse_required_expression(
&mut self,
bad_start_error_kind: ParseErrorKind,
required_by_position: crate::lexer::TokenPosition,
expression_context_position: crate::lexer::TokenPosition,
required_by_position: TokenPosition,
) -> ParseExpressionResult<'src, 'arena> {
if self.next_token_definitely_cannot_start_expression() {
let error_position = self.peek_position_or_eof();
return Err(self
.make_error_at(bad_start_error_kind, error_position)
.sync_error_until(self, crate::parser::SyncLevel::ExpressionStart)
.blame_token(error_position)
.related_token(
diagnostic_labels::EXPRESSION_REQUIRED_BY,
required_by_position,
));
}
self.parse_expression_with_min_precedence_rank(PrecedenceRank::LOOSEST)
}
pub(super) fn parse_required_expression_with_context(
&mut self,
bad_start_error_kind: ParseErrorKind,
required_by_position: TokenPosition,
expression_context_position: TokenPosition,
) -> ParseExpressionResult<'src, 'arena> {
if self.next_token_definitely_cannot_start_expression() {
let error_position = self.peek_position_or_eof();
//self.advance();
return Err(self
.make_error_at(bad_start_error_kind, error_position)
@ -109,6 +129,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
expression_context_position,
));
}
self.parse_expression_with_min_precedence_rank(PrecedenceRank::LOOSEST)
}
@ -159,9 +180,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
// 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)
);
return Err(self.make_error_at(ParseErrorKind::ExpressionExpected, token_position));
}
self.advance();
if let Ok(operator) = ast::PrefixOperator::try_from(token) {

View File

@ -2,7 +2,7 @@
//!
//! This module implements parsing of primary expressions via
//! [`Parser::parse_primary_from_current_token`] and its helper
//! [`Parser::parse_keyword_primary`].
//! [`Parser::try_parse_keyword_primary`].
//!
//! ## What is a "primary expression" here?
//!
@ -25,6 +25,34 @@
//! So "primary" here does not mean "smallest atomic expression".
//! It means "an expression form that does not need a left-hand side
//! in order to be parsed".
//!
//! ## Keyword-led primaries and identifier fallback
//!
//! Some lexer keywords are always parsed as keyword-led primary expressions
//! in expression position: `if`, `while`, `do`, `foreach`, `return`, `break`,
//! `continue`, `new`, `true`, `false`, and `none`.
//!
//! Other keywords are accepted as keyword-led forms only when the following
//! tokens commit to that syntax. Otherwise they remain available as
//! identifier-like primaries.
//!
//! - `for` is parsed as a loop only when followed by a parenthesized header
//! containing a top-level `;`, matching `for (init; condition; step)`.
//! - `switch` is parsed as a switch expression only when followed by `(`.
//! - `goto` is parsed as a label jump only when it is not followed by `(`.
//! - `class` is parsed as a class type expression only when followed by `<`.
//!
//! These rules are local and syntactic. They avoid name resolution while still
//! supporting existing legacy code that uses some keywords as ordinary names.
//!
//! ### Why is `switch` handled differently?
//!
//! `switch` is handled differently because, in existing `UnrealScript` code,
//! it may appear either as a keyword-led construct or as an identifier.
//!
//! Its disambiguation rule is simpler than for `for`: if the next token is
//! `(`, `switch` is parsed as a `switch` expression; otherwise it remains
//! available as an identifier.
use crate::ast::{Expression, ExpressionRef, OptionalExpression};
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};

View File

@ -7,6 +7,7 @@ use crate::parser::{ParseErrorKind, Parser, ResultRecoveryExt, SyncLevel};
/// Determines how parsing of the class specifier proceeds after the optional
/// `new(...)` argument list has been parsed or recovered.
#[must_use]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
enum NewClassSpecifierParseAction {
Parse,

View File

@ -104,7 +104,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
left_bracket_position: TokenPosition,
) -> ParseExpressionResult<'src, 'arena> {
let index_expression = self
.parse_expression_with_start_error(
.parse_required_expression_with_context(
ParseErrorKind::IndexMissingExpression,
left_hand_side.span().end,
left_bracket_position,

View File

@ -1,176 +1,508 @@
//! Switch parsing for Fermented `UnrealScript`.
//! Parsing for `switch (...) { ... }` expressions in Fermented UnrealScript.
//!
//! Provides routines for parsing `switch (...) { ... }` expressions.
use crate::arena::ArenaVec;
use crate::ast::{ExpressionRef, StatementRef};
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};
use crate::parser::{ParseErrorKind, ResultRecoveryExt};
//! Dispatch into this module happens only after `primary.rs` has committed to
//! keyword-led `switch` syntax. That commitment is purely syntactic: `switch`
//! followed by `(` is treated as a switch expression; otherwise `switch` may
//! still be parsed as an identifier-like primary.
//!
//! This module owns parsing of the selector, body braces, `case` labels,
//! `default`, duplicate-default diagnostics, cases-after-default diagnostics,
//! and recovery for invalid top-level switch items.
impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
/// Parses a `switch` expression after the `switch` keyword has been
/// consumed.
use crate::arena::ArenaVec;
use crate::ast::{self, ExpressionRef, StatementList, StatementRef, SwitchCaseRef};
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};
use crate::parser::{
ParseError, ParseErrorKind, ParseResult, Parser, ResultRecoveryExt, SyncLevel,
};
/*
Parser structure:
switch body = everything between `{` and `}`
switch section = one labeled part: `case ...:` body or `default:` body
case labels = stacked `case <expr>:` labels
section body = statements until `case`, `default`, or `}`
preamble = invalid statements before the first section label
parse_switch_tail
parse_switch_header_tail
parse_switch_sections_tail
parse_case_section_into_state
parse_case_labels
expect_case_label_colon
parse_switch_section_body
parse_default_section_into_state
parse_switch_section_body
parse_invalid_switch_preamble
parse_switch_section_body
*/
#[derive(Debug)]
struct SwitchParseState<'src, 'arena> {
switch_keyword_position: TokenPosition,
left_brace_position: TokenPosition,
selector: ExpressionRef<'src, 'arena>,
cases: ArenaVec<'arena, SwitchCaseRef<'src, 'arena>>,
default_arm: Option<StatementList<'src, 'arena>>,
// Retained until the full switch is parsed so diagnostics can report all
// duplicate `default`s and all `case`s that follow the first `default`.
default_keyword_positions: Vec<TokenPosition>,
case_keyword_positions_after_default: Vec<TokenPosition>,
span: TokenSpan,
}
impl<'src, 'arena> SwitchParseState<'src, 'arena> {
#[must_use]
fn new(
switch_keyword_position: TokenPosition,
left_brace_position: TokenPosition,
selector: ExpressionRef<'src, 'arena>,
cases: ArenaVec<'arena, SwitchCaseRef<'src, 'arena>>,
) -> Self {
Self {
switch_keyword_position,
left_brace_position,
selector,
cases,
default_arm: None,
default_keyword_positions: Vec::new(),
case_keyword_positions_after_default: Vec::new(),
span: TokenSpan::new(switch_keyword_position),
}
}
#[must_use]
fn has_default(&self) -> bool {
!self.default_keyword_positions.is_empty()
}
#[must_use]
fn first_default_keyword_position(&self) -> Option<TokenPosition> {
self.default_keyword_positions.first().copied()
}
#[must_use]
fn diagnostic_context(&self) -> SwitchDiagnosticContext {
SwitchDiagnosticContext {
switch_keyword_position: self.switch_keyword_position,
selector_span: *self.selector.span(),
left_brace_position: self.left_brace_position,
}
}
}
/// Carries source locations reused by switch diagnostics.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
struct SwitchDiagnosticContext {
switch_keyword_position: TokenPosition,
selector_span: TokenSpan,
left_brace_position: TokenPosition,
}
impl SwitchDiagnosticContext {
#[must_use]
fn attach_to_error(self, error: ParseError) -> ParseError {
error
.related_token("switch_keyword", self.switch_keyword_position)
.related("selector", self.selector_span)
.related_token("left_brace", self.left_brace_position)
}
#[must_use]
fn attach_to_result<'src, 'arena, T>(
self,
result: ParseResult<'src, 'arena, T>,
) -> ParseResult<'src, 'arena, T> {
result.map_err(|error| self.attach_to_error(error))
}
}
#[must_use]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
enum SwitchSectionBodyExit {
AtSectionBoundary,
RecoveredAtSwitchBoundary,
}
#[must_use]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
enum SwitchSectionsExit {
ClosedByRightBrace,
ClosedByRecovery,
EndOfFile,
}
impl<'src, 'arena> Parser<'src, 'arena> {
/// Parses a `switch` expression after consuming the `switch` keyword.
///
/// Returns an [`crate::ast::Expression::Switch`] whose span covers the
/// entire construct, from `switch_start_position` to the closing `}`.
/// If the switch is closed normally, returns an [`ast::Expression::Switch`]
/// whose span covers the construct from `switch_start_position` through the
/// closing `}`.
///
/// Only one `default` arm is recorded. Duplicate defaults and `case` arms
/// after a `default` are reported as errors.
///
/// On premature end-of-file, reports an error and returns a best-effort
/// switch node.
/// On errors, reports diagnostics and returns a best-effort switch node.
#[must_use]
pub(crate) fn parse_switch_tail(
&mut self,
switch_start_position: TokenPosition,
) -> ExpressionRef<'src, 'arena> {
let selector = self.parse_expression();
let mut cases = self.arena.vec();
let mut default_arm = None;
let mut span = TokenSpan::new(switch_start_position);
if self
.expect(Token::LeftBrace, ParseErrorKind::SwitchMissingBody)
.report_error(self)
{
return self.alloc_switch_node(selector, cases, default_arm, span);
let mut state = match self.parse_switch_header_tail(switch_start_position) {
Ok(state) => state,
Err(switch_node) => return switch_node,
};
match self.parse_switch_sections_tail(&mut state) {
SwitchSectionsExit::ClosedByRightBrace | SwitchSectionsExit::ClosedByRecovery => {
self.report_delayed_switch_errors(&state);
self.alloc_switch_node_from_state(state)
}
SwitchSectionsExit::EndOfFile => {
let eof_position = self.peek_position_or_eof();
state
.diagnostic_context()
.attach_to_error(
self.make_error_at(ParseErrorKind::SwitchMissingClosingBrace, eof_position)
.sync_error_at_matching_delimiter(self, state.left_brace_position),
)
.report(self);
state.span.extend_to(self.last_consumed_position_or_start());
self.report_delayed_switch_errors(&state);
self.alloc_switch_node_from_state(state)
}
}
}
fn parse_switch_header_tail(
&mut self,
switch_start_position: TokenPosition,
) -> Result<SwitchParseState<'src, 'arena>, ExpressionRef<'src, 'arena>> {
// The caller has already accepted `switch` as expression syntax,
// so selector parsing can rely on normal expression recovery instead of
// revalidating the legacy `switch (` disambiguation.
let selector = self.parse_expression();
let span = TokenSpan::new(switch_start_position).extended(selector.span().end);
let Some(left_brace_position) = self
.expect(Token::LeftBrace, ParseErrorKind::SwitchMissingBody)
.related("selector", *selector.span())
.related_token("switch_keyword", switch_start_position)
.ok_or_report(self)
else {
return Err(self.alloc_switch_node(selector, self.arena.vec(), None, span));
};
Ok(SwitchParseState::new(
switch_start_position,
left_brace_position,
selector,
self.arena.vec(),
))
}
fn parse_switch_sections_tail(
&mut self,
state: &mut SwitchParseState<'src, 'arena>,
) -> SwitchSectionsExit {
while let Some((token, token_position)) = self.peek_token_and_position() {
match token {
let body_exit = match token {
Token::RightBrace => {
self.advance(); // '}'
span.extend_to(token_position);
return self.alloc_switch_node(selector, cases, default_arm, span);
state.span.extend_to(token_position);
return SwitchSectionsExit::ClosedByRightBrace;
}
Token::Keyword(Keyword::Case) => {
if default_arm.is_some() {
self.report_error_here(ParseErrorKind::SwitchCasesAfterDefault);
}
let case_node = self.parse_switch_case_group(token_position);
cases.push(case_node);
self.parse_case_section_into_state(state, token_position)
}
Token::Keyword(Keyword::Default) => {
if default_arm.is_some() {
self.report_error_here(ParseErrorKind::SwitchDuplicateDefault);
}
// Duplicate `default` is still parsed so that diagnostics
// in its body can be reported.
self.parse_switch_default_arm(
token_position,
default_arm.get_or_insert_with(|| self.arena.vec()),
);
self.parse_default_section_into_state(state, token_position)
}
// Items before the first arm declaration are not allowed, but
// are parsed for basic diagnostics and simplicity.
_ => self.parse_switch_preamble_items(token_position),
// Invalid switch-level items are parsed before being discarded
// so ordinary statement diagnostics still run.
_ => self.parse_invalid_switch_preamble(state.diagnostic_context(), token_position),
};
if body_exit == SwitchSectionBodyExit::RecoveredAtSwitchBoundary {
state.span.extend_to(self.last_consumed_position_or_start());
return SwitchSectionsExit::ClosedByRecovery;
}
// Guard against parser bugs that would otherwise leave
// block parsing stuck on the same token.
self.ensure_forward_progress(token_position);
}
self.report_error_here(ParseErrorKind::SwitchMissingClosingBrace);
// This can only be `None` in the pathological case of
// an empty token stream
span.extend_to(
self.last_consumed_position()
.unwrap_or(switch_start_position),
);
self.alloc_switch_node(selector, cases, default_arm, span)
SwitchSectionsExit::EndOfFile
}
/// Parses a stacked `case` group and its body:
/// `case <expr>: (case <expr>:)* <arm-body>`.
/// Parses a `case` section and appends it to `state`.
///
/// Returns the allocated [`crate::ast::CaseRef`] node.
/// A section may have stacked `case <expr>:` labels and contains statements
/// until the next section boundary.
///
/// The returned node span covers the entire group, from
/// `first_case_position` to the end of the arm body, or to the end of the
/// last label if the body is empty.
#[must_use]
fn parse_switch_case_group(
/// Returns the boundary that stopped body parsing.
fn parse_case_section_into_state(
&mut self,
state: &mut SwitchParseState<'src, 'arena>,
first_case_position: TokenPosition,
) -> crate::ast::SwitchCaseRef<'src, 'arena> {
let mut labels = self.arena.vec();
while let Some((Keyword::Case, case_position)) = self.peek_keyword_and_position() {
self.advance(); // 'case'
labels.push(self.parse_expression());
// `:` is required after each case label; missing `:` is recovered
// at statement sync level.
self.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon)
.widen_error_span_from(case_position)
.sync_error_until(self, crate::parser::SyncLevel::StatementStart)
.report_error(self);
) -> SwitchSectionBodyExit {
if state.has_default() {
state
.case_keyword_positions_after_default
.push(first_case_position);
}
let mut body = self.arena.vec();
self.parse_switch_arm_body(&mut body);
let case_span = compute_case_span(first_case_position, &labels, &body);
self.arena
.alloc_node(crate::ast::SwitchCase { labels, body }, case_span)
let switch_context = state.diagnostic_context();
let labels = self.parse_case_labels(switch_context);
let mut statements = self.arena.vec();
let body_exit =
self.parse_switch_section_body(switch_context.left_brace_position, &mut statements);
let case_span = compute_switch_case_span(first_case_position, &labels, &statements);
let case_node = self.arena.alloc_node(
ast::SwitchCase {
labels,
body: statements,
},
case_span,
);
state.cases.push(case_node);
body_exit
}
/// Parses a `default:` arm and appends its statements to `statements`.
fn parse_switch_default_arm(
/// Parses a `default:` section into `state`.
///
/// Duplicate `default` sections contribute statements to the first one so
/// recovery preserves their bodies while diagnostics are delayed.
///
/// Returns the boundary that stopped body parsing.
fn parse_default_section_into_state(
&mut self,
state: &mut SwitchParseState<'src, 'arena>,
default_position: TokenPosition,
statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>,
) {
) -> SwitchSectionBodyExit {
self.advance(); // 'default'
self.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon)
.widen_error_span_from(default_position)
.sync_error_until(self, crate::parser::SyncLevel::StatementStart)
state.default_keyword_positions.push(default_position);
let switch_context = state.diagnostic_context();
let default_arm = state.default_arm.get_or_insert_with(|| self.arena.vec());
switch_context
.attach_to_result(
self.expect(Token::Colon, ParseErrorKind::SwitchDefaultMissingColon)
.widen_error_span_from(default_position)
.related_token("default_keyword", default_position),
)
.sync_error_until(self, SyncLevel::StatementStart)
.report_error(self);
self.parse_switch_arm_body(statements);
self.parse_switch_section_body(switch_context.left_brace_position, default_arm)
}
/// Parses statements of a single switch arm body.
fn parse_switch_arm_body(
/// Parses invalid switch-level items up to the next section boundary.
///
/// Parsed statements are discarded after diagnostics; they are not part of
/// any switch arm.
fn parse_invalid_switch_preamble(
&mut self,
statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>,
) {
while let Some((token, token_position)) = self.peek_token_and_position() {
match token {
Token::Keyword(Keyword::Case | Keyword::Default) | Token::RightBrace => break,
_ => {
self.parse_and_append_next_block_item(statements);
self.ensure_forward_progress(token_position);
}
}
}
}
/// Parses items that appear before any `case` or `default` arm declaration.
///
/// Such items are not allowed, but they are parsed to produce diagnostics
/// and maintain forward progress.
///
/// Parsed statements are discarded; only error reporting is preserved.
///
/// Parsing stops at a boundary token or end-of-file.
/// Boundary tokens: `case`, `default`, `}`.
fn parse_switch_preamble_items(&mut self, preamble_start_position: TokenPosition)
switch_context: SwitchDiagnosticContext,
preamble_start_position: TokenPosition,
) -> SwitchSectionBodyExit
where
'src: 'arena,
{
// Discard parsed statements into a sink vector.
// This is a bit "hacky", but I don't want to adapt code to skip
// production of AST nodes just to report errors in
// one problematic case.
let mut sink = self.arena.vec();
self.parse_switch_arm_body(&mut sink);
self.make_error_at_last_consumed(ParseErrorKind::SwitchTopLevelItemNotCase)
.widen_error_span_from(preamble_start_position)
// Build the statements only to reuse normal statement diagnostics and
// recovery; switch-level items cannot be represented in the switch AST.
let mut discarded_statements = self.arena.vec();
let body_exit = self.parse_switch_section_body(
switch_context.left_brace_position,
&mut discarded_statements,
);
let preamble_span = TokenSpan::range(
preamble_start_position,
self.last_consumed_position_or_start(),
);
let error = switch_context.attach_to_error(
self.make_error_at_last_consumed(ParseErrorKind::SwitchTopLevelItemNotCase)
.widen_error_span_from(preamble_start_position)
.blame(preamble_span),
);
if discarded_statements.len() > 1 {
error.related("multiple_items", preamble_span)
} else {
error.related("single_item", preamble_span)
}
.report(self);
body_exit
}
fn parse_case_labels(
&mut self,
switch_context: SwitchDiagnosticContext,
) -> ArenaVec<'arena, ExpressionRef<'src, 'arena>> {
let mut labels = self.arena.vec();
while let Some((Keyword::Case, case_position)) = self.peek_keyword_and_position() {
self.advance(); // 'case'
let mut case_expression_span = None;
let mut should_expect_colon = true;
if let Some((Token::Colon, colon_position)) = self.peek_token_and_position() {
switch_context
.attach_to_error(
self.make_error_at(
ParseErrorKind::SwitchCaseMissingExpression,
colon_position,
)
.blame_token(colon_position)
.related_token("case_keyword", case_position),
)
.report(self);
} else {
// Recover only to the label delimiter here;
// `expect_case_label_colon` will consume it and avoid
// a duplicate missing-colon diagnostic.
should_expect_colon = !self.next_token_definitely_cannot_start_expression();
let expression = switch_context
.attach_to_result(self.parse_required_expression(
ParseErrorKind::SwitchCaseExpressionInvalidStart,
case_position,
))
.related_token("case_keyword", case_position)
.sync_error_at(self, SyncLevel::ColonDelimiter)
.unwrap_or_fallback(self);
case_expression_span = Some(*expression.span());
labels.push(expression);
}
// Expression recovery may still leave a valid colon to consume
if self.peek_token() == Some(Token::Colon) {
should_expect_colon = true;
}
if should_expect_colon {
self.expect_case_label_colon(switch_context, case_position, case_expression_span);
}
}
labels
}
fn expect_case_label_colon(
&mut self,
switch_context: SwitchDiagnosticContext,
case_position: TokenPosition,
case_expression_span: Option<TokenSpan>,
) {
// If the colon is missing, skip to a statement-or-stronger boundary so
// the damaged label is not parsed as arm body.
let missing_colon_error = self
.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon)
.widen_error_span_from(case_position)
.related_token("case_keyword", case_position);
let missing_colon_error = if let Some(case_expression_span) = case_expression_span {
missing_colon_error.related("case_expression", case_expression_span)
} else {
missing_colon_error
};
switch_context
.attach_to_result(missing_colon_error)
.sync_error_until(self, SyncLevel::StatementStart)
.report_error(self);
}
/// Helper to allocate a `Switch` expression with the given span.
/// Parses the statements belonging to the current switch section.
///
/// Returns [`SwitchSectionBodyExit::AtSectionBoundary`] when parsing stops
/// at `case`, `default`, `}`, or end-of-file. Returns
/// [`SwitchSectionBodyExit::RecoveredAtSwitchBoundary`] when statement
/// recovery has to synchronize to the switch's closing delimiter.
fn parse_switch_section_body(
&mut self,
left_brace_position: TokenPosition,
statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>,
) -> SwitchSectionBodyExit {
while let Some((token, token_position)) = self.peek_token_and_position() {
match token {
Token::Keyword(Keyword::Case | Keyword::Default) | Token::RightBrace => {
return SwitchSectionBodyExit::AtSectionBoundary;
}
// Boundaries outside this switch are left to block-item parsing
// so it can attach the more specific item-level diagnostic.
_ => match self.parse_and_append_next_block_item(statements) {
Ok(()) => {
// Guard against parser bugs that would otherwise leave
// switch parsing stuck on the same token.
self.ensure_forward_progress(token_position);
}
Err(error) => {
// Item recovery could not find a local boundary, so
// recover at the switch's matching closing brace.
let error =
error.sync_error_at_matching_delimiter(self, left_brace_position);
let error_statement = error.fallback(self);
statements.push(error_statement);
return SwitchSectionBodyExit::RecoveredAtSwitchBoundary;
}
},
}
}
SwitchSectionBodyExit::AtSectionBoundary
}
fn report_delayed_switch_errors(&mut self, state: &SwitchParseState<'src, 'arena>) {
self.report_duplicate_switch_defaults(state);
self.report_switch_cases_after_default(state);
}
fn report_duplicate_switch_defaults(&mut self, state: &SwitchParseState<'src, 'arena>) {
let Some((first_default_position, duplicate_positions)) =
state.default_keyword_positions.split_first()
else {
return;
};
let Some(first_duplicate_position) = duplicate_positions.first().copied() else {
return;
};
let mut error = state.diagnostic_context().attach_to_error(
self.make_error_at(
ParseErrorKind::SwitchDuplicateDefault,
first_duplicate_position,
)
.related_token("first_default", *first_default_position),
);
for (index, duplicate_position) in duplicate_positions.iter().copied().enumerate() {
error = error.related_token(
format!("duplicate_default_{}", index + 1),
duplicate_position,
);
}
error.report(self);
}
fn report_switch_cases_after_default(&mut self, state: &SwitchParseState<'src, 'arena>) {
let Some(first_default_position) = state.first_default_keyword_position() else {
return;
};
let Some(first_case_position) = state.case_keyword_positions_after_default.first().copied()
else {
return;
};
let mut error = state.diagnostic_context().attach_to_error(
self.make_error_at(ParseErrorKind::SwitchCasesAfterDefault, first_case_position)
.related_token("first_default", first_default_position),
);
for (index, case_position) in state
.case_keyword_positions_after_default
.iter()
.copied()
.enumerate()
{
error = error.related_token(format!("case_after_default_{}", index + 1), case_position);
}
error.report(self);
}
#[must_use]
fn alloc_switch_node(
&self,
selector: ExpressionRef<'src, 'arena>,
cases: ArenaVec<'arena, crate::ast::SwitchCaseRef<'src, 'arena>>,
default_arm: Option<ArenaVec<'arena, StatementRef<'src, 'arena>>>,
cases: ArenaVec<'arena, SwitchCaseRef<'src, 'arena>>,
default_arm: Option<StatementList<'src, 'arena>>,
span: TokenSpan,
) -> ExpressionRef<'src, 'arena> {
self.arena.alloc_node(
crate::ast::Expression::Switch {
ast::Expression::Switch {
selector,
cases,
default_arm,
@ -178,23 +510,28 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
span,
)
}
#[must_use]
fn alloc_switch_node_from_state(
&self,
state: SwitchParseState<'src, 'arena>,
) -> ExpressionRef<'src, 'arena> {
self.alloc_switch_node(state.selector, state.cases, state.default_arm, state.span)
}
}
/// Computes an [`AstSpan`] covering a `case` group.
/// Computes the span of a `case` section.
///
/// The span begins at `labels_start_position` and extends to:
/// - the end of the last statement in `body`, if present; otherwise
/// - the end of the last label in `labels`, if present.
///
/// If both are empty, the span covers only `labels_start_position`.
/// The span starts at the first `case` label and extends through the section
/// body, or through the last label for an empty section.
#[must_use]
fn compute_case_span(
labels_start_position: TokenPosition,
fn compute_switch_case_span(
first_case_position: TokenPosition,
labels: &[ExpressionRef],
body: &[StatementRef],
statements: &[StatementRef],
) -> TokenSpan {
let mut span = TokenSpan::new(labels_start_position);
if let Some(last_statement) = body.last() {
let mut span = TokenSpan::new(first_case_position);
if let Some(last_statement) = statements.last() {
span.extend_to(last_statement.span().end);
} else if let Some(last_label) = labels.last() {
span.extend_to(last_label.span().end);

View File

@ -45,6 +45,7 @@ pub enum SyncLevel {
/// Closing `)` of a parenthesized/grouped construct.
CloseParenthesis,
ColonDelimiter,
/// A statement boundary or statement starter.
///
@ -59,7 +60,7 @@ pub enum SyncLevel {
///
/// This is useful because `case` / `default` are stronger boundaries than
/// ordinary statements inside switch parsing.
SwitchArmStart,
SwitchSectionBoundary,
/// Start of a declaration-like item.
///
@ -79,8 +80,9 @@ impl SyncLevel {
const fn for_token(token: Token) -> Option<Self> {
use crate::lexer::Keyword;
use SyncLevel::{
BlockBoundary, CloseAngleBracket, CloseBracket, CloseParenthesis, DeclarationStart,
ExpressionStart, ListSeparator, StatementStart, StatementTerminator, SwitchArmStart,
BlockBoundary, CloseAngleBracket, CloseBracket, CloseParenthesis, ColonDelimiter,
DeclarationStart, ExpressionStart, ListSeparator, StatementStart, StatementTerminator,
SwitchSectionBoundary,
};
match token {
@ -106,10 +108,12 @@ impl SyncLevel {
| Keyword::Local,
) => Some(StatementStart),
Token::Colon => Some(ColonDelimiter),
Token::Semicolon => Some(StatementTerminator),
// Switch-specific stronger boundary
Token::Keyword(Keyword::Case | Keyword::Default) => Some(SwitchArmStart),
Token::Keyword(Keyword::Case | Keyword::Default) => Some(SwitchSectionBoundary),
// Declaration/member starts
Token::Keyword(

View File

@ -9,6 +9,7 @@ mod block_items;
mod control_flow_expressions;
mod primary_expressions;
mod selector_expressions;
mod switch_expressions;
#[derive(Debug)]
pub(super) struct ExpectedLabel {

View File

@ -397,7 +397,7 @@ pub(super) const P0033_FIXTURES: &[Fixture] = &[
},
Fixture {
label: "files/P0033_02.uc",
source: "{\n Func\n (A\n :,\n B);\n Log(\"after\");\n}\n",
source: "{\n Func\n (A\n #,\n B);\n Log(\"after\");\n}\n",
},
Fixture {
label: "files/P0033_03.uc",
@ -670,7 +670,7 @@ fn check_p0033_fixtures() {
assert_diagnostic(
&runs.get_any("files/P0033_02.uc"),
&ExpectedDiagnostic {
headline: "expected `,` or `)` after argument, found `:`",
headline: "expected `,` or `)` after argument, found `#`",
severity: Severity::Error,
code: Some("P0033"),
primary_label: Some(ExpectedLabel {
@ -678,7 +678,7 @@ fn check_p0033_fixtures() {
start: TokenPosition(10),
end: TokenPosition(10),
},
message: "unexpected `:`",
message: "unexpected `#`",
}),
secondary_labels: &[
ExpectedLabel {

File diff suppressed because it is too large Load Diff