Skip to main content

mini_lang_nom/
mini_lang_nom.rs

1//! The mini language's function skeleton parsed with **nom** instead of
2//! hand-rolled scanning — the `increparse-nom` showcase.
3//!
4//! Compare with `examples/mini_lang.rs` in the core crate: the pass is the
5//! same shape, but the scanning is nom combinators emitting *slice-relative*
6//! ranges; `increparse_nom::nom_pass` rebases them to absolute spans so the
7//! incremental tree can reuse regions across edits.
8//!
9//! Note the mixed schedule: round 0 is a nom pass, rounds 1–2 are
10//! hand-rolled. Passes are independent — mix freely.
11//!
12//! Run with `cargo run -p increparse-nom --example mini_lang_nom`.
13
14use std::error::Error;
15use std::ops::Range;
16
17use increparse::{
18    CancelToken, Engine, NodeId, Outcome, ParseTree, Pass, Schedule, SerialExecutor, Session, Span,
19    Status,
20};
21use increparse_nom::{nom_pass, NomChildren};
22use nom::branch::alt;
23use nom::bytes::complete::{tag, take, take_till, take_while1};
24use nom::character::complete::{anychar, multispace0};
25use nom::combinator::{map, opt, recognize};
26use nom::multi::many0;
27use nom::IResult;
28use nom::Parser;
29use nom_locate::LocatedSpan;
30
31type Located<'a> = LocatedSpan<&'a str>;
32
33#[derive(Clone, Debug, PartialEq, Eq)]
34enum LangCtx {
35    File,
36    Function { name: String, params: Vec<String> },
37    Return { function: String },
38}
39
40fn ws(i: Located) -> IResult<Located, Located> {
41    recognize(multispace0).parse(i)
42}
43
44fn ident(i: Located) -> IResult<Located, Located> {
45    let (i, s) = take_while1(|c: char| c.is_ascii_alphanumeric() || c == '_').parse(i)?;
46    if s.fragment()
47        .chars()
48        .next()
49        .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
50    {
51        Ok((i, s))
52    } else {
53        Err(nom::Err::Error(nom::error::Error::new(
54            i,
55            nom::error::ErrorKind::Verify,
56        )))
57    }
58}
59
60/// Recognizes a balanced `{ ... }` block. Combinators don't count nesting,
61/// so this is a tiny custom scanner wearing a nom signature.
62fn braced(i: Located) -> IResult<Located, Range<usize>> {
63    let base = i.location_offset();
64    let bytes = i.fragment().as_bytes();
65    if bytes.first() != Some(&b'{') {
66        return Err(nom::Err::Error(nom::error::Error::new(
67            i,
68            nom::error::ErrorKind::Tag,
69        )));
70    }
71    let mut depth = 0usize;
72    let mut len = None;
73    for (k, &b) in bytes.iter().enumerate() {
74        match b {
75            b'{' => depth += 1,
76            b'}' => {
77                depth -= 1;
78                if depth == 0 {
79                    len = Some(k + 1);
80                    break;
81                }
82            }
83            _ => {}
84        }
85    }
86    let Some(len) = len else {
87        return Err(nom::Err::Error(nom::error::Error::new(
88            i,
89            nom::error::ErrorKind::TakeWhile1,
90        )));
91    };
92    let (i, _) = take(len).parse(i)?;
93    Ok((i, base..base + len))
94}
95
96fn param_list(i: Located) -> IResult<Located, Vec<String>> {
97    let (i, text) = take_till(|c| c == ')' || c == '\n')(i)?;
98    let mut params = Vec::new();
99    for part in text.split(',') {
100        let part = part.trim();
101        if part.is_empty() {
102            continue;
103        }
104        if ident(LocatedSpan::new(part)).is_err() {
105            return Err(nom::Err::Error(nom::error::Error::new(
106                i,
107                nom::error::ErrorKind::Verify,
108            )));
109        }
110        params.push(part.to_string());
111    }
112    Ok((i, params))
113}
114
115/// One `def name(params) { ... }`, as a relative range plus context.
116fn function_def(i: Located) -> IResult<Located, (Range<usize>, LangCtx)> {
117    let start = i.location_offset();
118    let (i, _) = tag("def").parse(i)?;
119    let (i, name_loc) = recognize((ws, ident, multispace0)).parse(i)?;
120    let name = name_loc.fragment().trim().to_string();
121    let (i, _) = tag("(").parse(i)?;
122    let (i, params) = param_list.parse(i)?;
123    let (i, _) = tag(")").parse(i)?;
124    let (i, _) = recognize((multispace0, opt(tag(";")))).parse(i)?;
125    let (i, body) = braced.parse(i)?;
126    let end = body.end;
127    Ok((
128        i,
129        (
130            start..end,
131            LangCtx::Function {
132                name: name.trim_end().to_string(),
133                params,
134            },
135        ),
136    ))
137}
138
139/// The file: skip garbage byte by byte, collect well-formed functions.
140/// Malformed definitions (like `def noise( {`) cost one byte of resync, and
141/// everything after them still parses.
142fn functions_pass_fn(i: Located) -> IResult<Located, NomChildren<LangCtx>> {
143    let (i, defs) = many0(alt((map(function_def, Some), map(anychar, |_| None)))).parse(i)?;
144    Ok((i, defs.into_iter().flatten().collect()))
145}
146
147struct FunctionsPass;
148
149impl Pass for FunctionsPass {
150    type Ctx = LangCtx;
151
152    fn parse(&self, source: &str, span: Span, ctx: &LangCtx) -> Outcome<LangCtx> {
153        if !matches!(ctx, LangCtx::File) {
154            return Outcome::Failed;
155        }
156        nom_pass(functions_pass_fn).parse(source, span, ctx)
157    }
158}
159
160struct BodyPass;
161
162impl Pass for BodyPass {
163    type Ctx = LangCtx;
164
165    fn parse(&self, source: &str, span: Span, ctx: &LangCtx) -> Outcome<LangCtx> {
166        let LangCtx::Function { name, .. } = ctx else {
167            return Outcome::Failed;
168        };
169        let bytes = source.as_bytes();
170        let mut children = Vec::new();
171        let mut i = span.start;
172        while i < span.end {
173            i = skip_ws(bytes, i);
174            if source[i..].starts_with("return") {
175                let Some(semi) = (i..span.end).find(|&j| bytes[j] == b';') else {
176                    break;
177                };
178                children.push((
179                    Span::new(i, semi + 1, span.rev),
180                    LangCtx::Return {
181                        function: name.clone(),
182                    },
183                ));
184                i = semi + 1;
185            } else {
186                i += 1;
187            }
188        }
189        Outcome::Expand(children)
190    }
191}
192
193fn skip_ws(bytes: &[u8], mut i: usize) -> usize {
194    while i < bytes.len() && bytes[i].is_ascii_whitespace() {
195        i += 1;
196    }
197    i
198}
199
200struct ReturnPass;
201
202impl Pass for ReturnPass {
203    type Ctx = LangCtx;
204
205    fn parse(&self, source: &str, span: Span, ctx: &LangCtx) -> Outcome<LangCtx> {
206        let LangCtx::Return { function: _ } = ctx else {
207            return Outcome::Failed;
208        };
209        let text = source[span.to_range()].trim();
210        let expr = text
211            .strip_prefix("return")
212            .and_then(|rest| rest.strip_suffix(';'))
213            .map(str::trim)
214            .unwrap_or("");
215        if expr.is_empty() {
216            Outcome::Failed
217        } else {
218            Outcome::Done
219        }
220    }
221}
222
223fn function_ids(tree: &ParseTree<LangCtx>) -> Vec<(String, NodeId)> {
224    tree.nodes()
225        .filter_map(|id| match tree.ctx(id) {
226            LangCtx::Function { name, .. } => Some((name.clone(), id)),
227            _ => None,
228        })
229        .collect()
230}
231
232fn dump(tree: &ParseTree<LangCtx>, source: &str, id: NodeId, depth: usize) {
233    let indent = "  ".repeat(depth);
234    let text = tree.text(source, id).replace('\n', "\\n");
235    let text = if text.len() > 40 {
236        format!("{}...", &text[..40])
237    } else {
238        text
239    };
240    println!(
241        "{indent}{} {:?} {} {:?} {:?}",
242        depth,
243        tree.status(id),
244        tree.span(id),
245        tree.ctx(id),
246        text
247    );
248    for child in tree.children(id) {
249        dump(tree, source, *child, depth + 1);
250    }
251}
252
253fn main() -> Result<(), Box<dyn Error>> {
254    let source = r#"
255def add(a, b) { return a + b; }
256def bad(x) { return; }
257def noise( { return broken;
258def zero() { return 0; }
259"#;
260
261    let mut schedule = Schedule::new();
262    schedule.push(FunctionsPass);
263    schedule.push(BodyPass);
264    schedule.push(ReturnPass);
265    let engine = Engine::new(schedule);
266
267    let mut session: Session<LangCtx> =
268        Session::new(0, Span::new(0, source.len(), 0), LangCtx::File);
269    let report = session.run(&engine, source, &SerialExecutor, &CancelToken::new());
270    println!("report: {report:?}\n");
271    dump(session.tree(), source, session.tree().root(), 0);
272
273    assert!(
274        function_ids(session.tree())
275            .iter()
276            .map(|(n, _)| n.as_str())
277            .collect::<Vec<_>>()
278            == ["add", "bad", "zero"],
279        "three well-formed functions captured via nom"
280    );
281    assert!(
282        session
283            .tree()
284            .nodes()
285            .any(|id| session.tree().status(id) == Status::Failed),
286        "bad's empty return failed"
287    );
288
289    // Incremental: append a function — only the new chain is re-parsed.
290    let mut source = source.to_string();
291    let appended = "\ndef ten() { return 10; }\n";
292    session.edit(increparse::Edit::insert(source.len(), appended.len()));
293    source.push_str(appended);
294    let report = session.run(&engine, &source, &SerialExecutor, &CancelToken::new());
295    println!("\nafter append: {report:?}");
296    assert_eq!(report.nodes_processed, 3, "root + `ten` + its return only");
297
298    Ok(())
299}