Skip to main content

m2_syn/
cst.rs

1//! Typed syntax traits and the compatibility bridge from external CSTs.
2//!
3//! [`Token`] and generated node fields are the typed graph.
4//! [`CstNode`] 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::{Spanned, TokenTree};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17/// Backend-local node identity used only during external CST reconstruction.
18pub struct CSTNodeClassLabel<'syntax> {
19    pub name: Cow<'syntax, str>,
20    pub named: bool,
21}
22
23impl<'syntax> CSTNodeClassLabel<'syntax> {
24    pub fn new(name: impl Into<Cow<'syntax, str>>, named: bool) -> Self {
25        Self {
26            name: name.into(),
27            named,
28        }
29    }
30
31    pub fn matches(&self, name: &str, named: bool) -> bool {
32        self.named == named && self.name == name
33    }
34}
35
36pub struct ExternalCstChild<N> {
37    pub field: Option<&'static str>,
38    pub node: N,
39}
40
41/// Minimal untyped view required to reconstruct generated typed nodes.
42///
43/// This trait is deliberately confined to parser adapters. Its homogeneous
44/// child iterator must not be used to model or walk the typed syntax graph.
45pub trait ExternalCstNode: Sized + Spanned {
46    type Children<'syntax>: Iterator<Item = ExternalCstChild<Self>>
47    where
48        Self: 'syntax;
49
50    fn identity(&self) -> CSTNodeClassLabel<'_>;
51    fn children(&self) -> Self::Children<'_>;
52    fn text(&self) -> Cow<'_, str>;
53    fn is_extra(&self) -> bool {
54        false
55    }
56}
57
58/// A generated closed token atom backed by one matching raw [`TokenTree`].
59///
60/// Parsing validates the raw category and spelling, then retains the raw atom
61/// so emission preserves distinctions such as implicit whitespace versus the
62/// explicit `SPACE` spelling.
63pub trait Token: Spanned + Sized {
64    /// Canonical spelling used in diagnostics and freshly constructed atoms.
65    const SPELLING: &'static str;
66
67    /// Tests whether a raw atom belongs to this closed token type.
68    fn matches_token_tree(token: &TokenTree) -> bool;
69
70    /// Validates and retains a raw atom as this typed token.
71    fn from_token_tree(token: TokenTree) -> Option<Self>;
72}
73
74/// Reconstructs one typed value from a parser backend's untyped CST node.
75pub trait Reconstruct<N: ExternalCstNode>: Sized {
76    fn matches(node: &N) -> bool;
77    fn reconstruct(node: N) -> Result<Self, ReconstructError>;
78}
79
80/// Single-pass field selection used only by generated reconstruction code.
81pub struct ChildCursor<N> {
82    children: Vec<Option<ExternalCstChild<N>>>,
83    parent: String,
84}
85
86impl<N: ExternalCstNode> ChildCursor<N> {
87    pub fn new(parent: &N) -> Self {
88        Self {
89            children: parent
90                .children()
91                .filter(|child| !child.node.is_extra())
92                .map(Some)
93                .collect(),
94            parent: parent.identity().name.into_owned(),
95        }
96    }
97
98    pub fn required_field(&mut self, field: &'static str) -> Result<N, ReconstructError> {
99        self.optional_field(field)
100            .ok_or_else(|| ReconstructError::MissingField {
101                node: self.parent.clone(),
102                field,
103            })
104    }
105
106    pub fn optional_field(&mut self, field: &'static str) -> Option<N> {
107        self.take_first(|child| child.field == Some(field))
108    }
109
110    pub fn repeated_field(&mut self, field: &'static str) -> Vec<N> {
111        self.take_all(|child| child.field == Some(field))
112    }
113
114    pub fn required_matching<T>(&mut self) -> Result<N, ReconstructError>
115    where
116        T: Reconstruct<N>,
117    {
118        self.optional_matching::<T>()
119            .ok_or_else(|| ReconstructError::MissingChild {
120                node: self.parent.clone(),
121                expected: std::any::type_name::<T>(),
122            })
123    }
124
125    pub fn optional_matching<T>(&mut self) -> Option<N>
126    where
127        T: Reconstruct<N>,
128    {
129        self.take_first(|child| child.field.is_none() && T::matches(&child.node))
130    }
131
132    pub fn repeated_matching<T>(&mut self) -> Vec<N>
133    where
134        T: Reconstruct<N>,
135    {
136        self.take_all(|child| child.field.is_none() && T::matches(&child.node))
137    }
138
139    fn take_first(&mut self, predicate: impl Fn(&ExternalCstChild<N>) -> bool) -> Option<N> {
140        self.children
141            .iter_mut()
142            .find(|child| child.as_ref().is_some_and(&predicate))
143            .and_then(Option::take)
144            .map(|child| child.node)
145    }
146
147    fn take_all(&mut self, predicate: impl Fn(&ExternalCstChild<N>) -> bool) -> Vec<N> {
148        self.children
149            .iter_mut()
150            .filter_map(|child| {
151                child
152                    .as_ref()
153                    .is_some_and(&predicate)
154                    .then(|| child.take().expect("matched child").node)
155            })
156            .collect()
157    }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum ReconstructError {
162    WrongNode {
163        expected: &'static str,
164        expected_named: bool,
165        actual: String,
166        actual_named: bool,
167    },
168    WrongCategory {
169        category: &'static str,
170        actual: String,
171        actual_named: bool,
172    },
173    MissingField {
174        node: String,
175        field: &'static str,
176    },
177    MissingChild {
178        node: String,
179        expected: &'static str,
180    },
181}
182
183impl ReconstructError {
184    pub fn wrong_node(
185        expected: &'static str,
186        expected_named: bool,
187        actual: CSTNodeClassLabel<'_>,
188    ) -> Self {
189        Self::WrongNode {
190            expected,
191            expected_named,
192            actual: actual.name.into_owned(),
193            actual_named: actual.named,
194        }
195    }
196
197    pub fn wrong_category(category: &'static str, actual: CSTNodeClassLabel<'_>) -> Self {
198        Self::WrongCategory {
199            category,
200            actual: actual.name.into_owned(),
201            actual_named: actual.named,
202        }
203    }
204}
205
206impl Display for ReconstructError {
207    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
208        match self {
209            Self::WrongNode {
210                expected,
211                expected_named,
212                actual,
213                actual_named,
214            } => write!(
215                formatter,
216                "expected {} node {expected:?}, found {} node {actual:?}",
217                visibility(*expected_named),
218                visibility(*actual_named),
219            ),
220            Self::WrongCategory {
221                category,
222                actual,
223                actual_named,
224            } => write!(
225                formatter,
226                "node {} {actual:?} does not belong to {category}",
227                visibility(*actual_named),
228            ),
229            Self::MissingField { node, field } => {
230                write!(formatter, "node {node:?} has no {field:?} field")
231            }
232            Self::MissingChild { node, expected } => {
233                write!(formatter, "node {node:?} has no child matching {expected}")
234            }
235        }
236    }
237}
238
239impl Error for ReconstructError {}
240
241fn visibility(named: bool) -> &'static str {
242    if named { "named" } else { "anonymous" }
243}