1use 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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub enum ModuleEdgeKind {
36 StaticRuntime,
37 TypeOnly,
38 DynamicRuntime,
39}
40
41#[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#[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#[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#[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 #[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#[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#[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#[derive(Clone, Debug)]
341pub struct ProgramLoader {
342 root: ProjectRoot,
343 options: CompilerOptions,
344}
345
346impl ProgramLoader {
347 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 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 ¶meter.data().decorators {
975 self.scan_expression(&decorator.data().expression);
976 }
977 self.scan_pattern(¶meter.data().binding);
978 if let Some(initializer) = ¶meter.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#[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 #[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#[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#[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
1577pub 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}