1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
32
33use lex_syntax::lock::{LockEntry, LockFile};
34use lex_types::{Ty, TypeError};
35
36use crate::render::{map_stage_names, module_file, package_head_at_op, stage_prefix, NameSite};
37use crate::store::{Store, StoreError};
38
39#[derive(Debug, Clone, Default)]
44pub struct ResolvedDeps {
45 pub modules: BTreeMap<String, Ty>,
47 pub types: BTreeMap<String, Vec<lex_ast::TypeDecl>>,
51 pub prefixes: BTreeMap<String, String>,
53 pub diagnostics: Vec<TypeError>,
56}
57
58#[derive(Debug, Clone)]
60pub struct ModuleSurface {
61 pub record: Ty,
63 pub types: Vec<lex_ast::TypeDecl>,
66 pub prefix: Option<String>,
70}
71
72pub fn split_package_import(reference: &str) -> Option<(&str, &str)> {
76 if reference.starts_with("./")
77 || reference.starts_with("../")
78 || reference.starts_with('/')
79 || reference.starts_with("std.")
80 {
81 return None;
82 }
83 reference.split_once('/')
84}
85
86pub const UNPINNED_HINT: &str =
88 "hosted verification resolves dependencies only through `lex.lock` \
89pins into registry stores (it never fetches git): declare a registry source for it in `lex.toml` \
90and run `lex pkg lock`";
91
92pub fn unpinned(reference: &str, package: &str) -> TypeError {
94 TypeError::UnpinnedDependency {
95 at_node: "n_0".into(),
96 reference: reference.to_string(),
97 package: package.to_string(),
98 hint: UNPINNED_HINT.to_string(),
99 }
100}
101
102fn unresolved(reference: &str, package: &str, reason: impl Into<String>) -> TypeError {
103 TypeError::UnresolvedDependency {
104 at_node: "n_0".into(),
105 reference: reference.to_string(),
106 package: package.to_string(),
107 reason: reason.into(),
108 }
109}
110
111pub trait DepLocator {
113 fn open(&self, entry: &LockEntry) -> Result<(String, Store), String>;
119}
120
121fn import_refs(stages: &[lex_ast::Stage]) -> Vec<String> {
124 let mut seen = BTreeSet::new();
125 stages
126 .iter()
127 .filter_map(|s| match s {
128 lex_ast::Stage::Import(i) if seen.insert(i.reference.clone()) => {
129 Some(i.reference.clone())
130 }
131 _ => None,
132 })
133 .collect()
134}
135
136pub fn resolve_with_lock(
139 locator: &dyn DepLocator,
140 stages: &[lex_ast::Stage],
141 lock: Option<&LockFile>,
142) -> ResolvedDeps {
143 let mut walk = Walk {
144 locator,
145 surfaces: HashMap::new(),
146 deps: HashMap::new(),
147 visiting: HashSet::new(),
148 pins: BTreeMap::new(),
149 };
150 let mut out = walk.resolve_refs(&import_refs(stages), lock);
151 for (package, heads) in &walk.pins {
153 if heads.len() > 1 {
154 out.diagnostics.push(TypeError::DependencyConflict {
155 at_node: "n_0".into(),
156 package: package.clone(),
157 heads: heads.iter().cloned().collect(),
158 });
159 }
160 }
161 out
162}
163
164pub fn parse_lock(toml: &str) -> Option<LockFile> {
167 LockFile::from_toml(toml).ok()
168}
169
170struct Walk<'a> {
171 locator: &'a dyn DepLocator,
172 surfaces: HashMap<(String, String, String), Result<ModuleSurface, String>>,
174 deps: HashMap<(String, String), ResolvedDeps>,
176 visiting: HashSet<(String, String)>,
178 pins: BTreeMap<String, BTreeSet<String>>,
180}
181
182impl Walk<'_> {
183 fn resolve_refs(&mut self, refs: &[String], lock: Option<&LockFile>) -> ResolvedDeps {
184 let mut out = ResolvedDeps::default();
185 for reference in refs {
186 let Some((pkg, module)) = split_package_import(reference) else {
187 continue;
188 };
189 let Some(entry) = lock.and_then(|l| l.entry(pkg)) else {
190 out.diagnostics.push(unpinned(reference, pkg));
191 continue;
192 };
193 let Some(head) = entry.head_op.clone() else {
194 out.diagnostics.push(unresolved(
195 reference,
196 pkg,
197 format!(
198 "the lock pins version {} but no op-log head; re-run `lex pkg lock` against a registry that serves heads",
199 entry.version
200 ),
201 ));
202 continue;
203 };
204 self.pins
205 .entry(pkg.to_string())
206 .or_default()
207 .insert(head.clone());
208 match self.surface(entry, &head, pkg, module) {
209 Ok(s) => {
210 out.modules.insert(reference.clone(), s.record);
211 if !s.types.is_empty() {
212 out.types.insert(reference.clone(), s.types);
213 }
214 if let Some(p) = s.prefix {
215 out.prefixes.insert(reference.clone(), p);
216 }
217 }
218 Err(reason) => out.diagnostics.push(unresolved(reference, pkg, reason)),
219 }
220 }
221 out
222 }
223
224 fn surface(
225 &mut self,
226 entry: &LockEntry,
227 head: &str,
228 pkg: &str,
229 module: &str,
230 ) -> Result<ModuleSurface, String> {
231 let (id, store) = self.locator.open(entry)?;
232 let key = (id.clone(), head.to_string(), module.to_string());
233 if let Some(hit) = self.surfaces.get(&key) {
234 return hit.clone();
235 }
236 let node = (id.clone(), head.to_string());
237 if self.visiting.contains(&node) {
238 return Err(format!(
240 "dependency cycle: `{pkg}` at {head} depends on itself"
241 ));
242 }
243 let deps = match self.deps.get(&node) {
244 Some(d) => d.clone(),
245 None => {
246 self.visiting.insert(node.clone());
247 let d = self.deps_of(&store, head);
248 self.visiting.remove(&node);
249 self.deps.insert(node.clone(), d.clone());
250 d
251 }
252 };
253 let result = if deps.diagnostics.is_empty() {
254 module_surface_at_op_with(&store, head, pkg, module, &deps).map_err(|e| e.to_string())
255 } else {
256 let inner: Vec<String> = deps.diagnostics.iter().map(|d| d.to_string()).collect();
257 Err(format!(
258 "its own dependencies do not resolve: {}",
259 inner.join("; ")
260 ))
261 };
262 self.surfaces.insert(key, result.clone());
263 result
264 }
265
266 fn deps_of(&mut self, store: &Store, head: &str) -> ResolvedDeps {
268 let refs: Vec<String> = match package_head_at_op(store, head) {
269 Ok(h) => h.flat_imports.keys().cloned().collect(),
270 Err(e) => {
271 let mut d = ResolvedDeps::default();
272 d.diagnostics
273 .push(unresolved("", "", format!("reading head {head}: {e}")));
274 return d;
275 }
276 };
277 if !refs.iter().any(|r| split_package_import(r).is_some()) {
278 return ResolvedDeps::default();
279 }
280 let lock = store
281 .committed_lock_inherited(head)
282 .ok()
283 .flatten()
284 .and_then(|t| parse_lock(&t));
285 self.resolve_refs(&refs, lock.as_ref())
286 }
287}
288
289pub fn module_surface_at_op_with(
306 store: &Store,
307 head_op: &str,
308 package: &str,
309 module: &str,
310 deps: &ResolvedDeps,
311) -> Result<ModuleSurface, StoreError> {
312 let head = package_head_at_op(store, head_op)?;
313 let pairs: Vec<(String, String)> = head
314 .map
315 .iter()
316 .map(|(s, st)| (s.clone(), st.clone()))
317 .collect();
318 let mut decls: Vec<(Option<String>, lex_ast::Stage)> = Vec::new();
319 for ((sig, _), ast) in pairs.iter().zip(store.get_asts_for_sigs_bulk(&pairs)) {
320 decls.push((head.sig_files.get(sig).cloned(), ast?));
321 }
322
323 let alias_prefix: BTreeMap<String, String> = head
325 .flat_imports
326 .iter()
327 .filter_map(|(r, alias)| deps.prefixes.get(r).map(|p| (alias.clone(), p.clone())))
328 .collect();
329 let dep_types: Vec<lex_ast::TypeDecl> = {
330 let mut seen = BTreeSet::new();
331 deps.types
332 .values()
333 .flatten()
334 .filter(|d| seen.insert(d.name.clone()))
335 .cloned()
336 .collect()
337 };
338
339 let prefixed = !decls.is_empty()
340 && decls
341 .iter()
342 .all(|(file, s)| file.is_some() && stage_prefix(s).is_some());
343
344 let mut rename: BTreeMap<String, String> = BTreeMap::new();
346 let mut file_prefix: BTreeMap<String, String> = BTreeMap::new();
347 if prefixed {
348 for (file, s) in &decls {
349 let (Some(file), Some(stored)) = (file, stage_prefix(s)) else {
350 continue;
351 };
352 let canonical = lex_syntax::package_file_prefix(package, file);
353 rename.insert(stored, canonical.clone());
354 file_prefix.insert(file.clone(), canonical);
355 }
356 }
357
358 let mut stages: Vec<lex_ast::Stage> = head
359 .flat_imports
360 .iter()
361 .map(|(reference, alias)| {
362 lex_ast::Stage::Import(lex_ast::Import {
363 reference: reference.clone(),
364 alias: alias.clone(),
365 })
366 })
367 .collect();
368 let body: Vec<lex_ast::Stage> = if prefixed {
369 decls
370 .into_iter()
371 .map(|(_, mut s)| {
372 map_stage_names(&mut s, &mut |site, name| {
373 let Some((q, rest)) = name.split_once('.') else {
374 return name.to_string();
375 };
376 if let Some(c) = rename.get(q) {
377 return format!("{c}.{rest}");
378 }
379 match (site, alias_prefix.get(q)) {
380 (NameSite::Type, Some(p)) => format!("{p}.{rest}"),
381 _ => name.to_string(),
382 }
383 });
384 s
385 })
386 .collect()
387 } else {
388 let mut v = match crate::render::demangled_module_stages(store, head_op, module) {
390 Ok(v) => v,
391 Err(_) => crate::render::demangled_head_stages(store, head_op)?,
392 };
393 v.retain(|s| !matches!(s, lex_ast::Stage::Import(_)));
394 for s in &mut v {
395 map_stage_names(s, &mut |site, name| match (site, name.split_once('.')) {
396 (NameSite::Type, Some((q, rest))) => match alias_prefix.get(q) {
397 Some(p) => format!("{p}.{rest}"),
398 None => name.to_string(),
399 },
400 _ => name.to_string(),
401 });
402 }
403 v
404 };
405 stages.extend(body.iter().cloned());
406
407 let types =
408 lex_types::check_program_with_deps(&stages, &deps.modules, &deps.types, &deps.prefixes)
409 .map_err(StoreError::TypeError)?;
410
411 let own_types = body.iter().filter_map(|s| match s {
412 lex_ast::Stage::TypeDecl(td) => Some(td.clone()),
413 _ => None,
414 });
415
416 if prefixed {
417 let target = module_file(file_prefix.keys(), module)
418 .or_else(|| {
419 (file_prefix.len() == 1)
420 .then(|| file_prefix.keys().next().cloned())
421 .flatten()
422 })
423 .ok_or(StoreError::UnsupportedMultiModuleDependency)?;
424 let prefix = file_prefix[&target].clone();
425 let dot = format!("{prefix}.");
426 let record = lex_types::module_record_from_fields(types.fn_signatures.iter().filter_map(
427 |(name, scheme)| {
428 name.strip_prefix(&dot)
429 .map(|bare| (bare.to_string(), scheme.ty.clone()))
430 },
431 ));
432 let mut all = dep_types;
433 all.extend(own_types);
434 Ok(ModuleSurface {
435 record,
436 types: all,
437 prefix: Some(prefix),
438 })
439 } else {
440 let record = lex_types::module_record_from_fields(
441 types
442 .fn_signatures
443 .iter()
444 .filter(|(name, _)| !name.contains('.'))
445 .map(|(name, scheme)| (name.clone(), scheme.ty.clone())),
446 );
447 let own: Vec<lex_ast::TypeDecl> = own_types.filter(|t| !t.name.contains('.')).collect();
450 Ok(ModuleSurface {
451 record,
452 types: own,
453 prefix: None,
454 })
455 }
456}