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}