diff --git a/Cargo.toml b/Cargo.toml index 4e9485c..1142916 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ codegen-units = 1 # Reduce number of codegen units to increase optimizations debug = false # strip all debug info [profile.flamegraph] -inherits = "release" # start from release +inherits = "dev" # start from release strip = false debug = true # full DWARF info for unwinding split-debuginfo = "unpacked" # keep symbols inside the binary diff --git a/dev_tests/src/pretty.rs b/dev_tests/src/pretty.rs index d47150c..1ff1ffe 100644 --- a/dev_tests/src/pretty.rs +++ b/dev_tests/src/pretty.rs @@ -8,7 +8,6 @@ pub fn render_diagnostic( _file: &TokenizedFile, file_name: Option<&str>, colors: bool, -) -> String { +) { diag.render(_file, file_name.unwrap_or("")); - "fuck it".to_string() } diff --git a/dev_tests/src/uc_lexer_verify.rs b/dev_tests/src/uc_lexer_verify.rs index 64b661e..3080fac 100644 --- a/dev_tests/src/uc_lexer_verify.rs +++ b/dev_tests/src/uc_lexer_verify.rs @@ -308,7 +308,6 @@ fn main() { // first window for (k, d) in diags.iter().take(first_n).enumerate() { let s = pretty::render_diagnostic(d, tf, Some(&fname), use_colors); - eprintln!("{s}"); if ALSO_PRINT_DEBUG_AFTER_PRETTY { eprintln!("#{}: {:#?}", k + 1, d); } @@ -319,7 +318,6 @@ fn main() { for (offset, d) in diags.iter().skip(start).enumerate() { let idx_global = start + offset + 1; let s = pretty::render_diagnostic(d, tf, Some(&fname), use_colors); - eprintln!("{s}"); if ALSO_PRINT_DEBUG_AFTER_PRETTY { eprintln!("#{idx_global}: {d:#?}"); } @@ -327,7 +325,6 @@ fn main() { } else { for (k, d) in diags.iter().enumerate() { let s = pretty::render_diagnostic(d, tf, Some(&fname), use_colors); - eprintln!("{s}"); if ALSO_PRINT_DEBUG_AFTER_PRETTY { eprintln!("#{}: {:#?}", k + 1, d); } diff --git a/dev_tests/src/verify_expr.rs b/dev_tests/src/verify_expr.rs index 0f13204..0b526dd 100644 --- a/dev_tests/src/verify_expr.rs +++ b/dev_tests/src/verify_expr.rs @@ -14,14 +14,138 @@ mod pretty; // a * * * +/// Expressions to test. +/// +/// Add, remove, or edit entries here. +/// Using `(&str, &str)` gives each case a human-readable label. /// Expressions to test. /// /// Add, remove, or edit entries here. /// Using `(&str, &str)` gives each case a human-readable label. const TEST_CASES: &[(&str, &str)] = &[ - ("files/P0003_01.uc", "(a + b && c / d ^ e @ f"), - ("files/P0003_02.uc", "(a]"), - ("files/P0003_03.uc", "(a\n;"), + // P0016: invalid initializer start after `for (` + ( + "files/P0016_01.uc", + "for\n(] ; )", + ), + ( + "files/P0016_02.uc", + "for (\n ]\n ;\n)\n Body();\n", + ), + ( + "files/P0016_03.uc", + "for (\n }\n ;\n)\n", + ), + ( + "files/P0016_06.uc", + "for (\n ]\n\n\n ; Step)\n", + ), + + // P0017: initializer parsed, but first `;` is missing + ( + "files/P0017_01.uc", + "for (Init ] ; )", + ), + ( + "files/P0017_02.uc", + "for (Init\n ]\n ;\n)\n", + ), + ( + "files/P0017_04.uc", + "for (Init {\n Body();\n}; )\n", + ), + ( + "files/P0017_05.uc", + "for (Init", + ), + + // P0018: invalid condition start after first `;` + ( + "files/P0018_01.uc", + "for \n\n (; ] ; )", + ), + ( + "files/P0018_02.uc", + "for (;\n ]\n ;\n)\n Body();\n", + ), + ( + "files/P0018_03.uc", + "for (;\n }\n ;\n)\n", + ), + ( + "files/P0018_06.uc", + "for (;", + ), + + // P0019: condition parsed, but second `;` is missing + ( + "files/P0019_01.uc", + "for (; bCondition )", + ), + ( + "files/P0019_02.uc", + "for (; bCondition\n)\n Body();\n", + ), + ( + "files/P0019_03.uc", + "for (; bCondition ] ; )", + ), + ( + "files/P0019_04.uc", + "for (; bCondition\n{\n Body();\n}\n;\n)\n", + ), + ( + "files/P0019_06.uc", + "for (; bCondition", + ), + ( + "files/P0019_07.uc", + "for (; bCondition Step)", + ), + + // P0020: invalid step start after second `;` + ( + "files/P0020_01.uc", + "for (;;;)", + ), + ( + "files/P0020_02.uc", + "for (;;\n ;\n)\n", + ), + ( + "files/P0020_03.uc", + "for (;; ])", + ), + ( + "files/P0020_04.uc", + "for (;;\n }\n)\n", + ), + ( + "files/P0020_08.uc", + "for (;;\n ]\n", + ), + + // P0021: missing `)` to close `for` header + ( + "files/P0021_01.uc", + "for (;;", + ), + ( + "files/P0021_02.uc", + "for (;; Step", + ), + ( + "files/P0021_03.uc", + "for (;; Step;\n Body();\n", + ), + ( + "files/P0021_05.uc", + "for (Init; bCondition; Step\n{\n Body();\n}\n", + ), + ( + "files/P0021_09.uc", + "for\n(Init;\n bCondition;\n Step\n]\n", + ), ]; /// If true, print the parsed expression using Debug formatting. @@ -33,24 +157,14 @@ const ALWAYS_PRINT_DIAGNOSTICS: bool = true; fn main() { let arena = Arena::new(); - println!("Running {} expression test case(s)...", TEST_CASES.len()); - println!(); - let mut had_any_problem = false; for (idx, (label, source)) in TEST_CASES.iter().enumerate() { - println!("============================================================"); - println!("Case #{:02}: {}", idx + 1, label); - println!("Source: {}", source); - println!("------------------------------------------------------------"); - let tf = TokenizedFile::tokenize(source); let mut parser = Parser::new(&tf, &arena); let expr = parser.parse_expression(); - println!("parse_expression() returned."); - if PRINT_PARSED_EXPR { println!("Parsed expression:"); println!("{expr:#?}"); @@ -60,14 +174,10 @@ fn main() { println!("Diagnostics: none"); } else { had_any_problem = true; - println!("Diagnostics: {}", parser.diagnostics.len()); - if ALWAYS_PRINT_DIAGNOSTICS { let use_colors = false; for (k, diag) in parser.diagnostics.iter().enumerate() { - let rendered = pretty::render_diagnostic(diag, &tf, Some(label), use_colors); - println!("Diagnostic #{}:", k + 1); - println!("{rendered}"); + pretty::render_diagnostic(diag, &tf, Some(label), use_colors); } } } @@ -82,4 +192,4 @@ fn main() { } else { println!("Done. All cases completed without diagnostics."); } -} \ No newline at end of file +} diff --git a/perf.data.old b/perf.data.old index 5b25c69..fc2a444 100644 Binary files a/perf.data.old and b/perf.data.old differ diff --git a/rottlib/src/diagnostics/expression_diagnostics.rs b/rottlib/src/diagnostics/expression_diagnostics.rs deleted file mode 100644 index 4aba278..0000000 --- a/rottlib/src/diagnostics/expression_diagnostics.rs +++ /dev/null @@ -1,284 +0,0 @@ -use super::{Diagnostic, DiagnosticBuilder}; -use crate::lexer::{TokenPosition, TokenSpan, TokenizedFile}; -use crate::parser::{ParseError, ParseErrorKind}; - -pub(crate) fn diagnostic_from_parse_error<'src>( - error: ParseError, - file: &TokenizedFile<'src>, -) -> Diagnostic { - match error.kind { - ParseErrorKind::ParenthesizedExpressionInvalidStart => { - diagnostic_parenthesized_expression_invalid_start(error, file) - } - - ParseErrorKind::ExpressionExpected => diagnostic_expression_expected(error, file), - - ParseErrorKind::ClassTypeMissingTypeArgument { - left_angle_bracket_position, - } => diagnostic_class_type_missing_type_argument(error, left_angle_bracket_position), - - ParseErrorKind::ClassTypeMissingClosingAngleBracket { - left_angle_bracket_position, - } => { - diagnostic_class_type_missing_closing_angle_bracket(error, left_angle_bracket_position) - } - - ParseErrorKind::ParenthesizedExpressionMissingClosingParenthesis => { - diagnostic_parenthesized_expression_missing_closing_parenthesis(error, file) - } - - ParseErrorKind::ClassTypeInvalidTypeArgument { - left_angle_bracket_position, - } => diagnostic_class_type_invalid_type_argument(error, left_angle_bracket_position), - - ParseErrorKind::NewTooManyArguments { - left_parenthesis_position, - } => diagnostic_new_too_many_arguments(error, left_parenthesis_position), - - ParseErrorKind::NewMissingClosingParenthesis { - left_parenthesis_position, - } => diagnostic_new_missing_closing_parenthesis(error, left_parenthesis_position), - - ParseErrorKind::NewMissingClassSpecifier { - new_keyword_position, - } => diagnostic_new_missing_class_specifier(error, new_keyword_position), - - _ => DiagnosticBuilder::error(format!("error {:?} while parsing", error.kind)) - .primary_label(error.covered_span, "happened here") - .build(), - } -} - -fn diagnostic_parenthesized_expression_invalid_start<'src>( - mut error: ParseError, - file: &TokenizedFile<'src>, -) -> Diagnostic { - let (header_text, primary_text) = - if let Some(token_text) = file.token_text(error.blame_span.end) { - ( - format!( - "expected expression inside parentheses, found `{}`", - token_text - ), - format!("unexpected `{}`", token_text), - ) - } else if file.is_eof(&error.blame_span.end) { - ( - "expected expression, found end of file".to_string(), - "reached end of file here".to_string(), - ) - } else { - ( - "expected expression inside parentheses".to_string(), - "expected expression".to_string(), - ) - }; - let mut builder = DiagnosticBuilder::error(header_text); - if let Some(related_span) = error.related_spans.get("left_parenthesis") - && !file.same_line(related_span.start, error.blame_span.end) - { - builder = builder.secondary_label(*related_span, "parenthesized expression starts here"); - }; - // It is more clear to see what happened if just the first token is - // highlighted in case blame span never leaves the line - if file.same_line(error.blame_span.start, error.blame_span.end) { - error.blame_span.start = error.blame_span.end; - } - builder - .primary_label(error.blame_span, primary_text) - .code("P0001") - .build() -} - -fn diagnostic_expression_expected<'src>( - mut error: ParseError, - file: &TokenizedFile<'src>, -) -> Diagnostic { - let prefix_operator_span = error.related_spans.get("prefix_operator").copied(); - let infix_operator_span = error.related_spans.get("infix_operator").copied(); - let operator_span = infix_operator_span.or(prefix_operator_span); - - let operator_text = operator_span.and_then(|span| file.token_text(span.end)); - - let (header_text, primary_text) = match (operator_text, file.token_text(error.blame_span.end)) { - (Some(operator_text), Some(token_text)) => ( - format!( - "expected expression after `{}`, found `{}`", - operator_text, token_text - ), - format!("unexpected `{}`", token_text), - ), - (Some(operator_text), None) if file.is_eof(&error.blame_span.end) => ( - format!( - "expected expression after `{}`, found end of file", - operator_text - ), - "reached end of file here".to_string(), - ), - (Some(operator_text), None) => ( - format!("expected expression after `{}`", operator_text), - "expected expression".to_string(), - ), - - (None, Some(token_text)) => ( - format!("expected expression, found `{}`", token_text), - format!("unexpected `{}`", token_text), - ), - (None, None) if file.is_eof(&error.blame_span.end) => ( - "expected expression, found end of file".to_string(), - "reached end of file here".to_string(), - ), - (None, None) => ( - "expected expression".to_string(), - "expected expression".to_string(), - ), - }; - - let mut builder = DiagnosticBuilder::error(header_text); - - // Only need this hint if lines are different - if let Some(span) = operator_span - && !file.same_line(span.start, error.blame_span.end) - { - let secondary_text = if let Some(operator_text) = operator_text { - format!("after this `{}`, an expression was expected", operator_text) - } else { - "an expression was expected after this operator".to_string() - }; - - builder = builder.secondary_label(span, secondary_text); - } - - builder - .primary_label(error.blame_span, primary_text) - .code("P0002") - .build() -} - -fn diagnostic_parenthesized_expression_missing_closing_parenthesis<'src>( - mut error: ParseError, - file: &TokenizedFile<'src>, -) -> Diagnostic { - let left_parenthesis_span = error.related_spans.get("left_parenthesis").copied(); - - let primary_text = if let Some(token_text) = file.token_text(error.blame_span.end) { - format!("expected `)` before `{}`", token_text) - } else if file.is_eof(&error.blame_span.end) { - "expected `)` before end of file".to_string() - } else { - "expected `)` here".to_string() - }; - - let mut builder = DiagnosticBuilder::error("missing `)` to close parenthesized expression"); - - if let Some(span) = left_parenthesis_span - && !file.same_line(span.start, error.blame_span.end) - { - builder = builder.secondary_label(span, "parenthesized expression starts here"); - } - - // On a single line, point only at the exact place where `)` was expected. - // On multiple lines, keep the full span so the renderer can connect the - // opening `(` to the failure point. - if file.same_line(error.blame_span.start, error.blame_span.end) { - error.blame_span.start = error.blame_span.end; - } - - builder - .primary_label(error.blame_span, primary_text) - .code("P0003") - .build() -} - -fn diagnostic_class_type_missing_type_argument( - error: ParseError, - left_angle_bracket_position: TokenPosition, -) -> Diagnostic { - DiagnosticBuilder::error("missing type argument in `class<...>`") - .primary_label(error.blame_span, "expected a type name here") - .secondary_label( - TokenSpan::new(left_angle_bracket_position), - "type argument list starts here", - ) - .help("Write a type name, for example `class`.") - .build() -} - -fn diagnostic_class_type_missing_closing_angle_bracket( - error: ParseError, - left_angle_bracket_position: TokenPosition, -) -> Diagnostic { - DiagnosticBuilder::error("missing closing `>` in `class<...>`") - .primary_label(error.blame_span, "expected `>` here") - .secondary_label( - TokenSpan::new(left_angle_bracket_position), - "this `<` starts the type argument", - ) - .help("Add `>` to close the class type expression.") - .build() -} - -fn diagnostic_class_type_invalid_type_argument( - error: ParseError, - left_angle_bracket_position: TokenPosition, -) -> Diagnostic { - DiagnosticBuilder::error("invalid type argument in `class<...>`") - .primary_label(error.blame_span, "expected a qualified type name here") - .secondary_label( - TokenSpan::new(left_angle_bracket_position), - "type argument list starts here", - ) - .note("Only a qualified type name is accepted between `<` and `>` here.") - .build() -} - -fn diagnostic_new_too_many_arguments( - error: ParseError, - left_parenthesis_position: TokenPosition, -) -> Diagnostic { - DiagnosticBuilder::error("too many arguments in `new(...)`") - .primary_label(error.blame_span, "unexpected extra argument") - .secondary_label( - TokenSpan::new(left_parenthesis_position), - "this argument list accepts at most three arguments", - ) - .note("The three slots are `outer`, `name`, and `flags`.") - .help("Remove the extra argument.") - .build() -} - -fn diagnostic_new_missing_closing_parenthesis( - error: ParseError, - left_parenthesis_position: TokenPosition, -) -> Diagnostic { - DiagnosticBuilder::error("missing closing `)` in `new(...)`") - .primary_label(error.blame_span, "expected `)` here") - .secondary_label( - TokenSpan::new(left_parenthesis_position), - "this argument list starts here", - ) - .help("Add `)` to close the argument list.") - .build() -} - -fn diagnostic_new_missing_class_specifier( - error: ParseError, - new_keyword_position: TokenPosition, -) -> Diagnostic { - let mut builder = DiagnosticBuilder::error("missing class specifier in `new` expression") - .primary_label( - error.blame_span, - "expected the class or expression to instantiate here", - ) - .secondary_label( - TokenSpan::new(new_keyword_position), - "`new` expression starts here", - ) - .help("Add the class or expression to instantiate after `new` or `new(...)`."); - - if let Some(related_span) = error.related_spans.get("blablabla") { - builder = builder.secondary_label(*related_span, "optional `new(...)` arguments end here"); - } - - builder.build() -} diff --git a/rottlib/src/diagnostics/mod.rs b/rottlib/src/diagnostics/mod.rs index c0855c4..aedc1e9 100644 --- a/rottlib/src/diagnostics/mod.rs +++ b/rottlib/src/diagnostics/mod.rs @@ -4,11 +4,11 @@ //! parsing or doing lightweight frontend checks. They are intentionally small, //! depend only on [`AstSpan`], and are easy to construct and store. -mod expression_diagnostics; +mod parse_error_diagnostics; mod render; use crate::lexer::TokenSpan; -pub(crate) use expression_diagnostics::diagnostic_from_parse_error; +pub(crate) use parse_error_diagnostics::diagnostic_from_parse_error; /// Classification of a diagnostic by its impact. /// diff --git a/rottlib/src/diagnostics/parse_error_diagnostics/control_flow_expressions.rs b/rottlib/src/diagnostics/parse_error_diagnostics/control_flow_expressions.rs new file mode 100644 index 0000000..6e2b274 --- /dev/null +++ b/rottlib/src/diagnostics/parse_error_diagnostics/control_flow_expressions.rs @@ -0,0 +1,679 @@ +use super::{ + Diagnostic, DiagnosticBuilder, FoundAt, collapse_span_to_end_on_same_line, found_at, + primary_span_with_optional_multiline_context, should_show_context_label, +}; +use crate::lexer::{TokenSpan, TokenizedFile}; +use crate::parser::{ParseError, diagnostic_labels}; + +pub(super) fn diagnostic_condition_expected<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let control_keyword_span = error + .related_spans + .get(diagnostic_labels::EXPRESSION_REQUIRED_BY) + .copied(); + let control_keyword_text = control_keyword_span.and_then(|span| file.token_text(span.end)); + + let do_keyword_span = error.related_spans.get("do_keyword").copied(); + + // Present only for the recovery path where a parsed block expression is + // treated as the likely branch body, meaning the condition before it is + // missing. + let branch_body_span = error.related_spans.get("branch_body").copied(); + let found_branch_body = branch_body_span.is_some(); + + let found = found_at(file, error.blame_span.end); + + let (header_text, primary_text) = match (control_keyword_text, found) { + (Some(keyword_text), FoundAt::Token(token_text)) => { + let primary_text = if found_branch_body && token_text == "{" { + "body starts here, but the condition is missing".to_string() + } else { + format!("unexpected `{}`", token_text) + }; + + ( + format!( + "expected condition after `{}`, found `{}`", + keyword_text, token_text + ), + primary_text, + ) + } + (Some(keyword_text), FoundAt::EndOfFile) => ( + format!( + "expected condition after `{}`, found end of file", + keyword_text + ), + "reached end of file here".to_string(), + ), + (Some(keyword_text), FoundAt::Unknown) => ( + format!("expected condition after `{}`", keyword_text), + "expected condition here".to_string(), + ), + + (None, FoundAt::Token(token_text)) => { + let primary_text = if found_branch_body && token_text == "{" { + "body starts here, but the condition is missing".to_string() + } else { + format!("unexpected `{}`", token_text) + }; + + ( + format!("expected condition, found `{}`", token_text), + primary_text, + ) + } + (None, FoundAt::EndOfFile) => ( + "expected condition, found end of file".to_string(), + "reached end of file here".to_string(), + ), + (None, FoundAt::Unknown) => ( + "expected condition".to_string(), + "expected condition here".to_string(), + ), + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + let spans_are_same = + |left: TokenSpan, right: TokenSpan| left.start == right.start && left.end == right.end; + + if let Some(do_span) = do_keyword_span { + let same_as_control_keyword = control_keyword_span + .map(|control_span| spans_are_same(do_span, control_span)) + .unwrap_or(false); + + let same_line_as_control_keyword = control_keyword_span + .map(|control_span| file.same_line(do_span.start, control_span.start)) + .unwrap_or(false); + + if !same_as_control_keyword + && !same_line_as_control_keyword + && !file.same_line(do_span.start, error.blame_span.end) + { + builder = builder.secondary_label(do_span, "`do` expression starts here"); + } + } + + if let Some(control_keyword_span) = control_keyword_span + && !file.same_line(control_keyword_span.start, error.blame_span.end) + { + let secondary_text = if let Some(keyword_text) = control_keyword_text { + format!("after this `{}`, a condition was expected", keyword_text) + } else { + "after this control-flow keyword, a condition was expected".to_string() + }; + + builder = builder.secondary_label(control_keyword_span, secondary_text); + } + + builder + .primary_label(error.blame_span, primary_text) + .code("P0012") + .build() +} + +pub(super) fn diagnostic_control_flow_body_expected<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let control_keyword_span = error + .related_spans + .get(diagnostic_labels::EXPRESSION_REQUIRED_BY) + .copied(); + let control_keyword_text = control_keyword_span.and_then(|span| file.token_text(span.end)); + + let body_context_span = error + .related_spans + .get(diagnostic_labels::EXPRESSION_EXPECTED_AFTER) + .copied(); + let body_context_text = body_context_span.and_then(|span| file.token_text(span.end)); + + let found = found_at(file, error.blame_span.end); + + let (header_text, primary_text) = match (control_keyword_text, found) { + (Some(keyword_text), FoundAt::Token(token_text)) => ( + format!( + "expected body for `{}`, found `{}`", + keyword_text, token_text + ), + format!("unexpected `{}`", token_text), + ), + (Some(keyword_text), FoundAt::EndOfFile) => ( + format!("expected body for `{}`, found end of file", keyword_text), + "reached end of file here".to_string(), + ), + (Some(keyword_text), FoundAt::Unknown) => ( + format!("expected body for `{}`", keyword_text), + "expected body here".to_string(), + ), + + (None, FoundAt::Token(token_text)) => ( + format!("expected body, found `{}`", token_text), + format!("unexpected `{}`", token_text), + ), + (None, FoundAt::EndOfFile) => ( + "expected body, found end of file".to_string(), + "reached end of file here".to_string(), + ), + (None, FoundAt::Unknown) => ( + "expected body".to_string(), + "expected body here".to_string(), + ), + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + let spans_are_same = + |left: TokenSpan, right: TokenSpan| left.start == right.start && left.end == right.end; + + let body_context_is_same_as_keyword = match (control_keyword_span, body_context_span) { + (Some(control_span), Some(body_span)) => spans_are_same(control_span, body_span), + _ => false, + }; + + let body_context_is_trivial_same_line_eof = match body_context_span { + Some(body_span) => { + matches!(found, FoundAt::EndOfFile) + && file.same_line(body_span.start, error.blame_span.end) + } + None => false, + }; + + let should_fallback_to_control_keyword = match body_context_span { + None => true, + Some(_) if body_context_is_same_as_keyword => true, + Some(_) => false, + }; + + if let Some(body_span) = body_context_span + && !body_context_is_same_as_keyword + && !body_context_is_trivial_same_line_eof + { + let secondary_text = if let Some(context_text) = body_context_text { + format!("after this `{}`, a body was expected", context_text) + } else { + "after this construct, a body was expected".to_string() + }; + + builder = builder.secondary_label(body_span, secondary_text); + } else if should_fallback_to_control_keyword + && let Some(keyword_span) = control_keyword_span + && !file.same_line(keyword_span.start, error.blame_span.end) + { + let secondary_text = if let Some(keyword_text) = control_keyword_text { + format!("after this `{}`, a body was expected", keyword_text) + } else { + "after this control-flow keyword, a body was expected".to_string() + }; + + builder = builder.secondary_label(keyword_span, secondary_text); + } + + builder + .primary_label(error.blame_span, primary_text) + .code("P0013") + .build() +} + +pub(super) fn diagnostic_do_missing_until<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let do_keyword_span = error.related_spans.get("do_keyword").copied(); + + let found = found_at(file, error.blame_span.end); + + let primary_text = match found { + FoundAt::Token(token_text) => { + format!("expected `until` before `{}`", token_text) + } + FoundAt::EndOfFile => "expected `until` before end of file".to_string(), + FoundAt::Unknown => "expected `until` here".to_string(), + }; + + let mut builder = DiagnosticBuilder::error("missing `until` after `do` body"); + + let primary_context_span = + do_keyword_span.filter(|span| should_show_context_label(file, *span, error.blame_span)); + + if let Some(span) = primary_context_span { + builder = builder.secondary_label(span, "`do` expression starts here"); + } + + let primary_span = + primary_span_with_optional_multiline_context(file, primary_context_span, error.blame_span); + + builder + .primary_label(primary_span, primary_text) + .code("P0014") + .build() +} + +pub(super) fn diagnostic_for_each_iterator_expression_expected<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let control_keyword_span = error + .related_spans + .get(diagnostic_labels::EXPRESSION_REQUIRED_BY) + .copied(); + 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 (header_text, primary_text) = match (control_keyword_text, found) { + (Some(keyword_text), FoundAt::Token(token_text)) => { + let primary_text = if token_text == "{" { + "body starts here, but the iterator expression is missing".to_string() + } else { + format!("unexpected `{}`", token_text) + }; + + ( + format!( + "expected iterator expression after `{}`, found `{}`", + keyword_text, token_text + ), + primary_text, + ) + } + (Some(keyword_text), FoundAt::EndOfFile) => ( + format!( + "expected iterator expression after `{}`, found end of file", + keyword_text + ), + "reached end of file here".to_string(), + ), + (Some(keyword_text), FoundAt::Unknown) => ( + format!("expected iterator expression after `{}`", keyword_text), + "expected iterator expression here".to_string(), + ), + + (None, FoundAt::Token(token_text)) => { + let primary_text = if token_text == "{" { + "body starts here, but the iterator expression is missing".to_string() + } else { + format!("unexpected `{}`", token_text) + }; + + ( + format!("expected iterator expression, found `{}`", token_text), + primary_text, + ) + } + (None, FoundAt::EndOfFile) => ( + "expected iterator expression, found end of file".to_string(), + "reached end of file here".to_string(), + ), + (None, FoundAt::Unknown) => ( + "expected iterator expression".to_string(), + "expected iterator expression here".to_string(), + ), + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + if let Some(control_keyword_span) = control_keyword_span + && !file.same_line(control_keyword_span.start, error.blame_span.end) + { + let secondary_text = if let Some(keyword_text) = control_keyword_text { + format!( + "after this `{}`, an iterator expression was expected", + keyword_text + ) + } else { + "after this control-flow keyword, an iterator expression was expected".to_string() + }; + + builder = builder.secondary_label(control_keyword_span, secondary_text); + } + + builder + .primary_label(error.blame_span, primary_text) + .code("P0015") + .build() +} + +pub(super) fn diagnostic_for_loop_header_initializer_invalid_start<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let for_keyword_span = error + .related_spans + .get(diagnostic_labels::EXPRESSION_REQUIRED_BY) + .copied(); + + let left_parenthesis_span = error + .related_spans + .get(diagnostic_labels::EXPRESSION_EXPECTED_AFTER) + .copied(); + + let found = found_at(file, error.blame_span.end); + + let (header_text, primary_text) = match found { + FoundAt::Token(token_text) => ( + format!( + "expected initializer expression or `;` after `(` in `for` header, found `{}`", + token_text + ), + format!("unexpected `{}`", token_text), + ), + FoundAt::EndOfFile => ( + "expected initializer expression or `;` after `(` in `for` header, found end of file" + .to_string(), + "reached end of file here".to_string(), + ), + FoundAt::Unknown => ( + "expected initializer expression or `;` after `(` in `for` header".to_string(), + "expected initializer expression or `;` here".to_string(), + ), + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + let left_parenthesis_label_is_shown = if let Some(span) = left_parenthesis_span + && !file.same_line(span.start, error.blame_span.end) + { + builder = builder.secondary_label( + span, + "after this `(`, an initializer expression or `;` was expected", + ); + true + } else { + false + }; + + if !left_parenthesis_label_is_shown && let Some(for_span) = for_keyword_span { + let for_is_separated_from_error = !file.same_line(for_span.start, error.blame_span.end); + let for_is_separated_from_left_parenthesis = left_parenthesis_span + .map(|span| !file.same_line(for_span.start, span.start)) + .unwrap_or(true); + + if for_is_separated_from_error && for_is_separated_from_left_parenthesis { + builder = builder.secondary_label(for_span, "`for` loop starts here"); + } + } + + builder + .primary_label(error.blame_span, primary_text) + .code("P0016") + .build() +} + +pub(super) fn diagnostic_for_loop_header_missing_semicolon_after_initializer<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let initializer_span = error.related_spans.get("for_header_initializer").copied(); + + let found = found_at(file, error.blame_span.end); + let initializer_is_omitted = initializer_span.is_none(); + + let header_text = if initializer_is_omitted { + "missing first `;` in `for` header".to_string() + } else { + "missing `;` after initializer in `for` header".to_string() + }; + + let primary_text = match found { + FoundAt::Token(token_text) => { + if initializer_is_omitted { + format!("expected first `;` before `{}`", token_text) + } else { + format!("expected `;` before `{}`", token_text) + } + } + FoundAt::EndOfFile => { + if initializer_is_omitted { + "expected first `;` before end of file".to_string() + } else { + "expected `;` before end of file".to_string() + } + } + FoundAt::Unknown => { + if initializer_is_omitted { + "expected first `;` here".to_string() + } else { + "expected `;` here".to_string() + } + } + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + if let Some(span) = initializer_span { + builder = builder.secondary_label(span, "initializer ends here"); + } + + let primary_span = collapse_span_to_end_on_same_line(file, error.blame_span); + + builder + .primary_label(primary_span, primary_text) + .code("P0017") + .build() +} + +pub(super) fn diagnostic_for_loop_header_condition_invalid_start<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let for_keyword_span = error + .related_spans + .get(diagnostic_labels::EXPRESSION_REQUIRED_BY) + .copied(); + + let first_semicolon_span = error + .related_spans + .get(diagnostic_labels::EXPRESSION_EXPECTED_AFTER) + .copied(); + + let found = found_at(file, error.blame_span.end); + + let (header_text, primary_text) = match found { + FoundAt::Token(token_text) => ( + format!( + "expected condition expression or second `;` in `for` header, found `{}`", + token_text + ), + format!("unexpected `{}`", token_text), + ), + FoundAt::EndOfFile => ( + "expected condition expression or second `;` in `for` header, found end of file" + .to_string(), + "reached end of file here".to_string(), + ), + FoundAt::Unknown => ( + "expected condition expression or second `;` in `for` header".to_string(), + "expected condition expression or `;` here".to_string(), + ), + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + let first_semicolon_label_is_shown = if let Some(span) = first_semicolon_span + && !file.same_line(span.start, error.blame_span.end) + { + builder = builder.secondary_label( + span, + "after this `;`, a condition expression or another `;` was expected", + ); + true + } else { + false + }; + + if !first_semicolon_label_is_shown && let Some(for_span) = for_keyword_span { + let for_is_separated_from_error = !file.same_line(for_span.start, error.blame_span.end); + let for_is_separated_from_first_semicolon = first_semicolon_span + .map(|span| !file.same_line(for_span.start, span.start)) + .unwrap_or(true); + + if for_is_separated_from_error && for_is_separated_from_first_semicolon { + builder = builder.secondary_label(for_span, "`for` loop starts here"); + } + } + + builder + .primary_label(error.blame_span, primary_text) + .code("P0018") + .build() +} + +pub(super) fn diagnostic_for_loop_header_missing_semicolon_after_condition<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let condition_span = error.related_spans.get("for_header_condition").copied(); + + let found = found_at(file, error.blame_span.end); + + let condition_is_omitted = condition_span.is_none(); + let found_is_eof = matches!(found, FoundAt::EndOfFile); + + let header_text = if condition_is_omitted && found_is_eof { + "missing second `;` in `for` header".to_string() + } else { + "missing `;` after condition in `for` header".to_string() + }; + + let primary_text = match found { + FoundAt::Token(token_text) => { + if condition_is_omitted { + format!("expected second `;` before `{}`", token_text) + } else { + format!("expected `;` before `{}`", token_text) + } + } + FoundAt::EndOfFile => { + if condition_is_omitted { + "expected second `;` before end of file".to_string() + } else { + "expected `;` before end of file".to_string() + } + } + FoundAt::Unknown => { + if condition_is_omitted { + "expected second `;` here".to_string() + } else { + "expected `;` here".to_string() + } + } + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + if let Some(span) = condition_span { + builder = builder.secondary_label(span, "condition ends here"); + } + + let primary_span = collapse_span_to_end_on_same_line(file, error.blame_span); + + builder + .primary_label(primary_span, primary_text) + .code("P0019") + .build() +} + +pub(super) fn diagnostic_for_loop_header_step_invalid_start<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let for_keyword_span = error + .related_spans + .get(diagnostic_labels::EXPRESSION_REQUIRED_BY) + .copied(); + + let second_semicolon_span = error + .related_spans + .get(diagnostic_labels::EXPRESSION_EXPECTED_AFTER) + .copied(); + + let found = found_at(file, error.blame_span.end); + + let (header_text, primary_text) = match found { + FoundAt::Token(";") => ( + "unexpected third `;` in `for` header".to_string(), + "expected step expression or `)` after the second `;`".to_string(), + ), + FoundAt::Token(token_text) => ( + format!( + "expected step expression or `)` after the second `;` in `for` header, found `{}`", + token_text + ), + format!("unexpected `{}`", token_text), + ), + FoundAt::EndOfFile => ( + "expected step expression or `)` after the second `;` in `for` header, found end of file" + .to_string(), + "reached end of file here".to_string(), + ), + FoundAt::Unknown => ( + "expected step expression or `)` after the second `;` in `for` header".to_string(), + "expected step expression or `)` here".to_string(), + ), + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + let second_semicolon_label_is_shown = if let Some(span) = second_semicolon_span + && !file.same_line(span.start, error.blame_span.end) + { + builder = builder.secondary_label( + span, + "after this `;`, a step expression or `)` was expected", + ); + true + } else { + false + }; + + if !second_semicolon_label_is_shown && let Some(for_span) = for_keyword_span { + let for_is_separated_from_error = !file.same_line(for_span.start, error.blame_span.end); + let for_is_separated_from_second_semicolon = second_semicolon_span + .map(|span| !file.same_line(for_span.start, span.start)) + .unwrap_or(true); + + if for_is_separated_from_error && for_is_separated_from_second_semicolon { + builder = builder.secondary_label(for_span, "`for` loop starts here"); + } + } + + builder + .primary_label(error.blame_span, primary_text) + .code("P0020") + .build() +} + +pub(super) fn diagnostic_for_loop_header_missing_closing_parenthesis<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let for_header_start_span = error.related_spans.get("for_header_start").copied(); + + let primary_text = match found_at(file, error.blame_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 `)` to close `for` header"); + + let primary_context_span = for_header_start_span + .filter(|span| should_show_context_label(file, *span, error.blame_span)); + + if let Some(span) = primary_context_span { + builder = builder.secondary_label(span, "`for` header starts here"); + } + + let primary_span = + primary_span_with_optional_multiline_context(file, primary_context_span, error.blame_span); + + builder + .primary_label(primary_span, primary_text) + .code("P0021") + .build() +} diff --git a/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs b/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs new file mode 100644 index 0000000..e659493 --- /dev/null +++ b/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs @@ -0,0 +1,143 @@ +//! Conversion from parser errors to user-facing diagnostics. +//! +//! This module maps [`ParseError`] values produced by the parser to structured +//! [`Diagnostic`] values suitable for rendering. +//! +//! It owns the top-level dispatch by [`crate::parser::ParseErrorKind`] and +//! keeps small shared utilities used by parse-error diagnostic constructors. +//! +//! Concrete diagnostic constructors are grouped into submodules that mirror +//! parser areas or grammar families. + +use super::{Diagnostic, DiagnosticBuilder}; +use crate::lexer::{TokenPosition, TokenSpan, TokenizedFile}; +use crate::parser::{ParseError, ParseErrorKind}; + +mod primary_expressions; +mod control_flow_expressions; + +#[derive(Clone, Copy)] +enum FoundAt<'src> { + Token(&'src str), + EndOfFile, + Unknown, +} + +fn found_at<'src>(file: &TokenizedFile<'src>, position: TokenPosition) -> FoundAt<'src> { + if let Some(token_text) = file.token_text(position) { + FoundAt::Token(token_text) + } else if file.is_eof(&position) { + FoundAt::EndOfFile + } else { + FoundAt::Unknown + } +} + +fn collapse_span_to_end_on_same_line(file: &TokenizedFile<'_>, mut span: TokenSpan) -> TokenSpan { + if file.same_line(span.start, span.end) { + span.start = span.end; + } + span +} + +fn should_show_context_label<'src>( + file: &TokenizedFile<'src>, + context_span: TokenSpan, + blame_span: TokenSpan, +) -> bool { + !file.same_line(context_span.start, blame_span.end) +} + +fn primary_span_with_optional_multiline_context<'src>( + file: &TokenizedFile<'src>, + context_span: Option, + blame_span: TokenSpan, +) -> TokenSpan { + if let Some(context_span) = context_span + && should_show_context_label(file, context_span, blame_span) + { + TokenSpan { + start: context_span.start, + end: blame_span.end, + } + } else { + collapse_span_to_end_on_same_line(file, blame_span) + } +} + +pub(crate) fn diagnostic_from_parse_error<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + use primary_expressions::*; + use control_flow_expressions::*; + match error.kind { + // primary_expressions.rs + ParseErrorKind::ParenthesizedExpressionInvalidStart => { + diagnostic_parenthesized_expression_invalid_start(error, file) + } + ParseErrorKind::ExpressionExpected => diagnostic_expression_expected(error, file), + ParseErrorKind::ParenthesizedExpressionMissingClosingParenthesis => { + diagnostic_parenthesized_expression_missing_closing_parenthesis(error, file) + } + ParseErrorKind::ClassTypeMissingTypeArgument => { + diagnostic_class_type_missing_type_argument(error, file) + } + ParseErrorKind::ClassTypeExpectedQualifiedTypeName => { + diagnostic_class_type_expected_qualified_type_name(error, file) + } + ParseErrorKind::ClassTypeInvalidStart => diagnostic_class_type_invalid_start(error, file), + ParseErrorKind::ClassTypeMissingClosingAngleBracket => { + diagnostic_class_type_missing_closing_angle_bracket(error, file) + } + ParseErrorKind::NewMissingClassSpecifier => { + diagnostic_new_missing_class_specifier(error, file) + } + ParseErrorKind::NewTooManyArguments => diagnostic_new_too_many_arguments(error, file), + + ParseErrorKind::NewMissingClosingParenthesis => { + diagnostic_new_missing_closing_parenthesis(error, file) + } + ParseErrorKind::NewArgumentMissingComma => { + diagnostic_new_argument_missing_comma(error, file) + } + // control_flow_expressions.rs + ParseErrorKind::ConditionExpected => { + diagnostic_condition_expected(error, file) + } + ParseErrorKind::ControlFlowBodyExpected => { + diagnostic_control_flow_body_expected(error, file) + } + ParseErrorKind::DoMissingUntil => { + diagnostic_do_missing_until(error, file) + } + ParseErrorKind::ForEachIteratorExpressionExpected => { + diagnostic_for_each_iterator_expression_expected(error, file) + } + ParseErrorKind::ForEachIteratorExpressionExpected => { + diagnostic_for_each_iterator_expression_expected(error, file) + } + ParseErrorKind::ForLoopHeaderInitializerInvalidStart => { + diagnostic_for_loop_header_initializer_invalid_start(error, file) + } + ParseErrorKind::ForLoopHeaderMissingSemicolonAfterInitializer => { + diagnostic_for_loop_header_missing_semicolon_after_initializer(error, file) + } + ParseErrorKind::ForLoopHeaderConditionInvalidStart => { + diagnostic_for_loop_header_condition_invalid_start(error, file) + } + ParseErrorKind::ForLoopHeaderMissingSemicolonAfterCondition => { + diagnostic_for_loop_header_missing_semicolon_after_condition(error, file) + } + ParseErrorKind::ForLoopHeaderStepInvalidStart => { + diagnostic_for_loop_header_step_invalid_start(error, file) + } + ParseErrorKind::ForLoopHeaderMissingClosingParenthesis => { + diagnostic_for_loop_header_missing_closing_parenthesis(error, file) + } + + _ => DiagnosticBuilder::error(format!("error {:?} while parsing", error.kind)) + .primary_label(error.covered_span, "happened here") + .build(), + } +} \ No newline at end of file diff --git a/rottlib/src/diagnostics/parse_error_diagnostics/primary_expressions.rs b/rottlib/src/diagnostics/parse_error_diagnostics/primary_expressions.rs new file mode 100644 index 0000000..8c07233 --- /dev/null +++ b/rottlib/src/diagnostics/parse_error_diagnostics/primary_expressions.rs @@ -0,0 +1,538 @@ +use super::{Diagnostic, DiagnosticBuilder, FoundAt, collapse_span_to_end_on_same_line, found_at}; +use crate::lexer::{TokenSpan, TokenizedFile}; +use crate::parser::ParseError; + +pub(super) fn diagnostic_parenthesized_expression_invalid_start<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let (header_text, primary_text) = match found_at(file, error.blame_span.end) { + FoundAt::Token(token_text) => ( + format!( + "expected expression inside parentheses, found `{}`", + token_text + ), + format!("unexpected `{}`", token_text), + ), + FoundAt::EndOfFile => ( + "expected expression, found end of file".to_string(), + "reached end of file here".to_string(), + ), + FoundAt::Unknown => ( + "expected expression inside parentheses".to_string(), + "expected expression".to_string(), + ), + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + if let Some(related_span) = error.related_spans.get("left_parenthesis") + && !file.same_line(related_span.start, error.blame_span.end) + { + builder = builder.secondary_label(*related_span, "parenthesized expression starts here"); + } + + let primary_span = collapse_span_to_end_on_same_line(file, error.blame_span); + + builder + .primary_label(primary_span, primary_text) + .code("P0001") + .build() +} + +pub(super) fn diagnostic_expression_expected<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let prefix_operator_span = error.related_spans.get("prefix_operator").copied(); + let infix_operator_span = error.related_spans.get("infix_operator").copied(); + let operator_span = infix_operator_span.or(prefix_operator_span); + + let operator_text = operator_span.and_then(|span| file.token_text(span.end)); + + let (header_text, primary_text) = match (operator_text, found_at(file, error.blame_span.end)) { + (Some(operator_text), FoundAt::Token(token_text)) => ( + format!( + "expected expression after `{}`, found `{}`", + operator_text, token_text + ), + format!("unexpected `{}`", token_text), + ), + (Some(operator_text), FoundAt::EndOfFile) => ( + format!( + "expected expression after `{}`, found end of file", + operator_text + ), + "reached end of file here".to_string(), + ), + (Some(operator_text), FoundAt::Unknown) => ( + format!("expected expression after `{}`", operator_text), + "expected expression".to_string(), + ), + + (None, FoundAt::Token(token_text)) => ( + format!("expected expression, found `{}`", token_text), + format!("unexpected `{}`", token_text), + ), + (None, FoundAt::EndOfFile) => ( + "expected expression, found end of file".to_string(), + "reached end of file here".to_string(), + ), + (None, FoundAt::Unknown) => ( + "expected expression".to_string(), + "expected expression".to_string(), + ), + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + if let Some(span) = operator_span + && !file.same_line(span.start, error.blame_span.end) + { + let secondary_text = if let Some(operator_text) = operator_text { + format!("after this `{}`, an expression was expected", operator_text) + } else { + "an expression was expected after this operator".to_string() + }; + + builder = builder.secondary_label(span, secondary_text); + } + + builder + .primary_label(error.blame_span, primary_text) + .code("P0002") + .build() +} + +pub(super) fn diagnostic_parenthesized_expression_missing_closing_parenthesis<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let left_parenthesis_span = error.related_spans.get("left_parenthesis").copied(); + + let primary_text = match found_at(file, error.blame_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 `)` to close parenthesized expression"); + + if let Some(span) = left_parenthesis_span + && !file.same_line(span.start, error.blame_span.end) + { + builder = builder.secondary_label(span, "parenthesized expression starts here"); + } + + let primary_span = collapse_span_to_end_on_same_line(file, error.blame_span); + + builder + .primary_label(primary_span, primary_text) + .code("P0003") + .build() +} + +pub(super) fn diagnostic_class_type_missing_type_argument<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let left_angle_bracket_span = error.related_spans.get("left_angle_bracket").copied(); + + let primary_text = match found_at(file, error.blame_span.end) { + FoundAt::Token(token_text) => format!("expected a type name before `{}`", token_text), + FoundAt::EndOfFile => "expected a type name before end of file".to_string(), + FoundAt::Unknown => "expected a type name here".to_string(), + }; + + let mut builder = DiagnosticBuilder::error("missing type argument in `class<...>`"); + + if let Some(span) = left_angle_bracket_span + && !file.same_line(span.start, error.blame_span.end) + { + builder = builder.secondary_label(span, "type argument starts here"); + } + + let primary_span = collapse_span_to_end_on_same_line(file, error.blame_span); + + builder + .primary_label(primary_span, primary_text) + .code("P0004") + .build() +} + +pub(super) fn diagnostic_class_type_expected_qualified_type_name<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let qualifier_dot_span = error.related_spans.get("qualifier_dot").copied(); + let class_span = error.related_spans.get("class_keyword").copied(); + + let (header_text, primary_text) = match found_at(file, error.blame_span.end) { + FoundAt::Token(token_text) => ( + format!( + "expected another type segment after `.`, found `{}`", + token_text + ), + format!("unexpected `{}`", token_text), + ), + FoundAt::EndOfFile => ( + "expected another type segment after `.`, found end of file".to_string(), + "reached end of file here".to_string(), + ), + FoundAt::Unknown => ( + "expected another type segment after `.`".to_string(), + "expected another type segment here".to_string(), + ), + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + if let Some(dot_span) = qualifier_dot_span { + if !file.same_line(dot_span.start, error.blame_span.end) { + builder = builder.secondary_label( + dot_span, + "after this `.`, another type segment was expected", + ); + } + + if let Some(class_span) = class_span + && !file.same_line(class_span.end, dot_span.start) + { + builder = builder.secondary_label( + class_span, + "while parsing this `class<...>` type expression", + ); + } + } + + let primary_span = collapse_span_to_end_on_same_line(file, error.blame_span); + + builder + .primary_label(primary_span, primary_text) + .code("P0005") + .build() +} + +pub(super) fn diagnostic_class_type_invalid_start<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let left_angle_bracket_span = error.related_spans.get("left_angle_bracket").copied(); + let class_keyword_span = error.related_spans.get("class_keyword").copied(); + + let (header_text, primary_text) = match found_at(file, error.blame_span.end) { + FoundAt::Token(token_text) => ( + format!( + "expected a type argument after `<` in `class<...>`, found `{}`", + token_text + ), + format!("unexpected `{}`", token_text), + ), + FoundAt::EndOfFile => ( + "expected a type argument after `<` in `class<...>`, found end of file".to_string(), + "reached end of file here".to_string(), + ), + FoundAt::Unknown => ( + "expected a type argument after `<` in `class<...>`".to_string(), + "expected a type argument here".to_string(), + ), + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + if let Some(left_angle_span) = left_angle_bracket_span { + if !file.same_line(left_angle_span.start, error.blame_span.end) { + builder = builder.secondary_label(left_angle_span, "type argument starts here"); + } + + if let Some(class_span) = class_keyword_span + && !file.same_line(class_span.end, left_angle_span.start) + { + builder = builder.secondary_label( + class_span, + "while parsing this `class<...>` type expression", + ); + } + } + + let primary_span = collapse_span_to_end_on_same_line(file, error.blame_span); + + builder + .primary_label(primary_span, primary_text) + .code("P0006") + .build() +} + +pub(super) fn diagnostic_class_type_missing_closing_angle_bracket<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let left_angle_bracket_span = error.related_spans.get("left_angle_bracket").copied(); + let class_keyword_span = error.related_spans.get("class_keyword").copied(); + + let primary_text = match found_at(file, error.blame_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 `>` to close `class<...>`"); + + if let Some(left_angle_bracket_span) = left_angle_bracket_span { + if !file.same_line(left_angle_bracket_span.start, error.blame_span.end) { + builder = builder.secondary_label(left_angle_bracket_span, "type argument starts here"); + } + + if let Some(class_keyword_span) = class_keyword_span + && !file.same_line(class_keyword_span.end, left_angle_bracket_span.start) + { + builder = builder.secondary_label( + class_keyword_span, + "while parsing this `class<...>` type expression", + ); + } + } + + let primary_span = collapse_span_to_end_on_same_line(file, error.blame_span); + + builder + .primary_label(primary_span, primary_text) + .code("P0007") + .build() +} + +pub(super) fn diagnostic_new_missing_class_specifier<'src>( + mut error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let new_keyword_span = error.related_spans.get("new_keyword").copied(); + let argument_list_end_span = error.related_spans.get("argument_list_end").copied(); + + let construct_text = if argument_list_end_span.is_some() { + "`new(...)`" + } else { + "`new`" + }; + + let (header_text, primary_text) = match found_at(file, error.blame_span.end) { + FoundAt::Token(token_text) => ( + format!( + "expected class expression after {}, found `{}`", + construct_text, token_text + ), + format!("unexpected `{}`", token_text), + ), + FoundAt::EndOfFile => ( + format!( + "expected class expression after {}, found end of file", + construct_text + ), + "reached end of file here".to_string(), + ), + FoundAt::Unknown => ( + format!("expected class expression after {}", construct_text), + "expected class expression here".to_string(), + ), + }; + + let mut builder = DiagnosticBuilder::error(header_text); + + if let Some(new_keyword_span) = new_keyword_span + && !file.same_line(new_keyword_span.start, error.blame_span.end) + { + builder = builder.secondary_label(new_keyword_span, "`new` expression starts here"); + } + + match argument_list_end_span { + Some(argument_list_end_span) + if !file.same_line(argument_list_end_span.start, error.blame_span.end) => + { + builder = builder.secondary_label( + argument_list_end_span, + "optional `new(...)` arguments end here", + ); + error.blame_span.start = argument_list_end_span.start; + } + + Some(_) | None => { + error.blame_span.start = error.blame_span.end; + } + } + + builder + .primary_label(error.blame_span, primary_text) + .code("P0008") + .build() +} + +pub(super) fn diagnostic_new_too_many_arguments<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let new_keyword_span = error.related_spans.get("new_keyword").copied(); + let left_parenthesis_span = error.related_spans.get("left_parenthesis").copied(); + let last_allowed_argument_span = error.related_spans.get("last_allowed_argument").copied(); + let first_extra_argument_span = error.related_spans.get("first_extra_argument").copied(); + + let found = found_at(file, error.blame_span.end); + + let (primary_text, mut primary_span) = + if let Some(first_extra_argument_span) = first_extra_argument_span { + ( + "a fourth argument is not allowed in `new(...)`".to_string(), + first_extra_argument_span, + ) + } else if matches!(found, FoundAt::Token(",")) { + ( + "this `,` starts a fourth argument, which is not allowed here".to_string(), + error.blame_span, + ) + } else if matches!(found, FoundAt::EndOfFile) { + ( + "a fourth argument is not allowed here".to_string(), + error.blame_span, + ) + } else if let FoundAt::Token(token_text) = found { + ( + format!("unexpected start of a fourth argument: `{}`", token_text), + error.blame_span, + ) + } else { + ( + "a fourth argument is not allowed here".to_string(), + error.blame_span, + ) + }; + + let mut builder = DiagnosticBuilder::error("too many arguments in `new(...)`"); + + if let Some(new_keyword_span) = new_keyword_span { + let show_new_label = !file.same_line(new_keyword_span.start, primary_span.end) + && match left_parenthesis_span { + Some(left_parenthesis_span) => { + !file.same_line(new_keyword_span.start, left_parenthesis_span.start) + } + None => true, + }; + + if show_new_label { + builder = builder.secondary_label(new_keyword_span, "`new` expression starts here"); + } + } + + if let Some(left_parenthesis_span) = left_parenthesis_span + && !file.same_line(left_parenthesis_span.start, primary_span.end) + { + builder = builder.secondary_label( + left_parenthesis_span, + "`new(...)` argument list starts here", + ); + } + + if let Some(last_allowed_argument_span) = last_allowed_argument_span + && !file.same_line(last_allowed_argument_span.start, primary_span.end) + { + builder = builder.secondary_label( + last_allowed_argument_span, + "the third allowed argument ends here", + ); + + primary_span.start = last_allowed_argument_span.start; + } + + builder + .primary_label(primary_span, primary_text) + .note("`new(...)` accepts up to three optional arguments: `outer`, `name`, and `flags`.") + .code("P0009") + .build() +} + +pub(super) fn diagnostic_new_missing_closing_parenthesis<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let left_parenthesis_span = error.related_spans.get("left_parenthesis").copied(); + let new_keyword_span = error.related_spans.get("new_keyword").copied(); + + let primary_text = match found_at(file, error.blame_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 `)` to close `new(...)` argument list"); + + if let Some(left_parenthesis_span) = left_parenthesis_span { + if !file.same_line(left_parenthesis_span.start, error.blame_span.end) { + if let Some(new_keyword_span) = new_keyword_span + && !file.same_line(new_keyword_span.end, left_parenthesis_span.start) + && !file.same_line(new_keyword_span.start, error.blame_span.end) + { + builder = builder.secondary_label(new_keyword_span, "`new` expression starts here"); + } + + builder = builder.secondary_label( + left_parenthesis_span, + "`new(...)` argument list starts here", + ); + } + } + + let primary_span = collapse_span_to_end_on_same_line(file, error.blame_span); + + builder + .primary_label(primary_span, primary_text) + .code("P0010") + .build() +} + +pub(super) fn diagnostic_new_argument_missing_comma<'src>( + error: ParseError, + file: &TokenizedFile<'src>, +) -> Diagnostic { + let new_keyword_span = error.related_spans.get("new_keyword").copied(); + let left_parenthesis_span = error.related_spans.get("left_parenthesis").copied(); + let previous_argument_span = error.related_spans.get("previous_argument").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 `,` before this argument".to_string(), + }; + + let mut builder = DiagnosticBuilder::error("missing `,` between `new(...)` arguments"); + + if let Some(new_keyword_span) = new_keyword_span { + let show_new_label = !file.same_line(new_keyword_span.start, primary_span.end) + && match left_parenthesis_span { + Some(left_parenthesis_span) => { + !file.same_line(new_keyword_span.start, left_parenthesis_span.start) + } + None => true, + }; + + if show_new_label { + builder = builder.secondary_label(new_keyword_span, "`new` expression starts here"); + } + } + + if let Some(left_parenthesis_span) = left_parenthesis_span + && !file.same_line(left_parenthesis_span.start, primary_span.end) + { + builder = builder.secondary_label( + left_parenthesis_span, + "`new(...)` argument list starts here", + ); + } + + if let Some(previous_argument_span) = previous_argument_span { + builder = builder.secondary_label(previous_argument_span, "previous argument ends here"); + } + + builder + .primary_label(primary_span, primary_text) + .code("P0011") + .build() +} diff --git a/rottlib/src/diagnostics/render.rs b/rottlib/src/diagnostics/render.rs index 6ac36f8..a33f603 100644 --- a/rottlib/src/diagnostics/render.rs +++ b/rottlib/src/diagnostics/render.rs @@ -1,15 +1,13 @@ -use crate::diagnostics::{self, Diagnostic, Severity}; +use crate::diagnostics::{Diagnostic, Severity}; use crate::lexer::{TokenSpan, TokenizedFile}; use core::convert::Into; use crossterm::style::Stylize; -use crossterm::terminal::disable_raw_mode; use std::cmp::max; use std::collections::HashMap; use std::ops::RangeInclusive; const INDENT: &str = " "; -const MAX_LINES_LIMIT: usize = 10; /* error: expected one of `,`, `:`, or `}`, found `token_to` @@ -77,7 +75,7 @@ struct RangeSet { } impl RangeSet { - fn get(&self, index: usize) -> Option<&RangeInclusive> { + /*fn get(&self, index: usize) -> Option<&RangeInclusive> { if self.primary_range.is_some() { if index == 0 { return self.primary_range.as_ref(); @@ -91,7 +89,7 @@ impl RangeSet { fn len(&self) -> usize { self.secondary_ranges.len() + if self.primary_range.is_some() { 1 } else { 0 } - } + }*/ fn iter(&self) -> impl Iterator> { self.primary_range @@ -232,7 +230,10 @@ fn max_line_number_width(ranges: &RangeSet) -> usize { } } -fn span_to_range<'src>(span: TokenSpan, file: &TokenizedFile<'src>) -> Option> { +fn span_to_range<'src>( + span: TokenSpan, + file: &TokenizedFile<'src>, +) -> Option> { let start_line = file.token_line(span.start)?; let end_line = file.token_line(span.end)?; @@ -264,6 +265,7 @@ impl Diagnostic { self.render_header(); println!("{INDENT}{}: {}", "in file".blue().bold(), file_path.into()); self.render_lines(file); + self.render_help_and_notes(file); } /*StartRange { label_type: LabelType, @@ -669,4 +671,44 @@ impl Diagnostic { ); } } + + fn render_help_and_notes<'src>(&self, file: &TokenizedFile<'src>) { + if self.help().is_none() && self.notes().is_empty() { + return; + } + + let ranges = make_ranges(file, self); + let max_line_number_width = max(max_line_number_width(&ranges), 3); + + // Blank gutter separator, like rustc's trailing `|` + println!( + "{}", + format!(" {} |", " ".repeat(max_line_number_width)) + .blue() + .bold() + ); + + if let Some(help) = self.help() { + self.render_trailer_line("help", help, max_line_number_width); + } + + for note in self.notes() { + self.render_trailer_line("note", note, max_line_number_width); + } + } + + fn render_trailer_line(&self, kind: &str, message: &str, max_line_number_width: usize) { + let prefix = format!(" {} = ", " ".repeat(max_line_number_width)) + .blue() + .bold() + .to_string(); + + let kind = match kind { + "help" => "help".green().bold().to_string(), + "note" => "note".blue().bold().to_string(), + _ => kind.bold().to_string(), + }; + + println!("{prefix}{kind}: {message}"); + } } diff --git a/rottlib/src/lexer/token.rs b/rottlib/src/lexer/token.rs index 5e167d6..b8ec8b3 100644 --- a/rottlib/src/lexer/token.rs +++ b/rottlib/src/lexer/token.rs @@ -336,6 +336,76 @@ impl Token { | Keyword::Exec ) } + + /// Returns `true` if this token is definitely not a valid first token of an + /// expression. + /// + /// This is a conservative recovery predicate: + /// - `true` means expression parsing should not be attempted at this token; + /// - `false` means the token might start an expression, or that the normal + /// expression parser should report the more specific error. + #[must_use] + pub const fn is_definitely_not_expression_start(self) -> bool { + match self { + Self::Keyword(keyword) => keyword.is_definitely_not_expression_start(), + + // Closing delimiters / separators. + Self::RightParenthesis + | Self::RightBrace + | Self::RightBracket + | Self::Semicolon + | Self::Comma + | Self::Colon + | Self::Question + + // Tokens that only continue a previous expression. + | Self::Period + + // Infix / postfix / assignment operators. + | Self::Exponentiation + | Self::Multiply + | Self::Divide + | Self::Modulo + | Self::ConcatSpace + | Self::Concat + | Self::LeftShift + | Self::LogicalRightShift + | Self::RightShift + | Self::Less + | Self::LessEqual + | Self::Greater + | Self::GreaterEqual + | Self::Equal + | Self::NotEqual + | Self::ApproximatelyEqual + | Self::BitwiseAnd + | Self::BitwiseOr + | Self::BitwiseXor + | Self::LogicalAnd + | Self::LogicalXor + | Self::LogicalOr + | Self::Assign + | Self::MultiplyAssign + | Self::DivideAssign + | Self::ModuloAssign + | Self::PlusAssign + | Self::MinusAssign + | Self::ConcatAssign + | Self::ConcatSpaceAssign + + // Non-expression trivia / technical tokens. + | Self::ExecDirective + | Self::CppBlock + | Self::Hash + | Self::LineComment + | Self::BlockComment + | Self::Newline + | Self::Whitespace + | Self::Error => true, + + _ => false, + } + } } /// Reserved words of Fermented `UnrealScript`. @@ -557,4 +627,11 @@ impl Keyword { | Self::Delegate ) } + + /// Returns `true` if this keyword is definitely not a valid first token of + /// an expression. + #[must_use] + pub const fn is_definitely_not_expression_start(self) -> bool { + matches!(self, Self::Else | Self::Case | Self::Until) + } } diff --git a/rottlib/src/parser/cursor.rs b/rottlib/src/parser/cursor.rs index c989e44..d0cca2b 100644 --- a/rottlib/src/parser/cursor.rs +++ b/rottlib/src/parser/cursor.rs @@ -4,6 +4,8 @@ //! [`TriviaIndexBuilder`]. Significant tokens exclude whitespace and comments; //! see [`parser::TriviaKind`]. +// TODO: need a refactor pass + use std::collections::VecDeque; // TODO: NO RETURNING EOF use crate::{ @@ -287,6 +289,18 @@ impl<'src, 'arena> Parser<'src, 'arena> { self.eat(Token::Keyword(keyword)) } + #[must_use] + pub(crate) fn eat_with_position(&mut self, token: Token) -> Option { + if let Some((next_token, next_token_position)) = self.peek_token_and_position() + && next_token == token + { + self.advance(); + Some(next_token_position) + } else { + None + } + } + /// Expects `expected` token as the next significant one. /// /// On match consumes the token and returns its [`TokenPosition`]. @@ -365,6 +379,9 @@ impl<'src, 'arena> Parser<'src, 'arena> { if peeked_position <= old_position { self.advance(); } + if self.file.is_eof(&old_position) { + panic!("parsing stuck at the eof"); + } } } } diff --git a/rottlib/src/parser/errors.rs b/rottlib/src/parser/errors.rs index a00f601..ade3856 100644 --- a/rottlib/src/parser/errors.rs +++ b/rottlib/src/parser/errors.rs @@ -22,55 +22,46 @@ pub enum ParseErrorKind { ExpressionExpected, /// P0003 ParenthesizedExpressionMissingClosingParenthesis, - // headline: missing type argument in \class<...>`` - // primary label on > or insertion site: expected a type name here - // secondary label on < or on class: type argument list starts here - // help: Write a type name, for example \class`.` - ClassTypeMissingTypeArgument { - left_angle_bracket_position: TokenPosition, - }, - // headline: missing closing \>` in `class<...>`` - // primary label on offending following token or EOF: expected \>` before this token` or at EOF: expected \>` here` - // secondary label on <: this \<` starts the type argument` - // help: Add \>` to close the class type expression.` - ClassTypeMissingClosingAngleBracket { - left_angle_bracket_position: TokenPosition, - }, - // headline: invalid type argument in \class<...>`` - // primary label on the bad token inside the angle brackets: expected a qualified type name here - // secondary label on class or <: while parsing this class type expression - // note: Only a type name is accepted between \<` and `>` here.` - ClassTypeInvalidTypeArgument { - left_angle_bracket_position: TokenPosition, - }, - // headline: too many arguments in \new(...)`` - // primary label on the fourth argument, or on the comma before it if that is easier: unexpected extra argument - // secondary label on the opening (: this argument list accepts at most three arguments - // note: The three slots are \outer`, `name`, and `flags`.` - // help: Remove the extra argument. - NewTooManyArguments { - left_parenthesis_position: TokenPosition, - }, - // headline: missing closing \)' in `new(...)`` - // primary label: expected \)' here` - // secondary label on the opening (: this argument list starts here - // help: Add \)' to close the argument list.` - NewMissingClosingParenthesis { - left_parenthesis_position: TokenPosition, - }, - // missing class specifier in \new` expression` - // Primary label on the first token where a class specifier should have started: expected a class specifier here - // Secondary label on new: \new` expression starts here` If there was an argument list, an additional secondary on ( is also reasonable: optional \new(...)` arguments end here` - // Help: Add the class or expression to instantiate after \new` or `new(...)`.` - NewMissingClassSpecifier { - new_keyword_position: TokenPosition, - }, + /// P0004 + ClassTypeMissingTypeArgument, + /// P0005 + ClassTypeExpectedQualifiedTypeName, + /// P0006 + ClassTypeInvalidStart, + /// P0007 + ClassTypeMissingClosingAngleBracket, + /// P0008 + NewMissingClassSpecifier, + /// P0009 + NewTooManyArguments, + /// P0010 + NewMissingClosingParenthesis, + /// P0011 + NewArgumentMissingComma, + /// P0012 + ConditionExpected, + /// P0013 + ControlFlowBodyExpected, + /// P0014 + DoMissingUntil, + /// P0015 + ForEachIteratorExpressionExpected, + /// P0016 + ForLoopHeaderInitializerInvalidStart, + /// P0017 + ForLoopHeaderMissingSemicolonAfterInitializer, + /// P0018 + ForLoopHeaderConditionInvalidStart, + /// P0019 + ForLoopHeaderMissingSemicolonAfterCondition, + /// P0020 + ForLoopHeaderStepInvalidStart, + /// P0021 + ForLoopHeaderMissingClosingParenthesis, // ================== Old errors to be thrown away! ================== /// Expression inside `(...)` could not be parsed and no closing `)` /// was found. FunctionCallMissingClosingParenthesis, - /// A `do` block was not followed by a matching `until`. - DoMissingUntil, /// Found an unexpected token while parsing an expression. ExpressionUnexpectedToken, DeclEmptyVariableDeclarations, @@ -86,14 +77,6 @@ pub enum ParseErrorKind { TypeSpecClassMissingInnerType, TypeSpecClassMissingClosingAngle, - /// A `for` loop is missing its opening `(`. - ForMissingOpeningParenthesis, - /// The first `;` in `for (init; cond; step)` is missing. - ForMissingInitializationSemicolon, - /// The second `;` in `for (init; cond; step)` is missing. - ForMissingConditionSemicolon, - /// The closing `)` of a `for` loop is missing. - ForMissingClosingParenthesis, /// An expression inside a block is not terminated with `;`. BlockMissingSemicolonAfterExpression, /// A statement inside a block is not terminated with `;`. @@ -308,7 +291,6 @@ pub enum ParseErrorKind { FunctionArgumentMissingComma, // Expression was required, but none started MissingExpression, - MissingBranchBody, CallableExpectedHeader, CallableExpectedKind, CallableOperatorInvalidPrecedence, @@ -339,7 +321,7 @@ pub struct ParseError { pub type ParseResult<'src, 'arena, T> = Result; impl crate::parser::Parser<'_, '_> { - pub(crate) fn make_error_here(&self, error_kind: ParseErrorKind) -> ParseError { + pub(crate) fn make_error_at_last_consumed(&self, error_kind: ParseErrorKind) -> ParseError { self.make_error_at(error_kind, self.last_consumed_position_or_start()) } diff --git a/rottlib/src/parser/grammar/class.rs b/rottlib/src/parser/grammar/class.rs index 62af34d..76b49d0 100644 --- a/rottlib/src/parser/grammar/class.rs +++ b/rottlib/src/parser/grammar/class.rs @@ -101,7 +101,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { }; let Some((token, modifier_position)) = self.peek_token_and_position() else { - return Err(self.make_error_here(ParseErrorKind::UnexpectedEndOfFile)); + return Err(self.make_error_at_last_consumed(ParseErrorKind::UnexpectedEndOfFile)); }; let mut consumed_inside_match = false; @@ -279,7 +279,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { self.advance(); Reliability::Unreliable } - _ => return Err(self.make_error_here(ParseErrorKind::ReplicationMissingReliability)), + _ => return Err(self.make_error_at_last_consumed(ParseErrorKind::ReplicationMissingReliability)), }; let condition = if self.eat_keyword(Keyword::If) { @@ -350,7 +350,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { .unwrap_or_else(|| self.last_consumed_position_or_start()); if self.peek_token().is_none() { - return Err(self.make_error_here(ParseErrorKind::UnexpectedEndOfFile)); + return Err(self.make_error_at_last_consumed(ParseErrorKind::UnexpectedEndOfFile)); } match self.parse_replication_rule() { @@ -679,12 +679,12 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { Ok(i128::MIN) } else { let magnitude_as_i128 = i128::try_from(magnitude) - .map_err(|_| self.make_error_here(ParseErrorKind::InvalidNumericLiteral))?; + .map_err(|_| self.make_error_at_last_consumed(ParseErrorKind::InvalidNumericLiteral))?; Ok(-magnitude_as_i128) } } else { i128::try_from(magnitude) - .map_err(|_| self.make_error_here(ParseErrorKind::InvalidNumericLiteral)) + .map_err(|_| self.make_error_at_last_consumed(ParseErrorKind::InvalidNumericLiteral)) } } @@ -692,7 +692,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { use ParseErrorKind::InvalidNumericLiteral; if body.is_empty() { - return Err(self.make_error_here(InvalidNumericLiteral)); + return Err(self.make_error_at_last_consumed(InvalidNumericLiteral)); } let (base, digits) = @@ -707,7 +707,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { }; if digits.is_empty() { - return Err(self.make_error_here(InvalidNumericLiteral)); + return Err(self.make_error_at_last_consumed(InvalidNumericLiteral)); } let mut accumulator: u128 = 0; @@ -719,15 +719,15 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { '0'..='9' => u128::from(character as u32 - '0' as u32), 'a'..='f' => u128::from(10 + (character as u32 - 'a' as u32)), 'A'..='F' => u128::from(10 + (character as u32 - 'A' as u32)), - _ => return Err(self.make_error_here(InvalidNumericLiteral)), + _ => return Err(self.make_error_at_last_consumed(InvalidNumericLiteral)), }; if digit_value >= base { - return Err(self.make_error_here(InvalidNumericLiteral)); + return Err(self.make_error_at_last_consumed(InvalidNumericLiteral)); } accumulator = accumulator .checked_mul(base) .and_then(|value| value.checked_add(digit_value)) - .ok_or_else(|| self.make_error_here(InvalidNumericLiteral))?; + .ok_or_else(|| self.make_error_at_last_consumed(InvalidNumericLiteral))?; } Ok(accumulator) @@ -767,7 +767,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { } _ => { return Err( - self.make_error_here(ParseErrorKind::DeclarationLiteralUnexpectedToken) + self.make_error_at_last_consumed(ParseErrorKind::DeclarationLiteralUnexpectedToken) ); } } @@ -812,7 +812,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { )?; if !matches!(next_token, Token::NameLiteral) { return Err( - self.make_error_here(ParseErrorKind::DeclarationLiteralUnexpectedToken) + self.make_error_at_last_consumed(ParseErrorKind::DeclarationLiteralUnexpectedToken) ); } let inner = &next_lexeme[1..next_lexeme.len() - 1]; @@ -827,7 +827,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { self.advance(); DeclarationLiteral::Identifier(lexeme) } - _ => return Err(self.make_error_here(ParseErrorKind::ExpressionUnexpectedToken)), + _ => return Err(self.make_error_at_last_consumed(ParseErrorKind::ExpressionUnexpectedToken)), }; Ok(DeclarationLiteralRef { @@ -936,7 +936,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { } } _ => { - self.make_error_here(ParseErrorKind::ListInvalidIdentifier) + self.make_error_at_last_consumed(ParseErrorKind::ListInvalidIdentifier) .sync_error_until(self, SyncLevel::ListSeparator) .report_error(self); } diff --git a/rottlib/src/parser/grammar/declarations/enum_definition.rs b/rottlib/src/parser/grammar/declarations/enum_definition.rs index 140afbe..76d7286 100644 --- a/rottlib/src/parser/grammar/declarations/enum_definition.rs +++ b/rottlib/src/parser/grammar/declarations/enum_definition.rs @@ -86,7 +86,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { while self.peek_token() == Some(Token::Comma) { self.advance(); } - self.make_error_here(ParseErrorKind::EnumEmptyVariants) + self.make_error_at_last_consumed(ParseErrorKind::EnumEmptyVariants) .widen_error_span_from(error_start_position) .report_error(self); if matches!(self.peek_token(), Some(Token::RightBrace) | None) { @@ -128,7 +128,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { // If we don't even get a good identifier - error is different return ControlFlow::Break(()); }; - self.make_error_here(ParseErrorKind::EnumNoSeparatorBetweenVariants) + self.make_error_at_last_consumed(ParseErrorKind::EnumNoSeparatorBetweenVariants) .widen_error_span_from(error_start_position) .report_error(self); diff --git a/rottlib/src/parser/grammar/declarations/type_specifier.rs b/rottlib/src/parser/grammar/declarations/type_specifier.rs index bb649a6..4fa595a 100644 --- a/rottlib/src/parser/grammar/declarations/type_specifier.rs +++ b/rottlib/src/parser/grammar/declarations/type_specifier.rs @@ -42,7 +42,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { .arena .alloc_node(TypeSpecifier::Named(type_name), full_span)) } - _ => Err(self.make_error_here(ParseErrorKind::TypeSpecExpectedType)), + _ => Err(self.make_error_at_last_consumed(ParseErrorKind::TypeSpecExpectedType)), } } diff --git a/rottlib/src/parser/grammar/declarations/var_specifiers.rs b/rottlib/src/parser/grammar/declarations/var_specifiers.rs index bc144aa..9df1c48 100644 --- a/rottlib/src/parser/grammar/declarations/var_specifiers.rs +++ b/rottlib/src/parser/grammar/declarations/var_specifiers.rs @@ -67,7 +67,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { next_token_position, )); } else { - self.make_error_here(ParseErrorKind::VarSpecNotIdentifier) + self.make_error_at_last_consumed(ParseErrorKind::VarSpecNotIdentifier) .sync_error_until(self, SyncLevel::ListSeparator) .report_error(self); } diff --git a/rottlib/src/parser/grammar/declarations/variable_declarators.rs b/rottlib/src/parser/grammar/declarations/variable_declarators.rs index 4ba4d9a..22762c2 100644 --- a/rottlib/src/parser/grammar/declarations/variable_declarators.rs +++ b/rottlib/src/parser/grammar/declarations/variable_declarators.rs @@ -88,7 +88,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { while self.peek_token() == Some(Token::Comma) { self.advance(); } - self.make_error_here(ParseErrorKind::DeclEmptyVariableDeclarations) + self.make_error_at_last_consumed(ParseErrorKind::DeclEmptyVariableDeclarations) .widen_error_span_from(error_start_position) .report_error(self); if matches!(self.peek_token(), Some(Token::Semicolon) | None) { @@ -125,7 +125,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { .sync_error_until(self, SyncLevel::Statement) .ok_or_report(self) { - self.make_error_here(ParseErrorKind::DeclNoSeparatorBetweenVariableDeclarations) + self.make_error_at_last_consumed(ParseErrorKind::DeclNoSeparatorBetweenVariableDeclarations) .widen_error_span_from(error_start_position) .report_error(self); declarators.push(parsed_declarator); diff --git a/rottlib/src/parser/grammar/expression/control_flow.rs b/rottlib/src/parser/grammar/expression/control_flow.rs index 023a745..12d9657 100644 --- a/rottlib/src/parser/grammar/expression/control_flow.rs +++ b/rottlib/src/parser/grammar/expression/control_flow.rs @@ -1,15 +1,26 @@ //! Control expression parsing for Fermented `UnrealScript`. //! -//! ## Condition parsing and legacy compatibility +//! ## Condition boundary recovery and legacy compatibility //! -//! Fermented `UnrealScript` allows omitting parentheses `(...)` around the -//! condition expression of `if`/`while`/etc. For compatibility with older -//! `UnrealScript` code, we also apply a special rule: +//! Fermented `UnrealScript` allows omitting parentheses `(...)` around +//! condition expressions of `if`/`while`/`until` and similar constructs. +//! Conditions are therefore parsed as ordinary expressions by default. //! -//! If a condition starts with `(`, we parse the condition as exactly the -//! matching parenthesized subexpression and stop at its corresponding `)`. -//! In other words, `( ... )` must cover the whole condition; trailing tokens -//! like `* c == d` are not allowed to continue the condition. +//! This means that a leading parenthesized expression may still be part of a +//! larger condition: +//! +//! ```unrealscript +//! if (2 + 2) * 2 < 7 { ... } +//! while (Index + 1) < Count DoWork(); +//! ``` +//! +//! For compatibility with older `UnrealScript` code, we apply one conservative +//! 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. //! //! This prevents the parser from accidentally consuming the following //! statement/body as part of the condition in older code such as: @@ -18,8 +29,18 @@ //! if ( AIController(Controller) != None ) Cross = vect(0,0,0); //! ``` //! -//! Trade-off: you cannot write `if (a + b) * c == d`; -//! write `if ((a + b) * c == d)` or `if d == (a + b) * c` instead. +//! 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. +//! +//! Operator tokens such as `*`, `+`, `<`, `==`, etc. do not trigger this +//! legacy cut-off. They allow the normal expression parser to continue the +//! condition. +//! +//! Trade-off: if an identifier-like token after the closing `)` was intended as +//! 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 //! @@ -58,37 +79,112 @@ //! 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::ast::{BranchBody, Expression, ExpressionRef, OptionalExpression}; use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan}; -use crate::parser::{ParseErrorKind, Parser, ResultRecoveryExt, SyncLevel}; +use crate::parser::{ + ParseErrorKind, ParseExpressionResult, ParseResult, Parser, ResultRecoveryExt, SyncLevel, + diagnostic_labels, +}; + +struct ParsedForHeader<'src, 'arena> { + initialization: OptionalExpression<'src, 'arena>, + condition: OptionalExpression<'src, 'arena>, + step: OptionalExpression<'src, 'arena>, + right_parenthesis_position: Option, +} impl<'src, 'arena> Parser<'src, 'arena> { + /// Returns whether a leading parenthesized condition should be cut off + /// at its closing `)` for legacy compatibility. + /// + /// This checks for the shape: + /// + /// ```text + /// ( ... ) identifier-like-token + /// ``` + /// + /// When this shape is found, the parser stops the condition at the matching + /// `)` and leaves the following identifier-like token for the branch body. + /// + /// This preserves old single-line forms such as: + /// + /// ```unrealscript + /// if (Condition) Cross = 7; + /// ``` + /// + /// while still allowing ordinary operator continuations such as: + /// + /// ```unrealscript + /// if (2 + 2) * 2 < 7 { ... } + /// ``` + fn should_apply_legacy_parenthesized_condition_cutoff(&mut self) -> bool { + if self.peek_token() != Some(Token::LeftParenthesis) { + return false; + } + return true; + let mut nesting_depth: usize = 1; + let mut lookahead_token_offset: usize = 1; + while let Some(next_token) = self.peek_token_at(lookahead_token_offset) { + match next_token { + Token::LeftParenthesis => nesting_depth += 1, + Token::RightParenthesis => { + if nesting_depth <= 1 { + return self + .peek_token_at(lookahead_token_offset + 1) + .map(|token| token.is_valid_identifier_name()) + .unwrap_or_default(); + } + nesting_depth -= 1; + } + _ => (), + } + lookahead_token_offset += 1; + } + // End-of-file is reached before finding matching `)` - a clear error; + // doesn't matter if we parse it like legacy condition. + false + } + + // TODO: note how weird returned result here is /// Parses a control-flow condition. /// - /// If the next token is `(`, attempts to consume one parenthesized - /// subexpression and returns it wrapped as [`Expression::Parentheses`]. - /// Otherwise consumes a general expression. - fn parse_condition(&mut self) -> ExpressionRef<'src, 'arena> { - if let Some((Token::LeftParenthesis, left_parenthesis_position)) = - self.peek_token_and_position() + /// Conditions are parsed as ordinary expressions by default. + /// + /// For legacy compatibility, if the condition starts with a parenthesized + /// expression followed by an identifier-like token, the parenthesized + /// expression is treated as the complete condition and returned as + /// [`Expression::Parentheses`]. The following identifier-like token is left + /// for the branch body. + /// + /// This preserves old forms like: + /// + /// ```unrealscript + /// if (Condition) Cross -= 3; + /// ``` + /// + /// while still allowing common operator continuations like: + /// + /// ```unrealscript + /// if (2 + 2) * 2 < 7 { ... } + /// ``` + fn parse_condition( + &mut self, + error_kind: ParseErrorKind, + ) -> ParseExpressionResult<'src, 'arena> { + if self.next_token_definitely_cannot_start_expression() { + let keyword_position = self.last_consumed_position_or_start(); + let error_position = self.peek_position_or_eof(); + return Err(self + .make_error_at(error_kind, error_position) + .blame_token(error_position) + .related_token(diagnostic_labels::EXPRESSION_REQUIRED_BY, keyword_position)); + } + if self.should_apply_legacy_parenthesized_condition_cutoff() + && let Some(left_parenthesis_position) = self.eat_with_position(Token::LeftParenthesis) { - self.advance(); // '(' - let condition_expression = self.parse_expression(); - let right_parenthesis_position = self - .expect( - Token::RightParenthesis, - ParseErrorKind::ParenthesizedExpressionMissingClosingParenthesis, - ) - .widen_error_span_from(left_parenthesis_position) - .sync_error_at(self, SyncLevel::CloseParenthesis) - .unwrap_or_fallback(self); - self.arena.alloc_node_between( - Expression::Parentheses(condition_expression), - left_parenthesis_position, - right_parenthesis_position, - ) + Ok(self.parse_parenthesized_expression_tail(left_parenthesis_position)) } else { - self.parse_expression() + Ok(self.parse_expression()) } } @@ -103,9 +199,22 @@ impl<'src, 'arena> Parser<'src, 'arena> { /// /// For non-block bodies, this method consumes a trailing `;` when present /// and records its position in the returned [`BranchBody`]. - fn parse_branch_body(&mut self) -> BranchBody<'src, 'arena> { + fn parse_branch_body( + &mut self, + control_keyword_position: TokenPosition, + ) -> BranchBody<'src, 'arena> { let Some((first_token, first_token_position)) = self.peek_token_and_position() else { - let error = self.make_error_here(ParseErrorKind::MissingBranchBody); + let error = self + .make_error_at_last_consumed(ParseErrorKind::ControlFlowBodyExpected) + .blame_token(self.file.eof()) + .related_token( + diagnostic_labels::EXPRESSION_REQUIRED_BY, + control_keyword_position, + ) + .related_token( + diagnostic_labels::EXPRESSION_EXPECTED_AFTER, + self.last_consumed_position_or_start(), + ); let end_anchor_token_position = error.covered_span.end; self.report_error(error); return BranchBody { @@ -128,16 +237,16 @@ impl<'src, 'arena> Parser<'src, 'arena> { return BranchBody { expression: None, semicolon_position: None, - // `unwrap` actually triggering is effectively impossible, - // because by the time a branch body is parsed, some prior token - // (e.g. `if`, `)`, etc.) has already been consumed, - // so the parser should have a last-consumed position - end_anchor_token_position: self - .last_consumed_position() - .unwrap_or(first_token_position), + end_anchor_token_position: self.last_consumed_position_or_start(), }; } - let branch_expression = self.parse_expression(); + let branch_expression = self + .parse_expression_with_start_error( + ParseErrorKind::ControlFlowBodyExpected, + control_keyword_position, + self.last_consumed_position_or_start(), + ) + .unwrap_or_fallback(self); let end_anchor_token_position = branch_expression.span().end; // A block body in `if {...}` or `if {...};` owns its own terminator; // a following `;` does not belong to the branch body. @@ -162,6 +271,26 @@ impl<'src, 'arena> Parser<'src, 'arena> { } } + fn parse_condition_and_branch_body( + &mut self, + condition_context: TokenPosition, + error_kind: ParseErrorKind, + ) -> ParseResult<'src, 'arena, (ExpressionRef<'src, 'arena>, BranchBody<'src, 'arena>)> { + let first_position = self.peek_position_or_eof(); + let condition = self.parse_condition(error_kind)?; + if let Expression::Block(..) = *condition + && self.next_token_definitely_cannot_start_expression() + { + return Err(self + .make_error_at(error_kind, first_position) + .blame_token(first_position) + .related_token(diagnostic_labels::EXPRESSION_REQUIRED_BY, condition_context) + .related("branch_body", *condition.span())); + } + let body = self.parse_branch_body(condition_context); + Ok((condition, body)) + } + /// Parses an `if` expression after the `if` keyword. /// /// The resulting [`Expression::If`] spans from `if_keyword_position` to the @@ -172,12 +301,16 @@ impl<'src, 'arena> Parser<'src, 'arena> { &mut self, if_keyword_position: TokenPosition, ) -> ExpressionRef<'src, 'arena> { - let condition = self.parse_condition(); - let body = self.parse_branch_body(); + let (condition, body) = match self + .parse_condition_and_branch_body(if_keyword_position, ParseErrorKind::ConditionExpected) + { + Ok(good_result) => good_result, + Err(error) => return error.fallback(self), + }; let (else_body, if_end_position) = if self.peek_keyword() == Some(Keyword::Else) { self.advance(); // 'else' - let else_body = self.parse_branch_body(); + let else_body = self.parse_branch_body(self.last_consumed_position_or_start()); let else_body_end = else_body.end_anchor_token_position; (Some(else_body), else_body_end) } else { @@ -204,8 +337,13 @@ impl<'src, 'arena> Parser<'src, 'arena> { &mut self, while_keyword_position: TokenPosition, ) -> ExpressionRef<'src, 'arena> { - let condition = self.parse_condition(); - let body = self.parse_branch_body(); + let (condition, body) = match self.parse_condition_and_branch_body( + while_keyword_position, + ParseErrorKind::ConditionExpected, + ) { + Ok(good_result) => good_result, + Err(error) => return error.fallback(self), + }; let span = TokenSpan::range(while_keyword_position, body.end_anchor_token_position); self.arena .alloc_node(Expression::While { condition, body }, span) @@ -220,11 +358,12 @@ impl<'src, 'arena> Parser<'src, 'arena> { &mut self, do_keyword_position: TokenPosition, ) -> ExpressionRef<'src, 'arena> { - let body = self.parse_branch_body(); + let body = self.parse_branch_body(do_keyword_position); let condition = if self .expect_keyword(Keyword::Until, ParseErrorKind::DoMissingUntil) .widen_error_span_from(do_keyword_position) + .related_token("do_keyword", do_keyword_position) .report_error(self) { crate::arena::ArenaNode::new_in( @@ -233,7 +372,9 @@ impl<'src, 'arena> Parser<'src, 'arena> { self.arena, ) } else { - self.parse_condition() + self.parse_condition(ParseErrorKind::ConditionExpected) + .related_token("do_keyword", do_keyword_position) + .unwrap_or_fallback(self) }; let span = TokenSpan::range(do_keyword_position, condition.span().end); self.arena @@ -252,12 +393,30 @@ impl<'src, 'arena> Parser<'src, 'arena> { &mut self, foreach_keyword_position: TokenPosition, ) -> ExpressionRef<'src, 'arena> { - // UnrealScript `foreach` iterator expressions are simple enough that - // they do not need the special parenthesized-condition handling used by - // `parse_condition()`. - let iterated_expression = self.parse_expression(); - - let body = self.parse_branch_body(); + if self + .peek_token() + .map(|error| !error.is_valid_identifier_name()) + .unwrap_or_default() + { + let error_position = self.peek_position_or_eof(); + return self + .make_error_at( + ParseErrorKind::ForEachIteratorExpressionExpected, + error_position, + ) + .blame_token(error_position) + .related_token( + diagnostic_labels::EXPRESSION_REQUIRED_BY, + foreach_keyword_position, + ) + .fallback(self); + } + let iterated_expression = + match self.parse_condition(ParseErrorKind::ForEachIteratorExpressionExpected) { + Ok(good_result) => good_result, + Err(error) => return error.fallback(self), + }; + let body = self.parse_branch_body(foreach_keyword_position); let span = TokenSpan::range(foreach_keyword_position, body.end_anchor_token_position); self.arena.alloc_node( Expression::ForEach { @@ -275,10 +434,12 @@ impl<'src, 'arena> Parser<'src, 'arena> { /// top-level `;` appears before the matching `)` is closed or input ends. /// /// This is used only for loop-vs-identifier disambiguation. - pub(crate) fn is_for_loop_header_ahead(&mut self) -> bool { - if self.peek_token() != Some(Token::LeftParenthesis) { - return false; - } + pub(super) fn is_for_loop_header_ahead(&mut self) -> Option { + let Some((Token::LeftParenthesis, left_parenthesis_position)) = + self.peek_token_and_position() + else { + return None; + }; let mut nesting_depth: usize = 1; let mut lookahead_token_offset: usize = 1; while let Some(next_token) = self.peek_token_at(lookahead_token_offset) { @@ -288,16 +449,20 @@ impl<'src, 'arena> Parser<'src, 'arena> { if nesting_depth <= 1 { // End of the immediate `for (...)` group without a // top-level `;`: not a loop header. - return false; + return None; } nesting_depth -= 1; } - Token::Semicolon if nesting_depth == 1 => return true, + Token::Semicolon if nesting_depth == 1 => return Some(left_parenthesis_position), _ => (), } lookahead_token_offset += 1; } - false + // EOF before closing the immediate `for (...)` group. Treat this as an + // incomplete `for` loop header, not as a function call, so recovery can + // produce `P0017` / `P0021`-style diagnostics. + Some(left_parenthesis_position) + //None } /// Parses a `for` expression after the `for` keyword. @@ -311,71 +476,173 @@ impl<'src, 'arena> Parser<'src, 'arena> { pub(crate) fn parse_for_tail( &mut self, for_keyword_position: TokenPosition, + left_parenthesis_position: TokenPosition, ) -> ExpressionRef<'src, 'arena> { - // This path is expected to be entered only after - // `is_for_loop_header_ahead()`, so the opening `(` and at least one - // top-level `;` should already be structurally guaranteed. - self.expect( - Token::LeftParenthesis, - ParseErrorKind::ForMissingOpeningParenthesis, - ) - .widen_error_span_from(for_keyword_position) - .report_error(self); - - let initialization = if self.peek_token() == Some(Token::Semicolon) { - self.advance(); - None - } else { - let init = self.parse_expression(); - self.expect( - Token::Semicolon, - ParseErrorKind::ForMissingInitializationSemicolon, - ) - .report_error(self); - Some(init) - }; - - let condition = if self.peek_token() == Some(Token::Semicolon) { - self.advance(); - None - } else { - let condition = self.parse_expression(); - self.expect( - Token::Semicolon, - ParseErrorKind::ForMissingConditionSemicolon, - ) - .report_error(self); - Some(condition) - }; - - let step = if self.peek_token() == Some(Token::RightParenthesis) { - self.advance(); - None - } else { - let step = self.parse_expression(); - self.expect( - Token::RightParenthesis, - ParseErrorKind::ForMissingClosingParenthesis, - ) - .widen_error_span_from(for_keyword_position) - .sync_error_at(self, SyncLevel::CloseParenthesis) - .report_error(self); - Some(step) - }; - - let body = self.parse_branch_body(); + let header = self.parse_for_header(for_keyword_position, left_parenthesis_position); + if header.right_parenthesis_position.is_none() { + return self.arena.alloc_node( + Expression::Error, + TokenSpan::range(for_keyword_position, self.last_consumed_position_or_start()), + ); + } + let body = self.parse_branch_body(for_keyword_position); let span = TokenSpan::range(for_keyword_position, body.end_anchor_token_position); self.arena.alloc_node( Expression::For { - initialization, - condition, - step, + initialization: header.initialization, + condition: header.condition, + step: header.step, body, }, span, ) } + fn parse_for_optional_expression( + &mut self, + bad_start_error_kind: crate::parser::ParseErrorKind, + stop_token: Token, + for_keyword_position: crate::lexer::TokenPosition, + left_parenthesis_position: crate::lexer::TokenPosition, + ) -> OptionalExpression<'src, 'arena> { + if let Some(next_token) = self.peek_token() + && next_token != stop_token + { + Some( + self.parse_expression_with_start_error( + bad_start_error_kind, + for_keyword_position, + left_parenthesis_position, + ) + .sync_error_until(self, SyncLevel::CloseParenthesis) + .unwrap_or_fallback(self), + ) + } else { + None + } + } + + fn parse_for_header( + &mut self, + for_keyword_position: TokenPosition, + left_parenthesis_position: TokenPosition, + ) -> ParsedForHeader<'src, 'arena> { + let mut header = ParsedForHeader { + initialization: None, + condition: None, + step: None, + right_parenthesis_position: None, + }; + header.initialization = self.parse_for_optional_expression( + ParseErrorKind::ForLoopHeaderInitializerInvalidStart, + Token::Semicolon, + for_keyword_position, + left_parenthesis_position, + ); + let error_token = match self.peek_token_and_position() { + Some((Token::Semicolon, _)) => { + self.advance(); + None + } + Some((Token::RightParenthesis, right_parenthesis_position)) => { + header.right_parenthesis_position = Some(right_parenthesis_position); + self.advance(); + Some(right_parenthesis_position) + } + Some((_, next_token_position)) => Some(next_token_position), + None => Some(self.peek_position_or_eof()), + }; + if let Some(error_token) = error_token { + if let Some(ref a) = header.initialization { + if matches!(**a, Expression::Error) { + return header; + } + } + let mut error = self + .make_error_at( + ParseErrorKind::ForLoopHeaderMissingSemicolonAfterInitializer, + error_token, + ) + .widen_error_span_from(for_keyword_position) + .blame_token(error_token); + if let Some(ref a) = header.initialization { + error = error.related("for_header_initializer", *a.span()) + }; + error.report_error(self); + return header; + } + let first_semicolon_position = self.last_consumed_position_or_start(); + header.condition = self.parse_for_optional_expression( + ParseErrorKind::ForLoopHeaderConditionInvalidStart, + Token::Semicolon, + for_keyword_position, + first_semicolon_position, + ); + let error_token = match self.peek_token_and_position() { + Some((Token::Semicolon, _)) => { + self.advance(); + None + } + Some((Token::RightParenthesis, right_parenthesis_position)) => { + header.right_parenthesis_position = Some(right_parenthesis_position); + self.advance(); + Some(right_parenthesis_position) + } + Some((_, next_token_position)) => Some(next_token_position), + None => Some(self.peek_position_or_eof()), + }; + if let Some(error_token) = error_token { + if let Some(ref a) = header.condition { + if matches!(**a, Expression::Error) { + return header; + } + } + let mut error = self + .make_error_at( + ParseErrorKind::ForLoopHeaderMissingSemicolonAfterCondition, + error_token, + ) + .widen_error_span_from(for_keyword_position) + .blame_token(error_token); + if let Some(ref a) = header.condition { + error = error.related("for_header_condition", *a.span()) + }; + error.report_error(self); + return header; + } + let second_semicolon_position = self.last_consumed_position_or_start(); + header.step = self.parse_for_optional_expression( + ParseErrorKind::ForLoopHeaderStepInvalidStart, + Token::RightParenthesis, + for_keyword_position, + second_semicolon_position, + ); + // ////////////////////////////////// + if let Some(ref a) = header.step + && matches!(**a, Expression::Error) + { + if let Some((Token::RightParenthesis, right_parenthesis_position)) = + self.peek_token_and_position() + { + header.right_parenthesis_position = Some(right_parenthesis_position); + self.advance(); + } + return header; + } + + header.right_parenthesis_position = self + .expect( + Token::RightParenthesis, + ParseErrorKind::ForLoopHeaderMissingClosingParenthesis, + ) + .widen_error_span_from(for_keyword_position) + .related_token("for_header_start", left_parenthesis_position) + .sync_error_at(self, SyncLevel::CloseParenthesis) + .ok_or_report(self); + // ////////////////////////////////// + header + } + /// Parses the continuation of a `return` expression after its keyword. /// /// If the next token is not `;`, consumes a return value expression. @@ -432,7 +699,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { label_position, ); } - self.make_error_here(ParseErrorKind::GotoMissingLabel) + self.make_error_at_last_consumed(ParseErrorKind::GotoMissingLabel) .widen_error_span_from(goto_keyword_position) .sync_error_until(self, SyncLevel::Statement) .report_error(self); diff --git a/rottlib/src/parser/grammar/expression/identifier.rs b/rottlib/src/parser/grammar/expression/identifier.rs index 9e80438..3e88f73 100644 --- a/rottlib/src/parser/grammar/expression/identifier.rs +++ b/rottlib/src/parser/grammar/expression/identifier.rs @@ -20,7 +20,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { let (token, token_position) = self.require_token_and_position(invalid_identifier_error_kind)?; let identifier = Parser::identifier_token_from_token(token, token_position) - .ok_or_else(|| self.make_error_here(invalid_identifier_error_kind))?; + .ok_or_else(|| self.make_error_at_last_consumed(invalid_identifier_error_kind))?; self.advance(); Ok(identifier) } @@ -56,11 +56,15 @@ impl<'src, 'arena> Parser<'src, 'arena> { let span_start = head.0; let mut span_end = span_start; - while self.peek_token() == Some(Token::Period) { + while let Some((Token::Period, dot_position)) = self.peek_token_and_position() { self.advance(); // '.' - let next_segment = self + let next_segment = match self .parse_identifier(invalid_identifier_error_kind) - .widen_error_span_from(head.0)?; + .widen_error_span_from(head.0) + { + Ok(next_segment) => next_segment, + Err(error) => return Err(error.related_token("qualifier_dot", dot_position)), + }; span_end = next_segment.0; let tail_vec = tail.get_or_insert_with(|| ArenaVec::new_in(self.arena)); diff --git a/rottlib/src/parser/grammar/expression/literals.rs b/rottlib/src/parser/grammar/expression/literals.rs index fa99c9e..0626b61 100644 --- a/rottlib/src/parser/grammar/expression/literals.rs +++ b/rottlib/src/parser/grammar/expression/literals.rs @@ -33,7 +33,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { }; let digits_without_underscores = content.replace('_', ""); u128::from_str_radix(&digits_without_underscores, base) - .map_err(|_| self.make_error_here(ParseErrorKind::InvalidNumericLiteral)) + .map_err(|_| self.make_error_at_last_consumed(ParseErrorKind::InvalidNumericLiteral)) } /// Decodes a float literal as `f64`, following the permissive and only @@ -79,7 +79,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { .unwrap_or(content); content .parse::() - .map_err(|_| self.make_error_here(ParseErrorKind::InvalidNumericLiteral)) + .map_err(|_| self.make_error_at_last_consumed(ParseErrorKind::InvalidNumericLiteral)) } /// Unescapes a tokenized string literal into an arena string. diff --git a/rottlib/src/parser/grammar/expression/pratt.rs b/rottlib/src/parser/grammar/expression/pratt.rs index 4d99603..9c590c0 100644 --- a/rottlib/src/parser/grammar/expression/pratt.rs +++ b/rottlib/src/parser/grammar/expression/pratt.rs @@ -33,7 +33,7 @@ //! - [`super::precedence`] - operator precedence definitions use crate::ast::{self, Expression, ExpressionRef}; -use crate::parser::{self, Parser, ResultRecoveryExt}; +use crate::parser::{self, ParseExpressionResult, Parser, ResultRecoveryExt, diagnostic_labels}; pub use super::precedence::PrecedenceRank; @@ -63,9 +63,51 @@ impl<'src, 'arena> Parser<'src, 'arena> { #[must_use] pub fn parse_expression(&mut self) -> ExpressionRef<'src, 'arena> { self.parse_expression_with_min_precedence_rank(PrecedenceRank::LOOSEST) + .sync_error_until(self, parser::SyncLevel::ExpressionStart) .unwrap_or_fallback(self) } + /// Parses an expression in a grammar position where an expression is + /// required. + /// + /// This is the checked variant of [`Parser::parse_expression`]. If the next + /// token is known not to be a valid expression starter, this reports + /// `bad_start_error_kind`, consumes the bad token, and starts panic-mode + /// recovery until [`crate::parser::SyncLevel::ExpressionStart`]. + /// + /// `required_by_position` identifies the token or construct that created + /// the requirement for an expression. It is attached to the diagnostic with + /// the [`diagnostic_labels::EXPRESSION_REQUIRED_BY`] label. + /// + /// `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( + &mut self, + bad_start_error_kind: crate::parser::ParseErrorKind, + required_by_position: crate::lexer::TokenPosition, + expression_context_position: crate::lexer::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) + .sync_error_until(self, crate::parser::SyncLevel::ExpressionStart) + .blame_token(error_position) + .related_token( + diagnostic_labels::EXPRESSION_REQUIRED_BY, + required_by_position, + ) + .related_token( + diagnostic_labels::EXPRESSION_EXPECTED_AFTER, + expression_context_position, + )); + } + self.parse_expression_with_min_precedence_rank(PrecedenceRank::LOOSEST) + } + /// Parses an expression, including only operators with binding power /// at least `min_precedence_rank` (as tight or tighter). fn parse_expression_with_min_precedence_rank( diff --git a/rottlib/src/parser/grammar/expression/primary.rs b/rottlib/src/parser/grammar/expression/primary/mod.rs similarity index 54% rename from rottlib/src/parser/grammar/expression/primary.rs rename to rottlib/src/parser/grammar/expression/primary/mod.rs index 0d1df7f..93bb5ff 100644 --- a/rottlib/src/parser/grammar/expression/primary.rs +++ b/rottlib/src/parser/grammar/expression/primary/mod.rs @@ -1,4 +1,4 @@ -//! Parser for primary expressions in Fermented `UnrealScript`. +//! Parser for primary expressions in Fermented UnrealScript. //! //! This module implements parsing of primary expressions via //! [`Parser::parse_primary_from_current_token`] and its helper @@ -26,11 +26,12 @@ //! It means "an expression form that does not need a left-hand side //! in order to be parsed". -use super::selectors::ParsedCallArgumentSlot; use crate::ast::{Expression, ExpressionRef, OptionalExpression}; -use crate::lexer::{Keyword, Token, TokenPosition}; +use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan}; use crate::parser::{ParseErrorKind, ParseExpressionResult, Parser, ResultRecoveryExt, SyncLevel}; +mod new; + impl<'src, 'arena> Parser<'src, 'arena> { /// Parses a primary expression starting from the provided token. /// @@ -47,7 +48,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { /// /// Returns [`ParseErrorKind::ExpressionExpected`] if the provided /// token cannot begin any valid primary expression in this position. - pub(crate) fn parse_primary_from_current_token( + pub(super) fn parse_primary_from_current_token( &mut self, token: Token, token_lexeme: &'src str, @@ -78,10 +79,12 @@ impl<'src, 'arena> Parser<'src, 'arena> { ), Token::LeftParenthesis => self.parse_parenthesized_expression_tail(token_position), Token::LeftBrace => self.parse_block_tail(token_position), - Token::Keyword(keyword) => match self.parse_keyword_primary(keyword, token_position) { - Some(keyword_expression) => keyword_expression, - None => return self.parse_identifier_like_primary(token, token_position), - }, + Token::Keyword(keyword) => { + match self.try_parse_keyword_primary(keyword, token_position) { + Some(keyword_expression) => keyword_expression, + None => return self.parse_identifier_like_primary(token, token_position), + } + } _ => return self.parse_identifier_like_primary(token, token_position), }) } @@ -90,7 +93,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { /// /// Returns `None` if the keyword should instead be interpreted as an /// identifier in this position. - fn parse_keyword_primary( + fn try_parse_keyword_primary( &mut self, keyword: Keyword, token_position: TokenPosition, @@ -115,7 +118,12 @@ impl<'src, 'arena> Parser<'src, 'arena> { Keyword::New => self.parse_new_expression_tail(token_position), // These keywords remain valid identifiers unless the following // tokens commit to the keyword-led form. - Keyword::For if self.is_for_loop_header_ahead() => self.parse_for_tail(token_position), + Keyword::For + if let Some(left_parenthesis_position) = self.is_for_loop_header_ahead() => + { + self.advance(); // `(` + self.parse_for_tail(token_position, left_parenthesis_position) + } Keyword::Goto if !matches!(self.peek_token(), Some(Token::LeftParenthesis)) => { self.parse_goto_tail(token_position) } @@ -125,10 +133,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { self.parse_switch_tail(token_position) } Keyword::Class => { - if let Some((Token::Less, left_angle_bracket_position)) = - self.peek_token_and_position() - { - self.advance(); // '<' + if let Some(left_angle_bracket_position) = self.eat_with_position(Token::Less) { self.parse_class_type_tail(token_position, left_angle_bracket_position) } else { return None; @@ -151,10 +156,9 @@ impl<'src, 'arena> Parser<'src, 'arena> { primary_token_position: TokenPosition, ) -> ParseExpressionResult<'src, 'arena> { let identifier_token = - Parser::identifier_token_from_token(primary_token, primary_token_position).ok_or_else( + Self::identifier_token_from_token(primary_token, primary_token_position).ok_or_else( || self.make_error_at(ParseErrorKind::ExpressionExpected, primary_token_position), )?; - // A token that is valid as an identifier may still start a tagged-name // literal such as `Texture'Foo.Bar'`. let expression = if let Some((Token::NameLiteral, lexeme, name_position)) = @@ -182,28 +186,20 @@ impl<'src, 'arena> Parser<'src, 'arena> { /// /// Assumes the opening `(` has already been consumed. /// Reports and recovers from a missing closing `)`. - fn parse_parenthesized_expression_tail( + pub(super) fn parse_parenthesized_expression_tail( &mut self, left_parenthesis_position: TokenPosition, ) -> ExpressionRef<'src, 'arena> { - // Continue parsing normally - let inner_expression = if self.next_token_definitely_cannot_start_expression() { - let error = self - .make_error_here(ParseErrorKind::ParenthesizedExpressionInvalidStart) + if self.next_token_definitely_cannot_start_expression() { + return self + .make_error_at_last_consumed(ParseErrorKind::ParenthesizedExpressionInvalidStart) .widen_error_span_from(left_parenthesis_position) - .sync_error_until(self, SyncLevel::Expression) .extend_blame_to_next_token(self) - .related_token("left_parenthesis", left_parenthesis_position); - let error_span = error.covered_span; - self.report_error(error); - return crate::arena::ArenaNode::new_in( - crate::ast::Expression::Error, - error_span, - self.arena, - ); - } else { - self.parse_expression() + .related_token("left_parenthesis", left_parenthesis_position) + .sync_error_until(self, SyncLevel::CloseParenthesis) + .fallback(self); }; + let inner_expression = self.parse_expression(); let right_parenthesis_position = self .expect( Token::RightParenthesis, @@ -230,50 +226,55 @@ impl<'src, 'arena> Parser<'src, 'arena> { class_keyword_position: TokenPosition, left_angle_bracket_position: TokenPosition, ) -> ExpressionRef<'src, 'arena> { - // Special case for an empty argument - if let Some((Token::Greater, right_angle_bracket_position)) = self.peek_token_and_position() - { - self.make_error_here(ParseErrorKind::ClassTypeMissingTypeArgument { - left_angle_bracket_position, - }) - .widen_error_span_from(left_angle_bracket_position) - .sync_error_at(self, SyncLevel::CloseAngleBracket) - .blame_token(right_angle_bracket_position) - .report_error(self); - return self.arena.alloc_node_between( - Expression::Error, + match self.peek_token_and_position() { + Some((Token::Greater, right_angle_bracket_position)) => self + .report_missing_class_type_argument( + class_keyword_position, + left_angle_bracket_position, + right_angle_bracket_position, + ), + Some((first_token, _)) if first_token.is_valid_identifier_name() => self + .parse_nonempty_class_type_tail( + class_keyword_position, + left_angle_bracket_position, + ), + Some((_, bad_position)) => self.report_invalid_class_type_start( class_keyword_position, - right_angle_bracket_position, - ); - } - // Qualified identifiers do not have a meaningful fallback option - let class_type = match self - .parse_qualified_identifier(ParseErrorKind::ClassTypeInvalidTypeArgument { left_angle_bracket_position, - }) + bad_position, + ), + None => self.report_invalid_class_type_start( + class_keyword_position, + left_angle_bracket_position, + self.file.eof(), + ), + } + } + + fn parse_nonempty_class_type_tail( + &mut self, + class_keyword_position: TokenPosition, + left_angle_bracket_position: TokenPosition, + ) -> ExpressionRef<'src, 'arena> { + let class_type = match self + .parse_qualified_identifier(ParseErrorKind::ClassTypeExpectedQualifiedTypeName) .widen_error_span_from(class_keyword_position) + .extend_blame_to_next_token(self) .sync_error_at(self, SyncLevel::CloseAngleBracket) + .related_token("class_keyword", class_keyword_position) { Ok(class_type) => class_type, - Err(error) => { - self.report_error(error); - return self.arena.alloc_node_between( - Expression::Error, - class_keyword_position, - self.last_consumed_position() - .unwrap_or(class_keyword_position), - ); - } + Err(error) => return self.report_error_with_fallback(error), }; let right_angle_bracket_position = self .expect( Token::Greater, - ParseErrorKind::ClassTypeMissingClosingAngleBracket { - left_angle_bracket_position, - }, + ParseErrorKind::ClassTypeMissingClosingAngleBracket, ) .widen_error_span_from(class_keyword_position) .sync_error_at(self, SyncLevel::CloseAngleBracket) + .related_token("left_angle_bracket", left_angle_bracket_position) + .related_token("class_keyword", class_keyword_position) .unwrap_or_fallback(self); self.arena.alloc_node_between( Expression::ClassType(class_type), @@ -282,101 +283,37 @@ impl<'src, 'arena> Parser<'src, 'arena> { ) } - /// Parses a `new` expression with an optional parenthesized argument list. - /// - /// Assumes the `new` keyword has already been consumed. - /// The parenthesized argument list is optional. - fn parse_new_expression_tail( + fn report_missing_class_type_argument( &mut self, - new_keyword_position: TokenPosition, + class_keyword_position: TokenPosition, + left_angle_bracket_position: TokenPosition, + right_angle_bracket_position: TokenPosition, ) -> ExpressionRef<'src, 'arena> { - let (outer_argument, name_argument, flags_argument) = - if let Some((Token::LeftParenthesis, left_parenthesis_position)) = - self.peek_token_and_position() - { - self.advance(); - self.parse_new_argument_list_tail(left_parenthesis_position) - } else { - (None, None, None) - }; - // The class specifier is often a literal class reference, but any - // expression is accepted here. - let class_specifier = if self.next_token_definitely_cannot_start_expression() { - let error = self - .make_error_here(ParseErrorKind::NewMissingClassSpecifier { - new_keyword_position, - }) - .widen_error_span_from(new_keyword_position) - .sync_error_at(self, SyncLevel::Expression); - let error_span = error.covered_span; - self.report_error(error); - crate::arena::ArenaNode::new_in(crate::ast::Expression::Error, error_span, self.arena) - } else { - self.parse_expression() - }; - let class_specifier_end_position = class_specifier.span().end; - self.arena.alloc_node_between( - Expression::New { - outer_argument, - name_argument, - flags_argument, - class_specifier, - }, - new_keyword_position, - class_specifier_end_position, - ) + self.advance(); + self.make_error_at_last_consumed(ParseErrorKind::ClassTypeMissingTypeArgument) + .widen_error_span_from(class_keyword_position) + .blame(TokenSpan::range( + left_angle_bracket_position, + right_angle_bracket_position, + )) + .related_token("left_angle_bracket", left_angle_bracket_position) + .related_token("class_keyword", class_keyword_position) + .fallback(self) } - /// Parses the optional parenthesized arguments of a `new` expression. - /// - /// Assumes the opening `(` has already been consumed. - /// Returns the `outer`, `name`, and `flags` argument slots, each of which - /// may be omitted. Reports and recovers from a missing closing `)`. - fn parse_new_argument_list_tail( + fn report_invalid_class_type_start( &mut self, - left_parenthesis_position: TokenPosition, - ) -> ( - OptionalExpression<'src, 'arena>, - OptionalExpression<'src, 'arena>, - OptionalExpression<'src, 'arena>, - ) { - let mut outer_argument = None; - let mut name_argument = None; - let mut flags_argument = None; - - let mut first_call = true; - for slot in [&mut outer_argument, &mut name_argument, &mut flags_argument] { - match self.parse_call_argument_slot(left_parenthesis_position, first_call) { - ParsedCallArgumentSlot::Argument(argument) => *slot = argument, - ParsedCallArgumentSlot::NoMoreArguments => break, - } - first_call = false; - } - - if let Some((next_token, next_token_position)) = self.peek_token_and_position() - && next_token != Token::RightParenthesis - { - self.make_error_here(ParseErrorKind::NewTooManyArguments { - left_parenthesis_position, - }) - .widen_error_span_from(left_parenthesis_position) - .sync_error_until(self, SyncLevel::CloseParenthesis) - .blame_token(next_token_position) - .extend_blame_end_to_covered_end() - .report_error(self); - } - - self.expect( - Token::RightParenthesis, - ParseErrorKind::NewMissingClosingParenthesis { - left_parenthesis_position, - }, - ) - .widen_error_span_from(left_parenthesis_position) - .sync_error_at(self, SyncLevel::CloseParenthesis) - .report_error(self); - - (outer_argument, name_argument, flags_argument) + class_keyword_position: TokenPosition, + left_angle_bracket_position: TokenPosition, + bad_position: TokenPosition, + ) -> ExpressionRef<'src, 'arena> { + self.make_error_at_last_consumed(ParseErrorKind::ClassTypeInvalidStart) + .widen_error_span_from(class_keyword_position) + .sync_error_at(self, SyncLevel::CloseAngleBracket) + .blame_token(bad_position) + .related_token("left_angle_bracket", left_angle_bracket_position) + .related_token("class_keyword", class_keyword_position) + .fallback(self) } /// Returns `true` iff the next token is definitely not a valid start of an @@ -387,63 +324,8 @@ impl<'src, 'arena> Parser<'src, 'arena> { /// - `false` means "might be valid", so the normal expression parser should /// decide and potentially emit a more specific error. #[must_use] - pub(crate) fn next_token_definitely_cannot_start_expression(&mut self) -> bool { - matches!( - self.peek_token(), - None - // Closing delimiters / separators - | Some(Token::RightParenthesis) - | Some(Token::RightBrace) - | Some(Token::RightBracket) - | Some(Token::Semicolon) - | Some(Token::Comma) - | Some(Token::Colon) - | Some(Token::Question) - - // Tokens that only continue a previous expression - | Some(Token::Period) - - // Infix / postfix / assignment operators - | Some(Token::Exponentiation) - | Some(Token::Multiply) - | Some(Token::Divide) - | Some(Token::Modulo) - | Some(Token::ConcatSpace) - | Some(Token::Concat) - | Some(Token::LeftShift) - | Some(Token::LogicalRightShift) - | Some(Token::RightShift) - | Some(Token::Less) - | Some(Token::LessEqual) - | Some(Token::Greater) - | Some(Token::GreaterEqual) - | Some(Token::Equal) - | Some(Token::NotEqual) - | Some(Token::ApproximatelyEqual) - | Some(Token::BitwiseAnd) - | Some(Token::BitwiseOr) - | Some(Token::BitwiseXor) - | Some(Token::LogicalAnd) - | Some(Token::LogicalXor) - | Some(Token::LogicalOr) - | Some(Token::Assign) - | Some(Token::MultiplyAssign) - | Some(Token::DivideAssign) - | Some(Token::ModuloAssign) - | Some(Token::PlusAssign) - | Some(Token::MinusAssign) - | Some(Token::ConcatAssign) - | Some(Token::ConcatSpaceAssign) - - // Non-expression trivia / technical tokens - | Some(Token::ExecDirective) - | Some(Token::CppBlock) - | Some(Token::Hash) - | Some(Token::LineComment) - | Some(Token::BlockComment) - | Some(Token::Newline) - | Some(Token::Whitespace) - | Some(Token::Error) - ) + pub(super) fn next_token_definitely_cannot_start_expression(&mut self) -> bool { + self.peek_token() + .map_or(true, Token::is_definitely_not_expression_start) } } diff --git a/rottlib/src/parser/grammar/expression/primary/new.rs b/rottlib/src/parser/grammar/expression/primary/new.rs new file mode 100644 index 0000000..f2ef6ec --- /dev/null +++ b/rottlib/src/parser/grammar/expression/primary/new.rs @@ -0,0 +1,342 @@ +//! Parser for `new` expressions in Fermented UnrealScript. + +use super::super::selectors::{CallArgumentListParseState, ParsedCallArgumentSlot}; +use crate::ast::{Expression, ExpressionRef, OptionalExpression}; +use crate::lexer::{Token, TokenPosition, TokenSpan}; +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. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +enum NewClassSpecifierParseAction { + Parse, + Skip, +} + +/// Stores the parsed `new(...)` arguments and the action to take for +/// class-specifier parsing. +#[derive(Debug)] +struct NewArgumentListParseResult<'src, 'arena> { + outer_argument: OptionalExpression<'src, 'arena>, + name_argument: OptionalExpression<'src, 'arena>, + flags_argument: OptionalExpression<'src, 'arena>, + class_specifier_parse_action: NewClassSpecifierParseAction, +} + +impl<'src, 'arena> NewArgumentListParseResult<'src, 'arena> { + /// Returns the parse result for `new` without an argument list. + fn without_argument_list() -> Self { + Self { + outer_argument: None, + name_argument: None, + flags_argument: None, + class_specifier_parse_action: NewClassSpecifierParseAction::Parse, + } + } +} + +/// Holds shared state for parsing the optional `new(...)` argument list. +#[derive(Debug)] +struct NewArgumentListParseState<'src, 'arena> { + call_argument_list_parse_state: CallArgumentListParseState, + + new_keyword_position: TokenPosition, + left_parenthesis_position: TokenPosition, + + outer_argument: OptionalExpression<'src, 'arena>, + name_argument: OptionalExpression<'src, 'arena>, + flags_argument: OptionalExpression<'src, 'arena>, +} + +impl<'src, 'arena> NewArgumentListParseState<'src, 'arena> { + /// Stores an argument in the current `new` argument slot. + fn store_argument_in_current_slot(&mut self, argument: OptionalExpression<'src, 'arena>) { + match self.call_argument_list_parse_state.parsed_slot_count { + 1 => self.outer_argument = argument, + 2 => self.name_argument = argument, + 3 => self.flags_argument = argument, + _ => unreachable!("this method cannot be called after parsing three arguments"), + } + } + + /// Returns the span of the argument in the current parsed slot. + /// + /// Assumes the current slot has already been stored. + #[must_use] + fn current_argument_span(&self) -> Option { + debug_assert!( + (1..=3).contains(&self.call_argument_list_parse_state.parsed_slot_count), + "parsed_slot_count out of range in new-argument parser" + ); + match self.call_argument_list_parse_state.parsed_slot_count { + 1 => &self.outer_argument, + 2 => &self.name_argument, + 3 => &self.flags_argument, + // Diagnostics can fall back to a missing span here. + _ => return None, + } + .as_ref() + .map(|argument| *argument.span()) + } + + /// Returns the span of the last parsed non-empty `new` argument. + #[must_use] + fn last_parsed_allowed_argument_span(&self) -> Option { + [ + &self.flags_argument, + &self.name_argument, + &self.outer_argument, + ] + .into_iter() + .find_map(|slot| slot.as_ref().map(|argument| *argument.span())) + } + + /// Finishes argument-list parsing and returns the collected result. + #[must_use] + fn into_result( + self, + class_specifier_parse_action: NewClassSpecifierParseAction, + ) -> NewArgumentListParseResult<'src, 'arena> { + NewArgumentListParseResult { + outer_argument: self.outer_argument, + name_argument: self.name_argument, + flags_argument: self.flags_argument, + class_specifier_parse_action, + } + } +} + +impl<'src, 'arena> Parser<'src, 'arena> { + /// Parses a `new` expression. + /// + /// Assumes the `new` keyword has already been consumed. Parses an optional + /// parenthesized argument list before the required class specifier. + #[must_use] + pub(super) fn parse_new_expression_tail( + &mut self, + new_keyword_position: TokenPosition, + ) -> ExpressionRef<'src, 'arena> { + let mut argument_list_end_position = None; + let NewArgumentListParseResult { + outer_argument, + name_argument, + flags_argument, + class_specifier_parse_action, + } = if let Some(left_parenthesis_position) = self.eat_with_position(Token::LeftParenthesis) + { + let parsed_argument_list = + self.parse_new_argument_list_tail(new_keyword_position, left_parenthesis_position); + argument_list_end_position = self.last_consumed_position(); + parsed_argument_list + } else { + NewArgumentListParseResult::without_argument_list() + }; + + let class_specifier = self.parse_new_class_specifier( + new_keyword_position, + argument_list_end_position, + class_specifier_parse_action, + ); + let class_specifier_end_position = class_specifier.span().end; + self.arena.alloc_node_between( + Expression::New { + outer_argument, + name_argument, + flags_argument, + class_specifier, + }, + new_keyword_position, + class_specifier_end_position, + ) + } + + /// Parses the parenthesized argument list of a `new` expression. + /// + /// Assumes the opening `(` has already been consumed. + #[must_use] + fn parse_new_argument_list_tail( + &mut self, + new_keyword_position: TokenPosition, + left_parenthesis_position: TokenPosition, + ) -> NewArgumentListParseResult<'src, 'arena> { + let mut state = NewArgumentListParseState { + new_keyword_position, + left_parenthesis_position, + outer_argument: None, + name_argument: None, + flags_argument: None, + call_argument_list_parse_state: CallArgumentListParseState::new(), + }; + if let Some(class_specifier_parse_action) = self.parse_new_argument_slots(&mut state) { + return state.into_result(class_specifier_parse_action); + } + self.diagnose_extra_new_arguments(&mut state); + let class_specifier_parse_action = if self.eat(Token::RightParenthesis) { + NewClassSpecifierParseAction::Parse + } else { + self.recover_from_missing_new_closing_parenthesis(&state) + }; + state.into_result(class_specifier_parse_action) + } + + /// Parses up to three positional `new` arguments. + /// + /// Returns [`Some`] only when recovery determines whether + /// the class specifier should be parsed or skipped. + #[must_use] + fn parse_new_argument_slots( + &mut self, + state: &mut NewArgumentListParseState<'src, 'arena>, + ) -> Option { + // Only successful slot parses continue the loop, + // so each iteration makes progress. + while state.call_argument_list_parse_state.parsed_slot_count < 3 + && let ParsedCallArgumentSlot::Argument(argument) = + self.parse_call_argument_slot(&mut state.call_argument_list_parse_state) + { + // On `ParsedCallArgumentSlot::Argument(_)`, + // `parse_call_argument_slot` increases `parsed_slot_count` by 1, + // so it is now in `1`, `2` or `3`, guaranteeing that this call + // is valid and won't hit `unreachable!`. + state.store_argument_in_current_slot(argument); + + if state + .call_argument_list_parse_state + .last_slot_missing_boundary + { + if let Some(class_specifier_parse_action) = + self.recover_from_missing_new_argument_separator(state) + { + return Some(class_specifier_parse_action); + } + } + } + None + } + + /// Recovers from a missing separator between `new` arguments. + /// + /// Returns [`Some`] when recovery instead treats the boundary as the end of + /// the argument list. + fn recover_from_missing_new_argument_separator( + &mut self, + state: &mut NewArgumentListParseState<'src, 'arena>, + ) -> Option { + let has_parsed_all_allowed_arguments = + state.call_argument_list_parse_state.parsed_slot_count > 2; + let likely_missing_comma = !self.next_token_definitely_cannot_start_expression() + && !has_parsed_all_allowed_arguments; + if likely_missing_comma { + let next_argument_position = self.peek_position_or_eof(); + let mut error = self + .make_error_at_last_consumed(ParseErrorKind::NewArgumentMissingComma) + .widen_error_span_from(state.left_parenthesis_position) + .blame_token(next_argument_position) + .related_token("new_keyword", state.new_keyword_position) + .related_token("left_parenthesis", state.left_parenthesis_position); + if let Some(argument_span) = state.current_argument_span() { + error = error.related("previous_argument", argument_span); + } + error.report_error(self); + None + } else { + // If this does not look like another argument, + // treat the boundary as the missing `)` rather than inventing + // an extra comma diagnostic. + Some(self.recover_from_missing_new_closing_parenthesis(state)) + } + } + + /// Diagnoses and skips any arguments beyond the three accepted by `new`. + fn diagnose_extra_new_arguments( + &mut self, + state: &mut NewArgumentListParseState<'src, 'arena>, + ) { + // Require an explicit comma before diagnosing extra arguments so this + // does not mask a missing `)`. + if let Some((Token::Comma, extra_argument_comma_position)) = self.peek_token_and_position() + { + // Preserve the first extra argument span for a more precise + // diagnostic before we do any syncing. + let first_extra_argument_span = + match self.parse_call_argument_slot(&mut state.call_argument_list_parse_state) { + ParsedCallArgumentSlot::Argument(Some(argument)) => Some(*argument.span()), + ParsedCallArgumentSlot::Argument(None) => None, + ParsedCallArgumentSlot::NoMoreArguments => None, + }; + let mut error = self + .make_error_at_last_consumed(ParseErrorKind::NewTooManyArguments) + .widen_error_span_from(state.left_parenthesis_position) + .blame_token(extra_argument_comma_position) + .related_token("new_keyword", state.new_keyword_position) + .related_token("left_parenthesis", state.left_parenthesis_position); + if let Some(span) = state.last_parsed_allowed_argument_span() { + error = error.related("last_allowed_argument", span); + } + if let Some(span) = first_extra_argument_span { + error = error.related("first_extra_argument", span); + } + error + .sync_error_until(self, SyncLevel::CloseParenthesis) + .report_error(self); + } + } + + /// Reports a missing closing `)` in a `new(...)` argument list and + /// determines whether the class specifier should be parsed or skipped. + #[must_use] + fn recover_from_missing_new_closing_parenthesis( + &mut self, + state: &NewArgumentListParseState<'src, 'arena>, + ) -> NewClassSpecifierParseAction { + let mut error = self + .make_error_at_last_consumed(ParseErrorKind::NewMissingClosingParenthesis) + .widen_error_span_from(state.left_parenthesis_position) + .blame_token(self.peek_position_or_eof()) + .related_token("new_keyword", state.new_keyword_position) + .related_token("left_parenthesis", state.left_parenthesis_position); + let class_specifier_parse_action = if self.next_token_definitely_cannot_start_expression() { + error = error.sync_error_at(self, SyncLevel::CloseParenthesis); + // Skipping the class specifier avoids cascading errors when + // the next token cannot start an expression anyway. + NewClassSpecifierParseAction::Skip + } else { + NewClassSpecifierParseAction::Parse + }; + error.report_error(self); + class_specifier_parse_action + } + + /// Parses the class specifier of a `new` expression after argument-list + /// parsing and recovery. + #[must_use] + fn parse_new_class_specifier( + &mut self, + new_keyword_position: TokenPosition, + argument_list_end: Option, + class_specifier_parse_action: NewClassSpecifierParseAction, + ) -> ExpressionRef<'src, 'arena> { + match class_specifier_parse_action { + NewClassSpecifierParseAction::Parse + if self.next_token_definitely_cannot_start_expression() => + { + let mut error = self + .make_error_at_last_consumed(ParseErrorKind::NewMissingClassSpecifier) + .widen_error_span_from(new_keyword_position) + .sync_error_until(self, SyncLevel::ExpressionStart) + .extend_blame_to_next_token(self) + .related_token("new_keyword", new_keyword_position); + if let Some(argument_list_end) = argument_list_end { + error = error.related_token("argument_list_end", argument_list_end); + } + return self.report_error_with_fallback(error); + } + NewClassSpecifierParseAction::Parse => self.parse_expression(), + NewClassSpecifierParseAction::Skip => crate::arena::ArenaNode::new_in( + Expression::Error, + TokenSpan::new(self.peek_position_or_eof()), + self.arena, + ), + } + } +} diff --git a/rottlib/src/parser/grammar/expression/selectors.rs b/rottlib/src/parser/grammar/expression/selectors.rs index 7287bf5..9d49441 100644 --- a/rottlib/src/parser/grammar/expression/selectors.rs +++ b/rottlib/src/parser/grammar/expression/selectors.rs @@ -11,6 +11,36 @@ use crate::ast::{Expression, ExpressionRef, OptionalExpression}; use crate::lexer::{Token, TokenPosition, TokenSpan}; use crate::parser::{ParseErrorKind, ParseExpressionResult, Parser, ResultRecoveryExt, SyncLevel}; +// TODO: think about importing/moving out these fucking structs a level up. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct CallArgumentListParseState { + /// Number of argument slots already returned as `Argument(...)`. + /// + /// This counts omitted slots too, for example in `f(,x)` or `f(x,,z)`. + pub parsed_slot_count: usize, + + /// Whether the most recently returned argument slot was not followed by + /// a valid argument boundary such as `,` or `)`. + /// + /// This flag is reset at the start of each call, so after + /// `NoMoreArguments` it is always `false`. + pub last_slot_missing_boundary: bool, +} + +impl CallArgumentListParseState { + #[must_use] + pub(crate) fn new() -> Self { + Self { + parsed_slot_count: 0, + last_slot_missing_boundary: false, + } + } + #[must_use] + pub(crate) fn is_first_slot(&self) -> bool { + self.parsed_slot_count == 0 + } +} + /// Represents the result of parsing one call argument slot. /// /// This distinguishes between the end of the argument list and a parsed @@ -133,36 +163,59 @@ impl<'src, 'arena> Parser<'src, 'arena> { ) } + // TODO: add note that `parsed_slot_count` is guaranteed to be incremented + // by 1 at most (and when). + // TODO: say that errors must be handled by caller. /// Parses one call argument slot after an already consumed `(`. /// /// In `UnrealScript`, every comma introduces a follow-up argument slot, so a /// trailing comma immediately before `)` denotes an omitted final argument. /// - /// Returns [`ParsedCallArgumentSlot::NoMoreArguments`] when the argument - /// list ends, and `Argument(None)` for an omitted argument slot. + /// Returns [`ParsedCallArgumentSlot::NoMoreArguments`] when the argument list + /// ends, and `Argument(None)` for an omitted argument slot. + /// + /// Per-call status is recorded into `state`. pub(crate) fn parse_call_argument_slot( &mut self, - left_parenthesis_position: TokenPosition, - first_call: bool, + state: &mut CallArgumentListParseState, ) -> ParsedCallArgumentSlot<'src, 'arena> { + state.last_slot_missing_boundary = false; + + // This function consumes arguments one at a time and the way we chose + // to handle this is by consuming a comma *before* each new argument, + // not *after*. + // Normal (non-empty) case of special argument will simply skip this + // `match`. But first *empty* argument must be handled as + // a special case. match self.peek_token() { - Some(Token::RightParenthesis) => return ParsedCallArgumentSlot::NoMoreArguments, + None | Some(Token::RightParenthesis) => { + return ParsedCallArgumentSlot::NoMoreArguments; + } Some(Token::Comma) => { - if !first_call { + // We handle special case of first empty argument by *not* + // consuming first comma (it will be consumed together with + // the second argument). + // + // We do change parsing state by incrementing + // `state.parsed_slot_count`, which ensures that + // `is_first_slot()` will return `false` from now on. + if !state.is_first_slot() { self.advance(); } + // This `if`'s body is guaranteed to run if we've skipped + // `advance()` above. if self.at_call_argument_boundary() { + state.parsed_slot_count += 1; return ParsedCallArgumentSlot::Argument(None); } } _ => (), } + let argument = self.parse_expression(); - if !self.at_call_argument_boundary() { - self.make_error_here(ParseErrorKind::FunctionArgumentMissingComma) - .widen_error_span_from(left_parenthesis_position) - .report_error(self); - } + state.parsed_slot_count += 1; + state.last_slot_missing_boundary = !self.at_call_argument_boundary(); + ParsedCallArgumentSlot::Argument(Some(argument)) } @@ -176,12 +229,16 @@ impl<'src, 'arena> Parser<'src, 'arena> { ) -> ArenaVec<'arena, Option>> { let mut argument_list = ArenaVec::new_in(self.arena); - let mut first_call = true; + let mut call_state = CallArgumentListParseState::new(); + //let mut old_position = self.peek_position_or_eof(); + // This caused infinite loop? (on eof?) what? while let ParsedCallArgumentSlot::Argument(argument) = - self.parse_call_argument_slot(left_parenthesis_position, first_call) + self.parse_call_argument_slot(&mut call_state) { argument_list.push(argument); - first_call = false; + // TODO: ensure progress here shouldn't be necessary actually + //self.ensure_forward_progress(old_position); + //old_position = self.peek_position_or_eof(); } argument_list diff --git a/rottlib/src/parser/grammar/expression/switch.rs b/rottlib/src/parser/grammar/expression/switch.rs index 0e7a011..be948b4 100644 --- a/rottlib/src/parser/grammar/expression/switch.rs +++ b/rottlib/src/parser/grammar/expression/switch.rs @@ -155,7 +155,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> { // one problematic case. let mut sink = self.arena.vec(); self.parse_switch_arm_body(&mut sink); - self.make_error_here(ParseErrorKind::SwitchTopLevelItemNotCase) + self.make_error_at_last_consumed(ParseErrorKind::SwitchTopLevelItemNotCase) .widen_error_span_from(preamble_start_position) .report_error(self); } diff --git a/rottlib/src/parser/grammar/function/definition.rs b/rottlib/src/parser/grammar/function/definition.rs index 0ae98af..3402781 100644 --- a/rottlib/src/parser/grammar/function/definition.rs +++ b/rottlib/src/parser/grammar/function/definition.rs @@ -137,7 +137,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { return Ok(kind); } } - Err(self.make_error_here(ParseErrorKind::CallableExpectedKind)) + Err(self.make_error_at_last_consumed(ParseErrorKind::CallableExpectedKind)) } fn parse_callable_name( @@ -153,7 +153,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { ParseErrorKind::CallablePrefixOperatorInvalidSymbol, )?; let operator = PrefixOperator::try_from(token).map_err(|()| { - self.make_error_here(ParseErrorKind::CallablePrefixOperatorInvalidSymbol) + self.make_error_at_last_consumed(ParseErrorKind::CallablePrefixOperatorInvalidSymbol) })?; self.advance(); Ok(CallableName::PrefixOperator(PrefixOperatorName { @@ -166,7 +166,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { ParseErrorKind::CallableInfixOperatorInvalidSymbol, )?; let operator = InfixOperator::try_from(token).map_err(|()| { - self.make_error_here(ParseErrorKind::CallableInfixOperatorInvalidSymbol) + self.make_error_at_last_consumed(ParseErrorKind::CallableInfixOperatorInvalidSymbol) })?; self.advance(); Ok(CallableName::InfixOperator(InfixOperatorName { @@ -179,7 +179,7 @@ impl<'src, 'arena> Parser<'src, 'arena> { ParseErrorKind::CallablePostfixOperatorInvalidSymbol, )?; let operator = PostfixOperator::try_from(token).map_err(|()| { - self.make_error_here(ParseErrorKind::CallablePostfixOperatorInvalidSymbol) + self.make_error_at_last_consumed(ParseErrorKind::CallablePostfixOperatorInvalidSymbol) })?; self.advance(); Ok(CallableName::PostfixOperator(PostfixOperatorName { diff --git a/rottlib/src/parser/grammar/mod.rs b/rottlib/src/parser/grammar/mod.rs index 6467085..76768e7 100644 --- a/rottlib/src/parser/grammar/mod.rs +++ b/rottlib/src/parser/grammar/mod.rs @@ -10,6 +10,6 @@ mod class; mod declarations; -mod expression; +pub(super) mod expression; mod function; mod statement; diff --git a/rottlib/src/parser/mod.rs b/rottlib/src/parser/mod.rs index 50a8200..dba56c3 100644 --- a/rottlib/src/parser/mod.rs +++ b/rottlib/src/parser/mod.rs @@ -26,7 +26,6 @@ //! low-level plumbing lives in submodules. use super::lexer; -use crate::lexer::TokenSpan; pub use lexer::{TokenData, Tokens}; @@ -44,6 +43,13 @@ pub(crate) use trivia::{TriviaKind, TriviaToken}; pub type ParseExpressionResult<'src, 'arena> = ParseResult<'src, 'arena, crate::ast::ExpressionRef<'src, 'arena>>; +pub(crate) mod diagnostic_labels { + pub(crate) const EXPRESSION_REQUIRED_BY: &str = "expression_required_by"; + pub(crate) const EXPRESSION_EXPECTED_AFTER: &str = "expression_expected_after"; +} + +// TODO: add some kind of bailing for infinite loops +// let remaining_steps = file.token_count().saturating_mul(256).saturating_add(1024); /// A recursive-descent parser over token from [`crate::lexer::TokenizedFile`]. pub struct Parser<'src, 'arena> { file: &'src lexer::TokenizedFile<'src>, diff --git a/rottlib/src/parser/recovery.rs b/rottlib/src/parser/recovery.rs index 773272a..57f8e50 100644 --- a/rottlib/src/parser/recovery.rs +++ b/rottlib/src/parser/recovery.rs @@ -8,6 +8,9 @@ //! General idea is that any method that returns something other than an error //! can be assumed to have reported it. +#![allow(dead_code)] +// TODO: remove dead code + use crate::ast::{CallableKind, IdentifierToken, QualifiedIdentifier}; use crate::diagnostics::diagnostic_from_parse_error; use crate::lexer::{Token, TokenPosition, TokenSpan}; @@ -26,7 +29,7 @@ pub enum SyncLevel { /// Tokens that can reasonably continue or restart an expression. /// /// This is the loosest recovery level. - Expression, + ExpressionStart, /// Separator between homogeneous list elements, e.g. `,`. /// @@ -37,12 +40,12 @@ pub enum SyncLevel { /// Closing `>` of an angle-bracket-delimited type/class argument list. CloseAngleBracket, - /// Closing `)` of a parenthesized/grouped construct. - CloseParenthesis, - /// Closing `]` of an index or bracket-delimited construct. CloseBracket, + /// Closing `)` of a parenthesized/grouped construct. + CloseParenthesis, + /// A statement boundary or statement starter. /// /// Includes `;` and keywords that begin standalone statements / @@ -74,54 +77,10 @@ impl SyncLevel { use crate::lexer::Keyword; use SyncLevel::{ BlockBoundary, CloseAngleBracket, CloseBracket, CloseParenthesis, DeclarationStart, - Expression, ListSeparator, Statement, SwitchArmStart, + ExpressionStart, ListSeparator, Statement, SwitchArmStart, }; match token { - // Expression-level recovery points - Token::Exponentiation - | Token::Increment - | Token::Decrement - | Token::Not - | Token::BitwiseNot - | Token::Multiply - | Token::Divide - | Token::Modulo - | Token::Plus - | Token::Minus - | Token::ConcatSpace - | Token::Concat - | Token::LeftShift - | Token::LogicalRightShift - | Token::RightShift - | Token::LessEqual - | Token::GreaterEqual - | Token::Equal - | Token::NotEqual - | Token::ApproximatelyEqual - | Token::BitwiseAnd - | Token::BitwiseOr - | Token::BitwiseXor - | Token::LogicalAnd - | Token::LogicalXor - | Token::LogicalOr - | Token::Assign - | Token::MultiplyAssign - | Token::DivideAssign - | Token::ModuloAssign - | Token::PlusAssign - | Token::MinusAssign - | Token::ConcatAssign - | Token::ConcatSpaceAssign - | Token::Period - | Token::Question - | Token::Colon - | Token::LeftParenthesis - | Token::Identifier - | Token::Keyword(Keyword::Dot | Keyword::Cross | Keyword::ClockwiseFrom) => { - Some(Expression) - } - // List / delimiter boundaries Token::Comma => Some(ListSeparator), Token::Greater => Some(CloseAngleBracket), @@ -170,12 +129,18 @@ impl SyncLevel { // Hard structural stop Token::LeftBrace | Token::CppBlock | Token::RightBrace => Some(BlockBoundary), - _ => None, + _ => { + if token.is_definitely_not_expression_start() { + None + } else { + Some(ExpressionStart) + } + } } } } -impl Parser<'_, '_> { +impl<'src, 'arena> Parser<'src, 'arena> { /// Converts a parse error into a diagnostic and queues it. /// /// Placeholder implementation. @@ -188,7 +153,7 @@ impl Parser<'_, '_> { /// Reports a parser error with [`crate::parser::ParseErrorKind`] at /// the current location and queues an appropriate diagnostic. pub fn report_error_here(&mut self, error_kind: crate::parser::ParseErrorKind) { - let new_error = self.make_error_here(error_kind); + let new_error = self.make_error_at_last_consumed(error_kind); self.report_error(new_error); } @@ -205,6 +170,20 @@ impl Parser<'_, '_> { self.advance(); } } + + /// Reports `error` and returns the recovery fallback for `T`. + /// + /// This is the primitive used when parsing must keep going with a + /// best-effort placeholder value of the expected type. + #[must_use] + pub(crate) fn report_error_with_fallback(&mut self, error: ParseError) -> T + where + T: RecoveryFallback<'src, 'arena>, + { + let fallback = T::fallback_value(self, &error); + self.report_error(error); + fallback + } } /// Supplies a fallback value after a parse error so parsing can continue and @@ -235,6 +214,8 @@ pub trait ResultRecoveryExt<'src, 'arena, T>: Sized { fn extend_blame_start_to_covered_start(self) -> Self; fn extend_blame_end_to_covered_end(self) -> Self; + // TODO: say that we use textual tags because they are very local to each error and read better + // than some kind of constant. fn related_token(self, tag: impl Into, related_position: TokenPosition) -> Self { self.related(tag, TokenSpan::new(related_position)) } @@ -328,11 +309,7 @@ impl<'src, 'arena, T> ResultRecoveryExt<'src, 'arena, T> for ParseResult<'src, ' where T: RecoveryFallback<'src, 'arena>, { - self.unwrap_or_else(|error| { - let value = T::fallback_value(parser, &error); - parser.report_error(error); - value - }) + self.unwrap_or_else(|error| parser.report_error_with_fallback(error)) } fn report_error(self, parser: &mut Parser<'src, 'arena>) -> bool { @@ -367,17 +344,17 @@ impl<'src, 'arena> ResultRecoveryExt<'src, 'arena, ()> for ParseError { } fn extend_blame_to_next_token(mut self, parser: &mut Parser<'src, 'arena>) -> Self { - self.blame_span.end = parser.peek_position_or_eof(); + self.blame_span.end = std::cmp::max(self.blame_span.end, parser.peek_position_or_eof()); self } fn extend_blame_start_to_covered_start(mut self) -> Self { - self.blame_span.start = self.covered_span.start; + self.blame_span.start = std::cmp::min(self.blame_span.start, self.covered_span.start); self } fn extend_blame_end_to_covered_end(mut self) -> Self { - self.blame_span.end = self.covered_span.end; + self.blame_span.end = std::cmp::max(self.blame_span.end, self.covered_span.end); self } @@ -604,3 +581,16 @@ impl<'src, 'arena> RecoveryFallback<'src, 'arena> for crate::ast::ExecDirectiveR crate::arena::ArenaNode::new_in(def, err.covered_span, parser.arena) } } + +impl ParseError { + pub fn fallback<'src, 'arena, T>(self, parser: &mut Parser<'src, 'arena>) -> T + where + T: RecoveryFallback<'src, 'arena>, + { + parser.report_error_with_fallback(self) + } + + pub fn report<'src, 'arena>(self, parser: &mut Parser<'src, 'arena>) { + parser.report_error(self); + } +} diff --git a/rottlib/src/parser/trivia.rs b/rottlib/src/parser/trivia.rs index b2b375c..0af5422 100644 --- a/rottlib/src/parser/trivia.rs +++ b/rottlib/src/parser/trivia.rs @@ -26,6 +26,9 @@ //! //! Violating this protocol is a logic error. +#![allow(dead_code)] +// TODO: remove dead code + use crate::lexer::TokenPosition; /// Kinds of trivia tokens corresponding to variants of [`crate::lexer::Token`]. @@ -83,7 +86,6 @@ enum BoundaryLocation { /// token, as well as file-leading and file-trailing trivia. Returned slices /// borrow the index, and the contained token texts live for `'src`. #[derive(Clone, Debug, PartialEq, Eq, Default)] -#[allow(dead_code)] pub struct TriviaIndex<'src> { /// All trivia tokens, stored contiguously in file order. tokens: Vec>, @@ -101,7 +103,6 @@ pub struct TriviaIndex<'src> { /// a token stream in file order. Once all tokens have been processed, call /// [`TriviaIndexBuilder::into_index`] to finalize the index. #[derive(Debug)] -#[allow(dead_code)] pub struct TriviaIndexBuilder<'src> { /// All trivia tokens, stored contiguously in file order. tokens: Vec>, @@ -203,7 +204,6 @@ impl<'src> TriviaIndex<'src> { /// Returns an empty slice if `position` does not identify a recorded /// significant token or if no trivia was recorded after it. #[must_use] - #[allow(dead_code)] pub(crate) fn trivia_after_token(&self, position: TokenPosition) -> &[TriviaToken<'src>] { self.slice_for( BoundaryLocation::Token(position), @@ -216,7 +216,6 @@ impl<'src> TriviaIndex<'src> { /// Returns an empty slice if `position` does not identify a recorded /// significant token or if no trivia was recorded before it. #[must_use] - #[allow(dead_code)] pub(crate) fn trivia_before_token(&self, position: TokenPosition) -> &[TriviaToken<'src>] { self.slice_for( BoundaryLocation::Token(position), @@ -228,7 +227,6 @@ impl<'src> TriviaIndex<'src> { /// /// If no significant tokens were recorded, returns all recorded trivia. #[must_use] - #[allow(dead_code)] pub(crate) fn leading_trivia(&self) -> &[TriviaToken<'src>] { self.slice_for(BoundaryLocation::StartOfFile, &self.trivia_after_boundary) } @@ -237,12 +235,10 @@ impl<'src> TriviaIndex<'src> { /// /// If no significant tokens were recorded, returns all recorded trivia. #[must_use] - #[allow(dead_code)] pub(crate) fn trailing_trivia(&self) -> &[TriviaToken<'src>] { self.slice_for(BoundaryLocation::EndOfFile, &self.trivia_before_boundary) } - #[allow(dead_code)] fn slice_for(&self, key: BoundaryLocation, map: &TriviaRangeMap) -> &[TriviaToken<'src>] { match map.get(&key) { Some(range) => { diff --git a/rottlib/tests/common.rs b/rottlib/tests/common.rs deleted file mode 100644 index d467771..0000000 --- a/rottlib/tests/common.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::path::{Path, PathBuf}; - -use rottlib::lexer::{Token, TokenData, TokenPosition, TokenizedFile}; - -pub fn fixture_path(name: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("fixtures") - .join(name) -} - -pub fn read_fixture(name: &str) -> String { - let path = fixture_path(name); - std::fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("failed to read fixture {}: {e}", path.display())) -} - -pub fn with_fixture(name: &str, f: impl for<'src> FnOnce(&'src str, TokenizedFile<'src>)) { - let source = read_fixture(name); - let file = TokenizedFile::tokenize(&source); - f(&source, file); -} - -pub fn line_lexemes<'file, 'src>(file: &'file TokenizedFile<'src>, line: usize) -> Vec<&'src str> { - file.line_tokens(line).map(|(_, t)| t.lexeme).collect() -} - -pub fn line_tokens<'src>(file: &TokenizedFile<'src>, line: usize) -> Vec { - file.line_tokens(line).map(|(_, t)| t.token).collect() -} - -pub fn line_positions<'src>(file: &TokenizedFile<'src>, line: usize) -> Vec { - file.line_tokens(line).map(|(pos, _)| pos).collect() -} - -pub fn line_pairs<'file, 'src>( - file: &'file TokenizedFile<'src>, - line: usize, -) -> Vec<(Token, &'src str)> { - file.line_tokens(line) - .map(|(_, t)| (t.token, t.lexeme)) - .collect() -} - -pub fn all_lexemes<'file, 'src>(file: &'file TokenizedFile<'src>) -> Vec<&'src str> { - file.iter().map(|(_, t)| t.lexeme).collect() -} - -pub fn all_tokens<'src>(file: &TokenizedFile<'src>) -> Vec { - file.iter().map(|(_, t)| t.token).collect() -} - -pub fn token_at<'src>(file: &TokenizedFile<'src>, index: usize) -> Option> { - file.token_at(TokenPosition(index)) -} - -pub fn reconstruct_source<'file, 'src>(file: &'file TokenizedFile<'src>) -> String { - file.iter().map(|(_, t)| t.lexeme).collect() -} - -pub fn find_line<'src>(file: &TokenizedFile<'src>, needle: &str) -> Option { - (0..file.line_count()).find(|&line| file.line_text(line).as_deref() == Some(needle)) -} diff --git a/rottlib/tests/diagnostics.rs b/rottlib/tests/diagnostics.rs new file mode 100644 index 0000000..28d7c7f --- /dev/null +++ b/rottlib/tests/diagnostics.rs @@ -0,0 +1 @@ +mod parser_diagnostics; \ No newline at end of file diff --git a/rottlib/tests/diagnostics_expressions.rs b/rottlib/tests/diagnostics_expressions.rs deleted file mode 100644 index 9a260cc..0000000 --- a/rottlib/tests/diagnostics_expressions.rs +++ /dev/null @@ -1,394 +0,0 @@ -use std::collections::HashMap; - -use rottlib::arena::Arena; -use rottlib::diagnostics::Diagnostic; -use rottlib::lexer::{TokenPosition, TokenSpan, TokenizedFile}; -use rottlib::parser::Parser; - -#[derive(Debug, Clone, Copy)] -pub struct Fixture { - pub code: &'static str, - pub label: &'static str, - pub source: &'static str, -} - -pub const FIXTURES: &[Fixture] = &[ - Fixture { - code: "P0001", - label: "files/P0001_01.uc", - source: "c && ( /*lol*/ ** calc_it())", - }, - Fixture { - code: "P0001", - label: "files/P0001_02.uc", - source: "\r\na + (\n//AAA\n//BBB\n//CCC\n//DDD\n//EEE\n//FFF\n ]", - }, - Fixture { - code: "P0001", - label: "files/P0001_03.uc", - source: "(\n// nothing here, bucko", - }, - Fixture { - code: "P0002", - label: "files/P0002_01.uc", - source: "a + [", - }, - Fixture { - code: "P0002", - label: "files/P0002_02.uc", - source: "a * \n//some\n//empty lines\n *", - }, - Fixture { - code: "P0002", - label: "files/P0002_03.uc", - source: "a &&", - }, - Fixture { - code: "P0002", - label: "files/P0002_04.uc", - source: "a * * *", - }, - Fixture { - code: "P0003", - label: "files/P0003_01.uc", - source: "(a + b && c / d ^ e @ f", - }, - Fixture { - code: "P0003", - label: "files/P0003_02.uc", - source: "(a]", - }, - Fixture { - code: "P0003", - label: "files/P0003_03.uc", - source: "(a\n;", - }, -]; - -pub struct FixtureRun<'src> { - pub fixture: &'static Fixture, - pub file: TokenizedFile<'src>, - pub diagnostics: Vec, -} - -pub struct FixtureRuns<'src> { - runs: HashMap<&'static str, FixtureRun<'src>>, -} - -impl<'src> FixtureRuns<'src> { - pub fn get(&self, label: &str) -> Option> { - self.runs - .get(label) - .map(|fixture_run| fixture_run.diagnostics.clone()) - } - - pub fn get_any(&self, label: &str) -> Diagnostic { - self.runs - .get(label) - .map(|fixture_run| fixture_run.diagnostics[0].clone()) - .unwrap() - } - - pub fn iter(&self) -> impl Iterator)> { - self.runs.iter().map(|(label, run)| (*label, run)) - } -} - -fn run_fixture(fixture: &'static Fixture) -> FixtureRun<'static> { - let arena = Arena::new(); - let file = TokenizedFile::tokenize(fixture.source); - let mut parser = Parser::new(&file, &arena); - - let _ = parser.parse_expression(); - let diagnostics = parser.diagnostics.clone(); - - FixtureRun { - fixture, - file, - diagnostics, - } -} - -pub fn run_fixtures(code: &str) -> FixtureRuns<'static> { - let mut runs = HashMap::new(); - - for fixture in FIXTURES.iter().filter(|fixture| fixture.code == code) { - runs.insert(fixture.label, run_fixture(fixture)); - } - - for (label, run) in runs.iter() { - run.diagnostics.iter().for_each(|diag| { - diag.render(&run.file, *label); - }); - println!(); - } - - FixtureRuns { runs } -} - -#[test] -fn check_p0001_fixtures() { - let runs = run_fixtures("P0001"); - - assert_eq!(runs.get("files/P0001_01.uc").unwrap().len(), 1); - assert_eq!(runs.get("files/P0001_02.uc").unwrap().len(), 1); - assert_eq!(runs.get("files/P0001_03.uc").unwrap().len(), 1); - - assert_eq!( - runs.get_any("files/P0001_01.uc").headline(), - "expected expression inside parentheses, found `**`" - ); - assert_eq!( - runs.get_any("files/P0001_02.uc").headline(), - "expected expression inside parentheses, found `]`" - ); - assert_eq!( - runs.get_any("files/P0001_03.uc").headline(), - "expected expression, found end of file" - ); - - assert_eq!(runs.get_any("files/P0001_01.uc").code(), Some("P0001")); - assert_eq!(runs.get_any("files/P0001_02.uc").code(), Some("P0001")); - assert_eq!(runs.get_any("files/P0001_03.uc").code(), Some("P0001")); - - assert_eq!( - runs.get_any("files/P0001_01.uc") - .primary_label() - .unwrap() - .span, - TokenSpan { - start: TokenPosition(8), - end: TokenPosition(8) - } - ); - - assert_eq!( - runs.get_any("files/P0001_02.uc") - .primary_label() - .unwrap() - .span, - TokenSpan { - start: TokenPosition(5), - end: TokenPosition(20) - } - ); - - assert_eq!( - runs.get_any("files/P0001_03.uc") - .primary_label() - .unwrap() - .span, - TokenSpan { - start: TokenPosition(0), - end: TokenPosition(3) - } - ); - - assert_eq!( - runs.get_any("files/P0001_01.uc") - .primary_label() - .unwrap() - .message, - "unexpected `**`" - ); - assert_eq!( - runs.get_any("files/P0001_02.uc") - .primary_label() - .unwrap() - .message, - "unexpected `]`" - ); - assert_eq!( - runs.get_any("files/P0001_03.uc") - .primary_label() - .unwrap() - .message, - "reached end of file here" - ); -} - -#[test] -fn check_p0002_fixtures() { - let runs = run_fixtures("P0002"); - - assert_eq!(runs.get("files/P0002_01.uc").unwrap().len(), 1); - assert_eq!(runs.get("files/P0002_02.uc").unwrap().len(), 1); - assert_eq!(runs.get("files/P0002_03.uc").unwrap().len(), 1); - assert_eq!(runs.get("files/P0002_04.uc").unwrap().len(), 1); - - assert_eq!( - runs.get_any("files/P0002_01.uc").headline(), - "expected expression after `+`, found `[`" - ); - assert_eq!( - runs.get_any("files/P0002_02.uc").headline(), - "expected expression after `*`, found `*`" - ); - assert_eq!( - runs.get_any("files/P0002_03.uc").headline(), - "expected expression after `&&`, found end of file" - ); - assert_eq!( - runs.get_any("files/P0002_04.uc").headline(), - "expected expression after `*`, found `*`" - ); - - assert_eq!(runs.get_any("files/P0002_01.uc").code(), Some("P0002")); - assert_eq!(runs.get_any("files/P0002_02.uc").code(), Some("P0002")); - assert_eq!(runs.get_any("files/P0002_03.uc").code(), Some("P0002")); - assert_eq!(runs.get_any("files/P0002_04.uc").code(), Some("P0002")); - - assert_eq!( - runs.get_any("files/P0002_01.uc") - .primary_label() - .unwrap() - .span, - TokenSpan { - start: TokenPosition(4), - end: TokenPosition(4), - } - ); - - assert_eq!( - runs.get_any("files/P0002_02.uc") - .primary_label() - .unwrap() - .span, - TokenSpan { - start: TokenPosition(10), - end: TokenPosition(10), - } - ); - - assert_eq!( - runs.get_any("files/P0002_03.uc") - .primary_label() - .unwrap() - .span, - TokenSpan { - start: TokenPosition(3), - end: TokenPosition(3), - } - ); - - assert_eq!( - runs.get_any("files/P0002_04.uc") - .primary_label() - .unwrap() - .span, - TokenSpan { - start: TokenPosition(4), - end: TokenPosition(4), - } - ); - - assert_eq!( - runs.get_any("files/P0002_01.uc") - .primary_label() - .unwrap() - .message, - "unexpected `[`" - ); - assert_eq!( - runs.get_any("files/P0002_02.uc") - .primary_label() - .unwrap() - .message, - "unexpected `*`" - ); - assert_eq!( - runs.get_any("files/P0002_03.uc") - .primary_label() - .unwrap() - .message, - "reached end of file here" - ); - assert_eq!( - runs.get_any("files/P0002_04.uc") - .primary_label() - .unwrap() - .message, - "unexpected `*`" - ); -} - -#[test] -fn check_p0003_fixtures() { - let runs = run_fixtures("P0003"); - - assert_eq!(runs.get("files/P0003_01.uc").unwrap().len(), 1); - assert_eq!(runs.get("files/P0003_02.uc").unwrap().len(), 1); - assert_eq!(runs.get("files/P0003_03.uc").unwrap().len(), 1); - - assert_eq!( - runs.get_any("files/P0003_01.uc").headline(), - "missing `)` to close parenthesized expression" - ); - assert_eq!( - runs.get_any("files/P0003_02.uc").headline(), - "missing `)` to close parenthesized expression" - ); - assert_eq!( - runs.get_any("files/P0003_03.uc").headline(), - "missing `)` to close parenthesized expression" - ); - - assert_eq!(runs.get_any("files/P0003_01.uc").code(), Some("P0003")); - assert_eq!(runs.get_any("files/P0003_02.uc").code(), Some("P0003")); - assert_eq!(runs.get_any("files/P0003_03.uc").code(), Some("P0003")); - - assert_eq!( - runs.get_any("files/P0003_01.uc") - .primary_label() - .unwrap() - .span, - TokenSpan { - start: TokenPosition(22), - end: TokenPosition(22), - } - ); - - assert_eq!( - runs.get_any("files/P0003_02.uc") - .primary_label() - .unwrap() - .span, - TokenSpan { - start: TokenPosition(2), - end: TokenPosition(2), - } - ); - - assert_eq!( - runs.get_any("files/P0003_03.uc") - .primary_label() - .unwrap() - .span, - TokenSpan { - start: TokenPosition(0), - end: TokenPosition(3), - } - ); - - assert_eq!( - runs.get_any("files/P0003_01.uc") - .primary_label() - .unwrap() - .message, - "expected `)` before end of file" - ); - assert_eq!( - runs.get_any("files/P0003_02.uc") - .primary_label() - .unwrap() - .message, - "expected `)` before `]`" - ); - assert_eq!( - runs.get_any("files/P0003_03.uc") - .primary_label() - .unwrap() - .message, - "expected `)` before `;`" - ); -} \ No newline at end of file diff --git a/rottlib/tests/parser_diagnostics/control_flow_expressions.rs b/rottlib/tests/parser_diagnostics/control_flow_expressions.rs new file mode 100644 index 0000000..47ba80d --- /dev/null +++ b/rottlib/tests/parser_diagnostics/control_flow_expressions.rs @@ -0,0 +1,1593 @@ +use super::*; + +pub(super) const P0012_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0012_01.uc", + source: "if ; DoThing();", + }, + Fixture { + label: "files/P0012_02.uc", + source: "while\n\n;\nDoThing();", + }, + Fixture { + label: "files/P0012_03.uc", + source: "do DoThing(); until ;", + }, + Fixture { + label: "files/P0012_04.uc", + source: "if ) DoThing();", + }, + Fixture { + label: "files/P0012_05.uc", + source: "do\n\nDoThing();\n\n\n\nuntil ;", + }, + Fixture { + label: "files/P0012_06.uc", + source: "if \n\n{DoThing();}\n\n\n\n", + }, +]; + +#[test] +fn check_p0012_fixtures() { + let runs = run_fixtures(P0012_FIXTURES); + + assert_eq!(runs.get("files/P0012_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0012_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0012_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0012_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0012_05.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0012_06.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0012_01.uc"), + &ExpectedDiagnostic { + headline: "expected condition after `if`, found `;`", + severity: Severity::Error, + code: Some("P0012"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "unexpected `;`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0012_02.uc"), + &ExpectedDiagnostic { + headline: "expected condition after `while`, found `;`", + severity: Severity::Error, + code: Some("P0012"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "unexpected `;`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "after this `while`, a condition was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0012_03.uc"), + &ExpectedDiagnostic { + headline: "expected condition after `until`, found `;`", + severity: Severity::Error, + code: Some("P0012"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "unexpected `;`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0012_04.uc"), + &ExpectedDiagnostic { + headline: "expected condition after `if`, found `)`", + severity: Severity::Error, + code: Some("P0012"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "unexpected `)`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0012_05.uc"), + &ExpectedDiagnostic { + headline: "expected condition after `until`, found `;`", + severity: Severity::Error, + code: Some("P0012"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(13), + end: TokenPosition(13), + }, + message: "unexpected `;`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`do` expression starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0012_06.uc"), + &ExpectedDiagnostic { + headline: "expected condition after `if`, found `{`", + severity: Severity::Error, + code: Some("P0012"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "body starts here, but the condition is missing", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "after this `if`, a condition was expected", + }], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0013_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0013_01.uc", + source: "if IsReady()", + }, + Fixture { + label: "files/P0013_02.uc", + source: "while\n\nIsReady() : ", + }, + Fixture { + label: "files/P0013_03.uc", + source: "for (I = 0; I < Count; ++I)\n\n", + }, + Fixture { + label: "files/P0013_04.uc", + source: "if IsReady()\n DoThing();\nelse\n\n\n\n:\n\n\n", + }, +]; + +#[test] +fn check_p0013_fixtures() { + let runs = run_fixtures(P0013_FIXTURES); + + assert_eq!(runs.get("files/P0013_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0013_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0013_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0013_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0013_01.uc"), + &ExpectedDiagnostic { + headline: "expected body for `if`, found end of file", + severity: Severity::Error, + code: Some("P0013"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "reached end of file here", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0013_02.uc"), + &ExpectedDiagnostic { + headline: "expected body for `while`, found `:`", + severity: Severity::Error, + code: Some("P0013"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "unexpected `:`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "after this `)`, a body was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0013_03.uc"), + &ExpectedDiagnostic { + headline: "expected body for `for`, found end of file", + severity: Severity::Error, + code: Some("P0013"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(22), + end: TokenPosition(22), + }, + message: "reached end of file here", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(19), + end: TokenPosition(19), + }, + message: "after this `)`, a body was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0013_04.uc"), + &ExpectedDiagnostic { + headline: "expected body for `else`, found `:`", + severity: Severity::Error, + code: Some("P0013"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(17), + end: TokenPosition(17), + }, + message: "unexpected `:`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "after this `else`, a body was expected", + }], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0014_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0014_01.uc", + source: "do DoThing();", + }, + Fixture { + label: "files/P0014_02.uc", + source: "do\n DoThing();\n", + }, + Fixture { + label: "files/P0014_03.uc", + source: "do\n\n;\n\n:\n", + }, + Fixture { + label: "files/P0014_04.uc", + source: "do\n{\n DoThing();\n}\n\n\n:\n", + }, +]; + +#[test] +fn check_p0014_fixtures() { + let runs = run_fixtures(P0014_FIXTURES); + + assert_eq!(runs.get("files/P0014_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0014_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0014_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0014_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0014_01.uc"), + &ExpectedDiagnostic { + headline: "missing `until` after `do` body", + severity: Severity::Error, + code: Some("P0014"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(6), + }, + message: "expected `until` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0014_02.uc"), + &ExpectedDiagnostic { + headline: "missing `until` after `do` body", + severity: Severity::Error, + code: Some("P0014"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(8), + }, + message: "expected `until` before end of file", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`do` expression starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0014_03.uc"), + &ExpectedDiagnostic { + headline: "missing `until` after `do` body", + severity: Severity::Error, + code: Some("P0014"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(6), + }, + message: "expected `until` before `:`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`do` expression starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0014_04.uc"), + &ExpectedDiagnostic { + headline: "missing `until` after `do` body", + severity: Severity::Error, + code: Some("P0014"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(14), + }, + message: "expected `until` before `:`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`do` expression starts here", + }], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0015_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0015_01.uc", + source: "foreach", + }, + Fixture { + label: "files/P0015_02.uc", + source: "foreach ;", + }, + Fixture { + label: "files/P0015_03.uc", + source: "foreach }", + }, + Fixture { + label: "files/P0015_04.uc", + source: "foreach\n\n)\n P.Destroy();\n", + }, + Fixture { + label: "files/P0015_05.uc", + source: "foreach\n{\n Log(A);\n}\n", + }, +]; + +#[test] +fn check_p0015_fixtures() { + let runs = run_fixtures(P0015_FIXTURES); + + assert_eq!(runs.get("files/P0015_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0015_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0015_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0015_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0015_05.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0015_01.uc"), + &ExpectedDiagnostic { + headline: "expected iterator expression after `foreach`, found end of file", + severity: Severity::Error, + code: Some("P0015"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(1), + end: TokenPosition(1), + }, + message: "reached end of file here", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0015_02.uc"), + &ExpectedDiagnostic { + headline: "expected iterator expression after `foreach`, found `;`", + severity: Severity::Error, + code: Some("P0015"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "unexpected `;`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0015_03.uc"), + &ExpectedDiagnostic { + headline: "expected iterator expression after `foreach`, found `}`", + severity: Severity::Error, + code: Some("P0015"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "unexpected `}`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0015_04.uc"), + &ExpectedDiagnostic { + headline: "expected iterator expression after `foreach`, found `)`", + severity: Severity::Error, + code: Some("P0015"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "unexpected `)`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "after this `foreach`, an iterator expression was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0015_05.uc"), + &ExpectedDiagnostic { + headline: "expected iterator expression after `foreach`, found `{`", + severity: Severity::Error, + code: Some("P0015"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "body starts here, but the iterator expression is missing", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "after this `foreach`, an iterator expression was expected", + }], + help: None, + notes: &[], + }, + ); +} + +pub(super) const FOR_HEADER_FIXTURES: &[Fixture] = &[ + // P0016: invalid initializer start after `for (` + Fixture { + label: "files/P0016_01.uc", + source: "for\n(] ; )", + }, + Fixture { + label: "files/P0016_02.uc", + source: "for (\n ]\n ;\n)\n Body();\n", + }, + Fixture { + label: "files/P0016_03.uc", + source: "for (\n }\n ;\n)\n", + }, + Fixture { + label: "files/P0016_06.uc", + source: "for (\n ]\n\n\n ; Step)\n", + }, + + // P0017: initializer parsed, but first `;` is missing + Fixture { + label: "files/P0017_01.uc", + source: "for (Init ] ; )", + }, + Fixture { + label: "files/P0017_02.uc", + source: "for (Init\n ]\n ;\n)\n", + }, + Fixture { + label: "files/P0017_04.uc", + source: "for (Init {\n Body();\n}; )\n", + }, + Fixture { + label: "files/P0017_05.uc", + source: "for (Init", + }, + + // P0018: invalid condition start after first `;` + Fixture { + label: "files/P0018_01.uc", + source: "for \n\n (; ] ; )", + }, + Fixture { + label: "files/P0018_02.uc", + source: "for (;\n ]\n ;\n)\n Body();\n", + }, + Fixture { + label: "files/P0018_03.uc", + source: "for (;\n }\n ;\n)\n", + }, + Fixture { + label: "files/P0018_06.uc", + source: "for (;", + }, + + // P0019: condition parsed, but second `;` is missing + Fixture { + label: "files/P0019_01.uc", + source: "for (; bCondition )", + }, + Fixture { + label: "files/P0019_02.uc", + source: "for (; bCondition\n)\n Body();\n", + }, + Fixture { + label: "files/P0019_03.uc", + source: "for (; bCondition ] ; )", + }, + Fixture { + label: "files/P0019_04.uc", + source: "for (; bCondition\n{\n Body();\n}\n;\n)\n", + }, + Fixture { + label: "files/P0019_06.uc", + source: "for (; bCondition", + }, + Fixture { + label: "files/P0019_07.uc", + source: "for (; bCondition Step)", + }, + + // P0020: invalid step start after second `;` + Fixture { + label: "files/P0020_01.uc", + source: "for (;;;)", + }, + Fixture { + label: "files/P0020_02.uc", + source: "for (;;\n ;\n)\n", + }, + Fixture { + label: "files/P0020_03.uc", + source: "for (;; ])", + }, + Fixture { + label: "files/P0020_04.uc", + source: "for (;;\n }\n)\n", + }, + Fixture { + label: "files/P0020_08.uc", + source: "for (;;\n ]\n", + }, + + // P0021: missing `)` to close `for` header + Fixture { + label: "files/P0021_01.uc", + source: "for (;;", + }, + Fixture { + label: "files/P0021_02.uc", + source: "for (;; Step", + }, + Fixture { + label: "files/P0021_03.uc", + source: "for (;; Step;\n Body();\n", + }, + Fixture { + label: "files/P0021_05.uc", + source: "for (Init; bCondition; Step\n{\n Body();\n}\n", + }, + Fixture { + label: "files/P0021_09.uc", + source: "for\n(Init;\n bCondition;\n Step\n]\n", + }, +]; + +#[test] +fn check_for_header_fixture_counts() { + let runs = run_fixtures(FOR_HEADER_FIXTURES); + + assert_eq!(runs.get("files/P0016_01.uc").unwrap().len(), 3); + assert_eq!(runs.get("files/P0016_02.uc").unwrap().len(), 2); + assert_eq!(runs.get("files/P0016_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0016_06.uc").unwrap().len(), 3); + + assert_eq!(runs.get("files/P0017_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0017_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0017_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0017_05.uc").unwrap().len(), 1); + + assert_eq!(runs.get("files/P0018_01.uc").unwrap().len(), 2); + assert_eq!(runs.get("files/P0018_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0018_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0018_06.uc").unwrap().len(), 1); + + assert_eq!(runs.get("files/P0019_01.uc").unwrap().len(), 2); + assert_eq!(runs.get("files/P0019_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0019_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0019_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0019_06.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0019_07.uc").unwrap().len(), 1); + + assert_eq!(runs.get("files/P0020_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0020_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0020_03.uc").unwrap().len(), 2); + assert_eq!(runs.get("files/P0020_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0020_08.uc").unwrap().len(), 1); + + assert_eq!(runs.get("files/P0021_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0021_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0021_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0021_05.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0021_09.uc").unwrap().len(), 1); +} + +#[test] +fn check_p0016_for_header_fixtures() { + let runs = run_fixtures(FOR_HEADER_FIXTURES); + + assert_diagnostic( + &runs.get_by_code("files/P0016_01.uc", "P0016"), + &ExpectedDiagnostic { + headline: "expected initializer expression or `;` after `(` in `for` header, found `]`", + severity: Severity::Error, + code: Some("P0016"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "unexpected `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`for` loop starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0016_02.uc", "P0016"), + &ExpectedDiagnostic { + headline: "expected initializer expression or `;` after `(` in `for` header, found `]`", + severity: Severity::Error, + code: Some("P0016"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "unexpected `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "after this `(`, an initializer expression or `;` was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0016_03.uc", "P0016"), + &ExpectedDiagnostic { + headline: "expected initializer expression or `;` after `(` in `for` header, found `}`", + severity: Severity::Error, + code: Some("P0016"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "unexpected `}`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "after this `(`, an initializer expression or `;` was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0016_06.uc", "P0016"), + &ExpectedDiagnostic { + headline: "expected initializer expression or `;` after `(` in `for` header, found `]`", + severity: Severity::Error, + code: Some("P0016"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "unexpected `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "after this `(`, an initializer expression or `;` was expected", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0017_for_header_fixtures() { + let runs = run_fixtures(FOR_HEADER_FIXTURES); + + assert_diagnostic( + &runs.get_by_code("files/P0017_01.uc", "P0017"), + &ExpectedDiagnostic { + headline: "missing `;` after initializer in `for` header", + severity: Severity::Error, + code: Some("P0017"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "expected `;` before `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "initializer ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0017_02.uc", "P0017"), + &ExpectedDiagnostic { + headline: "missing `;` after initializer in `for` header", + severity: Severity::Error, + code: Some("P0017"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(6), + }, + message: "expected `;` before `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "initializer ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0017_04.uc", "P0017"), + &ExpectedDiagnostic { + headline: "missing `;` after initializer in `for` header", + severity: Severity::Error, + code: Some("P0017"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "expected `;` before `{`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "initializer ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0017_05.uc", "P0017"), + &ExpectedDiagnostic { + headline: "missing `;` after initializer in `for` header", + severity: Severity::Error, + code: Some("P0017"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "expected `;` before end of file", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "initializer ends here", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0018_for_header_fixtures() { + let runs = run_fixtures(FOR_HEADER_FIXTURES); + + assert_diagnostic( + &runs.get_by_code("files/P0016_01.uc", "P0018"), + &ExpectedDiagnostic { + headline: "expected condition expression or second `;` in `for` header, found `)`", + severity: Severity::Error, + code: Some("P0018"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "unexpected `)`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`for` loop starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0016_02.uc", "P0018"), + &ExpectedDiagnostic { + headline: "expected condition expression or second `;` in `for` header, found `)`", + severity: Severity::Error, + code: Some("P0018"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(10), + end: TokenPosition(10), + }, + message: "unexpected `)`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "after this `;`, a condition expression or another `;` was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0018_01.uc", "P0018"), + &ExpectedDiagnostic { + headline: "expected condition expression or second `;` in `for` header, found `]`", + severity: Severity::Error, + code: Some("P0018"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "unexpected `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`for` loop starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0018_02.uc", "P0018"), + &ExpectedDiagnostic { + headline: "expected condition expression or second `;` in `for` header, found `]`", + severity: Severity::Error, + code: Some("P0018"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(6), + }, + message: "unexpected `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "after this `;`, a condition expression or another `;` was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0018_03.uc", "P0018"), + &ExpectedDiagnostic { + headline: "expected condition expression or second `;` in `for` header, found `}`", + severity: Severity::Error, + code: Some("P0018"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(6), + }, + message: "unexpected `}`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "after this `;`, a condition expression or another `;` was expected", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0019_for_header_fixtures() { + let runs = run_fixtures(FOR_HEADER_FIXTURES); + + assert_diagnostic( + &runs.get_by_code("files/P0016_06.uc", "P0019"), + &ExpectedDiagnostic { + headline: "missing `;` after condition in `for` header", + severity: Severity::Error, + code: Some("P0019"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(13), + end: TokenPosition(13), + }, + message: "expected `;` before `)`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "condition ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0018_06.uc", "P0019"), + &ExpectedDiagnostic { + headline: "missing second `;` in `for` header", + severity: Severity::Error, + code: Some("P0019"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "expected second `;` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0019_01.uc", "P0019"), + &ExpectedDiagnostic { + headline: "missing `;` after condition in `for` header", + severity: Severity::Error, + code: Some("P0019"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "expected `;` before `)`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "condition ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0019_02.uc", "P0019"), + &ExpectedDiagnostic { + headline: "missing `;` after condition in `for` header", + severity: Severity::Error, + code: Some("P0019"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "expected `;` before `)`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "condition ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0019_03.uc", "P0019"), + &ExpectedDiagnostic { + headline: "missing `;` after condition in `for` header", + severity: Severity::Error, + code: Some("P0019"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "expected `;` before `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "condition ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0019_04.uc", "P0019"), + &ExpectedDiagnostic { + headline: "missing `;` after condition in `for` header", + severity: Severity::Error, + code: Some("P0019"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "expected `;` before `{`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "condition ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0019_06.uc", "P0019"), + &ExpectedDiagnostic { + headline: "missing `;` after condition in `for` header", + severity: Severity::Error, + code: Some("P0019"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(6), + }, + message: "expected `;` before end of file", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "condition ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0019_07.uc", "P0019"), + &ExpectedDiagnostic { + headline: "missing `;` after condition in `for` header", + severity: Severity::Error, + code: Some("P0019"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "expected `;` before `Step`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "condition ends here", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0020_for_header_fixtures() { + let runs = run_fixtures(FOR_HEADER_FIXTURES); + + assert_diagnostic( + &runs.get_by_code("files/P0020_01.uc", "P0020"), + &ExpectedDiagnostic { + headline: "unexpected third `;` in `for` header", + severity: Severity::Error, + code: Some("P0020"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "expected step expression or `)` after the second `;`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0020_02.uc", "P0020"), + &ExpectedDiagnostic { + headline: "unexpected third `;` in `for` header", + severity: Severity::Error, + code: Some("P0020"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "expected step expression or `)` after the second `;`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "after this `;`, a step expression or `)` was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0020_03.uc", "P0020"), + &ExpectedDiagnostic { + headline: "expected step expression or `)` after the second `;` in `for` header, found `]`", + severity: Severity::Error, + code: Some("P0020"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(6), + }, + message: "unexpected `]`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0020_04.uc", "P0020"), + &ExpectedDiagnostic { + headline: "expected step expression or `)` after the second `;` in `for` header, found `}`", + severity: Severity::Error, + code: Some("P0020"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "unexpected `}`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "after this `;`, a step expression or `)` was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0020_08.uc", "P0020"), + &ExpectedDiagnostic { + headline: "expected step expression or `)` after the second `;` in `for` header, found `]`", + severity: Severity::Error, + code: Some("P0020"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "unexpected `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "after this `;`, a step expression or `)` was expected", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0021_for_header_fixtures() { + let runs = run_fixtures(FOR_HEADER_FIXTURES); + + assert_diagnostic( + &runs.get_by_code("files/P0021_01.uc", "P0021"), + &ExpectedDiagnostic { + headline: "missing `)` to close `for` header", + severity: Severity::Error, + code: Some("P0021"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "expected `)` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0021_02.uc", "P0021"), + &ExpectedDiagnostic { + headline: "missing `)` to close `for` header", + severity: Severity::Error, + code: Some("P0021"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "expected `)` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0021_03.uc", "P0021"), + &ExpectedDiagnostic { + headline: "missing `)` to close `for` header", + severity: Severity::Error, + code: Some("P0021"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "expected `)` before `;`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0021_05.uc", "P0021"), + &ExpectedDiagnostic { + headline: "missing `)` to close `for` header", + severity: Severity::Error, + code: Some("P0021"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(11), + }, + message: "expected `)` before `{`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "`for` header starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0021_09.uc", "P0021"), + &ExpectedDiagnostic { + headline: "missing `)` to close `for` header", + severity: Severity::Error, + code: Some("P0021"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(13), + }, + message: "expected `)` before `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "`for` header starts here", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_for_header_body_recovery_fixtures() { + let runs = run_fixtures(FOR_HEADER_FIXTURES); + + assert_diagnostic( + &runs.get_by_code("files/P0016_01.uc", "P0013"), + &ExpectedDiagnostic { + headline: "expected body for `for`, found end of file", + severity: Severity::Error, + code: Some("P0013"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "reached end of file here", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0016_06.uc", "P0013"), + &ExpectedDiagnostic { + headline: "expected body for `for`, found end of file", + severity: Severity::Error, + code: Some("P0013"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "reached end of file here", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0018_01.uc", "P0013"), + &ExpectedDiagnostic { + headline: "expected body for `for`, found end of file", + severity: Severity::Error, + code: Some("P0013"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(13), + end: TokenPosition(13), + }, + message: "reached end of file here", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0019_01.uc", "P0013"), + &ExpectedDiagnostic { + headline: "expected body for `for`, found end of file", + severity: Severity::Error, + code: Some("P0013"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "reached end of file here", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0020_03.uc", "P0013"), + &ExpectedDiagnostic { + headline: "expected body for `for`, found end of file", + severity: Severity::Error, + code: Some("P0013"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "reached end of file here", + }), + 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 new file mode 100644 index 0000000..c61c0bb --- /dev/null +++ b/rottlib/tests/parser_diagnostics/mod.rs @@ -0,0 +1,125 @@ +use std::collections::HashMap; + +use rottlib::arena::Arena; +use rottlib::diagnostics::{Diagnostic, Severity}; +use rottlib::lexer::{TokenPosition, TokenSpan, TokenizedFile}; +use rottlib::parser::Parser; + +mod control_flow_expressions; +mod primary_expressions; + +#[derive(Debug)] +pub(super) struct ExpectedLabel { + pub span: TokenSpan, + pub message: &'static str, +} + +#[derive(Debug)] +pub(super) struct ExpectedDiagnostic<'a> { + pub headline: &'static str, + pub severity: Severity, + pub code: Option<&'static str>, + pub primary_label: Option, + pub secondary_labels: &'a [ExpectedLabel], + pub help: Option<&'static str>, + pub notes: &'a [&'static str], +} + +#[track_caller] +pub(super) fn assert_diagnostic(actual: &Diagnostic, expected: &ExpectedDiagnostic<'_>) { + assert_eq!(actual.headline(), expected.headline); + assert_eq!(actual.severity(), expected.severity); + assert_eq!(actual.code(), expected.code); + assert_eq!(actual.help(), expected.help); + + match (actual.primary_label(), expected.primary_label.as_ref()) { + (None, None) => {} + (Some(actual), Some(expected)) => { + assert_eq!(actual.span, expected.span); + assert_eq!(actual.message, expected.message); + } + _ => panic!("primary label mismatch"), + } + + let actual_secondary = actual.secondary_labels(); + assert_eq!(actual_secondary.len(), expected.secondary_labels.len()); + + for (actual, expected) in actual_secondary + .iter() + .zip(expected.secondary_labels.iter()) + { + assert_eq!(actual.span, expected.span); + assert_eq!(actual.message, expected.message); + } + + let actual_notes = actual.notes(); + assert_eq!(actual_notes.len(), expected.notes.len()); + + for (actual, expected) in actual_notes.iter().zip(expected.notes.iter()) { + assert_eq!(actual, expected); + } +} + +#[derive(Debug, Clone, Copy)] +pub(super) struct Fixture { + pub label: &'static str, + pub source: &'static str, +} + +pub(super) type FixtureRun = Vec; + +pub(super) struct FixtureRuns { + runs: HashMap<&'static str, FixtureRun>, +} + +impl FixtureRuns { + #[track_caller] + pub fn get(&self, label: &str) -> Option> { + self.runs.get(label).map(|fixture_run| fixture_run.clone()) + } + + #[track_caller] + pub fn get_any(&self, label: &str) -> Diagnostic { + self.runs + .get(label) + .map(|fixture_run| fixture_run[0].clone()) + .unwrap() + } + + #[track_caller] + pub fn get_by_code(&self, label: &str, code: &str) -> Diagnostic { + self.runs + .get(label) + .unwrap_or_else(|| panic!("no fixture run for `{label}`")) + .iter() + .find(|diagnostic| diagnostic.code().as_deref() == Some(code)) + .unwrap_or_else(|| panic!("no `{code}` diagnostic in fixture `{label}`")) + .clone() + } +} + +fn run_fixture(fixture: &'static Fixture) -> FixtureRun { + let arena = Arena::new(); + let file = TokenizedFile::tokenize(fixture.source); + let mut parser = Parser::new(&file, &arena); + + let _ = parser.parse_expression(); + let diagnostics = parser.diagnostics.clone(); + + for diagnostic in &diagnostics { + diagnostic.render(&file, fixture.label); + println!(); + } + + diagnostics +} + +pub(super) fn run_fixtures(fixtures: &'static [Fixture]) -> FixtureRuns { + let mut runs = HashMap::new(); + + for fixture in fixtures { + runs.insert(fixture.label, run_fixture(fixture)); + } + + FixtureRuns { runs } +} diff --git a/rottlib/tests/parser_diagnostics/primary_expressions.rs b/rottlib/tests/parser_diagnostics/primary_expressions.rs new file mode 100644 index 0000000..2d161cb --- /dev/null +++ b/rottlib/tests/parser_diagnostics/primary_expressions.rs @@ -0,0 +1,1572 @@ +use super::*; + +const P0009_NOTES: &[&str] = + &["`new(...)` accepts up to three optional arguments: `outer`, `name`, and `flags`."]; + +pub(super) const P0001_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0001_01.uc", + source: "c && ( /*lol*/ ** calc_it())", + }, + Fixture { + label: "files/P0001_02.uc", + source: "\r\na + (\n//AAA\n//BBB\n//CCC\n//DDD\n//EEE\n//FFF\n ]", + }, + Fixture { + label: "files/P0001_03.uc", + source: "(\n// nothing here, bucko", + }, +]; + +pub(super) const P0002_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0002_01.uc", + source: "a + [", + }, + Fixture { + label: "files/P0002_02.uc", + source: "a * \n//some\n//empty lines\n *", + }, + Fixture { + label: "files/P0002_03.uc", + source: "a &&", + }, + Fixture { + label: "files/P0002_04.uc", + source: "a * * *", + }, +]; + +pub(super) const P0003_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0003_01.uc", + source: "(a + b && c / d ^ e @ f", + }, + Fixture { + label: "files/P0003_02.uc", + source: "(a]", + }, + Fixture { + label: "files/P0003_03.uc", + source: "(a\n;", + }, +]; + +pub(super) const P0004_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0004_01.uc", + source: "class<>", + }, + Fixture { + label: "files/P0004_02.uc", + source: "(class<>)", + }, + Fixture { + label: "files/P0004_03.uc", + source: "new class<\n\n\n\n\n//>WOah there!\r\n >", + }, +]; + +pub(super) const P0005_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0005_01.uc", + source: "class", + }, + Fixture { + label: "files/P0005_02.uc", + source: "class", + }, + Fixture { + label: "files/P0005_04.uc", + source: "class", + }, + Fixture { + label: "files/P0005_05.uc", + source: "class", + }, + Fixture { + label: "files/P0005_06.uc", + source: "new class", + }, + Fixture { + label: "files/P0005_07.uc", + source: "new class", + }, +]; + +pub(super) const P0006_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0006_01.uc", + source: "class<[", + }, + Fixture { + label: "files/P0006_02.uc", + source: "class\n<*>", + }, + Fixture { + label: "files/P0006_03.uc", + source: "class<", + }, + Fixture { + label: "files/P0006_04.uc", + source: "new class<\n// nothing valid here\n ]", + }, +]; + +pub(super) const P0007_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0007_01.uc", + source: "class", + }, +]; + +#[test] +fn check_p0001_fixtures() { + let runs = run_fixtures(P0001_FIXTURES); + + assert_eq!(runs.get("files/P0001_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0001_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0001_03.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0001_01.uc"), + &ExpectedDiagnostic { + headline: "expected expression inside parentheses, found `**`", + severity: Severity::Error, + code: Some("P0001"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "unexpected `**`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0001_02.uc"), + &ExpectedDiagnostic { + headline: "expected expression inside parentheses, found `]`", + severity: Severity::Error, + code: Some("P0001"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(20), + }, + message: "unexpected `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "parenthesized expression starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0001_03.uc"), + &ExpectedDiagnostic { + headline: "expected expression, found end of file", + severity: Severity::Error, + code: Some("P0001"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(3), + }, + message: "reached end of file here", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "parenthesized expression starts here", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0002_fixtures() { + let runs = run_fixtures(P0002_FIXTURES); + + assert_eq!(runs.get("files/P0002_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0002_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0002_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0002_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0002_01.uc"), + &ExpectedDiagnostic { + headline: "expected expression after `+`, found `[`", + severity: Severity::Error, + code: Some("P0002"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "unexpected `[`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0002_02.uc"), + &ExpectedDiagnostic { + headline: "expected expression after `*`, found `*`", + severity: Severity::Error, + code: Some("P0002"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(10), + end: TokenPosition(10), + }, + message: "unexpected `*`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "after this `*`, an expression was expected", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0002_03.uc"), + &ExpectedDiagnostic { + headline: "expected expression after `&&`, found end of file", + severity: Severity::Error, + code: Some("P0002"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "reached end of file here", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0002_04.uc"), + &ExpectedDiagnostic { + headline: "expected expression after `*`, found `*`", + severity: Severity::Error, + code: Some("P0002"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "unexpected `*`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0003_fixtures() { + let runs = run_fixtures(P0003_FIXTURES); + + assert_eq!(runs.get("files/P0003_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0003_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0003_03.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0003_01.uc"), + &ExpectedDiagnostic { + headline: "missing `)` to close parenthesized expression", + severity: Severity::Error, + code: Some("P0003"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(22), + end: TokenPosition(22), + }, + message: "expected `)` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0003_02.uc"), + &ExpectedDiagnostic { + headline: "missing `)` to close parenthesized expression", + severity: Severity::Error, + code: Some("P0003"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "expected `)` before `]`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0003_03.uc"), + &ExpectedDiagnostic { + headline: "missing `)` to close parenthesized expression", + severity: Severity::Error, + code: Some("P0003"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(3), + }, + message: "expected `)` before `;`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "parenthesized expression starts here", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0004_fixtures() { + let runs = run_fixtures(P0004_FIXTURES); + + assert_eq!(runs.get("files/P0004_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0004_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0004_03.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0004_01.uc"), + &ExpectedDiagnostic { + headline: "missing type argument in `class<...>`", + severity: Severity::Error, + code: Some("P0004"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "expected a type name before `>`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0004_02.uc"), + &ExpectedDiagnostic { + headline: "missing type argument in `class<...>`", + severity: Severity::Error, + code: Some("P0004"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "expected a type name before `>`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0004_03.uc"), + &ExpectedDiagnostic { + headline: "missing type argument in `class<...>`", + severity: Severity::Error, + code: Some("P0004"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(12), + }, + message: "expected a type name before `>`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "type argument starts here", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0005_fixtures() { + let runs = run_fixtures(P0005_FIXTURES); + + assert_eq!(runs.get("files/P0005_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0005_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0005_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0005_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0005_05.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0005_06.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0005_07.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0005_01.uc"), + &ExpectedDiagnostic { + headline: "expected another type segment after `.`, found `>`", + severity: Severity::Error, + code: Some("P0005"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "unexpected `>`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0005_02.uc"), + &ExpectedDiagnostic { + headline: "expected another type segment after `.`, found end of file", + severity: Severity::Error, + code: Some("P0005"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "reached end of file here", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0005_03.uc"), + &ExpectedDiagnostic { + headline: "expected another type segment after `.`, found `.`", + severity: Severity::Error, + code: Some("P0005"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "unexpected `.`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0005_04.uc"), + &ExpectedDiagnostic { + headline: "expected another type segment after `.`, found `*`", + severity: Severity::Error, + code: Some("P0005"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "unexpected `*`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0005_05.uc"), + &ExpectedDiagnostic { + headline: "expected another type segment after `.`, found `>`", + severity: Severity::Error, + code: Some("P0005"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(6), + }, + message: "unexpected `>`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0005_06.uc"), + &ExpectedDiagnostic { + headline: "expected another type segment after `.`, found `>`", + severity: Severity::Error, + code: Some("P0005"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(14), + }, + message: "unexpected `>`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "after this `.`, another type segment was expected", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "while parsing this `class<...>` type expression", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0005_07.uc"), + &ExpectedDiagnostic { + headline: "expected another type segment after `.`, found `>`", + severity: Severity::Error, + code: Some("P0005"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(10), + }, + message: "unexpected `>`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "after this `.`, another type segment was expected", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0006_fixtures() { + let runs = run_fixtures(P0006_FIXTURES); + + assert_eq!(runs.get("files/P0006_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0006_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0006_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0006_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0006_01.uc"), + &ExpectedDiagnostic { + headline: "expected a type argument after `<` in `class<...>`, found `[`", + severity: Severity::Error, + code: Some("P0006"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "unexpected `[`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0006_02.uc"), + &ExpectedDiagnostic { + headline: "expected a type argument after `<` in `class<...>`, found `*`", + severity: Severity::Error, + code: Some("P0006"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "unexpected `*`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "while parsing this `class<...>` type expression", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0006_03.uc"), + &ExpectedDiagnostic { + headline: "expected a type argument after `<` in `class<...>`, found end of file", + severity: Severity::Error, + code: Some("P0006"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "reached end of file here", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0006_04.uc"), + &ExpectedDiagnostic { + headline: "expected a type argument after `<` in `class<...>`, found `]`", + severity: Severity::Error, + code: Some("P0006"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "unexpected `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "type argument starts here", + }], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0007_fixtures() { + let runs = run_fixtures(P0007_FIXTURES); + + assert_eq!(runs.get("files/P0007_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0007_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0007_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0007_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0007_01.uc"), + &ExpectedDiagnostic { + headline: "missing `>` to close `class<...>`", + severity: Severity::Error, + code: Some("P0007"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "expected `>` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0007_02.uc"), + &ExpectedDiagnostic { + headline: "missing `>` to close `class<...>`", + severity: Severity::Error, + code: Some("P0007"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(6), + }, + message: "expected `>` before `]`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(1), + end: TokenPosition(1), + }, + message: "type argument starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0007_03.uc"), + &ExpectedDiagnostic { + headline: "missing `>` to close `class<...>`", + severity: Severity::Error, + code: Some("P0007"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "expected `>` before `;`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(3), + end: TokenPosition(3), + }, + message: "type argument starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0007_04.uc"), + &ExpectedDiagnostic { + headline: "missing `>` to close `class<...>`", + severity: Severity::Error, + code: Some("P0007"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(13), + end: TokenPosition(13), + }, + message: "expected `>` before end of file", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "type argument starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "while parsing this `class<...>` type expression", + }, + ], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0008_fixtures() { + let runs = run_fixtures(P0008_FIXTURES); + + assert_eq!(runs.get("files/P0008_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0008_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0008_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0008_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0008_01.uc"), + &ExpectedDiagnostic { + headline: "expected class expression after `new`, found end of file", + severity: Severity::Error, + code: Some("P0008"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(1), + end: TokenPosition(1), + }, + message: "reached end of file here", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0008_02.uc"), + &ExpectedDiagnostic { + headline: "expected class expression after `new`, found `;`", + severity: Severity::Error, + code: Some("P0008"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "unexpected `;`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`new` expression starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0008_03.uc"), + &ExpectedDiagnostic { + headline: "expected class expression after `new(...)`, found `]`", + severity: Severity::Error, + code: Some("P0008"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(14), + end: TokenPosition(16), + }, + message: "unexpected `]`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`new` expression starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(14), + end: TokenPosition(14), + }, + message: "optional `new(...)` arguments end here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0008_04.uc"), + &ExpectedDiagnostic { + headline: "expected class expression after `new(...)`, found end of file", + severity: Severity::Error, + code: Some("P0008"), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`new` expression starts here", + }], + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(14), + end: TokenPosition(14), + }, + message: "reached end of file here", + }), + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0009_fixtures() { + let runs = run_fixtures(P0009_FIXTURES); + + assert_eq!(runs.get("files/P0009_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0009_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0009_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0009_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0009_05.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0009_06.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0009_07.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0009_08.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0009_09.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0009_01.uc"), + &ExpectedDiagnostic { + headline: "too many arguments in `new(...)`", + severity: Severity::Error, + code: Some("P0009"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(11), + end: TokenPosition(11), + }, + message: "a fourth argument is not allowed in `new(...)`", + }), + secondary_labels: &[], + help: None, + notes: P0009_NOTES, + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0009_02.uc"), + &ExpectedDiagnostic { + headline: "too many arguments in `new(...)`", + severity: Severity::Error, + code: Some("P0009"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(16), + }, + message: "a fourth argument is not allowed in `new(...)`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(1), + end: TokenPosition(1), + }, + message: "`new(...)` argument list starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "the third allowed argument ends here", + }, + ], + help: None, + notes: P0009_NOTES, + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0009_03.uc"), + &ExpectedDiagnostic { + headline: "too many arguments in `new(...)`", + severity: Severity::Error, + code: Some("P0009"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(19), + }, + message: "a fourth argument is not allowed in `new(...)`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(1), + end: TokenPosition(1), + }, + message: "`new(...)` argument list starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "the third allowed argument ends here", + }, + ], + help: None, + notes: P0009_NOTES, + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0009_04.uc"), + &ExpectedDiagnostic { + headline: "too many arguments in `new(...)`", + severity: Severity::Error, + code: Some("P0009"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "this `,` starts a fourth argument, which is not allowed here", + }), + secondary_labels: &[], + help: None, + notes: P0009_NOTES, + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0009_05.uc"), + &ExpectedDiagnostic { + headline: "too many arguments in `new(...)`", + severity: Severity::Error, + code: Some("P0009"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(13), + end: TokenPosition(17), + }, + message: "a fourth argument is not allowed in `new(...)`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`new` expression starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "`new(...)` argument list starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(13), + end: TokenPosition(13), + }, + message: "the third allowed argument ends here", + }, + ], + help: None, + notes: P0009_NOTES, + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0009_06.uc"), + &ExpectedDiagnostic { + headline: "too many arguments in `new(...)`", + severity: Severity::Error, + code: Some("P0009"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(10), + end: TokenPosition(10), + }, + message: "a fourth argument is not allowed in `new(...)`", + }), + secondary_labels: &[], + help: None, + notes: P0009_NOTES, + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0009_07.uc"), + &ExpectedDiagnostic { + headline: "too many arguments in `new(...)`", + severity: Severity::Error, + code: Some("P0009"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(10), + end: TokenPosition(10), + }, + message: "a fourth argument is not allowed in `new(...)`", + }), + secondary_labels: &[], + help: None, + notes: P0009_NOTES, + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0009_08.uc"), + &ExpectedDiagnostic { + headline: "too many arguments in `new(...)`", + severity: Severity::Error, + code: Some("P0009"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(11), + end: TokenPosition(11), + }, + message: "a fourth argument is not allowed in `new(...)`", + }), + secondary_labels: &[], + help: None, + notes: P0009_NOTES, + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0009_09.uc"), + &ExpectedDiagnostic { + headline: "too many arguments in `new(...)`", + severity: Severity::Error, + code: Some("P0009"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(11), + end: TokenPosition(16), + }, + message: "a fourth argument is not allowed in `new(...)`", + }), + secondary_labels: &[], + help: None, + notes: P0009_NOTES, + }, + ); +} + +#[test] +fn check_p0010_fixtures() { + let runs = run_fixtures(P0010_FIXTURES); + + assert_eq!(runs.get("files/P0010_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0010_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0010_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0010_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0010_05.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0010_01.uc"), + &ExpectedDiagnostic { + headline: "missing `)` to close `new(...)` argument list", + severity: Severity::Error, + code: Some("P0010"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(10), + end: TokenPosition(10), + }, + message: "expected `)` before `SomeClass`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0010_02.uc"), + &ExpectedDiagnostic { + headline: "missing `)` to close `new(...)` argument list", + severity: Severity::Error, + code: Some("P0010"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(19), + end: TokenPosition(19), + }, + message: "expected `)` before `SomeClass`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`new` expression starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "`new(...)` argument list starts here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0010_03.uc"), + &ExpectedDiagnostic { + headline: "missing `)` to close `new(...)` argument list", + severity: Severity::Error, + code: Some("P0010"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(20), + end: TokenPosition(20), + }, + message: "expected `)` before `class`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(1), + end: TokenPosition(1), + }, + message: "`new(...)` argument list starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0010_04.uc"), + &ExpectedDiagnostic { + headline: "missing `)` to close `new(...)` argument list", + severity: Severity::Error, + code: Some("P0010"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(20), + end: TokenPosition(20), + }, + message: "expected `)` before end of file", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(1), + end: TokenPosition(1), + }, + message: "`new(...)` argument list starts here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0010_05.uc"), + &ExpectedDiagnostic { + headline: "missing `)` to close `new(...)` argument list", + severity: Severity::Error, + code: Some("P0010"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(13), + end: TokenPosition(13), + }, + message: "expected `)` before `]`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`new` expression starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "`new(...)` argument list starts here", + }, + ], + help: None, + notes: &[], + }, + ); +} + +#[test] +fn check_p0011_fixtures() { + let runs = run_fixtures(P0011_FIXTURES); + + assert_eq!(runs.get("files/P0011_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0011_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0011_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0011_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0011_01.uc"), + &ExpectedDiagnostic { + headline: "missing `,` between `new(...)` arguments", + severity: Severity::Error, + code: Some("P0011"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(4), + end: TokenPosition(4), + }, + message: "expected `,` before `Name`", + }), + secondary_labels: &[ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "previous argument ends here", + }], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0011_02.uc"), + &ExpectedDiagnostic { + headline: "missing `,` between `new(...)` arguments", + severity: Severity::Error, + code: Some("P0011"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(10), + end: TokenPosition(10), + }, + message: "expected `,` before `7`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(1), + end: TokenPosition(1), + }, + message: "`new(...)` argument list starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "previous argument ends here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0011_03.uc"), + &ExpectedDiagnostic { + headline: "missing `,` between `new(...)` arguments", + severity: Severity::Error, + code: Some("P0011"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(13), + end: TokenPosition(13), + }, + message: "expected `,` before `Name`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`new` expression starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(2), + end: TokenPosition(2), + }, + message: "`new(...)` argument list starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(10), + }, + message: "previous argument ends here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0011_04.uc"), + &ExpectedDiagnostic { + headline: "missing `,` between `new(...)` arguments", + severity: Severity::Error, + code: Some("P0011"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(17), + end: TokenPosition(17), + }, + message: "expected `,` before `SomeFlags`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(1), + end: TokenPosition(1), + }, + message: "`new(...)` argument list starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(14), + }, + message: "previous argument ends here", + }, + ], + help: None, + notes: &[], + }, + ); +} diff --git a/rottlib/tests/fixtures_tokenization.rs b/rottlib/tests/tokenization.rs similarity index 100% rename from rottlib/tests/fixtures_tokenization.rs rename to rottlib/tests/tokenization.rs