Implent better diagnostics for control flow and primaries

This commit is contained in:
dkanus 2026-04-28 14:54:36 +07:00
commit 519d0cd3a7
39 changed files with 5990 additions and 1270 deletions

View file

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

View file

@ -0,0 +1 @@
mod parser_diagnostics;

View file

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

File diff suppressed because it is too large Load diff

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

File diff suppressed because it is too large Load diff