Skip to main content

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    /// Mangling prefix → the file it belongs to (`schema_a1b2` →
171    /// `src/schema.lex`), for every file in the package. A declaration's
172    /// mangled name is `<prefix>.<local>`, so this is what lets a
173    /// consumer attribute each declaration in `program` back to its
174    /// source file — the record `export-git` needs to de-flatten the
175    /// package into its `src/*.lex` tree (#894).
176    pub module_prefixes: BTreeMap<String, String>,
177}
178
179/// Load a whole package as **one** program: every file gets its
180/// path-derived mangling prefix (no file is the unmangled "entry"), and
181/// each file's declarations appear exactly once however many other files
182/// import it.
183///
184/// [`load_program`] and [`load_program_with_root`] flatten each entry's
185/// whole local-import closure into that entry's program, so a caller
186/// holding N top-level files gets every shared dependency back N times —
187/// once per importer. The real 21-file `lex-schema` package, whose
188/// `error.lex` is imported by 17 of its files, yielded 2,239 `FnDecl`s
189/// for 693 distinct names that way, and a server that canonicalizes,
190/// type-checks, diffs and publishes each copy paid for all 2,239 (#828).
191/// One shared pass yields 447 — one per declaration.
192///
193/// Because no file is the entry, **no declaration keeps its bare
194/// source-level name**: `fn validate` in `src/field.lex` is
195/// `field_<hash>.validate`, not `validate`. That is what makes one
196/// program safe to type-check as a unit — two files may each declare
197/// their own local `validate`, and the checker's global scope is a map
198/// keyed by name, so bare names from different files would silently
199/// overwrite each other and check bodies against the wrong signature.
200///
201/// `namespace` is mixed into every mangling key ahead of the relative
202/// path, so the same internal layout in two different packages does not
203/// collapse onto one set of names. Callers publishing into a shared
204/// branch should pass the package name: a tenant hosting both
205/// `lex-schema` and `lex-ocpi` has two `src/error.lex` files, and a
206/// purely path-derived key gives both the same `error_<hash>.format`.
207///
208/// Stdlib imports are deduped by `(reference, alias)`. An alias bound to
209/// two *different* references inside one package is rejected with
210/// [`LoadError::ConflictingAlias`] rather than merged: the checker's
211/// alias scope is also name-keyed, so merging would silently resolve one
212/// file's calls against the other file's module.
213pub fn load_package(
214    entries: &[PathBuf],
215    root: &Path,
216    namespace: &str,
217) -> Result<LoadedPackage, LoadError> {
218    let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
219    let mut state = LoaderState {
220        in_progress: Vec::new(),
221        loaded: HashSet::new(),
222        prefixes: HashMap::new(),
223        prefix_root: Some(root),
224        prefix_namespace: Some(namespace.to_string()),
225        imports_by_file: BTreeMap::new(),
226    };
227    // Deliberately no empty-prefix seeding: see the doc comment above.
228    let mut items: Vec<Item> = Vec::new();
229    let mut aliases: HashMap<String, String> = HashMap::new();
230    for entry in entries {
231        let canonical = entry.canonicalize().map_err(|source| LoadError::Io {
232            path: entry.display().to_string(),
233            source,
234        })?;
235        for item in state.load(&canonical)?.items {
236            if let Item::Import(imp) = &item {
237                match aliases.get(&imp.alias) {
238                    // Same module under the same alias: one import is enough.
239                    Some(existing) if existing == &imp.reference => continue,
240                    Some(existing) => {
241                        return Err(LoadError::ConflictingAlias {
242                            alias: imp.alias.clone(),
243                            first: existing.clone(),
244                            second: imp.reference.clone(),
245                        })
246                    }
247                    None => {
248                        aliases.insert(imp.alias.clone(), imp.reference.clone());
249                    }
250                }
251            }
252            items.push(item);
253        }
254    }
255    // prefix → relative file path, for every mangled file (the entry
256    // has no empty prefix under `load_package`, so all are included).
257    let module_prefixes: BTreeMap<String, String> = state
258        .prefixes
259        .iter()
260        .filter(|(_, prefix)| !prefix.is_empty())
261        .filter_map(|(path, prefix)| state.relative_key(path).map(|rel| (prefix.clone(), rel)))
262        .collect();
263    Ok(LoadedPackage {
264        program: Program {
265            items,
266            leading_comments: Vec::new(),
267            trailing_comments: Vec::new(),
268        },
269        imports_by_file: state.imports_by_file,
270        module_prefixes,
271    })
272}
273
274fn load_rooted(entry: &Path, prefix_root: Option<PathBuf>) -> Result<Program, LoadError> {
275    let entry_canonical = entry.canonicalize().map_err(|source| LoadError::Io {
276        path: entry.display().to_string(),
277        source,
278    })?;
279    let mut state = LoaderState {
280        in_progress: Vec::new(),
281        loaded: HashSet::new(),
282        prefixes: HashMap::new(),
283        prefix_root,
284        prefix_namespace: None,
285        imports_by_file: BTreeMap::new(),
286    };
287    // Entry file's prefix is empty so `lex run main.lex process` works
288    // without users typing the hashed prefix.
289    state.prefixes.insert(entry_canonical.clone(), String::new());
290    state.load(&entry_canonical)
291}
292
293/// Load a Lex program from a string source. Local-path imports are
294/// rejected up-front since there's no base path to resolve from.
295pub fn load_program_from_str(src: &str) -> Result<Program, LoadError> {
296    let prog = parse_source(src).map_err(|source| LoadError::Syntax {
297        path: "<input>".into(),
298        source,
299    })?;
300    for item in &prog.items {
301        if let Item::Import(imp) = item {
302            if is_path_import(&imp.reference)
303                || split_package_import(&imp.reference).is_some()
304            {
305                return Err(LoadError::LocalImportInStringSource);
306            }
307        }
308    }
309    Ok(prog)
310}
311
312struct LoaderState {
313    in_progress: Vec<PathBuf>,
314    /// Canonical paths that have already been merged into the output.
315    /// A second `import "./shared"` from a different parent skips
316    /// re-merging — the file's mangled items are already there.
317    loaded: HashSet<PathBuf>,
318    /// Stable mangling prefix per canonical path. Computed lazily;
319    /// the entry file is seeded with an empty prefix.
320    prefixes: HashMap<PathBuf, String>,
321    /// When set, mangling prefixes hash each file's path relative to
322    /// this (already canonicalized) directory rather than its absolute
323    /// path, so the same package layout mangles identically wherever it
324    /// is unpacked. See the module header's "Mangling" section.
325    prefix_root: Option<PathBuf>,
326    /// Mixed into every relative mangling key ahead of the path, so two
327    /// packages sharing an internal layout (two `src/error.lex` files)
328    /// do not mangle to one set of names. Only [`load_package`] sets it.
329    prefix_namespace: Option<String>,
330    /// Stdlib modules imported by each file itself, keyed by the file's
331    /// root-relative path. Recorded for every file the loader reads;
332    /// only [`load_package`] hands it back.
333    imports_by_file: BTreeMap<String, BTreeSet<String>>,
334}
335
336impl LoaderState {
337    fn prefix_for(&mut self, canonical: &Path) -> String {
338        if let Some(p) = self.prefixes.get(canonical) {
339            return p.clone();
340        }
341        let stem = canonical
342            .file_stem()
343            .and_then(|s| s.to_str())
344            .unwrap_or("module");
345        let mut hasher = Sha256::new();
346        hasher.update(self.mangling_key(canonical).as_bytes());
347        let digest = hasher.finalize();
348        let prefix = format!("{stem}_{:08x}", u32::from_be_bytes([
349            digest[0], digest[1], digest[2], digest[3],
350        ]));
351        self.prefixes.insert(canonical.to_path_buf(), prefix.clone());
352        prefix
353    }
354
355    /// The string a file's mangling hash is taken over: `prefix_namespace`
356    /// (when set) followed by the file's path relative to `prefix_root`,
357    /// else its canonical absolute path. Relative keys are joined with
358    /// `/` regardless of platform so the same layout hashes the same on
359    /// Windows and Unix.
360    fn mangling_key(&self, canonical: &Path) -> String {
361        match (self.relative_key(canonical), &self.prefix_namespace) {
362            (Some(rel), Some(ns)) => format!("{ns}/{rel}"),
363            (Some(rel), None) => rel,
364            (None, _) => canonical.to_string_lossy().into_owned(),
365        }
366    }
367
368    /// A file's path relative to `prefix_root`, `/`-joined — `None` when
369    /// there is no root or the file lives outside it. Also the key
370    /// `imports_by_file` is reported under, which is why it carries no
371    /// namespace: those keys name files in the archive, and history
372    /// already records them under exactly this spelling.
373    fn relative_key(&self, canonical: &Path) -> Option<String> {
374        let root = self.prefix_root.as_ref()?;
375        let rel = canonical.strip_prefix(root).ok()?;
376        let key = rel
377            .components()
378            .map(|c| c.as_os_str().to_string_lossy())
379            .collect::<Vec<_>>()
380            .join("/");
381        // An empty key means `canonical == root` (a root pointing at the
382        // file itself) — not a usable key, and it would collide with any
383        // other such file.
384        if key.is_empty() {
385            None
386        } else {
387            Some(key)
388        }
389    }
390
391    fn load(&mut self, canonical: &Path) -> Result<Program, LoadError> {
392        if self.in_progress.contains(&canonical.to_path_buf()) {
393            let mut chain: Vec<String> = self
394                .in_progress
395                .iter()
396                .map(|p| p.display().to_string())
397                .collect();
398            chain.push(canonical.display().to_string());
399            return Err(LoadError::Cycle {
400                chain: chain.join(" -> "),
401            });
402        }
403        // Diamond dedupe: if this file was already merged on another
404        // path through the import graph, its items are already in the
405        // output Vec — return an empty Program so the caller's
406        // `merged_children.extend(...)` is a no-op for items, but the
407        // call still resolves so the parent's `path_imports` map gets
408        // populated below.
409        if self.loaded.contains(canonical) {
410            return Ok(Program {
411                items: Vec::new(),
412                leading_comments: Vec::new(),
413                trailing_comments: Vec::new(),
414            });
415        }
416        self.in_progress.push(canonical.to_path_buf());
417
418        let src = std::fs::read_to_string(canonical).map_err(|source| LoadError::Io {
419            path: canonical.display().to_string(),
420            source,
421        })?;
422        let prog = parse_source(&src).map_err(|source| LoadError::Syntax {
423            path: canonical.display().to_string(),
424            source,
425        })?;
426
427        let local_names: HashSet<String> = prog
428            .items
429            .iter()
430            .filter_map(|item| match item {
431                Item::FnDecl(fd) => Some(fd.name.clone()),
432                Item::TypeDecl(td) => Some(td.name.clone()),
433                _ => None,
434            })
435            .collect();
436
437        // alias used by this file → mangling prefix of the imported file
438        let mut path_imports: HashMap<String, String> = HashMap::new();
439        let mut merged_children: Vec<Item> = Vec::new();
440        let mut std_imports: Vec<Item> = Vec::new();
441        let mut my_items: Vec<Item> = Vec::new();
442
443        for item in prog.items {
444            match item {
445                Item::Import(ref imp) if is_path_import(&imp.reference) => {
446                    let resolved = resolve_import(canonical, &imp.reference)?;
447                    let child_prefix = self.prefix_for(&resolved);
448                    path_imports.insert(imp.alias.clone(), child_prefix);
449                    let child_prog = self.load(&resolved)?;
450                    merged_children.extend(child_prog.items);
451                }
452                Item::Import(ref imp)
453                    if split_package_import(&imp.reference).is_some() =>
454                {
455                    let (pkg, module) =
456                        split_package_import(&imp.reference).unwrap();
457                    let resolved =
458                        resolve_package_import(canonical, pkg, module)
459                            .map_err(LoadError::Package)?
460                            .canonicalize()
461                            .map_err(|source| LoadError::Io {
462                                path: imp.reference.clone(),
463                                source,
464                            })?;
465                    let child_prefix = self.prefix_for(&resolved);
466                    path_imports.insert(imp.alias.clone(), child_prefix);
467                    let child_prog = self.load(&resolved)?;
468                    merged_children.extend(child_prog.items);
469                }
470                Item::Import(_) => std_imports.push(item),
471                _ => my_items.push(item),
472            }
473        }
474
475        // Attribute this file's own stdlib imports to this file, before
476        // the merge below makes them indistinguishable from its
477        // children's. Every file gets an entry, imports or not, so a
478        // file that has dropped its last import is still represented.
479        if let Some(key) = self.relative_key(canonical) {
480            let entry = self.imports_by_file.entry(key).or_default();
481            for item in &std_imports {
482                if let Item::Import(imp) = item {
483                    entry.insert(imp.reference.clone());
484                }
485            }
486        }
487
488        let my_prefix = self.prefix_for(canonical);
489        let mangler = Mangler {
490            prefix: my_prefix,
491            local_names: &local_names,
492            path_imports: &path_imports,
493        };
494        let mangled: Vec<Item> = my_items
495            .into_iter()
496            .map(|i| mangler.mangle_item(i))
497            .collect();
498
499        self.in_progress.pop();
500        self.loaded.insert(canonical.to_path_buf());
501
502        // Output order: std imports first (deduped against children's),
503        // then merged children's items, then this file's items.
504        let mut out: Vec<Item> = Vec::new();
505        for s in std_imports {
506            if !merged_children.iter().any(|m| m == &s) {
507                out.push(s);
508            }
509        }
510        out.extend(merged_children);
511        out.extend(mangled);
512        // Top-of-file comments live on each source file independently;
513        // after import merging the merged Program represents many
514        // files at once, and there is no obvious single "top of file"
515        // to attribute them to. Drop here — they're preserved by
516        // `lex fmt` (which operates per-file) but not by the loader's
517        // import-merging path. Same rationale for trailing_comments.
518        Ok(Program {
519            items: out,
520            leading_comments: Vec::new(),
521            trailing_comments: Vec::new(),
522        })
523    }
524}
525
526fn is_path_import(reference: &str) -> bool {
527    reference.starts_with("./") || reference.starts_with("../") || reference.starts_with('/')
528}
529
530/// Returns `Some((pkg_name, module_path))` for package imports like
531/// `"lex-schema/validate"`. Stdlib (`std.*`) and relative paths are
532/// excluded — they are handled elsewhere.
533fn split_package_import(reference: &str) -> Option<(&str, &str)> {
534    if reference.starts_with("./")
535        || reference.starts_with("../")
536        || reference.starts_with('/')
537        || reference.starts_with("std.")
538    {
539        return None;
540    }
541    reference.split_once('/')
542}
543
544fn resolve_import(importer: &Path, reference: &str) -> Result<PathBuf, LoadError> {
545    let importer_dir = importer.parent().unwrap_or_else(|| Path::new("."));
546    let mut resolved: PathBuf = if reference.starts_with('/') {
547        PathBuf::from(reference)
548    } else {
549        importer_dir.join(reference)
550    };
551    if resolved.extension().is_none() {
552        resolved.set_extension("lex");
553    }
554    if !resolved.exists() {
555        return Err(LoadError::NotFound {
556            importer: importer.display().to_string(),
557            reference: reference.to_string(),
558        });
559    }
560    // Canonicalize so that `../../shared/foo` and `../other/../shared/foo`
561    // resolve to the same HashMap key, preventing duplicate loads and
562    // mismatched mangling prefixes in diamond-import graphs (#358).
563    resolved.canonicalize().map_err(|source| LoadError::Io {
564        path: resolved.display().to_string(),
565        source,
566    })
567}
568
569struct Mangler<'a> {
570    /// Mangling prefix for items declared in this file. Empty for the
571    /// entry file, `<stem>_<hash8>` for imported files.
572    prefix: String,
573    local_names: &'a HashSet<String>,
574    /// Map from local alias to the imported file's mangling prefix.
575    /// `m.foo` rewrites to `<imported_prefix>.foo` regardless of which
576    /// alias `m` was, so two parents importing the same module agree.
577    path_imports: &'a HashMap<String, String>,
578}
579
580impl<'a> Mangler<'a> {
581    fn qualify(&self, name: &str) -> String {
582        if self.prefix.is_empty() {
583            name.to_string()
584        } else {
585            format!("{}.{}", self.prefix, name)
586        }
587    }
588
589    fn mangle_item(&self, item: Item) -> Item {
590        match item {
591            Item::Import(imp) => Item::Import(imp),
592            Item::TypeDecl(td) => Item::TypeDecl(self.mangle_type_decl(td)),
593            Item::FnDecl(fd) => Item::FnDecl(self.mangle_fn_decl(fd)),
594        }
595    }
596
597    fn mangle_type_decl(&self, td: TypeDecl) -> TypeDecl {
598        TypeDecl {
599            name: self.qualify(&td.name),
600            params: td.params,
601            definition: self.mangle_type_expr(td.definition),
602            leading_comments: td.leading_comments,
603        }
604    }
605
606    fn mangle_fn_decl(&self, fd: FnDecl) -> FnDecl {
607        let mut shadow = HashSet::new();
608        for p in &fd.params {
609            shadow.insert(p.name.clone());
610        }
611        // Example args/expected sit outside the body's parameter scope:
612        // they're top-level expressions evaluated against the function
613        // signature, so the only names they can see are the file's
614        // top-level fns/types and any path-import aliases — i.e., an
615        // empty shadow set (#391).
616        let empty_shadow = HashSet::new();
617        let examples = fd
618            .examples
619            .into_iter()
620            .map(|ex| Example {
621                args: ex
622                    .args
623                    .into_iter()
624                    .map(|a| self.mangle_expr(a, &empty_shadow))
625                    .collect(),
626                expected: self.mangle_expr(ex.expected, &empty_shadow),
627            })
628            .collect();
629        FnDecl {
630            name: self.qualify(&fd.name),
631            type_params: fd.type_params,
632            params: fd
633                .params
634                .into_iter()
635                .map(|p| Param {
636                    name: p.name,
637                    ty: self.mangle_type_expr(p.ty),
638                })
639                .collect(),
640            effects: fd.effects,
641            effect_row_var: fd.effect_row_var,
642            return_type: self.mangle_type_expr(fd.return_type),
643            body: self.mangle_block(fd.body, &shadow),
644            examples,
645            leading_comments: fd.leading_comments,
646        }
647    }
648
649    fn mangle_type_expr(&self, te: TypeExpr) -> TypeExpr {
650        match te {
651            TypeExpr::Named { name, args } => TypeExpr::Named {
652                name: self.rewrite_type_name(&name),
653                args: args.into_iter().map(|a| self.mangle_type_expr(a)).collect(),
654            },
655            TypeExpr::Record(fields) => TypeExpr::Record(
656                fields
657                    .into_iter()
658                    .map(|f| TypeField {
659                        name: f.name,
660                        ty: self.mangle_type_expr(f.ty),
661                    })
662                    .collect(),
663            ),
664            TypeExpr::RecordWithSpreads { spreads, fields } => TypeExpr::RecordWithSpreads {
665                spreads: spreads.into_iter().map(|s| self.rewrite_type_name(&s)).collect(),
666                fields: fields
667                    .into_iter()
668                    .map(|f| TypeField {
669                        name: f.name,
670                        ty: self.mangle_type_expr(f.ty),
671                    })
672                    .collect(),
673            },
674            TypeExpr::Tuple(items) => {
675                TypeExpr::Tuple(items.into_iter().map(|t| self.mangle_type_expr(t)).collect())
676            }
677            TypeExpr::Function {
678                params,
679                effects,
680                effect_row_var,
681                ret,
682            } => TypeExpr::Function {
683                params: params
684                    .into_iter()
685                    .map(|t| self.mangle_type_expr(t))
686                    .collect(),
687                effects,
688                effect_row_var,
689                ret: Box::new(self.mangle_type_expr(*ret)),
690            },
691            TypeExpr::Union(variants) => TypeExpr::Union(
692                variants
693                    .into_iter()
694                    .map(|v| UnionVariant {
695                        name: v.name,
696                        payload: v.payload.map(|t| self.mangle_type_expr(t)),
697                    })
698                    .collect(),
699            ),
700            TypeExpr::Refined { base, binding, predicate } => TypeExpr::Refined {
701                base: Box::new(self.mangle_type_expr(*base)),
702                binding,
703                // The predicate is an expression; its names are
704                // resolved during type-check, not loader-time, so
705                // it passes through unchanged here. Slice 2 wires
706                // up discharge through the spec-checker.
707                predicate,
708            },
709        }
710    }
711
712    /// Rewrite a possibly-qualified type name to its mangled form.
713    fn rewrite_type_name(&self, name: &str) -> String {
714        if let Some((alias, rest)) = name.split_once('.') {
715            if let Some(child) = self.path_imports.get(alias) {
716                return format!("{child}.{rest}");
717            }
718            return name.to_string();
719        }
720        if self.local_names.contains(name) {
721            return self.qualify(name);
722        }
723        name.to_string()
724    }
725
726    fn mangle_block(&self, b: Block, shadow: &HashSet<String>) -> Block {
727        let mut shadow = shadow.clone();
728        let statements = b
729            .statements
730            .into_iter()
731            .map(|s| match s {
732                Statement::Let { name, ty, value } => {
733                    let value = self.mangle_expr(value, &shadow);
734                    let ty = ty.map(|t| self.mangle_type_expr(t));
735                    shadow.insert(name.clone());
736                    Statement::Let { name, ty, value }
737                }
738                Statement::Expr(e) => Statement::Expr(self.mangle_expr(e, &shadow)),
739            })
740            .collect();
741        let result = Box::new(self.mangle_expr(*b.result, &shadow));
742        Block { statements, result }
743    }
744
745    fn mangle_expr(&self, e: Expr, shadow: &HashSet<String>) -> Expr {
746        match e {
747            Expr::Lit(_) => e,
748            Expr::Var(name) => {
749                if !shadow.contains(&name) && self.local_names.contains(&name) {
750                    Expr::Var(self.qualify(&name))
751                } else {
752                    Expr::Var(name)
753                }
754            }
755            Expr::Block(b) => Expr::Block(self.mangle_block(b, shadow)),
756            Expr::Call { callee, args } => {
757                let mangled_args: Vec<Expr> = args
758                    .into_iter()
759                    .map(|a| self.mangle_expr(a, shadow))
760                    .collect();
761                if let Expr::Field { value, field } = (*callee).clone() {
762                    if let Expr::Var(alias) = *value {
763                        if !shadow.contains(&alias) {
764                            if let Some(child) = self.path_imports.get(&alias) {
765                                return Expr::Call {
766                                    callee: Box::new(Expr::Var(format!("{child}.{field}"))),
767                                    args: mangled_args,
768                                };
769                            }
770                        }
771                    }
772                }
773                Expr::Call {
774                    callee: Box::new(self.mangle_expr(*callee, shadow)),
775                    args: mangled_args,
776                }
777            }
778            Expr::Pipe { left, right } => Expr::Pipe {
779                left: Box::new(self.mangle_expr(*left, shadow)),
780                right: Box::new(self.mangle_expr(*right, shadow)),
781            },
782            Expr::Try(inner) => Expr::Try(Box::new(self.mangle_expr(*inner, shadow))),
783            Expr::Field { value, field } => {
784                if let Expr::Var(alias) = (*value).clone() {
785                    if !shadow.contains(&alias) {
786                        if let Some(child) = self.path_imports.get(&alias) {
787                            return Expr::Var(format!("{child}.{field}"));
788                        }
789                    }
790                }
791                Expr::Field {
792                    value: Box::new(self.mangle_expr(*value, shadow)),
793                    field,
794                }
795            }
796            Expr::BinOp { op, lhs, rhs } => Expr::BinOp {
797                op,
798                lhs: Box::new(self.mangle_expr(*lhs, shadow)),
799                rhs: Box::new(self.mangle_expr(*rhs, shadow)),
800            },
801            Expr::UnaryOp { op, expr } => Expr::UnaryOp {
802                op,
803                expr: Box::new(self.mangle_expr(*expr, shadow)),
804            },
805            Expr::If {
806                cond,
807                then_block,
808                else_block,
809            } => Expr::If {
810                cond: Box::new(self.mangle_expr(*cond, shadow)),
811                then_block: self.mangle_block(then_block, shadow),
812                else_block: self.mangle_block(else_block, shadow),
813            },
814            Expr::Match { scrutinee, arms } => Expr::Match {
815                scrutinee: Box::new(self.mangle_expr(*scrutinee, shadow)),
816                arms: arms
817                    .into_iter()
818                    .map(|a| {
819                        let mut arm_shadow = shadow.clone();
820                        collect_pattern_binders(&a.pattern, &mut arm_shadow);
821                        Arm {
822                            pattern: self.mangle_pattern(a.pattern),
823                            body: self.mangle_expr(a.body, &arm_shadow),
824                        }
825                    })
826                    .collect(),
827            },
828            Expr::RecordLit(fields) => Expr::RecordLit(
829                fields
830                    .into_iter()
831                    .map(|f| RecordLitField {
832                        name: f.name,
833                        value: self.mangle_expr(f.value, shadow),
834                    })
835                    .collect(),
836            ),
837            Expr::TupleLit(items) => Expr::TupleLit(
838                items
839                    .into_iter()
840                    .map(|i| self.mangle_expr(i, shadow))
841                    .collect(),
842            ),
843            Expr::ListLit(items) => Expr::ListLit(
844                items
845                    .into_iter()
846                    .map(|i| self.mangle_expr(i, shadow))
847                    .collect(),
848            ),
849            Expr::Constructor { name, args } => Expr::Constructor {
850                name,
851                args: args
852                    .into_iter()
853                    .map(|a| self.mangle_expr(a, shadow))
854                    .collect(),
855            },
856            Expr::Ascription { value, ty } => Expr::Ascription {
857                value: Box::new(self.mangle_expr(*value, shadow)),
858                ty: self.mangle_type_expr(ty),
859            },
860            Expr::Lambda(lambda) => {
861                let mut lam_shadow = shadow.clone();
862                for p in &lambda.params {
863                    lam_shadow.insert(p.name.clone());
864                }
865                Expr::Lambda(Box::new(Lambda {
866                    params: lambda
867                        .params
868                        .into_iter()
869                        .map(|p| Param {
870                            name: p.name,
871                            ty: self.mangle_type_expr(p.ty),
872                        })
873                        .collect(),
874                    return_type: self.mangle_type_expr(lambda.return_type),
875                    effects: lambda.effects,
876                    effect_row_var: lambda.effect_row_var,
877                    body: self.mangle_block(lambda.body, &lam_shadow),
878                }))
879            }
880        }
881    }
882
883    fn mangle_pattern(&self, p: Pattern) -> Pattern {
884        match p {
885            Pattern::Constructor { name, args } => Pattern::Constructor {
886                name,
887                args: args.into_iter().map(|a| self.mangle_pattern(a)).collect(),
888            },
889            Pattern::Record { fields, rest } => Pattern::Record {
890                fields: fields
891                    .into_iter()
892                    .map(|f| RecordPatField {
893                        name: f.name,
894                        pattern: f.pattern.map(|p| self.mangle_pattern(p)),
895                    })
896                    .collect(),
897                rest,
898            },
899            Pattern::Tuple(items) => {
900                Pattern::Tuple(items.into_iter().map(|p| self.mangle_pattern(p)).collect())
901            }
902            Pattern::Lit(_) | Pattern::Var(_) | Pattern::Wild => p,
903        }
904    }
905}
906
907fn collect_pattern_binders(p: &Pattern, out: &mut HashSet<String>) {
908    match p {
909        Pattern::Var(name) => {
910            out.insert(name.clone());
911        }
912        Pattern::Constructor { args, .. } => {
913            for a in args {
914                collect_pattern_binders(a, out);
915            }
916        }
917        Pattern::Record { fields, .. } => {
918            for f in fields {
919                match &f.pattern {
920                    Some(p) => collect_pattern_binders(p, out),
921                    // `{ name }` shorthand binds `name`.
922                    None => {
923                        out.insert(f.name.clone());
924                    }
925                }
926            }
927        }
928        Pattern::Tuple(items) => {
929            for p in items {
930                collect_pattern_binders(p, out);
931            }
932        }
933        Pattern::Lit(_) | Pattern::Wild => {}
934    }
935}