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