use ariadne::{Color, Label, Report, ReportKind, Source};
use chumsky::{extra::Full, prelude::*, text};
type Extras<'a> = Full<Rich<'a, char>, (), &'a str>;
fn indent<'a>() -> impl Parser<'a, &'a str, &'a str, Extras<'a>> + Copy {
let empty_lines = text::inline_whitespace().then(text::newline()).repeated();
empty_lines.ignore_then(text::inline_whitespace().to_slice())
}
#[derive(Clone, Debug)]
pub enum Stmt {
Expr,
Loop(Vec<Stmt>),
}
fn parser<'a>() -> impl Parser<'a, &'a str, Vec<Stmt>, Extras<'a>> {
let expr = text::ident();
let block = recursive(|block| {
let block_start_indent = indent().try_map_with(move |indent, ext| match *ext.ctx() {
ctx if indent.starts_with(ctx) && indent.len() > ctx.len() => Ok(indent),
_ => Err(Rich::custom(
ext.span(),
"Indent must increase here".to_string(),
)),
});
let expr_stmt = expr.then_ignore(text::newline()).to(Stmt::Expr);
let control_flow = just("loop:")
.then(text::newline())
.ignore_then(block_start_indent.ignore_with_ctx(block))
.map(Stmt::Loop);
let stmt = expr_stmt.or(control_flow);
let block_continue_indent = indent().try_map_with(move |indent, ext| match *ext.ctx() {
ctx if indent == ctx => Ok(indent),
ctx if ctx.starts_with(indent) => {
Err(Rich::custom(ext.span(), "Unexpected deindent".to_string()))
}
ctx if indent.starts_with(ctx) => {
Err(Rich::custom(ext.span(), "Unexpected indent".to_string()))
}
ctx => Err(Rich::custom(
ext.span(),
format!("Mismatched indent: expected {ctx:?}, found {indent:?}"),
)),
});
stmt.separated_by(block_continue_indent).collect()
});
indent()
.try_map_with(|indent, ext| {
if !indent.is_empty() {
Err(Rich::custom(ext.span(), "No indent allowed at root level"))
} else {
Ok("")
}
})
.ignore_with_ctx(block)
}
fn show_result(input: &str) {
let (res, errs) = parser().parse(input).into_output_errors();
println!("Final parse result: {res:#?}");
errs.into_iter().for_each(|e| {
Report::build(ReportKind::Error, ((), e.span().into_range()))
.with_config(ariadne::Config::new().with_index_type(ariadne::IndexType::Byte))
.with_message(e.to_string())
.with_label(
Label::new(((), e.span().into_range()))
.with_message(e.reason().to_string())
.with_color(Color::Red),
)
.finish()
.print(Source::from(&input))
.unwrap()
});
}
fn main() {
eprintln!("Well-formed example:");
show_result(
r#"
expr
expr
loop:
expr
loop:
expr
expr
expr
expr
"#,
);
eprintln!("Example with bad indent:");
show_result(
r#"
foo
bar
loop:
baz
loop:
caz
car
dar
daz
"#,
);
}