Implent better diagnostics for control flow and primaries
This commit is contained in:
parent
8632ba0a86
commit
519d0cd3a7
@ -20,7 +20,7 @@ codegen-units = 1 # Reduce number of codegen units to increase optimizations
|
|||||||
debug = false # strip all debug info
|
debug = false # strip all debug info
|
||||||
|
|
||||||
[profile.flamegraph]
|
[profile.flamegraph]
|
||||||
inherits = "release" # start from release
|
inherits = "dev" # start from release
|
||||||
strip = false
|
strip = false
|
||||||
debug = true # full DWARF info for unwinding
|
debug = true # full DWARF info for unwinding
|
||||||
split-debuginfo = "unpacked" # keep symbols inside the binary
|
split-debuginfo = "unpacked" # keep symbols inside the binary
|
||||||
|
|||||||
@ -8,7 +8,6 @@ pub fn render_diagnostic(
|
|||||||
_file: &TokenizedFile,
|
_file: &TokenizedFile,
|
||||||
file_name: Option<&str>,
|
file_name: Option<&str>,
|
||||||
colors: bool,
|
colors: bool,
|
||||||
) -> String {
|
) {
|
||||||
diag.render(_file, file_name.unwrap_or("<default>"));
|
diag.render(_file, file_name.unwrap_or("<default>"));
|
||||||
"fuck it".to_string()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -308,7 +308,6 @@ fn main() {
|
|||||||
// first window
|
// first window
|
||||||
for (k, d) in diags.iter().take(first_n).enumerate() {
|
for (k, d) in diags.iter().take(first_n).enumerate() {
|
||||||
let s = pretty::render_diagnostic(d, tf, Some(&fname), use_colors);
|
let s = pretty::render_diagnostic(d, tf, Some(&fname), use_colors);
|
||||||
eprintln!("{s}");
|
|
||||||
if ALSO_PRINT_DEBUG_AFTER_PRETTY {
|
if ALSO_PRINT_DEBUG_AFTER_PRETTY {
|
||||||
eprintln!("#{}: {:#?}", k + 1, d);
|
eprintln!("#{}: {:#?}", k + 1, d);
|
||||||
}
|
}
|
||||||
@ -319,7 +318,6 @@ fn main() {
|
|||||||
for (offset, d) in diags.iter().skip(start).enumerate() {
|
for (offset, d) in diags.iter().skip(start).enumerate() {
|
||||||
let idx_global = start + offset + 1;
|
let idx_global = start + offset + 1;
|
||||||
let s = pretty::render_diagnostic(d, tf, Some(&fname), use_colors);
|
let s = pretty::render_diagnostic(d, tf, Some(&fname), use_colors);
|
||||||
eprintln!("{s}");
|
|
||||||
if ALSO_PRINT_DEBUG_AFTER_PRETTY {
|
if ALSO_PRINT_DEBUG_AFTER_PRETTY {
|
||||||
eprintln!("#{idx_global}: {d:#?}");
|
eprintln!("#{idx_global}: {d:#?}");
|
||||||
}
|
}
|
||||||
@ -327,7 +325,6 @@ fn main() {
|
|||||||
} else {
|
} else {
|
||||||
for (k, d) in diags.iter().enumerate() {
|
for (k, d) in diags.iter().enumerate() {
|
||||||
let s = pretty::render_diagnostic(d, tf, Some(&fname), use_colors);
|
let s = pretty::render_diagnostic(d, tf, Some(&fname), use_colors);
|
||||||
eprintln!("{s}");
|
|
||||||
if ALSO_PRINT_DEBUG_AFTER_PRETTY {
|
if ALSO_PRINT_DEBUG_AFTER_PRETTY {
|
||||||
eprintln!("#{}: {:#?}", k + 1, d);
|
eprintln!("#{}: {:#?}", k + 1, d);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,14 +14,138 @@ mod pretty;
|
|||||||
|
|
||||||
// a * * *
|
// a * * *
|
||||||
|
|
||||||
|
/// Expressions to test.
|
||||||
|
///
|
||||||
|
/// Add, remove, or edit entries here.
|
||||||
|
/// Using `(&str, &str)` gives each case a human-readable label.
|
||||||
/// Expressions to test.
|
/// Expressions to test.
|
||||||
///
|
///
|
||||||
/// Add, remove, or edit entries here.
|
/// Add, remove, or edit entries here.
|
||||||
/// Using `(&str, &str)` gives each case a human-readable label.
|
/// Using `(&str, &str)` gives each case a human-readable label.
|
||||||
const TEST_CASES: &[(&str, &str)] = &[
|
const TEST_CASES: &[(&str, &str)] = &[
|
||||||
("files/P0003_01.uc", "(a + b && c / d ^ e @ f"),
|
// P0016: invalid initializer start after `for (`
|
||||||
("files/P0003_02.uc", "(a]"),
|
(
|
||||||
("files/P0003_03.uc", "(a\n;"),
|
"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.
|
/// If true, print the parsed expression using Debug formatting.
|
||||||
@ -33,24 +157,14 @@ const ALWAYS_PRINT_DIAGNOSTICS: bool = true;
|
|||||||
fn main() {
|
fn main() {
|
||||||
let arena = Arena::new();
|
let arena = Arena::new();
|
||||||
|
|
||||||
println!("Running {} expression test case(s)...", TEST_CASES.len());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
let mut had_any_problem = false;
|
let mut had_any_problem = false;
|
||||||
|
|
||||||
for (idx, (label, source)) in TEST_CASES.iter().enumerate() {
|
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 tf = TokenizedFile::tokenize(source);
|
||||||
|
|
||||||
let mut parser = Parser::new(&tf, &arena);
|
let mut parser = Parser::new(&tf, &arena);
|
||||||
let expr = parser.parse_expression();
|
let expr = parser.parse_expression();
|
||||||
|
|
||||||
println!("parse_expression() returned.");
|
|
||||||
|
|
||||||
if PRINT_PARSED_EXPR {
|
if PRINT_PARSED_EXPR {
|
||||||
println!("Parsed expression:");
|
println!("Parsed expression:");
|
||||||
println!("{expr:#?}");
|
println!("{expr:#?}");
|
||||||
@ -60,14 +174,10 @@ fn main() {
|
|||||||
println!("Diagnostics: none");
|
println!("Diagnostics: none");
|
||||||
} else {
|
} else {
|
||||||
had_any_problem = true;
|
had_any_problem = true;
|
||||||
println!("Diagnostics: {}", parser.diagnostics.len());
|
|
||||||
|
|
||||||
if ALWAYS_PRINT_DIAGNOSTICS {
|
if ALWAYS_PRINT_DIAGNOSTICS {
|
||||||
let use_colors = false;
|
let use_colors = false;
|
||||||
for (k, diag) in parser.diagnostics.iter().enumerate() {
|
for (k, diag) in parser.diagnostics.iter().enumerate() {
|
||||||
let rendered = pretty::render_diagnostic(diag, &tf, Some(label), use_colors);
|
pretty::render_diagnostic(diag, &tf, Some(label), use_colors);
|
||||||
println!("Diagnostic #{}:", k + 1);
|
|
||||||
println!("{rendered}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
perf.data.old
BIN
perf.data.old
Binary file not shown.
@ -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<Pawn>`.")
|
|
||||||
.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()
|
|
||||||
}
|
|
||||||
@ -4,11 +4,11 @@
|
|||||||
//! parsing or doing lightweight frontend checks. They are intentionally small,
|
//! parsing or doing lightweight frontend checks. They are intentionally small,
|
||||||
//! depend only on [`AstSpan`], and are easy to construct and store.
|
//! depend only on [`AstSpan`], and are easy to construct and store.
|
||||||
|
|
||||||
mod expression_diagnostics;
|
mod parse_error_diagnostics;
|
||||||
mod render;
|
mod render;
|
||||||
|
|
||||||
use crate::lexer::TokenSpan;
|
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.
|
/// Classification of a diagnostic by its impact.
|
||||||
///
|
///
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
143
rottlib/src/diagnostics/parse_error_diagnostics/mod.rs
Normal file
143
rottlib/src/diagnostics/parse_error_diagnostics/mod.rs
Normal file
@ -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<TokenSpan>,
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
@ -1,15 +1,13 @@
|
|||||||
use crate::diagnostics::{self, Diagnostic, Severity};
|
use crate::diagnostics::{Diagnostic, Severity};
|
||||||
use crate::lexer::{TokenSpan, TokenizedFile};
|
use crate::lexer::{TokenSpan, TokenizedFile};
|
||||||
|
|
||||||
use core::convert::Into;
|
use core::convert::Into;
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
use crossterm::terminal::disable_raw_mode;
|
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
const INDENT: &str = " ";
|
const INDENT: &str = " ";
|
||||||
const MAX_LINES_LIMIT: usize = 10;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
error: expected one of `,`, `:`, or `}`, found `token_to`
|
error: expected one of `,`, `:`, or `}`, found `token_to`
|
||||||
@ -77,7 +75,7 @@ struct RangeSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RangeSet {
|
impl RangeSet {
|
||||||
fn get(&self, index: usize) -> Option<&RangeInclusive<usize>> {
|
/*fn get(&self, index: usize) -> Option<&RangeInclusive<usize>> {
|
||||||
if self.primary_range.is_some() {
|
if self.primary_range.is_some() {
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
return self.primary_range.as_ref();
|
return self.primary_range.as_ref();
|
||||||
@ -91,7 +89,7 @@ impl RangeSet {
|
|||||||
|
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
self.secondary_ranges.len() + if self.primary_range.is_some() { 1 } else { 0 }
|
self.secondary_ranges.len() + if self.primary_range.is_some() { 1 } else { 0 }
|
||||||
}
|
}*/
|
||||||
|
|
||||||
fn iter(&self) -> impl Iterator<Item = &RangeInclusive<usize>> {
|
fn iter(&self) -> impl Iterator<Item = &RangeInclusive<usize>> {
|
||||||
self.primary_range
|
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<RangeInclusive<usize>> {
|
fn span_to_range<'src>(
|
||||||
|
span: TokenSpan,
|
||||||
|
file: &TokenizedFile<'src>,
|
||||||
|
) -> Option<RangeInclusive<usize>> {
|
||||||
let start_line = file.token_line(span.start)?;
|
let start_line = file.token_line(span.start)?;
|
||||||
let end_line = file.token_line(span.end)?;
|
let end_line = file.token_line(span.end)?;
|
||||||
|
|
||||||
@ -264,6 +265,7 @@ impl Diagnostic {
|
|||||||
self.render_header();
|
self.render_header();
|
||||||
println!("{INDENT}{}: {}", "in file".blue().bold(), file_path.into());
|
println!("{INDENT}{}: {}", "in file".blue().bold(), file_path.into());
|
||||||
self.render_lines(file);
|
self.render_lines(file);
|
||||||
|
self.render_help_and_notes(file);
|
||||||
}
|
}
|
||||||
/*StartRange {
|
/*StartRange {
|
||||||
label_type: LabelType,
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -336,6 +336,76 @@ impl Token {
|
|||||||
| Keyword::Exec
|
| 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`.
|
/// Reserved words of Fermented `UnrealScript`.
|
||||||
@ -557,4 +627,11 @@ impl Keyword {
|
|||||||
| Self::Delegate
|
| 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
//! [`TriviaIndexBuilder`]. Significant tokens exclude whitespace and comments;
|
//! [`TriviaIndexBuilder`]. Significant tokens exclude whitespace and comments;
|
||||||
//! see [`parser::TriviaKind`].
|
//! see [`parser::TriviaKind`].
|
||||||
|
|
||||||
|
// TODO: need a refactor pass
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
// TODO: NO RETURNING EOF
|
// TODO: NO RETURNING EOF
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -287,6 +289,18 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
self.eat(Token::Keyword(keyword))
|
self.eat(Token::Keyword(keyword))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn eat_with_position(&mut self, token: Token) -> Option<TokenPosition> {
|
||||||
|
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.
|
/// Expects `expected` token as the next significant one.
|
||||||
///
|
///
|
||||||
/// On match consumes the token and returns its [`TokenPosition`].
|
/// On match consumes the token and returns its [`TokenPosition`].
|
||||||
@ -365,6 +379,9 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
if peeked_position <= old_position {
|
if peeked_position <= old_position {
|
||||||
self.advance();
|
self.advance();
|
||||||
}
|
}
|
||||||
|
if self.file.is_eof(&old_position) {
|
||||||
|
panic!("parsing stuck at the eof");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,55 +22,46 @@ pub enum ParseErrorKind {
|
|||||||
ExpressionExpected,
|
ExpressionExpected,
|
||||||
/// P0003
|
/// P0003
|
||||||
ParenthesizedExpressionMissingClosingParenthesis,
|
ParenthesizedExpressionMissingClosingParenthesis,
|
||||||
// headline: missing type argument in \class<...>``
|
/// P0004
|
||||||
// primary label on > or insertion site: expected a type name here
|
ClassTypeMissingTypeArgument,
|
||||||
// secondary label on < or on class: type argument list starts here
|
/// P0005
|
||||||
// help: Write a type name, for example \class<Pawn>`.`
|
ClassTypeExpectedQualifiedTypeName,
|
||||||
ClassTypeMissingTypeArgument {
|
/// P0006
|
||||||
left_angle_bracket_position: TokenPosition,
|
ClassTypeInvalidStart,
|
||||||
},
|
/// P0007
|
||||||
// headline: missing closing \>` in `class<...>``
|
ClassTypeMissingClosingAngleBracket,
|
||||||
// primary label on offending following token or EOF: expected \>` before this token` or at EOF: expected \>` here`
|
/// P0008
|
||||||
// secondary label on <: this \<` starts the type argument`
|
NewMissingClassSpecifier,
|
||||||
// help: Add \>` to close the class type expression.`
|
/// P0009
|
||||||
ClassTypeMissingClosingAngleBracket {
|
NewTooManyArguments,
|
||||||
left_angle_bracket_position: TokenPosition,
|
/// P0010
|
||||||
},
|
NewMissingClosingParenthesis,
|
||||||
// headline: invalid type argument in \class<...>``
|
/// P0011
|
||||||
// primary label on the bad token inside the angle brackets: expected a qualified type name here
|
NewArgumentMissingComma,
|
||||||
// secondary label on class or <: while parsing this class type expression
|
/// P0012
|
||||||
// note: Only a type name is accepted between \<` and `>` here.`
|
ConditionExpected,
|
||||||
ClassTypeInvalidTypeArgument {
|
/// P0013
|
||||||
left_angle_bracket_position: TokenPosition,
|
ControlFlowBodyExpected,
|
||||||
},
|
/// P0014
|
||||||
// headline: too many arguments in \new(...)``
|
DoMissingUntil,
|
||||||
// primary label on the fourth argument, or on the comma before it if that is easier: unexpected extra argument
|
/// P0015
|
||||||
// secondary label on the opening (: this argument list accepts at most three arguments
|
ForEachIteratorExpressionExpected,
|
||||||
// note: The three slots are \outer`, `name`, and `flags`.`
|
/// P0016
|
||||||
// help: Remove the extra argument.
|
ForLoopHeaderInitializerInvalidStart,
|
||||||
NewTooManyArguments {
|
/// P0017
|
||||||
left_parenthesis_position: TokenPosition,
|
ForLoopHeaderMissingSemicolonAfterInitializer,
|
||||||
},
|
/// P0018
|
||||||
// headline: missing closing \)' in `new(...)``
|
ForLoopHeaderConditionInvalidStart,
|
||||||
// primary label: expected \)' here`
|
/// P0019
|
||||||
// secondary label on the opening (: this argument list starts here
|
ForLoopHeaderMissingSemicolonAfterCondition,
|
||||||
// help: Add \)' to close the argument list.`
|
/// P0020
|
||||||
NewMissingClosingParenthesis {
|
ForLoopHeaderStepInvalidStart,
|
||||||
left_parenthesis_position: TokenPosition,
|
/// P0021
|
||||||
},
|
ForLoopHeaderMissingClosingParenthesis,
|
||||||
// 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,
|
|
||||||
},
|
|
||||||
// ================== Old errors to be thrown away! ==================
|
// ================== Old errors to be thrown away! ==================
|
||||||
/// Expression inside `(...)` could not be parsed and no closing `)`
|
/// Expression inside `(...)` could not be parsed and no closing `)`
|
||||||
/// was found.
|
/// was found.
|
||||||
FunctionCallMissingClosingParenthesis,
|
FunctionCallMissingClosingParenthesis,
|
||||||
/// A `do` block was not followed by a matching `until`.
|
|
||||||
DoMissingUntil,
|
|
||||||
/// Found an unexpected token while parsing an expression.
|
/// Found an unexpected token while parsing an expression.
|
||||||
ExpressionUnexpectedToken,
|
ExpressionUnexpectedToken,
|
||||||
DeclEmptyVariableDeclarations,
|
DeclEmptyVariableDeclarations,
|
||||||
@ -86,14 +77,6 @@ pub enum ParseErrorKind {
|
|||||||
|
|
||||||
TypeSpecClassMissingInnerType,
|
TypeSpecClassMissingInnerType,
|
||||||
TypeSpecClassMissingClosingAngle,
|
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 `;`.
|
/// An expression inside a block is not terminated with `;`.
|
||||||
BlockMissingSemicolonAfterExpression,
|
BlockMissingSemicolonAfterExpression,
|
||||||
/// A statement inside a block is not terminated with `;`.
|
/// A statement inside a block is not terminated with `;`.
|
||||||
@ -308,7 +291,6 @@ pub enum ParseErrorKind {
|
|||||||
FunctionArgumentMissingComma,
|
FunctionArgumentMissingComma,
|
||||||
// Expression was required, but none started
|
// Expression was required, but none started
|
||||||
MissingExpression,
|
MissingExpression,
|
||||||
MissingBranchBody,
|
|
||||||
CallableExpectedHeader,
|
CallableExpectedHeader,
|
||||||
CallableExpectedKind,
|
CallableExpectedKind,
|
||||||
CallableOperatorInvalidPrecedence,
|
CallableOperatorInvalidPrecedence,
|
||||||
@ -339,7 +321,7 @@ pub struct ParseError {
|
|||||||
pub type ParseResult<'src, 'arena, T> = Result<T, ParseError>;
|
pub type ParseResult<'src, 'arena, T> = Result<T, ParseError>;
|
||||||
|
|
||||||
impl crate::parser::Parser<'_, '_> {
|
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())
|
self.make_error_at(error_kind, self.last_consumed_position_or_start())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -101,7 +101,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let Some((token, modifier_position)) = self.peek_token_and_position() else {
|
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;
|
let mut consumed_inside_match = false;
|
||||||
@ -279,7 +279,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
|||||||
self.advance();
|
self.advance();
|
||||||
Reliability::Unreliable
|
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) {
|
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());
|
.unwrap_or_else(|| self.last_consumed_position_or_start());
|
||||||
|
|
||||||
if self.peek_token().is_none() {
|
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() {
|
match self.parse_replication_rule() {
|
||||||
@ -679,12 +679,12 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
|||||||
Ok(i128::MIN)
|
Ok(i128::MIN)
|
||||||
} else {
|
} else {
|
||||||
let magnitude_as_i128 = i128::try_from(magnitude)
|
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)
|
Ok(-magnitude_as_i128)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
i128::try_from(magnitude)
|
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;
|
use ParseErrorKind::InvalidNumericLiteral;
|
||||||
|
|
||||||
if body.is_empty() {
|
if body.is_empty() {
|
||||||
return Err(self.make_error_here(InvalidNumericLiteral));
|
return Err(self.make_error_at_last_consumed(InvalidNumericLiteral));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (base, digits) =
|
let (base, digits) =
|
||||||
@ -707,7 +707,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if digits.is_empty() {
|
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;
|
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),
|
'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)),
|
||||||
'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 {
|
if digit_value >= base {
|
||||||
return Err(self.make_error_here(InvalidNumericLiteral));
|
return Err(self.make_error_at_last_consumed(InvalidNumericLiteral));
|
||||||
}
|
}
|
||||||
accumulator = accumulator
|
accumulator = accumulator
|
||||||
.checked_mul(base)
|
.checked_mul(base)
|
||||||
.and_then(|value| value.checked_add(digit_value))
|
.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)
|
Ok(accumulator)
|
||||||
@ -767,7 +767,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(
|
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) {
|
if !matches!(next_token, Token::NameLiteral) {
|
||||||
return Err(
|
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];
|
let inner = &next_lexeme[1..next_lexeme.len() - 1];
|
||||||
@ -827,7 +827,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
|||||||
self.advance();
|
self.advance();
|
||||||
DeclarationLiteral::Identifier(lexeme)
|
DeclarationLiteral::Identifier(lexeme)
|
||||||
}
|
}
|
||||||
_ => return Err(self.make_error_here(ParseErrorKind::ExpressionUnexpectedToken)),
|
_ => return Err(self.make_error_at_last_consumed(ParseErrorKind::ExpressionUnexpectedToken)),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(DeclarationLiteralRef {
|
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)
|
.sync_error_until(self, SyncLevel::ListSeparator)
|
||||||
.report_error(self);
|
.report_error(self);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -86,7 +86,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
while self.peek_token() == Some(Token::Comma) {
|
while self.peek_token() == Some(Token::Comma) {
|
||||||
self.advance();
|
self.advance();
|
||||||
}
|
}
|
||||||
self.make_error_here(ParseErrorKind::EnumEmptyVariants)
|
self.make_error_at_last_consumed(ParseErrorKind::EnumEmptyVariants)
|
||||||
.widen_error_span_from(error_start_position)
|
.widen_error_span_from(error_start_position)
|
||||||
.report_error(self);
|
.report_error(self);
|
||||||
if matches!(self.peek_token(), Some(Token::RightBrace) | None) {
|
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
|
// If we don't even get a good identifier - error is different
|
||||||
return ControlFlow::Break(());
|
return ControlFlow::Break(());
|
||||||
};
|
};
|
||||||
self.make_error_here(ParseErrorKind::EnumNoSeparatorBetweenVariants)
|
self.make_error_at_last_consumed(ParseErrorKind::EnumNoSeparatorBetweenVariants)
|
||||||
.widen_error_span_from(error_start_position)
|
.widen_error_span_from(error_start_position)
|
||||||
.report_error(self);
|
.report_error(self);
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
.arena
|
.arena
|
||||||
.alloc_node(TypeSpecifier::Named(type_name), full_span))
|
.alloc_node(TypeSpecifier::Named(type_name), full_span))
|
||||||
}
|
}
|
||||||
_ => Err(self.make_error_here(ParseErrorKind::TypeSpecExpectedType)),
|
_ => Err(self.make_error_at_last_consumed(ParseErrorKind::TypeSpecExpectedType)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -67,7 +67,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
next_token_position,
|
next_token_position,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
self.make_error_here(ParseErrorKind::VarSpecNotIdentifier)
|
self.make_error_at_last_consumed(ParseErrorKind::VarSpecNotIdentifier)
|
||||||
.sync_error_until(self, SyncLevel::ListSeparator)
|
.sync_error_until(self, SyncLevel::ListSeparator)
|
||||||
.report_error(self);
|
.report_error(self);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,7 +88,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
while self.peek_token() == Some(Token::Comma) {
|
while self.peek_token() == Some(Token::Comma) {
|
||||||
self.advance();
|
self.advance();
|
||||||
}
|
}
|
||||||
self.make_error_here(ParseErrorKind::DeclEmptyVariableDeclarations)
|
self.make_error_at_last_consumed(ParseErrorKind::DeclEmptyVariableDeclarations)
|
||||||
.widen_error_span_from(error_start_position)
|
.widen_error_span_from(error_start_position)
|
||||||
.report_error(self);
|
.report_error(self);
|
||||||
if matches!(self.peek_token(), Some(Token::Semicolon) | None) {
|
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)
|
.sync_error_until(self, SyncLevel::Statement)
|
||||||
.ok_or_report(self)
|
.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)
|
.widen_error_span_from(error_start_position)
|
||||||
.report_error(self);
|
.report_error(self);
|
||||||
declarators.push(parsed_declarator);
|
declarators.push(parsed_declarator);
|
||||||
|
|||||||
@ -1,15 +1,26 @@
|
|||||||
//! Control expression parsing for Fermented `UnrealScript`.
|
//! Control expression parsing for Fermented `UnrealScript`.
|
||||||
//!
|
//!
|
||||||
//! ## Condition parsing and legacy compatibility
|
//! ## Condition boundary recovery and legacy compatibility
|
||||||
//!
|
//!
|
||||||
//! Fermented `UnrealScript` allows omitting parentheses `(...)` around the
|
//! Fermented `UnrealScript` allows omitting parentheses `(...)` around
|
||||||
//! condition expression of `if`/`while`/etc. For compatibility with older
|
//! condition expressions of `if`/`while`/`until` and similar constructs.
|
||||||
//! `UnrealScript` code, we also apply a special rule:
|
//! Conditions are therefore parsed as ordinary expressions by default.
|
||||||
//!
|
//!
|
||||||
//! If a condition starts with `(`, we parse the condition as exactly the
|
//! This means that a leading parenthesized expression may still be part of a
|
||||||
//! matching parenthesized subexpression and stop at its corresponding `)`.
|
//! larger condition:
|
||||||
//! In other words, `( ... )` must cover the whole condition; trailing tokens
|
//!
|
||||||
//! like `* c == d` are not allowed to continue the 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
|
//! This prevents the parser from accidentally consuming the following
|
||||||
//! statement/body as part of the condition in older code such as:
|
//! 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);
|
//! if ( AIController(Controller) != None ) Cross = vect(0,0,0);
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Trade-off: you cannot write `if (a + b) * c == d`;
|
//! Without the legacy cut-off, a permissive expression parser could interpret
|
||||||
//! write `if ((a + b) * c == d)` or `if d == (a + b) * c` instead.
|
//! `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
|
//! ## Disambiguation of `for` as loop vs expression
|
||||||
//!
|
//!
|
||||||
@ -58,37 +79,112 @@
|
|||||||
//! lives in a separate module because the construct itself is more involved
|
//! lives in a separate module because the construct itself is more involved
|
||||||
//! than the control-flow forms handled here.
|
//! 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::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<TokenPosition>,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'src, 'arena> Parser<'src, 'arena> {
|
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.
|
/// Parses a control-flow condition.
|
||||||
///
|
///
|
||||||
/// If the next token is `(`, attempts to consume one parenthesized
|
/// Conditions are parsed as ordinary expressions by default.
|
||||||
/// subexpression and returns it wrapped as [`Expression::Parentheses`].
|
///
|
||||||
/// Otherwise consumes a general expression.
|
/// For legacy compatibility, if the condition starts with a parenthesized
|
||||||
fn parse_condition(&mut self) -> ExpressionRef<'src, 'arena> {
|
/// expression followed by an identifier-like token, the parenthesized
|
||||||
if let Some((Token::LeftParenthesis, left_parenthesis_position)) =
|
/// expression is treated as the complete condition and returned as
|
||||||
self.peek_token_and_position()
|
/// [`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(); // '('
|
Ok(self.parse_parenthesized_expression_tail(left_parenthesis_position))
|
||||||
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,
|
|
||||||
)
|
|
||||||
} else {
|
} 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
|
/// For non-block bodies, this method consumes a trailing `;` when present
|
||||||
/// and records its position in the returned [`BranchBody`].
|
/// 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 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;
|
let end_anchor_token_position = error.covered_span.end;
|
||||||
self.report_error(error);
|
self.report_error(error);
|
||||||
return BranchBody {
|
return BranchBody {
|
||||||
@ -128,16 +237,16 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
return BranchBody {
|
return BranchBody {
|
||||||
expression: None,
|
expression: None,
|
||||||
semicolon_position: None,
|
semicolon_position: None,
|
||||||
// `unwrap` actually triggering is effectively impossible,
|
end_anchor_token_position: self.last_consumed_position_or_start(),
|
||||||
// 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),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
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;
|
let end_anchor_token_position = branch_expression.span().end;
|
||||||
// A block body in `if {...}` or `if {...};` owns its own terminator;
|
// A block body in `if {...}` or `if {...};` owns its own terminator;
|
||||||
// a following `;` does not belong to the branch body.
|
// 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.
|
/// Parses an `if` expression after the `if` keyword.
|
||||||
///
|
///
|
||||||
/// The resulting [`Expression::If`] spans from `if_keyword_position` to the
|
/// The resulting [`Expression::If`] spans from `if_keyword_position` to the
|
||||||
@ -172,12 +301,16 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
if_keyword_position: TokenPosition,
|
if_keyword_position: TokenPosition,
|
||||||
) -> ExpressionRef<'src, 'arena> {
|
) -> ExpressionRef<'src, 'arena> {
|
||||||
let condition = self.parse_condition();
|
let (condition, body) = match self
|
||||||
let body = self.parse_branch_body();
|
.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) {
|
let (else_body, if_end_position) = if self.peek_keyword() == Some(Keyword::Else) {
|
||||||
self.advance(); // '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;
|
let else_body_end = else_body.end_anchor_token_position;
|
||||||
(Some(else_body), else_body_end)
|
(Some(else_body), else_body_end)
|
||||||
} else {
|
} else {
|
||||||
@ -204,8 +337,13 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
while_keyword_position: TokenPosition,
|
while_keyword_position: TokenPosition,
|
||||||
) -> ExpressionRef<'src, 'arena> {
|
) -> ExpressionRef<'src, 'arena> {
|
||||||
let condition = self.parse_condition();
|
let (condition, body) = match self.parse_condition_and_branch_body(
|
||||||
let body = self.parse_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);
|
let span = TokenSpan::range(while_keyword_position, body.end_anchor_token_position);
|
||||||
self.arena
|
self.arena
|
||||||
.alloc_node(Expression::While { condition, body }, span)
|
.alloc_node(Expression::While { condition, body }, span)
|
||||||
@ -220,11 +358,12 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
do_keyword_position: TokenPosition,
|
do_keyword_position: TokenPosition,
|
||||||
) -> ExpressionRef<'src, 'arena> {
|
) -> ExpressionRef<'src, 'arena> {
|
||||||
let body = self.parse_branch_body();
|
let body = self.parse_branch_body(do_keyword_position);
|
||||||
|
|
||||||
let condition = if self
|
let condition = if self
|
||||||
.expect_keyword(Keyword::Until, ParseErrorKind::DoMissingUntil)
|
.expect_keyword(Keyword::Until, ParseErrorKind::DoMissingUntil)
|
||||||
.widen_error_span_from(do_keyword_position)
|
.widen_error_span_from(do_keyword_position)
|
||||||
|
.related_token("do_keyword", do_keyword_position)
|
||||||
.report_error(self)
|
.report_error(self)
|
||||||
{
|
{
|
||||||
crate::arena::ArenaNode::new_in(
|
crate::arena::ArenaNode::new_in(
|
||||||
@ -233,7 +372,9 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
self.arena,
|
self.arena,
|
||||||
)
|
)
|
||||||
} else {
|
} 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);
|
let span = TokenSpan::range(do_keyword_position, condition.span().end);
|
||||||
self.arena
|
self.arena
|
||||||
@ -252,12 +393,30 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
foreach_keyword_position: TokenPosition,
|
foreach_keyword_position: TokenPosition,
|
||||||
) -> ExpressionRef<'src, 'arena> {
|
) -> ExpressionRef<'src, 'arena> {
|
||||||
// UnrealScript `foreach` iterator expressions are simple enough that
|
if self
|
||||||
// they do not need the special parenthesized-condition handling used by
|
.peek_token()
|
||||||
// `parse_condition()`.
|
.map(|error| !error.is_valid_identifier_name())
|
||||||
let iterated_expression = self.parse_expression();
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
let body = self.parse_branch_body();
|
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);
|
let span = TokenSpan::range(foreach_keyword_position, body.end_anchor_token_position);
|
||||||
self.arena.alloc_node(
|
self.arena.alloc_node(
|
||||||
Expression::ForEach {
|
Expression::ForEach {
|
||||||
@ -275,10 +434,12 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
/// top-level `;` appears before the matching `)` is closed or input ends.
|
/// top-level `;` appears before the matching `)` is closed or input ends.
|
||||||
///
|
///
|
||||||
/// This is used only for loop-vs-identifier disambiguation.
|
/// This is used only for loop-vs-identifier disambiguation.
|
||||||
pub(crate) fn is_for_loop_header_ahead(&mut self) -> bool {
|
pub(super) fn is_for_loop_header_ahead(&mut self) -> Option<TokenPosition> {
|
||||||
if self.peek_token() != Some(Token::LeftParenthesis) {
|
let Some((Token::LeftParenthesis, left_parenthesis_position)) =
|
||||||
return false;
|
self.peek_token_and_position()
|
||||||
}
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
let mut nesting_depth: usize = 1;
|
let mut nesting_depth: usize = 1;
|
||||||
let mut lookahead_token_offset: usize = 1;
|
let mut lookahead_token_offset: usize = 1;
|
||||||
while let Some(next_token) = self.peek_token_at(lookahead_token_offset) {
|
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 {
|
if nesting_depth <= 1 {
|
||||||
// End of the immediate `for (...)` group without a
|
// End of the immediate `for (...)` group without a
|
||||||
// top-level `;`: not a loop header.
|
// top-level `;`: not a loop header.
|
||||||
return false;
|
return None;
|
||||||
}
|
}
|
||||||
nesting_depth -= 1;
|
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;
|
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.
|
/// Parses a `for` expression after the `for` keyword.
|
||||||
@ -311,71 +476,173 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
pub(crate) fn parse_for_tail(
|
pub(crate) fn parse_for_tail(
|
||||||
&mut self,
|
&mut self,
|
||||||
for_keyword_position: TokenPosition,
|
for_keyword_position: TokenPosition,
|
||||||
|
left_parenthesis_position: TokenPosition,
|
||||||
) -> ExpressionRef<'src, 'arena> {
|
) -> ExpressionRef<'src, 'arena> {
|
||||||
// This path is expected to be entered only after
|
let header = self.parse_for_header(for_keyword_position, left_parenthesis_position);
|
||||||
// `is_for_loop_header_ahead()`, so the opening `(` and at least one
|
if header.right_parenthesis_position.is_none() {
|
||||||
// top-level `;` should already be structurally guaranteed.
|
return self.arena.alloc_node(
|
||||||
self.expect(
|
Expression::Error,
|
||||||
Token::LeftParenthesis,
|
TokenSpan::range(for_keyword_position, self.last_consumed_position_or_start()),
|
||||||
ParseErrorKind::ForMissingOpeningParenthesis,
|
);
|
||||||
)
|
}
|
||||||
.widen_error_span_from(for_keyword_position)
|
let body = self.parse_branch_body(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 span = TokenSpan::range(for_keyword_position, body.end_anchor_token_position);
|
let span = TokenSpan::range(for_keyword_position, body.end_anchor_token_position);
|
||||||
self.arena.alloc_node(
|
self.arena.alloc_node(
|
||||||
Expression::For {
|
Expression::For {
|
||||||
initialization,
|
initialization: header.initialization,
|
||||||
condition,
|
condition: header.condition,
|
||||||
step,
|
step: header.step,
|
||||||
body,
|
body,
|
||||||
},
|
},
|
||||||
span,
|
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.
|
/// Parses the continuation of a `return` expression after its keyword.
|
||||||
///
|
///
|
||||||
/// If the next token is not `;`, consumes a return value expression.
|
/// If the next token is not `;`, consumes a return value expression.
|
||||||
@ -432,7 +699,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
label_position,
|
label_position,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.make_error_here(ParseErrorKind::GotoMissingLabel)
|
self.make_error_at_last_consumed(ParseErrorKind::GotoMissingLabel)
|
||||||
.widen_error_span_from(goto_keyword_position)
|
.widen_error_span_from(goto_keyword_position)
|
||||||
.sync_error_until(self, SyncLevel::Statement)
|
.sync_error_until(self, SyncLevel::Statement)
|
||||||
.report_error(self);
|
.report_error(self);
|
||||||
|
|||||||
@ -20,7 +20,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
let (token, token_position) =
|
let (token, token_position) =
|
||||||
self.require_token_and_position(invalid_identifier_error_kind)?;
|
self.require_token_and_position(invalid_identifier_error_kind)?;
|
||||||
let identifier = Parser::identifier_token_from_token(token, token_position)
|
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();
|
self.advance();
|
||||||
Ok(identifier)
|
Ok(identifier)
|
||||||
}
|
}
|
||||||
@ -56,11 +56,15 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
|
|
||||||
let span_start = head.0;
|
let span_start = head.0;
|
||||||
let mut span_end = span_start;
|
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(); // '.'
|
self.advance(); // '.'
|
||||||
let next_segment = self
|
let next_segment = match self
|
||||||
.parse_identifier(invalid_identifier_error_kind)
|
.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;
|
span_end = next_segment.0;
|
||||||
|
|
||||||
let tail_vec = tail.get_or_insert_with(|| ArenaVec::new_in(self.arena));
|
let tail_vec = tail.get_or_insert_with(|| ArenaVec::new_in(self.arena));
|
||||||
|
|||||||
@ -33,7 +33,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
|||||||
};
|
};
|
||||||
let digits_without_underscores = content.replace('_', "");
|
let digits_without_underscores = content.replace('_', "");
|
||||||
u128::from_str_radix(&digits_without_underscores, base)
|
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
|
/// 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);
|
.unwrap_or(content);
|
||||||
content
|
content
|
||||||
.parse::<f64>()
|
.parse::<f64>()
|
||||||
.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.
|
/// Unescapes a tokenized string literal into an arena string.
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
//! - [`super::precedence`] - operator precedence definitions
|
//! - [`super::precedence`] - operator precedence definitions
|
||||||
|
|
||||||
use crate::ast::{self, Expression, ExpressionRef};
|
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;
|
pub use super::precedence::PrecedenceRank;
|
||||||
|
|
||||||
@ -63,9 +63,51 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn parse_expression(&mut self) -> ExpressionRef<'src, 'arena> {
|
pub fn parse_expression(&mut self) -> ExpressionRef<'src, 'arena> {
|
||||||
self.parse_expression_with_min_precedence_rank(PrecedenceRank::LOOSEST)
|
self.parse_expression_with_min_precedence_rank(PrecedenceRank::LOOSEST)
|
||||||
|
.sync_error_until(self, parser::SyncLevel::ExpressionStart)
|
||||||
.unwrap_or_fallback(self)
|
.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
|
/// Parses an expression, including only operators with binding power
|
||||||
/// at least `min_precedence_rank` (as tight or tighter).
|
/// at least `min_precedence_rank` (as tight or tighter).
|
||||||
fn parse_expression_with_min_precedence_rank(
|
fn parse_expression_with_min_precedence_rank(
|
||||||
|
|||||||
@ -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
|
//! This module implements parsing of primary expressions via
|
||||||
//! [`Parser::parse_primary_from_current_token`] and its helper
|
//! [`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
|
//! It means "an expression form that does not need a left-hand side
|
||||||
//! in order to be parsed".
|
//! in order to be parsed".
|
||||||
|
|
||||||
use super::selectors::ParsedCallArgumentSlot;
|
|
||||||
use crate::ast::{Expression, ExpressionRef, OptionalExpression};
|
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};
|
use crate::parser::{ParseErrorKind, ParseExpressionResult, Parser, ResultRecoveryExt, SyncLevel};
|
||||||
|
|
||||||
|
mod new;
|
||||||
|
|
||||||
impl<'src, 'arena> Parser<'src, 'arena> {
|
impl<'src, 'arena> Parser<'src, 'arena> {
|
||||||
/// Parses a primary expression starting from the provided token.
|
/// 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
|
/// Returns [`ParseErrorKind::ExpressionExpected`] if the provided
|
||||||
/// token cannot begin any valid primary expression in this position.
|
/// 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,
|
&mut self,
|
||||||
token: Token,
|
token: Token,
|
||||||
token_lexeme: &'src str,
|
token_lexeme: &'src str,
|
||||||
@ -78,10 +79,12 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
),
|
),
|
||||||
Token::LeftParenthesis => self.parse_parenthesized_expression_tail(token_position),
|
Token::LeftParenthesis => self.parse_parenthesized_expression_tail(token_position),
|
||||||
Token::LeftBrace => self.parse_block_tail(token_position),
|
Token::LeftBrace => self.parse_block_tail(token_position),
|
||||||
Token::Keyword(keyword) => match self.parse_keyword_primary(keyword, token_position) {
|
Token::Keyword(keyword) => {
|
||||||
Some(keyword_expression) => keyword_expression,
|
match self.try_parse_keyword_primary(keyword, token_position) {
|
||||||
None => return self.parse_identifier_like_primary(token, 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),
|
_ => 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
|
/// Returns `None` if the keyword should instead be interpreted as an
|
||||||
/// identifier in this position.
|
/// identifier in this position.
|
||||||
fn parse_keyword_primary(
|
fn try_parse_keyword_primary(
|
||||||
&mut self,
|
&mut self,
|
||||||
keyword: Keyword,
|
keyword: Keyword,
|
||||||
token_position: TokenPosition,
|
token_position: TokenPosition,
|
||||||
@ -115,7 +118,12 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
Keyword::New => self.parse_new_expression_tail(token_position),
|
Keyword::New => self.parse_new_expression_tail(token_position),
|
||||||
// These keywords remain valid identifiers unless the following
|
// These keywords remain valid identifiers unless the following
|
||||||
// tokens commit to the keyword-led form.
|
// 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)) => {
|
Keyword::Goto if !matches!(self.peek_token(), Some(Token::LeftParenthesis)) => {
|
||||||
self.parse_goto_tail(token_position)
|
self.parse_goto_tail(token_position)
|
||||||
}
|
}
|
||||||
@ -125,10 +133,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
self.parse_switch_tail(token_position)
|
self.parse_switch_tail(token_position)
|
||||||
}
|
}
|
||||||
Keyword::Class => {
|
Keyword::Class => {
|
||||||
if let Some((Token::Less, left_angle_bracket_position)) =
|
if let Some(left_angle_bracket_position) = self.eat_with_position(Token::Less) {
|
||||||
self.peek_token_and_position()
|
|
||||||
{
|
|
||||||
self.advance(); // '<'
|
|
||||||
self.parse_class_type_tail(token_position, left_angle_bracket_position)
|
self.parse_class_type_tail(token_position, left_angle_bracket_position)
|
||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
@ -151,10 +156,9 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
primary_token_position: TokenPosition,
|
primary_token_position: TokenPosition,
|
||||||
) -> ParseExpressionResult<'src, 'arena> {
|
) -> ParseExpressionResult<'src, 'arena> {
|
||||||
let identifier_token =
|
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),
|
|| self.make_error_at(ParseErrorKind::ExpressionExpected, primary_token_position),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// A token that is valid as an identifier may still start a tagged-name
|
// A token that is valid as an identifier may still start a tagged-name
|
||||||
// literal such as `Texture'Foo.Bar'`.
|
// literal such as `Texture'Foo.Bar'`.
|
||||||
let expression = if let Some((Token::NameLiteral, lexeme, name_position)) =
|
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.
|
/// Assumes the opening `(` has already been consumed.
|
||||||
/// Reports and recovers from a missing closing `)`.
|
/// Reports and recovers from a missing closing `)`.
|
||||||
fn parse_parenthesized_expression_tail(
|
pub(super) fn parse_parenthesized_expression_tail(
|
||||||
&mut self,
|
&mut self,
|
||||||
left_parenthesis_position: TokenPosition,
|
left_parenthesis_position: TokenPosition,
|
||||||
) -> ExpressionRef<'src, 'arena> {
|
) -> ExpressionRef<'src, 'arena> {
|
||||||
// Continue parsing normally
|
if self.next_token_definitely_cannot_start_expression() {
|
||||||
let inner_expression = if self.next_token_definitely_cannot_start_expression() {
|
return self
|
||||||
let error = self
|
.make_error_at_last_consumed(ParseErrorKind::ParenthesizedExpressionInvalidStart)
|
||||||
.make_error_here(ParseErrorKind::ParenthesizedExpressionInvalidStart)
|
|
||||||
.widen_error_span_from(left_parenthesis_position)
|
.widen_error_span_from(left_parenthesis_position)
|
||||||
.sync_error_until(self, SyncLevel::Expression)
|
|
||||||
.extend_blame_to_next_token(self)
|
.extend_blame_to_next_token(self)
|
||||||
.related_token("left_parenthesis", left_parenthesis_position);
|
.related_token("left_parenthesis", left_parenthesis_position)
|
||||||
let error_span = error.covered_span;
|
.sync_error_until(self, SyncLevel::CloseParenthesis)
|
||||||
self.report_error(error);
|
.fallback(self);
|
||||||
return crate::arena::ArenaNode::new_in(
|
|
||||||
crate::ast::Expression::Error,
|
|
||||||
error_span,
|
|
||||||
self.arena,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self.parse_expression()
|
|
||||||
};
|
};
|
||||||
|
let inner_expression = self.parse_expression();
|
||||||
let right_parenthesis_position = self
|
let right_parenthesis_position = self
|
||||||
.expect(
|
.expect(
|
||||||
Token::RightParenthesis,
|
Token::RightParenthesis,
|
||||||
@ -230,50 +226,55 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
class_keyword_position: TokenPosition,
|
class_keyword_position: TokenPosition,
|
||||||
left_angle_bracket_position: TokenPosition,
|
left_angle_bracket_position: TokenPosition,
|
||||||
) -> ExpressionRef<'src, 'arena> {
|
) -> ExpressionRef<'src, 'arena> {
|
||||||
// Special case for an empty argument
|
match self.peek_token_and_position() {
|
||||||
if let Some((Token::Greater, right_angle_bracket_position)) = self.peek_token_and_position()
|
Some((Token::Greater, right_angle_bracket_position)) => self
|
||||||
{
|
.report_missing_class_type_argument(
|
||||||
self.make_error_here(ParseErrorKind::ClassTypeMissingTypeArgument {
|
class_keyword_position,
|
||||||
left_angle_bracket_position,
|
left_angle_bracket_position,
|
||||||
})
|
right_angle_bracket_position,
|
||||||
.widen_error_span_from(left_angle_bracket_position)
|
),
|
||||||
.sync_error_at(self, SyncLevel::CloseAngleBracket)
|
Some((first_token, _)) if first_token.is_valid_identifier_name() => self
|
||||||
.blame_token(right_angle_bracket_position)
|
.parse_nonempty_class_type_tail(
|
||||||
.report_error(self);
|
class_keyword_position,
|
||||||
return self.arena.alloc_node_between(
|
left_angle_bracket_position,
|
||||||
Expression::Error,
|
),
|
||||||
|
Some((_, bad_position)) => self.report_invalid_class_type_start(
|
||||||
class_keyword_position,
|
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,
|
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)
|
.widen_error_span_from(class_keyword_position)
|
||||||
|
.extend_blame_to_next_token(self)
|
||||||
.sync_error_at(self, SyncLevel::CloseAngleBracket)
|
.sync_error_at(self, SyncLevel::CloseAngleBracket)
|
||||||
|
.related_token("class_keyword", class_keyword_position)
|
||||||
{
|
{
|
||||||
Ok(class_type) => class_type,
|
Ok(class_type) => class_type,
|
||||||
Err(error) => {
|
Err(error) => return self.report_error_with_fallback(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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let right_angle_bracket_position = self
|
let right_angle_bracket_position = self
|
||||||
.expect(
|
.expect(
|
||||||
Token::Greater,
|
Token::Greater,
|
||||||
ParseErrorKind::ClassTypeMissingClosingAngleBracket {
|
ParseErrorKind::ClassTypeMissingClosingAngleBracket,
|
||||||
left_angle_bracket_position,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.widen_error_span_from(class_keyword_position)
|
.widen_error_span_from(class_keyword_position)
|
||||||
.sync_error_at(self, SyncLevel::CloseAngleBracket)
|
.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);
|
.unwrap_or_fallback(self);
|
||||||
self.arena.alloc_node_between(
|
self.arena.alloc_node_between(
|
||||||
Expression::ClassType(class_type),
|
Expression::ClassType(class_type),
|
||||||
@ -282,101 +283,37 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a `new` expression with an optional parenthesized argument list.
|
fn report_missing_class_type_argument(
|
||||||
///
|
|
||||||
/// Assumes the `new` keyword has already been consumed.
|
|
||||||
/// The parenthesized argument list is optional.
|
|
||||||
fn parse_new_expression_tail(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
new_keyword_position: TokenPosition,
|
class_keyword_position: TokenPosition,
|
||||||
|
left_angle_bracket_position: TokenPosition,
|
||||||
|
right_angle_bracket_position: TokenPosition,
|
||||||
) -> ExpressionRef<'src, 'arena> {
|
) -> ExpressionRef<'src, 'arena> {
|
||||||
let (outer_argument, name_argument, flags_argument) =
|
self.advance();
|
||||||
if let Some((Token::LeftParenthesis, left_parenthesis_position)) =
|
self.make_error_at_last_consumed(ParseErrorKind::ClassTypeMissingTypeArgument)
|
||||||
self.peek_token_and_position()
|
.widen_error_span_from(class_keyword_position)
|
||||||
{
|
.blame(TokenSpan::range(
|
||||||
self.advance();
|
left_angle_bracket_position,
|
||||||
self.parse_new_argument_list_tail(left_parenthesis_position)
|
right_angle_bracket_position,
|
||||||
} else {
|
))
|
||||||
(None, None, None)
|
.related_token("left_angle_bracket", left_angle_bracket_position)
|
||||||
};
|
.related_token("class_keyword", class_keyword_position)
|
||||||
// The class specifier is often a literal class reference, but any
|
.fallback(self)
|
||||||
// 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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses the optional parenthesized arguments of a `new` expression.
|
fn report_invalid_class_type_start(
|
||||||
///
|
|
||||||
/// 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(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
left_parenthesis_position: TokenPosition,
|
class_keyword_position: TokenPosition,
|
||||||
) -> (
|
left_angle_bracket_position: TokenPosition,
|
||||||
OptionalExpression<'src, 'arena>,
|
bad_position: TokenPosition,
|
||||||
OptionalExpression<'src, 'arena>,
|
) -> ExpressionRef<'src, 'arena> {
|
||||||
OptionalExpression<'src, 'arena>,
|
self.make_error_at_last_consumed(ParseErrorKind::ClassTypeInvalidStart)
|
||||||
) {
|
.widen_error_span_from(class_keyword_position)
|
||||||
let mut outer_argument = None;
|
.sync_error_at(self, SyncLevel::CloseAngleBracket)
|
||||||
let mut name_argument = None;
|
.blame_token(bad_position)
|
||||||
let mut flags_argument = None;
|
.related_token("left_angle_bracket", left_angle_bracket_position)
|
||||||
|
.related_token("class_keyword", class_keyword_position)
|
||||||
let mut first_call = true;
|
.fallback(self)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` iff the next token is definitely not a valid start of an
|
/// 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
|
/// - `false` means "might be valid", so the normal expression parser should
|
||||||
/// decide and potentially emit a more specific error.
|
/// decide and potentially emit a more specific error.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub(crate) fn next_token_definitely_cannot_start_expression(&mut self) -> bool {
|
pub(super) fn next_token_definitely_cannot_start_expression(&mut self) -> bool {
|
||||||
matches!(
|
self.peek_token()
|
||||||
self.peek_token(),
|
.map_or(true, Token::is_definitely_not_expression_start)
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
342
rottlib/src/parser/grammar/expression/primary/new.rs
Normal file
342
rottlib/src/parser/grammar/expression/primary/new.rs
Normal file
@ -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<TokenSpan> {
|
||||||
|
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<TokenSpan> {
|
||||||
|
[
|
||||||
|
&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<NewClassSpecifierParseAction> {
|
||||||
|
// 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<NewClassSpecifierParseAction> {
|
||||||
|
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<TokenPosition>,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,36 @@ use crate::ast::{Expression, ExpressionRef, OptionalExpression};
|
|||||||
use crate::lexer::{Token, TokenPosition, TokenSpan};
|
use crate::lexer::{Token, TokenPosition, TokenSpan};
|
||||||
use crate::parser::{ParseErrorKind, ParseExpressionResult, Parser, ResultRecoveryExt, SyncLevel};
|
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.
|
/// Represents the result of parsing one call argument slot.
|
||||||
///
|
///
|
||||||
/// This distinguishes between the end of the argument list and a parsed
|
/// 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 `(`.
|
/// Parses one call argument slot after an already consumed `(`.
|
||||||
///
|
///
|
||||||
/// In `UnrealScript`, every comma introduces a follow-up argument slot, so a
|
/// In `UnrealScript`, every comma introduces a follow-up argument slot, so a
|
||||||
/// trailing comma immediately before `)` denotes an omitted final argument.
|
/// trailing comma immediately before `)` denotes an omitted final argument.
|
||||||
///
|
///
|
||||||
/// Returns [`ParsedCallArgumentSlot::NoMoreArguments`] when the argument
|
/// Returns [`ParsedCallArgumentSlot::NoMoreArguments`] when the argument list
|
||||||
/// list ends, and `Argument(None)` for an omitted argument slot.
|
/// ends, and `Argument(None)` for an omitted argument slot.
|
||||||
|
///
|
||||||
|
/// Per-call status is recorded into `state`.
|
||||||
pub(crate) fn parse_call_argument_slot(
|
pub(crate) fn parse_call_argument_slot(
|
||||||
&mut self,
|
&mut self,
|
||||||
left_parenthesis_position: TokenPosition,
|
state: &mut CallArgumentListParseState,
|
||||||
first_call: bool,
|
|
||||||
) -> ParsedCallArgumentSlot<'src, 'arena> {
|
) -> 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() {
|
match self.peek_token() {
|
||||||
Some(Token::RightParenthesis) => return ParsedCallArgumentSlot::NoMoreArguments,
|
None | Some(Token::RightParenthesis) => {
|
||||||
|
return ParsedCallArgumentSlot::NoMoreArguments;
|
||||||
|
}
|
||||||
Some(Token::Comma) => {
|
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();
|
self.advance();
|
||||||
}
|
}
|
||||||
|
// This `if`'s body is guaranteed to run if we've skipped
|
||||||
|
// `advance()` above.
|
||||||
if self.at_call_argument_boundary() {
|
if self.at_call_argument_boundary() {
|
||||||
|
state.parsed_slot_count += 1;
|
||||||
return ParsedCallArgumentSlot::Argument(None);
|
return ParsedCallArgumentSlot::Argument(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
let argument = self.parse_expression();
|
let argument = self.parse_expression();
|
||||||
if !self.at_call_argument_boundary() {
|
state.parsed_slot_count += 1;
|
||||||
self.make_error_here(ParseErrorKind::FunctionArgumentMissingComma)
|
state.last_slot_missing_boundary = !self.at_call_argument_boundary();
|
||||||
.widen_error_span_from(left_parenthesis_position)
|
|
||||||
.report_error(self);
|
|
||||||
}
|
|
||||||
ParsedCallArgumentSlot::Argument(Some(argument))
|
ParsedCallArgumentSlot::Argument(Some(argument))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,12 +229,16 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
) -> ArenaVec<'arena, Option<ExpressionRef<'src, 'arena>>> {
|
) -> ArenaVec<'arena, Option<ExpressionRef<'src, 'arena>>> {
|
||||||
let mut argument_list = ArenaVec::new_in(self.arena);
|
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) =
|
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);
|
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
|
argument_list
|
||||||
|
|||||||
@ -155,7 +155,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
|||||||
// one problematic case.
|
// one problematic case.
|
||||||
let mut sink = self.arena.vec();
|
let mut sink = self.arena.vec();
|
||||||
self.parse_switch_arm_body(&mut sink);
|
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)
|
.widen_error_span_from(preamble_start_position)
|
||||||
.report_error(self);
|
.report_error(self);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -137,7 +137,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
return Ok(kind);
|
return Ok(kind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(self.make_error_here(ParseErrorKind::CallableExpectedKind))
|
Err(self.make_error_at_last_consumed(ParseErrorKind::CallableExpectedKind))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_callable_name(
|
fn parse_callable_name(
|
||||||
@ -153,7 +153,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
ParseErrorKind::CallablePrefixOperatorInvalidSymbol,
|
ParseErrorKind::CallablePrefixOperatorInvalidSymbol,
|
||||||
)?;
|
)?;
|
||||||
let operator = PrefixOperator::try_from(token).map_err(|()| {
|
let operator = PrefixOperator::try_from(token).map_err(|()| {
|
||||||
self.make_error_here(ParseErrorKind::CallablePrefixOperatorInvalidSymbol)
|
self.make_error_at_last_consumed(ParseErrorKind::CallablePrefixOperatorInvalidSymbol)
|
||||||
})?;
|
})?;
|
||||||
self.advance();
|
self.advance();
|
||||||
Ok(CallableName::PrefixOperator(PrefixOperatorName {
|
Ok(CallableName::PrefixOperator(PrefixOperatorName {
|
||||||
@ -166,7 +166,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
ParseErrorKind::CallableInfixOperatorInvalidSymbol,
|
ParseErrorKind::CallableInfixOperatorInvalidSymbol,
|
||||||
)?;
|
)?;
|
||||||
let operator = InfixOperator::try_from(token).map_err(|()| {
|
let operator = InfixOperator::try_from(token).map_err(|()| {
|
||||||
self.make_error_here(ParseErrorKind::CallableInfixOperatorInvalidSymbol)
|
self.make_error_at_last_consumed(ParseErrorKind::CallableInfixOperatorInvalidSymbol)
|
||||||
})?;
|
})?;
|
||||||
self.advance();
|
self.advance();
|
||||||
Ok(CallableName::InfixOperator(InfixOperatorName {
|
Ok(CallableName::InfixOperator(InfixOperatorName {
|
||||||
@ -179,7 +179,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
ParseErrorKind::CallablePostfixOperatorInvalidSymbol,
|
ParseErrorKind::CallablePostfixOperatorInvalidSymbol,
|
||||||
)?;
|
)?;
|
||||||
let operator = PostfixOperator::try_from(token).map_err(|()| {
|
let operator = PostfixOperator::try_from(token).map_err(|()| {
|
||||||
self.make_error_here(ParseErrorKind::CallablePostfixOperatorInvalidSymbol)
|
self.make_error_at_last_consumed(ParseErrorKind::CallablePostfixOperatorInvalidSymbol)
|
||||||
})?;
|
})?;
|
||||||
self.advance();
|
self.advance();
|
||||||
Ok(CallableName::PostfixOperator(PostfixOperatorName {
|
Ok(CallableName::PostfixOperator(PostfixOperatorName {
|
||||||
|
|||||||
@ -10,6 +10,6 @@
|
|||||||
|
|
||||||
mod class;
|
mod class;
|
||||||
mod declarations;
|
mod declarations;
|
||||||
mod expression;
|
pub(super) mod expression;
|
||||||
mod function;
|
mod function;
|
||||||
mod statement;
|
mod statement;
|
||||||
|
|||||||
@ -26,7 +26,6 @@
|
|||||||
//! low-level plumbing lives in submodules.
|
//! low-level plumbing lives in submodules.
|
||||||
|
|
||||||
use super::lexer;
|
use super::lexer;
|
||||||
use crate::lexer::TokenSpan;
|
|
||||||
|
|
||||||
pub use lexer::{TokenData, Tokens};
|
pub use lexer::{TokenData, Tokens};
|
||||||
|
|
||||||
@ -44,6 +43,13 @@ pub(crate) use trivia::{TriviaKind, TriviaToken};
|
|||||||
pub type ParseExpressionResult<'src, 'arena> =
|
pub type ParseExpressionResult<'src, 'arena> =
|
||||||
ParseResult<'src, 'arena, crate::ast::ExpressionRef<'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`].
|
/// A recursive-descent parser over token from [`crate::lexer::TokenizedFile`].
|
||||||
pub struct Parser<'src, 'arena> {
|
pub struct Parser<'src, 'arena> {
|
||||||
file: &'src lexer::TokenizedFile<'src>,
|
file: &'src lexer::TokenizedFile<'src>,
|
||||||
|
|||||||
@ -8,6 +8,9 @@
|
|||||||
//! General idea is that any method that returns something other than an error
|
//! General idea is that any method that returns something other than an error
|
||||||
//! can be assumed to have reported it.
|
//! can be assumed to have reported it.
|
||||||
|
|
||||||
|
#![allow(dead_code)]
|
||||||
|
// TODO: remove dead code
|
||||||
|
|
||||||
use crate::ast::{CallableKind, IdentifierToken, QualifiedIdentifier};
|
use crate::ast::{CallableKind, IdentifierToken, QualifiedIdentifier};
|
||||||
use crate::diagnostics::diagnostic_from_parse_error;
|
use crate::diagnostics::diagnostic_from_parse_error;
|
||||||
use crate::lexer::{Token, TokenPosition, TokenSpan};
|
use crate::lexer::{Token, TokenPosition, TokenSpan};
|
||||||
@ -26,7 +29,7 @@ pub enum SyncLevel {
|
|||||||
/// Tokens that can reasonably continue or restart an expression.
|
/// Tokens that can reasonably continue or restart an expression.
|
||||||
///
|
///
|
||||||
/// This is the loosest recovery level.
|
/// This is the loosest recovery level.
|
||||||
Expression,
|
ExpressionStart,
|
||||||
|
|
||||||
/// Separator between homogeneous list elements, e.g. `,`.
|
/// Separator between homogeneous list elements, e.g. `,`.
|
||||||
///
|
///
|
||||||
@ -37,12 +40,12 @@ pub enum SyncLevel {
|
|||||||
/// Closing `>` of an angle-bracket-delimited type/class argument list.
|
/// Closing `>` of an angle-bracket-delimited type/class argument list.
|
||||||
CloseAngleBracket,
|
CloseAngleBracket,
|
||||||
|
|
||||||
/// Closing `)` of a parenthesized/grouped construct.
|
|
||||||
CloseParenthesis,
|
|
||||||
|
|
||||||
/// Closing `]` of an index or bracket-delimited construct.
|
/// Closing `]` of an index or bracket-delimited construct.
|
||||||
CloseBracket,
|
CloseBracket,
|
||||||
|
|
||||||
|
/// Closing `)` of a parenthesized/grouped construct.
|
||||||
|
CloseParenthesis,
|
||||||
|
|
||||||
/// A statement boundary or statement starter.
|
/// A statement boundary or statement starter.
|
||||||
///
|
///
|
||||||
/// Includes `;` and keywords that begin standalone statements /
|
/// Includes `;` and keywords that begin standalone statements /
|
||||||
@ -74,54 +77,10 @@ impl SyncLevel {
|
|||||||
use crate::lexer::Keyword;
|
use crate::lexer::Keyword;
|
||||||
use SyncLevel::{
|
use SyncLevel::{
|
||||||
BlockBoundary, CloseAngleBracket, CloseBracket, CloseParenthesis, DeclarationStart,
|
BlockBoundary, CloseAngleBracket, CloseBracket, CloseParenthesis, DeclarationStart,
|
||||||
Expression, ListSeparator, Statement, SwitchArmStart,
|
ExpressionStart, ListSeparator, Statement, SwitchArmStart,
|
||||||
};
|
};
|
||||||
|
|
||||||
match token {
|
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
|
// List / delimiter boundaries
|
||||||
Token::Comma => Some(ListSeparator),
|
Token::Comma => Some(ListSeparator),
|
||||||
Token::Greater => Some(CloseAngleBracket),
|
Token::Greater => Some(CloseAngleBracket),
|
||||||
@ -170,12 +129,18 @@ impl SyncLevel {
|
|||||||
// Hard structural stop
|
// Hard structural stop
|
||||||
Token::LeftBrace | Token::CppBlock | Token::RightBrace => Some(BlockBoundary),
|
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.
|
/// Converts a parse error into a diagnostic and queues it.
|
||||||
///
|
///
|
||||||
/// Placeholder implementation.
|
/// Placeholder implementation.
|
||||||
@ -188,7 +153,7 @@ impl Parser<'_, '_> {
|
|||||||
/// Reports a parser error with [`crate::parser::ParseErrorKind`] at
|
/// Reports a parser error with [`crate::parser::ParseErrorKind`] at
|
||||||
/// the current location and queues an appropriate diagnostic.
|
/// the current location and queues an appropriate diagnostic.
|
||||||
pub fn report_error_here(&mut self, error_kind: crate::parser::ParseErrorKind) {
|
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);
|
self.report_error(new_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,6 +170,20 @@ impl Parser<'_, '_> {
|
|||||||
self.advance();
|
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<T>(&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
|
/// 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_start_to_covered_start(self) -> Self;
|
||||||
fn extend_blame_end_to_covered_end(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<String>, related_position: TokenPosition) -> Self {
|
fn related_token(self, tag: impl Into<String>, related_position: TokenPosition) -> Self {
|
||||||
self.related(tag, TokenSpan::new(related_position))
|
self.related(tag, TokenSpan::new(related_position))
|
||||||
}
|
}
|
||||||
@ -328,11 +309,7 @@ impl<'src, 'arena, T> ResultRecoveryExt<'src, 'arena, T> for ParseResult<'src, '
|
|||||||
where
|
where
|
||||||
T: RecoveryFallback<'src, 'arena>,
|
T: RecoveryFallback<'src, 'arena>,
|
||||||
{
|
{
|
||||||
self.unwrap_or_else(|error| {
|
self.unwrap_or_else(|error| parser.report_error_with_fallback(error))
|
||||||
let value = T::fallback_value(parser, &error);
|
|
||||||
parser.report_error(error);
|
|
||||||
value
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn report_error(self, parser: &mut Parser<'src, 'arena>) -> bool {
|
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 {
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extend_blame_start_to_covered_start(mut self) -> 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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extend_blame_end_to_covered_end(mut self) -> 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
|
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)
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -26,6 +26,9 @@
|
|||||||
//!
|
//!
|
||||||
//! Violating this protocol is a logic error.
|
//! Violating this protocol is a logic error.
|
||||||
|
|
||||||
|
#![allow(dead_code)]
|
||||||
|
// TODO: remove dead code
|
||||||
|
|
||||||
use crate::lexer::TokenPosition;
|
use crate::lexer::TokenPosition;
|
||||||
|
|
||||||
/// Kinds of trivia tokens corresponding to variants of [`crate::lexer::Token`].
|
/// 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
|
/// token, as well as file-leading and file-trailing trivia. Returned slices
|
||||||
/// borrow the index, and the contained token texts live for `'src`.
|
/// borrow the index, and the contained token texts live for `'src`.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct TriviaIndex<'src> {
|
pub struct TriviaIndex<'src> {
|
||||||
/// All trivia tokens, stored contiguously in file order.
|
/// All trivia tokens, stored contiguously in file order.
|
||||||
tokens: Vec<TriviaToken<'src>>,
|
tokens: Vec<TriviaToken<'src>>,
|
||||||
@ -101,7 +103,6 @@ pub struct TriviaIndex<'src> {
|
|||||||
/// a token stream in file order. Once all tokens have been processed, call
|
/// a token stream in file order. Once all tokens have been processed, call
|
||||||
/// [`TriviaIndexBuilder::into_index`] to finalize the index.
|
/// [`TriviaIndexBuilder::into_index`] to finalize the index.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct TriviaIndexBuilder<'src> {
|
pub struct TriviaIndexBuilder<'src> {
|
||||||
/// All trivia tokens, stored contiguously in file order.
|
/// All trivia tokens, stored contiguously in file order.
|
||||||
tokens: Vec<TriviaToken<'src>>,
|
tokens: Vec<TriviaToken<'src>>,
|
||||||
@ -203,7 +204,6 @@ impl<'src> TriviaIndex<'src> {
|
|||||||
/// Returns an empty slice if `position` does not identify a recorded
|
/// Returns an empty slice if `position` does not identify a recorded
|
||||||
/// significant token or if no trivia was recorded after it.
|
/// significant token or if no trivia was recorded after it.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn trivia_after_token(&self, position: TokenPosition) -> &[TriviaToken<'src>] {
|
pub(crate) fn trivia_after_token(&self, position: TokenPosition) -> &[TriviaToken<'src>] {
|
||||||
self.slice_for(
|
self.slice_for(
|
||||||
BoundaryLocation::Token(position),
|
BoundaryLocation::Token(position),
|
||||||
@ -216,7 +216,6 @@ impl<'src> TriviaIndex<'src> {
|
|||||||
/// Returns an empty slice if `position` does not identify a recorded
|
/// Returns an empty slice if `position` does not identify a recorded
|
||||||
/// significant token or if no trivia was recorded before it.
|
/// significant token or if no trivia was recorded before it.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn trivia_before_token(&self, position: TokenPosition) -> &[TriviaToken<'src>] {
|
pub(crate) fn trivia_before_token(&self, position: TokenPosition) -> &[TriviaToken<'src>] {
|
||||||
self.slice_for(
|
self.slice_for(
|
||||||
BoundaryLocation::Token(position),
|
BoundaryLocation::Token(position),
|
||||||
@ -228,7 +227,6 @@ impl<'src> TriviaIndex<'src> {
|
|||||||
///
|
///
|
||||||
/// If no significant tokens were recorded, returns all recorded trivia.
|
/// If no significant tokens were recorded, returns all recorded trivia.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn leading_trivia(&self) -> &[TriviaToken<'src>] {
|
pub(crate) fn leading_trivia(&self) -> &[TriviaToken<'src>] {
|
||||||
self.slice_for(BoundaryLocation::StartOfFile, &self.trivia_after_boundary)
|
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.
|
/// If no significant tokens were recorded, returns all recorded trivia.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn trailing_trivia(&self) -> &[TriviaToken<'src>] {
|
pub(crate) fn trailing_trivia(&self) -> &[TriviaToken<'src>] {
|
||||||
self.slice_for(BoundaryLocation::EndOfFile, &self.trivia_before_boundary)
|
self.slice_for(BoundaryLocation::EndOfFile, &self.trivia_before_boundary)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn slice_for(&self, key: BoundaryLocation, map: &TriviaRangeMap) -> &[TriviaToken<'src>] {
|
fn slice_for(&self, key: BoundaryLocation, map: &TriviaRangeMap) -> &[TriviaToken<'src>] {
|
||||||
match map.get(&key) {
|
match map.get(&key) {
|
||||||
Some(range) => {
|
Some(range) => {
|
||||||
|
|||||||
@ -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<Token> {
|
|
||||||
file.line_tokens(line).map(|(_, t)| t.token).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn line_positions<'src>(file: &TokenizedFile<'src>, line: usize) -> Vec<TokenPosition> {
|
|
||||||
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<Token> {
|
|
||||||
file.iter().map(|(_, t)| t.token).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn token_at<'src>(file: &TokenizedFile<'src>, index: usize) -> Option<TokenData<'src>> {
|
|
||||||
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<usize> {
|
|
||||||
(0..file.line_count()).find(|&line| file.line_text(line).as_deref() == Some(needle))
|
|
||||||
}
|
|
||||||
1
rottlib/tests/diagnostics.rs
Normal file
1
rottlib/tests/diagnostics.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
mod parser_diagnostics;
|
||||||
@ -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<Diagnostic>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FixtureRuns<'src> {
|
|
||||||
runs: HashMap<&'static str, FixtureRun<'src>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'src> FixtureRuns<'src> {
|
|
||||||
pub fn get(&self, label: &str) -> Option<Vec<Diagnostic>> {
|
|
||||||
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<Item = (&'static str, &FixtureRun<'src>)> {
|
|
||||||
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 `;`"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
1593
rottlib/tests/parser_diagnostics/control_flow_expressions.rs
Normal file
1593
rottlib/tests/parser_diagnostics/control_flow_expressions.rs
Normal file
File diff suppressed because it is too large
Load Diff
125
rottlib/tests/parser_diagnostics/mod.rs
Normal file
125
rottlib/tests/parser_diagnostics/mod.rs
Normal file
@ -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<ExpectedLabel>,
|
||||||
|
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<Diagnostic>;
|
||||||
|
|
||||||
|
pub(super) struct FixtureRuns {
|
||||||
|
runs: HashMap<&'static str, FixtureRun>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixtureRuns {
|
||||||
|
#[track_caller]
|
||||||
|
pub fn get(&self, label: &str) -> Option<Vec<Diagnostic>> {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
1572
rottlib/tests/parser_diagnostics/primary_expressions.rs
Normal file
1572
rottlib/tests/parser_diagnostics/primary_expressions.rs
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user