Skip to main content

m2_syn/cst/
cst.rs

1//! Typed syntax traits and the reconstruction bridge from external CSTs.
2//!
3//! [`Token`] and generated node fields are the typed graph.
4//! [`ExternalCstNode`] and [`Reconstruct`] are an adapter-only seam for parsers such as
5//! Tree-sitter that first produce an untyped concrete tree. They are not a
6//! traversal interface for the typed graph; use `Visit`, `VisitMut`, or `Fold`
7//! for that. A parser that can construct typed nodes directly does not need
8//! this bridge.
9
10use std::borrow::Cow;
11use std::error::Error;
12use std::fmt::{Display, Formatter};
13
14use crate::{Delimiter, DelimiterKind, DoubleSpan, Spanned, TokenTree};
15
16type PunctuatedNodes<N> = (N, Vec<(N, N)>);
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19/// Backend-local node identity used only during external CST reconstruction.
20pub struct CSTNodeClassLabel<'syntax> {
21    /// The backend-specific node kind.
22    pub name: Cow<'syntax, str>,
23    /// Whether the backend exposes the node as named syntax.
24    pub named: bool,
25}
26
27impl<'syntax> CSTNodeClassLabel<'syntax> {
28    /// Creates a backend node identity.
29    pub fn new(name: impl Into<Cow<'syntax, str>>, named: bool) -> Self {
30        Self {
31            name: name.into(),
32            named,
33        }
34    }
35
36    /// Reports whether both the kind and namedness match.
37    pub fn matches(&self, name: &str, named: bool) -> bool {
38        self.named == named && self.name == name
39    }
40}
41
42/// One child supplied by an external CST backend.
43pub struct ExternalCstChild<N> {
44    /// The grammar field name, when the child occupies a named field.
45    pub field: Option<&'static str>,
46    /// The backend-local child node.
47    pub node: N,
48}
49
50/// Minimal untyped view required to reconstruct generated typed nodes.
51///
52/// This trait is deliberately confined to parser adapters. Its homogeneous
53/// child iterator must not be used to model or walk the typed syntax graph.
54pub trait ExternalCstNode: Sized + Spanned {
55    /// The iterator returned by [`Self::children`].
56    type Children<'syntax>: Iterator<Item = ExternalCstChild<Self>>
57    where
58        Self: 'syntax;
59
60    /// Returns the node kind used by reconstruction matching.
61    fn identity(&self) -> CSTNodeClassLabel<'_>;
62    /// Returns the node's immediate children in source order.
63    fn children(&self) -> Self::Children<'_>;
64    /// Returns the exact source text covered by the node.
65    fn text(&self) -> Cow<'_, str>;
66    /// Reports whether this is parser-added extra syntax such as a comment.
67    fn is_extra(&self) -> bool {
68        false
69    }
70}
71
72/// A generated closed token atom backed by one matching raw [`TokenTree`].
73///
74/// Parsing validates the raw category and spelling, then retains the raw atom
75/// so emission preserves its source text and span.
76pub trait Token: Spanned + Sized {
77    /// Canonical spelling used in diagnostics and freshly constructed atoms.
78    const SPELLING: &'static str;
79
80    /// Tests whether a raw atom belongs to this closed token type.
81    fn matches_token_tree(token: &TokenTree) -> bool;
82
83    /// Validates and retains a raw atom as this typed token.
84    fn from_token_tree(token: TokenTree) -> Option<Self>;
85}
86
87/// Reconstructs one typed value from a parser backend's untyped CST node.
88pub trait Reconstruct<N: ExternalCstNode>: Sized {
89    /// Reports whether `node` can reconstruct this typed value.
90    fn matches(node: &N) -> bool;
91    /// Converts a matching external node into typed syntax.
92    ///
93    /// # Errors
94    ///
95    /// Returns a [`ReconstructError`] when the node or its children do not
96    /// match the requested typed syntax.
97    fn reconstruct(node: N) -> Result<Self, ReconstructError>;
98}
99
100/// Single-pass field selection used only by generated reconstruction code.
101pub struct ChildCursor<N> {
102    children: Vec<Option<ExternalCstChild<N>>>,
103    parent: String,
104}
105
106impl<N: ExternalCstNode> ChildCursor<N> {
107    /// Collects the children of `parent` for single-pass field selection.
108    pub fn new(parent: &N) -> Self {
109        Self {
110            children: parent.children().map(Some).collect(),
111            parent: parent.identity().name.into_owned(),
112        }
113    }
114
115    /// Takes the child in required named `field`.
116    ///
117    /// # Errors
118    ///
119    /// Returns [`ReconstructError::MissingField`] when no child occupies it.
120    pub fn required_field(&mut self, field: &'static str) -> Result<N, ReconstructError> {
121        self.optional_field(field)
122            .ok_or_else(|| ReconstructError::MissingField {
123                node: self.parent.clone(),
124                field,
125            })
126    }
127
128    /// Takes the child in optional named `field`.
129    pub fn optional_field(&mut self, field: &'static str) -> Option<N> {
130        self.take_first(|child| !child.node.is_extra() && child.field == Some(field))
131    }
132
133    /// Takes every child in repeated named `field`.
134    pub fn repeated_field(&mut self, field: &'static str) -> Vec<N> {
135        self.take_all(|child| !child.node.is_extra() && child.field == Some(field))
136    }
137
138    /// Takes a required positional child reconstructible as `T`.
139    ///
140    /// # Errors
141    ///
142    /// Returns [`ReconstructError::MissingChild`] when no remaining positional
143    /// child can reconstruct `T`.
144    pub fn required_matching<T>(&mut self) -> Result<N, ReconstructError>
145    where
146        T: Reconstruct<N>,
147    {
148        self.optional_matching::<T>()
149            .ok_or_else(|| ReconstructError::MissingChild {
150                node: self.parent.clone(),
151                expected: std::any::type_name::<T>(),
152            })
153    }
154
155    /// Takes an optional positional child reconstructible as `T`.
156    pub fn optional_matching<T>(&mut self) -> Option<N>
157    where
158        T: Reconstruct<N>,
159    {
160        self.take_first(|child| {
161            !child.node.is_extra() && child.field.is_none() && T::matches(&child.node)
162        })
163    }
164
165    /// Takes every positional child reconstructible as `T`.
166    pub fn repeated_matching<T>(&mut self) -> Vec<N>
167    where
168        T: Reconstruct<N>,
169    {
170        self.take_all(|child| {
171            !child.node.is_extra() && child.field.is_none() && T::matches(&child.node)
172        })
173    }
174
175    /// Takes a required anonymous delimiter or punctuation child.
176    ///
177    /// # Errors
178    ///
179    /// Returns [`ReconstructError::MissingChild`] when the child is absent.
180    pub fn required_anonymous(&mut self, spelling: &'static str) -> Result<N, ReconstructError> {
181        self.take_first(|child| {
182            child.field.is_none() && child.node.identity().matches(spelling, false)
183        })
184        .ok_or_else(|| ReconstructError::MissingChild {
185            node: self.parent.clone(),
186            expected: spelling,
187        })
188    }
189
190    /// Takes an optional anonymous delimiter or punctuation child.
191    pub fn optional_anonymous(&mut self, spelling: &'static str) -> Option<N> {
192        self.take_first(|child| {
193            child.field.is_none() && child.node.identity().matches(spelling, false)
194        })
195    }
196
197    /// Reconstructs and consumes one of the supported delimiter pairs.
198    ///
199    /// # Errors
200    ///
201    /// Returns [`ReconstructError::MissingChild`] when no opening delimiter or
202    /// its matching closing delimiter remains.
203    pub fn required_delimiter(&mut self) -> Result<Delimiter, ReconstructError> {
204        for kind in [
205            DelimiterKind::Parenthesis,
206            DelimiterKind::Bracket,
207            DelimiterKind::Brace,
208            DelimiterKind::AngleBar,
209        ] {
210            let Some(opening) = self.optional_anonymous(kind.opening()) else {
211                continue;
212            };
213            let closing = self.required_anonymous(kind.closing())?;
214            return Ok(Delimiter::new(
215                kind,
216                DoubleSpan::new(opening.span(), closing.span()),
217            ));
218        }
219        Err(ReconstructError::MissingChild {
220            node: self.parent.clone(),
221            expected: "a delimiter",
222        })
223    }
224
225    /// Pairs a nonempty list of reconstructed value nodes with the anonymous
226    /// punctuation nodes separating them.
227    ///
228    /// # Errors
229    ///
230    /// Returns [`ReconstructError::InvalidPunctuated`] when there is no first
231    /// value or the parent does not contain exactly one separator between each
232    /// adjacent value.
233    pub fn punctuated(
234        &mut self,
235        values: Vec<N>,
236        spelling: &'static str,
237    ) -> Result<PunctuatedNodes<N>, ReconstructError> {
238        let punctuation = self.take_all(|child| {
239            child.field.is_none() && child.node.identity().matches(spelling, false)
240        });
241        let value_count = values.len();
242        let punctuation_count = punctuation.len();
243        let mut values = values.into_iter();
244        let Some(first) = values.next() else {
245            return Err(ReconstructError::InvalidPunctuated {
246                node: self.parent.clone(),
247                values: value_count,
248                punctuation: punctuation_count,
249            });
250        };
251        if punctuation_count + 1 != value_count {
252            return Err(ReconstructError::InvalidPunctuated {
253                node: self.parent.clone(),
254                values: value_count,
255                punctuation: punctuation_count,
256            });
257        }
258        Ok((first, punctuation.into_iter().zip(values).collect()))
259    }
260
261    /// Pairs an optional punctuated field without inventing a value or a
262    /// separator when the field is absent.
263    ///
264    /// # Errors
265    ///
266    /// Returns [`ReconstructError::InvalidPunctuated`] when separators remain
267    /// without values or their count does not match the present values.
268    pub fn optional_punctuated(
269        &mut self,
270        values: Vec<N>,
271        spelling: &'static str,
272    ) -> Result<Option<PunctuatedNodes<N>>, ReconstructError> {
273        if values.is_empty() {
274            let punctuation = self.take_all(|child| {
275                child.field.is_none() && child.node.identity().matches(spelling, false)
276            });
277            return if punctuation.is_empty() {
278                Ok(None)
279            } else {
280                Err(ReconstructError::InvalidPunctuated {
281                    node: self.parent.clone(),
282                    values: 0,
283                    punctuation: punctuation.len(),
284                })
285            };
286        }
287        self.punctuated(values, spelling).map(Some)
288    }
289
290    fn take_first(&mut self, predicate: impl Fn(&ExternalCstChild<N>) -> bool) -> Option<N> {
291        self.children
292            .iter_mut()
293            .find_map(|child| child.take_if(|child| predicate(child)))
294            .map(|child| child.node)
295    }
296
297    fn take_all(&mut self, predicate: impl Fn(&ExternalCstChild<N>) -> bool) -> Vec<N> {
298        self.children
299            .iter_mut()
300            .filter_map(|child| {
301                child
302                    .take_if(|child| predicate(child))
303                    .map(|child| child.node)
304            })
305            .collect()
306    }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
310/// A typed reconstruction failure.
311pub enum ReconstructError {
312    /// A concrete typed node received the wrong backend node kind.
313    WrongNode {
314        /// The required node kind.
315        expected: &'static str,
316        /// Whether the required kind is named.
317        expected_named: bool,
318        /// The received node kind.
319        actual: String,
320        /// Whether the received kind is named.
321        actual_named: bool,
322    },
323    /// No alternative in a typed category matched the backend node.
324    WrongCategory {
325        /// The typed category name.
326        category: &'static str,
327        /// The received node kind.
328        actual: String,
329        /// Whether the received kind is named.
330        actual_named: bool,
331    },
332    /// A required named field was absent.
333    MissingField {
334        /// The parent node kind.
335        node: String,
336        /// The absent grammar field.
337        field: &'static str,
338    },
339    /// A required positional or anonymous child was absent.
340    MissingChild {
341        /// The parent node kind.
342        node: String,
343        /// The expected child type or spelling.
344        expected: &'static str,
345    },
346    /// A punctuated field was empty or did not contain one separator between
347    /// every adjacent pair of values.
348    InvalidPunctuated {
349        /// The parent node kind.
350        node: String,
351        /// The number of value nodes found.
352        values: usize,
353        /// The number of punctuation nodes found.
354        punctuation: usize,
355    },
356}
357
358impl ReconstructError {
359    /// Creates a concrete node-kind mismatch.
360    pub fn wrong_node(
361        expected: &'static str,
362        expected_named: bool,
363        actual: CSTNodeClassLabel<'_>,
364    ) -> Self {
365        Self::WrongNode {
366            expected,
367            expected_named,
368            actual: actual.name.into_owned(),
369            actual_named: actual.named,
370        }
371    }
372
373    /// Creates a category mismatch.
374    pub fn wrong_category(category: &'static str, actual: CSTNodeClassLabel<'_>) -> Self {
375        Self::WrongCategory {
376            category,
377            actual: actual.name.into_owned(),
378            actual_named: actual.named,
379        }
380    }
381}
382
383impl Display for ReconstructError {
384    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
385        match self {
386            Self::WrongNode {
387                expected,
388                expected_named,
389                actual,
390                actual_named,
391            } => write!(
392                formatter,
393                "expected {} node {expected:?}, found {} node {actual:?}",
394                visibility(*expected_named),
395                visibility(*actual_named),
396            ),
397            Self::WrongCategory {
398                category,
399                actual,
400                actual_named,
401            } => write!(
402                formatter,
403                "node {} {actual:?} does not belong to {category}",
404                visibility(*actual_named),
405            ),
406            Self::MissingField { node, field } => {
407                write!(formatter, "node {node:?} has no {field:?} field")
408            }
409            Self::MissingChild { node, expected } => {
410                write!(formatter, "node {node:?} has no child matching {expected}")
411            }
412            Self::InvalidPunctuated {
413                node,
414                values,
415                punctuation,
416            } => write!(
417                formatter,
418                "node {node:?} has {values} punctuated values but {punctuation} separators",
419            ),
420        }
421    }
422}
423
424impl Error for ReconstructError {}
425
426fn visibility(named: bool) -> &'static str {
427    if named { "named" } else { "anonymous" }
428}