Skip to main content

m2_syn/cst/
cst.rs

1//! Typed syntax traits and the migration bridge from external CSTs.
2//!
3//! [`Token`] and generated node fields are the typed graph.
4//! [`ExternalCstNode`] and [`Migrate`] 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, Terminal, TokenTree, TrailingTrivia};
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 migration.
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 migrate 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 migration 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: Terminal + 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    /// Validates a raw atom and attaches the non-space trivia following it.
87    fn from_token_tree_with_trivia(
88        token: TokenTree,
89        trailing_trivia: TrailingTrivia,
90    ) -> Option<Self> {
91        let mut token = Self::from_token_tree(token)?;
92        *token.trailing_trivia_mut() = trailing_trivia;
93        Some(token)
94    }
95}
96
97/// Migrates one typed value from a parser backend's untyped CST node.
98pub trait Migrate<N: ExternalCstNode>: Sized {
99    /// Reports whether `node` can migrate this typed value.
100    fn matches(node: &N) -> bool;
101    /// Converts a matching external node into typed syntax.
102    ///
103    /// # Errors
104    ///
105    /// Returns a [`MigrateError`] when the node or its children do not
106    /// match the requested typed syntax.
107    fn migrate(node: N) -> Result<Self, MigrateError>;
108}
109
110/// Single-pass field selection used only by generated migration code.
111pub struct ChildCursor<N> {
112    children: Vec<Option<ExternalCstChild<N>>>,
113    parent: String,
114}
115
116impl<N: ExternalCstNode> ChildCursor<N> {
117    /// Collects the children of `parent` for single-pass field selection.
118    pub fn new(parent: &N) -> Self {
119        Self {
120            children: parent.children().map(Some).collect(),
121            parent: parent.identity().name.into_owned(),
122        }
123    }
124
125    /// Takes the child in required named `field`.
126    ///
127    /// # Errors
128    ///
129    /// Returns [`MigrateError::MissingField`] when no child occupies it.
130    pub fn required_field(&mut self, field: &'static str) -> Result<N, MigrateError> {
131        self.optional_field(field)
132            .ok_or_else(|| MigrateError::MissingField {
133                node: self.parent.clone(),
134                field,
135            })
136    }
137
138    /// Takes the child in optional named `field`.
139    pub fn optional_field(&mut self, field: &'static str) -> Option<N> {
140        self.take_first(|child| !child.node.is_extra() && child.field == Some(field))
141    }
142
143    /// Takes every child in repeated named `field`.
144    pub fn repeated_field(&mut self, field: &'static str) -> Vec<N> {
145        self.take_all(|child| !child.node.is_extra() && child.field == Some(field))
146    }
147
148    /// Takes a required positional child migrateible as `T`.
149    ///
150    /// # Errors
151    ///
152    /// Returns [`MigrateError::MissingChild`] when no remaining positional
153    /// child can migrate `T`.
154    pub fn required_matching<T>(&mut self) -> Result<N, MigrateError>
155    where
156        T: Migrate<N>,
157    {
158        self.optional_matching::<T>()
159            .ok_or_else(|| MigrateError::MissingChild {
160                node: self.parent.clone(),
161                expected: std::any::type_name::<T>(),
162            })
163    }
164
165    /// Takes an optional positional child migrateible as `T`.
166    pub fn optional_matching<T>(&mut self) -> Option<N>
167    where
168        T: Migrate<N>,
169    {
170        self.take_first(|child| {
171            !child.node.is_extra() && child.field.is_none() && T::matches(&child.node)
172        })
173    }
174
175    /// Takes every positional child migrateible as `T`.
176    pub fn repeated_matching<T>(&mut self) -> Vec<N>
177    where
178        T: Migrate<N>,
179    {
180        self.take_all(|child| {
181            !child.node.is_extra() && child.field.is_none() && T::matches(&child.node)
182        })
183    }
184
185    /// Takes a required anonymous delimiter or punctuation child.
186    ///
187    /// # Errors
188    ///
189    /// Returns [`MigrateError::MissingChild`] when the child is absent.
190    pub fn required_anonymous(&mut self, spelling: &'static str) -> Result<N, MigrateError> {
191        self.take_first(|child| {
192            child.field.is_none() && child.node.identity().matches(spelling, false)
193        })
194        .ok_or_else(|| MigrateError::MissingChild {
195            node: self.parent.clone(),
196            expected: spelling,
197        })
198    }
199
200    /// Takes an optional anonymous delimiter or punctuation child.
201    pub fn optional_anonymous(&mut self, spelling: &'static str) -> Option<N> {
202        self.take_first(|child| {
203            child.field.is_none() && child.node.identity().matches(spelling, false)
204        })
205    }
206
207    /// Migrates and consumes one of the supported delimiter pairs.
208    ///
209    /// # Errors
210    ///
211    /// Returns [`MigrateError::MissingChild`] when no opening delimiter or
212    /// its matching closing delimiter remains.
213    pub fn required_delimiter(&mut self) -> Result<Delimiter, MigrateError> {
214        for kind in [
215            DelimiterKind::Parenthesis,
216            DelimiterKind::Bracket,
217            DelimiterKind::Brace,
218            DelimiterKind::AngleBar,
219        ] {
220            let Some(opening) = self.optional_anonymous(kind.opening()) else {
221                continue;
222            };
223            let closing = self.required_anonymous(kind.closing())?;
224            return Ok(Delimiter::new(
225                kind,
226                DoubleSpan::new(opening.expect_span(), closing.expect_span()),
227            ));
228        }
229        Err(MigrateError::MissingChild {
230            node: self.parent.clone(),
231            expected: "a delimiter",
232        })
233    }
234
235    /// Pairs a nonempty list of migrated value nodes with the anonymous
236    /// punctuation nodes separating them.
237    ///
238    /// # Errors
239    ///
240    /// Returns [`MigrateError::InvalidPunctuated`] when there is no first
241    /// value or the parent does not contain exactly one separator between each
242    /// adjacent value.
243    pub fn punctuated(
244        &mut self,
245        values: Vec<N>,
246        spelling: &'static str,
247    ) -> Result<PunctuatedNodes<N>, MigrateError> {
248        let punctuation = self.take_all(|child| {
249            child.field.is_none() && child.node.identity().matches(spelling, false)
250        });
251        let value_count = values.len();
252        let punctuation_count = punctuation.len();
253        let mut values = values.into_iter();
254        let Some(first) = values.next() else {
255            return Err(MigrateError::InvalidPunctuated {
256                node: self.parent.clone(),
257                values: value_count,
258                punctuation: punctuation_count,
259            });
260        };
261        if punctuation_count + 1 != value_count {
262            return Err(MigrateError::InvalidPunctuated {
263                node: self.parent.clone(),
264                values: value_count,
265                punctuation: punctuation_count,
266            });
267        }
268        Ok((first, punctuation.into_iter().zip(values).collect()))
269    }
270
271    /// Pairs an optional punctuated field without inventing a value or a
272    /// separator when the field is absent.
273    ///
274    /// # Errors
275    ///
276    /// Returns [`MigrateError::InvalidPunctuated`] when separators remain
277    /// without values or their count does not match the present values.
278    pub fn optional_punctuated(
279        &mut self,
280        values: Vec<N>,
281        spelling: &'static str,
282    ) -> Result<Option<PunctuatedNodes<N>>, MigrateError> {
283        if values.is_empty() {
284            let punctuation = self.take_all(|child| {
285                child.field.is_none() && child.node.identity().matches(spelling, false)
286            });
287            return if punctuation.is_empty() {
288                Ok(None)
289            } else {
290                Err(MigrateError::InvalidPunctuated {
291                    node: self.parent.clone(),
292                    values: 0,
293                    punctuation: punctuation.len(),
294                })
295            };
296        }
297        self.punctuated(values, spelling).map(Some)
298    }
299
300    fn take_first(&mut self, predicate: impl Fn(&ExternalCstChild<N>) -> bool) -> Option<N> {
301        self.children
302            .iter_mut()
303            .find_map(|child| child.take_if(|child| predicate(child)))
304            .map(|child| child.node)
305    }
306
307    fn take_all(&mut self, predicate: impl Fn(&ExternalCstChild<N>) -> bool) -> Vec<N> {
308        self.children
309            .iter_mut()
310            .filter_map(|child| {
311                child
312                    .take_if(|child| predicate(child))
313                    .map(|child| child.node)
314            })
315            .collect()
316    }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
320/// A typed migration failure.
321pub enum MigrateError {
322    /// A concrete typed node received the wrong backend node kind.
323    WrongNode {
324        /// The required node kind.
325        expected: &'static str,
326        /// Whether the required kind is named.
327        expected_named: bool,
328        /// The received node kind.
329        actual: String,
330        /// Whether the received kind is named.
331        actual_named: bool,
332    },
333    /// No alternative in a typed category matched the backend node.
334    WrongCategory {
335        /// The typed category name.
336        category: &'static str,
337        /// The received node kind.
338        actual: String,
339        /// Whether the received kind is named.
340        actual_named: bool,
341    },
342    /// A required named field was absent.
343    MissingField {
344        /// The parent node kind.
345        node: String,
346        /// The absent grammar field.
347        field: &'static str,
348    },
349    /// A required positional or anonymous child was absent.
350    MissingChild {
351        /// The parent node kind.
352        node: String,
353        /// The expected child type or spelling.
354        expected: &'static str,
355    },
356    /// A punctuated field was empty or did not contain one separator between
357    /// every adjacent pair of values.
358    InvalidPunctuated {
359        /// The parent node kind.
360        node: String,
361        /// The number of value nodes found.
362        values: usize,
363        /// The number of punctuation nodes found.
364        punctuation: usize,
365    },
366}
367
368impl MigrateError {
369    /// Creates a concrete node-kind mismatch.
370    pub fn wrong_node(
371        expected: &'static str,
372        expected_named: bool,
373        actual: CSTNodeClassLabel<'_>,
374    ) -> Self {
375        Self::WrongNode {
376            expected,
377            expected_named,
378            actual: actual.name.into_owned(),
379            actual_named: actual.named,
380        }
381    }
382
383    /// Creates a category mismatch.
384    pub fn wrong_category(category: &'static str, actual: CSTNodeClassLabel<'_>) -> Self {
385        Self::WrongCategory {
386            category,
387            actual: actual.name.into_owned(),
388            actual_named: actual.named,
389        }
390    }
391}
392
393impl Display for MigrateError {
394    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
395        match self {
396            Self::WrongNode {
397                expected,
398                expected_named,
399                actual,
400                actual_named,
401            } => write!(
402                formatter,
403                "expected {} node {expected:?}, found {} node {actual:?}",
404                visibility(*expected_named),
405                visibility(*actual_named),
406            ),
407            Self::WrongCategory {
408                category,
409                actual,
410                actual_named,
411            } => write!(
412                formatter,
413                "node {} {actual:?} does not belong to {category}",
414                visibility(*actual_named),
415            ),
416            Self::MissingField { node, field } => {
417                write!(formatter, "node {node:?} has no {field:?} field")
418            }
419            Self::MissingChild { node, expected } => {
420                write!(formatter, "node {node:?} has no child matching {expected}")
421            }
422            Self::InvalidPunctuated {
423                node,
424                values,
425                punctuation,
426            } => write!(
427                formatter,
428                "node {node:?} has {values} punctuated values but {punctuation} separators",
429            ),
430        }
431    }
432}
433
434impl Error for MigrateError {}
435
436fn visibility(named: bool) -> &'static str {
437    if named { "named" } else { "anonymous" }
438}