@@ -609,6 +609,22 @@ enum ModuleStatus {
609609 Evaluated,
610610}
611611
612+ /// Result of spec-compliant ResolveExport algorithm.
613+ enum ResolveExportResult {
614+ /// Resolved to a concrete binding in (module_key, binding_name)
615+ Found(String, String),
616+ Null,
617+ Ambiguous,
618+ }
619+
620+ /// Context for module resolution validation (avoids threading many params).
621+ struct ModuleResolutionContext<'a> {
622+ local_exports: &'a std::collections::HashMap<String, std::collections::HashSet<String>>,
623+ all_reexport_deps: &'a std::collections::HashMap<String, Vec<(String, Vec<crate::core::ReexportSpec>)>>,
624+ import_bindings: &'a std::collections::HashMap<String, std::collections::HashMap<String, (String, String)>>,
625+ export_to_local: &'a std::collections::HashMap<String, std::collections::HashMap<String, String>>,
626+ }
627+
612628#[derive(Clone)]
613629struct StoredModuleRecord {
614630 source: String,
@@ -2643,6 +2659,295 @@ impl<'gc> VM<'gc> {
26432659 }
26442660 }
26452661
2662+ /// Validate module resolution: check that all indirect export entries and
2663+ /// import bindings resolve to actual exports in their source modules.
2664+ /// Per spec §15.2.1.16.4 step 9 (IndirectExportEntries validation).
2665+ pub fn validate_module_resolution(
2666+ &self,
2667+ main_module_key: &str,
2668+ main_statements: &[crate::core::Statement],
2669+ main_export_name_to_local: &std::collections::HashMap<String, String>,
2670+ main_reexport_sources: &[(String, Vec<crate::core::ReexportSpec>)],
2671+ entry_path: &std::path::Path,
2672+ ) -> Result<(), JSError> {
2673+ use crate::core::resolve_module_path;
2674+ use crate::core::statement::{ImportSpecifier, StatementKind};
2675+
2676+ // Build per-module data structures needed for spec-compliant ResolveExport
2677+ let mut local_exports: std::collections::HashMap<String, std::collections::HashSet<String>> = std::collections::HashMap::new();
2678+ // import_bindings: module_key → { local_name → (source_module_key, import_name) }
2679+ let mut import_bindings: std::collections::HashMap<String, std::collections::HashMap<String, (String, String)>> =
2680+ std::collections::HashMap::new();
2681+ // export_to_local: module_key → { export_name → local_name }
2682+ let mut export_to_local: std::collections::HashMap<String, std::collections::HashMap<String, String>> =
2683+ std::collections::HashMap::new();
2684+
2685+ // Helper: extract import bindings from statements
2686+ fn extract_imports(
2687+ stmts: &[crate::core::Statement],
2688+ base_path: &std::path::Path,
2689+ ) -> std::collections::HashMap<String, (String, String)> {
2690+ let mut map = std::collections::HashMap::new();
2691+ for stmt in stmts {
2692+ if let StatementKind::Import(specs, source) = &*stmt.kind {
2693+ let resolved = resolve_module_path(source, base_path).to_string_lossy().to_string();
2694+ for spec in specs {
2695+ match spec {
2696+ ImportSpecifier::Named(import_name, Some(local_name)) => {
2697+ map.insert(local_name.clone(), (resolved.clone(), import_name.clone()));
2698+ }
2699+ ImportSpecifier::Named(name, None) => {
2700+ map.insert(name.clone(), (resolved.clone(), name.clone()));
2701+ }
2702+ ImportSpecifier::Default(local_name) => {
2703+ map.insert(local_name.clone(), (resolved.clone(), "default".to_string()));
2704+ }
2705+ ImportSpecifier::Namespace(local_name) | ImportSpecifier::DeferredNamespace(local_name) => {
2706+ // import * as foo → namespace import; use sentinel
2707+ map.insert(local_name.clone(), (resolved.clone(), "\x00namespace".to_string()));
2708+ }
2709+ }
2710+ }
2711+ }
2712+ }
2713+ map
2714+ }
2715+
2716+ // Populate maps for all dependency modules
2717+ for (key, record) in &self.module_records {
2718+ let mut set = std::collections::HashSet::new();
2719+ for k in record.export_name_to_local.keys() {
2720+ set.insert(k.clone());
2721+ }
2722+ local_exports.insert(key.clone(), set);
2723+ import_bindings.insert(key.clone(), extract_imports(&record.statements, &record.resolved_path));
2724+ export_to_local.insert(key.clone(), record.export_name_to_local.clone());
2725+ }
2726+ // Add main module's exports and import bindings
2727+ {
2728+ let mut set = std::collections::HashSet::new();
2729+ for k in main_export_name_to_local.keys() {
2730+ set.insert(k.clone());
2731+ }
2732+ local_exports.insert(main_module_key.to_string(), set);
2733+ import_bindings.insert(main_module_key.to_string(), extract_imports(main_statements, entry_path));
2734+ export_to_local.insert(main_module_key.to_string(), main_export_name_to_local.clone());
2735+ }
2736+
2737+ // Build reexport_deps map including main module
2738+ let mut all_reexport_deps: std::collections::HashMap<String, Vec<(String, Vec<crate::core::ReexportSpec>)>> =
2739+ std::collections::HashMap::new();
2740+ {
2741+ let mut resolved_reexports = Vec::new();
2742+ for (source, specs) in main_reexport_sources {
2743+ let resolved = resolve_module_path(source, entry_path).to_string_lossy().to_string();
2744+ resolved_reexports.push((resolved, specs.clone()));
2745+ }
2746+ if !resolved_reexports.is_empty() {
2747+ all_reexport_deps.insert(main_module_key.to_string(), resolved_reexports);
2748+ }
2749+ }
2750+ // Dependency re-exports (already resolved)
2751+ for (key, deps) in &self.reexport_deps {
2752+ all_reexport_deps.insert(key.clone(), deps.clone());
2753+ }
2754+
2755+ let ctx = ModuleResolutionContext {
2756+ local_exports: &local_exports,
2757+ all_reexport_deps: &all_reexport_deps,
2758+ import_bindings: &import_bindings,
2759+ export_to_local: &export_to_local,
2760+ };
2761+
2762+ // Validate: for each module's IndirectExportEntries (named re-exports),
2763+ // resolve the export using spec-compliant ResolveExport.
2764+ for reexport_list in all_reexport_deps.values() {
2765+ for (src_key, specs) in reexport_list {
2766+ for spec in specs {
2767+ if let crate::core::ReexportSpec::Named(name, alias) = spec {
2768+ let export_name = alias.as_deref().unwrap_or(name);
2769+ let mut visited = std::collections::HashSet::new();
2770+ let result = Self::resolve_export_static(src_key, name, &ctx, &mut visited);
2771+ match result {
2772+ ResolveExportResult::Found(..) => {}
2773+ ResolveExportResult::Null => {
2774+ let display_src = self.get_module_display_name(src_key);
2775+ return Err(crate::raise_syntax_error!(format!(
2776+ "The requested module '{}' does not provide an export named '{}'",
2777+ display_src, export_name
2778+ )));
2779+ }
2780+ ResolveExportResult::Ambiguous => {
2781+ let display_src = self.get_module_display_name(src_key);
2782+ return Err(crate::raise_syntax_error!(format!(
2783+ "The requested module '{}' contains ambiguous star exports for the name '{}'",
2784+ display_src, export_name
2785+ )));
2786+ }
2787+ }
2788+ }
2789+ }
2790+ }
2791+ }
2792+
2793+ // Validate import bindings in main module
2794+ for stmt in main_statements {
2795+ if let StatementKind::Import(specs, source) = &*stmt.kind {
2796+ let resolved = resolve_module_path(source, entry_path).to_string_lossy().to_string();
2797+ Self::validate_import_specs_static(specs, &resolved, source, &ctx)?;
2798+ }
2799+ }
2800+ // Validate import bindings in dependency modules
2801+ for record in self.module_records.values() {
2802+ for stmt in &record.statements {
2803+ if let StatementKind::Import(specs, source) = &*stmt.kind {
2804+ let resolved = resolve_module_path(source, &record.resolved_path).to_string_lossy().to_string();
2805+ Self::validate_import_specs_static(specs, &resolved, source, &ctx)?;
2806+ }
2807+ }
2808+ }
2809+
2810+ Ok(())
2811+ }
2812+
2813+ /// Validate import specifiers against a source module.
2814+ fn validate_import_specs_static(
2815+ specs: &[crate::core::statement::ImportSpecifier],
2816+ resolved_key: &str,
2817+ display_source: &str,
2818+ ctx: &ModuleResolutionContext<'_>,
2819+ ) -> Result<(), JSError> {
2820+ use crate::core::statement::ImportSpecifier;
2821+ for spec in specs {
2822+ let binding_name = match spec {
2823+ ImportSpecifier::Named(name, _) => Some(name.as_str()),
2824+ ImportSpecifier::Default(_) => Some("default"),
2825+ ImportSpecifier::Namespace(_) | ImportSpecifier::DeferredNamespace(_) => None,
2826+ };
2827+ if let Some(name) = binding_name {
2828+ let mut visited = std::collections::HashSet::new();
2829+ let result = Self::resolve_export_static(resolved_key, name, ctx, &mut visited);
2830+ match result {
2831+ ResolveExportResult::Found(..) => {}
2832+ ResolveExportResult::Null => {
2833+ return Err(crate::raise_syntax_error!(format!(
2834+ "The requested module '{}' does not provide an export named '{}'",
2835+ display_source, name
2836+ )));
2837+ }
2838+ ResolveExportResult::Ambiguous => {
2839+ return Err(crate::raise_syntax_error!(format!(
2840+ "The requested module '{}' contains ambiguous star exports for the name '{}'",
2841+ display_source, name
2842+ )));
2843+ }
2844+ }
2845+ }
2846+ }
2847+ Ok(())
2848+ }
2849+
2850+ /// Spec-compliant ResolveExport: traces export resolution through re-exports.
2851+ fn resolve_export_static(
2852+ module_key: &str,
2853+ export_name: &str,
2854+ ctx: &ModuleResolutionContext<'_>,
2855+ visited: &mut std::collections::HashSet<(String, String)>,
2856+ ) -> ResolveExportResult {
2857+ let pair = (module_key.to_string(), export_name.to_string());
2858+ if visited.contains(&pair) {
2859+ return ResolveExportResult::Null;
2860+ }
2861+ visited.insert(pair);
2862+
2863+ // 1. Check local/direct exports
2864+ if let Some(locals) = ctx.local_exports.get(module_key)
2865+ && locals.contains(export_name)
2866+ {
2867+ // Per spec ParseModule step 10.1.ii: if the local name is in
2868+ // importedBoundNames, it becomes an indirect export entry —
2869+ // follow the import chain to find the true origin.
2870+ if let Some(etl) = ctx.export_to_local.get(module_key)
2871+ && let Some(local_name) = etl.get(export_name)
2872+ && let Some(imports) = ctx.import_bindings.get(module_key)
2873+ && let Some((src_mod, import_name)) = imports.get(local_name)
2874+ {
2875+ if import_name == "\x00namespace" {
2876+ // import * as foo; export { foo } → namespace re-export
2877+ return ResolveExportResult::Found(src_mod.clone(), "\x00namespace".to_string());
2878+ }
2879+ return Self::resolve_export_static(src_mod, import_name, ctx, visited);
2880+ }
2881+ return ResolveExportResult::Found(module_key.to_string(), export_name.to_string());
2882+ }
2883+
2884+ // 2. Check indirect export entries (named re-exports + namespace re-exports)
2885+ if let Some(reexport_list) = ctx.all_reexport_deps.get(module_key) {
2886+ for (src_key, specs) in reexport_list {
2887+ for spec in specs {
2888+ match spec {
2889+ crate::core::ReexportSpec::Named(name, alias) => {
2890+ let ename = alias.as_deref().unwrap_or(name);
2891+ if ename == export_name {
2892+ return Self::resolve_export_static(src_key, name, ctx, visited);
2893+ }
2894+ }
2895+ crate::core::ReexportSpec::Namespace(name) => {
2896+ if name == export_name {
2897+ // export * as foo from 'mod' -> per spec resolves to
2898+ // { Module: importedModule, BindingName: namespace }
2899+ return ResolveExportResult::Found(src_key.clone(), "\x00namespace".to_string());
2900+ }
2901+ }
2902+ crate::core::ReexportSpec::Star => {}
2903+ }
2904+ }
2905+ }
2906+ }
2907+
2908+ // 3. Check star re-exports (StarExportEntries)
2909+ if export_name == "default" {
2910+ return ResolveExportResult::Null;
2911+ }
2912+ let mut star_resolution: Option<(String, String)> = None;
2913+ if let Some(reexport_list) = ctx.all_reexport_deps.get(module_key) {
2914+ for (src_key, specs) in reexport_list {
2915+ let has_star = specs.iter().any(|s| matches!(s, crate::core::ReexportSpec::Star));
2916+ if !has_star {
2917+ continue;
2918+ }
2919+ let resolution = Self::resolve_export_static(src_key, export_name, ctx, visited);
2920+ match resolution {
2921+ ResolveExportResult::Ambiguous => return ResolveExportResult::Ambiguous,
2922+ ResolveExportResult::Found(ref m, ref b) => {
2923+ if let Some((ref pm, ref pb)) = star_resolution {
2924+ if m != pm || b != pb {
2925+ return ResolveExportResult::Ambiguous;
2926+ }
2927+ } else {
2928+ star_resolution = Some((m.clone(), b.clone()));
2929+ }
2930+ }
2931+ ResolveExportResult::Null => {}
2932+ }
2933+ }
2934+ }
2935+ match star_resolution {
2936+ Some((m, b)) => ResolveExportResult::Found(m, b),
2937+ None => ResolveExportResult::Null,
2938+ }
2939+ }
2940+
2941+ /// Get a human-readable display name for a module key (use relative path if possible).
2942+ fn get_module_display_name(&self, module_key: &str) -> String {
2943+ if let Some(record) = self.module_records.get(module_key)
2944+ && let Some(fname) = record.resolved_path.file_name()
2945+ {
2946+ return format!("./{}", fname.to_string_lossy());
2947+ }
2948+ module_key.to_string()
2949+ }
2950+
26462951 /// Inject loaded module bindings into `module_locals` based on `chunk.loaded_module_vars`.
26472952 pub fn inject_loaded_module_bindings(&mut self, ctx: &GcContext<'gc>) {
26482953 let vars: Vec<(String, String, String)> = self
0 commit comments