From 149c1a65890448a6ff876cc8a80c505ead644ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 13 Sep 2026 06:41:05 +0200 Subject: [PATCH] fix(codegen): publish self namespace re-exports as live accessors (#10160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `export * as Self from "./self"` lowered to a NestedNamespace entry whose value was a plain load of the module's own `@__perry_ns_` global — inside the populator that builds that very namespace, before the global is stored. The dynamic `import()` namespace therefore carried the key with an `undefined` value while the static import path still resolved. OpenCode uses this self re-export in 265 modules and reads it through dynamic imports in its command bootstrap, so every real command failed with `Cannot read properties of undefined (reading 'Service')`. Self-referencing entries are now published like LocalVar/ForeignVar live bindings: the populator marks them live and hands `js_create_namespace` a getter singleton, and the getter wrapper loads the namespace global at read time. Foreign `export * as` entries keep the direct load. Claude-Session: https://claude.ai/code/session_01GixUo7gwcatEk4kibdeCWF --- .../10160-self-namespace-dynamic-import.md | 3 + crates/perry-codegen/src/codegen/artifacts.rs | 17 ++++ crates/perry-codegen/src/codegen/helpers.rs | 27 ++++++- .../tests/source_graph_export_regressions.rs | 2 + .../issue_10160.rs | 77 +++++++++++++++++++ 5 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 changelog.d/10160-self-namespace-dynamic-import.md create mode 100644 crates/perry/tests/source_graph_export_regressions/issue_10160.rs diff --git a/changelog.d/10160-self-namespace-dynamic-import.md b/changelog.d/10160-self-namespace-dynamic-import.md new file mode 100644 index 0000000000..209c2e5708 --- /dev/null +++ b/changelog.d/10160-self-namespace-dynamic-import.md @@ -0,0 +1,3 @@ +### Fixed + +- `export * as Self from "./self"` now reads as the module's own namespace object through a dynamic `import()` namespace (and its destructuring); the entry was `undefined` because the populator loaded the module's namespace global before it was stored (#10160). diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index fcfd8ec434..6d39180555 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -1356,6 +1356,23 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { blk.ret(DOUBLE, &value); continue; } + crate::NamespaceEntryKind::NestedNamespace { source_prefix } + if source_prefix == module_prefix => + { + // #10160: `export * as Self from "./self"` reads this + // module's own namespace global at access time; the + // populator publishes the entry as a live accessor. + let wrapper = llmod.define_function( + &wrapper_name, + DOUBLE, + vec![(I64, "%this_closure".to_string())], + ); + let _ = wrapper.create_block("entry"); + let blk = wrapper.block_mut(0).unwrap(); + let value = blk.load(DOUBLE, &format!("@__perry_ns_{module_prefix}")); + blk.ret(DOUBLE, &value); + continue; + } crate::NamespaceEntryKind::ForeignVar { source_prefix, source_local, diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index 39e1bc74e5..aa7ff04a61 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -1428,10 +1428,21 @@ pub(super) fn emit_namespace_populator( let len_slot = blk.gep(I32, &lens_buf, &[(I64, &idx_str)]); blk.store(I32, &format!("{}", key_len), &len_slot); - let is_live_binding = matches!( - entry.kind, - NamespaceEntryKind::LocalVar { .. } | NamespaceEntryKind::ForeignVar { .. } + // #10160: `export * as Self from "./self"` — this module's own + // `@__perry_ns_` is only stored after `js_create_namespace` + // returns below, so loading it here would freeze `undefined` into + // the entry. Publish it as a live accessor instead; its getter + // wrapper (emitted next to the LocalVar wrappers in artifacts.rs) + // loads the global at read time. + let is_self_namespace = matches!( + &entry.kind, + NamespaceEntryKind::NestedNamespace { source_prefix } if source_prefix == module_prefix ); + let is_live_binding = is_self_namespace + || matches!( + entry.kind, + NamespaceEntryKind::LocalVar { .. } | NamespaceEntryKind::ForeignVar { .. } + ); let live_slot = blk.gep(I8, &live_buf, &[(I64, &idx_str)]); blk.store(I8, if is_live_binding { "1" } else { "0" }, &live_slot); @@ -1496,6 +1507,16 @@ pub(super) fn emit_namespace_populator( ); crate::expr::nanbox_pointer_inline(blk, &handle) } + NamespaceEntryKind::NestedNamespace { .. } if is_self_namespace => { + let wrapper = namespace_live_getter_wrapper_symbol(module_prefix, i); + let blk = ctx.block(); + let handle = blk.call( + I64, + "js_closure_alloc_singleton", + &[(PTR, &format!("@{}", wrapper))], + ); + crate::expr::nanbox_pointer_inline(blk, &handle) + } NamespaceEntryKind::NestedNamespace { source_prefix } => ctx .block() .load(DOUBLE, &format!("@__perry_ns_{}", source_prefix)), diff --git a/crates/perry/tests/source_graph_export_regressions.rs b/crates/perry/tests/source_graph_export_regressions.rs index ceade525bc..12c85b80fe 100644 --- a/crates/perry/tests/source_graph_export_regressions.rs +++ b/crates/perry/tests/source_graph_export_regressions.rs @@ -954,3 +954,5 @@ fn mixed_type_and_value_specifier_import_keeps_runtime_edge() { "dependency initialized\n42\n" ); } +#[path = "source_graph_export_regressions/issue_10160.rs"] +mod issue_10160; diff --git a/crates/perry/tests/source_graph_export_regressions/issue_10160.rs b/crates/perry/tests/source_graph_export_regressions/issue_10160.rs new file mode 100644 index 0000000000..19a4fafe1c --- /dev/null +++ b/crates/perry/tests/source_graph_export_regressions/issue_10160.rs @@ -0,0 +1,77 @@ +//! `export * as Self from "./self"` must read as the module's own namespace +//! through a dynamic `import()` namespace, not as `undefined` (#10160). + +use super::{compile_and_run, write}; + +fn store(dir: &std::path::Path) { + write( + dir, + "store.ts", + "export class Service {\n\ + \x20 static use(f: (s: string) => string) { return f(\"store-ok\") }\n\ + }\n\ + export const node = \"node-layer\"\n\ + export * as InstanceStore from \"./store\"\n", + ); +} + +#[test] +fn self_namespace_reexport_is_defined_on_a_dynamic_import_namespace() { + let dir = tempfile::tempdir().unwrap(); + store(dir.path()); + write( + dir.path(), + "main.ts", + "import { InstanceStore as Static } from \"./store\"\n\ + console.log(typeof Static, typeof Static.Service, Static.Service.use((s) => s))\n\ + const mod = await import(\"./store\")\n\ + console.log(Object.keys(mod).sort().join(\",\"))\n\ + const { InstanceStore } = mod\n\ + console.log(typeof InstanceStore, typeof InstanceStore.Service, InstanceStore.Service.use((s) => s + \"!\"))\n\ + console.log(InstanceStore.InstanceStore === InstanceStore, InstanceStore.node, Static.node)\n", + ); + assert_eq!( + compile_and_run(dir.path(), "main.ts"), + "object function store-ok\n\ + InstanceStore,Service,node\n\ + object function store-ok!\n\ + true node-layer node-layer\n" + ); +} + +#[test] +fn self_namespace_reexport_is_defined_on_a_static_star_import() { + let dir = tempfile::tempdir().unwrap(); + store(dir.path()); + write( + dir.path(), + "main.ts", + "import * as ns from \"./store\"\n\ + console.log(typeof ns.InstanceStore, ns.InstanceStore.Service.use((s) => s), ns.InstanceStore.node)\n", + ); + assert_eq!( + compile_and_run(dir.path(), "main.ts"), + "object store-ok node-layer\n" + ); +} + +#[test] +fn foreign_namespace_reexport_stays_intact_on_a_dynamic_import_namespace() { + let dir = tempfile::tempdir().unwrap(); + write(dir.path(), "other.ts", "export const value = 42\n"); + write( + dir.path(), + "barrel.ts", + "export * as Other from \"./other\"\nexport * as Barrel from \"./barrel\"\n", + ); + write( + dir.path(), + "main.ts", + "const mod = await import(\"./barrel\")\n\ + console.log(typeof mod.Other, mod.Other.value, typeof mod.Barrel, mod.Barrel.Other.value)\n", + ); + assert_eq!( + compile_and_run(dir.path(), "main.ts"), + "object 42 object 42\n" + ); +}