Skip to main content

increparse_chumsky/
lib.rs

1//! Wrap [chumsky](https://docs.rs/chumsky) parsers in increparse
2//! [`Pass`](increparse::Pass)es.
3//!
4//! chumsky 0.10 reports spans as [`SimpleSpan`]s relative to the input slice
5//! it parsed; increparse needs *absolute* spans carrying a source revision —
6//! and the incremental tree only reuses regions whose spans match exactly.
7//! This crate does the translation once, correctly, so your chumsky parsers
8//! can stay in slice-relative coordinates.
9//!
10//! A pass is a function from the region's slice to a chumsky
11//! [`ParseResult`] whose output is the parsed children: each child is a
12//! **slice-relative** [`SimpleSpan`] plus a context value for the next
13//! round. The wrapper converts the result to an
14//! [`Outcome`](increparse::Outcome):
15//!
16//! * parse succeeded with children → `Outcome::Expand` with rebased
17//!   absolute spans,
18//! * succeeded with no children → `Outcome::Done`,
19//! * parse failed → `Outcome::Failed` (errors dropped; the region stays in
20//!   the tree as a leaf and later passes may retry it).
21//!
22//! Note: chumsky's `Parser::parse` implicitly requires the *whole* input to
23//! be consumed. To claim children and ignore the rest of the region, end
24//! your parser with `.then_ignore(any().repeated())`.
25//!
26//! Because parsers are typically built per-call (they are cheap), the
27//! closure receives the slice and runs its parser itself:
28//!
29//! # Examples
30//!
31//! ```
32//! use chumsky::prelude::*;
33//! use increparse::{Outcome, Pass, Span};
34//! use increparse_chumsky::{chumsky_pass, ChumChildren};
35//!
36//! // Parsers are built per call, tied to the input's lifetime.
37//! fn split_at_hi(slice: &str) -> ParseResult<ChumChildren<()>, Rich<'_, char>> {
38//!     fn parser<'a>() -> impl Parser<'a, &'a str, ChumChildren<()>, extra::Err<Rich<'a, char>>> {
39//!         just("hi")
40//!             .map_with(|_out, e| vec![(e.span(), ())])
41//!             .then_ignore(any().repeated())
42//!     }
43//!     parser().parse(slice)
44//! }
45//!
46//! let pass = chumsky_pass(split_at_hi);
47//! let source = "hi there";
48//! match pass.parse(source, Span::new(0, source.len(), 0), &()) {
49//!     Outcome::Expand(children) => {
50//!         // The child span is absolute, even though chumsky saw a slice.
51//!         assert_eq!(children[0].0.start, 0);
52//!         assert_eq!(children[0].0.end, 2);
53//!     }
54//!     _ => panic!("expected expansion"),
55//! }
56//! ```
57//!
58//! Spans you emit are validated by the engine like any other: they must be
59//! contained in the region and (unless
60//! [`EngineConfig::enforce_shrink`](increparse::EngineConfig::enforce_shrink)
61//! is disabled) strictly smaller.
62
63#![forbid(unsafe_code)]
64#![deny(missing_docs)]
65
66use chumsky::prelude::Rich;
67use chumsky::span::SimpleSpan;
68use chumsky::ParseResult;
69use increparse::{Outcome, Pass, Span};
70
71/// The children a chumsky parser produces: slice-relative spans plus the
72/// context each child carries into the next round.
73pub type ChumChildren<C> = Vec<(SimpleSpan, C)>;
74
75/// A [`Pass`] driven by a chumsky parser.
76///
77/// See the [crate docs](self) for the conversion rules. Build one with
78/// [`chumsky_pass`].
79#[derive(Debug, Clone, Copy)]
80pub struct ChumskyPass<F> {
81    f: F,
82}
83
84/// Creates a pass from a closure that parses a slice into [`ChumChildren`].
85///
86/// chumsky 0.10's [`Parser`] trait is tied to the input lifetime, so parsers
87/// are built per call (they are cheap) by a small factory function; the
88/// closure builds one and runs it against the slice:
89///
90/// ```
91/// use chumsky::prelude::*;
92/// use increparse::{Outcome, Pass, Span};
93/// use increparse_chumsky::{chumsky_pass, ChumChildren};
94///
95/// #[derive(Clone, Debug, PartialEq)]
96/// enum Ctx {
97///     Ident,
98/// }
99///
100/// // The factory ties the parser to the input lifetime...
101/// fn idents<'a>() -> impl Parser<'a, &'a str, ChumChildren<Ctx>, extra::Err<Rich<'a, char>>> {
102///     text::ident()
103///         .map_with(|_name, e| vec![(e.span(), Ctx::Ident)])
104///         .then_ignore(any().repeated())
105/// }
106///
107/// // ...and the closure is the pass body.
108/// fn idents_pass(slice: &str) -> ParseResult<ChumChildren<Ctx>, Rich<'_, char>> {
109///     idents().parse(slice)
110/// }
111///
112/// let pass = chumsky_pass(idents_pass);
113/// let source = "one two";
114/// assert!(matches!(pass.parse(source, Span::new(0, source.len(), 0), &Ctx::Ident),
115///     Outcome::Expand(_)));
116/// ```
117pub fn chumsky_pass<F, C>(f: F) -> ChumskyPass<F>
118where
119    F: for<'a> Fn(&'a str) -> ParseResult<ChumChildren<C>, Rich<'a, char>>,
120{
121    ChumskyPass { f }
122}
123
124impl<C, F> Pass for ChumskyPass<F>
125where
126    F: for<'a> Fn(&'a str) -> ParseResult<ChumChildren<C>, Rich<'a, char>> + Send + Sync,
127{
128    type Ctx = C;
129
130    fn parse(&self, source: &str, span: Span, _ctx: &C) -> Outcome<C> {
131        let slice = &source[span.to_range()];
132        let result = (self.f)(slice).into_result();
133        match result {
134            Ok(children) => {
135                if children.is_empty() {
136                    return Outcome::Done;
137                }
138                let children = children
139                    .into_iter()
140                    .map(|(simple, ctx)| {
141                        (
142                            Span::new(span.start + simple.start, span.start + simple.end, span.rev),
143                            ctx,
144                        )
145                    })
146                    .collect();
147                Outcome::Expand(children)
148            }
149            Err(_) => Outcome::Failed,
150        }
151    }
152
153    fn name(&self) -> &'static str {
154        "ChumskyPass"
155    }
156}