Skip to main content

ts_gen/parse/
scope.rs

1//! TypeScript type scope tracking with arena-based ownership.
2//!
3//! Each scope maps type names to `TypeId`s — indices into the global type arena.
4//! Scopes form a tree via parent links, enabling JS-like scoping where child
5//! scopes shadow or extend parent scopes.
6//!
7//! Scopes are stored in a flat arena (`ScopeArena`) and referenced by
8//! well-typed `ScopeId` indices.
9
10use std::collections::HashMap;
11
12use crate::context::TypeId;
13
14/// Index into a `ScopeArena`. Lightweight, Copy, and well-typed.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub struct ScopeId(pub(crate) u32);
17
18/// A single level of type scope.
19#[derive(Clone, Debug)]
20pub struct TypeScope {
21    /// Optional parent scope — resolution walks up the chain.
22    pub parent: Option<ScopeId>,
23    /// Types defined or imported in this scope: name → TypeId.
24    names: HashMap<String, TypeId>,
25}
26
27impl TypeScope {
28    fn new(parent: Option<ScopeId>) -> Self {
29        Self {
30            parent,
31            names: HashMap::new(),
32        }
33    }
34
35    /// Insert a name into this scope.
36    pub fn insert(&mut self, name: String, type_id: TypeId) {
37        self.names.insert(name, type_id);
38    }
39
40    /// Look up a name in this scope only (not parent scopes).
41    pub fn get(&self, name: &str) -> Option<TypeId> {
42        self.names.get(name).copied()
43    }
44
45    /// Iterate over all names in this scope.
46    pub fn iter(&self) -> impl Iterator<Item = (&String, &TypeId)> {
47        self.names.iter()
48    }
49}
50
51/// Arena that owns all scopes. Scopes are created via `create_root` and
52/// `create_child`, and referenced by `ScopeId`.
53#[derive(Clone, Debug)]
54pub struct ScopeArena {
55    scopes: Vec<TypeScope>,
56}
57
58impl ScopeArena {
59    pub fn new() -> Self {
60        Self { scopes: Vec::new() }
61    }
62
63    /// Create a root scope (no parent).
64    pub fn create_root(&mut self) -> ScopeId {
65        let id = ScopeId(self.scopes.len() as u32);
66        self.scopes.push(TypeScope::new(None));
67        id
68    }
69
70    /// Create a child scope with the given parent.
71    pub fn create_child(&mut self, parent: ScopeId) -> ScopeId {
72        let id = ScopeId(self.scopes.len() as u32);
73        self.scopes.push(TypeScope::new(Some(parent)));
74        id
75    }
76
77    /// Get a reference to a scope by id.
78    pub fn get(&self, id: ScopeId) -> &TypeScope {
79        &self.scopes[id.0 as usize]
80    }
81
82    /// Get a mutable reference to a scope by id.
83    pub fn get_mut(&mut self, id: ScopeId) -> &mut TypeScope {
84        &mut self.scopes[id.0 as usize]
85    }
86
87    /// Insert a name into the given scope.
88    pub fn insert(&mut self, scope: ScopeId, name: String, type_id: TypeId) {
89        self.get_mut(scope).insert(name, type_id);
90    }
91
92    /// Resolve a simple name by walking up the scope chain from the given scope.
93    pub fn resolve(&self, scope: ScopeId, name: &str) -> Option<TypeId> {
94        let s = self.get(scope);
95        if let Some(type_id) = s.get(name) {
96            return Some(type_id);
97        }
98        if let Some(parent) = s.parent {
99            return self.resolve(parent, name);
100        }
101        None
102    }
103}
104
105impl Default for ScopeArena {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111/// An unresolved import that needs to be resolved during import resolution.
112/// Stored in a side table on GlobalContext, not in the scope.
113#[derive(Clone, Debug)]
114pub struct PendingImport {
115    /// The scope that contains this import.
116    pub scope: ScopeId,
117    /// The local name in the importing scope.
118    pub local_name: String,
119    /// The module specifier from the import statement.
120    pub from_module: String,
121    /// The original name in the source module.
122    pub original_name: String,
123}
124
125#[cfg(test)]
126mod tests {
127    use crate::context::GlobalContext;
128
129    #[test]
130    fn test_basic_resolution() {
131        let mut gctx = GlobalContext::new();
132        let root = gctx.create_root_scope();
133        let type_id = gctx.insert_type(crate::ir::TypeDeclaration {
134            kind: crate::ir::TypeKind::Interface(crate::ir::InterfaceDecl {
135                name: "Foo".to_string(),
136                js_name: "Foo".to_string(),
137                type_params: vec![],
138                extends: vec![],
139                members: vec![],
140                classification: crate::ir::InterfaceClassification::ClassLike,
141            }),
142            module_context: crate::ir::ModuleContext::Global,
143            doc: None,
144            scope_id: root,
145            exported: false,
146        });
147        gctx.scopes.insert(root, "Foo".to_string(), type_id);
148
149        assert!(gctx.scopes.resolve(root, "Foo").is_some());
150        assert!(gctx.scopes.resolve(root, "Bar").is_none());
151    }
152
153    #[test]
154    fn test_child_scope_shadows_parent() {
155        let mut gctx = GlobalContext::new();
156        let parent = gctx.create_root_scope();
157        let child = gctx.scopes.create_child(parent);
158
159        let id_a = gctx.insert_type(crate::ir::TypeDeclaration {
160            kind: crate::ir::TypeKind::Interface(crate::ir::InterfaceDecl {
161                name: "Foo".to_string(),
162                js_name: "Foo".to_string(),
163                type_params: vec![],
164                extends: vec![],
165                members: vec![],
166                classification: crate::ir::InterfaceClassification::ClassLike,
167            }),
168            module_context: crate::ir::ModuleContext::Global,
169            doc: None,
170            scope_id: parent,
171            exported: false,
172        });
173        let id_b = gctx.insert_type(crate::ir::TypeDeclaration {
174            kind: crate::ir::TypeKind::Interface(crate::ir::InterfaceDecl {
175                name: "Foo".to_string(),
176                js_name: "Foo".to_string(),
177                type_params: vec![],
178                extends: vec![],
179                members: vec![],
180                classification: crate::ir::InterfaceClassification::ClassLike,
181            }),
182            module_context: crate::ir::ModuleContext::Global,
183            doc: None,
184            scope_id: child,
185            exported: false,
186        });
187
188        gctx.scopes.insert(parent, "Foo".to_string(), id_a);
189        gctx.scopes.insert(child, "Foo".to_string(), id_b);
190
191        // Child shadows parent
192        assert_eq!(gctx.scopes.resolve(child, "Foo"), Some(id_b));
193        // Parent still resolves to id_a
194        assert_eq!(gctx.scopes.resolve(parent, "Foo"), Some(id_a));
195    }
196
197    #[test]
198    fn test_child_inherits_parent() {
199        let mut gctx = GlobalContext::new();
200        let parent = gctx.create_root_scope();
201        let child = gctx.scopes.create_child(parent);
202
203        let id = gctx.insert_type(crate::ir::TypeDeclaration {
204            kind: crate::ir::TypeKind::Interface(crate::ir::InterfaceDecl {
205                name: "Foo".to_string(),
206                js_name: "Foo".to_string(),
207                type_params: vec![],
208                extends: vec![],
209                members: vec![],
210                classification: crate::ir::InterfaceClassification::ClassLike,
211            }),
212            module_context: crate::ir::ModuleContext::Global,
213            doc: None,
214            scope_id: parent,
215            exported: false,
216        });
217        gctx.scopes.insert(parent, "Foo".to_string(), id);
218
219        // Child inherits from parent
220        assert_eq!(gctx.scopes.resolve(child, "Foo"), Some(id));
221    }
222}