Skip to main content

harn_modules/
symbol_reachability.rs

1//! Graph-wide export demand for closed programs.
2//!
3//! Module discovery remains effect-conservative: every syntactic import edge
4//! stays in the closed graph and every reached initializer still executes. The
5//! lattice only controls which public symbols and callable bytecode a closed
6//! artifact must retain.
7
8use std::collections::{BTreeMap, BTreeSet};
9use std::path::{Path, PathBuf};
10
11use harn_parser::{namespace_import_demands, NamespaceDemand};
12use serde::{Deserialize, Serialize};
13
14use crate::{canonical_path, ModuleGraphBuild};
15
16#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
17pub enum ExportDemand {
18    #[default]
19    InitializationOnly,
20    Members(BTreeSet<String>),
21    WholeNamespace,
22}
23
24impl ExportDemand {
25    pub fn add_members<I>(&mut self, members: I) -> bool
26    where
27        I: IntoIterator<Item = String>,
28    {
29        match self {
30            Self::WholeNamespace => false,
31            Self::InitializationOnly => {
32                let members = members.into_iter().collect::<BTreeSet<_>>();
33                let changed = !members.is_empty();
34                *self = Self::Members(members);
35                changed
36            }
37            Self::Members(current) => {
38                let before = current.len();
39                current.extend(members);
40                current.len() != before
41            }
42        }
43    }
44
45    pub fn widen(&mut self) -> bool {
46        if matches!(self, Self::WholeNamespace) {
47            false
48        } else {
49            *self = Self::WholeNamespace;
50            true
51        }
52    }
53
54    pub fn contains(&self, name: &str) -> bool {
55        match self {
56            Self::InitializationOnly => false,
57            Self::Members(members) => members.contains(name),
58            Self::WholeNamespace => true,
59        }
60    }
61}
62
63#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
64pub struct ModuleSymbolDemand {
65    pub exports: ExportDemand,
66}
67
68#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
69pub struct SymbolReachability {
70    pub modules: BTreeMap<PathBuf, ModuleSymbolDemand>,
71}
72
73impl SymbolReachability {
74    pub fn demand_for(&self, path: &Path) -> ExportDemand {
75        self.modules
76            .get(&canonical_path(path))
77            .map(|demand| demand.exports.clone())
78            .unwrap_or(ExportDemand::WholeNamespace)
79    }
80}
81
82/// Resolve export demand across a closed graph to a monotone fixpoint.
83///
84/// Selective and namespace imports retain exact members when structural
85/// analysis proves them. Wildcard flattening and every unresolved edge widen
86/// the target to its complete namespace. Import edges and module initializers
87/// are never removed by this pass.
88pub fn closed_program_reachability(
89    build: &ModuleGraphBuild,
90    entrypoint: &Path,
91) -> SymbolReachability {
92    let mut modules = build
93        .graph
94        .module_paths()
95        .into_iter()
96        .map(|path| (canonical_path(&path), ModuleSymbolDemand::default()))
97        .collect::<BTreeMap<_, _>>();
98    // The entry is executed as a chunk rather than imported, but treating its
99    // public surface as whole keeps future typed-entry selection conservative.
100    modules
101        .entry(canonical_path(entrypoint))
102        .or_default()
103        .exports
104        .widen();
105
106    loop {
107        let mut changed = false;
108        for path in build.graph.module_paths() {
109            let path = canonical_path(&path);
110            let namespace_demands = build
111                .parsed_sources
112                .get(&path)
113                .map(|parsed| namespace_import_demands(&parsed.program))
114                .unwrap_or_default();
115            for import in build.graph.imports_for_module(&path) {
116                let Some(target) = import.resolved_path.map(|path| canonical_path(&path)) else {
117                    // A malformed graph cannot produce a specialized artifact;
118                    // keeping every known module whole is the safe diagnostic
119                    // fallback until the caller rejects the unresolved import.
120                    for demand in modules.values_mut() {
121                        changed |= demand.exports.widen();
122                    }
123                    continue;
124                };
125                let target_demand = modules.entry(target).or_default();
126                if let Some(alias) = import.namespace_alias {
127                    match namespace_demands.get(&alias) {
128                        Some(NamespaceDemand::Members(members)) => {
129                            changed |= target_demand.exports.add_members(members.iter().cloned());
130                        }
131                        Some(NamespaceDemand::Whole) | None => {
132                            changed |= target_demand.exports.widen();
133                        }
134                    }
135                } else if let Some(names) = import.selective_names {
136                    changed |= target_demand.exports.add_members(names);
137                } else {
138                    changed |= target_demand.exports.widen();
139                }
140            }
141        }
142        if !changed {
143            break;
144        }
145    }
146    SymbolReachability { modules }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use std::fs;
153
154    #[test]
155    fn namespace_members_union_across_graph_and_alias_escape_widens() {
156        let dir = tempfile::tempdir().unwrap();
157        let library = dir.path().join("library.harn");
158        let exact = dir.path().join("exact.harn");
159        let whole = dir.path().join("whole.harn");
160        let entry = dir.path().join("entry.harn");
161        fs::write(&library, "pub fn kept() { 1 }\npub fn other() { 2 }").unwrap();
162        fs::write(
163            &exact,
164            "import * as lib from \"./library.harn\"\npub fn value() { lib.kept() }",
165        )
166        .unwrap();
167        fs::write(
168            &whole,
169            "import * as lib from \"./library.harn\"\npub fn value() { lib }",
170        )
171        .unwrap();
172        fs::write(
173            &entry,
174            "import { value } from \"./exact.harn\"\nfn main() { value() }",
175        )
176        .unwrap();
177
178        let build = crate::build_closed_program(std::slice::from_ref(&entry));
179        assert_eq!(
180            closed_program_reachability(&build, &entry).demand_for(&library),
181            ExportDemand::Members(BTreeSet::from(["kept".to_string()]))
182        );
183
184        fs::write(
185            &entry,
186            "import { value } from \"./whole.harn\"\nfn main() { value() }",
187        )
188        .unwrap();
189        let build = crate::build_closed_program(std::slice::from_ref(&entry));
190        assert_eq!(
191            closed_program_reachability(&build, &entry).demand_for(&library),
192            ExportDemand::WholeNamespace
193        );
194    }
195}