diff --git a/changelog.d/9887-fs-promises-namespace-identity.md b/changelog.d/9887-fs-promises-namespace-identity.md new file mode 100644 index 0000000000..8c9db28b17 --- /dev/null +++ b/changelog.d/9887-fs-promises-namespace-identity.md @@ -0,0 +1,3 @@ +### Fixed + +- Reuse the canonical `fs/promises` and `stream/promises` namespace objects when their native-module references become values. diff --git a/crates/perry-codegen/src/expr/property_get/tests.rs b/crates/perry-codegen/src/expr/property_get/tests.rs index c57e8d35ac..e23438090e 100644 --- a/crates/perry-codegen/src/expr/property_get/tests.rs +++ b/crates/perry-codegen/src/expr/property_get/tests.rs @@ -315,6 +315,27 @@ fn fs_parent_promises_property_installs_before_resolution() { ); } +#[test] +fn fs_promises_native_module_value_uses_submodule_singleton() { + let mut module = Module::new("fs_promises_native_module_value.ts"); + module.init = vec![Stmt::Return(Some(Expr::NativeModuleRef( + "fs/promises".to_string(), + )))]; + + let ir = String::from_utf8(compile_module(&module, ir_opts(false, None)).unwrap()) + .expect("LLVM IR should be UTF-8"); + let install = ir + .find("call void @js_node_submod_install_fs_promises()") + .unwrap_or_else(|| panic!("fs/promises must emit its submodule installer:\n{ir}")); + let namespace = ir + .find("call double @js_node_submodule_namespace") + .unwrap_or_else(|| panic!("fs/promises must use its submodule singleton:\n{ir}")); + assert!( + install < namespace, + "fs/promises installation must precede namespace creation:\n{ir}" + ); +} + /// #7753, paired with `pic_cache_words_match_codegen` in /// `perry-runtime/src/object/field_get_set/ic_miss.rs`. /// diff --git a/crates/perry-codegen/src/expr/static_field_meta.rs b/crates/perry-codegen/src/expr/static_field_meta.rs index e768a9685d..421c055eb6 100644 --- a/crates/perry-codegen/src/expr/static_field_meta.rs +++ b/crates/perry-codegen/src/expr/static_field_meta.rs @@ -910,6 +910,22 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // require("node:fs"); fs.constants` (call-result shape, fallback // path here) both produce a real namespace object. Expr::NativeModuleRef(name) => { + if let Some(submod_key) = crate::nm_install::native_namespace_submodule_key(name) { + let submod_idx = ctx.strings.intern(submod_key); + let submod_bytes_global = + format!("@{}", ctx.strings.entry(submod_idx).bytes_global); + let submod_len = submod_key.len().to_string(); + let install_sym = crate::nm_install::nm_submod_install_symbol(submod_key); + let blk = ctx.block(); + if let Some(symbol) = install_sym { + blk.call_void(symbol, &[]); + } + return Ok(blk.call( + DOUBLE, + "js_node_submodule_namespace", + &[(PTR, &submod_bytes_global), (I32, &submod_len)], + )); + } let mod_idx = ctx.strings.intern(name); let mod_bytes_global = format!("@{}", ctx.strings.entry(mod_idx).bytes_global); let mod_len_str = name.len().to_string(); diff --git a/crates/perry-codegen/src/lower_call/native/mod.rs b/crates/perry-codegen/src/lower_call/native/mod.rs index 7b384c1e47..8c255a1bd9 100644 --- a/crates/perry-codegen/src/lower_call/native/mod.rs +++ b/crates/perry-codegen/src/lower_call/native/mod.rs @@ -480,12 +480,7 @@ pub(crate) fn lower_native_method_call( // TAG_UNDEFINED sentinel below and `promises.realpath(p)` resolved // `undefined` (the compiled CLI's file cache then normalized every path to // `undefined` and each later fs call threw). - let normalized_module = module.strip_prefix("node:").unwrap_or(module); - let promises_submod_key = match normalized_module { - "fs/promises" => Some("fs_promises"), - "stream/promises" => Some("stream_promises"), - _ => None, - }; + let promises_submod_key = crate::nm_install::native_namespace_submodule_key(module); if let Some(submod_key) = promises_submod_key { let submod_label = crate::expr::emit_string_literal_global(ctx, submod_key); let install_sym = crate::nm_install::nm_submod_install_symbol(submod_key); diff --git a/crates/perry-codegen/src/nm_install.rs b/crates/perry-codegen/src/nm_install.rs index d742be0543..fab6d26474 100644 --- a/crates/perry-codegen/src/nm_install.rs +++ b/crates/perry-codegen/src/nm_install.rs @@ -151,6 +151,16 @@ pub(crate) fn nm_submod_install_symbol(key: &str) -> Option<&'static str> { } } +/// Native-module spellings whose value is implemented by the node-submodule +/// registry rather than a generic native-module namespace object. +pub(crate) fn native_namespace_submodule_key(name: &str) -> Option<&'static str> { + match name.strip_prefix("node:").unwrap_or(name) { + "fs/promises" => Some("fs_promises"), + "stream/promises" => Some("stream_promises"), + _ => None, + } +} + #[allow(dead_code)] // consumed only by codegen configurations that emit dispatch declarations pub(crate) const NM_SUBMOD_INSTALL_SYMBOLS: &[&str] = &[ "js_node_submod_install_vm",