Skip to main content

shape_ast/
module_utils.rs

1//! Shared module resolution utilities.
2//!
3//! Types and functions used by both `shape-runtime` (module loader) and
4//! `shape-vm` (import inlining) to inspect module exports and manipulate
5//! AST item lists during import resolution.
6
7use crate::ast::{ExportItem, Item, Program, Span};
8use crate::error::{Result, ShapeError};
9
10// ---------------------------------------------------------------------------
11// Types
12// ---------------------------------------------------------------------------
13
14/// High-level kind of an exported symbol.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ModuleExportKind {
17    Function,
18    BuiltinFunction,
19    TypeAlias,
20    BuiltinType,
21    Trait,
22    Enum,
23    Annotation,
24    Value,
25}
26
27/// Exported symbol metadata discovered from a module's AST.
28#[derive(Debug, Clone)]
29pub struct ModuleExportSymbol {
30    /// Original symbol name in module scope.
31    pub name: String,
32    /// Alias if exported as `name as alias`.
33    pub alias: Option<String>,
34    /// High-level symbol kind.
35    pub kind: ModuleExportKind,
36    /// Source span for navigation/diagnostics.
37    pub span: Span,
38}
39
40// ---------------------------------------------------------------------------
41// direct_export_target
42// ---------------------------------------------------------------------------
43
44/// Map a direct (non-`Named`) export item to its name and kind.
45///
46/// Returns `None` for `ExportItem::Named`, which requires scope-level
47/// resolution handled by [`collect_exported_symbols`].
48pub fn direct_export_target(export_item: &ExportItem) -> Option<(String, ModuleExportKind)> {
49    match export_item {
50        ExportItem::Function(function) => {
51            Some((function.name.clone(), ModuleExportKind::Function))
52        }
53        ExportItem::BuiltinFunction(function) => {
54            Some((function.name.clone(), ModuleExportKind::BuiltinFunction))
55        }
56        ExportItem::BuiltinType(type_decl) => {
57            Some((type_decl.name.clone(), ModuleExportKind::BuiltinType))
58        }
59        ExportItem::TypeAlias(alias) => Some((alias.name.clone(), ModuleExportKind::TypeAlias)),
60        ExportItem::Enum(enum_def) => Some((enum_def.name.clone(), ModuleExportKind::Enum)),
61        ExportItem::Struct(struct_def) => {
62            Some((struct_def.name.clone(), ModuleExportKind::TypeAlias))
63        }
64        ExportItem::Trait(trait_def) => {
65            Some((trait_def.name.clone(), ModuleExportKind::Trait))
66        }
67        ExportItem::Annotation(annotation) => {
68            Some((annotation.name.clone(), ModuleExportKind::Annotation))
69        }
70        ExportItem::ForeignFunction(function) => {
71            Some((function.name.clone(), ModuleExportKind::Function))
72        }
73        ExportItem::Named(_) => None,
74    }
75}
76
77// ---------------------------------------------------------------------------
78// strip_import_items
79// ---------------------------------------------------------------------------
80
81/// Remove all `Item::Import` entries from a list of AST items.
82///
83/// Used when inlining module contents into a consumer program — the module's
84/// own imports have already been resolved and should not pollute the
85/// consumer's import set.
86pub fn strip_import_items(items: Vec<Item>) -> Vec<Item> {
87    items
88        .into_iter()
89        .filter(|item| !matches!(item, Item::Import(..)))
90        .collect()
91}
92
93// ---------------------------------------------------------------------------
94// collect_exported_symbols
95// ---------------------------------------------------------------------------
96
97/// Internal scope-symbol kind mirroring [`ModuleExportKind`] for scope
98/// resolution of `export { name }` statements.
99///
100/// `Const` is the well-bounded module-level `const` binding kind introduced
101/// in R8 W8 Cluster A (2026-05-24, user Option (a) ruling); it is the only
102/// `VariableDecl`-shape that may be re-exported via `pub { name }` or appear
103/// as a `pub const` source_decl. `Value` is the residual catch-all for
104/// `let`/`var` decls which remain non-exportable.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106enum ScopeSymbolKind {
107    Function,
108    BuiltinFunction,
109    TypeAlias,
110    BuiltinType,
111    Trait,
112    Enum,
113    Annotation,
114    Value,
115    Const,
116}
117
118fn scope_symbol_kind_to_export(kind: ScopeSymbolKind) -> ModuleExportKind {
119    match kind {
120        ScopeSymbolKind::Function => ModuleExportKind::Function,
121        ScopeSymbolKind::BuiltinFunction => ModuleExportKind::BuiltinFunction,
122        ScopeSymbolKind::TypeAlias => ModuleExportKind::TypeAlias,
123        ScopeSymbolKind::BuiltinType => ModuleExportKind::BuiltinType,
124        ScopeSymbolKind::Trait => ModuleExportKind::Trait,
125        ScopeSymbolKind::Enum => ModuleExportKind::Enum,
126        ScopeSymbolKind::Annotation => ModuleExportKind::Annotation,
127        ScopeSymbolKind::Value => ModuleExportKind::Value,
128        ScopeSymbolKind::Const => ModuleExportKind::Value,
129    }
130}
131
132/// Lightweight scope used to resolve `export { name }` to the right kind.
133struct ScopeTable {
134    symbols: std::collections::HashMap<String, (ScopeSymbolKind, Span)>,
135}
136
137impl ScopeTable {
138    fn from_program(program: &Program) -> Self {
139        let mut symbols = std::collections::HashMap::new();
140        for item in &program.items {
141            match item {
142                Item::Function(f, span) => {
143                    symbols.insert(f.name.clone(), (ScopeSymbolKind::Function, *span));
144                }
145                Item::BuiltinFunctionDecl(f, span) => {
146                    symbols.insert(f.name.clone(), (ScopeSymbolKind::BuiltinFunction, *span));
147                }
148                Item::BuiltinTypeDecl(t, span) => {
149                    symbols.insert(t.name.clone(), (ScopeSymbolKind::BuiltinType, *span));
150                }
151                Item::TypeAlias(a, span) => {
152                    symbols.insert(a.name.clone(), (ScopeSymbolKind::TypeAlias, *span));
153                }
154                Item::Enum(e, span) => {
155                    symbols.insert(e.name.clone(), (ScopeSymbolKind::Enum, *span));
156                }
157                Item::StructType(s, span) => {
158                    symbols.insert(s.name.clone(), (ScopeSymbolKind::TypeAlias, *span));
159                }
160                Item::Trait(t, span) => {
161                    symbols.insert(t.name.clone(), (ScopeSymbolKind::Trait, *span));
162                }
163                Item::VariableDecl(decl, span) => {
164                    if let Some(name) = decl.pattern.as_identifier() {
165                        // R8 W8 Cluster A: `const` at module scope is the
166                        // exportable variant; `let`/`var` stay Value (still
167                        // rejected by the Named-export resolution branch).
168                        let kind = if decl.kind == crate::ast::VarKind::Const {
169                            ScopeSymbolKind::Const
170                        } else {
171                            ScopeSymbolKind::Value
172                        };
173                        symbols.insert(name.to_string(), (kind, *span));
174                    }
175                }
176                Item::Export(export, export_span) => {
177                    // `pub const NAME = ...` parses as an Export wrapping a
178                    // Named([NAME]) export with a `source_decl` carrying the
179                    // VariableDecl. Pick those up here so that the Named-
180                    // export resolution branch can find them in scope.
181                    if let Some(decl) = export.source_decl.as_ref() {
182                        if let Some(name) = decl.pattern.as_identifier() {
183                            let kind = if decl.kind == crate::ast::VarKind::Const {
184                                ScopeSymbolKind::Const
185                            } else {
186                                ScopeSymbolKind::Value
187                            };
188                            symbols
189                                .entry(name.to_string())
190                                .or_insert((kind, *export_span));
191                        }
192                    }
193                }
194                Item::AnnotationDef(a, span) => {
195                    symbols.insert(a.name.clone(), (ScopeSymbolKind::Annotation, *span));
196                }
197                _ => {}
198            }
199        }
200        Self { symbols }
201    }
202
203    fn resolve(&self, name: &str) -> Option<(ScopeSymbolKind, Span)> {
204        self.symbols.get(name).copied()
205    }
206}
207
208/// Collect exported symbol metadata from a parsed module AST.
209///
210/// This is the canonical implementation shared by both the runtime module
211/// loader and the VM import inliner. It handles both direct exports
212/// (`pub fn`, `pub type`, etc.) and named re-exports (`export { a, b }`).
213pub fn collect_exported_symbols(program: &Program) -> Result<Vec<ModuleExportSymbol>> {
214    let scope = ScopeTable::from_program(program);
215    let mut symbols = Vec::new();
216
217    for item in &program.items {
218        let Item::Export(export, _) = item else {
219            continue;
220        };
221
222        // Direct exports: the ExportItem already carries name + kind.
223        if let Some((name, kind)) = direct_export_target(&export.item) {
224            let span = match &export.item {
225                ExportItem::Function(f) => f.name_span,
226                ExportItem::BuiltinFunction(f) => f.name_span,
227                ExportItem::Annotation(a) => a.name_span,
228                ExportItem::ForeignFunction(f) => f.name_span,
229                _ => scope
230                    .resolve(&name)
231                    .map(|(_, span)| span)
232                    .unwrap_or_default(),
233            };
234            symbols.push(ModuleExportSymbol {
235                name,
236                alias: None,
237                kind,
238                span,
239            });
240            continue;
241        }
242
243        // Named re-exports: resolve through scope table.
244        if let ExportItem::Named(specs) = &export.item {
245            for spec in specs {
246                match scope.resolve(&spec.name) {
247                    Some((kind, span)) => {
248                        // R8 W8 Cluster A (2026-05-24): `Value` (i.e. `let`/`var`)
249                        // remains non-exportable; `Const` is the bounded
250                        // exportable shape introduced by the module-level
251                        // `const` feature.
252                        if kind == ScopeSymbolKind::Value {
253                            return Err(ShapeError::ModuleError {
254                                message: format!(
255                                    "Cannot export variable '{}': variable exports are not yet supported. \
256                                     Only `const` bindings (plus functions and types) can be exported.",
257                                    spec.name
258                                ),
259                                module_path: None,
260                            });
261                        }
262                        symbols.push(ModuleExportSymbol {
263                            name: spec.name.clone(),
264                            alias: spec.alias.clone(),
265                            kind: scope_symbol_kind_to_export(kind),
266                            span,
267                        });
268                    }
269                    None => {
270                        return Err(ShapeError::ModuleError {
271                            message: format!(
272                                "Cannot export '{}': not found in module scope",
273                                spec.name
274                            ),
275                            module_path: None,
276                        });
277                    }
278                }
279            }
280        }
281    }
282
283    Ok(symbols)
284}
285
286// ---------------------------------------------------------------------------
287// export_kind_description
288// ---------------------------------------------------------------------------
289
290/// Human-readable description of an export kind for diagnostics.
291pub fn export_kind_description(kind: ModuleExportKind) -> &'static str {
292    match kind {
293        ModuleExportKind::Function => "a function",
294        ModuleExportKind::BuiltinFunction => "a builtin function",
295        ModuleExportKind::TypeAlias => "a type",
296        ModuleExportKind::BuiltinType => "a builtin type",
297        ModuleExportKind::Trait => "a trait",
298        ModuleExportKind::Enum => "an enum",
299        ModuleExportKind::Annotation => "an annotation",
300        ModuleExportKind::Value => "a value",
301    }
302}