Skip to main content

bamts_compiler/
program.rs

1//! Compiler-owned whole-program loading and canonical module identity.
2
3use std::{
4    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
5    fmt, fs, io,
6    path::{Path, PathBuf},
7    sync::Arc,
8};
9
10use bamts_bytecode::{
11    Binding, BindingId, BindingKind, Constant, ConstantId, EcmaString, EcmaStringBuilder, Edge,
12    EdgeId, EdgeKind, EdgeTarget, Export, ExportSource, ModuleId, Program as BytecodeProgram,
13    ProgramModule, ProgramVerifyError, Verified,
14};
15
16use crate::{
17    lower::{self, LowerError, LowerOptions},
18    parser,
19    pipeline::ProgramFrontendOutput,
20    project::{
21        CompilerOptions, ModuleResolutionError, PackageError, PackageJson, PackageMode,
22        PackageTarget, ProjectRoot, ResolutionConditions, ResolutionFlavor, plan_relative_module,
23    },
24    scanner,
25    source::{ScriptKind, SourceId, SourceIdentity, SourceText, TextRange, Utf16Pos},
26    syntax::{
27        ExportDeclaration, ExportDefaultValue, ExportNamedDeclaration, ExportSpecifierMode,
28        ImportBinding, ImportSpecifierMode, ModuleExportName, SourceFile, Statement, TokenKind,
29        VariableKind,
30    },
31};
32
33/// The semantic role of one resolved module dependency.
34#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub enum ModuleEdgeKind {
36    StaticRuntime,
37    TypeOnly,
38    DynamicRuntime,
39}
40
41/// The canonical identity of a resolved module dependency.
42#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
43pub enum ModuleTarget {
44    Local(SourceId),
45    External(Arc<str>),
46}
47
48impl ModuleTarget {
49    #[must_use]
50    pub const fn local_source_id(&self) -> Option<SourceId> {
51        match self {
52            Self::Local(source_id) => Some(*source_id),
53            Self::External(_) => None,
54        }
55    }
56
57    #[must_use]
58    pub fn external_specifier(&self) -> Option<&str> {
59        match self {
60            Self::Local(_) => None,
61            Self::External(specifier) => Some(specifier),
62        }
63    }
64}
65
66/// One source-anchored, resolved dependency.
67#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct ModuleEdge {
69    kind: ModuleEdgeKind,
70    specifier: Arc<str>,
71    target: ModuleTarget,
72    range: TextRange,
73}
74
75impl ModuleEdge {
76    #[must_use]
77    pub const fn kind(&self) -> ModuleEdgeKind {
78        self.kind
79    }
80
81    #[must_use]
82    pub fn specifier(&self) -> &str {
83        &self.specifier
84    }
85
86    #[must_use]
87    pub const fn target(&self) -> &ModuleTarget {
88        &self.target
89    }
90
91    #[must_use]
92    pub const fn range(&self) -> TextRange {
93        self.range
94    }
95}
96
97/// A module loaded exactly once under its canonical filesystem identity.
98#[derive(Clone, Debug)]
99pub struct ResolvedModule {
100    identity: SourceIdentity,
101    script_kind: ScriptKind,
102    source: Arc<SourceText>,
103    dependencies: Arc<[ModuleEdge]>,
104}
105
106impl ResolvedModule {
107    #[must_use]
108    pub const fn identity(&self) -> &SourceIdentity {
109        &self.identity
110    }
111
112    #[must_use]
113    pub const fn source_id(&self) -> SourceId {
114        self.identity.source_id()
115    }
116
117    #[must_use]
118    pub fn path(&self) -> &Path {
119        self.identity.path()
120    }
121
122    #[must_use]
123    pub const fn script_kind(&self) -> ScriptKind {
124        self.script_kind
125    }
126
127    #[must_use]
128    pub const fn source(&self) -> &Arc<SourceText> {
129        &self.source
130    }
131
132    #[must_use]
133    pub fn dependencies(&self) -> &[ModuleEdge] {
134        &self.dependencies
135    }
136}
137
138/// The canonical whole-program value shared by every compiler and execution phase.
139///
140/// Modules are stored in deterministic dependency-first DFS postorder. Cycles are
141/// retained as ordinary edges; each canonical file still appears exactly once.
142#[derive(Clone, Debug)]
143pub struct ResolvedProgram {
144    root: ProjectRoot,
145    entrypoint: SourceId,
146    modules: Arc<[ResolvedModule]>,
147    module_indices: HashMap<SourceId, usize>,
148}
149
150impl ResolvedProgram {
151    #[must_use]
152    pub const fn root(&self) -> &ProjectRoot {
153        &self.root
154    }
155
156    #[must_use]
157    pub const fn entrypoint_id(&self) -> SourceId {
158        self.entrypoint
159    }
160
161    #[must_use]
162    pub fn entrypoint(&self) -> &ResolvedModule {
163        self.module(self.entrypoint)
164            .expect("resolved program always contains its entrypoint")
165    }
166
167    #[must_use]
168    pub fn modules(&self) -> &[ResolvedModule] {
169        &self.modules
170    }
171
172    #[must_use]
173    pub fn module(&self, source_id: SourceId) -> Option<&ResolvedModule> {
174        self.module_indices
175            .get(&source_id)
176            .map(|index| &self.modules[*index])
177    }
178
179    /// Returns the eager runtime closure in the program's canonical order.
180    /// Type-only, dynamic, and external edges do not cause eager runtime initialization.
181    #[must_use]
182    pub fn runtime_modules(&self) -> Vec<&ResolvedModule> {
183        let mut reachable = HashSet::new();
184        let mut pending = vec![self.entrypoint];
185        while let Some(source_id) = pending.pop() {
186            if !reachable.insert(source_id) {
187                continue;
188            }
189            let module = self
190                .module(source_id)
191                .expect("every local edge target belongs to the resolved program");
192            pending.extend(module.dependencies().iter().filter_map(|edge| {
193                match (edge.kind, edge.target()) {
194                    (ModuleEdgeKind::StaticRuntime, ModuleTarget::Local(source_id)) => {
195                        Some(*source_id)
196                    }
197                    _ => None,
198                }
199            }));
200        }
201        self.modules
202            .iter()
203            .filter(|module| reachable.contains(&module.source_id()))
204            .collect()
205    }
206}
207
208/// A typed module-resolution failure anchored at the importing source.
209#[derive(Clone, Debug, Eq, PartialEq)]
210pub struct UnresolvedModuleDiagnostic {
211    importer: Arc<Path>,
212    specifier: Arc<str>,
213    kind: ModuleEdgeKind,
214    range: TextRange,
215}
216
217impl UnresolvedModuleDiagnostic {
218    #[must_use]
219    pub fn importer(&self) -> &Path {
220        &self.importer
221    }
222
223    #[must_use]
224    pub fn specifier(&self) -> &str {
225        &self.specifier
226    }
227
228    #[must_use]
229    pub const fn kind(&self) -> ModuleEdgeKind {
230        self.kind
231    }
232
233    #[must_use]
234    pub const fn range(&self) -> TextRange {
235        self.range
236    }
237}
238
239/// Fail-fast program loading errors. No partially resolved graph is exposed.
240#[derive(Debug)]
241pub enum ProgramLoadError {
242    InvalidRoot(io::Error),
243    EntryOutsideRoot(PathBuf),
244    TraversalRejected {
245        path: PathBuf,
246        root: PathBuf,
247    },
248    Read {
249        path: PathBuf,
250        source: io::Error,
251    },
252    UnsupportedSource(PathBuf),
253    TooManySources,
254    IllFormedModuleSpecifier {
255        importer: PathBuf,
256        range: TextRange,
257    },
258    InvalidSpecifier {
259        diagnostic: UnresolvedModuleDiagnostic,
260        source: ModuleResolutionError,
261    },
262    InvalidPackage {
263        diagnostic: UnresolvedModuleDiagnostic,
264        source: PackageError,
265    },
266    UnresolvedModule(UnresolvedModuleDiagnostic),
267}
268
269impl fmt::Display for ProgramLoadError {
270    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
271        match self {
272            Self::InvalidRoot(error) => {
273                write!(formatter, "cannot canonicalize project root: {error}")
274            }
275            Self::EntryOutsideRoot(path) => {
276                write!(
277                    formatter,
278                    "entrypoint {} is outside the project root",
279                    path.display()
280                )
281            }
282            Self::TraversalRejected { path, root } => write!(
283                formatter,
284                "resolved path {} escapes project root {}",
285                path.display(),
286                root.display()
287            ),
288            Self::Read { path, source } => {
289                write!(formatter, "cannot read {}: {source}", path.display())
290            }
291            Self::UnsupportedSource(path) => {
292                write!(
293                    formatter,
294                    "unsupported source extension: {}",
295                    path.display()
296                )
297            }
298            Self::TooManySources => {
299                formatter.write_str("program contains more than u32::MAX sources")
300            }
301            Self::IllFormedModuleSpecifier { importer, .. } => write!(
302                formatter,
303                "module specifier in {} is not well-formed UTF-16",
304                importer.display()
305            ),
306            Self::InvalidSpecifier { diagnostic, source } => write!(
307                formatter,
308                "invalid module specifier {:?} in {}: {source}",
309                diagnostic.specifier(),
310                diagnostic.importer().display()
311            ),
312            Self::InvalidPackage { diagnostic, source } => write!(
313                formatter,
314                "invalid package specifier {:?} in {}: {source}",
315                diagnostic.specifier(),
316                diagnostic.importer().display()
317            ),
318            Self::UnresolvedModule(diagnostic) => write!(
319                formatter,
320                "cannot resolve {:?} from {}",
321                diagnostic.specifier(),
322                diagnostic.importer().display()
323            ),
324        }
325    }
326}
327
328impl std::error::Error for ProgramLoadError {
329    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
330        match self {
331            Self::InvalidRoot(error) | Self::Read { source: error, .. } => Some(error),
332            Self::InvalidSpecifier { source, .. } => Some(source),
333            Self::InvalidPackage { source, .. } => Some(source),
334            _ => None,
335        }
336    }
337}
338
339/// Loads one entrypoint and its complete local module graph.
340#[derive(Clone, Debug)]
341pub struct ProgramLoader {
342    root: ProjectRoot,
343    options: CompilerOptions,
344}
345
346impl ProgramLoader {
347    /// Creates a loader using the project's already-validated compiler options.
348    pub fn new(root: &ProjectRoot, options: &CompilerOptions) -> Result<Self, ProgramLoadError> {
349        let canonical = fs::canonicalize(root.path()).map_err(ProgramLoadError::InvalidRoot)?;
350        let root = ProjectRoot::new(canonical).map_err(|error| {
351            ProgramLoadError::InvalidRoot(io::Error::new(io::ErrorKind::InvalidInput, error))
352        })?;
353        Ok(Self {
354            root,
355            options: options.clone(),
356        })
357    }
358
359    /// Resolves and loads an entrypoint. The entrypoint may be root-relative or absolute.
360    pub fn load(&self, entrypoint: impl AsRef<Path>) -> Result<ResolvedProgram, ProgramLoadError> {
361        let requested = self
362            .root
363            .resolve(entrypoint.as_ref())
364            .map_err(|_| ProgramLoadError::EntryOutsideRoot(entrypoint.as_ref().to_path_buf()))?;
365        let entrypoint = self
366            .select_absolute(&requested, ResolutionFlavor::Runtime)?
367            .ok_or_else(|| ProgramLoadError::Read {
368                path: requested,
369                source: io::Error::new(io::ErrorKind::NotFound, "entrypoint does not exist"),
370            })?;
371
372        let mut state = LoadState {
373            loader: self,
374            identities: HashMap::new(),
375            modules: Vec::new(),
376        };
377        let entrypoint = state.visit(entrypoint)?;
378        let module_indices = state
379            .modules
380            .iter()
381            .enumerate()
382            .map(|(index, module)| (module.source_id(), index))
383            .collect();
384        Ok(ResolvedProgram {
385            root: self.root.clone(),
386            entrypoint,
387            modules: Arc::from(state.modules),
388            module_indices,
389        })
390    }
391
392    fn select_absolute(
393        &self,
394        requested: &Path,
395        flavor: ResolutionFlavor,
396    ) -> Result<Option<PathBuf>, ProgramLoadError> {
397        let relative = requested.strip_prefix(self.root.path()).map_err(|_| {
398            ProgramLoadError::TraversalRejected {
399                path: requested.to_path_buf(),
400                root: self.root.path().to_path_buf(),
401            }
402        })?;
403        let specifier = format!("./{}", relative.to_string_lossy().replace('\\', "/"));
404        let synthetic_importer = self.root.path().join("__bamts_program__.ts");
405        let plan = plan_relative_module(
406            &self.root,
407            synthetic_importer,
408            &specifier,
409            flavor,
410            self.options.resolve_json_module(),
411        )
412        .map_err(|source| ProgramLoadError::InvalidSpecifier {
413            diagnostic: diagnostic(
414                self.root.path(),
415                &specifier,
416                edge_kind(flavor),
417                TextRange::new(Utf16Pos::ZERO, Utf16Pos::ZERO)
418                    .expect("equal range endpoints are valid"),
419            ),
420            source,
421        })?;
422        self.canonical_selection(plan.candidates(), flavor)
423    }
424
425    fn canonical_selection(
426        &self,
427        candidates: &[PathBuf],
428        flavor: ResolutionFlavor,
429    ) -> Result<Option<PathBuf>, ProgramLoadError> {
430        if flavor == ResolutionFlavor::Runtime {
431            for candidate in candidates {
432                if is_declaration_path(candidate) || !candidate.is_file() {
433                    continue;
434                }
435                return self.canonical_candidate(candidate).map(Some);
436            }
437        }
438        for candidate in candidates {
439            if candidate.is_file() {
440                return self.canonical_candidate(candidate).map(Some);
441            }
442        }
443        Ok(None)
444    }
445
446    fn canonical_candidate(&self, candidate: &Path) -> Result<PathBuf, ProgramLoadError> {
447        let canonical = fs::canonicalize(candidate).map_err(|source| ProgramLoadError::Read {
448            path: candidate.to_path_buf(),
449            source,
450        })?;
451        if !canonical.starts_with(self.root.path()) {
452            return Err(ProgramLoadError::TraversalRejected {
453                path: canonical,
454                root: self.root.path().to_path_buf(),
455            });
456        }
457        Ok(canonical)
458    }
459
460    fn resolve_edge(
461        &self,
462        importer: &Path,
463        edge: &UnresolvedEdge,
464    ) -> Result<ResolvedEdgeTarget, ProgramLoadError> {
465        if edge.specifier.starts_with("node:") {
466            return Ok(ResolvedEdgeTarget::External(Arc::clone(&edge.specifier)));
467        }
468
469        let flavor = match edge.kind {
470            ModuleEdgeKind::TypeOnly => ResolutionFlavor::Types,
471            ModuleEdgeKind::StaticRuntime | ModuleEdgeKind::DynamicRuntime => {
472                ResolutionFlavor::Runtime
473            }
474        };
475        let selected = if edge.specifier.starts_with("./") || edge.specifier.starts_with("../") {
476            let plan = plan_relative_module(
477                &self.root,
478                importer,
479                &edge.specifier,
480                flavor,
481                self.options.resolve_json_module(),
482            )
483            .map_err(|source| ProgramLoadError::InvalidSpecifier {
484                diagnostic: diagnostic(importer, &edge.specifier, edge.kind, edge.range),
485                source,
486            })?;
487            self.canonical_selection(plan.candidates(), flavor)?
488                .map(ResolvedEdgeTarget::Local)
489        } else if edge.specifier.starts_with('#') {
490            self.resolve_package_import(importer, edge, flavor)?
491        } else {
492            match self.resolve_mapped(&edge.specifier, flavor)? {
493                Some(mapped) => Some(ResolvedEdgeTarget::Local(mapped)),
494                None => self
495                    .resolve_package(importer, edge, flavor)?
496                    .map(ResolvedEdgeTarget::Local),
497            }
498        };
499        if let Some(target) = selected {
500            return Ok(target);
501        }
502        if edge.kind == ModuleEdgeKind::TypeOnly
503            && split_package_specifier(&edge.specifier).is_some()
504        {
505            return Ok(ResolvedEdgeTarget::External(Arc::clone(&edge.specifier)));
506        }
507        Err(ProgramLoadError::UnresolvedModule(diagnostic(
508            importer,
509            &edge.specifier,
510            edge.kind,
511            edge.range,
512        )))
513    }
514
515    fn resolve_mapped(
516        &self,
517        specifier: &str,
518        flavor: ResolutionFlavor,
519    ) -> Result<Option<PathBuf>, ProgramLoadError> {
520        for mapping in self.options.paths() {
521            let Some(capture) = pattern_capture(mapping.pattern(), specifier) else {
522                continue;
523            };
524            for target in mapping.targets() {
525                let target = PathBuf::from(target.to_string_lossy().replace('*', capture));
526                if let Some(selected) = self.select_absolute(&target, flavor)? {
527                    return Ok(Some(selected));
528                }
529            }
530        }
531        Ok(None)
532    }
533
534    fn resolve_package(
535        &self,
536        importer: &Path,
537        edge: &UnresolvedEdge,
538        flavor: ResolutionFlavor,
539    ) -> Result<Option<PathBuf>, ProgramLoadError> {
540        let Some((package_name, subpath)) = split_package_specifier(&edge.specifier) else {
541            return Ok(None);
542        };
543        let mut directory = importer.parent();
544        while let Some(current) = directory {
545            if !current.starts_with(self.root.path()) {
546                break;
547            }
548            let package_directory = current.join("node_modules").join(package_name);
549            let package_path = package_directory.join("package.json");
550            if package_path.is_file() {
551                let package_source =
552                    fs::read_to_string(&package_path).map_err(|source| ProgramLoadError::Read {
553                        path: package_path.clone(),
554                        source,
555                    })?;
556                let package = PackageJson::parse(&self.root, &package_path, &package_source)
557                    .map_err(|source| ProgramLoadError::InvalidPackage {
558                        diagnostic: diagnostic(importer, &edge.specifier, edge.kind, edge.range),
559                        source,
560                    })?;
561                let mode = if flavor == ResolutionFlavor::Types {
562                    PackageMode::Types
563                } else {
564                    PackageMode::Import
565                };
566                let conditions = ResolutionConditions::for_mode(mode);
567                let target = package
568                    .resolve_export(&self.root, &subpath, mode, &conditions)
569                    .map_err(|source| ProgramLoadError::InvalidPackage {
570                        diagnostic: diagnostic(importer, &edge.specifier, edge.kind, edge.range),
571                        source,
572                    })?;
573                return self.select_absolute(&target, flavor);
574            }
575            if current == self.root.path() {
576                break;
577            }
578            directory = current.parent();
579        }
580        Ok(None)
581    }
582    fn resolve_package_import(
583        &self,
584        importer: &Path,
585        edge: &UnresolvedEdge,
586        flavor: ResolutionFlavor,
587    ) -> Result<Option<ResolvedEdgeTarget>, ProgramLoadError> {
588        let mut directory = importer.parent();
589        while let Some(current) = directory {
590            if !current.starts_with(self.root.path()) {
591                break;
592            }
593            let package_path = current.join("package.json");
594            if package_path.is_file() {
595                let package_source =
596                    fs::read_to_string(&package_path).map_err(|source| ProgramLoadError::Read {
597                        path: package_path.clone(),
598                        source,
599                    })?;
600                let package = PackageJson::parse(&self.root, &package_path, &package_source)
601                    .map_err(|source| ProgramLoadError::InvalidPackage {
602                        diagnostic: diagnostic(importer, &edge.specifier, edge.kind, edge.range),
603                        source,
604                    })?;
605                let mode = if flavor == ResolutionFlavor::Types {
606                    PackageMode::Types
607                } else {
608                    PackageMode::Import
609                };
610                let conditions = ResolutionConditions::for_mode(mode);
611                let target = package
612                    .resolve_import(&self.root, &edge.specifier, &conditions)
613                    .map_err(|source| ProgramLoadError::InvalidPackage {
614                        diagnostic: diagnostic(importer, &edge.specifier, edge.kind, edge.range),
615                        source,
616                    })?;
617                return match target {
618                    PackageTarget::Path(path) => Ok(self
619                        .select_absolute(&path, flavor)?
620                        .map(ResolvedEdgeTarget::Local)),
621                    PackageTarget::External(specifier) => {
622                        let external = UnresolvedEdge {
623                            kind: edge.kind,
624                            specifier,
625                            range: edge.range,
626                        };
627                        match self.resolve_mapped(&external.specifier, flavor)? {
628                            Some(mapped) => Ok(Some(ResolvedEdgeTarget::Local(mapped))),
629                            None => match self.resolve_package(importer, &external, flavor)? {
630                                Some(package) => Ok(Some(ResolvedEdgeTarget::Local(package))),
631                                None => Ok(Some(ResolvedEdgeTarget::External(external.specifier))),
632                            },
633                        }
634                    }
635                };
636            }
637            if current == self.root.path() {
638                break;
639            }
640            directory = current.parent();
641        }
642        Ok(None)
643    }
644}
645
646#[derive(Clone, Debug)]
647enum ResolvedEdgeTarget {
648    Local(PathBuf),
649    External(Arc<str>),
650}
651
652struct LoadState<'a> {
653    loader: &'a ProgramLoader,
654    identities: HashMap<PathBuf, SourceId>,
655    modules: Vec<ResolvedModule>,
656}
657
658impl LoadState<'_> {
659    fn visit(&mut self, path: PathBuf) -> Result<SourceId, ProgramLoadError> {
660        if let Some(source_id) = self.identities.get(&path) {
661            return Ok(*source_id);
662        }
663        let source_id = SourceId::new(
664            u32::try_from(self.identities.len()).map_err(|_| ProgramLoadError::TooManySources)?,
665        );
666        self.identities.insert(path.clone(), source_id);
667
668        let script_kind =
669            script_kind(&path).ok_or_else(|| ProgramLoadError::UnsupportedSource(path.clone()))?;
670        let text = fs::read_to_string(&path).map_err(|source| ProgramLoadError::Read {
671            path: path.clone(),
672            source,
673        })?;
674        let source = Arc::new(SourceText::new(text));
675        let parsed = parser::parse(scanner::scan(source_id, script_kind, Arc::clone(&source)));
676        let unresolved = collect_edges(parsed.product()).map_err(|range| {
677            ProgramLoadError::IllFormedModuleSpecifier {
678                importer: path.clone(),
679                range,
680            }
681        })?;
682        let mut dependencies = Vec::with_capacity(unresolved.len());
683        for edge in unresolved {
684            let target = match self.loader.resolve_edge(&path, &edge)? {
685                ResolvedEdgeTarget::Local(target_path) => {
686                    ModuleTarget::Local(self.visit(target_path)?)
687                }
688                ResolvedEdgeTarget::External(specifier) => ModuleTarget::External(specifier),
689            };
690            dependencies.push(ModuleEdge {
691                kind: edge.kind,
692                specifier: edge.specifier,
693                target,
694                range: edge.range,
695            });
696        }
697        self.modules.push(ResolvedModule {
698            identity: SourceIdentity::new(source_id, Arc::from(path)),
699            script_kind,
700            source,
701            dependencies: Arc::from(dependencies),
702        });
703        Ok(source_id)
704    }
705}
706
707#[derive(Clone, Debug)]
708struct UnresolvedEdge {
709    kind: ModuleEdgeKind,
710    specifier: Arc<str>,
711    range: TextRange,
712}
713
714fn collect_edges(source: &SourceFile) -> Result<Vec<UnresolvedEdge>, TextRange> {
715    let mut edges = Vec::new();
716    for statement in source.statements() {
717        match statement.data() {
718            Statement::Import(import) => {
719                let kind = if import.type_only || import_clause_is_type_only(import.clause.as_ref())
720                {
721                    ModuleEdgeKind::TypeOnly
722                } else {
723                    ModuleEdgeKind::StaticRuntime
724                };
725                push_literal_edge(source, &mut edges, kind, &import.source)?;
726            }
727            Statement::ImportEquals(import) => {
728                if let crate::syntax::ExternalModuleReference::Require(specifier) =
729                    &import.reference
730                {
731                    let kind = if import.is_type_only {
732                        ModuleEdgeKind::TypeOnly
733                    } else {
734                        ModuleEdgeKind::StaticRuntime
735                    };
736                    push_literal_edge(source, &mut edges, kind, specifier)?;
737                }
738            }
739            Statement::Export(ExportDeclaration::All(export)) => {
740                let kind = if export.type_only {
741                    ModuleEdgeKind::TypeOnly
742                } else {
743                    ModuleEdgeKind::StaticRuntime
744                };
745                push_literal_edge(source, &mut edges, kind, &export.source)?;
746            }
747            Statement::Export(ExportDeclaration::Named(ExportNamedDeclaration::Specifiers {
748                type_only,
749                specifiers,
750                source: Some(module),
751                ..
752            })) => {
753                let only_types = *type_only
754                    || (!specifiers.is_empty()
755                        && specifiers.iter().all(|specifier| {
756                            specifier.data().mode == ExportSpecifierMode::TypeOnly
757                        }));
758                push_literal_edge(
759                    source,
760                    &mut edges,
761                    if only_types {
762                        ModuleEdgeKind::TypeOnly
763                    } else {
764                        ModuleEdgeKind::StaticRuntime
765                    },
766                    module,
767                )?;
768            }
769            _ => {}
770        }
771    }
772    let ill_formed = {
773        let mut collector = DynamicEdgeCollector {
774            source,
775            edges: &mut edges,
776            ill_formed: None,
777        };
778        collector.scan_statements(source.statements());
779        collector.ill_formed
780    };
781    if let Some(range) = ill_formed {
782        return Err(range);
783    }
784    let tokens: Vec<_> = source
785        .tokens()
786        .iter()
787        .filter(|token| {
788            !matches!(
789                token.kind(),
790                TokenKind::Whitespace
791                    | TokenKind::LineComment
792                    | TokenKind::BlockComment
793                    | TokenKind::Shebang
794            )
795        })
796        .collect();
797    for window in tokens.windows(3) {
798        if window[0].kind() != TokenKind::KwImport
799            || window[1].kind() != TokenKind::LParen
800            || window[2].kind() != TokenKind::StringLiteral
801            || edges.iter().any(|edge| edge.range == window[2].range())
802        {
803            continue;
804        }
805        let Some(value) = source.token_text(window[2]).and_then(unquote) else {
806            continue;
807        };
808        let specifier = value.to_utf8_strict().map_err(|_| window[2].range())?;
809        edges.push(UnresolvedEdge {
810            kind: ModuleEdgeKind::TypeOnly,
811            specifier: Arc::from(specifier),
812            range: window[2].range(),
813        });
814    }
815    Ok(edges)
816}
817
818struct DynamicEdgeCollector<'a> {
819    source: &'a SourceFile,
820    edges: &'a mut Vec<UnresolvedEdge>,
821    ill_formed: Option<TextRange>,
822}
823
824impl DynamicEdgeCollector<'_> {
825    fn push_literal_edge(
826        &mut self,
827        kind: ModuleEdgeKind,
828        literal: &crate::syntax::StringLiteralNode,
829    ) {
830        if self.ill_formed.is_none() {
831            self.ill_formed = push_literal_edge(self.source, self.edges, kind, literal).err();
832        }
833    }
834
835    fn scan_statements(&mut self, statements: &[crate::syntax::Stmt]) {
836        for statement in statements {
837            self.scan_statement(statement);
838        }
839    }
840
841    fn scan_statement(&mut self, statement: &crate::syntax::Stmt) {
842        use crate::syntax::{ExportDefaultValue, ForInitializer, Statement};
843
844        match statement.data() {
845            Statement::Variable(declaration) => {
846                for declarator in &declaration.declarations {
847                    self.scan_pattern(&declarator.data().binding);
848                    if let Some(initializer) = &declarator.data().initializer {
849                        self.scan_expression(initializer);
850                    }
851                }
852            }
853            Statement::Function(declaration) => self.scan_function(&declaration.function),
854            Statement::Class(class) => self.scan_class(class),
855            Statement::Namespace(namespace) => {
856                self.scan_statements(&namespace.body.data().statements)
857            }
858            Statement::Declare(inner)
859            | Statement::Labeled(crate::syntax::LabeledStatement { body: inner, .. }) => {
860                self.scan_statement(inner)
861            }
862            Statement::Block(block) => self.scan_statements(&block.data().statements),
863            Statement::Expression(expression) => self.scan_expression(&expression.expression),
864            Statement::If(value) => {
865                self.scan_expression(&value.test);
866                self.scan_statement(&value.consequent);
867                if let Some(alternate) = &value.alternate {
868                    self.scan_statement(alternate);
869                }
870            }
871            Statement::Switch(value) => {
872                self.scan_expression(&value.discriminant);
873                for case in &value.cases {
874                    if let Some(test) = &case.data().test {
875                        self.scan_expression(test);
876                    }
877                    self.scan_statements(&case.data().consequent);
878                }
879            }
880            Statement::For(value) => {
881                if let Some(initializer) = &value.initializer {
882                    match initializer {
883                        ForInitializer::Variable(declaration) => {
884                            for declarator in &declaration.declarations {
885                                self.scan_pattern(&declarator.data().binding);
886                                if let Some(initializer) = &declarator.data().initializer {
887                                    self.scan_expression(initializer);
888                                }
889                            }
890                        }
891                        ForInitializer::Expression(expression) => self.scan_expression(expression),
892                    }
893                }
894                if let Some(test) = &value.test {
895                    self.scan_expression(test);
896                }
897                if let Some(update) = &value.update {
898                    self.scan_expression(update);
899                }
900                self.scan_statement(&value.body);
901            }
902            Statement::ForIn(value) => {
903                self.scan_for_binding(&value.binding);
904                self.scan_expression(&value.object);
905                self.scan_statement(&value.body);
906            }
907            Statement::ForOf(value) => {
908                self.scan_for_binding(&value.binding);
909                self.scan_expression(&value.iterable);
910                self.scan_statement(&value.body);
911            }
912            Statement::While(value) => {
913                self.scan_expression(&value.test);
914                self.scan_statement(&value.body);
915            }
916            Statement::DoWhile(value) => {
917                self.scan_statement(&value.body);
918                self.scan_expression(&value.test);
919            }
920            Statement::Try(value) => {
921                self.scan_statements(&value.block.data().statements);
922                if let Some(handler) = &value.handler {
923                    if let Some(binding) = &handler.data().binding {
924                        self.scan_pattern(binding);
925                    }
926                    self.scan_statements(&handler.data().body.data().statements);
927                }
928                if let Some(finalizer) = &value.finalizer {
929                    self.scan_statements(&finalizer.data().statements);
930                }
931            }
932            Statement::With(value) => {
933                self.scan_expression(&value.object);
934                self.scan_statement(&value.body);
935            }
936            Statement::Return(value) => {
937                if let Some(argument) = &value.argument {
938                    self.scan_expression(argument);
939                }
940            }
941            Statement::Throw(value) => self.scan_expression(&value.argument),
942            Statement::Export(ExportDeclaration::Named(ExportNamedDeclaration::Declaration(
943                inner,
944            ))) => self.scan_statement(inner),
945            Statement::Export(ExportDeclaration::Default(value)) => match &value.value {
946                ExportDefaultValue::Function(function) => self.scan_function(function),
947                ExportDefaultValue::Class(class) => self.scan_class(class),
948                ExportDefaultValue::Expression(expression) => self.scan_expression(expression),
949                ExportDefaultValue::Missing(_) => {}
950            },
951            Statement::Export(ExportDeclaration::Assignment(expression)) => {
952                self.scan_expression(expression)
953            }
954            _ => {}
955        }
956    }
957
958    fn scan_for_binding(&mut self, binding: &crate::syntax::ForBinding) {
959        match binding {
960            crate::syntax::ForBinding::Variable(declaration) => {
961                for declarator in &declaration.declarations {
962                    self.scan_pattern(&declarator.data().binding);
963                    if let Some(initializer) = &declarator.data().initializer {
964                        self.scan_expression(initializer);
965                    }
966                }
967            }
968            crate::syntax::ForBinding::Target(target) => self.scan_target(target),
969        }
970    }
971
972    fn scan_parameters(&mut self, parameters: &[crate::syntax::ParameterNode]) {
973        for parameter in parameters {
974            for decorator in &parameter.data().decorators {
975                self.scan_expression(&decorator.data().expression);
976            }
977            self.scan_pattern(&parameter.data().binding);
978            if let Some(initializer) = &parameter.data().initializer {
979                self.scan_expression(initializer);
980            }
981        }
982    }
983
984    fn scan_pattern(&mut self, pattern: &crate::syntax::Pattern) {
985        use crate::syntax::{ArrayBindingElement, BindingPattern, PropertyName};
986        match pattern.data() {
987            BindingPattern::Object(object) => {
988                for property in &object.properties {
989                    if let PropertyName::Computed(expression) = &property.name {
990                        self.scan_expression(expression);
991                    }
992                    if let Some(initializer) = &property.initializer {
993                        self.scan_expression(initializer);
994                    }
995                    self.scan_pattern(&property.binding);
996                }
997            }
998            BindingPattern::Array(array) => {
999                for element in &array.elements {
1000                    if let ArrayBindingElement::Binding(inner) = element {
1001                        self.scan_pattern(inner);
1002                    }
1003                }
1004            }
1005            BindingPattern::Rest(rest) => self.scan_pattern(&rest.argument),
1006            BindingPattern::Assignment(value) => {
1007                self.scan_pattern(&value.left);
1008                self.scan_expression(&value.right);
1009            }
1010            BindingPattern::Identifier(_) | BindingPattern::Missing(_) => {}
1011        }
1012    }
1013
1014    fn scan_function(&mut self, function: &crate::syntax::FunctionLike) {
1015        self.scan_parameters(&function.parameters);
1016        if let Some(body) = &function.body {
1017            match body {
1018                crate::syntax::FunctionBody::Block(block) => {
1019                    self.scan_statements(&block.data().statements)
1020                }
1021                crate::syntax::FunctionBody::Expression(expression) => {
1022                    self.scan_expression(expression)
1023                }
1024                crate::syntax::FunctionBody::Missing(_) => {}
1025            }
1026        }
1027    }
1028
1029    fn scan_class(&mut self, class: &crate::syntax::ClassDeclaration) {
1030        use crate::syntax::{ClassMember, PropertyName};
1031        for decorator in &class.decorators {
1032            self.scan_expression(&decorator.data().expression);
1033        }
1034        if let Some(heritage) = &class.extends {
1035            self.scan_expression(&heritage.expression);
1036        }
1037        for member in &class.members {
1038            match member.data() {
1039                ClassMember::Constructor(value) => {
1040                    self.scan_parameters(&value.parameters);
1041                    self.scan_statements(&value.body.data().statements);
1042                }
1043                ClassMember::Method(value) => {
1044                    if let PropertyName::Computed(expression) = &value.name {
1045                        self.scan_expression(expression);
1046                    }
1047                    self.scan_function(&value.function);
1048                }
1049                ClassMember::Property(value) => {
1050                    if let PropertyName::Computed(expression) = &value.name {
1051                        self.scan_expression(expression);
1052                    }
1053                    if let Some(initializer) = &value.initializer {
1054                        self.scan_expression(initializer);
1055                    }
1056                }
1057                ClassMember::AutoAccessor(value) => {
1058                    if let PropertyName::Computed(expression) = &value.name {
1059                        self.scan_expression(expression);
1060                    }
1061                    if let Some(initializer) = &value.initializer {
1062                        self.scan_expression(initializer);
1063                    }
1064                }
1065                ClassMember::StaticBlock(block) => self.scan_statements(&block.data().statements),
1066                ClassMember::IndexSignature(_) | ClassMember::Missing(_) => {}
1067            }
1068        }
1069    }
1070
1071    fn scan_expression(&mut self, expression: &crate::syntax::Expr) {
1072        use crate::syntax::{
1073            ArrayElement, Expression, Literal, MemberProperty, ObjectMember, PropertyName,
1074        };
1075        match expression.data() {
1076            Expression::Template(value) => {
1077                for expression in &value.expressions {
1078                    self.scan_expression(expression);
1079                }
1080            }
1081            Expression::TaggedTemplate(value) => {
1082                self.scan_expression(&value.tag);
1083                for expression in &value.template.expressions {
1084                    self.scan_expression(expression);
1085                }
1086            }
1087            Expression::Array(value) => {
1088                for element in &value.elements {
1089                    match element {
1090                        ArrayElement::Expression(value) => self.scan_expression(value),
1091                        ArrayElement::Spread(value) => self.scan_expression(&value.argument),
1092                        _ => {}
1093                    }
1094                }
1095            }
1096            Expression::Object(value) => {
1097                for member in &value.members {
1098                    match member.data() {
1099                        ObjectMember::Property(value) => {
1100                            if let PropertyName::Computed(key) = &value.name {
1101                                self.scan_expression(key);
1102                            }
1103                            self.scan_expression(&value.value);
1104                        }
1105                        ObjectMember::Method(value) => {
1106                            if let PropertyName::Computed(key) = &value.name {
1107                                self.scan_expression(key);
1108                            }
1109                            self.scan_function(&value.function);
1110                        }
1111                        ObjectMember::Spread(value) => self.scan_expression(&value.argument),
1112                        ObjectMember::Missing(_) => {}
1113                    }
1114                }
1115            }
1116            Expression::Function(value) => self.scan_function(&value.function),
1117            Expression::Class(value) => self.scan_class(&value.class),
1118            Expression::Arrow(value) => {
1119                self.scan_parameters(&value.parameters);
1120                match &value.body {
1121                    crate::syntax::FunctionBody::Block(block) => {
1122                        self.scan_statements(&block.data().statements)
1123                    }
1124                    crate::syntax::FunctionBody::Expression(value) => self.scan_expression(value),
1125                    crate::syntax::FunctionBody::Missing(_) => {}
1126                }
1127            }
1128            Expression::Call(value) => {
1129                self.scan_expression(&value.callee);
1130                self.scan_arguments(&value.arguments);
1131            }
1132            Expression::New(value) => {
1133                self.scan_expression(&value.callee);
1134                self.scan_arguments(&value.arguments);
1135            }
1136            Expression::Member(value) => {
1137                self.scan_expression(&value.object);
1138                if let MemberProperty::Computed(value) = &value.property {
1139                    self.scan_expression(value);
1140                }
1141            }
1142            Expression::Await(value) => self.scan_expression(&value.argument),
1143            Expression::Yield(value) => {
1144                if let Some(argument) = &value.argument {
1145                    self.scan_expression(argument);
1146                }
1147            }
1148            Expression::Unary(value) => self.scan_expression(&value.argument),
1149            Expression::Update(value) => self.scan_target(&value.argument),
1150            Expression::Binary(value) => {
1151                self.scan_expression(&value.left);
1152                self.scan_expression(&value.right);
1153            }
1154            Expression::Logical(value) => {
1155                self.scan_expression(&value.left);
1156                self.scan_expression(&value.right);
1157            }
1158            Expression::Conditional(value) => {
1159                self.scan_expression(&value.test);
1160                self.scan_expression(&value.consequent);
1161                self.scan_expression(&value.alternate);
1162            }
1163            Expression::Assignment(value) => {
1164                self.scan_target(&value.left);
1165                self.scan_expression(&value.right);
1166            }
1167            Expression::Sequence(value) => {
1168                for expression in &value.expressions {
1169                    self.scan_expression(expression);
1170                }
1171            }
1172            Expression::Parenthesized(value) => self.scan_expression(value),
1173            Expression::As(value) => self.scan_expression(&value.expression),
1174            Expression::Satisfies(value) => self.scan_expression(&value.expression),
1175            Expression::TypeAssertion(value) => self.scan_expression(&value.expression),
1176            Expression::NonNull(value) => self.scan_expression(&value.expression),
1177            Expression::Import(value) => {
1178                if let Expression::Literal(Literal::String(literal)) = value.source.data() {
1179                    self.push_literal_edge(ModuleEdgeKind::DynamicRuntime, literal);
1180                }
1181                self.scan_expression(&value.source);
1182                if let Some(options) = &value.options {
1183                    self.scan_expression(options);
1184                }
1185            }
1186            Expression::Identifier(_)
1187            | Expression::This
1188            | Expression::Super
1189            | Expression::Literal(_)
1190            | Expression::Meta(_)
1191            | Expression::Missing(_) => {}
1192        }
1193    }
1194
1195    fn scan_arguments(&mut self, arguments: &[crate::syntax::CallArgument]) {
1196        for argument in arguments {
1197            match argument {
1198                crate::syntax::CallArgument::Expression(value) => self.scan_expression(value),
1199                crate::syntax::CallArgument::Spread(value) => self.scan_expression(&value.argument),
1200                crate::syntax::CallArgument::Missing(_) => {}
1201            }
1202        }
1203    }
1204
1205    fn scan_target(&mut self, target: &crate::syntax::AssignmentTargetNode) {
1206        use crate::syntax::{
1207            AssignmentArrayElement, AssignmentTarget, MemberProperty, PropertyName,
1208        };
1209        match target.data() {
1210            AssignmentTarget::Member(value) => {
1211                self.scan_expression(&value.object);
1212                if let MemberProperty::Computed(value) = &value.property {
1213                    self.scan_expression(value);
1214                }
1215            }
1216            AssignmentTarget::Object(value) => {
1217                for property in &value.properties {
1218                    if let PropertyName::Computed(key) = &property.name {
1219                        self.scan_expression(key);
1220                    }
1221                    if let Some(initializer) = &property.initializer {
1222                        self.scan_expression(initializer);
1223                    }
1224                    self.scan_target(&property.target);
1225                }
1226            }
1227            AssignmentTarget::Array(value) => {
1228                for element in &value.elements {
1229                    if let AssignmentArrayElement::Target(value) = element {
1230                        self.scan_target(value);
1231                    }
1232                }
1233            }
1234            AssignmentTarget::Identifier(_) | AssignmentTarget::Missing(_) => {}
1235        }
1236    }
1237}
1238
1239fn import_clause_is_type_only(clause: Option<&crate::syntax::ImportClause>) -> bool {
1240    let Some(clause) = clause else {
1241        return false;
1242    };
1243    clause.default.is_none()
1244        && matches!(
1245            &clause.binding,
1246            Some(ImportBinding::Named(specifiers))
1247                if !specifiers.is_empty()
1248                    && specifiers.iter().all(|specifier| {
1249                        specifier.data().mode == ImportSpecifierMode::TypeOnly
1250                    })
1251        )
1252}
1253
1254fn push_literal_edge(
1255    source: &SourceFile,
1256    edges: &mut Vec<UnresolvedEdge>,
1257    kind: ModuleEdgeKind,
1258    literal: &crate::syntax::StringLiteralNode,
1259) -> Result<(), TextRange> {
1260    let Some(value) = source.token_text(literal.data().token()).and_then(unquote) else {
1261        return Ok(());
1262    };
1263    let specifier = value.to_utf8_strict().map_err(|_| literal.range())?;
1264    edges.push(UnresolvedEdge {
1265        kind,
1266        specifier: Arc::from(specifier),
1267        range: literal.range(),
1268    });
1269    Ok(())
1270}
1271
1272fn unquote(text: &str) -> Option<EcmaString> {
1273    let quote = text.as_bytes().first().copied()?;
1274    if !matches!(quote, b'\'' | b'"') || text.as_bytes().last().copied() != Some(quote) {
1275        return None;
1276    }
1277    let body = &text[1..text.len() - 1];
1278    let bytes = body.as_bytes();
1279    let mut output = EcmaStringBuilder::with_capacity(body.encode_utf16().count());
1280    let mut index = 0;
1281    while index < bytes.len() {
1282        if bytes[index] != b'\\' {
1283            let character = body[index..].chars().next()?;
1284            output.push_code_point(u32::from(character)).ok()?;
1285            index += character.len_utf8();
1286            continue;
1287        }
1288        index += 1;
1289        let escaped = *bytes.get(index)?;
1290        index += 1;
1291        match escaped {
1292            b'b' => output.push_unit(0x0008),
1293            b'f' => output.push_unit(0x000C),
1294            b'n' => output.push_unit(u16::from(b'\n')),
1295            b'r' => output.push_unit(u16::from(b'\r')),
1296            b't' => output.push_unit(u16::from(b'\t')),
1297            b'v' => output.push_unit(0x000B),
1298            b'0' => output.push_unit(0),
1299            b'\n' => {}
1300            b'\r' => {
1301                if bytes.get(index) == Some(&b'\n') {
1302                    index += 1;
1303                }
1304            }
1305            b'x' => {
1306                let value = parse_hex(bytes.get(index..index + 2)?)?;
1307                output.push_unit(value as u16);
1308                index += 2;
1309            }
1310            b'u' if bytes.get(index) == Some(&b'{') => {
1311                let end = bytes[index + 1..].iter().position(|byte| *byte == b'}')? + index + 1;
1312                let value = parse_hex(bytes.get(index + 1..end)?)?;
1313                output.push_code_point(value).ok()?;
1314                index = end + 1;
1315            }
1316            b'u' => {
1317                let value = parse_hex(bytes.get(index..index + 4)?)?;
1318                output.push_unit(value as u16);
1319                index += 4;
1320            }
1321            _ if escaped.is_ascii() => output.push_unit(u16::from(escaped)),
1322            _ => {
1323                let character = body[index - 1..].chars().next()?;
1324                output.push_code_point(u32::from(character)).ok()?;
1325                index += character.len_utf8() - 1;
1326            }
1327        }
1328    }
1329    Some(output.finish())
1330}
1331
1332fn parse_hex(bytes: &[u8]) -> Option<u32> {
1333    if bytes.is_empty() {
1334        return None;
1335    }
1336    bytes.iter().try_fold(0_u32, |value, byte| {
1337        char::from(*byte)
1338            .to_digit(16)
1339            .map(|digit| value * 16 + digit)
1340    })
1341}
1342
1343fn script_kind(path: &Path) -> Option<ScriptKind> {
1344    match path.extension()?.to_str()? {
1345        "ts" | "mts" | "cts" => Some(ScriptKind::TypeScript),
1346        "tsx" => Some(ScriptKind::TypeScriptReact),
1347        "js" | "mjs" | "cjs" => Some(ScriptKind::JavaScript),
1348        "jsx" => Some(ScriptKind::JavaScriptReact),
1349        "json" => Some(ScriptKind::Json),
1350        _ => None,
1351    }
1352}
1353
1354fn is_declaration_path(path: &Path) -> bool {
1355    let name = path
1356        .file_name()
1357        .and_then(|name| name.to_str())
1358        .unwrap_or_default();
1359    name.ends_with(".d.ts") || name.ends_with(".d.mts") || name.ends_with(".d.cts")
1360}
1361
1362fn pattern_capture<'a>(pattern: &str, specifier: &'a str) -> Option<&'a str> {
1363    let Some(star) = pattern.find('*') else {
1364        return (pattern == specifier).then_some("");
1365    };
1366    let (prefix, suffix_with_star) = pattern.split_at(star);
1367    let suffix = &suffix_with_star[1..];
1368    specifier.strip_prefix(prefix)?.strip_suffix(suffix)
1369}
1370
1371fn split_package_specifier(specifier: &str) -> Option<(&str, String)> {
1372    if specifier.is_empty() || specifier.starts_with('/') || specifier.starts_with('#') {
1373        return None;
1374    }
1375    let component_count = if specifier.starts_with('@') { 2 } else { 1 };
1376    let mut boundaries = specifier.match_indices('/').map(|(index, _)| index);
1377    let boundary = if component_count == 1 {
1378        boundaries.next()
1379    } else {
1380        boundaries.nth(1)
1381    };
1382    match boundary {
1383        Some(index) => Some((
1384            &specifier[..index],
1385            format!("./{}", &specifier[index + 1..]),
1386        )),
1387        None if component_count == 1 || specifier.matches('/').count() == 1 => {
1388            Some((specifier, ".".to_owned()))
1389        }
1390        None => None,
1391    }
1392}
1393
1394fn diagnostic(
1395    importer: &Path,
1396    specifier: &str,
1397    kind: ModuleEdgeKind,
1398    range: TextRange,
1399) -> UnresolvedModuleDiagnostic {
1400    UnresolvedModuleDiagnostic {
1401        importer: Arc::from(importer),
1402        specifier: Arc::from(specifier),
1403        kind,
1404        range,
1405    }
1406}
1407
1408const fn edge_kind(flavor: ResolutionFlavor) -> ModuleEdgeKind {
1409    match flavor {
1410        ResolutionFlavor::Runtime => ModuleEdgeKind::StaticRuntime,
1411        ResolutionFlavor::Types => ModuleEdgeKind::TypeOnly,
1412    }
1413}
1414
1415/// Compiler-only identity and resolved-edge provenance for one executable module.
1416#[derive(Clone, Debug)]
1417pub struct ExecutableModuleProvenance {
1418    module: ModuleId,
1419    source: SourceIdentity,
1420    edges: Arc<[ModuleEdge]>,
1421}
1422
1423impl ExecutableModuleProvenance {
1424    #[must_use]
1425    pub const fn module(&self) -> ModuleId {
1426        self.module
1427    }
1428
1429    #[must_use]
1430    pub const fn source(&self) -> &SourceIdentity {
1431        &self.source
1432    }
1433
1434    /// Canonical compiler edges, including identities intentionally absent from the wire format.
1435    #[must_use]
1436    pub fn edges(&self) -> &[ModuleEdge] {
1437        &self.edges
1438    }
1439
1440    pub fn type_only_edges(&self) -> impl Iterator<Item = &ModuleEdge> {
1441        self.edges
1442            .iter()
1443            .filter(|edge| edge.kind() == ModuleEdgeKind::TypeOnly)
1444    }
1445}
1446
1447/// The compiler's sole executable product: one verified wire program plus non-wire provenance.
1448#[derive(Clone, Debug)]
1449pub struct ExecutableProgram {
1450    wire: BytecodeProgram<Verified>,
1451    provenance: Vec<ExecutableModuleProvenance>,
1452}
1453
1454impl ExecutableProgram {
1455    #[must_use]
1456    pub const fn wire(&self) -> &BytecodeProgram<Verified> {
1457        &self.wire
1458    }
1459
1460    #[must_use]
1461    pub fn provenance(&self) -> &[ExecutableModuleProvenance] {
1462        &self.provenance
1463    }
1464}
1465
1466#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1467pub enum ProgramLowerPhase {
1468    Frontend,
1469    Metadata,
1470    Module,
1471    Link,
1472}
1473
1474#[derive(Clone, Debug, Eq, PartialEq)]
1475pub enum ProgramLowerErrorKind {
1476    FrontendEntrypointMismatch {
1477        resolved: SourceId,
1478        frontend: SourceId,
1479    },
1480    MissingFrontend {
1481        source: SourceId,
1482    },
1483    UnexpectedFrontend {
1484        source: SourceId,
1485    },
1486    InvalidModuleName,
1487    IllFormedMetadataString,
1488    MissingRuntimeEdge {
1489        specifier: String,
1490    },
1491    ConflictingRuntimeEdge {
1492        specifier: String,
1493    },
1494    Lower(LowerError),
1495    Link(ProgramVerifyError),
1496}
1497
1498/// A whole-program lowering failure anchored to a canonical module path and phase.
1499#[derive(Clone, Debug, Eq, PartialEq)]
1500pub struct ProgramLowerError {
1501    pub module: PathBuf,
1502    pub phase: ProgramLowerPhase,
1503    pub kind: ProgramLowerErrorKind,
1504}
1505
1506impl fmt::Display for ProgramLowerError {
1507    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1508        write!(
1509            formatter,
1510            "program lowering failed in {} during {:?}: {:?}",
1511            self.module.display(),
1512            self.phase,
1513            self.kind
1514        )
1515    }
1516}
1517
1518impl std::error::Error for ProgramLowerError {
1519    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1520        match &self.kind {
1521            ProgramLowerErrorKind::Lower(error) => Some(error),
1522            ProgramLowerErrorKind::Link(error) => Some(error),
1523            _ => None,
1524        }
1525    }
1526}
1527
1528#[derive(Clone)]
1529struct RawEdge {
1530    specifier: String,
1531    target: EdgeTarget,
1532    external_identity: Option<String>,
1533    kind: EdgeKind,
1534}
1535
1536#[derive(Clone)]
1537enum RawBindingKind {
1538    Hoisted,
1539    Lexical,
1540    Imported { edge: EdgeId, name: String },
1541    Namespace { edge: EdgeId },
1542}
1543
1544#[derive(Clone)]
1545struct RawBinding {
1546    name: String,
1547    kind: RawBindingKind,
1548}
1549
1550#[derive(Clone)]
1551enum RawExportSource {
1552    Local(String),
1553    Indirect { edge: EdgeId, name: String },
1554}
1555
1556#[derive(Clone)]
1557struct RawExport {
1558    name: String,
1559    source: RawExportSource,
1560}
1561
1562struct RawModule {
1563    name: String,
1564    edges: Vec<RawEdge>,
1565    bindings: Vec<RawBinding>,
1566    exports: Vec<RawExport>,
1567    stars: Vec<EdgeId>,
1568}
1569
1570#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
1571enum ExportOrigin {
1572    Local(ModuleId, String),
1573    Indirect(ModuleId, String),
1574    External(String, String),
1575}
1576
1577/// Lowers one canonical resolved program and its matching frontend products.
1578///
1579/// Static linkage becomes live metadata only. Dynamic imports remain instructions
1580/// backed by dynamic-capable edges.
1581///
1582/// # Errors
1583/// Returns a path- and phase-typed failure for frontend mismatch, metadata construction,
1584/// module lowering/verification, or final program linking.
1585pub fn lower_program(
1586    resolved: &ResolvedProgram,
1587    frontend: &ProgramFrontendOutput,
1588    options: LowerOptions,
1589) -> Result<ExecutableProgram, ProgramLowerError> {
1590    if frontend.entrypoint_id() != resolved.entrypoint_id() {
1591        return Err(program_lower_error(
1592            resolved.entrypoint().path(),
1593            ProgramLowerPhase::Frontend,
1594            ProgramLowerErrorKind::FrontendEntrypointMismatch {
1595                resolved: resolved.entrypoint_id(),
1596                frontend: frontend.entrypoint_id(),
1597            },
1598        ));
1599    }
1600    for output in frontend.modules() {
1601        let source = output.source_file().source_id();
1602        if resolved.module(source).is_none() {
1603            return Err(program_lower_error(
1604                resolved.entrypoint().path(),
1605                ProgramLowerPhase::Frontend,
1606                ProgramLowerErrorKind::UnexpectedFrontend { source },
1607            ));
1608        }
1609    }
1610
1611    let module_ids: HashMap<_, _> = resolved
1612        .modules()
1613        .iter()
1614        .enumerate()
1615        .map(|(index, module)| (module.source_id(), ModuleId::new(index as u32)))
1616        .collect();
1617    let mut raw_modules = Vec::with_capacity(resolved.modules().len());
1618    for module in resolved.modules() {
1619        let output = frontend.module(module.source_id()).ok_or_else(|| {
1620            program_lower_error(
1621                module.path(),
1622                ProgramLowerPhase::Frontend,
1623                ProgramLowerErrorKind::MissingFrontend {
1624                    source: module.source_id(),
1625                },
1626            )
1627        })?;
1628        let name = normalized_module_name(resolved.root(), module.path()).ok_or_else(|| {
1629            program_lower_error(
1630                module.path(),
1631                ProgramLowerPhase::Metadata,
1632                ProgramLowerErrorKind::InvalidModuleName,
1633            )
1634        })?;
1635        raw_modules.push(collect_raw_module(
1636            module,
1637            output.source_file(),
1638            name,
1639            &module_ids,
1640        )?);
1641    }
1642    expand_star_exports(&mut raw_modules);
1643
1644    let mut linked_modules = Vec::with_capacity(raw_modules.len());
1645    let mut provenance = Vec::with_capacity(raw_modules.len());
1646    for (index, (resolved_module, raw)) in resolved
1647        .modules()
1648        .iter()
1649        .zip(raw_modules.iter())
1650        .enumerate()
1651    {
1652        let file = frontend
1653            .module(resolved_module.source_id())
1654            .expect("frontend presence checked above")
1655            .source_file();
1656        let strings = linkage_strings(raw);
1657        let code = lower::assemble_program_module(file, options, &strings)
1658            .and_then(|module| {
1659                module.verify().map_err(|error| LowerError {
1660                    source: file.source_id(),
1661                    range: file.range(),
1662                    kind: lower::LowerErrorKind::Verify(error),
1663                })
1664            })
1665            .map_err(|error| {
1666                program_lower_error(
1667                    resolved_module.path(),
1668                    ProgramLowerPhase::Module,
1669                    ProgramLowerErrorKind::Lower(error),
1670                )
1671            })?;
1672        linked_modules.push(materialize_program_module(code, raw));
1673        provenance.push(ExecutableModuleProvenance {
1674            module: ModuleId::new(index as u32),
1675            source: resolved_module.identity().clone(),
1676            edges: Arc::from(resolved_module.dependencies()),
1677        });
1678    }
1679    let entry = module_ids[&resolved.entrypoint_id()];
1680    let wire = BytecodeProgram::link(linked_modules, entry).map_err(|error| {
1681        let path = error
1682            .module
1683            .and_then(|module| resolved.modules().get(module.get() as usize))
1684            .map_or_else(|| resolved.entrypoint().path(), ResolvedModule::path);
1685        program_lower_error(
1686            path,
1687            ProgramLowerPhase::Link,
1688            ProgramLowerErrorKind::Link(error),
1689        )
1690    })?;
1691    Ok(ExecutableProgram { wire, provenance })
1692}
1693
1694fn collect_raw_module(
1695    module: &ResolvedModule,
1696    file: &SourceFile,
1697    name: String,
1698    module_ids: &HashMap<SourceId, ModuleId>,
1699) -> Result<RawModule, ProgramLowerError> {
1700    let mut edges: Vec<RawEdge> = Vec::new();
1701    let mut edge_ids: HashMap<String, EdgeId> = HashMap::new();
1702    for dependency in module
1703        .dependencies()
1704        .iter()
1705        .filter(|edge| edge.kind() != ModuleEdgeKind::TypeOnly)
1706    {
1707        let kind = match dependency.kind() {
1708            ModuleEdgeKind::StaticRuntime => EdgeKind::Static,
1709            ModuleEdgeKind::DynamicRuntime => EdgeKind::Dynamic,
1710            ModuleEdgeKind::TypeOnly => unreachable!("type-only edges were filtered"),
1711        };
1712        let (target, external_identity) = match dependency.target() {
1713            ModuleTarget::Local(source) => (EdgeTarget::Local(module_ids[source]), None),
1714            ModuleTarget::External(identity) => (EdgeTarget::External, Some(identity.to_string())),
1715        };
1716        if let Some(existing) = edge_ids.get(dependency.specifier()).copied() {
1717            let edge: &mut RawEdge = &mut edges[existing.get() as usize];
1718            if edge.target != target || edge.external_identity != external_identity {
1719                return Err(program_lower_error(
1720                    module.path(),
1721                    ProgramLowerPhase::Metadata,
1722                    ProgramLowerErrorKind::ConflictingRuntimeEdge {
1723                        specifier: dependency.specifier().to_owned(),
1724                    },
1725                ));
1726            }
1727            edge.kind = edge.kind.union(kind);
1728            continue;
1729        }
1730        let id = EdgeId::new(edges.len() as u32);
1731        edge_ids.insert(dependency.specifier().to_owned(), id);
1732        edges.push(RawEdge {
1733            specifier: dependency.specifier().to_owned(),
1734            target,
1735            external_identity,
1736            kind,
1737        });
1738    }
1739
1740    let edge = |specifier: String| {
1741        edge_ids.get(&specifier).copied().ok_or_else(|| {
1742            program_lower_error(
1743                module.path(),
1744                ProgramLowerPhase::Metadata,
1745                ProgramLowerErrorKind::MissingRuntimeEdge { specifier },
1746            )
1747        })
1748    };
1749    let mut bindings = Vec::new();
1750    let mut hoisted = Vec::new();
1751    lower::collect_var_names(file, file.statements(), &mut hoisted);
1752    bindings.extend(hoisted.into_iter().map(|name| RawBinding {
1753        name,
1754        kind: RawBindingKind::Hoisted,
1755    }));
1756    let mut exports = Vec::new();
1757    let mut stars = Vec::new();
1758    for statement in file.statements() {
1759        collect_top_level_statement(
1760            module,
1761            file,
1762            statement.data(),
1763            &edge,
1764            &mut bindings,
1765            &mut exports,
1766            &mut stars,
1767        )?;
1768    }
1769    let mut hoisted_names = HashSet::new();
1770    bindings.retain(|binding| {
1771        !matches!(binding.kind, RawBindingKind::Hoisted)
1772            || hoisted_names.insert(binding.name.clone())
1773    });
1774    let binding_names: HashSet<_> = bindings
1775        .iter()
1776        .map(|binding| binding.name.as_str())
1777        .collect();
1778    exports.retain(|export| match &export.source {
1779        RawExportSource::Local(name) => binding_names.contains(name.as_str()),
1780        RawExportSource::Indirect { .. } => true,
1781    });
1782    Ok(RawModule {
1783        name,
1784        edges,
1785        bindings,
1786        exports,
1787        stars,
1788    })
1789}
1790
1791fn collect_top_level_statement(
1792    module: &ResolvedModule,
1793    file: &SourceFile,
1794    statement: &Statement,
1795    edge: &impl Fn(String) -> Result<EdgeId, ProgramLowerError>,
1796    bindings: &mut Vec<RawBinding>,
1797    exports: &mut Vec<RawExport>,
1798    stars: &mut Vec<EdgeId>,
1799) -> Result<(), ProgramLowerError> {
1800    match statement {
1801        Statement::Import(import) if !import.type_only => {
1802            let runtime = import.clause.as_ref().is_none_or(|clause| {
1803                clause.default.is_some()
1804                    || !matches!(
1805                        &clause.binding,
1806                        Some(ImportBinding::Named(specifiers))
1807                            if specifiers.iter().all(|specifier| {
1808                                specifier.data().mode == ImportSpecifierMode::TypeOnly
1809                            })
1810                    )
1811            });
1812            if !runtime {
1813                return Ok(());
1814            }
1815            let edge_id = edge(metadata_string_literal(module, file, &import.source)?)?;
1816            if let Some(clause) = &import.clause {
1817                if let Some(default) = &clause.default {
1818                    bindings.push(RawBinding {
1819                        name: identifier(file, default),
1820                        kind: RawBindingKind::Imported {
1821                            edge: edge_id,
1822                            name: "default".to_owned(),
1823                        },
1824                    });
1825                }
1826                match &clause.binding {
1827                    Some(ImportBinding::Namespace(local)) => bindings.push(RawBinding {
1828                        name: identifier(file, local),
1829                        kind: RawBindingKind::Namespace { edge: edge_id },
1830                    }),
1831                    Some(ImportBinding::Named(specifiers)) => {
1832                        for specifier in specifiers {
1833                            let specifier = specifier.data();
1834                            if specifier.mode == ImportSpecifierMode::TypeOnly {
1835                                continue;
1836                            }
1837                            bindings.push(RawBinding {
1838                                name: identifier(file, &specifier.local),
1839                                kind: RawBindingKind::Imported {
1840                                    edge: edge_id,
1841                                    name: metadata_export_name(module, file, &specifier.imported)?,
1842                                },
1843                            });
1844                        }
1845                    }
1846                    None => {}
1847                }
1848            }
1849        }
1850        Statement::Variable(declaration)
1851            if matches!(declaration.kind, VariableKind::Let | VariableKind::Const) =>
1852        {
1853            for declarator in &declaration.declarations {
1854                let mut names = Vec::new();
1855                lower::collect_pattern_names(file, &declarator.data().binding, &mut names);
1856                bindings.extend(names.into_iter().map(|name| RawBinding {
1857                    name,
1858                    kind: RawBindingKind::Lexical,
1859                }));
1860            }
1861        }
1862        Statement::Function(declaration) => {
1863            if declaration.function.body.is_some()
1864                && let Some(name) = &declaration.function.name
1865            {
1866                bindings.push(RawBinding {
1867                    name: identifier(file, name),
1868                    kind: RawBindingKind::Hoisted,
1869                });
1870            }
1871        }
1872        Statement::Class(class) => {
1873            if let Some(name) = &class.name {
1874                bindings.push(RawBinding {
1875                    name: identifier(file, name),
1876                    kind: RawBindingKind::Lexical,
1877                });
1878            }
1879        }
1880        Statement::Export(export) => {
1881            collect_export(module, file, export, edge, bindings, exports, stars)?
1882        }
1883        _ => {}
1884    }
1885    Ok(())
1886}
1887
1888fn collect_export(
1889    module: &ResolvedModule,
1890    file: &SourceFile,
1891    declaration: &ExportDeclaration,
1892    edge: &impl Fn(String) -> Result<EdgeId, ProgramLowerError>,
1893    bindings: &mut Vec<RawBinding>,
1894    exports: &mut Vec<RawExport>,
1895    stars: &mut Vec<EdgeId>,
1896) -> Result<(), ProgramLowerError> {
1897    match declaration {
1898        ExportDeclaration::Named(ExportNamedDeclaration::Declaration(statement)) => {
1899            collect_top_level_statement(
1900                module,
1901                file,
1902                statement.data(),
1903                edge,
1904                bindings,
1905                exports,
1906                stars,
1907            )?;
1908            let has_runtime_value = !matches!(
1909                statement.data(),
1910                Statement::Function(declaration) if declaration.function.body.is_none()
1911            );
1912            if has_runtime_value {
1913                for name in lower::declared_names(file, statement) {
1914                    exports.push(RawExport {
1915                        name: name.clone(),
1916                        source: RawExportSource::Local(name),
1917                    });
1918                }
1919            }
1920        }
1921        ExportDeclaration::Named(ExportNamedDeclaration::Specifiers {
1922            type_only,
1923            specifiers,
1924            source,
1925            ..
1926        }) if !type_only => {
1927            if let Some(source) = source {
1928                let edge_id = edge(metadata_string_literal(module, file, source)?)?;
1929                for specifier in specifiers {
1930                    let specifier = specifier.data();
1931                    if specifier.mode == ExportSpecifierMode::TypeOnly {
1932                        continue;
1933                    }
1934                    exports.push(RawExport {
1935                        name: metadata_export_name(module, file, &specifier.exported)?,
1936                        source: RawExportSource::Indirect {
1937                            edge: edge_id,
1938                            name: metadata_export_name(module, file, &specifier.local)?,
1939                        },
1940                    });
1941                }
1942            } else {
1943                for specifier in specifiers {
1944                    let specifier = specifier.data();
1945                    if specifier.mode == ExportSpecifierMode::TypeOnly {
1946                        continue;
1947                    }
1948                    exports.push(RawExport {
1949                        name: metadata_export_name(module, file, &specifier.exported)?,
1950                        source: RawExportSource::Local(metadata_export_name(
1951                            module,
1952                            file,
1953                            &specifier.local,
1954                        )?),
1955                    });
1956                }
1957            }
1958        }
1959        ExportDeclaration::All(all) if !all.type_only => {
1960            let edge_id = edge(metadata_string_literal(module, file, &all.source)?)?;
1961            if let Some(exported) = &all.exported {
1962                let exported = metadata_export_name(module, file, exported)?;
1963                let binding = format!("*namespace:{exported}*");
1964                bindings.push(RawBinding {
1965                    name: binding.clone(),
1966                    kind: RawBindingKind::Namespace { edge: edge_id },
1967                });
1968                exports.push(RawExport {
1969                    name: exported,
1970                    source: RawExportSource::Local(binding),
1971                });
1972            } else {
1973                stars.push(edge_id);
1974            }
1975        }
1976        ExportDeclaration::Default(default) => {
1977            let kind = match &default.value {
1978                ExportDefaultValue::Function(function) if function.body.is_some() => {
1979                    if let Some(name) = &function.name {
1980                        bindings.push(RawBinding {
1981                            name: identifier(file, name),
1982                            kind: RawBindingKind::Hoisted,
1983                        });
1984                    }
1985                    RawBindingKind::Hoisted
1986                }
1987                ExportDefaultValue::Class(class) => {
1988                    if let Some(name) = &class.name {
1989                        bindings.push(RawBinding {
1990                            name: identifier(file, name),
1991                            kind: RawBindingKind::Lexical,
1992                        });
1993                    }
1994                    RawBindingKind::Lexical
1995                }
1996                ExportDefaultValue::Expression(_) => RawBindingKind::Lexical,
1997                _ => return Ok(()),
1998            };
1999            bindings.push(RawBinding {
2000                name: "*default*".to_owned(),
2001                kind,
2002            });
2003            exports.push(RawExport {
2004                name: "default".to_owned(),
2005                source: RawExportSource::Local("*default*".to_owned()),
2006            });
2007        }
2008        _ => {}
2009    }
2010    Ok(())
2011}
2012
2013fn expand_star_exports(modules: &mut [RawModule]) {
2014    let explicit: Vec<BTreeSet<String>> = modules
2015        .iter()
2016        .map(|module| {
2017            module
2018                .exports
2019                .iter()
2020                .map(|export| export.name.clone())
2021                .collect()
2022        })
2023        .collect();
2024    let mut origins: Vec<BTreeMap<String, BTreeSet<ExportOrigin>>> = modules
2025        .iter()
2026        .enumerate()
2027        .map(|(index, module)| {
2028            module
2029                .exports
2030                .iter()
2031                .map(|export| {
2032                    (
2033                        export.name.clone(),
2034                        BTreeSet::from([canonical_export_origin(
2035                            modules,
2036                            export_origin(ModuleId::new(index as u32), module, export),
2037                            &mut BTreeSet::new(),
2038                        )]),
2039                    )
2040                })
2041                .collect()
2042        })
2043        .collect();
2044    loop {
2045        let previous = origins.clone();
2046        let mut changed = false;
2047        for (index, module) in modules.iter().enumerate() {
2048            for star in &module.stars {
2049                let EdgeTarget::Local(target) = module.edges[star.get() as usize].target else {
2050                    continue;
2051                };
2052                for (name, candidates) in &previous[target.get() as usize] {
2053                    if name == "default" || explicit[index].contains(name) {
2054                        continue;
2055                    }
2056                    let entry = origins[index].entry(name.clone()).or_default();
2057                    let before = entry.len();
2058                    entry.extend(candidates.iter().cloned());
2059                    changed |= entry.len() != before;
2060                }
2061            }
2062        }
2063        if !changed {
2064            break;
2065        }
2066    }
2067    for (index, module) in modules.iter_mut().enumerate() {
2068        for (name, candidates) in &origins[index] {
2069            if explicit[index].contains(name) || candidates.len() != 1 {
2070                continue;
2071            }
2072            let origin = candidates.first().expect("singleton candidate");
2073            if let Some(edge) = module.stars.iter().copied().find(|edge| {
2074                let EdgeTarget::Local(target) = module.edges[edge.get() as usize].target else {
2075                    return false;
2076                };
2077                origins[target.get() as usize]
2078                    .get(name)
2079                    .is_some_and(|origins| origins.contains(origin))
2080            }) {
2081                module.exports.push(RawExport {
2082                    name: name.clone(),
2083                    source: RawExportSource::Indirect {
2084                        edge,
2085                        name: name.clone(),
2086                    },
2087                });
2088            }
2089        }
2090    }
2091}
2092
2093fn export_origin(module_id: ModuleId, module: &RawModule, export: &RawExport) -> ExportOrigin {
2094    match &export.source {
2095        RawExportSource::Local(name) => {
2096            if let Some(binding) = module.bindings.iter().find(|binding| binding.name == *name) {
2097                match &binding.kind {
2098                    RawBindingKind::Imported { edge, name } => match module.edges
2099                        [edge.get() as usize]
2100                        .target
2101                    {
2102                        EdgeTarget::Local(target) => ExportOrigin::Indirect(target, name.clone()),
2103                        EdgeTarget::External => ExportOrigin::External(
2104                            module.edges[edge.get() as usize]
2105                                .external_identity
2106                                .clone()
2107                                .unwrap_or_else(|| {
2108                                    module.edges[edge.get() as usize].specifier.clone()
2109                                }),
2110                            name.clone(),
2111                        ),
2112                    },
2113                    _ => ExportOrigin::Local(module_id, name.clone()),
2114                }
2115            } else {
2116                ExportOrigin::Local(module_id, name.clone())
2117            }
2118        }
2119        RawExportSource::Indirect { edge, name } => {
2120            match module.edges[edge.get() as usize].target {
2121                EdgeTarget::Local(target) => ExportOrigin::Indirect(target, name.clone()),
2122                EdgeTarget::External => ExportOrigin::External(
2123                    module.edges[edge.get() as usize]
2124                        .external_identity
2125                        .clone()
2126                        .unwrap_or_else(|| module.edges[edge.get() as usize].specifier.clone()),
2127                    name.clone(),
2128                ),
2129            }
2130        }
2131    }
2132}
2133
2134fn canonical_export_origin(
2135    modules: &[RawModule],
2136    origin: ExportOrigin,
2137    visited: &mut BTreeSet<(ModuleId, String)>,
2138) -> ExportOrigin {
2139    let ExportOrigin::Indirect(module_id, name) = &origin else {
2140        return origin;
2141    };
2142    if !visited.insert((*module_id, name.clone())) {
2143        return origin;
2144    }
2145    let module = &modules[module_id.get() as usize];
2146    let Some(export) = module.exports.iter().find(|export| export.name == *name) else {
2147        return origin;
2148    };
2149    let next = export_origin(*module_id, module, export);
2150    if next == origin {
2151        origin
2152    } else {
2153        canonical_export_origin(modules, next, visited)
2154    }
2155}
2156
2157fn linkage_strings(module: &RawModule) -> Vec<String> {
2158    let mut strings = Vec::new();
2159    strings.push(module.name.clone());
2160    strings.extend(module.edges.iter().map(|edge| edge.specifier.clone()));
2161    for binding in &module.bindings {
2162        strings.push(binding.name.clone());
2163        if let RawBindingKind::Imported { name, .. } = &binding.kind {
2164            strings.push(name.clone());
2165        }
2166    }
2167    for export in &module.exports {
2168        strings.push(export.name.clone());
2169        if let RawExportSource::Indirect { name, .. } = &export.source {
2170            strings.push(name.clone());
2171        }
2172    }
2173    strings
2174}
2175
2176fn materialize_program_module(
2177    code: bamts_bytecode::Module<Verified>,
2178    raw: &RawModule,
2179) -> ProgramModule<Verified> {
2180    let constant = |value: &str| {
2181        ConstantId::new(
2182            code.constants()
2183                .iter()
2184                .position(|constant| matches!(constant, Constant::String(text) if text.as_units().iter().copied().eq(value.encode_utf16())))
2185                .expect("all linkage strings were interned before verification") as u32,
2186        )
2187    };
2188    let binding_ids: HashMap<_, _> = raw
2189        .bindings
2190        .iter()
2191        .enumerate()
2192        .map(|(index, binding)| (binding.name.as_str(), BindingId::new(index as u32)))
2193        .collect();
2194    let name = constant(&raw.name);
2195    let edges = raw
2196        .edges
2197        .iter()
2198        .map(|edge| Edge {
2199            specifier: constant(&edge.specifier),
2200            target: edge.target,
2201            kind: edge.kind,
2202        })
2203        .collect();
2204    let bindings = raw
2205        .bindings
2206        .iter()
2207        .map(|binding| Binding {
2208            name: constant(&binding.name),
2209            kind: match &binding.kind {
2210                RawBindingKind::Hoisted => BindingKind::Hoisted,
2211                RawBindingKind::Lexical => BindingKind::Lexical,
2212                RawBindingKind::Imported { edge, name } => BindingKind::Imported {
2213                    edge: *edge,
2214                    name: constant(name),
2215                },
2216                RawBindingKind::Namespace { edge } => BindingKind::Namespace { edge: *edge },
2217            },
2218        })
2219        .collect();
2220    let exports = raw
2221        .exports
2222        .iter()
2223        .map(|export| Export {
2224            name: constant(&export.name),
2225            source: match &export.source {
2226                RawExportSource::Local(name) => ExportSource::Local(binding_ids[name.as_str()]),
2227                RawExportSource::Indirect { edge, name } => ExportSource::Indirect {
2228                    edge: *edge,
2229                    name: constant(name),
2230                },
2231            },
2232        })
2233        .collect();
2234    ProgramModule {
2235        name,
2236        code,
2237        edges,
2238        bindings,
2239        exports,
2240    }
2241}
2242
2243fn normalized_module_name(root: &ProjectRoot, path: &Path) -> Option<String> {
2244    let relative = path.strip_prefix(root.path()).ok()?;
2245    let mut name = String::new();
2246    for component in relative.components() {
2247        if !name.is_empty() {
2248            name.push('/');
2249        }
2250        name.push_str(component.as_os_str().to_str()?);
2251    }
2252    (!name.is_empty()).then_some(name)
2253}
2254
2255fn identifier(file: &SourceFile, node: &crate::syntax::IdentifierNode) -> String {
2256    file.token_text(node.data().token())
2257        .expect("parser identifier range belongs to its source")
2258        .to_owned()
2259}
2260
2261fn metadata_string_literal(
2262    module: &ResolvedModule,
2263    file: &SourceFile,
2264    node: &crate::syntax::StringLiteralNode,
2265) -> Result<String, ProgramLowerError> {
2266    let value = file
2267        .token_text(node.data().token())
2268        .and_then(unquote)
2269        .ok_or_else(|| malformed_metadata_error(module))?;
2270    value
2271        .to_utf8_strict()
2272        .map_err(|_| malformed_metadata_error(module))
2273}
2274
2275fn metadata_export_name(
2276    module: &ResolvedModule,
2277    file: &SourceFile,
2278    name: &ModuleExportName,
2279) -> Result<String, ProgramLowerError> {
2280    match name {
2281        ModuleExportName::Identifier(identifier_node) => Ok(identifier(file, identifier_node)),
2282        ModuleExportName::String(string) => metadata_string_literal(module, file, string),
2283        ModuleExportName::Missing(_) => Ok(String::new()),
2284    }
2285}
2286
2287fn malformed_metadata_error(module: &ResolvedModule) -> ProgramLowerError {
2288    program_lower_error(
2289        module.path(),
2290        ProgramLowerPhase::Metadata,
2291        ProgramLowerErrorKind::IllFormedMetadataString,
2292    )
2293}
2294
2295fn program_lower_error(
2296    module: &Path,
2297    phase: ProgramLowerPhase,
2298    kind: ProgramLowerErrorKind,
2299) -> ProgramLowerError {
2300    ProgramLowerError {
2301        module: module.to_path_buf(),
2302        phase,
2303        kind,
2304    }
2305}
2306
2307#[cfg(test)]
2308mod tests {
2309    use std::{
2310        fs,
2311        path::{Path, PathBuf},
2312        sync::{
2313            Arc,
2314            atomic::{AtomicU64, Ordering},
2315        },
2316    };
2317
2318    use super::{
2319        ExecutableProgram, ModuleEdgeKind, ModuleTarget, ProgramLoadError, ProgramLoader,
2320        ProgramLowerErrorKind, ProgramLowerPhase, lower_program,
2321    };
2322    use crate::{
2323        lower::LowerOptions,
2324        pipeline::{FrontendMode, compile_program_frontend},
2325        project::{ProjectConfig, ProjectRoot},
2326    };
2327    use bamts_bytecode::{
2328        BindingKind, EcmaString, EdgeKind, EdgeTarget, ExportSource, Instruction, ProgramModule,
2329        ProgramVerifyErrorKind, ResolvedExport, Verified,
2330    };
2331
2332    static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
2333
2334    struct Fixture(PathBuf);
2335
2336    impl Fixture {
2337        fn new() -> Self {
2338            let path = std::env::temp_dir().join(format!(
2339                "bamts-program-{}-{}",
2340                std::process::id(),
2341                NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
2342            ));
2343            fs::create_dir_all(&path).unwrap();
2344            Self(path)
2345        }
2346
2347        fn write(&self, path: &str, source: &str) {
2348            let path = self.0.join(path);
2349            fs::create_dir_all(path.parent().unwrap()).unwrap();
2350            fs::write(path, source).unwrap();
2351        }
2352
2353        fn loader(&self) -> ProgramLoader {
2354            let root = ProjectRoot::new(fs::canonicalize(&self.0).unwrap()).unwrap();
2355            let config = ProjectConfig::parse(&root, self.0.join("tsconfig.json"), "{}").unwrap();
2356            ProgramLoader::new(&root, config.options()).unwrap()
2357        }
2358    }
2359
2360    impl Drop for Fixture {
2361        fn drop(&mut self) {
2362            fs::remove_dir_all(&self.0).unwrap();
2363        }
2364    }
2365
2366    fn lower_fixture(fixture: &Fixture, entrypoint: &str) -> ExecutableProgram {
2367        let resolved = fixture.loader().load(entrypoint).unwrap();
2368        let frontend = compile_program_frontend(&resolved, FrontendMode::Check);
2369        lower_program(
2370            &resolved,
2371            &frontend,
2372            LowerOptions {
2373                javascript_compatibility: true,
2374            },
2375        )
2376        .unwrap()
2377    }
2378
2379    #[test]
2380    fn malformed_surrogate_metadata_is_a_typed_error() {
2381        let fixture = Fixture::new();
2382        fixture.write("main.ts", "const x = 1; export { x as \"\\uD800\" };");
2383        let resolved = fixture.loader().load("main.ts").unwrap();
2384        let frontend = compile_program_frontend(&resolved, FrontendMode::Check);
2385        let error = lower_program(
2386            &resolved,
2387            &frontend,
2388            LowerOptions {
2389                javascript_compatibility: true,
2390            },
2391        )
2392        .expect_err("ill-formed metadata must not reach host String conversion");
2393        assert_eq!(error.phase, ProgramLowerPhase::Metadata);
2394        assert_eq!(error.kind, ProgramLowerErrorKind::IllFormedMetadataString);
2395    }
2396
2397    fn module_name(module: &ProgramModule<Verified>) -> String {
2398        match &module.code().constants()[module.name().get() as usize] {
2399            bamts_bytecode::Constant::String(name) => name
2400                .to_utf8_strict()
2401                .expect("compiler-produced module metadata is well-formed"),
2402            _ => panic!("verified module name is a string"),
2403        }
2404    }
2405
2406    fn module<'a>(program: &'a ExecutableProgram, name: &str) -> &'a ProgramModule<Verified> {
2407        program
2408            .wire()
2409            .modules()
2410            .iter()
2411            .find(|module| module_name(module) == name)
2412            .unwrap_or_else(|| panic!("missing module {name}"))
2413    }
2414
2415    fn constant_string(module: &ProgramModule<Verified>, id: bamts_bytecode::ConstantId) -> String {
2416        match &module.code().constants()[id.get() as usize] {
2417            bamts_bytecode::Constant::String(value) => value
2418                .to_utf8_strict()
2419                .expect("compiler-produced linkage metadata is well-formed"),
2420            _ => panic!("verified linkage constant is a string"),
2421        }
2422    }
2423
2424    fn instructions(module: &ProgramModule<Verified>) -> impl Iterator<Item = Instruction> + '_ {
2425        module
2426            .code()
2427            .functions()
2428            .iter()
2429            .flat_map(|function| function.code().iter().copied())
2430    }
2431
2432    fn names(program: &super::ResolvedProgram) -> Vec<&str> {
2433        program
2434            .modules()
2435            .iter()
2436            .map(|module| module.path().file_name().unwrap().to_str().unwrap())
2437            .collect()
2438    }
2439
2440    #[test]
2441    fn program_lowering_keeps_static_imports_live_without_snapshot_opcodes() {
2442        let fixture = Fixture::new();
2443        fixture.write("dep.ts", "export let value = 1; value = 2;");
2444        fixture.write(
2445            "main.ts",
2446            "import { value as observed } from './dep.js'; export { observed };",
2447        );
2448
2449        let executable = lower_fixture(&fixture, "main.ts");
2450        let main = module(&executable, "main.ts");
2451        assert_eq!(main.edges().len(), 1);
2452        assert_eq!(main.edges()[0].kind, EdgeKind::Static);
2453        let binding = main
2454            .bindings()
2455            .iter()
2456            .find(|binding| constant_string(main, binding.name) == "observed")
2457            .unwrap();
2458        assert!(matches!(binding.kind, BindingKind::Imported { .. }));
2459        assert!(!instructions(main).any(|instruction| matches!(
2460            instruction,
2461            Instruction::Import { .. }
2462                | Instruction::GetProperty { .. }
2463                | Instruction::Export { .. }
2464        )));
2465
2466        let main_id = executable.wire().entry();
2467        let export = main
2468            .exports()
2469            .iter()
2470            .find(|export| constant_string(main, export.name) == "observed")
2471            .unwrap();
2472        assert!(matches!(export.source, ExportSource::Local(_)));
2473        assert!(matches!(
2474            executable.wire().resolve_export(main_id, &EcmaString::from_utf8("observed")),
2475            Some(ResolvedExport::Local { module, .. })
2476                if module != main_id
2477        ));
2478    }
2479
2480    #[test]
2481    fn program_lowering_records_namespace_imports() {
2482        let fixture = Fixture::new();
2483        fixture.write("dep.ts", "export const value = 1; export default 2;");
2484        fixture.write(
2485            "main.ts",
2486            "import fallback, * as namespace from './dep.js'; fallback; namespace.value;",
2487        );
2488
2489        let executable = lower_fixture(&fixture, "main.ts");
2490        let main = module(&executable, "main.ts");
2491        assert!(main.bindings().iter().any(|binding| {
2492            constant_string(main, binding.name) == "namespace"
2493                && matches!(binding.kind, BindingKind::Namespace { .. })
2494        }));
2495        assert!(main.bindings().iter().any(|binding| {
2496            constant_string(main, binding.name) == "fallback"
2497                && matches!(
2498                    binding.kind,
2499                    BindingKind::Imported { name, .. }
2500                        if constant_string(main, name) == "default"
2501                )
2502        }));
2503    }
2504
2505    #[test]
2506    fn program_lowering_resolves_alias_reexports() {
2507        let fixture = Fixture::new();
2508        fixture.write("dep.ts", "export const original = 1;");
2509        fixture.write("main.ts", "export { original as renamed } from './dep.js';");
2510
2511        let executable = lower_fixture(&fixture, "main.ts");
2512        let main = module(&executable, "main.ts");
2513        let export = main
2514            .exports()
2515            .iter()
2516            .find(|export| constant_string(main, export.name) == "renamed")
2517            .unwrap();
2518        assert!(matches!(export.source, ExportSource::Indirect { .. }));
2519        assert!(matches!(
2520            executable
2521                .wire()
2522                .resolve_export(executable.wire().entry(), &EcmaString::from_utf8("renamed")),
2523            Some(ResolvedExport::Local { module, .. })
2524                if module != executable.wire().entry()
2525        ));
2526    }
2527
2528    #[test]
2529    fn program_lowering_omits_ambiguous_star_exports() {
2530        let fixture = Fixture::new();
2531        fixture.write(
2532            "a.ts",
2533            "export const collision = 1; export const onlyA = 1;",
2534        );
2535        fixture.write(
2536            "b.ts",
2537            "export const collision = 2; export const onlyB = 2;",
2538        );
2539        fixture.write("main.ts", "export * from './a.js'; export * from './b.js';");
2540
2541        let executable = lower_fixture(&fixture, "main.ts");
2542        let main = module(&executable, "main.ts");
2543        let names: Vec<_> = main
2544            .exports()
2545            .iter()
2546            .map(|export| constant_string(main, export.name))
2547            .collect();
2548        assert!(names.iter().any(|name| name == "onlyA"));
2549        assert!(names.iter().any(|name| name == "onlyB"));
2550        assert!(!names.iter().any(|name| name == "collision"));
2551    }
2552
2553    #[test]
2554    fn program_lowering_keeps_diamond_star_reexports_unambiguous() {
2555        let fixture = Fixture::new();
2556        fixture.write("a.ts", "export const value = 1;");
2557        fixture.write("b.ts", "export { value } from './a.js';");
2558        fixture.write("main.ts", "export * from './a.js'; export * from './b.js';");
2559
2560        let executable = lower_fixture(&fixture, "main.ts");
2561        let main = module(&executable, "main.ts");
2562        assert_eq!(
2563            main.exports()
2564                .iter()
2565                .filter(|export| constant_string(main, export.name) == "value")
2566                .count(),
2567            1
2568        );
2569        assert!(
2570            executable
2571                .wire()
2572                .resolve_export(executable.wire().entry(), &EcmaString::from_utf8("value"))
2573                .is_some()
2574        );
2575    }
2576
2577    #[test]
2578    fn program_lowering_canonicalizes_external_reexport_identity() {
2579        let fixture = Fixture::new();
2580        fixture.write("a.ts", "export { readFile } from 'node:fs';");
2581        fixture.write("b.ts", "export { readFile } from 'node:fs';");
2582        fixture.write("main.ts", "export * from './a.js'; export * from './b.js';");
2583
2584        let executable = lower_fixture(&fixture, "main.ts");
2585        let main = module(&executable, "main.ts");
2586        assert_eq!(
2587            main.exports()
2588                .iter()
2589                .filter(|export| constant_string(main, export.name) == "readFile")
2590                .count(),
2591            1
2592        );
2593        assert!(matches!(
2594            executable.wire().resolve_export(
2595                executable.wire().entry(),
2596                &EcmaString::from_utf8("readFile")
2597            ),
2598            Some(ResolvedExport::External { .. })
2599        ));
2600    }
2601
2602    #[test]
2603    fn program_lowering_materializes_default_expression_binding() {
2604        let fixture = Fixture::new();
2605        fixture.write("main.ts", "export default 1 + 2;");
2606
2607        let executable = lower_fixture(&fixture, "main.ts");
2608        let main = module(&executable, "main.ts");
2609        let binding_index = main
2610            .bindings()
2611            .iter()
2612            .position(|binding| constant_string(main, binding.name) == "*default*")
2613            .unwrap();
2614        assert_eq!(main.bindings()[binding_index].kind, BindingKind::Lexical);
2615        assert!(main.exports().iter().any(|export| {
2616            constant_string(main, export.name) == "default"
2617                && export.source
2618                    == ExportSource::Local(bamts_bytecode::BindingId::new(binding_index as u32))
2619        }));
2620        assert!(
2621            !instructions(main)
2622                .any(|instruction| matches!(instruction, Instruction::Export { .. }))
2623        );
2624    }
2625
2626    #[test]
2627    fn program_lowering_initializes_default_named_class_binding() {
2628        let fixture = Fixture::new();
2629        fixture.write("main.ts", "export default class Foo {}; Foo;");
2630
2631        let executable = lower_fixture(&fixture, "main.ts");
2632        let main = module(&executable, "main.ts");
2633        assert!(main.bindings().iter().any(|binding| {
2634            constant_string(main, binding.name) == "Foo" && binding.kind == BindingKind::Lexical
2635        }));
2636        let stored: Vec<_> = instructions(main)
2637            .filter_map(|instruction| match instruction {
2638                Instruction::StoreGlobal { name, .. } => Some(constant_string(main, name)),
2639                _ => None,
2640            })
2641            .collect();
2642        assert!(stored.iter().any(|name| name == "Foo"));
2643        assert!(stored.iter().any(|name| name == "*default*"));
2644    }
2645
2646    #[test]
2647    fn program_lowering_erases_type_only_edges_from_wire_but_retains_provenance() {
2648        let fixture = Fixture::new();
2649        fixture.write("types.ts", "export interface Shape { value: number }");
2650        fixture.write(
2651            "main.ts",
2652            "import type { Shape } from './types.js'; let x: Shape;",
2653        );
2654
2655        let executable = lower_fixture(&fixture, "main.ts");
2656        let main = module(&executable, "main.ts");
2657        assert!(main.edges().is_empty());
2658        assert!(
2659            main.bindings()
2660                .iter()
2661                .all(|binding| { constant_string(main, binding.name) != "Shape" })
2662        );
2663        let provenance = executable
2664            .provenance()
2665            .iter()
2666            .find(|item| item.source().path().ends_with("main.ts"))
2667            .unwrap();
2668        assert_eq!(provenance.type_only_edges().count(), 1);
2669    }
2670
2671    #[test]
2672    fn program_lowering_links_cycles_with_hoisted_and_tdz_bindings() {
2673        let fixture = Fixture::new();
2674        fixture.write(
2675            "a.ts",
2676            "import { fromB } from './b.js'; export let fromA = fromB; export var hoistedVar; export class LexicalClass {}",
2677        );
2678        fixture.write(
2679            "b.ts",
2680            "import { fromA } from './a.js'; export function fromB() { return fromA; }",
2681        );
2682
2683        let executable = lower_fixture(&fixture, "a.ts");
2684        let a = module(&executable, "a.ts");
2685        let b = module(&executable, "b.ts");
2686        assert!(a.bindings().iter().any(|binding| {
2687            constant_string(a, binding.name) == "fromA" && binding.kind == BindingKind::Lexical
2688        }));
2689        assert!(a.bindings().iter().any(|binding| {
2690            constant_string(a, binding.name) == "hoistedVar" && binding.kind == BindingKind::Hoisted
2691        }));
2692        assert!(a.bindings().iter().any(|binding| {
2693            constant_string(a, binding.name) == "LexicalClass"
2694                && binding.kind == BindingKind::Lexical
2695        }));
2696        assert!(b.bindings().iter().any(|binding| {
2697            constant_string(b, binding.name) == "fromB" && binding.kind == BindingKind::Hoisted
2698        }));
2699        assert!(a.edges().iter().all(|edge| edge.kind == EdgeKind::Static));
2700        assert!(b.edges().iter().all(|edge| edge.kind == EdgeKind::Static));
2701    }
2702
2703    #[test]
2704    fn program_lowering_keeps_dynamic_import_as_dynamic_instruction_edge() {
2705        let fixture = Fixture::new();
2706        fixture.write("dep.ts", "export const value = 1;");
2707        fixture.write("main.ts", "const pending = import('./dep.js');");
2708
2709        let executable = lower_fixture(&fixture, "main.ts");
2710        let main = module(&executable, "main.ts");
2711        assert_eq!(main.edges().len(), 1);
2712        assert_eq!(main.edges()[0].kind, EdgeKind::Dynamic);
2713        assert!(
2714            instructions(main).any(|instruction| matches!(instruction, Instruction::Import { .. }))
2715        );
2716    }
2717
2718    #[test]
2719    fn program_lowering_coalesces_static_and_dynamic_imports_of_one_target() {
2720        let fixture = Fixture::new();
2721        fixture.write("dep.ts", "export const value = 1;");
2722        fixture.write(
2723            "main.ts",
2724            "import { value } from './dep.js'; const pending = import('./dep.js'); value;",
2725        );
2726
2727        let executable = lower_fixture(&fixture, "main.ts");
2728        let main = module(&executable, "main.ts");
2729        assert_eq!(main.edges().len(), 1);
2730        assert_eq!(main.edges()[0].kind, EdgeKind::StaticAndDynamic);
2731        assert!(main.bindings().iter().any(|binding| {
2732            constant_string(main, binding.name) == "value"
2733                && matches!(binding.kind, BindingKind::Imported { .. })
2734        }));
2735        assert!(
2736            instructions(main).any(|instruction| matches!(instruction, Instruction::Import { .. }))
2737        );
2738    }
2739
2740    #[test]
2741    fn program_lowering_rejects_same_specifier_with_different_targets() {
2742        let fixture = Fixture::new();
2743        fixture.write("dep.ts", "export const value = 1;");
2744        fixture.write("other.ts", "export const other = 2;");
2745        fixture.write(
2746            "main.ts",
2747            "import { value } from './dep.js'; const pending = import('./dep.js'); import './other.js'; value;",
2748        );
2749        let mut resolved = fixture.loader().load("main.ts").unwrap();
2750        let other = resolved
2751            .modules()
2752            .iter()
2753            .find(|module| module.path().ends_with("other.ts"))
2754            .unwrap()
2755            .source_id();
2756        let modules = Arc::get_mut(&mut resolved.modules).unwrap();
2757        let main = modules
2758            .iter_mut()
2759            .find(|module| module.path().ends_with("main.ts"))
2760            .unwrap();
2761        let dependencies = Arc::make_mut(&mut main.dependencies);
2762        dependencies
2763            .iter_mut()
2764            .find(|edge| edge.kind == ModuleEdgeKind::DynamicRuntime)
2765            .unwrap()
2766            .target = ModuleTarget::Local(other);
2767
2768        let frontend = compile_program_frontend(&resolved, FrontendMode::Check);
2769        let error = lower_program(&resolved, &frontend, LowerOptions::default()).unwrap_err();
2770        assert_eq!(error.phase, ProgramLowerPhase::Metadata);
2771        assert_eq!(
2772            error.kind,
2773            ProgramLowerErrorKind::ConflictingRuntimeEdge {
2774                specifier: "./dep.js".to_owned(),
2775            }
2776        );
2777    }
2778
2779    #[test]
2780    fn program_lowering_rejects_duplicate_exports_during_link() {
2781        let fixture = Fixture::new();
2782        fixture.write(
2783            "main.ts",
2784            "const value = 1; export { value }; export { value };",
2785        );
2786        let resolved = fixture.loader().load("main.ts").unwrap();
2787        let frontend = compile_program_frontend(&resolved, FrontendMode::Check);
2788        let error = lower_program(&resolved, &frontend, LowerOptions::default()).unwrap_err();
2789        assert_eq!(error.phase, ProgramLowerPhase::Link);
2790        assert!(matches!(
2791            error.kind,
2792            ProgramLowerErrorKind::Link(bamts_bytecode::ProgramVerifyError {
2793                kind: ProgramVerifyErrorKind::DuplicateExport { .. },
2794                ..
2795            })
2796        ));
2797    }
2798
2799    #[test]
2800    fn program_lowering_preserves_external_identity_only_in_provenance() {
2801        let fixture = Fixture::new();
2802        fixture.write("main.ts", "import * as fs from 'node:fs'; fs.readFile;");
2803
2804        let executable = lower_fixture(&fixture, "main.ts");
2805        let main = module(&executable, "main.ts");
2806        assert_eq!(main.edges().len(), 1);
2807        assert_eq!(main.edges()[0].target, EdgeTarget::External);
2808        let provenance = executable
2809            .provenance()
2810            .iter()
2811            .find(|item| item.source().path().ends_with("main.ts"))
2812            .unwrap();
2813        assert!(matches!(
2814            provenance.edges()[0].target(),
2815            ModuleTarget::External(specifier) if specifier.as_ref() == "node:fs"
2816        ));
2817    }
2818
2819    #[test]
2820    fn program_lowering_is_deterministic_and_names_modules_root_relatively() {
2821        let fixture = Fixture::new();
2822        fixture.write("lib/dep.ts", "export const value = 1;");
2823        fixture.write("src/main.ts", "export { value } from '../lib/dep.js';");
2824
2825        let first = lower_fixture(&fixture, "src/main.ts");
2826        let second = lower_fixture(&fixture, "src/main.ts");
2827        assert_eq!(first.wire().encode(), second.wire().encode());
2828        assert_eq!(
2829            first
2830                .wire()
2831                .modules()
2832                .iter()
2833                .map(module_name)
2834                .collect::<Vec<_>>(),
2835            ["lib/dep.ts", "src/main.ts"]
2836        );
2837        for (left, right) in first.wire().modules().iter().zip(second.wire().modules()) {
2838            assert_eq!(left.code().encode(), right.code().encode());
2839        }
2840    }
2841
2842    #[test]
2843    fn resolves_extensions_and_directory_indexes_dependency_first() {
2844        let fixture = Fixture::new();
2845        fixture.write("main.ts", "import './leaf.js'; import './branch';");
2846        fixture.write("leaf.ts", "export const leaf = 1;");
2847        fixture.write("branch/index.ts", "export const branch = 1;");
2848
2849        let program = fixture.loader().load("main.ts").unwrap();
2850
2851        assert_eq!(names(&program), ["leaf.ts", "index.ts", "main.ts"]);
2852    }
2853
2854    #[test]
2855    fn excludes_type_only_dependencies_from_runtime_closure() {
2856        let fixture = Fixture::new();
2857        fixture.write(
2858            "main.ts",
2859            "import type { Shape } from './shape'; import { value } from './value'; void value;",
2860        );
2861        fixture.write("shape.ts", "export interface Shape { x: number }");
2862        fixture.write("value.ts", "export const value = 1;");
2863
2864        let program = fixture.loader().load("main.ts").unwrap();
2865        let runtime: Vec<_> = program
2866            .runtime_modules()
2867            .iter()
2868            .map(|module| module.path().file_name().unwrap().to_str().unwrap())
2869            .collect();
2870
2871        assert_eq!(runtime, ["value.ts", "main.ts"]);
2872        assert!(program.entrypoint().dependencies().iter().any(|edge| {
2873            edge.kind() == ModuleEdgeKind::TypeOnly && edge.specifier() == "./shape"
2874        }));
2875    }
2876
2877    #[test]
2878    fn deduplicates_diamond_dependencies_by_canonical_path() {
2879        let fixture = Fixture::new();
2880        fixture.write("main.ts", "import './left'; import './right';");
2881        fixture.write("left.ts", "import './shared';");
2882        fixture.write("right.ts", "import './shared';");
2883        fixture.write("shared.ts", "export const shared = 1;");
2884
2885        let program = fixture.loader().load("main.ts").unwrap();
2886
2887        assert_eq!(
2888            names(&program),
2889            ["shared.ts", "left.ts", "right.ts", "main.ts"]
2890        );
2891        assert_eq!(program.modules().len(), 4);
2892    }
2893
2894    #[test]
2895    fn preserves_cycles_without_duplicate_modules() {
2896        let fixture = Fixture::new();
2897        fixture.write("a.ts", "import './b';");
2898        fixture.write("b.ts", "import './a';");
2899
2900        let program = fixture.loader().load("a.ts").unwrap();
2901        let b = &program.modules()[0];
2902
2903        assert_eq!(names(&program), ["b.ts", "a.ts"]);
2904        assert_eq!(
2905            b.dependencies()[0].target(),
2906            &ModuleTarget::Local(program.entrypoint_id())
2907        );
2908    }
2909
2910    #[test]
2911    fn resolves_package_exports_entry() {
2912        let fixture = Fixture::new();
2913        fixture.write("main.ts", "import { answer } from 'waybread'; void answer;");
2914        fixture.write(
2915            "node_modules/waybread/package.json",
2916            r#"{"name":"waybread","exports":{".":{"import":"./src/entry.js"}}}"#,
2917        );
2918        fixture.write(
2919            "node_modules/waybread/src/entry.ts",
2920            "export const answer = 42;",
2921        );
2922
2923        let program = fixture.loader().load("main.ts").unwrap();
2924
2925        assert_eq!(names(&program), ["entry.ts", "main.ts"]);
2926    }
2927    #[test]
2928    fn resolves_package_import_maps() {
2929        let fixture = Fixture::new();
2930        fixture.write(
2931            "package.json",
2932            r##"{"name":"root","imports":{"#waybread":"./src/alias.js"}}"##,
2933        );
2934        fixture.write(
2935            "main.ts",
2936            "import { answer } from '#waybread'; void answer;",
2937        );
2938        fixture.write("src/alias.ts", "export const answer = 42;");
2939
2940        let program = fixture.loader().load("main.ts").unwrap();
2941
2942        assert_eq!(names(&program), ["alias.ts", "main.ts"]);
2943    }
2944
2945    #[test]
2946    fn decodes_escaped_module_specifiers() {
2947        let fixture = Fixture::new();
2948        fixture.write("main.ts", r#"import './\u0066oo';"#);
2949        fixture.write("foo.ts", "export const answer = 42;");
2950
2951        let program = fixture.loader().load("main.ts").unwrap();
2952
2953        assert_eq!(names(&program), ["foo.ts", "main.ts"]);
2954    }
2955
2956    #[test]
2957    fn rejects_ill_formed_utf16_module_specifiers() {
2958        for source in [
2959            r#"import type { T } from "\uD800";"#,
2960            r#"type T = import("\uD800").T;"#,
2961        ] {
2962            let fixture = Fixture::new();
2963            fixture.write("main.ts", source);
2964
2965            assert!(matches!(
2966                fixture.loader().load("main.ts"),
2967                Err(ProgramLoadError::IllFormedModuleSpecifier { .. })
2968            ));
2969        }
2970    }
2971
2972    #[test]
2973    fn rejects_relative_traversal_before_loading() {
2974        let fixture = Fixture::new();
2975        fixture.write("main.ts", "import '../outside.ts';");
2976
2977        let error = fixture.loader().load("main.ts").unwrap_err();
2978
2979        assert!(matches!(
2980            error,
2981            ProgramLoadError::InvalidSpecifier { .. } | ProgramLoadError::UnresolvedModule(_)
2982        ));
2983    }
2984
2985    #[test]
2986    fn retains_literal_dynamic_import_edges() {
2987        let fixture = Fixture::new();
2988        fixture.write(
2989            "main.ts",
2990            "async function load() { return import('./later'); }",
2991        );
2992        fixture.write("later.ts", "export const later = 1;");
2993
2994        let program = fixture.loader().load("main.ts").unwrap();
2995
2996        assert_eq!(names(&program), ["later.ts", "main.ts"]);
2997        assert_eq!(
2998            program.entrypoint().dependencies()[0].kind(),
2999            ModuleEdgeKind::DynamicRuntime
3000        );
3001        assert_eq!(program.runtime_modules().len(), 1);
3002    }
3003    #[test]
3004    fn classifies_import_types_without_creating_dynamic_runtime_edges() {
3005        let fixture = Fixture::new();
3006        fixture.write("main.ts", "type Shape = import('./shape').Shape; void 0;");
3007        fixture.write("shape.ts", "export interface Shape { x: number }");
3008
3009        let program = fixture.loader().load("main.ts").unwrap();
3010
3011        assert_eq!(names(&program), ["shape.ts", "main.ts"]);
3012        assert_eq!(
3013            program.entrypoint().dependencies()[0].kind(),
3014            ModuleEdgeKind::TypeOnly
3015        );
3016        assert_eq!(program.runtime_modules().len(), 1);
3017    }
3018
3019    #[test]
3020    fn unresolved_runtime_edge_reports_typed_diagnostic() {
3021        let fixture = Fixture::new();
3022        fixture.write("main.ts", "import './missing';");
3023
3024        let ProgramLoadError::UnresolvedModule(diagnostic) =
3025            fixture.loader().load("main.ts").unwrap_err()
3026        else {
3027            panic!("expected unresolved-module diagnostic");
3028        };
3029
3030        assert_eq!(diagnostic.kind(), ModuleEdgeKind::StaticRuntime);
3031        assert_eq!(diagnostic.specifier(), "./missing");
3032        assert_eq!(
3033            diagnostic.importer().file_name(),
3034            Some(Path::new("main.ts").as_os_str())
3035        );
3036    }
3037
3038    #[test]
3039    fn retains_node_builtin_as_external_static_runtime_edge() {
3040        let fixture = Fixture::new();
3041        fixture.write(
3042            "main.ts",
3043            "import { parseArgs } from 'node:util'; void parseArgs;",
3044        );
3045
3046        let program = fixture.loader().load("main.ts").unwrap();
3047        let edge = &program.entrypoint().dependencies()[0];
3048
3049        assert_eq!(names(&program), ["main.ts"]);
3050        assert_eq!(edge.kind(), ModuleEdgeKind::StaticRuntime);
3051        assert_eq!(edge.target().external_specifier(), Some("node:util"));
3052        assert_eq!(program.runtime_modules().len(), 1);
3053    }
3054
3055    #[test]
3056    fn preserves_unresolved_type_package_as_external_identity() {
3057        let fixture = Fixture::new();
3058        fixture.write("main.ts", "import type { JsonValue } from 'type-fest';");
3059
3060        let program = fixture.loader().load("main.ts").unwrap();
3061        let edge = &program.entrypoint().dependencies()[0];
3062
3063        assert_eq!(edge.kind(), ModuleEdgeKind::TypeOnly);
3064        assert_eq!(edge.target().external_specifier(), Some("type-fest"));
3065        assert_eq!(program.runtime_modules().len(), 1);
3066    }
3067
3068    #[test]
3069    fn unresolved_ordinary_runtime_package_is_rejected() {
3070        let fixture = Fixture::new();
3071        fixture.write("main.ts", "import 'waybread';");
3072
3073        let ProgramLoadError::UnresolvedModule(diagnostic) =
3074            fixture.loader().load("main.ts").unwrap_err()
3075        else {
3076            panic!("expected unresolved-module diagnostic");
3077        };
3078
3079        assert_eq!(diagnostic.kind(), ModuleEdgeKind::StaticRuntime);
3080        assert_eq!(diagnostic.specifier(), "waybread");
3081    }
3082
3083    #[test]
3084    fn retains_dynamic_engine_module_as_external_edge() {
3085        let fixture = Fixture::new();
3086        fixture.write(
3087            "package.json",
3088            r##"{"name":"root","imports":{"#engine":"engine:clock"}}"##,
3089        );
3090        fixture.write(
3091            "main.ts",
3092            "async function load() { return import('#engine'); }",
3093        );
3094
3095        let program = fixture.loader().load("main.ts").unwrap();
3096        let edge = &program.entrypoint().dependencies()[0];
3097
3098        assert_eq!(edge.kind(), ModuleEdgeKind::DynamicRuntime);
3099        assert_eq!(edge.specifier(), "#engine");
3100        assert_eq!(edge.target().external_specifier(), Some("engine:clock"));
3101        assert_eq!(program.runtime_modules().len(), 1);
3102    }
3103
3104    #[test]
3105    fn local_type_package_takes_precedence_over_external_fallback() {
3106        let fixture = Fixture::new();
3107        fixture.write("main.ts", "import type { Shape } from 'waybread';");
3108        fixture.write(
3109            "node_modules/waybread/package.json",
3110            r#"{"name":"waybread","types":"./index.d.ts"}"#,
3111        );
3112        fixture.write(
3113            "node_modules/waybread/index.d.ts",
3114            "export interface Shape { x: number }",
3115        );
3116
3117        let program = fixture.loader().load("main.ts").unwrap();
3118        let edge = &program.entrypoint().dependencies()[0];
3119
3120        assert_eq!(names(&program), ["index.d.ts", "main.ts"]);
3121        assert!(matches!(edge.target(), ModuleTarget::Local(_)));
3122    }
3123
3124    #[test]
3125    fn lowers_and_verifies_exactly_the_twenty_pinned_corpus_programs() {
3126        let repository = Path::new(env!("CARGO_MANIFEST_DIR"))
3127            .join("../..")
3128            .canonicalize()
3129            .unwrap();
3130        let manifest = fs::read_to_string(repository.join("corpus/manifest.toml")).unwrap();
3131        let entrypoints: Vec<_> = manifest
3132            .lines()
3133            .filter_map(|line| {
3134                line.strip_prefix("entrypoint = \"")
3135                    .and_then(|value| value.strip_suffix('"'))
3136            })
3137            .collect();
3138        assert_eq!(entrypoints.len(), 20);
3139
3140        let root = ProjectRoot::new(repository).unwrap();
3141        let config = ProjectConfig::parse(&root, root.path().join("tsconfig.json"), "{}").unwrap();
3142        let loader = ProgramLoader::new(&root, config.options()).unwrap();
3143        for entrypoint in entrypoints {
3144            let resolved = loader
3145                .load(entrypoint)
3146                .unwrap_or_else(|error| panic!("{entrypoint}: {error}"));
3147            let frontend = compile_program_frontend(&resolved, FrontendMode::Check);
3148            let executable = lower_program(
3149                &resolved,
3150                &frontend,
3151                LowerOptions {
3152                    javascript_compatibility: true,
3153                },
3154            )
3155            .unwrap_or_else(|error| panic!("{entrypoint}: {error}"));
3156            assert_eq!(executable.wire().modules().len(), resolved.modules().len());
3157        }
3158    }
3159}