harn_modules/
symbol_reachability.rs1use 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
82pub 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 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 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}