Skip to main content

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