Skip to main content

mini_lang_chumsky/
mini_lang_chumsky.rs

1//! The mini language's function skeleton parsed with **chumsky** — the
2//! `increparse-chumsky` showcase.
3//!
4//! The pass emits *slice-relative* [`SimpleSpan`]s; `increparse_chumsky::chumsky_pass`
5//! rebases them to absolute spans so the incremental tree can reuse regions
6//! across edits.
7//!
8//! chumsky notes baked into this example:
9//!
10//! * parsers are built in a factory fn tied to the input lifetime,
11//! * `Parser::parse` demands whole-input consumption, so the file scanner
12//!   ends with `.then_ignore(any().repeated())` and the round-1 pass
13//!   consumes the entire region,
14//! * malformed definitions are skipped by a one-byte fallback
15//!   (`func.or(any())` inside `repeated`) — error resilience without
16//!   leaving the combinator.
17//!
18//! Run with `cargo run -p increparse-chumsky --example mini_lang_chumsky`.
19
20use std::error::Error;
21
22use chumsky::prelude::*;
23use increparse::{
24    CancelToken, Engine, NodeId, Outcome, ParseTree, Pass, Schedule, SerialExecutor, Session, Span,
25};
26use increparse_chumsky::{chumsky_pass, ChumChildren};
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29enum LangCtx {
30    File,
31    Function { name: String, params: Vec<String> },
32    Return { function: String },
33}
34
35/// Round 0: every `def name(params) { ... }` in the file.
36///
37/// MiniLang bodies contain no nested braces, so a body is "everything up to
38/// the next `}`"; malformed definitions are skipped by `recover_with`.
39fn functions_parser<'a>(
40) -> impl Parser<'a, &'a str, ChumChildren<LangCtx>, extra::Err<Rich<'a, char>>> {
41    let ident = text::ident().map(|s: &str| s.to_string());
42    let params = ident
43        .padded()
44        .separated_by(just(',').padded())
45        .allow_trailing()
46        .collect::<Vec<String>>()
47        .delimited_by(just('('), just(')'))
48        .padded();
49    // No nested braces in MiniLang bodies.
50    let body = none_of('}').repeated().to_slice();
51
52    just("def")
53        .ignore_then(ident.padded())
54        .then(params)
55        .then(just('{').ignore_then(body).then_ignore(just('}')))
56        .map_with(
57            |((name, params), _body): ((String, Vec<String>), &str), e| {
58                let span: SimpleSpan = e.span();
59                vec![(span, LangCtx::Function { name, params })]
60            },
61        )
62        .padded()
63        .or(any().to(Vec::new()))
64        .repeated()
65        .collect::<Vec<Vec<(SimpleSpan, LangCtx)>>>()
66        .map(|defs| {
67            defs.into_iter()
68                .flatten()
69                .collect::<Vec<(SimpleSpan, LangCtx)>>()
70        })
71        .then_ignore(any().repeated())
72}
73
74struct FunctionsPass;
75
76impl Pass for FunctionsPass {
77    type Ctx = LangCtx;
78
79    fn parse(&self, source: &str, span: Span, ctx: &LangCtx) -> Outcome<LangCtx> {
80        if !matches!(ctx, LangCtx::File) {
81            return Outcome::Failed;
82        }
83        chumsky_pass(|slice: &str| functions_parser().parse(slice)).parse(source, span, ctx)
84    }
85}
86
87/// Round 1: expand `return …;` statements (hand-rolled — passes mix freely).
88struct BodyPass;
89
90impl Pass for BodyPass {
91    type Ctx = LangCtx;
92
93    fn parse(&self, source: &str, span: Span, ctx: &LangCtx) -> Outcome<LangCtx> {
94        let LangCtx::Function { name, .. } = ctx else {
95            return Outcome::Failed;
96        };
97        let bytes = source.as_bytes();
98        let mut children = Vec::new();
99        let mut i = span.start;
100        while i < span.end {
101            while i < span.end && bytes[i].is_ascii_whitespace() {
102                i += 1;
103            }
104            if source[i..].starts_with("return") {
105                let Some(semi) = (i..span.end).find(|&j| bytes[j] == b';') else {
106                    break;
107                };
108                children.push((
109                    Span::new(i, semi + 1, span.rev),
110                    LangCtx::Return {
111                        function: name.clone(),
112                    },
113                ));
114                i = semi + 1;
115            } else {
116                i += 1;
117            }
118        }
119        Outcome::Expand(children)
120    }
121}
122
123/// Round 2: the checker — an empty `return;` fails and stays as a leaf.
124struct ReturnPass;
125
126impl Pass for ReturnPass {
127    type Ctx = LangCtx;
128
129    fn parse(&self, source: &str, span: Span, ctx: &LangCtx) -> Outcome<LangCtx> {
130        let LangCtx::Return { function: _ } = ctx else {
131            return Outcome::Failed;
132        };
133        let text = source[span.to_range()].trim();
134        let expr = text
135            .strip_prefix("return")
136            .and_then(|rest| rest.strip_suffix(';'))
137            .map(str::trim)
138            .unwrap_or("");
139        if expr.is_empty() {
140            Outcome::Failed
141        } else {
142            Outcome::Done
143        }
144    }
145}
146
147fn function_ids(tree: &ParseTree<LangCtx>) -> Vec<(String, NodeId)> {
148    tree.nodes()
149        .filter_map(|id| match tree.ctx(id) {
150            LangCtx::Function { name, .. } => Some((name.clone(), id)),
151            _ => None,
152        })
153        .collect()
154}
155
156fn dump(tree: &ParseTree<LangCtx>, source: &str, id: NodeId, depth: usize) {
157    let indent = "  ".repeat(depth);
158    let text = tree.text(source, id).replace('\n', "\\n");
159    let text = if text.len() > 40 {
160        format!("{}...", &text[..40])
161    } else {
162        text
163    };
164    println!(
165        "{indent}{} {:?} {} {:?} {:?}",
166        depth,
167        tree.status(id),
168        tree.span(id),
169        tree.ctx(id),
170        text
171    );
172    for child in tree.children(id) {
173        dump(tree, source, *child, depth + 1);
174    }
175}
176
177fn main() -> Result<(), Box<dyn Error>> {
178    let source = r#"
179def add(a, b) { return a + b; }
180def bad(x) { return; }
181def noise( { return broken;
182def zero() { return 0; }
183"#;
184
185    let mut schedule = Schedule::new();
186    schedule.push(FunctionsPass);
187    schedule.push(BodyPass);
188    schedule.push(ReturnPass);
189    let engine = Engine::new(schedule);
190
191    let mut session: Session<LangCtx> =
192        Session::new(0, Span::new(0, source.len(), 0), LangCtx::File);
193    let report = session.run(&engine, source, &SerialExecutor, &CancelToken::new());
194    println!("report: {report:?}\n");
195    dump(session.tree(), source, session.tree().root(), 0);
196
197    assert!(
198        function_ids(session.tree())
199            .iter()
200            .map(|(n, _)| n.as_str())
201            .collect::<Vec<_>>()
202            == ["add", "bad", "zero"],
203        "three well-formed functions captured via chumsky"
204    );
205
206    // Incremental: append a function — only the new chain is re-parsed.
207    let mut source = source.to_string();
208    let appended = "\ndef ten() { return 10; }\n";
209    session.edit(increparse::Edit::insert(source.len(), appended.len()));
210    source.push_str(appended);
211    let report = session.run(&engine, &source, &SerialExecutor, &CancelToken::new());
212    println!("\nafter append: {report:?}");
213    assert_eq!(report.nodes_processed, 3, "root + `ten` + its return only");
214
215    Ok(())
216}