Skip to main content

harn_parser/
namespace_demand.rs

1//! Conservative export demand for namespace imports.
2//!
3//! This analysis is shared by bytecode compilation and module artifact
4//! construction. It only selects individual members when every use of an
5//! imported namespace is a statically named member access. Any use whose
6//! meaning could depend on the complete namespace widens the demand to
7//! [`NamespaceDemand::Whole`].
8
9use std::collections::{BTreeMap, BTreeSet};
10
11use crate::{lexical::binding_pattern_names, visit::immediate_children, Node, SNode};
12
13/// The public exports an importer can observe through a namespace binding.
14#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
15pub enum NamespaceDemand {
16    /// Preserve the complete public namespace.
17    Whole,
18    /// Project only these statically named members.
19    Members(BTreeSet<String>),
20}
21
22impl NamespaceDemand {
23    fn add_member(&mut self, member: &str) {
24        if let Self::Members(members) = self {
25            members.insert(member.to_string());
26        }
27    }
28
29    fn widen(&mut self) {
30        *self = Self::Whole;
31    }
32}
33
34/// Compute conservative member demand for every namespace import in `program`.
35///
36/// An unused private namespace has an empty member set: importing it still
37/// loads and initializes its target module, but consumers may omit all public
38/// exports from the namespace projection. Public namespace imports, alias
39/// escapes, dynamic access, duplicate/conflicting bindings, and future syntax
40/// not recognized as a direct member access all require the whole namespace.
41pub fn namespace_import_demands(program: &[SNode]) -> BTreeMap<String, NamespaceDemand> {
42    let mut demands = BTreeMap::new();
43    let mut aliases = BTreeSet::new();
44
45    for node in program {
46        collect_imports(node, &mut aliases, &mut demands);
47    }
48
49    for alias in aliases {
50        let demand = demands
51            .get_mut(&alias)
52            .expect("namespace alias and demand are recorded together");
53        if matches!(demand, NamespaceDemand::Whole) {
54            continue;
55        }
56        if program
57            .iter()
58            .any(|node| conflicts_with_alias(node, &alias))
59        {
60            demand.widen();
61            continue;
62        }
63        for node in program {
64            analyze_node(node, &alias, demand);
65            if matches!(demand, NamespaceDemand::Whole) {
66                break;
67            }
68        }
69    }
70
71    demands
72}
73
74fn collect_imports(
75    node: &SNode,
76    aliases: &mut BTreeSet<String>,
77    demands: &mut BTreeMap<String, NamespaceDemand>,
78) {
79    if let Node::NamespaceImport { alias, is_pub, .. } = &node.node {
80        let first = aliases.insert(alias.clone());
81        let demand = if *is_pub || !first {
82            NamespaceDemand::Whole
83        } else {
84            NamespaceDemand::Members(BTreeSet::new())
85        };
86        demands
87            .entry(alias.clone())
88            .and_modify(NamespaceDemand::widen)
89            .or_insert(demand);
90    }
91    for child in immediate_children(node) {
92        collect_imports(child, aliases, demands);
93    }
94}
95
96fn conflicts_with_alias(node: &SNode, alias: &str) -> bool {
97    let conflicts = match &node.node {
98        // A wildcard can introduce any public name, so it prevents proving
99        // which binding a bare identifier denotes.
100        Node::ImportDecl { .. } => true,
101        Node::SelectiveImport { names, .. } => names.iter().any(|name| name == alias),
102        Node::NamespaceImport { .. } => false,
103        Node::LetBinding { pattern, .. } | Node::ConstBinding { pattern, .. } => {
104            binding_pattern_names(pattern)
105                .iter()
106                .any(|name| name == alias)
107        }
108        Node::Pipeline { name, params, .. }
109        | Node::FnDecl { name, params, .. }
110        | Node::ToolDecl { name, params, .. } => {
111            name == alias || params.iter().any(|param| param.name == alias)
112        }
113        Node::OverrideDecl { name, params, .. } => {
114            name == alias || params.iter().any(|param| param == alias)
115        }
116        Node::Closure { params, .. } => params.iter().any(|param| param.name == alias),
117        Node::ForIn { pattern, .. } => binding_pattern_names(pattern)
118            .iter()
119            .any(|name| name == alias),
120        Node::TryCatch { error_var, .. } => error_var.as_deref() == Some(alias),
121        Node::Parallel { variable, .. } => variable.as_deref() == Some(alias),
122        Node::SelectExpr { cases, .. } => cases.iter().any(|case| case.variable == alias),
123        Node::EnumDecl { name, .. }
124        | Node::StructDecl { name, .. }
125        | Node::TypeDecl { name, .. }
126        | Node::InterfaceDecl { name, .. }
127        | Node::SkillDecl { name, .. } => name == alias,
128        Node::EvalPackDecl { binding_name, .. } => binding_name == alias,
129        _ => false,
130    };
131    conflicts
132        || immediate_children(node)
133            .into_iter()
134            .any(|child| conflicts_with_alias(child, alias))
135}
136
137fn analyze_node(node: &SNode, alias: &str, demand: &mut NamespaceDemand) {
138    if matches!(demand, NamespaceDemand::Whole) {
139        return;
140    }
141
142    match &node.node {
143        Node::NamespaceImport { .. } => {}
144        Node::PropertyAccess { object, property }
145        | Node::OptionalPropertyAccess { object, property }
146            if is_identifier(object, alias) =>
147        {
148            demand.add_member(property);
149        }
150        Node::MethodCall {
151            object,
152            method,
153            args,
154        }
155        | Node::OptionalMethodCall {
156            object,
157            method,
158            args,
159        } if is_identifier(object, alias) => {
160            demand.add_member(method);
161            for arg in args {
162                analyze_node(arg, alias, demand);
163            }
164        }
165        Node::Assignment { target, value, .. } => {
166            if contains_alias(target, alias) {
167                demand.widen();
168            } else {
169                analyze_node(value, alias, demand);
170            }
171        }
172        Node::Identifier(name) if name == alias => demand.widen(),
173        Node::FunctionCall { name, .. } if name == alias => demand.widen(),
174        Node::EnumConstruct { enum_name, .. } if enum_name == alias => demand.widen(),
175        Node::StructConstruct { struct_name, .. } if struct_name == alias => demand.widen(),
176        _ => {
177            for child in immediate_children(node) {
178                analyze_node(child, alias, demand);
179                if matches!(demand, NamespaceDemand::Whole) {
180                    break;
181                }
182            }
183        }
184    }
185}
186
187fn is_identifier(node: &SNode, name: &str) -> bool {
188    matches!(&node.node, Node::Identifier(candidate) if candidate == name)
189}
190
191fn contains_alias(node: &SNode, alias: &str) -> bool {
192    match &node.node {
193        Node::Identifier(name) => name == alias,
194        Node::FunctionCall { name, .. } => {
195            name == alias
196                || immediate_children(node)
197                    .into_iter()
198                    .any(|child| contains_alias(child, alias))
199        }
200        Node::EnumConstruct { enum_name, .. } => {
201            enum_name == alias
202                || immediate_children(node)
203                    .into_iter()
204                    .any(|child| contains_alias(child, alias))
205        }
206        Node::StructConstruct { struct_name, .. } => {
207            struct_name == alias
208                || immediate_children(node)
209                    .into_iter()
210                    .any(|child| contains_alias(child, alias))
211        }
212        _ => immediate_children(node)
213            .into_iter()
214            .any(|child| contains_alias(child, alias)),
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::parse_source;
222
223    fn demand(source: &str, alias: &str) -> NamespaceDemand {
224        namespace_import_demands(&parse_source(source).expect("source parses"))[alias].clone()
225    }
226
227    fn members(names: &[&str]) -> NamespaceDemand {
228        NamespaceDemand::Members(names.iter().map(|name| (*name).to_string()).collect())
229    }
230
231    #[test]
232    fn collects_static_property_and_method_members() {
233        assert_eq!(
234            demand(
235                r#"
236                import * as ui from "./ui.harn"
237                const page = ui.page
238                ui.render(page)
239                ui?.close()
240                "#,
241                "ui",
242            ),
243            members(&["close", "page", "render"])
244        );
245    }
246
247    #[test]
248    fn unused_private_namespace_has_empty_member_demand() {
249        assert_eq!(
250            demand(r#"import * as ui from "./ui.harn""#, "ui"),
251            members(&[])
252        );
253    }
254
255    #[test]
256    fn alias_escape_and_dynamic_access_require_whole_namespace() {
257        for source in [
258            r#"import * as ui from "./ui.harn"
259               return ui"#,
260            r#"import * as ui from "./ui.harn"
261               const name = "page"
262               return ui[name]"#,
263        ] {
264            assert_eq!(demand(source, "ui"), NamespaceDemand::Whole);
265        }
266    }
267
268    #[test]
269    fn public_import_and_shadowing_ambiguity_require_whole_namespace() {
270        for source in [
271            r#"pub import * as ui from "./ui.harn""#,
272            r#"import * as ui from "./ui.harn"
273               fn render(ui) { return ui.page }
274               return ui.page"#,
275            r#"import * as ui from "./ui.harn"
276               import "./other.harn"
277               return ui.page"#,
278        ] {
279            assert_eq!(demand(source, "ui"), NamespaceDemand::Whole);
280        }
281    }
282
283    #[test]
284    fn assignment_through_namespace_requires_whole_namespace() {
285        assert_eq!(
286            demand(
287                r#"import * as ui from "./ui.harn"
288                   ui.page = "replacement""#,
289                "ui",
290            ),
291            NamespaceDemand::Whole
292        );
293    }
294}