lex_syntax/loader.rs
1//! Multi-file loader: resolves `import "./..."`, `import "../..."`, and
2//! `import "/abs/..."` statements relative to the importer, recursively
3//! parses, and produces a single [`Program`] with all stages merged.
4//!
5//! Names that are local to an imported file are mangled with a
6//! **per-file-path** prefix, so the same module imported via multiple
7//! aliases (or from multiple parents in a diamond shape) collapses to
8//! one set of mangled names — same SigId, same nominal identity.
9//! Stdlib imports (`import "std.foo" as bar`) pass through unchanged.
10//!
11//! ## Mangling
12//!
13//! Each loaded file gets a prefix derived from its filesystem path.
14//! The entry file's prefix is empty (so `lex run main.lex process`
15//! works unchanged). Imported files use `<stem>_<hash>` where `hash`
16//! is the first 8 hex chars of SHA-256 of the file's *mangling key*.
17//! The hash disambiguates same-stem files in different directories
18//! without forcing a project manifest.
19//!
20//! The mangling key is the canonical absolute path by default, and the
21//! path **relative to a caller-supplied root** when loading through
22//! [`load_program_with_root`] or [`load_package`]. Absolute paths are
23//! only stable as long as the tree stays put, which makes them unusable
24//! for anything that loads the same logical package from a fresh
25//! directory each time: a server unpacking an uploaded package into a
26//! per-request temp dir got a different prefix — and therefore a
27//! brand-new set of function names — for every file reached through a
28//! local import on every single request, so byte-identical republishes
29//! diffed as all-new functions and grew the branch's function set
30//! without bound (#826). Pass the package root and the key becomes
31//! `src/error.lex`, identical across requests. Files outside the root
32//! keep the absolute-path key (a dependency in the shared package cache
33//! lives at a stable absolute path of its own, and "relative to this
34//! package" says nothing useful about it).
35//!
36//! [`load_package`] adds a `namespace` ahead of the relative path
37//! (`lex-schema/src/error.lex`), because a relative key is only unique
38//! *within* one package: two packages published into one branch can both
39//! have a `src/error.lex`, and without the namespace both get the same
40//! `error_<hash>.format`.
41//!
42//! ## Whole-package loading
43//!
44//! [`load_program`] and [`load_program_with_root`] each flatten one
45//! entry's entire local-import closure into that entry's program, which
46//! is what `lex run`/`lex check` want for a single file. A caller holding
47//! *every* file of a package — a publish server, say — gets each shared
48//! dependency back once per importer instead: 2,239 declarations for 693
49//! distinct names on a real 21-file package whose `error.lex` 17 files
50//! import (#828). [`load_package`] is the whole-package entry point: one
51//! shared pass, every file exactly once, and every file mangled (no
52//! unmangled entry), since bare names from different files would collide
53//! in one program.
54//!
55//! Within a file at prefix `P`:
56//!
57//! - `fn foo` declared in this file becomes `<P>.foo` (just `foo` at root).
58//! - `type T` declared in this file becomes `<P>.T`.
59//! - References to a locally-declared name get mangled, **unless** the
60//! name is shadowed by a binder (let, fn param, lambda param, or
61//! pattern binder) in scope.
62//! - `m.foo` where `m` is a path-import alias is rewritten to the
63//! imported file's prefix-qualified name. Two parents importing the
64//! same file see the same prefix → calls and types unify.
65//! - `m.foo` where `m` is a stdlib alias is unchanged.
66//!
67//! Variant constructors are **not** mangled — they live in a global
68//! namespace, and a collision between two imported types' constructors
69//! surfaces later as a type-check error. Same for record field names.
70//!
71//! ## Diamond imports
72//!
73//! `main.lex` imports `./left` and `./right`, both of which import
74//! `./shared`. `shared.lex` is parsed once per resolution, but its
75//! mangled items are merged into the output exactly once (subsequent
76//! loads from the same canonical path return an empty Program). This
77//! is what makes `s.build_report(...)` and `v.read_score(...)` agree
78//! on `Report`'s nominal identity.
79//!
80//! ## Limitations (tracked separately)
81//!
82//! The mangling key is a filesystem path (see above). Moving a file
83//! changes its SigId; renaming changes the file-stem half of the
84//! prefix, and under [`load_package`] that applies to every
85//! declaration, not only imported ones — a function moved between two
86//! files of a package is a new function there. A root-relative key
87//! narrows this to moves *within* the package, but does not remove it.
88//! The eventual fix — content-addressed identity decoupled from
89//! filesystem layout — lives with store-native imports
90//! (`import "stage:..."`); see the corresponding follow-up tracker.
91
92use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
93use std::path::{Path, PathBuf};
94use thiserror::Error;
95
96use sha2::{Digest, Sha256};
97
98use crate::syntax::*;
99use crate::workspace::{resolve_package_import, PackageError};
100use crate::{parse_source, SyntaxError};
101
102#[derive(Debug, Error)]
103pub enum LoadError {
104 #[error("read {path}: {source}")]
105 Io {
106 path: String,
107 #[source]
108 source: std::io::Error,
109 },
110 #[error("parse {path}: {source}")]
111 Syntax {
112 path: String,
113 #[source]
114 source: SyntaxError,
115 },
116 #[error("import cycle: {chain}")]
117 Cycle { chain: String },
118 #[error("import \"{reference}\" from {importer}: file not found")]
119 NotFound { importer: String, reference: String },
120 #[error("local imports (`./`, `../`, `/`) require a base path; cannot resolve from a string source")]
121 LocalImportInStringSource,
122 #[error(
123 "alias `{alias}` is bound to both \"{first}\" and \"{second}\" within one package; \
124 loading the package as a single unit cannot keep both"
125 )]
126 ConflictingAlias {
127 alias: String,
128 first: String,
129 second: String,
130 },
131 #[error("package import error: {0}")]
132 Package(#[from] PackageError),
133}
134
135/// Load a multi-file Lex program, expanding local imports relative to
136/// the entry path. Stdlib imports (`std.*`) pass through unchanged.
137pub fn load_program(entry: &Path) -> Result<Program, LoadError> {
138 load_rooted(entry, None)
139}
140
141/// Load a multi-file Lex program like [`load_program`], but derive
142/// mangling prefixes from each file's path **relative to `root`**
143/// instead of its absolute path.
144///
145/// Use this whenever the same logical package can be loaded from a
146/// different directory each time — an unpacked upload, a CI checkout, a
147/// scratch clone — and the mangled names it produces must match across
148/// those loads (#826). Files that do not live under `root` keep the
149/// absolute-path key, as do all files if `root` cannot be canonicalized.
150pub fn load_program_with_root(entry: &Path, root: &Path) -> Result<Program, LoadError> {
151 // Canonicalize the root too: the entry path is canonicalized below,
152 // and a root reached through a symlink (macOS's `/var/folders/...`
153 // temp dirs being the common case) would never prefix-match the
154 // canonicalized file paths otherwise.
155 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
156 load_rooted(entry, Some(root))
157}
158
159/// A package loaded as one unit by [`load_package`].
160#[derive(Debug)]
161pub struct LoadedPackage {
162 /// Every file's declarations, each exactly once, all prefix-mangled.
163 pub program: Program,
164 /// The stdlib modules each file imports *itself*, keyed by the file's
165 /// path relative to the package root (`src/schema.lex`). Unlike
166 /// `program`, this is per-file: the flattening entry points cannot
167 /// report it, because by the time they return, a file's imports and
168 /// those of everything it imports are one undifferentiated list.
169 pub imports_by_file: BTreeMap<String, BTreeSet<String>>,
170}
171
172/// Load a whole package as **one** program: every file gets its
173/// path-derived mangling prefix (no file is the unmangled "entry"), and
174/// each file's declarations appear exactly once however many other files
175/// import it.
176///
177/// [`load_program`] and [`load_program_with_root`] flatten each entry's
178/// whole local-import closure into that entry's program, so a caller
179/// holding N top-level files gets every shared dependency back N times —
180/// once per importer. The real 21-file `lex-schema` package, whose
181/// `error.lex` is imported by 17 of its files, yielded 2,239 `FnDecl`s
182/// for 693 distinct names that way, and a server that canonicalizes,
183/// type-checks, diffs and publishes each copy paid for all 2,239 (#828).
184/// One shared pass yields 447 — one per declaration.
185///
186/// Because no file is the entry, **no declaration keeps its bare
187/// source-level name**: `fn validate` in `src/field.lex` is
188/// `field_<hash>.validate`, not `validate`. That is what makes one
189/// program safe to type-check as a unit — two files may each declare
190/// their own local `validate`, and the checker's global scope is a map
191/// keyed by name, so bare names from different files would silently
192/// overwrite each other and check bodies against the wrong signature.
193///
194/// `namespace` is mixed into every mangling key ahead of the relative
195/// path, so the same internal layout in two different packages does not
196/// collapse onto one set of names. Callers publishing into a shared
197/// branch should pass the package name: a tenant hosting both
198/// `lex-schema` and `lex-ocpi` has two `src/error.lex` files, and a
199/// purely path-derived key gives both the same `error_<hash>.format`.
200///
201/// Stdlib imports are deduped by `(reference, alias)`. An alias bound to
202/// two *different* references inside one package is rejected with
203/// [`LoadError::ConflictingAlias`] rather than merged: the checker's
204/// alias scope is also name-keyed, so merging would silently resolve one
205/// file's calls against the other file's module.
206pub fn load_package(
207 entries: &[PathBuf],
208 root: &Path,
209 namespace: &str,
210) -> Result<LoadedPackage, LoadError> {
211 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
212 let mut state = LoaderState {
213 in_progress: Vec::new(),
214 loaded: HashSet::new(),
215 prefixes: HashMap::new(),
216 prefix_root: Some(root),
217 prefix_namespace: Some(namespace.to_string()),
218 imports_by_file: BTreeMap::new(),
219 };
220 // Deliberately no empty-prefix seeding: see the doc comment above.
221 let mut items: Vec<Item> = Vec::new();
222 let mut aliases: HashMap<String, String> = HashMap::new();
223 for entry in entries {
224 let canonical = entry.canonicalize().map_err(|source| LoadError::Io {
225 path: entry.display().to_string(),
226 source,
227 })?;
228 for item in state.load(&canonical)?.items {
229 if let Item::Import(imp) = &item {
230 match aliases.get(&imp.alias) {
231 // Same module under the same alias: one import is enough.
232 Some(existing) if existing == &imp.reference => continue,
233 Some(existing) => {
234 return Err(LoadError::ConflictingAlias {
235 alias: imp.alias.clone(),
236 first: existing.clone(),
237 second: imp.reference.clone(),
238 })
239 }
240 None => {
241 aliases.insert(imp.alias.clone(), imp.reference.clone());
242 }
243 }
244 }
245 items.push(item);
246 }
247 }
248 Ok(LoadedPackage {
249 program: Program {
250 items,
251 leading_comments: Vec::new(),
252 trailing_comments: Vec::new(),
253 },
254 imports_by_file: state.imports_by_file,
255 })
256}
257
258fn load_rooted(entry: &Path, prefix_root: Option<PathBuf>) -> Result<Program, LoadError> {
259 let entry_canonical = entry.canonicalize().map_err(|source| LoadError::Io {
260 path: entry.display().to_string(),
261 source,
262 })?;
263 let mut state = LoaderState {
264 in_progress: Vec::new(),
265 loaded: HashSet::new(),
266 prefixes: HashMap::new(),
267 prefix_root,
268 prefix_namespace: None,
269 imports_by_file: BTreeMap::new(),
270 };
271 // Entry file's prefix is empty so `lex run main.lex process` works
272 // without users typing the hashed prefix.
273 state.prefixes.insert(entry_canonical.clone(), String::new());
274 state.load(&entry_canonical)
275}
276
277/// Load a Lex program from a string source. Local-path imports are
278/// rejected up-front since there's no base path to resolve from.
279pub fn load_program_from_str(src: &str) -> Result<Program, LoadError> {
280 let prog = parse_source(src).map_err(|source| LoadError::Syntax {
281 path: "<input>".into(),
282 source,
283 })?;
284 for item in &prog.items {
285 if let Item::Import(imp) = item {
286 if is_path_import(&imp.reference)
287 || split_package_import(&imp.reference).is_some()
288 {
289 return Err(LoadError::LocalImportInStringSource);
290 }
291 }
292 }
293 Ok(prog)
294}
295
296struct LoaderState {
297 in_progress: Vec<PathBuf>,
298 /// Canonical paths that have already been merged into the output.
299 /// A second `import "./shared"` from a different parent skips
300 /// re-merging — the file's mangled items are already there.
301 loaded: HashSet<PathBuf>,
302 /// Stable mangling prefix per canonical path. Computed lazily;
303 /// the entry file is seeded with an empty prefix.
304 prefixes: HashMap<PathBuf, String>,
305 /// When set, mangling prefixes hash each file's path relative to
306 /// this (already canonicalized) directory rather than its absolute
307 /// path, so the same package layout mangles identically wherever it
308 /// is unpacked. See the module header's "Mangling" section.
309 prefix_root: Option<PathBuf>,
310 /// Mixed into every relative mangling key ahead of the path, so two
311 /// packages sharing an internal layout (two `src/error.lex` files)
312 /// do not mangle to one set of names. Only [`load_package`] sets it.
313 prefix_namespace: Option<String>,
314 /// Stdlib modules imported by each file itself, keyed by the file's
315 /// root-relative path. Recorded for every file the loader reads;
316 /// only [`load_package`] hands it back.
317 imports_by_file: BTreeMap<String, BTreeSet<String>>,
318}
319
320impl LoaderState {
321 fn prefix_for(&mut self, canonical: &Path) -> String {
322 if let Some(p) = self.prefixes.get(canonical) {
323 return p.clone();
324 }
325 let stem = canonical
326 .file_stem()
327 .and_then(|s| s.to_str())
328 .unwrap_or("module");
329 let mut hasher = Sha256::new();
330 hasher.update(self.mangling_key(canonical).as_bytes());
331 let digest = hasher.finalize();
332 let prefix = format!("{stem}_{:08x}", u32::from_be_bytes([
333 digest[0], digest[1], digest[2], digest[3],
334 ]));
335 self.prefixes.insert(canonical.to_path_buf(), prefix.clone());
336 prefix
337 }
338
339 /// The string a file's mangling hash is taken over: `prefix_namespace`
340 /// (when set) followed by the file's path relative to `prefix_root`,
341 /// else its canonical absolute path. Relative keys are joined with
342 /// `/` regardless of platform so the same layout hashes the same on
343 /// Windows and Unix.
344 fn mangling_key(&self, canonical: &Path) -> String {
345 match (self.relative_key(canonical), &self.prefix_namespace) {
346 (Some(rel), Some(ns)) => format!("{ns}/{rel}"),
347 (Some(rel), None) => rel,
348 (None, _) => canonical.to_string_lossy().into_owned(),
349 }
350 }
351
352 /// A file's path relative to `prefix_root`, `/`-joined — `None` when
353 /// there is no root or the file lives outside it. Also the key
354 /// `imports_by_file` is reported under, which is why it carries no
355 /// namespace: those keys name files in the archive, and history
356 /// already records them under exactly this spelling.
357 fn relative_key(&self, canonical: &Path) -> Option<String> {
358 let root = self.prefix_root.as_ref()?;
359 let rel = canonical.strip_prefix(root).ok()?;
360 let key = rel
361 .components()
362 .map(|c| c.as_os_str().to_string_lossy())
363 .collect::<Vec<_>>()
364 .join("/");
365 // An empty key means `canonical == root` (a root pointing at the
366 // file itself) — not a usable key, and it would collide with any
367 // other such file.
368 if key.is_empty() {
369 None
370 } else {
371 Some(key)
372 }
373 }
374
375 fn load(&mut self, canonical: &Path) -> Result<Program, LoadError> {
376 if self.in_progress.contains(&canonical.to_path_buf()) {
377 let mut chain: Vec<String> = self
378 .in_progress
379 .iter()
380 .map(|p| p.display().to_string())
381 .collect();
382 chain.push(canonical.display().to_string());
383 return Err(LoadError::Cycle {
384 chain: chain.join(" -> "),
385 });
386 }
387 // Diamond dedupe: if this file was already merged on another
388 // path through the import graph, its items are already in the
389 // output Vec — return an empty Program so the caller's
390 // `merged_children.extend(...)` is a no-op for items, but the
391 // call still resolves so the parent's `path_imports` map gets
392 // populated below.
393 if self.loaded.contains(canonical) {
394 return Ok(Program {
395 items: Vec::new(),
396 leading_comments: Vec::new(),
397 trailing_comments: Vec::new(),
398 });
399 }
400 self.in_progress.push(canonical.to_path_buf());
401
402 let src = std::fs::read_to_string(canonical).map_err(|source| LoadError::Io {
403 path: canonical.display().to_string(),
404 source,
405 })?;
406 let prog = parse_source(&src).map_err(|source| LoadError::Syntax {
407 path: canonical.display().to_string(),
408 source,
409 })?;
410
411 let local_names: HashSet<String> = prog
412 .items
413 .iter()
414 .filter_map(|item| match item {
415 Item::FnDecl(fd) => Some(fd.name.clone()),
416 Item::TypeDecl(td) => Some(td.name.clone()),
417 _ => None,
418 })
419 .collect();
420
421 // alias used by this file → mangling prefix of the imported file
422 let mut path_imports: HashMap<String, String> = HashMap::new();
423 let mut merged_children: Vec<Item> = Vec::new();
424 let mut std_imports: Vec<Item> = Vec::new();
425 let mut my_items: Vec<Item> = Vec::new();
426
427 for item in prog.items {
428 match item {
429 Item::Import(ref imp) if is_path_import(&imp.reference) => {
430 let resolved = resolve_import(canonical, &imp.reference)?;
431 let child_prefix = self.prefix_for(&resolved);
432 path_imports.insert(imp.alias.clone(), child_prefix);
433 let child_prog = self.load(&resolved)?;
434 merged_children.extend(child_prog.items);
435 }
436 Item::Import(ref imp)
437 if split_package_import(&imp.reference).is_some() =>
438 {
439 let (pkg, module) =
440 split_package_import(&imp.reference).unwrap();
441 let resolved =
442 resolve_package_import(canonical, pkg, module)
443 .map_err(LoadError::Package)?
444 .canonicalize()
445 .map_err(|source| LoadError::Io {
446 path: imp.reference.clone(),
447 source,
448 })?;
449 let child_prefix = self.prefix_for(&resolved);
450 path_imports.insert(imp.alias.clone(), child_prefix);
451 let child_prog = self.load(&resolved)?;
452 merged_children.extend(child_prog.items);
453 }
454 Item::Import(_) => std_imports.push(item),
455 _ => my_items.push(item),
456 }
457 }
458
459 // Attribute this file's own stdlib imports to this file, before
460 // the merge below makes them indistinguishable from its
461 // children's. Every file gets an entry, imports or not, so a
462 // file that has dropped its last import is still represented.
463 if let Some(key) = self.relative_key(canonical) {
464 let entry = self.imports_by_file.entry(key).or_default();
465 for item in &std_imports {
466 if let Item::Import(imp) = item {
467 entry.insert(imp.reference.clone());
468 }
469 }
470 }
471
472 let my_prefix = self.prefix_for(canonical);
473 let mangler = Mangler {
474 prefix: my_prefix,
475 local_names: &local_names,
476 path_imports: &path_imports,
477 };
478 let mangled: Vec<Item> = my_items
479 .into_iter()
480 .map(|i| mangler.mangle_item(i))
481 .collect();
482
483 self.in_progress.pop();
484 self.loaded.insert(canonical.to_path_buf());
485
486 // Output order: std imports first (deduped against children's),
487 // then merged children's items, then this file's items.
488 let mut out: Vec<Item> = Vec::new();
489 for s in std_imports {
490 if !merged_children.iter().any(|m| m == &s) {
491 out.push(s);
492 }
493 }
494 out.extend(merged_children);
495 out.extend(mangled);
496 // Top-of-file comments live on each source file independently;
497 // after import merging the merged Program represents many
498 // files at once, and there is no obvious single "top of file"
499 // to attribute them to. Drop here — they're preserved by
500 // `lex fmt` (which operates per-file) but not by the loader's
501 // import-merging path. Same rationale for trailing_comments.
502 Ok(Program {
503 items: out,
504 leading_comments: Vec::new(),
505 trailing_comments: Vec::new(),
506 })
507 }
508}
509
510fn is_path_import(reference: &str) -> bool {
511 reference.starts_with("./") || reference.starts_with("../") || reference.starts_with('/')
512}
513
514/// Returns `Some((pkg_name, module_path))` for package imports like
515/// `"lex-schema/validate"`. Stdlib (`std.*`) and relative paths are
516/// excluded — they are handled elsewhere.
517fn split_package_import(reference: &str) -> Option<(&str, &str)> {
518 if reference.starts_with("./")
519 || reference.starts_with("../")
520 || reference.starts_with('/')
521 || reference.starts_with("std.")
522 {
523 return None;
524 }
525 reference.split_once('/')
526}
527
528fn resolve_import(importer: &Path, reference: &str) -> Result<PathBuf, LoadError> {
529 let importer_dir = importer.parent().unwrap_or_else(|| Path::new("."));
530 let mut resolved: PathBuf = if reference.starts_with('/') {
531 PathBuf::from(reference)
532 } else {
533 importer_dir.join(reference)
534 };
535 if resolved.extension().is_none() {
536 resolved.set_extension("lex");
537 }
538 if !resolved.exists() {
539 return Err(LoadError::NotFound {
540 importer: importer.display().to_string(),
541 reference: reference.to_string(),
542 });
543 }
544 // Canonicalize so that `../../shared/foo` and `../other/../shared/foo`
545 // resolve to the same HashMap key, preventing duplicate loads and
546 // mismatched mangling prefixes in diamond-import graphs (#358).
547 resolved.canonicalize().map_err(|source| LoadError::Io {
548 path: resolved.display().to_string(),
549 source,
550 })
551}
552
553struct Mangler<'a> {
554 /// Mangling prefix for items declared in this file. Empty for the
555 /// entry file, `<stem>_<hash8>` for imported files.
556 prefix: String,
557 local_names: &'a HashSet<String>,
558 /// Map from local alias to the imported file's mangling prefix.
559 /// `m.foo` rewrites to `<imported_prefix>.foo` regardless of which
560 /// alias `m` was, so two parents importing the same module agree.
561 path_imports: &'a HashMap<String, String>,
562}
563
564impl<'a> Mangler<'a> {
565 fn qualify(&self, name: &str) -> String {
566 if self.prefix.is_empty() {
567 name.to_string()
568 } else {
569 format!("{}.{}", self.prefix, name)
570 }
571 }
572
573 fn mangle_item(&self, item: Item) -> Item {
574 match item {
575 Item::Import(imp) => Item::Import(imp),
576 Item::TypeDecl(td) => Item::TypeDecl(self.mangle_type_decl(td)),
577 Item::FnDecl(fd) => Item::FnDecl(self.mangle_fn_decl(fd)),
578 }
579 }
580
581 fn mangle_type_decl(&self, td: TypeDecl) -> TypeDecl {
582 TypeDecl {
583 name: self.qualify(&td.name),
584 params: td.params,
585 definition: self.mangle_type_expr(td.definition),
586 leading_comments: td.leading_comments,
587 }
588 }
589
590 fn mangle_fn_decl(&self, fd: FnDecl) -> FnDecl {
591 let mut shadow = HashSet::new();
592 for p in &fd.params {
593 shadow.insert(p.name.clone());
594 }
595 // Example args/expected sit outside the body's parameter scope:
596 // they're top-level expressions evaluated against the function
597 // signature, so the only names they can see are the file's
598 // top-level fns/types and any path-import aliases — i.e., an
599 // empty shadow set (#391).
600 let empty_shadow = HashSet::new();
601 let examples = fd
602 .examples
603 .into_iter()
604 .map(|ex| Example {
605 args: ex
606 .args
607 .into_iter()
608 .map(|a| self.mangle_expr(a, &empty_shadow))
609 .collect(),
610 expected: self.mangle_expr(ex.expected, &empty_shadow),
611 })
612 .collect();
613 FnDecl {
614 name: self.qualify(&fd.name),
615 type_params: fd.type_params,
616 params: fd
617 .params
618 .into_iter()
619 .map(|p| Param {
620 name: p.name,
621 ty: self.mangle_type_expr(p.ty),
622 })
623 .collect(),
624 effects: fd.effects,
625 effect_row_var: fd.effect_row_var,
626 return_type: self.mangle_type_expr(fd.return_type),
627 body: self.mangle_block(fd.body, &shadow),
628 examples,
629 leading_comments: fd.leading_comments,
630 }
631 }
632
633 fn mangle_type_expr(&self, te: TypeExpr) -> TypeExpr {
634 match te {
635 TypeExpr::Named { name, args } => TypeExpr::Named {
636 name: self.rewrite_type_name(&name),
637 args: args.into_iter().map(|a| self.mangle_type_expr(a)).collect(),
638 },
639 TypeExpr::Record(fields) => TypeExpr::Record(
640 fields
641 .into_iter()
642 .map(|f| TypeField {
643 name: f.name,
644 ty: self.mangle_type_expr(f.ty),
645 })
646 .collect(),
647 ),
648 TypeExpr::RecordWithSpreads { spreads, fields } => TypeExpr::RecordWithSpreads {
649 spreads: spreads.into_iter().map(|s| self.rewrite_type_name(&s)).collect(),
650 fields: fields
651 .into_iter()
652 .map(|f| TypeField {
653 name: f.name,
654 ty: self.mangle_type_expr(f.ty),
655 })
656 .collect(),
657 },
658 TypeExpr::Tuple(items) => {
659 TypeExpr::Tuple(items.into_iter().map(|t| self.mangle_type_expr(t)).collect())
660 }
661 TypeExpr::Function {
662 params,
663 effects,
664 effect_row_var,
665 ret,
666 } => TypeExpr::Function {
667 params: params
668 .into_iter()
669 .map(|t| self.mangle_type_expr(t))
670 .collect(),
671 effects,
672 effect_row_var,
673 ret: Box::new(self.mangle_type_expr(*ret)),
674 },
675 TypeExpr::Union(variants) => TypeExpr::Union(
676 variants
677 .into_iter()
678 .map(|v| UnionVariant {
679 name: v.name,
680 payload: v.payload.map(|t| self.mangle_type_expr(t)),
681 })
682 .collect(),
683 ),
684 TypeExpr::Refined { base, binding, predicate } => TypeExpr::Refined {
685 base: Box::new(self.mangle_type_expr(*base)),
686 binding,
687 // The predicate is an expression; its names are
688 // resolved during type-check, not loader-time, so
689 // it passes through unchanged here. Slice 2 wires
690 // up discharge through the spec-checker.
691 predicate,
692 },
693 }
694 }
695
696 /// Rewrite a possibly-qualified type name to its mangled form.
697 fn rewrite_type_name(&self, name: &str) -> String {
698 if let Some((alias, rest)) = name.split_once('.') {
699 if let Some(child) = self.path_imports.get(alias) {
700 return format!("{child}.{rest}");
701 }
702 return name.to_string();
703 }
704 if self.local_names.contains(name) {
705 return self.qualify(name);
706 }
707 name.to_string()
708 }
709
710 fn mangle_block(&self, b: Block, shadow: &HashSet<String>) -> Block {
711 let mut shadow = shadow.clone();
712 let statements = b
713 .statements
714 .into_iter()
715 .map(|s| match s {
716 Statement::Let { name, ty, value } => {
717 let value = self.mangle_expr(value, &shadow);
718 let ty = ty.map(|t| self.mangle_type_expr(t));
719 shadow.insert(name.clone());
720 Statement::Let { name, ty, value }
721 }
722 Statement::Expr(e) => Statement::Expr(self.mangle_expr(e, &shadow)),
723 })
724 .collect();
725 let result = Box::new(self.mangle_expr(*b.result, &shadow));
726 Block { statements, result }
727 }
728
729 fn mangle_expr(&self, e: Expr, shadow: &HashSet<String>) -> Expr {
730 match e {
731 Expr::Lit(_) => e,
732 Expr::Var(name) => {
733 if !shadow.contains(&name) && self.local_names.contains(&name) {
734 Expr::Var(self.qualify(&name))
735 } else {
736 Expr::Var(name)
737 }
738 }
739 Expr::Block(b) => Expr::Block(self.mangle_block(b, shadow)),
740 Expr::Call { callee, args } => {
741 let mangled_args: Vec<Expr> = args
742 .into_iter()
743 .map(|a| self.mangle_expr(a, shadow))
744 .collect();
745 if let Expr::Field { value, field } = (*callee).clone() {
746 if let Expr::Var(alias) = *value {
747 if !shadow.contains(&alias) {
748 if let Some(child) = self.path_imports.get(&alias) {
749 return Expr::Call {
750 callee: Box::new(Expr::Var(format!("{child}.{field}"))),
751 args: mangled_args,
752 };
753 }
754 }
755 }
756 }
757 Expr::Call {
758 callee: Box::new(self.mangle_expr(*callee, shadow)),
759 args: mangled_args,
760 }
761 }
762 Expr::Pipe { left, right } => Expr::Pipe {
763 left: Box::new(self.mangle_expr(*left, shadow)),
764 right: Box::new(self.mangle_expr(*right, shadow)),
765 },
766 Expr::Try(inner) => Expr::Try(Box::new(self.mangle_expr(*inner, shadow))),
767 Expr::Field { value, field } => {
768 if let Expr::Var(alias) = (*value).clone() {
769 if !shadow.contains(&alias) {
770 if let Some(child) = self.path_imports.get(&alias) {
771 return Expr::Var(format!("{child}.{field}"));
772 }
773 }
774 }
775 Expr::Field {
776 value: Box::new(self.mangle_expr(*value, shadow)),
777 field,
778 }
779 }
780 Expr::BinOp { op, lhs, rhs } => Expr::BinOp {
781 op,
782 lhs: Box::new(self.mangle_expr(*lhs, shadow)),
783 rhs: Box::new(self.mangle_expr(*rhs, shadow)),
784 },
785 Expr::UnaryOp { op, expr } => Expr::UnaryOp {
786 op,
787 expr: Box::new(self.mangle_expr(*expr, shadow)),
788 },
789 Expr::If {
790 cond,
791 then_block,
792 else_block,
793 } => Expr::If {
794 cond: Box::new(self.mangle_expr(*cond, shadow)),
795 then_block: self.mangle_block(then_block, shadow),
796 else_block: self.mangle_block(else_block, shadow),
797 },
798 Expr::Match { scrutinee, arms } => Expr::Match {
799 scrutinee: Box::new(self.mangle_expr(*scrutinee, shadow)),
800 arms: arms
801 .into_iter()
802 .map(|a| {
803 let mut arm_shadow = shadow.clone();
804 collect_pattern_binders(&a.pattern, &mut arm_shadow);
805 Arm {
806 pattern: self.mangle_pattern(a.pattern),
807 body: self.mangle_expr(a.body, &arm_shadow),
808 }
809 })
810 .collect(),
811 },
812 Expr::RecordLit(fields) => Expr::RecordLit(
813 fields
814 .into_iter()
815 .map(|f| RecordLitField {
816 name: f.name,
817 value: self.mangle_expr(f.value, shadow),
818 })
819 .collect(),
820 ),
821 Expr::TupleLit(items) => Expr::TupleLit(
822 items
823 .into_iter()
824 .map(|i| self.mangle_expr(i, shadow))
825 .collect(),
826 ),
827 Expr::ListLit(items) => Expr::ListLit(
828 items
829 .into_iter()
830 .map(|i| self.mangle_expr(i, shadow))
831 .collect(),
832 ),
833 Expr::Constructor { name, args } => Expr::Constructor {
834 name,
835 args: args
836 .into_iter()
837 .map(|a| self.mangle_expr(a, shadow))
838 .collect(),
839 },
840 Expr::Ascription { value, ty } => Expr::Ascription {
841 value: Box::new(self.mangle_expr(*value, shadow)),
842 ty: self.mangle_type_expr(ty),
843 },
844 Expr::Lambda(lambda) => {
845 let mut lam_shadow = shadow.clone();
846 for p in &lambda.params {
847 lam_shadow.insert(p.name.clone());
848 }
849 Expr::Lambda(Box::new(Lambda {
850 params: lambda
851 .params
852 .into_iter()
853 .map(|p| Param {
854 name: p.name,
855 ty: self.mangle_type_expr(p.ty),
856 })
857 .collect(),
858 return_type: self.mangle_type_expr(lambda.return_type),
859 effects: lambda.effects,
860 effect_row_var: lambda.effect_row_var,
861 body: self.mangle_block(lambda.body, &lam_shadow),
862 }))
863 }
864 }
865 }
866
867 fn mangle_pattern(&self, p: Pattern) -> Pattern {
868 match p {
869 Pattern::Constructor { name, args } => Pattern::Constructor {
870 name,
871 args: args.into_iter().map(|a| self.mangle_pattern(a)).collect(),
872 },
873 Pattern::Record { fields, rest } => Pattern::Record {
874 fields: fields
875 .into_iter()
876 .map(|f| RecordPatField {
877 name: f.name,
878 pattern: f.pattern.map(|p| self.mangle_pattern(p)),
879 })
880 .collect(),
881 rest,
882 },
883 Pattern::Tuple(items) => {
884 Pattern::Tuple(items.into_iter().map(|p| self.mangle_pattern(p)).collect())
885 }
886 Pattern::Lit(_) | Pattern::Var(_) | Pattern::Wild => p,
887 }
888 }
889}
890
891fn collect_pattern_binders(p: &Pattern, out: &mut HashSet<String>) {
892 match p {
893 Pattern::Var(name) => {
894 out.insert(name.clone());
895 }
896 Pattern::Constructor { args, .. } => {
897 for a in args {
898 collect_pattern_binders(a, out);
899 }
900 }
901 Pattern::Record { fields, .. } => {
902 for f in fields {
903 match &f.pattern {
904 Some(p) => collect_pattern_binders(p, out),
905 // `{ name }` shorthand binds `name`.
906 None => {
907 out.insert(f.name.clone());
908 }
909 }
910 }
911 }
912 Pattern::Tuple(items) => {
913 for p in items {
914 collect_pattern_binders(p, out);
915 }
916 }
917 Pattern::Lit(_) | Pattern::Wild => {}
918 }
919}