Skip to main content

zerodds_lang_rules/adapter/
ast.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The common AST as a seesaw v2 graph. Spec §3.1.
4//!
5//! Every node of the model appears as a typed seesaw node; every
6//! ownership relation and every reference appears as an anonymous
7//! directed link. Facets appear as nodes too, not as attributes, so a
8//! rule can match on `@key` being present the same way it matches on a
9//! member being present.
10//!
11//! The adapter is total over `NodeKind`. A node kind with no
12//! representation would be a construct no rule could ever match — a
13//! silent gap, which is the failure mode this whole layer exists to
14//! avoid.
15
16use seesaw_tgg::graph::{Graph, ValueStore};
17use seesaw_tgg::ident::{GhostId, Status};
18use std::collections::BTreeMap;
19use zerodds_ast::{AnyId, Ast, NodeKind, TypeKind};
20use zerodds_ast_core::NodeId;
21
22/// A model presented as a seesaw graph, with the map back.
23pub struct ModelGraph {
24    /// The graph a rule matches over.
25    pub graph: Graph,
26    /// The values its leaves carry.
27    pub values: ValueStore,
28    /// Which model node each seesaw node came from.
29    by_ghost: BTreeMap<GhostId, NodeId>,
30    /// Which seesaw node each model node became.
31    by_node: BTreeMap<NodeId, GhostId>,
32}
33
34/// One node of the model, as the adapter names it.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct NodeRef {
37    /// The seesaw node.
38    pub ghost: GhostId,
39    /// The model node it came from.
40    pub node: NodeId,
41}
42
43impl ModelGraph {
44    /// Present `ast` as a graph.
45    #[must_use]
46    pub fn of(ast: &Ast) -> Self {
47        let mut this = Self {
48            graph: Graph::new(),
49            values: ValueStore::default(),
50            by_ghost: BTreeMap::new(),
51            by_node: BTreeMap::new(),
52        };
53        this.add_nodes(ast);
54        this.add_links(ast);
55        this
56    }
57
58    /// The model node a seesaw node came from.
59    #[must_use]
60    pub fn model_node(&self, ghost: &GhostId) -> Option<NodeId> {
61        self.by_ghost.get(ghost).copied()
62    }
63
64    /// The seesaw node a model node became.
65    #[must_use]
66    pub fn ghost(&self, node: NodeId) -> Option<GhostId> {
67        self.by_node.get(&node).copied()
68    }
69
70    /// Every pairing of seesaw node and model node.
71    pub fn pairs(&self) -> impl Iterator<Item = (GhostId, NodeId)> + '_ {
72        self.by_ghost.iter().map(|(ghost, node)| (*ghost, *node))
73    }
74
75    /// How many nodes the graph holds.
76    #[must_use]
77    pub fn node_count(&self) -> usize {
78        self.graph.node_count()
79    }
80
81    fn add_nodes(&mut self, ast: &Ast) {
82        // Every node kind, in the order the model iterates them, so two
83        // runs over one model produce the same graph.
84        for (id, node) in ast.modules() {
85            self.add(AnyId::Module(id, node.node_id), type_name_of_module());
86        }
87        for (id, node) in ast.types() {
88            self.add(AnyId::Type(id, node.node_id), type_name_of_type(&node.kind));
89            self.add_facets(ast, AnyId::Type(id, node.node_id));
90        }
91        for (id, node) in ast.members() {
92            self.add(AnyId::Member(id, node.node_id), "Member");
93            self.add_facets(ast, AnyId::Member(id, node.node_id));
94        }
95        for (id, node) in ast.enumerators() {
96            self.add(AnyId::Enumerator(id, node.node_id), "Enumerator");
97            self.add_facets(ast, AnyId::Enumerator(id, node.node_id));
98        }
99        for (id, node) in ast.interfaces() {
100            self.add(AnyId::Interface(id, node.node_id), "Interface");
101            self.add_facets(ast, AnyId::Interface(id, node.node_id));
102            // An attribute is reachable only from the interface that
103            // declares it — the model keeps no arena iterator for one —
104            // so it is walked here rather than in a loop of its own.
105            for attribute in &node.attributes {
106                let Ok(attribute_node) = ast.attribute(*attribute) else {
107                    continue;
108                };
109                let any = AnyId::Attribute(*attribute, attribute_node.node_id);
110                let ghost = self.add(any, "Attribute");
111                self.add_facets(ast, any);
112                // `readonly` is a node and not an attribute value, for
113                // the reason `@key` is: a rule matching a read-only
114                // attribute is then a two-node pattern rather than a
115                // predicate on a field, and "not read-only" is never a
116                // pattern.
117                if attribute_node.readonly {
118                    self.add_flag(ghost, "Readonly");
119                }
120            }
121        }
122        // An operation belongs to an interface or to a valuetype, which
123        // is why it has an arena of its own and why the walk is over
124        // that arena rather than over the interfaces.
125        for (id, node) in ast.operations() {
126            let any = AnyId::Operation(id, node.node_id);
127            let ghost = self.add(any, "Operation");
128            self.add_facets(ast, any);
129            if node.oneway {
130                self.add_flag(ghost, "Oneway");
131            }
132            for param in &node.params {
133                let Ok(param_node) = ast.param(*param) else {
134                    continue;
135                };
136                let any = AnyId::Param(*param, param_node.node_id);
137                let param_ghost = self.add(any, "Param");
138                self.add_facets(ast, any);
139                // Every direction gets a node, unlike the flags above:
140                // IDL 4.2 rule (87) makes the direction mandatory, so
141                // there is no default whose absence could carry meaning.
142                self.add_flag(param_ghost, direction_name(param_node.direction));
143            }
144        }
145        for (id, node) in ast.cases() {
146            self.add(AnyId::Case(id, node.node_id), "Case");
147        }
148        for (id, node) in ast.bit_values() {
149            self.add(AnyId::BitValue(id, node.node_id), "BitValue");
150        }
151        for (id, node) in ast.constants() {
152            self.add(AnyId::Const(id, node.node_id), "Const");
153        }
154        for (id, node) in ast.exceptions() {
155            self.add(AnyId::Exception(id, node.node_id), "Exception");
156        }
157    }
158
159    /// Add one node, keeping both directions of the map.
160    fn add(&mut self, any: AnyId, type_name: &str) -> GhostId {
161        let node = any.node_id();
162        let external = external_name(node);
163        let ghost = self.graph.add_baseline(&external, type_name);
164        let _ = self.by_ghost.insert(ghost, node);
165        let _ = self.by_node.insert(node, ghost);
166        ghost
167    }
168
169    /// A flag becomes a node hanging off the node it qualifies.
170    ///
171    /// The same shape [`Self::add_facets`] gives `@key`, for a flag the
172    /// model carries as a field rather than in the X-Types facet:
173    /// `oneway`, `readonly` and a parameter's direction.
174    fn add_flag(&mut self, owner: GhostId, name: &str) {
175        let flag = self.graph.add_ghost(&owner, name);
176        let _ = self.graph.connect(owner, flag, Status::Solid);
177    }
178
179    /// A facet becomes a node linked from the node it describes.
180    ///
181    /// `@key` on a member is a `Key` node hanging off that member, so a
182    /// rule matching a keyed member is a two-node pattern rather than a
183    /// value predicate on an attribute.
184    fn add_facets(&mut self, ast: &Ast, any: AnyId) {
185        let Some(facets) = ast.facets(any) else {
186            return;
187        };
188        let Some(xtypes) = facets.xtypes.as_ref() else {
189            return;
190        };
191        let owner = self.by_node.get(&any.node_id()).copied();
192        let Some(owner) = owner else {
193            return;
194        };
195
196        // One node per flag that a rule can branch on. A flag that is
197        // false has no node: the rules are positive, so "not key" is
198        // never a pattern, only "key" is.
199        for (present, name) in [
200            (xtypes.is_key, "Key"),
201            (xtypes.is_optional, "Optional"),
202            (xtypes.is_external, "External"),
203            (xtypes.is_must_understand, "MustUnderstand"),
204            (xtypes.is_non_serialized, "NonSerialized"),
205            (xtypes.is_nested, "Nested"),
206            (xtypes.is_default_literal, "DefaultLiteral"),
207        ] {
208            if !present {
209                continue;
210            }
211            let facet = self.graph.add_ghost(&owner, name);
212            let _ = self.graph.connect(owner, facet, Status::Solid);
213        }
214
215        if let Some(extensibility) = xtypes.extensibility {
216            let name = match extensibility {
217                zerodds_ast::Extensibility::Final => "Final",
218                zerodds_ast::Extensibility::Appendable => "Appendable",
219                zerodds_ast::Extensibility::Mutable => "Mutable",
220            };
221            let facet = self.graph.add_ghost(&owner, name);
222            let _ = self.graph.connect(owner, facet, Status::Solid);
223        }
224    }
225
226    /// Ownership and references, as anonymous directed links.
227    fn add_links(&mut self, ast: &Ast) {
228        for (id, node) in ast.types() {
229            let Some(type_ghost) = self.by_node.get(&node.node_id).copied() else {
230                continue;
231            };
232            // A type belongs to its module.
233            if let Ok(module) = ast.module(node.module) {
234                if let Some(module_ghost) = self.by_node.get(&module.node_id).copied() {
235                    let _ = self.graph.connect(module_ghost, type_ghost, Status::Solid);
236                }
237            }
238            // An aggregate owns its members, in declaration order.
239            for member in ast.members_of(id) {
240                let Ok(member_node) = ast.member(*member) else {
241                    continue;
242                };
243                if let Some(member_ghost) = self.by_node.get(&member_node.node_id).copied() {
244                    let _ = self.graph.connect(type_ghost, member_ghost, Status::Solid);
245                }
246            }
247            // A union owns its cases, a bitmask its bits.
248            for case in ast.cases_of(id) {
249                let Ok(node) = ast.case(*case) else {
250                    continue;
251                };
252                if let Some(ghost) = self.by_node.get(&node.node_id).copied() {
253                    let _ = self.graph.connect(type_ghost, ghost, Status::Solid);
254                }
255            }
256            for bit in ast.bits_of(id) {
257                let Ok(node) = ast.bit_value(*bit) else {
258                    continue;
259                };
260                if let Some(ghost) = self.by_node.get(&node.node_id).copied() {
261                    let _ = self.graph.connect(type_ghost, ghost, Status::Solid);
262                }
263            }
264            // An enumeration owns its enumerators.
265            for enumerator in ast.enumerators_of(id) {
266                let Ok(node) = ast.enumerator(*enumerator) else {
267                    continue;
268                };
269                if let Some(ghost) = self.by_node.get(&node.node_id).copied() {
270                    let _ = self.graph.connect(type_ghost, ghost, Status::Solid);
271                }
272            }
273        }
274
275        // An interface belongs to its module, owns its attributes and
276        // points at every base it inherits from, in declaration order.
277        // Without these links the interface node stands alone in the
278        // graph and the `interface-to-…` rules of `rule::{cpp,java,
279        // typescript}` match a node with nothing under it.
280        for (id, node) in ast.interfaces() {
281            let Some(interface_ghost) = self.by_node.get(&node.node_id).copied() else {
282                continue;
283            };
284            if let Ok(module) = ast.module(node.module)
285                && let Some(module_ghost) = self.by_node.get(&module.node_id).copied()
286            {
287                let _ = self
288                    .graph
289                    .connect(module_ghost, interface_ghost, Status::Solid);
290            }
291            for base in &node.bases {
292                let Some(base_id) = base.resolved() else {
293                    continue;
294                };
295                let Ok(base_node) = ast.interface(base_id) else {
296                    continue;
297                };
298                if let Some(base_ghost) = self.by_node.get(&base_node.node_id).copied() {
299                    let _ = self
300                        .graph
301                        .connect(interface_ghost, base_ghost, Status::Solid);
302                }
303            }
304            for attribute in &node.attributes {
305                let Ok(attribute_node) = ast.attribute(*attribute) else {
306                    continue;
307                };
308                if let Some(attribute_ghost) = self.by_node.get(&attribute_node.node_id).copied() {
309                    let _ = self
310                        .graph
311                        .connect(interface_ghost, attribute_ghost, Status::Solid);
312                }
313                if let Some(target) = attribute_node.type_.resolved()
314                    && let Ok(target_node) = ast.type_(target)
315                    && let Some(target_ghost) = self.by_node.get(&target_node.node_id).copied()
316                    && let Some(attribute_ghost) =
317                        self.by_node.get(&attribute_node.node_id).copied()
318                {
319                    let _ = self
320                        .graph
321                        .connect(attribute_ghost, target_ghost, Status::Solid);
322                }
323            }
324            let _ = id;
325        }
326
327        // An operation belongs to whatever declared it, owns its
328        // parameters in declaration order, and points at its return
329        // type and at every exception it raises.
330        for (_, node) in ast.operations() {
331            let Some(operation_ghost) = self.by_node.get(&node.node_id).copied() else {
332                continue;
333            };
334            if let Some(owner_ghost) = self.by_node.get(&node.parent.node_id()).copied() {
335                let _ = self
336                    .graph
337                    .connect(owner_ghost, operation_ghost, Status::Solid);
338            }
339            if let Some(target) = node.return_type.as_ref().and_then(|r| r.resolved())
340                && let Ok(target_node) = ast.type_(target)
341                && let Some(target_ghost) = self.by_node.get(&target_node.node_id).copied()
342            {
343                let _ = self
344                    .graph
345                    .connect(operation_ghost, target_ghost, Status::Solid);
346            }
347            for raised in &node.raises {
348                let Some(raised_id) = raised.resolved() else {
349                    continue;
350                };
351                let Ok(raised_node) = ast.exception(raised_id) else {
352                    continue;
353                };
354                if let Some(raised_ghost) = self.by_node.get(&raised_node.node_id).copied() {
355                    let _ = self
356                        .graph
357                        .connect(operation_ghost, raised_ghost, Status::Solid);
358                }
359            }
360            for param in &node.params {
361                let Ok(param_node) = ast.param(*param) else {
362                    continue;
363                };
364                let Some(param_ghost) = self.by_node.get(&param_node.node_id).copied() else {
365                    continue;
366                };
367                let _ = self
368                    .graph
369                    .connect(operation_ghost, param_ghost, Status::Solid);
370                if let Some(target) = param_node.type_.resolved()
371                    && let Ok(target_node) = ast.type_(target)
372                    && let Some(target_ghost) = self.by_node.get(&target_node.node_id).copied()
373                {
374                    let _ = self.graph.connect(param_ghost, target_ghost, Status::Solid);
375                }
376            }
377        }
378
379        // A member points at its type.
380        for (_, node) in ast.members() {
381            let Some(member_ghost) = self.by_node.get(&node.node_id).copied() else {
382                continue;
383            };
384            let Some(target) = node.type_.resolved() else {
385                continue;
386            };
387            let Ok(target_node) = ast.type_(target) else {
388                continue;
389            };
390            if let Some(target_ghost) = self.by_node.get(&target_node.node_id).copied() {
391                let _ = self
392                    .graph
393                    .connect(member_ghost, target_ghost, Status::Solid);
394            }
395        }
396    }
397}
398
399/// The external name a model node takes in the graph.
400///
401/// The node's own identity, which is stable across snapshots and
402/// processes, so a graph built twice from one model is the same graph.
403fn external_name(node: NodeId) -> String {
404    format!("{}:{}", node.space().as_u32(), node.counter())
405}
406
407const fn type_name_of_module() -> &'static str {
408    "Module"
409}
410
411/// The seesaw node name a parameter's direction takes. IDL 4.2 rule
412/// (87).
413const fn direction_name(direction: zerodds_ast::ParamDirection) -> &'static str {
414    match direction {
415        zerodds_ast::ParamDirection::In => "In",
416        zerodds_ast::ParamDirection::Out => "Out",
417        zerodds_ast::ParamDirection::InOut => "InOut",
418    }
419}
420
421/// The seesaw type name a model type takes.
422///
423/// One name per `TypeKind`, with no catch-all: a kind added to the model
424/// without a name here is a compile error rather than a type no rule can
425/// match.
426const fn type_name_of_type(kind: &TypeKind) -> &'static str {
427    match kind {
428        TypeKind::Primitive(_) => "Primitive",
429        TypeKind::Fixed { .. } => "Fixed",
430        TypeKind::String { .. } => "StringType",
431        TypeKind::Sequence { .. } => "Sequence",
432        TypeKind::Array { .. } => "Array",
433        TypeKind::Map { .. } => "Map",
434        TypeKind::Structure { .. } => "Structure",
435        TypeKind::Union { .. } => "Union",
436        TypeKind::Enumeration { .. } => "Enumeration",
437        TypeKind::Bitmask { .. } => "Bitmask",
438        TypeKind::Bitset { .. } => "Bitset",
439        TypeKind::Alias { .. } => "Alias",
440        TypeKind::Valuetype { .. } => "Valuetype",
441        TypeKind::ValueBox { .. } => "ValueBox",
442        TypeKind::Native => "Native",
443        TypeKind::Any => "Any",
444        TypeKind::ValueBase => "ValueBase",
445        TypeKind::ObjectRef { .. } => "ObjectRef",
446        TypeKind::AnnotationType { .. } => "AnnotationType",
447    }
448}
449
450/// The seesaw type name a node kind takes.
451///
452/// Total over `NodeKind` and with no catch-all, which is what §3.1
453/// requires: a kind with no name would be a construct no rule can match.
454#[must_use]
455pub const fn type_name_of_kind(kind: NodeKind) -> &'static str {
456    match kind {
457        NodeKind::Module => "Module",
458        NodeKind::Type => "Type",
459        NodeKind::Member => "Member",
460        NodeKind::Case => "Case",
461        NodeKind::Enumerator => "Enumerator",
462        NodeKind::BitValue => "BitValue",
463        NodeKind::Bitfield => "Bitfield",
464        NodeKind::Const => "Const",
465        NodeKind::ConstExpr => "ConstExpr",
466        NodeKind::Interface => "Interface",
467        NodeKind::Operation => "Operation",
468        NodeKind::Param => "Param",
469        NodeKind::Attribute => "Attribute",
470        NodeKind::Exception => "Exception",
471        NodeKind::AnnotationDef => "AnnotationDef",
472        NodeKind::AnnotationMember => "AnnotationMember",
473        NodeKind::Domain => "Domain",
474        NodeKind::Topic => "Topic",
475        NodeKind::QosProfile => "QosProfile",
476        NodeKind::Participant => "Participant",
477        NodeKind::Endpoint => "Endpoint",
478        NodeKind::Library => "Library",
479    }
480}