diff --git a/changelog.d/7189-export-star-as-namespace-object.md b/changelog.d/7189-export-star-as-namespace-object.md new file mode 100644 index 0000000000..0ecc4d92ad --- /dev/null +++ b/changelog.d/7189-export-star-as-namespace-object.md @@ -0,0 +1,40 @@ +`export * as ns from "./m.ts"` now appears on the re-exporting module's +namespace object. + +The binding existed, but only under one of the two ways of reaching it: + +```ts +// mid.ts +export * as deep from "./leaf.ts"; + +// main.ts +import { deep } from "./mid.ts"; // worked +import * as B from "./mid.ts"; +B.deep; // undefined +Object.keys(B); // "deep" missing entirely +``` + +The practical cost was zod. Its `external.ts` re-exports four sub-namespaces +exactly this way, so `z.coerce`, `z.iso`, `z.core` and `z.locales` were all +undefined and any code touching them died with "Cannot read properties of +undefined". + +Two things were missing. A namespace alias has no declaring binding to point +at — it names a whole module, not something inside one — so it never entered +the export surface the compiler builds for a namespace import, which is why +`Object.keys` did not list it. And every way of resolving a namespace member +ended in a symbol to call, which an alias does not have, so the read fell +through to a generic property lookup on an object that had no such key. + +The fix reuses the object the compiler already knows how to build. Perry +already emits a populated namespace object for any module reachable by a +dynamic `import()`, and that populator already handles nested namespaces +recursively. So a namespace re-export target now gets one too, and `B.deep` +loads it. Nesting several levels deep works for the same reason: it is the +same populator all the way down, not a second implementation that has to +reproduce the first one's behaviour. + +Members reached through the object behave normally — `B.deep.alpha`, +`B.deep.beta()`, and `Object.keys(B.deep)` all match Node. + +Closes #7189. diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index 26df4b38f6..a4f7c389c5 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -1433,9 +1433,36 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { // `BTreeSet` so a multi-path site that resolves to N targets emits // N declarations exactly once even when multiple `paths` arrays // share entries. - if !cross_module.dynamic_import_path_to_prefix.is_empty() { - let mut foreign_prefixes: std::collections::BTreeSet = - std::collections::BTreeSet::new(); + // #7189: nested namespace members (`export * as ns from "./m.ts"`, read as + // `B.ns` through a static `import * as B`) load the SAME + // `@__perry_ns_` global a dynamic import would, so they need the + // same extern declaration. Without it the module references a global it + // never declared and LLVM refuses to parse the IR — which is a good way for + // this to fail, since it fails at build time and names the missing global. + let mut nested_prefixes: std::collections::BTreeSet = + std::collections::BTreeSet::new(); + for key in &cross_module.namespace_member_nested { + if let Some(prefix) = cross_module.namespace_member_prefixes.get(key) { + if prefix != module_prefix.as_str() { + nested_prefixes.insert(prefix.clone()); + } + } + } + // The other direction: this module's OWN populator has a nested entry for + // each `export * as ns` it declares, and that entry's value is the source + // module's namespace global. Before #7189 this could not come up, because a + // module only got a populator by being a dynamic-import target and those + // already declare their targets. Now that a namespace re-exporter gets one + // too, its source needs declaring on the same terms. + for entry in &cross_module.namespace_entries { + if let crate::NamespaceEntryKind::NestedNamespace { source_prefix } = &entry.kind { + if source_prefix != module_prefix.as_str() { + nested_prefixes.insert(source_prefix.clone()); + } + } + } + if !cross_module.dynamic_import_path_to_prefix.is_empty() || !nested_prefixes.is_empty() { + let mut foreign_prefixes: std::collections::BTreeSet = nested_prefixes; for prefix in cross_module.dynamic_import_path_to_prefix.values() { if prefix != module_prefix.as_str() { foreign_prefixes.insert(prefix.clone()); diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index ae5717a7b1..cc6649b715 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -947,6 +947,7 @@ pub(super) fn compile_closure( object_literal_locals: HashSet::new(), namespace_imports: &cross_module.namespace_imports, namespace_member_prefixes: &cross_module.namespace_member_prefixes, + namespace_member_nested: &cross_module.namespace_member_nested, namespace_member_origin_names: &cross_module.namespace_member_origin_names, imported_async_funcs: &cross_module.imported_async_funcs, local_async_funcs: &cross_module.local_async_funcs, diff --git a/crates/perry-codegen/src/codegen/emission_order_tests.rs b/crates/perry-codegen/src/codegen/emission_order_tests.rs index 65e435e711..d4ffe4ce77 100644 --- a/crates/perry-codegen/src/codegen/emission_order_tests.rs +++ b/crates/perry-codegen/src/codegen/emission_order_tests.rs @@ -85,6 +85,7 @@ fn ir_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index b6d894ebd4..ed145be200 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -756,6 +756,7 @@ pub(super) fn compile_module_entry( object_literal_locals: HashSet::new(), namespace_imports: &cross_module.namespace_imports, namespace_member_prefixes: &cross_module.namespace_member_prefixes, + namespace_member_nested: &cross_module.namespace_member_nested, namespace_member_origin_names: &cross_module.namespace_member_origin_names, imported_async_funcs: &cross_module.imported_async_funcs, local_async_funcs: &cross_module.local_async_funcs, @@ -1424,6 +1425,7 @@ pub(super) fn compile_module_entry( object_literal_locals: HashSet::new(), namespace_imports: &cross_module.namespace_imports, namespace_member_prefixes: &cross_module.namespace_member_prefixes, + namespace_member_nested: &cross_module.namespace_member_nested, namespace_member_origin_names: &cross_module.namespace_member_origin_names, imported_async_funcs: &cross_module.imported_async_funcs, local_async_funcs: &cross_module.local_async_funcs, diff --git a/crates/perry-codegen/src/codegen/entry/tests.rs b/crates/perry-codegen/src/codegen/entry/tests.rs index 0d17e1769f..b3e646dd59 100644 --- a/crates/perry-codegen/src/codegen/entry/tests.rs +++ b/crates/perry-codegen/src/codegen/entry/tests.rs @@ -20,6 +20,7 @@ fn entry_opts(output_type: &str) -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index dcc135c7da..af8230201e 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -743,6 +743,7 @@ pub(super) fn compile_function( object_literal_locals: HashSet::new(), namespace_imports: &cross_module.namespace_imports, namespace_member_prefixes: &cross_module.namespace_member_prefixes, + namespace_member_nested: &cross_module.namespace_member_nested, namespace_member_origin_names: &cross_module.namespace_member_origin_names, imported_async_funcs: &cross_module.imported_async_funcs, local_async_funcs: &cross_module.local_async_funcs, diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index 71a648eeca..70c0bbac63 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -478,6 +478,7 @@ pub(super) fn compile_method( object_literal_locals: HashSet::new(), namespace_imports: &cross_module.namespace_imports, namespace_member_prefixes: &cross_module.namespace_member_prefixes, + namespace_member_nested: &cross_module.namespace_member_nested, namespace_member_origin_names: &cross_module.namespace_member_origin_names, imported_async_funcs: &cross_module.imported_async_funcs, local_async_funcs: &cross_module.local_async_funcs, @@ -1540,6 +1541,7 @@ pub(super) fn compile_static_method( object_literal_locals: HashSet::new(), namespace_imports: &cross_module.namespace_imports, namespace_member_prefixes: &cross_module.namespace_member_prefixes, + namespace_member_nested: &cross_module.namespace_member_nested, namespace_member_origin_names: &cross_module.namespace_member_origin_names, imported_async_funcs: &cross_module.imported_async_funcs, local_async_funcs: &cross_module.local_async_funcs, diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index e8d17bbfa0..1c991a189c 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -1619,6 +1619,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> let mut cross_module = CrossModuleCtx { namespace_imports: opts.namespace_imports.iter().cloned().collect(), + namespace_member_nested: opts.namespace_member_nested.iter().cloned().collect(), namespace_member_prefixes: opts.namespace_member_prefixes, namespace_member_origin_names: opts.namespace_member_origin_names, imported_async_funcs: opts.imported_async_funcs, diff --git a/crates/perry-codegen/src/codegen/number_exactness_tests.rs b/crates/perry-codegen/src/codegen/number_exactness_tests.rs index c3308451d0..81e9213422 100644 --- a/crates/perry-codegen/src/codegen/number_exactness_tests.rs +++ b/crates/perry-codegen/src/codegen/number_exactness_tests.rs @@ -44,6 +44,7 @@ fn ir_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/src/codegen/opts.rs b/crates/perry-codegen/src/codegen/opts.rs index 69306eed32..c3fc8fdf7d 100644 --- a/crates/perry-codegen/src/codegen/opts.rs +++ b/crates/perry-codegen/src/codegen/opts.rs @@ -203,6 +203,15 @@ pub struct CompileOptions { /// Codegen uses this to know that `X.foo()` should be dispatched as /// a cross-module call rather than an object method call. pub namespace_imports: Vec, + /// #7189: `(namespace local, member)` pairs whose member is itself a MODULE + /// NAMESPACE, from `export * as ns from "./m.ts"` in the imported module. + /// + /// These have no `perry_fn___` symbol to call, because the member + /// is a whole module rather than a binding inside one. They resolve instead + /// to that module's `@__perry_ns_` global — the same object a + /// dynamic `import()` of it would hand back — with the prefix coming from + /// `namespace_member_prefixes` under the same key. + pub namespace_member_nested: Vec<(String, String)>, /// Imported class definitions from other native modules, keyed by /// the local alias (or original name when no alias). Each entry /// carries the class HIR, the module prefix of its origin, and an @@ -565,6 +574,15 @@ impl ImportedCtor { /// Built once in `compile_module` from `CompileOptions`. pub(crate) struct CrossModuleCtx { pub namespace_imports: std::collections::HashSet, + /// #7189: `(namespace local, member)` pairs whose member is itself a MODULE + /// NAMESPACE, from `export * as ns from "./m.ts"` in the imported module. + /// + /// These have no `perry_fn___` symbol to call, because the member + /// is a whole module rather than a binding inside one. They resolve instead + /// to that module's `@__perry_ns_` global — the same object a + /// dynamic `import()` of it would hand back — with the prefix coming from + /// `namespace_member_prefixes` under the same key. + pub namespace_member_nested: std::collections::HashSet<(String, String)>, /// Issue #680: per-namespace member resolution. See doc on /// `CompileOptions::namespace_member_prefixes`. pub namespace_member_prefixes: std::collections::HashMap<(String, String), String>, diff --git a/crates/perry-codegen/src/collectors/proven_this_routing_tests.rs b/crates/perry-codegen/src/collectors/proven_this_routing_tests.rs index 2e7b296f0a..eb28cdf8c9 100644 --- a/crates/perry-codegen/src/collectors/proven_this_routing_tests.rs +++ b/crates/perry-codegen/src/collectors/proven_this_routing_tests.rs @@ -55,6 +55,7 @@ fn ir_opts(is_entry: bool) -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/src/expr/conforming_layout_note_tests.rs b/crates/perry-codegen/src/expr/conforming_layout_note_tests.rs index 4263c21d0e..5f50e3111b 100644 --- a/crates/perry-codegen/src/expr/conforming_layout_note_tests.rs +++ b/crates/perry-codegen/src/expr/conforming_layout_note_tests.rs @@ -51,6 +51,7 @@ fn ir_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs index 391de5f875..0b7573c089 100644 --- a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs +++ b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs @@ -21,7 +21,7 @@ use super::{ /// on the current block: a native submodule (`__node_submod__`), a native /// builtin (`__native_mod__`), or a compiled module (`__init` + /// `@__perry_ns_`). Returns a NaN-boxed `DOUBLE` value id. -fn namespace_value_for_prefix(ctx: &mut FnCtx<'_>, prefix: &str) -> String { +pub(crate) fn namespace_value_for_prefix(ctx: &mut FnCtx<'_>, prefix: &str) -> String { if let Some(key) = prefix.strip_prefix("__node_submod__") { let key = key.to_string(); let submod_label = emit_string_literal_global(ctx, &key); diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 11db9021c0..b3d19b9cb0 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -545,6 +545,10 @@ pub(crate) struct FnCtx<'a> { /// by namespace member access lowering to disambiguate when the same /// export name appears in multiple `import * as X / Y` sources. pub namespace_member_prefixes: &'a std::collections::HashMap<(String, String), String>, + /// #7189: `(namespace local, member)` pairs whose member is another + /// module's namespace object rather than a binding. See the doc on + /// `CompileOptions::namespace_member_nested`. + pub namespace_member_nested: &'a std::collections::HashSet<(String, String)>, /// Issue #5924: per-namespace origin-name resolution. Keyed by /// `(namespace_local_name, member_name)` → `origin_name`. Consulted /// before `import_function_origin_names` when computing the symbol diff --git a/crates/perry-codegen/src/expr/property_get.rs b/crates/perry-codegen/src/expr/property_get.rs index babaa38d46..2c9a3d87d5 100644 --- a/crates/perry-codegen/src/expr/property_get.rs +++ b/crates/perry-codegen/src/expr/property_get.rs @@ -929,6 +929,37 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // (the registry duplication bug was the first). if let Expr::ExternFuncRef { name, .. } = object.as_ref() { if ctx.namespace_imports.contains(name) { + // #7189: `B.deep` where the imported module says + // `export * as deep from "./m.ts"`. The member's value is + // another module's namespace OBJECT, so there is no + // `perry_fn___deep` symbol to call — every arm below + // resolves to a symbol, and this member does not have one. + // + // The object it should produce is the one + // `@__perry_ns_` already holds, built by the same + // populator a dynamic `import()` of that module would use. + // Reusing it means nested re-exports (a namespace whose own + // exports include another `export * as`) come out right + // without a second implementation, because the populator + // already recurses. + // + // First, ahead of the class and member-prefix arms: a + // namespace alias can collide with a class or function name + // exported elsewhere, and the alias is what the source said. + if ctx + .namespace_member_nested + .contains(&(name.clone(), property.to_string())) + { + if let Some(prefix) = ctx + .namespace_member_prefixes + .get(&(name.clone(), property.to_string())) + .cloned() + { + return Ok(crate::expr::dyn_extern_i18n::namespace_value_for_prefix( + ctx, &prefix, + )); + } + } // Issue #841: namespace member access for the five // recognized Node submodules — `import * as ns from // "node:timers/promises"; ns.setTimeout`. Resolve diff --git a/crates/perry-codegen/src/expr/property_get/tests.rs b/crates/perry-codegen/src/expr/property_get/tests.rs index e9651ef22e..d75d2c484b 100644 --- a/crates/perry-codegen/src/expr/property_get/tests.rs +++ b/crates/perry-codegen/src/expr/property_get/tests.rs @@ -33,6 +33,7 @@ fn ir_opts(debug_locations: bool, module_source: Option<&str>) -> CompileOptions verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), @@ -267,3 +268,95 @@ fn generic_property_get_tries_ways_before_calling_the_miss_handler() { "the megamorphic gate must read the way-state word:\n{ir}" ); } + +/// #7189 — `B.ns` where the imported module says `export * as ns from "./m.ts"`. +/// +/// The member's value is another module's namespace OBJECT, so there is no +/// `perry_fn___ns` symbol for it. Every other namespace-member arm +/// resolves to a symbol, so before this the read fell through to the generic +/// path and produced `undefined` — which is how `z.coerce`, `z.iso`, `z.core` +/// and `z.locales` all came back undefined under zod. +mod nested_namespace_members { + use super::*; + + fn nested_opts() -> CompileOptions { + let mut opts = ir_opts(false, None); + opts.namespace_imports = vec!["B".to_string()]; + opts.namespace_member_prefixes + .insert(("B".to_string(), "deep".to_string()), "ns2_ts".to_string()); + opts.namespace_member_prefixes + .insert(("B".to_string(), "gamma".to_string()), "ns3_ts".to_string()); + opts.namespace_member_nested = vec![("B".to_string(), "deep".to_string())]; + opts + } + + fn module_reading(member: &str) -> Module { + let mut m = Module::new("nsmain.ts"); + m.init = vec![Stmt::Expr(Expr::PropertyGet { + object: Box::new(Expr::ExternFuncRef { + name: "B".to_string(), + param_types: Vec::new(), + return_type: perry_hir::types::Type::Any, + }), + property: member.to_string(), + byte_offset: 0, + })]; + m.init_kind = ModuleInitKind::Eager; + m + } + + fn emit_read(member: &str) -> String { + String::from_utf8(compile_module(&module_reading(member), nested_opts()).unwrap()) + .expect("LLVM IR should be UTF-8") + } + + /// Slice out the body that actually runs the module's statements. + /// + /// Assertions have to be made HERE and not against the whole module. The + /// declaration pass emits `@__perry_ns_ns2_ts = external` on its own, so a + /// test that searched the whole IR passed with the read-site fix removed — + /// it was confirming the declaration existed, not that anything used it. + /// + /// For an entry module the statements land in `@main`; `__init` is an + /// empty stub. Slicing the stub is its own way of asserting nothing, which + /// is the mistake this helper exists to avoid making twice. + fn entry_body(ir: &str) -> String { + let start = ir.find("define i32 @main()").expect("main must be emitted"); + let end = ir[start..].find("\n}").expect("main must terminate") + start; + ir[start..end].to_string() + } + + #[test] + fn a_nested_namespace_member_loads_the_target_namespace_global() { + let ir = emit_read("deep"); + let body = entry_body(&ir); + assert!( + body.contains("load double, ptr @__perry_ns_ns2_ts"), + "the read must load the target module's namespace object:\n{body}" + ); + // The target's init has to run first, or the namespace is read before + // it has been populated and every member comes back undefined. + assert!( + body.contains("call void @ns2_ts__init()"), + "the target's init must run before its namespace is loaded:\n{body}" + ); + // The global lives in another module, so this one must declare it or + // LLVM refuses to parse the IR at all. + assert!( + ir.contains("@__perry_ns_ns2_ts = external"), + "the foreign namespace global must be declared, not just referenced:\n{ir}" + ); + } + + #[test] + fn an_ordinary_namespace_member_is_untouched() { + // The guard against over-reaching: a normal member still resolves the + // way it always did, through its origin module's symbol rather than a + // namespace object. + let body = entry_body(&emit_read("gamma")); + assert!( + !body.contains("load double, ptr @__perry_ns_ns3_ts"), + "a plain member must not be turned into a namespace load:\n{body}" + ); + } +} diff --git a/crates/perry-codegen/src/native_root_coverage/mod.rs b/crates/perry-codegen/src/native_root_coverage/mod.rs index e6929f41d3..3ada630dc1 100644 --- a/crates/perry-codegen/src/native_root_coverage/mod.rs +++ b/crates/perry-codegen/src/native_root_coverage/mod.rs @@ -131,6 +131,7 @@ pub(crate) fn ir_opts(target: &str, is_entry: bool) -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/src/stmt/class_field_loop_tests.rs b/crates/perry-codegen/src/stmt/class_field_loop_tests.rs index 20fb823b7f..fee95a7d92 100644 --- a/crates/perry-codegen/src/stmt/class_field_loop_tests.rs +++ b/crates/perry-codegen/src/stmt/class_field_loop_tests.rs @@ -50,6 +50,7 @@ fn ir_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/src/stmt/element_shape_loop_tests.rs b/crates/perry-codegen/src/stmt/element_shape_loop_tests.rs index d39ecbf469..ae2d4976ff 100644 --- a/crates/perry-codegen/src/stmt/element_shape_loop_tests.rs +++ b/crates/perry-codegen/src/stmt/element_shape_loop_tests.rs @@ -40,6 +40,7 @@ fn ir_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/src/stmt/prealloc_module_global_tests.rs b/crates/perry-codegen/src/stmt/prealloc_module_global_tests.rs index 9baf8b5950..cfa71925fd 100644 --- a/crates/perry-codegen/src/stmt/prealloc_module_global_tests.rs +++ b/crates/perry-codegen/src/stmt/prealloc_module_global_tests.rs @@ -58,6 +58,7 @@ fn ir_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/src/temp_root_coverage/mod.rs b/crates/perry-codegen/src/temp_root_coverage/mod.rs index c0bf93a5de..72a464a2b0 100644 --- a/crates/perry-codegen/src/temp_root_coverage/mod.rs +++ b/crates/perry-codegen/src/temp_root_coverage/mod.rs @@ -65,6 +65,7 @@ fn entry_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/src/type_analysis/numeric/tests.rs b/crates/perry-codegen/src/type_analysis/numeric/tests.rs index 77050dbc97..f5364f5e60 100644 --- a/crates/perry-codegen/src/type_analysis/numeric/tests.rs +++ b/crates/perry-codegen/src/type_analysis/numeric/tests.rs @@ -30,6 +30,7 @@ fn ir_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/app_window_config_options.rs b/crates/perry-codegen/tests/app_window_config_options.rs index c8cd34171c..a2e887720a 100644 --- a/crates/perry-codegen/tests/app_window_config_options.rs +++ b/crates/perry-codegen/tests/app_window_config_options.rs @@ -30,6 +30,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/argless_builtin_extra_args.rs b/crates/perry-codegen/tests/argless_builtin_extra_args.rs index 361b3f0a21..030dece5f6 100644 --- a/crates/perry-codegen/tests/argless_builtin_extra_args.rs +++ b/crates/perry-codegen/tests/argless_builtin_extra_args.rs @@ -26,6 +26,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/class_field_store_pointer_test.rs b/crates/perry-codegen/tests/class_field_store_pointer_test.rs index c4b4627ca1..075060b99d 100644 --- a/crates/perry-codegen/tests/class_field_store_pointer_test.rs +++ b/crates/perry-codegen/tests/class_field_store_pointer_test.rs @@ -36,6 +36,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/class_keys_gc_root.rs b/crates/perry-codegen/tests/class_keys_gc_root.rs index 3fac7354b5..8487b930c0 100644 --- a/crates/perry-codegen/tests/class_keys_gc_root.rs +++ b/crates/perry-codegen/tests/class_keys_gc_root.rs @@ -45,6 +45,7 @@ fn entry_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/constructor_recursion.rs b/crates/perry-codegen/tests/constructor_recursion.rs index a19354dbc3..0445c2f3f3 100644 --- a/crates/perry-codegen/tests/constructor_recursion.rs +++ b/crates/perry-codegen/tests/constructor_recursion.rs @@ -20,6 +20,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/destructure_call_location.rs b/crates/perry-codegen/tests/destructure_call_location.rs index 2d82e52b4e..f1481491f9 100644 --- a/crates/perry-codegen/tests/destructure_call_location.rs +++ b/crates/perry-codegen/tests/destructure_call_location.rs @@ -37,6 +37,7 @@ fn base_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/i64_spec_ternary_recursion.rs b/crates/perry-codegen/tests/i64_spec_ternary_recursion.rs index 61a935e8a2..afadc04af3 100644 --- a/crates/perry-codegen/tests/i64_spec_ternary_recursion.rs +++ b/crates/perry-codegen/tests/i64_spec_ternary_recursion.rs @@ -37,6 +37,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/large_object_barriers.rs b/crates/perry-codegen/tests/large_object_barriers.rs index ef342db57f..5a0d15f40a 100644 --- a/crates/perry-codegen/tests/large_object_barriers.rs +++ b/crates/perry-codegen/tests/large_object_barriers.rs @@ -20,6 +20,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/loop_safepoint_purity.rs b/crates/perry-codegen/tests/loop_safepoint_purity.rs index ee6f24261e..488c91d315 100644 --- a/crates/perry-codegen/tests/loop_safepoint_purity.rs +++ b/crates/perry-codegen/tests/loop_safepoint_purity.rs @@ -57,6 +57,7 @@ fn entry_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs b/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs index 56c611b5a4..18169626c5 100644 --- a/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs +++ b/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs @@ -26,6 +26,7 @@ fn entry_opts(target: Option<&str>) -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/native_proof_buffer_views.rs b/crates/perry-codegen/tests/native_proof_buffer_views.rs index d808f99cb3..e93519c3c1 100644 --- a/crates/perry-codegen/tests/native_proof_buffer_views.rs +++ b/crates/perry-codegen/tests/native_proof_buffer_views.rs @@ -45,6 +45,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index 01b7ae8df8..e411ff547f 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -42,6 +42,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/node_test_mock_property_presence.rs b/crates/perry-codegen/tests/node_test_mock_property_presence.rs index 118ea8e46e..f846b76806 100644 --- a/crates/perry-codegen/tests/node_test_mock_property_presence.rs +++ b/crates/perry-codegen/tests/node_test_mock_property_presence.rs @@ -23,6 +23,7 @@ fn ir_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/perry_builtin_name_collision.rs b/crates/perry-codegen/tests/perry_builtin_name_collision.rs index 624517d880..d6ebf81268 100644 --- a/crates/perry-codegen/tests/perry_builtin_name_collision.rs +++ b/crates/perry-codegen/tests/perry_builtin_name_collision.rs @@ -38,6 +38,7 @@ fn base_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/scalar_replaced_slot_roots.rs b/crates/perry-codegen/tests/scalar_replaced_slot_roots.rs index ff043dcdad..a9aae62d21 100644 --- a/crates/perry-codegen/tests/scalar_replaced_slot_roots.rs +++ b/crates/perry-codegen/tests/scalar_replaced_slot_roots.rs @@ -77,6 +77,7 @@ fn entry_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/shadow_slot_hygiene.rs b/crates/perry-codegen/tests/shadow_slot_hygiene.rs index 885e026eb6..31f1d0e037 100644 --- a/crates/perry-codegen/tests/shadow_slot_hygiene.rs +++ b/crates/perry-codegen/tests/shadow_slot_hygiene.rs @@ -50,6 +50,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/static_symbol_hygiene.rs b/crates/perry-codegen/tests/static_symbol_hygiene.rs index b7712ab1c0..558f28152e 100644 --- a/crates/perry-codegen/tests/static_symbol_hygiene.rs +++ b/crates/perry-codegen/tests/static_symbol_hygiene.rs @@ -20,6 +20,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/temp_root_operand_temporaries.rs b/crates/perry-codegen/tests/temp_root_operand_temporaries.rs index e9fc9804db..e65cfd0463 100644 --- a/crates/perry-codegen/tests/temp_root_operand_temporaries.rs +++ b/crates/perry-codegen/tests/temp_root_operand_temporaries.rs @@ -75,6 +75,7 @@ fn entry_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/typed_feedback.rs b/crates/perry-codegen/tests/typed_feedback.rs index 82bd5e0adc..ed5a368c1f 100644 --- a/crates/perry-codegen/tests/typed_feedback.rs +++ b/crates/perry-codegen/tests/typed_feedback.rs @@ -101,6 +101,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs b/crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs index 7b08d84daf..4399ee9fe3 100644 --- a/crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs +++ b/crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs @@ -45,6 +45,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/typed_shape_descriptor.rs b/crates/perry-codegen/tests/typed_shape_descriptor.rs index 52dae0c4d7..1dbe6c89c5 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptor.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptor.rs @@ -20,6 +20,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry-codegen/tests/typed_shape_descriptors.rs b/crates/perry-codegen/tests/typed_shape_descriptors.rs index e811a0e078..abf10582c4 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptors.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptors.rs @@ -23,6 +23,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs b/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs index 1f62918914..dd54983291 100644 --- a/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs +++ b/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs @@ -29,6 +29,7 @@ fn empty_opts() -> CompileOptions { verify_native_regions: false, disable_buffer_fast_path: false, namespace_imports: Vec::new(), + namespace_member_nested: Vec::new(), imported_classes: Vec::new(), imported_enums: Vec::new(), imported_async_funcs: std::collections::HashSet::new(), diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index f8be61907e..e0de04fc4f 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -1915,6 +1915,46 @@ pub fn run_with_parse_cache( } } } + // #7189: `export * as ns from "./m.ts"` names a member whose VALUE is + // another module's namespace object. That object is exactly what + // `@__perry_ns_` already holds for dynamic-import targets, and + // `emit_namespace_populator` already builds it correctly — including nested + // namespaces of its own, recursively. So rather than invent a second way to + // materialize one, mark every namespace-re-export TARGET as needing that + // global, and point the member at it. + // + // Keyed by re-exporting module path → exported alias → target module path. + let mut namespace_reexport_targets: HashMap> = HashMap::new(); + for (path, hir_module) in &ctx.native_modules { + for export in &hir_module.exports { + let perry_hir::Export::NamespaceReExport { source, name } = export else { + continue; + }; + // `source` was rewritten to `Module::name` on `module_name_to_module` + // above, but `hir_module` here is the ORIGINAL, so this is still the + // specifier as written. + let Some((target_path, _)) = resolve_import( + source, + path, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) else { + continue; + }; + if !ctx.native_modules.contains_key(&target_path) { + continue; + } + namespace_reexport_targets + .entry(path.to_string_lossy().to_string()) + .or_default() + .insert(name.clone(), target_path.clone()); + // The target needs its own `@__perry_ns_` global emitted and + // populated, which is what being in this set means. + dyn_target_paths.insert(target_path); + } + } + // Per-module precomputed namespace_entries (keyed by path). let mut per_module_namespace_entries: HashMap> = HashMap::new(); @@ -2443,6 +2483,13 @@ pub fn run_with_parse_cache( std::collections::HashMap<(String, String), String> = std::collections::HashMap::new(); let mut namespace_imports: Vec = Vec::new(); + // #7189: members of a namespace whose value is ITSELF a module + // namespace, from `export * as ns from "./m.ts"` in the imported + // module. They resolve to `@__perry_ns_` rather than to a + // `perry_fn___` symbol, because no such symbol exists — + // the member is a whole module, not a binding in one. + let mut namespace_member_nested: std::collections::HashSet<(String, String)> = + std::collections::HashSet::new(); let mut imported_classes: Vec = Vec::new(); let mut imported_enums: Vec<(String, Vec<(String, perry_hir::EnumValue)>)> = Vec::new(); let mut imported_async_set: std::collections::HashSet = @@ -2711,6 +2758,24 @@ pub fn run_with_parse_cache( } } } + // #7189: the source module's `export * as ns` aliases. + // These are NOT in `all_module_exports` — that map holds + // name → module-that-declares-the-binding, and a + // namespace alias has no declaring binding to point at. + // Registering the alias here is what puts it in + // `Object.keys(ns)`, since the whole-value materializer + // enumerates `namespace_member_prefixes`. + if let Some(nested) = namespace_reexport_targets.get(&resolved_path_str) { + for (alias, target_path) in nested { + let target_prefix = + compute_module_prefix(&target_path.to_string_lossy(), &ctx.project_root); + namespace_member_prefixes.insert( + (local.clone(), alias.clone()), + target_prefix, + ); + namespace_member_nested.insert((local.clone(), alias.clone())); + } + } // Register all exports from the source module if let Some(exports) = all_module_exports.get(&resolved_path_str) { for (export_name, origin_path) in exports { @@ -4184,6 +4249,7 @@ pub fn run_with_parse_cache( verify_native_regions, disable_buffer_fast_path, namespace_imports, + namespace_member_nested: namespace_member_nested.into_iter().collect(), imported_classes, imported_enums, imported_async_funcs: imported_async_set, diff --git a/test-files/issue_7189_namespace_object_reexport/leaf.ts b/test-files/issue_7189_namespace_object_reexport/leaf.ts new file mode 100644 index 0000000000..f0670d02d2 --- /dev/null +++ b/test-files/issue_7189_namespace_object_reexport/leaf.ts @@ -0,0 +1,2 @@ +export const alpha = 1; +export function beta(): number { return 2; } diff --git a/test-files/issue_7189_namespace_object_reexport/mid.ts b/test-files/issue_7189_namespace_object_reexport/mid.ts new file mode 100644 index 0000000000..e59a428d18 --- /dev/null +++ b/test-files/issue_7189_namespace_object_reexport/mid.ts @@ -0,0 +1,2 @@ +export * as deep from "./leaf.ts"; +export const gamma = 3; diff --git a/test-files/issue_7189_namespace_object_reexport/outer.ts b/test-files/issue_7189_namespace_object_reexport/outer.ts new file mode 100644 index 0000000000..b737d7eb44 --- /dev/null +++ b/test-files/issue_7189_namespace_object_reexport/outer.ts @@ -0,0 +1,2 @@ +export * as inner from "./mid.ts"; +export const delta = 4; diff --git a/test-files/test_gap_export_star_as_namespace_object.ts b/test-files/test_gap_export_star_as_namespace_object.ts new file mode 100644 index 0000000000..9d5c208ee4 --- /dev/null +++ b/test-files/test_gap_export_star_as_namespace_object.ts @@ -0,0 +1,26 @@ +// #7189: `export * as ns from "./m.ts"` must appear on the re-exporting +// module's NAMESPACE OBJECT, not only as a named binding. Before the fix +// `B.deep` was `undefined` and `Object.keys(B)` omitted it entirely, while +// `import { deep }` of the same thing worked — which is the shape that made +// zod's `z.coerce` / `z.iso` / `z.core` / `z.locales` all undefined. +import * as B from "./issue_7189_namespace_object_reexport/mid.ts"; +import * as C from "./issue_7189_namespace_object_reexport/outer.ts"; +import { deep } from "./issue_7189_namespace_object_reexport/mid.ts"; + +console.log("typeof B.deep :", typeof B.deep); +console.log("keys(B) :", JSON.stringify(Object.keys(B).sort())); +console.log("B.gamma :", B.gamma); + +// The named import of the same binding always worked; it must keep working. +console.log("typeof deep :", typeof deep, deep.alpha); + +// Members reached THROUGH the namespace object. +console.log("B.deep.alpha :", B.deep.alpha); +console.log("B.deep.beta() :", B.deep.beta()); +console.log("keys(B.deep) :", JSON.stringify(Object.keys(B.deep).sort())); + +// Two levels: a namespace re-export whose target itself re-exports one. +console.log("C.inner.gamma :", C.inner.gamma); +console.log("C.inner.deep.alpha :", C.inner.deep.alpha); +console.log("keys(C) :", JSON.stringify(Object.keys(C).sort())); +console.log("C.delta :", C.delta);