diff --git a/crates/perry-hir/src/lower/expr_call/native_module.rs b/crates/perry-hir/src/lower/expr_call/native_module.rs index 523242d3c5..524f14501a 100644 --- a/crates/perry-hir/src/lower/expr_call/native_module.rs +++ b/crates/perry-hir/src/lower/expr_call/native_module.rs @@ -37,6 +37,11 @@ fn is_cluster_default_event_emitter_method(method_name: &str) -> bool { ) } +/// Internal process helpers that return arrays through native dispatch. +fn is_process_active_array_helper(method: &str) -> bool { + matches!(method, "_getActiveHandles" | "_getActiveRequests") +} + /// Peel runtime-transparent TypeScript wrappers (`as`, `as const`, `!`, /// `satisfies`, angle-bracket assertions, parens) off an expression so a /// cast receiver like `(Readable as any).toWeb(...)` still matches the @@ -380,6 +385,15 @@ pub(super) fn try_native_module_methods( args, })); } + method_name if is_process_active_array_helper(method_name) => { + return Ok(Ok(Expr::NativeMethodCall { + module: "process".to_string(), + class_name: None, + object: None, + method: method_name.to_string(), + args, + })); + } "setSourceMapsEnabled" => { // #1400 / #3108: process.setSourceMapsEnabled(bool) // toggles the live source-map flag. Perry compiles @@ -1860,7 +1874,14 @@ pub(super) fn try_native_module_methods( #[cfg(test)] mod bundled_mysql2_tests { - use super::mysql2_config_signature; + use super::{is_process_active_array_helper, mysql2_config_signature}; + + #[test] + fn process_active_array_helper_predicate_matches_supported_methods() { + assert!(is_process_active_array_helper("_getActiveHandles")); + assert!(is_process_active_array_helper("_getActiveRequests")); + assert!(!is_process_active_array_helper("getActiveResourcesInfo")); + } #[test] fn matches_pool_with_uri_and_pool_option() { diff --git a/crates/perry-runtime/src/child_process/options.rs b/crates/perry-runtime/src/child_process/options.rs index e60b2287a1..192549960a 100644 --- a/crates/perry-runtime/src/child_process/options.rs +++ b/crates/perry-runtime/src/child_process/options.rs @@ -294,6 +294,29 @@ fn cp_default_shell() -> String { } } +/// Whether a self-launch uses a Node CLI mode that evaluates source text. +fn cp_should_use_node_interpreter(cmd: &str, args: &[String]) -> bool { + let is_self = std::env::args().next().as_deref() == Some(cmd) + || std::env::current_exe().is_ok_and(|current| current == std::path::Path::new(cmd)); + is_self + && args + .iter() + .take_while(|arg| arg.as_str() != "--" && arg.starts_with('-')) + .any(|arg| { + matches!(arg.as_str(), "-e" | "--eval" | "-p" | "--print") + || arg.starts_with("--eval=") + || arg.starts_with("--print=") + }) +} + +/// Node interpreter used for source-evaluating self-launches. +fn cp_default_node_interpreter() -> String { + std::env::var("PERRY_FORK_EXECPATH") + .ok() + .filter(|path| !path.is_empty()) + .unwrap_or_else(|| "node".to_string()) +} + /// Build a `Command` for `spawn(cmd, args, opts)`, honoring the `shell` option /// (Node joins `cmd` + `args` into a single line passed to ` -c`) and /// then applying `cwd`/`env`. With no `shell` the file is run directly. #1780. @@ -304,13 +327,22 @@ pub(crate) fn cp_build_command(cmd: &str, args: &[String], opts_val: f64) -> Com cp_undefined() }; + // A compiled Perry program is not a Node CLI, so relaunching itself with + // `-e` would rerun its AOT entry point. Use the same configurable Node + // interpreter as `fork()` for any eval source passed through execPath. + let program = if cp_should_use_node_interpreter(cmd, args) { + cp_default_node_interpreter() + } else { + cmd.to_string() + }; + let mut command = if crate::value::js_is_truthy(shell) != 0 { // `shell: ""` picks the binary; `shell: true` uses the default. let shell_bin = match cp_value_to_string(shell) { Some(s) if !s.is_empty() => s, _ => cp_default_shell(), }; - let mut line = String::from(cmd); + let mut line = program.clone(); for a in args { line.push(' '); line.push_str(a); @@ -322,7 +354,7 @@ pub(crate) fn cp_build_command(cmd: &str, args: &[String], opts_val: f64) -> Com c.arg("-c").arg(line); c } else { - let mut c = Command::new(cmd); + let mut c = Command::new(program); c.args(args); c }; @@ -332,3 +364,49 @@ pub(crate) fn cp_build_command(cmd: &str, args: &[String], opts_val: f64) -> Com cp_apply_detached(&mut command, opts_val); command } + +#[cfg(test)] +mod tests { + use super::cp_should_use_node_interpreter; + + #[test] + fn self_exec_node_cli_eval_modes_use_node_interpreter() { + let current = std::env::current_exe().expect("current executable"); + let current = current.to_string_lossy(); + + assert!(cp_should_use_node_interpreter( + ¤t, + &["-e".to_string(), "console.log(42)".to_string()], + )); + assert!(cp_should_use_node_interpreter( + ¤t, + &["--eval=console.log(43)".to_string()], + )); + assert!(cp_should_use_node_interpreter( + ¤t, + &[ + "--no-warnings".to_string(), + "--eval".to_string(), + "console.log(44)".to_string(), + ], + )); + for flag in ["-p", "--print"] { + assert!(cp_should_use_node_interpreter( + ¤t, + &[flag.to_string(), "40 + 2".to_string()], + )); + } + assert!(cp_should_use_node_interpreter( + ¤t, + &["--print=40 + 2".to_string()], + )); + assert!(!cp_should_use_node_interpreter( + ¤t, + &["ordinary-argument".to_string()], + )); + assert!(!cp_should_use_node_interpreter( + "some-other-program", + &["-e".to_string(), "console.log(45)".to_string()], + )); + } +} diff --git a/crates/perry/src/commands/compile/collect_modules/feature_detect.rs b/crates/perry/src/commands/compile/collect_modules/feature_detect.rs index 2acd0ea704..f6848f73b6 100644 --- a/crates/perry/src/commands/compile/collect_modules/feature_detect.rs +++ b/crates/perry/src/commands/compile/collect_modules/feature_detect.rs @@ -29,6 +29,12 @@ fn debug_hir_uses_regex(hir_debug: &str) -> bool { || hir_debug.contains("property: \"globSync\"") } +fn debug_hir_uses_get_builtin_module(hir_debug: &str) -> bool { + hir_debug.contains("property: \"getBuiltinModule\"") + || (hir_debug.contains("module: \"process\"") + && hir_debug.contains("method: \"getBuiltinModule\"")) +} + fn imports_fs_promises_glob(hir_module: &perry_hir::Module) -> bool { hir_module.imports.iter().any(|import| { !import.type_only @@ -319,7 +325,10 @@ pub(super) fn detect_optional_feature_usage( // diagnostics (GC-diag / typed-feedback JSON) ride the same feature and // degrade gracefully when off, so they need no detection. { - let hir_debug: String = format!("{:?}{:?}", &hir_module.init, &hir_module.functions); + let hir_debug: String = format!( + "{:?}{:?}{:?}", + &hir_module.init, &hir_module.functions, &hir_module.classes + ); if hir_debug.contains("method: \"getHeapSnapshot\"") || hir_debug.contains("method: \"writeHeapSnapshot\"") || hir_debug.contains("property: \"report\"") @@ -332,6 +341,9 @@ pub(super) fn detect_optional_feature_usage( if hir_debug.contains("module: \"dgram\"") { ctx.uses_dgram = true; } + if debug_hir_uses_get_builtin_module(&hir_debug) { + ctx.uses_get_builtin_module = true; + } } // Detect readline usage via process.stdin raw/lifecycle methods. These @@ -373,7 +385,9 @@ pub(super) fn detect_optional_feature_usage( #[cfg(test)] mod tests { - use super::{debug_hir_uses_regex, imports_fs_promises_glob}; + use super::{ + debug_hir_uses_get_builtin_module, debug_hir_uses_regex, imports_fs_promises_glob, + }; use perry_hir::{Import, ImportSpecifier, Module, ModuleKind}; #[test] @@ -386,6 +400,19 @@ mod tests { )); } + #[test] + fn get_builtin_module_gate_detects_direct_and_extracted_calls() { + assert!(debug_hir_uses_get_builtin_module( + r#"NativeMethodCall { module: "process", method: "getBuiltinModule" }"# + )); + assert!(debug_hir_uses_get_builtin_module( + r#"PropertyGet { property: "getBuiltinModule" }"# + )); + assert!(!debug_hir_uses_get_builtin_module( + r#"NativeMethodCall { module: "process", method: "cwd" }"# + )); + } + #[test] fn fs_promises_glob_gate_uses_import_provenance() { let mut module = Module::new("entry.ts"); diff --git a/crates/perry/src/commands/compile/optimized_libs/freshness.rs b/crates/perry/src/commands/compile/optimized_libs/freshness.rs index 25721af4d5..f2189cb339 100644 --- a/crates/perry/src/commands/compile/optimized_libs/freshness.rs +++ b/crates/perry/src/commands/compile/optimized_libs/freshness.rs @@ -12,6 +12,10 @@ use crate::OutputFormat; use super::super::library_search::{find_harmonyos_sdk, harmonyos_cross_env}; use super::super::{find_perry_workspace_root, rust_target_triple, CompilationContext}; +fn needs_http2_constants(ctx: &CompilationContext) -> bool { + ctx.native_module_imports.contains("http2") || ctx.uses_get_builtin_module +} + pub(crate) fn auto_optimized_archives_are_fresh( workspace_root: &Path, runtime_path: &Path, @@ -77,10 +81,10 @@ pub(crate) fn auto_optimized_cache_key( ctx.uses_intl_locale, ctx.uses_diagnostics, ctx.uses_dgram, - // #6468: an http2 import pulls in `perry-runtime/mod-http2-constants`, - // so a runtime built without the constant tables must not be reused for - // an http2 program — key the freshness stamp on it like the other gates. - ctx.native_module_imports.contains("http2"), + // HTTP/2 imports and dynamic builtin resolution pull in + // `perry-runtime/mod-http2-constants`, so key the cache on the shared + // gate like the other optional runtime features. + needs_http2_constants(ctx), // #6559: dyn-eval presence changes the built archive, so it must // key the freshness stamp like every other runtime feature toggle. perry_hir::has_deferred_dynamic_code_sites(), @@ -155,13 +159,10 @@ pub(crate) fn auto_optimized_cross_features( if ctx.uses_dgram { cross_features.push("perry-runtime/mod-dgram".to_string()); } - // #6468 — the `node:http2` constant tables (`node_http2_constants`, ~20 KB - // of NGHTTP2_*/HTTP_STATUS_* cold data) are only reachable through the http2 - // namespace object, which only exists when the program imports `node:http2`. - // `http2` is a stdlib-backed module, so its import is recorded in - // `native_module_imports` — a reliable, zero-false-negative activation - // signal. A program that never imports it links none of the tables. - if ctx.native_module_imports.contains("http2") { + // #6468 — keep the `node:http2` constant tables (~20 KB) when source imports + // `node:http2` or calls `process.getBuiltinModule`, whose target is only + // known at runtime. Programs using neither path still link none of them. + if needs_http2_constants(ctx) { cross_features.push("perry-runtime/mod-http2-constants".to_string()); } // #6559: a deferred dynamic-code site (`eval(...)` / `new Function(...)` diff --git a/crates/perry/src/commands/compile/optimized_libs/tests.rs b/crates/perry/src/commands/compile/optimized_libs/tests.rs index a523e7559d..5b20bb0ff2 100644 --- a/crates/perry/src/commands/compile/optimized_libs/tests.rs +++ b/crates/perry/src/commands/compile/optimized_libs/tests.rs @@ -372,6 +372,16 @@ fn http2_import_enables_http2_constants_cross_feature() { .any(|f| f == "perry-runtime/mod-http2-constants"), "no http2 import should leave mod-http2-constants off, got {cross_off:?}" ); + + let mut dynamic = CompilationContext::new(dir.path().to_path_buf()); + dynamic.uses_get_builtin_module = true; + let dynamic_features = auto_optimized_cross_features(&dynamic, &empty_features, &[]); + assert!( + dynamic_features + .iter() + .any(|f| f == "perry-runtime/mod-http2-constants"), + "getBuiltinModule should enable mod-http2-constants, got {dynamic_features:?}" + ); } #[test] @@ -392,6 +402,14 @@ fn http2_import_changes_optimized_libs_cache_key() { key_without, key_with, "an http2 import must change the auto-optimized cache key" ); + + let mut dynamic = CompilationContext::new(dir.path().to_path_buf()); + dynamic.uses_get_builtin_module = true; + assert_ne!( + key_without, + auto_optimized_cache_key("", true, None, &dynamic), + "getBuiltinModule must change the auto-optimized cache key" + ); } #[test] diff --git a/crates/perry/src/commands/compile/types.rs b/crates/perry/src/commands/compile/types.rs index 64bb7a9353..1d837aefd8 100644 --- a/crates/perry/src/commands/compile/types.rs +++ b/crates/perry/src/commands/compile/types.rs @@ -703,6 +703,10 @@ pub struct CompilationContext { /// none of it. NB: not via `native_module_imports`, which only tracks /// `requires_stdlib` modules — dgram is runtime-only. pub uses_dgram: bool, + /// Whether any module calls `process.getBuiltinModule`. The requested + /// module is only known at runtime, so auto-optimized runtimes must retain + /// optional builtin namespace data such as the HTTP/2 key tables. + pub uses_get_builtin_module: bool, /// Whether `perry/thread` is imported. When true, the runtime must /// keep `panic = "unwind"` so that worker-thread panics translate to /// promise rejections via `catch_unwind` in `perry-runtime/src/thread.rs` @@ -1028,6 +1032,7 @@ impl CompilationContext { uses_intl_datetime: false, uses_diagnostics: false, uses_dgram: false, + uses_get_builtin_module: false, needs_thread: false, cross_module_class_field_types: HashMap::new(), cross_module_class_accessors: HashMap::new(), diff --git a/crates/perry/tests/createrequire_builtin_modules.rs b/crates/perry/tests/createrequire_builtin_modules.rs index 30eb3f43d7..1e49fadeb2 100644 --- a/crates/perry/tests/createrequire_builtin_modules.rs +++ b/crates/perry/tests/createrequire_builtin_modules.rs @@ -50,6 +50,21 @@ fn compile_and_run(dir: &std::path::Path, source: &str) -> String { String::from_utf8_lossy(&run.stdout).into_owned() } +#[test] +fn extracted_get_builtin_module_keeps_dynamic_namespace_keys() { + let dir = tempfile::tempdir().expect("tempdir"); + let stdout = compile_and_run( + dir.path(), + r#" +import process from "node:process"; +const getBuiltinModule = process.getBuiltinModule; +const http2 = getBuiltinModule("http2"); +console.log(["connect", "createServer", "constants"].map((key) => Object.keys(http2).includes(key)).join(",")); +"#, + ); + assert_eq!(stdout, "true,true,true\n"); +} + /// #6644 (pi wall #3): `require('node:diagnostics_channel')` through /// `createRequire` threw `ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE` — the module is /// implemented as a node_submodules spec (real pub/sub channel registry) but was