Skip to main content

zerodds_ast/
resolve.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Reference resolution. Spec §2.
4//!
5//! Resolution is a state transition on an existing node, not a rebuild.
6//! A failed attempt lands in `Broken` with a cause and is not retried by
7//! a later pass: a diagnostic already reported must not silently vanish.
8
9use crate::error::AstError;
10use crate::model::Ast;
11use crate::node::AnyId;
12use crate::refs::RefKind;
13use alloc::string::String;
14use alloc::vec::Vec;
15use zerodds_ast_core::ScopedName;
16use zerodds_ast_core::{BreakCause, Id, IndexLookup, NodeId, PendingRef, RefState};
17
18/// What one resolution pass achieved.
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
20pub struct ResolveReport {
21    /// How many references moved from pending to resolved.
22    pub resolved: usize,
23    /// Every reference that moved from pending to broken, with why.
24    pub broken: Vec<(NodeId, BreakCause)>,
25}
26
27impl ResolveReport {
28    /// Whether the pass left nothing unresolved and broke nothing.
29    #[must_use]
30    pub fn is_clean(&self) -> bool {
31        self.broken.is_empty()
32    }
33}
34
35impl Ast {
36    /// Every node waiting on a given lookup key.
37    #[must_use]
38    pub fn pending_waiters(&self, key: &PendingRef) -> &[NodeId] {
39        self.by_pending.get(key)
40    }
41
42    /// How many references are still pending.
43    #[must_use]
44    pub fn pending_count(&self) -> usize {
45        self.by_pending.iter().map(|(_, nodes)| nodes.len()).sum()
46    }
47
48    /// Resolve every waiter on `key` to `target`.
49    ///
50    /// Returns how many references changed. Each one emits an `Updated`
51    /// change carrying both the old and the new reference state.
52    ///
53    /// # Errors
54    /// [`AstError::WrongKind`] when the target cannot satisfy the
55    /// waiters — a member's type reference pointing at an interface, for
56    /// instance.
57    pub fn resolve_pending(
58        &mut self,
59        key: &PendingRef,
60        target: Id<crate::TypeNode>,
61    ) -> Result<usize, AstError> {
62        let waiters: Vec<NodeId> = self.by_pending.get(key).to_vec();
63        if waiters.is_empty() {
64            return Ok(0);
65        }
66        let Ok(node) = self.types.get(target) else {
67            return Ok(0);
68        };
69        let target_any = AnyId::Type(target, node.node_id);
70        let mut changed = 0usize;
71        for waiter in waiters {
72            if self.set_reference_to(waiter, key, target_any) {
73                changed += 1;
74            }
75        }
76        self.by_pending.remove_key(key);
77        Ok(changed)
78    }
79
80    /// Resolve every waiter on `key` to any kind of node.
81    ///
82    /// The general form of [`Ast::resolve_pending`]: a constant named in
83    /// an array dimension, an exception in a `raises` clause and a base
84    /// interface all resolve through here.
85    ///
86    /// # Errors
87    /// [`AstError::Core`] on an arena failure while rewriting.
88    pub fn resolve_pending_to(
89        &mut self,
90        key: &PendingRef,
91        target: AnyId,
92    ) -> Result<usize, AstError> {
93        let waiters: Vec<NodeId> = self.by_pending.get(key).to_vec();
94        if waiters.is_empty() {
95            return Ok(0);
96        }
97        let mut changed = 0usize;
98        for waiter in waiters {
99            if self.set_reference_to(waiter, key, target) {
100                changed += 1;
101            }
102        }
103        self.by_pending.remove_key(key);
104        Ok(changed)
105    }
106
107    /// Mark every waiter on `key` broken, with a cause.
108    ///
109    /// # Errors
110    /// Never fails today; the signature matches [`Ast::resolve_pending`]
111    /// so callers can treat both the same way.
112    pub fn break_pending(
113        &mut self,
114        key: &PendingRef,
115        cause: BreakCause,
116    ) -> Result<usize, AstError> {
117        let waiters: Vec<NodeId> = self.by_pending.get(key).to_vec();
118        let mut changed = 0usize;
119        for waiter in waiters {
120            if self.break_reference(waiter, key, &cause) {
121                changed += 1;
122            }
123        }
124        self.by_pending.remove_key(key);
125        Ok(changed)
126    }
127
128    /// Resolve every pending scoped-name reference against the name
129    /// index, in one pass.
130    ///
131    /// A name that resolves to the wrong kind of node, that is ambiguous
132    /// or that is absent lands in `Broken` with the corresponding cause
133    /// rather than staying pending.
134    ///
135    /// The declaration a pending name reaches from the scopes its
136    /// waiters sit in, innermost first.
137    ///
138    /// The rule is §7.5's: a name is looked for in the scope it is
139    /// written in, then in each enclosing one. This runs only after the
140    /// root lookup has already failed, so it adds names that were
141    /// previously unresolvable and moves none that were not.
142    ///
143    /// Ambiguity refuses: a name that reaches two different declarations
144    /// from two different waiters is not one this can decide, and
145    /// picking either would bind half the file to a type nobody named.
146    fn find_in_enclosing_scopes(&self, key: &PendingRef) -> Option<AnyId> {
147        let PendingRef::ScopedName(name) = key else {
148            return None;
149        };
150        let written = name.segments();
151        if written.is_empty() {
152            return None;
153        }
154        let mut found: Option<AnyId> = None;
155        for waiter in self.by_pending.get(key) {
156            let Some(scope) = self.enclosing_module_path(*waiter) else {
157                continue;
158            };
159            // Innermost first: drop one enclosing segment at a time.
160            for depth in (0..=scope.len()).rev() {
161                let mut segments: Vec<String> = scope.iter().take(depth).cloned().collect();
162                segments.extend(written.iter().cloned());
163                let candidate = ScopedName::absolute(segments);
164                let Ok(node) = self.by_scoped_name.unique(&candidate) else {
165                    continue;
166                };
167                let Some(any) = self.resolve_node_id(node) else {
168                    continue;
169                };
170                match found {
171                    None => found = Some(any),
172                    // Two waiters reaching two different declarations
173                    // through one written name.
174                    Some(first) if first != any => return None,
175                    Some(_) => {}
176                }
177                break;
178            }
179        }
180        found
181    }
182
183    /// The module path a node sits in, outermost first.
184    fn enclosing_module_path(&self, node: NodeId) -> Option<Vec<String>> {
185        let any = self.resolve_node_id(node)?;
186        let module = match any {
187            AnyId::Type(id, _) => self.types.get(id).ok()?.module,
188            AnyId::Member(id, _) => match self.members.get(id).ok()?.parent {
189                AnyId::Type(type_, _) => self.types.get(type_).ok()?.module,
190                _ => return None,
191            },
192            AnyId::Case(id, _) => {
193                let parent = self.cases.get(id).ok()?.parent;
194                self.types.get(parent).ok()?.module
195            }
196            _ => return None,
197        };
198        self.scoped_name_of(AnyId::Module(
199            module,
200            self.modules.get(module).ok()?.node_id,
201        ))
202        .map(|name| name.segments().to_vec())
203        .or_else(|| Some(Vec::new()))
204    }
205
206    /// # Errors
207    /// [`AstError::Core`] on an arena failure while rewriting.
208    pub fn resolve_all_by_name(&mut self) -> Result<ResolveReport, AstError> {
209        let keys: Vec<PendingRef> = self.by_pending.keys().cloned().collect();
210        let mut report = ResolveReport::default();
211
212        for key in keys {
213            let PendingRef::ScopedName(name) = &key else {
214                continue;
215            };
216            match self.by_scoped_name.unique(name) {
217                Ok(node) => match self.resolve_node_id(node) {
218                    // The waiters decide what the name has to be: a
219                    // member's type reference wants a type, a raises
220                    // clause an exception, an array dimension a
221                    // constant. One name, several kinds of waiter.
222                    // An interface named where a type is expected is an
223                    // object reference — IDL 4.2 §7.4.6 — so those
224                    // waiters get the derived object-reference type and
225                    // the rest get the interface. Without this every
226                    // `struct S { I x; }`, every interface-typed
227                    // parameter and every `provides I p` broke with
228                    // `KindMismatch`, because an interface is not a type
229                    // and nothing offered the type that stands for one.
230                    Some(target @ AnyId::Interface(interface, _))
231                        if self.any_waiter_wants(&key, RefKind::Type) =>
232                    {
233                        let object_ref = self.object_reference_to(interface)?;
234                        let node_id = self.types.get(object_ref)?.node_id;
235                        report.resolved +=
236                            self.point_waiters(&key, AnyId::Type(object_ref, node_id));
237                        report.resolved += self.point_waiters(&key, target);
238                        self.break_or_clear(&key, &mut report)?;
239                    }
240                    Some(target) if self.any_waiter_accepts(&key, target) => {
241                        report.resolved += self.resolve_pending_to(&key, target)?;
242                        // Waiters the target could not satisfy are still
243                        // waiting; they break rather than stay pending
244                        // on a name that has already been decided.
245                        let remaining = self.by_pending.get(&key).to_vec();
246                        if !remaining.is_empty() {
247                            let cause = BreakCause::KindMismatch {
248                                expected: "a different kind of declaration",
249                                found: target.kind().as_str(),
250                            };
251                            for waiter in &remaining {
252                                report.broken.push((*waiter, cause.clone()));
253                            }
254                            let _ = self.break_pending(&key, cause)?;
255                        }
256                    }
257                    found => {
258                        let found = found.map_or("nothing", |any| any.kind().as_str());
259                        let cause = BreakCause::KindMismatch {
260                            expected: "a declaration the reference accepts",
261                            found,
262                        };
263                        for waiter in self.by_pending.get(&key).to_vec() {
264                            report.broken.push((waiter, cause.clone()));
265                        }
266                        let _ = self.break_pending(&key, cause)?;
267                    }
268                },
269                Err(IndexLookup::Missing) => {
270                    // §7.5: a name that is not found at the root is
271                    // looked for in the enclosing scopes, innermost
272                    // first. `Lowerer::scoped_name_of` marks every name
273                    // absolute whether or not the source wrote `::`, so
274                    // without this a member typed `inner::Point` from
275                    // inside `module outer` — ordinary IDL — resolved to
276                    // nothing, the field never reached the generated
277                    // type, and the sample went onto the wire short.
278                    if let Some(target) = self.find_in_enclosing_scopes(&key) {
279                        report.resolved += self.point_waiters(&key, target);
280                        continue;
281                    }
282                    let cause = BreakCause::NotFound { what: key.clone() };
283                    for waiter in self.by_pending.get(&key).to_vec() {
284                        report.broken.push((waiter, cause.clone()));
285                    }
286                    let _ = self.break_pending(&key, cause)?;
287                }
288                Err(IndexLookup::Ambiguous(candidates)) => {
289                    let cause = BreakCause::Ambiguous {
290                        what: key.clone(),
291                        candidates,
292                    };
293                    for waiter in self.by_pending.get(&key).to_vec() {
294                        report.broken.push((waiter, cause.clone()));
295                    }
296                    let _ = self.break_pending(&key, cause)?;
297                }
298            }
299        }
300
301        Ok(report)
302    }
303
304    /// Point every waiter on `key` that accepts `target` at it, without
305    /// clearing the key.
306    ///
307    /// [`Ast::resolve_pending_to`] removes the key once it is done, which
308    /// is right when one target satisfies every waiter and wrong when a
309    /// name is served by two — an interface and the object-reference type
310    /// derived from it.
311    fn point_waiters(&mut self, key: &PendingRef, target: AnyId) -> usize {
312        let waiters: Vec<NodeId> = self.by_pending.get(key).to_vec();
313        waiters
314            .into_iter()
315            .filter(|waiter| self.set_reference_to(*waiter, key, target))
316            .count()
317    }
318
319    /// Break whatever still waits on `key` and drop it from the index.
320    fn break_or_clear(
321        &mut self,
322        key: &PendingRef,
323        report: &mut ResolveReport,
324    ) -> Result<(), AstError> {
325        let remaining: Vec<NodeId> = self
326            .by_pending
327            .get(key)
328            .iter()
329            .copied()
330            .filter(|waiter| {
331                self.resolve_node_id(*waiter)
332                    .is_some_and(|any| self.pending_slots(any).iter().any(|slot| &slot.key == key))
333            })
334            .collect();
335        if remaining.is_empty() {
336            self.by_pending.remove_key(key);
337            return Ok(());
338        }
339        let cause = BreakCause::KindMismatch {
340            expected: "a different kind of declaration",
341            found: "interface",
342        };
343        for waiter in &remaining {
344            report.broken.push((*waiter, cause.clone()));
345        }
346        let _ = self.break_pending(key, cause)?;
347        Ok(())
348    }
349
350    /// Whether any node waiting on `key` holds a reference of `kind`.
351    fn any_waiter_wants(&self, key: &PendingRef, kind: RefKind) -> bool {
352        self.by_pending.get(key).iter().any(|waiter| {
353            self.resolve_node_id(*waiter).is_some_and(|any| {
354                self.pending_slots(any)
355                    .iter()
356                    .any(|slot| &slot.key == key && slot.kind == kind)
357            })
358        })
359    }
360
361    /// Whether any node waiting on `key` accepts `target`.
362    fn any_waiter_accepts(&self, key: &PendingRef, target: AnyId) -> bool {
363        self.by_pending.get(key).iter().any(|waiter| {
364            self.resolve_node_id(*waiter).is_some_and(|any| {
365                self.pending_slots(any)
366                    .iter()
367                    .any(|slot| &slot.key == key && slot.kind.accepts(target))
368            })
369        })
370    }
371
372    /// Point every reference of `node` that waits on `key` at `target`.
373    ///
374    /// Returns whether anything changed. A reference whose kind the
375    /// target cannot satisfy is left alone rather than pointed at the
376    /// wrong sort of node; `resolve_all_by_name` breaks it with a
377    /// `KindMismatch` instead.
378    fn set_reference_to(&mut self, node: NodeId, key: &PendingRef, target: AnyId) -> bool {
379        let Some(any) = self.resolve_node_id(node) else {
380            return false;
381        };
382        let slots: Vec<usize> = self
383            .pending_slots(any)
384            .into_iter()
385            .filter(|slot| &slot.key == key && slot.kind.accepts(target))
386            .map(|slot| slot.slot)
387            .collect();
388
389        let mut changed = Vec::new();
390        for slot in slots {
391            if self.set_slot(any, slot, target) {
392                changed.push(slot);
393            }
394        }
395        // One record per reference: a rollback has to put each of them
396        // back on its own, and a map whose key and value waited on the
397        // same name has two.
398        for slot in &changed {
399            self.record_reference_change(node, *slot, key, RefState::Resolved(target.node_id()));
400        }
401        !changed.is_empty()
402    }
403
404    /// Break every reference of `node` that waits on `key`.
405    fn break_reference(&mut self, node: NodeId, key: &PendingRef, cause: &BreakCause) -> bool {
406        let Some(any) = self.resolve_node_id(node) else {
407            return false;
408        };
409        let slots: Vec<usize> = self
410            .pending_slots(any)
411            .into_iter()
412            .filter(|slot| &slot.key == key)
413            .map(|slot| slot.slot)
414            .collect();
415
416        let mut changed = Vec::new();
417        for slot in slots {
418            if self.break_slot(any, slot, cause.clone()) {
419                changed.push(slot);
420            }
421        }
422        for slot in &changed {
423            self.record_reference_change(node, *slot, key, RefState::Broken);
424        }
425        !changed.is_empty()
426    }
427
428    fn record_reference_change(
429        &mut self,
430        node: NodeId,
431        slot: usize,
432        key: &PendingRef,
433        after: RefState,
434    ) {
435        let change = zerodds_ast_core::Change::Updated {
436            node,
437            field: crate::field::REFERENCE,
438            before: zerodds_ast_core::FieldValue::Reference(zerodds_ast_core::RefState::Pending),
439            after: zerodds_ast_core::FieldValue::Reference(after),
440        };
441        let undo = zerodds_ast_core::UndoRecord {
442            node,
443            payload: zerodds_ast_core::UndoPayload::RestorePending {
444                key: key.clone(),
445                slot,
446            },
447        };
448        if let Some(log) = self.pending.as_mut() {
449            log.record(undo, change);
450        } else if let Err(error) = self.record_standalone(undo, change) {
451            self.last_commit_error = Some(error);
452        }
453    }
454
455    /// Register a node's pending references in the waiter index.
456    pub(crate) fn index_pending(&mut self, any: AnyId) {
457        let node = any.node_id();
458        for slot in self.pending_slots(any) {
459            self.by_pending.insert(slot.key, node);
460        }
461    }
462
463    /// Rebuild a node's entries in the waiter index.
464    ///
465    /// For a mutation that replaced or added references: the old keys go
466    /// and the current ones are indexed. An unindexed pending reference
467    /// never resolves.
468    pub fn reindex_pending(&mut self, any: AnyId) {
469        self.unindex_pending(any);
470        self.index_pending(any);
471    }
472
473    /// Drop a node's pending keys from the waiter index.
474    ///
475    /// Needed wherever a node's references are replaced wholesale — a
476    /// forward declaration gaining its body, for one. A stale waiter
477    /// entry would send a later resolution at references that are gone.
478    pub(crate) fn unindex_pending(&mut self, any: AnyId) {
479        self.by_pending.remove_node(any.node_id());
480    }
481}