Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion crates/perry-hir/src/lower/expr_call/native_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down
82 changes: 80 additions & 2 deletions crates/perry-runtime/src/child_process/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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=")
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// 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 `<shell> -c`) and
/// then applying `cwd`/`env`. With no `shell` the file is run directly. #1780.
Expand All @@ -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: "<path>"` 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);
Expand All @@ -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
};
Expand All @@ -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(
&current,
&["-e".to_string(), "console.log(42)".to_string()],
));
assert!(cp_should_use_node_interpreter(
&current,
&["--eval=console.log(43)".to_string()],
));
assert!(cp_should_use_node_interpreter(
&current,
&[
"--no-warnings".to_string(),
"--eval".to_string(),
"console.log(44)".to_string(),
],
));
for flag in ["-p", "--print"] {
assert!(cp_should_use_node_interpreter(
&current,
&[flag.to_string(), "40 + 2".to_string()],
));
}
assert!(cp_should_use_node_interpreter(
&current,
&["--print=40 + 2".to_string()],
));
assert!(!cp_should_use_node_interpreter(
&current,
&["ordinary-argument".to_string()],
));
assert!(!cp_should_use_node_interpreter(
"some-other-program",
&["-e".to_string(), "console.log(45)".to_string()],
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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\"")
Expand All @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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");
Expand Down
23 changes: 12 additions & 11 deletions crates/perry/src/commands/compile/optimized_libs/freshness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(...)`
Expand Down
18 changes: 18 additions & 0 deletions crates/perry/src/commands/compile/optimized_libs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down
5 changes: 5 additions & 0 deletions crates/perry/src/commands/compile/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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(),
Expand Down
15 changes: 15 additions & 0 deletions crates/perry/tests/createrequire_builtin_modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading