Skip to main content

zerodds_idl_lower/
scope.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Scopes and name binding. Spec §4.
4
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7use zerodds_ast::{AnyId, ModuleNode};
8use zerodds_ast_core::{Id, ScopedName};
9
10/// What a scope has declared directly in it, in declaration order.
11type Bindings = Vec<(String, AnyId)>;
12
13/// One lexical scope.
14#[derive(Debug, Clone)]
15struct Scope {
16    /// The module this scope corresponds to, if it is one.
17    module: Option<Id<ModuleNode>>,
18    /// The name of this scope, empty for the root.
19    name: String,
20    /// Whether this is an interface or a value type, whose name is a
21    /// segment of what is declared in it. See [`ScopeStack::push_container`].
22    container: bool,
23    /// What was declared directly in it.
24    bindings: Bindings,
25}
26
27/// The scope stack.
28///
29/// IDL name lookup walks outwards from the innermost scope, which is
30/// what makes a type usable by its simple name inside its own module and
31/// by a scoped name outside. OMG IDL 4.2 §7.5.
32#[derive(Debug, Clone)]
33pub struct ScopeStack {
34    scopes: Vec<Scope>,
35    /// What each module scope held when it was last left.
36    ///
37    /// A module may be reopened — OMG IDL 4.2 §7.4.2 — and the second
38    /// opening is the same scope as the first, not a fresh one. Without
39    /// this, a scope left behind takes its bindings with it, and
40    /// `module common { struct First {…}; }; module common { struct S {
41    /// First f; }; };` fails on a name the first opening declared.
42    closed: Vec<(Id<ModuleNode>, Bindings)>,
43}
44
45impl ScopeStack {
46    /// A stack that has not been given its root yet.
47    ///
48    /// Only for construction order: the lowering needs a context before
49    /// it can ask the model for its root module.
50    #[must_use]
51    pub fn new_placeholder() -> Self {
52        Self {
53            scopes: Vec::new(),
54            closed: Vec::new(),
55        }
56    }
57
58    /// A stack holding only the global scope.
59    #[must_use]
60    pub fn new(root: Id<ModuleNode>) -> Self {
61        Self {
62            scopes: alloc::vec![Scope {
63                module: Some(root),
64                name: String::new(),
65                container: false,
66                bindings: Vec::new(),
67            }],
68            closed: Vec::new(),
69        }
70    }
71
72    /// Enter a named scope.
73    ///
74    /// Reopening a module resumes it: the scope starts with everything
75    /// the previous opening left in it, so an unqualified reference
76    /// reaches back across the `};` and a name declared twice is still
77    /// a collision.
78    pub fn push(&mut self, name: &str, module: Option<Id<ModuleNode>>) {
79        let bindings = module
80            .and_then(|id| self.closed.iter().find(|(closed, _)| *closed == id))
81            .map_or_else(Vec::new, |(_, bindings)| bindings.clone());
82        self.scopes.push(Scope {
83            module,
84            name: name.to_string(),
85            container: false,
86            bindings,
87        });
88    }
89
90    /// Enter the body of an interface or a value type.
91    ///
92    /// A scope of its own for lookup like any other, and in addition one
93    /// that names what is declared in it: the scoped name and the
94    /// repository id of `exception NotFound` in `interface NamingContext`
95    /// in `module CosNaming` go through all three (OMG IDL 4.2 §7.5,
96    /// CORBA 3.3 part 1 §14.7.1.1). An annotation declaration's body is a
97    /// scope that names nothing and is entered with [`Self::push`].
98    pub fn push_container(&mut self, name: &str) {
99        self.scopes.push(Scope {
100            module: None,
101            name: name.to_string(),
102            container: true,
103            bindings: Vec::new(),
104        });
105    }
106
107    /// The interface or value type a declaration made now sits in, when
108    /// the innermost scope is one.
109    #[must_use]
110    pub fn enclosing_container(&self) -> Option<&str> {
111        self.scopes
112            .last()
113            .filter(|scope| scope.container)
114            .map(|scope| scope.name.as_str())
115    }
116
117    /// Leave the innermost scope.
118    pub fn pop(&mut self) {
119        // The global scope is never popped: a stray `}` would otherwise
120        // leave the stack empty and every later lookup would fail for
121        // the wrong reason.
122        if self.scopes.len() <= 1 {
123            return;
124        }
125        let Some(scope) = self.scopes.pop() else {
126            return;
127        };
128        // A module scope is kept so a later opening can resume it. The
129        // bindings it carries already include what earlier openings put
130        // there, `push` having seeded them, so this replaces rather than
131        // appends.
132        let Some(module) = scope.module else {
133            return;
134        };
135        match self.closed.iter_mut().find(|(closed, _)| *closed == module) {
136            Some((_, stored)) => *stored = scope.bindings,
137            None => self.closed.push((module, scope.bindings)),
138        }
139    }
140
141    /// The module the innermost enclosing scope belongs to.
142    #[must_use]
143    pub fn current_module(&self) -> Option<Id<ModuleNode>> {
144        self.scopes.iter().rev().find_map(|scope| scope.module)
145    }
146
147    /// Bind `name` in the innermost scope.
148    pub fn bind(&mut self, name: &str, node: AnyId) {
149        if let Some(scope) = self.scopes.last_mut() {
150            scope.bindings.push((name.to_string(), node));
151        }
152    }
153
154    /// Whether `name` is already bound in the innermost scope.
155    ///
156    /// Case-insensitively, because IDL treats two identifiers differing
157    /// only by case as the same name for collision purposes.
158    /// OMG IDL 4.2 §7.2.3.
159    #[must_use]
160    pub fn is_bound_here(&self, name: &str) -> bool {
161        self.scopes.last().is_some_and(|scope| {
162            scope
163                .bindings
164                .iter()
165                .any(|(bound, _)| bound.eq_ignore_ascii_case(name))
166        })
167    }
168
169    /// Look `name` up in the innermost scope only, matching exactly.
170    ///
171    /// Exact rather than case-insensitive: completing a forward
172    /// declaration requires the same name, while two names differing
173    /// only by case are a collision and have to be reported as one.
174    #[must_use]
175    pub fn lookup_here(&self, name: &str) -> Option<AnyId> {
176        self.scopes.last().and_then(|scope| {
177            scope
178                .bindings
179                .iter()
180                .find(|(bound, _)| bound == name)
181                .map(|(_, node)| *node)
182        })
183    }
184
185    /// Look `name` up, innermost scope outwards.
186    #[must_use]
187    pub fn lookup(&self, name: &str) -> Option<AnyId> {
188        self.scopes.iter().rev().find_map(|scope| {
189            scope
190                .bindings
191                .iter()
192                .find(|(bound, _)| bound == name)
193                .map(|(_, node)| *node)
194        })
195    }
196
197    /// The scoped name a declaration in the innermost scope would have.
198    #[must_use]
199    pub fn qualify(&self, name: &str) -> ScopedName {
200        let mut segments: Vec<String> = self
201            .scopes
202            .iter()
203            .filter(|scope| !scope.name.is_empty())
204            .map(|scope| scope.name.clone())
205            .collect();
206        segments.push(name.to_string());
207        ScopedName::absolute(segments)
208    }
209
210    /// The scoped name a written name has from the current scope.
211    ///
212    /// A directive inside a module names its target relatively, so the
213    /// enclosing scope's path comes first. IDL 4.2 §7.5.
214    #[must_use]
215    pub fn qualify_segments(&self, segments: &[String]) -> ScopedName {
216        let mut path: Vec<String> = self
217            .scopes
218            .iter()
219            .filter(|scope| !scope.name.is_empty())
220            .map(|scope| scope.name.clone())
221            .collect();
222        path.extend_from_slice(segments);
223        ScopedName::absolute(path)
224    }
225
226    /// Every scoped name a written name could denote, innermost first.
227    ///
228    /// §7.5: a name is looked up in the scope that writes it and then in
229    /// each enclosing scope outwards, and the first declaration found is
230    /// the one it means. [`Self::qualify_segments`] gives only the first
231    /// of those, which is right for a declaration being made and wrong
232    /// for a name being *used*: `module m { typeprefix m "…"; };` named
233    /// `m::m`, a scope that does not exist, and the directive was
234    /// reported as naming nothing while `::m` sat one step out.
235    ///
236    /// The list always ends at the root, so a name declared at top level
237    /// is reachable from any depth.
238    #[must_use]
239    pub fn lookup_candidates(&self, segments: &[String]) -> Vec<ScopedName> {
240        let enclosing: Vec<String> = self
241            .scopes
242            .iter()
243            .filter(|scope| !scope.name.is_empty())
244            .map(|scope| scope.name.clone())
245            .collect();
246        let mut out = Vec::with_capacity(enclosing.len() + 1);
247        for depth in (0..=enclosing.len()).rev() {
248            let mut path = enclosing[..depth].to_vec();
249            path.extend_from_slice(segments);
250            out.push(ScopedName::absolute(path));
251        }
252        out
253    }
254
255    /// The name of the innermost named scope, if there is one.
256    ///
257    /// Empty for the root, whose scope carries no name. IDL 4.2 §7.5.2
258    /// forbids redefining that name inside it, and the rule needs to
259    /// know what it is.
260    #[must_use]
261    pub fn innermost_name(&self) -> Option<&str> {
262        self.scopes
263            .last()
264            .map(|scope| scope.name.as_str())
265            .filter(|name| !name.is_empty())
266    }
267
268    /// How deep the stack is, the global scope counting as one.
269    #[must_use]
270    pub fn depth(&self) -> usize {
271        self.scopes.len()
272    }
273}