use std::ops::Range;
use m2_syn::native::{parse_native, NativeParseError};
use m2_syn::nodes::SourceFile;
use m2_syn::{LexErrorKind, PrettyNode, PrettyTree, Spanned};
#[derive(Debug, PartialEq, Eq)]
pub struct ParsedCell {
pub range: Range<usize>,
pub tree: PrettyNode,
}
#[derive(Debug, PartialEq, Eq)]
pub struct SyntaxDiagnostic {
pub message: String,
pub range: Option<Range<usize>>,
}
#[derive(Debug, PartialEq)]
pub enum ParseVerdict {
Complete(Vec<ParsedCell>),
Incomplete,
SyntaxError(SyntaxDiagnostic),
}
fn lexer_ran_out(kind: LexErrorKind) -> bool {
matches!(
kind,
LexErrorKind::UnterminatedGroup
| LexErrorKind::UnterminatedString
| LexErrorKind::UnterminatedRawString
| LexErrorKind::UnterminatedBlockComment
)
}
fn ran_out_of_input(error: &NativeParseError, source: &str) -> bool {
let span = error.span();
let reaches_end = span.end().is_some_and(|end| end >= source.trim_end().len());
match error {
NativeParseError::Lex(error) => lexer_ran_out(error.kind()),
NativeParseError::MissingOperand { .. } => reaches_end,
_ => reaches_end && span.start() == span.end(),
}
}
pub fn check(source: &str) -> ParseVerdict {
match parse_native(source) {
Ok(file) => ParseVerdict::Complete(parsed_cells(&file)),
Err(error) if ran_out_of_input(&error, source) => ParseVerdict::Incomplete,
Err(error) => {
let range = error
.span()
.bounds()
.map(|(start, end)| start.min(source.len())..end.min(source.len()));
ParseVerdict::SyntaxError(SyntaxDiagnostic {
message: error.to_string(),
range,
})
}
}
}
fn parsed_cells(file: &SourceFile) -> Vec<ParsedCell> {
file.cells
.iter()
.map(|cell| {
let start = cell.content.span().start()?;
let end = match &cell.semicolon {
Some(semicolon) => semicolon.span().end()?,
None => cell.content.span().end()?,
};
Some(ParsedCell {
range: start..end,
tree: cell.pretty_tree(),
})
})
.collect::<Option<Vec<_>>>()
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn cells_of(source: &str) -> Vec<&str> {
let ParseVerdict::Complete(ranges) = check(source) else {
panic!("expected {source:?} to parse");
};
ranges.into_iter().map(|cell| &source[cell.range]).collect()
}
#[test]
fn reproduces_the_oracle_verdicts() {
let complete = [
"1+1",
"a",
"\"a\"",
"a = 5;\nb = 6\n\n",
"local symbol",
"if if 1\n\nthen\n\t1\nelse \n 1then 1",
];
let incomplete = [
"a*",
"*",
"* *",
"if",
"(",
"({{}",
"symbol",
"if if",
"if 1",
"if if 1 then 1",
"if if 1\n\n",
"if if 1\n\nthen ",
"if if 1\n\nthen\n\t1\nelse \n 1then",
"if if 1\n\nthen\n\t1\nelse \n 1",
"if if 1\n\nthen\n\t1\nelse",
"\"",
"///",
"/////",
];
let errors = ["if;", "then", ")", "**"];
let mut wrong = Vec::new();
for source in complete {
let verdict = check(source);
if !matches!(verdict, ParseVerdict::Complete(_)) {
wrong.push(format!("{source:?} => {verdict:?}, wanted Complete"));
}
}
for source in incomplete {
let verdict = check(source);
if verdict != ParseVerdict::Incomplete {
wrong.push(format!("{source:?} => {verdict:?}, wanted Incomplete"));
}
}
for source in errors {
let verdict = check(source);
if !matches!(verdict, ParseVerdict::SyntaxError(_)) {
wrong.push(format!("{source:?} => {verdict:?}, wanted SyntaxError"));
}
}
assert!(
wrong.is_empty(),
"verdict mismatches:\n{}",
wrong.join("\n")
);
}
#[test]
fn splits_newline_delimited_cells() {
let source = "a = 5\nb = 6";
let cells = cells_of(source);
assert_eq!(cells, vec!["a = 5", "b = 6"]);
}
#[test]
fn single_cell_is_one_range() {
assert_eq!(cells_of("1 + 1").len(), 1);
}
#[test]
fn literal_delimiters_travel_with_their_cell() {
for source in ["help \"escape\"", "print ///a///"] {
assert_eq!(cells_of(source), vec![source]);
}
}
#[test]
fn input_without_statements_parses_to_no_cells() {
for source in ["", " ", "\n\n", "-- just a note"] {
assert!(
cells_of(source).is_empty(),
"expected no cells in {source:?}"
);
}
}
#[test]
fn a_semicolon_travels_with_its_cell() {
let source = "z = 7;\n1 + 1";
let cells = cells_of(source);
assert_eq!(cells, vec!["z = 7;", "1 + 1"]);
}
#[test]
fn clean_code_has_no_error() {
assert!(matches!(check("a = 5\nb = 6"), ParseVerdict::Complete(_)));
assert!(matches!(check(";;"), ParseVerdict::SyntaxError(_)));
}
}