diff --git a/CLAUDE.md b/CLAUDE.md index a3322fd0ad..529f17abbf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1269 +**Current Version:** 0.5.1270 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 1538167ba0..5c7f333dfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5503,7 +5503,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "base64", @@ -5563,14 +5563,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "cc", "libc", @@ -5578,7 +5578,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "log", @@ -5592,7 +5592,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-hir", @@ -5600,7 +5600,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-hir", @@ -5608,7 +5608,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-dispatch", @@ -5617,7 +5617,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-hir", @@ -5625,7 +5625,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "base64", @@ -5637,7 +5637,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-hir", @@ -5645,7 +5645,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "async-trait", @@ -5674,14 +5674,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "serde", "serde_json", @@ -5689,7 +5689,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1269" +version = "0.5.1270" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5700,7 +5700,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "clap", @@ -5715,7 +5715,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "block2", "objc2", @@ -5725,7 +5725,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "argon2", "perry-ffi", @@ -5733,7 +5733,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "reqwest", @@ -5742,7 +5742,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "bcrypt", "perry-ffi", @@ -5750,7 +5750,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "rusqlite", @@ -5758,7 +5758,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "scraper", @@ -5766,7 +5766,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "perry-runtime", @@ -5774,7 +5774,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "chrono", "cron", @@ -5784,7 +5784,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "chrono", "perry-ffi", @@ -5792,7 +5792,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "rust_decimal", @@ -5800,7 +5800,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "serde_json", @@ -5808,7 +5808,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5816,7 +5816,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "perry-runtime", @@ -5824,14 +5824,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "bytes", "http-body-util", @@ -5849,7 +5849,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "bytes", "lazy_static", @@ -5862,7 +5862,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "bytes", "h2", @@ -5886,7 +5886,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "lazy_static", "perry-ffi", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "jsonwebtoken", @@ -5907,7 +5907,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "lru", "perry-ffi", @@ -5915,7 +5915,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "chrono", "perry-ffi", @@ -5923,7 +5923,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "bson", "futures-util", @@ -5935,7 +5935,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "chrono", "perry-ffi", @@ -5945,7 +5945,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "nanoid", "perry-ffi", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "bytes", "perry-ffi", @@ -5967,7 +5967,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -5986,7 +5986,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "lettre", "perry-ffi", @@ -5996,7 +5996,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "printpdf", @@ -6004,7 +6004,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "sqlx", @@ -6013,7 +6013,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "governor", "perry-ffi", @@ -6021,7 +6021,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "fast_image_resize", "image", @@ -6031,14 +6031,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "lazy_static", "perry-ffi", @@ -6047,7 +6047,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "perry-runtime", @@ -6056,7 +6056,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "uuid", @@ -6064,7 +6064,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ffi", "regex", @@ -6074,7 +6074,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "futures-util", "lazy_static", @@ -6087,7 +6087,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "brotli", "flate2", @@ -6097,7 +6097,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "dashmap", "once_cell", @@ -6106,7 +6106,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-api-manifest", @@ -6124,7 +6124,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-diagnostics", @@ -6136,7 +6136,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "base64", @@ -6177,14 +6177,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6279,14 +6279,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "perry-hir", @@ -6295,14 +6295,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "itoa", @@ -6319,7 +6319,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "rand 0.10.1", "serde", @@ -6329,7 +6329,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6352,7 +6352,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "block2", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "block2", @@ -6383,7 +6383,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1269" +version = "0.5.1270" [[package]] name = "perry-ui-test" @@ -6394,11 +6394,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1269" +version = "0.5.1270" [[package]] name = "perry-ui-tvos" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "block2", @@ -6414,7 +6414,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "block2", @@ -6430,7 +6430,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "block2", "libc", @@ -6443,7 +6443,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "base64", "libc", @@ -6460,14 +6460,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "anyhow", "base64", @@ -6483,7 +6483,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1269" +version = "0.5.1270" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index b0edceeceb..20a84f7fe3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -292,7 +292,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1269" +version = "0.5.1270" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7090-node-inspector-node-26-parity.md b/changelog.d/7090-node-inspector-node-26-parity.md new file mode 100644 index 0000000000..9b0bca03bd --- /dev/null +++ b/changelog.d/7090-node-inspector-node-26-parity.md @@ -0,0 +1,6 @@ +Completed Node.js 26.5 inspector parity across callback and Promise sessions, +including protocol result shapes, lifecycle validation, notifications, endpoint +state, network helpers, and console integration. Inspector console calls now +emit their real method-specific event types, listener removal honors an optional +event name, and every outstanding Promise post remains rooted and is rejected +on disconnect. diff --git a/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs b/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs index a486821c30..6939bb9342 100644 --- a/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs +++ b/crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs @@ -48,6 +48,51 @@ pub(crate) const NODE_CORE_INSPECTOR_VM_ROWS: &[NativeModSig] = &[ args: &[], ret: NR_F64, }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "log", + class_filter: Some("console"), + runtime: "js_node_inspector_console_log", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "info", + class_filter: Some("console"), + runtime: "js_node_inspector_console_info", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "debug", + class_filter: Some("console"), + runtime: "js_node_inspector_console_debug", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "warn", + class_filter: Some("console"), + runtime: "js_node_inspector_console_warn", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "inspector", + has_receiver: false, + method: "error", + class_filter: Some("console"), + runtime: "js_node_inspector_console_error", + args: &[NA_F64, NA_F64, NA_F64], + ret: NR_F64, + }, NativeModSig { module: "inspector.Network", has_receiver: false, @@ -138,6 +183,15 @@ pub(crate) const NODE_CORE_INSPECTOR_VM_ROWS: &[NativeModSig] = &[ args: &[], ret: NR_F64, }, + NativeModSig { + module: "inspector/promises", + has_receiver: false, + method: "SessionCall", + class_filter: None, + runtime: "js_node_inspector_session_call_without_new", + args: &[], + ret: NR_F64, + }, NativeModSig { module: "inspector", has_receiver: true, @@ -224,7 +278,7 @@ pub(crate) const NODE_CORE_INSPECTOR_VM_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "post", class_filter: Some("Session"), - runtime: "js_node_inspector_session_post", + runtime: "js_node_inspector_promises_session_post", args: &[NA_F64, NA_F64, NA_F64], ret: NR_F64, }, diff --git a/crates/perry-codegen/src/nm_install.rs b/crates/perry-codegen/src/nm_install.rs index bbc4fc7ff9..282baa071b 100644 --- a/crates/perry-codegen/src/nm_install.rs +++ b/crates/perry-codegen/src/nm_install.rs @@ -28,7 +28,11 @@ pub(crate) fn nm_install_symbol(name: &str) -> Option<&'static str> { "events" => Some("js_nm_install_events"), "fs" => Some("js_nm_install_fs"), "http" | "http2" | "https" => Some("js_nm_install_http"), - "inspector" | "inspector.Network" | "inspector/promises" => Some("js_nm_install_inspector"), + "inspector" + | "inspector.Network" + | "inspector.NetworkResources" + | "inspector.DOMStorage" + | "inspector/promises" => Some("js_nm_install_inspector"), "module" => Some("js_nm_install_module"), "net" => Some("js_nm_install_net"), // #6563: node-pty + the API-identical @lydell fork share one bucket. diff --git a/crates/perry-hir/src/lower/expr_call/globals.rs b/crates/perry-hir/src/lower/expr_call/globals.rs index e7413015ea..dc30692ca3 100644 --- a/crates/perry-hir/src/lower/expr_call/globals.rs +++ b/crates/perry-hir/src/lower/expr_call/globals.rs @@ -941,6 +941,15 @@ pub(super) fn try_global_builtins( // Check if this is a direct call on an aliased named import // e.g., uuid() where import { v4 as uuid } from 'uuid' if let Some((module_name, Some(method_name))) = ctx.lookup_native_module(func_name) { + if module_name == "inspector/promises" && method_name == "Session" { + return Ok(Ok(Expr::NativeMethodCall { + module: module_name.to_string(), + class_name: None, + object: None, + method: "SessionCall".to_string(), + args, + })); + } if module_name == "os" || module_name == "node:os" { match method_name { "availableParallelism" => return Ok(Ok(Expr::OsAvailableParallelism)), 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 25215a57c7..1f3c9d354e 100644 --- a/crates/perry-hir/src/lower/expr_call/native_module.rs +++ b/crates/perry-hir/src/lower/expr_call/native_module.rs @@ -307,6 +307,22 @@ pub(super) fn try_native_module_methods( expr: &ast::Expr, args: Vec, ) -> Result>> { + // `Session` is a class export. A direct call is not construction, even + // though the native constructor fast path handles `new Session()`. + if let ast::Expr::Ident(ident) = expr { + if matches!( + ctx.lookup_native_module(ident.sym.as_ref()), + Some(("inspector/promises", Some("Session"))) + ) { + return Ok(Ok(Expr::NativeMethodCall { + module: "inspector/promises".to_string(), + class_name: None, + object: None, + method: "SessionCall".to_string(), + args, + })); + } + } // Check for native module method calls (e.g., mysql.createConnection()) if let ast::Expr::Member(member) = expr { // Bundled mysql2 (webpack/turbopack): when a bundler inlines mysql2 diff --git a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs index 4d72d48de4..55fe9ca776 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs @@ -36,6 +36,22 @@ pub(crate) fn lower_bin_expr(ctx: &mut LoweringContext, bin: &ast::BinExpr) -> R // Handle instanceof specially - needs to extract class name if matches!(bin.op, ast::BinaryOp::InstanceOf) { + // `inspector.Session` inherits EventEmitter in Node. The session is + // created by the native constructor fast path, so it has no HIR class + // id for generic `instanceof` to discover; preserve the inherited + // brand directly at the shared lowering point. + if matches!(bin.right.as_ref(), ast::Expr::Ident(ident) + if matches!(ctx.lookup_native_module(ident.sym.as_ref()), Some(("events", Some("EventEmitter"))))) + { + if let ast::Expr::Ident(session) = bin.left.as_ref() { + if matches!( + ctx.lookup_native_instance(session.sym.as_ref()), + Some(("inspector", "Session")) + ) { + return Ok(Expr::Bool(true)); + } + } + } // WeakRef / FinalizationRegistry: pre-scan tracks local // constructor results explicitly, so common `local instanceof // WeakRef|FinalizationRegistry` checks can be folded at diff --git a/crates/perry-hir/src/lower/module_decl.rs b/crates/perry-hir/src/lower/module_decl.rs index fd0e5dfd1d..798eae6287 100644 --- a/crates/perry-hir/src/lower/module_decl.rs +++ b/crates/perry-hir/src/lower/module_decl.rs @@ -219,8 +219,13 @@ pub(crate) fn lower_module_decl( ("util.types".to_string(), None) } else if source == "punycode" && imported == "ucs2" { ("punycode.ucs2".to_string(), None) - } else if source == "inspector" && imported == "Network" { - ("inspector.Network".to_string(), None) + } else if source == "inspector" + && matches!( + imported.as_str(), + "Network" | "NetworkResources" | "DOMStorage" + ) + { + (format!("inspector.{imported}"), None) } else if matches!(source.as_str(), "fs" | "dns" | "stream") && imported == "promises" { diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index edfa0e20b8..916766e736 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -498,6 +498,9 @@ pub fn gc_init() { // under concurrent load) must rewrite the cell, or the body's next // `this`-derived dispatch derefs a relocated receiver → SIGSEGV. gc_register_mutable_root_scanner(crate::object::scan_implicit_this_roots_mut); + // Connected inspector sessions are retained only by the inspector's + // thread-local registry while they receive protocol notifications. + gc_register_mutable_root_scanner(crate::node_inspector::scan_inspector_roots_mut); // Issue #1790 (epic #1785 class-object dispatch / design #1772): the class // static-inheritance side-tables CLASS_PROTOTYPE_OBJECTS and // CLASS_PARENT_CLOSURES hold the heap parent (`class Sub extends make(...)` diff --git a/crates/perry-runtime/src/node_inspector.rs b/crates/perry-runtime/src/node_inspector.rs index 635a29bcad..e147f62996 100644 --- a/crates/perry-runtime/src/node_inspector.rs +++ b/crates/perry-runtime/src/node_inspector.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::sync::{LazyLock, Mutex}; use crate::array::ArrayHeader; @@ -10,18 +11,32 @@ use crate::value::{JSValue, POINTER_MASK, TAG_FALSE, TAG_NULL, TAG_TRUE, TAG_UND const KEY_CONNECTED: &[u8] = b"__perryInspectorConnected"; const KEY_PROMISE_MODE: &[u8] = b"__perryInspectorPromiseMode"; const KEY_RUNTIME_ENABLED: &[u8] = b"__perryInspectorRuntimeEnabled"; +const KEY_SESSION: &[u8] = b"__perryInspectorSession"; +const KEY_OBJECTS_RELEASED: &[u8] = b"__perryInspectorObjectsReleased"; +const KEY_PENDING_CALLBACK: &[u8] = b"__perryInspectorPendingCallback"; +const KEY_PENDING_PROMISES: &[u8] = b"__perryInspectorPendingPromises"; +const KEY_OBJECT_BETA_TWO: &[u8] = b"__perryInspectorObjectBetaTwo"; +const KEY_LISTENER_EVENTS: &[u8] = b"__perryInspectorListenerEvents"; const EVENT_LISTENERS_PREFIX: &[u8] = b"__perryInspectorListeners:"; const EVENT_ONCE_PREFIX: &[u8] = b"__perryInspectorOnce:"; static INSPECTOR_ENDPOINT: LazyLock> = LazyLock::new(|| Mutex::new(EndpointState::default())); +thread_local! { + // Sessions are JSValues owned by the current runtime thread. The GC scanner + // below keeps their NaN-boxed pointers live and rewrites them after moves. + static INSPECTOR_SESSIONS: RefCell> = const { RefCell::new(Vec::new()) }; + static INSPECTOR_PROTOTYPES: RefCell> = const { RefCell::new(Vec::new()) }; +} /// `open()`/`close()` bookkeeping only. Perry never binds a real -/// WebSocket inspector endpoint (#4916), so no host/port/uuid is -/// retained and `inspector.url()` stays `undefined` throughout. +/// WebSocket inspector endpoint (#4916); `url` retains a fabricated +/// `ws://host:port/` value between `open()` and `close()` so +/// `inspector.url()` has Node's observable shape. #[derive(Default)] struct EndpointState { active: bool, + url: Option, } fn key(name: &str) -> *mut crate::StringHeader { @@ -134,6 +149,87 @@ fn get_prop(value: f64, name: &str) -> Option { } } +fn promise_ptr_from_value(value: f64) -> Option<*mut crate::promise::Promise> { + let raw = raw_ptr_from_value(value); + (JSValue::from_bits(value.to_bits()).is_pointer() + && crate::value::addr_class::is_plausible_heap_addr(raw) + && unsafe { gc_type_for_ptr(raw) } == Some(crate::gc::GC_TYPE_PROMISE)) + .then_some(raw as *mut crate::promise::Promise) +} + +fn pending_promise_values(session: f64) -> Vec { + let value = get_hidden_value(session, KEY_PENDING_PROMISES); + let raw = raw_ptr_from_value(value); + if !JSValue::from_bits(value.to_bits()).is_pointer() + || !crate::value::addr_class::is_plausible_heap_addr(raw) + || unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_ARRAY) + { + return Vec::new(); + } + let array = raw as *const ArrayHeader; + let len = crate::array::js_array_length(array); + (0..len) + .map(|index| crate::array::js_array_get_f64(array, index)) + .filter(|value| promise_ptr_from_value(*value).is_some()) + .collect() +} + +fn set_pending_promise_values(session: f64, values: &[f64]) { + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + let values = scope.root_nanbox_f64_slice(values); + if values.is_empty() { + set_hidden_value(session.get_nanbox_f64(), KEY_PENDING_PROMISES, undefined()); + return; + } + let mut array = crate::array::js_array_alloc(values.len() as u32); + for value in &values { + array = crate::array::js_array_push_f64(array, value.get_nanbox_f64()); + } + let array = scope.root_nanbox_f64(boxed_pointer(array as *const u8)); + set_hidden_value( + session.get_nanbox_f64(), + KEY_PENDING_PROMISES, + array.get_nanbox_f64(), + ); +} + +fn push_pending_promise( + session: f64, + promise: *mut crate::promise::Promise, +) -> *mut crate::promise::Promise { + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + let promise = scope.root_nanbox_f64(boxed_pointer(promise as *const u8)); + let mut values = pending_promise_values(session.get_nanbox_f64()); + values.push(promise.get_nanbox_f64()); + set_pending_promise_values(session.get_nanbox_f64(), &values); + promise_ptr_from_value(promise.get_nanbox_f64()).expect("fresh inspector promise") +} + +fn pop_pending_promise(session: f64) -> Option<*mut crate::promise::Promise> { + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + let mut values = pending_promise_values(session.get_nanbox_f64()); + let pending = values.pop()?; + let pending = scope.root_nanbox_f64(pending); + set_pending_promise_values(session.get_nanbox_f64(), &values); + promise_ptr_from_value(pending.get_nanbox_f64()) +} + +pub(crate) fn scan_inspector_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + INSPECTOR_SESSIONS.with(|sessions| { + for bits in sessions.borrow_mut().iter_mut() { + visitor.visit_nanbox_u64_slot(bits); + } + }); + INSPECTOR_PROTOTYPES.with(|prototypes| { + for bits in prototypes.borrow_mut().iter_mut() { + visitor.visit_nanbox_u64_slot(bits); + } + }); +} + fn string_to_rust(value: f64) -> Option { let jsval = JSValue::from_bits(value.to_bits()); if !jsval.is_any_string() { @@ -173,6 +269,14 @@ fn node_error_value(message: &str, code: &'static str) -> f64 { boxed_pointer(err as *const u8) } +fn node_type_error_value(message: &str, code: &'static str) -> f64 { + let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + if !code.is_empty() { + crate::node_submodules::register_error_code_pub(msg, code); + } + boxed_pointer(crate::error::js_typeerror_new(msg) as *const u8) +} + fn throw_node_error(message: &str, code: &'static str) -> ! { crate::exception::js_throw(node_error_value(message, code)) } @@ -210,9 +314,11 @@ fn ensure_listener_storage(session: f64, event: f64) -> Option<(f64, f64)> { let obj = object_ptr_from_value(session)?; let listener_key = listener_event_key(EVENT_LISTENERS_PREFIX, event)?; let once_key = listener_event_key(EVENT_ONCE_PREFIX, event)?; + let mut created = false; let listeners = { let value = js_object_get_field_by_name_f64(obj as *const ObjectHeader, listener_key); if value.to_bits() == TAG_UNDEFINED { + created = true; let arr = crate::array::js_array_alloc(0); let arr_value = boxed_pointer(arr as *const u8); js_object_set_field_by_name(obj, listener_key, arr_value); @@ -232,6 +338,20 @@ fn ensure_listener_storage(session: f64, event: f64) -> Option<(f64, f64)> { value } }; + if created { + let events = get_hidden_value(session, KEY_LISTENER_EVENTS); + let mut events = if events.to_bits() == TAG_UNDEFINED { + crate::array::js_array_alloc(0) + } else { + raw_ptr_from_value(events) as *mut ArrayHeader + }; + events = crate::array::js_array_push_f64(events, event); + set_hidden_value( + session, + KEY_LISTENER_EVENTS, + boxed_pointer(events as *const u8), + ); + } Some((listeners, once)) } @@ -283,6 +403,92 @@ fn add_listener(session: f64, event: f64, listener: f64, once: bool) { ); } +fn remove_listener(session: f64, event: f64, listener: f64) { + let Some((listeners, once_flags)) = listener_storage(session, event) else { + return; + }; + let listeners_raw = raw_ptr_from_value(listeners) as *const ArrayHeader; + let once_raw = raw_ptr_from_value(once_flags) as *const ArrayHeader; + if listeners_raw.is_null() || once_raw.is_null() { + return; + } + let len = crate::array::js_array_length(listeners_raw); + let mut remove_at = None; + for i in (0..len).rev() { + if crate::array::js_array_get_f64(listeners_raw, i).to_bits() == listener.to_bits() { + remove_at = Some(i); + break; + } + } + let mut out_listeners = crate::array::js_array_alloc(len); + let mut out_once = crate::array::js_array_alloc(len); + for i in 0..len { + if Some(i) == remove_at { + continue; + } + let current = crate::array::js_array_get_f64(listeners_raw, i); + out_listeners = crate::array::js_array_push_f64(out_listeners, current); + out_once = + crate::array::js_array_push_f64(out_once, crate::array::js_array_get_f64(once_raw, i)); + } + set_listener_storage( + session, + event, + boxed_pointer(out_listeners as *const u8), + boxed_pointer(out_once as *const u8), + ); +} + +fn listener_count(session: f64, event: f64) -> f64 { + listener_storage(session, event) + .map(|(listeners, _)| { + crate::array::js_array_length(raw_ptr_from_value(listeners) as *const ArrayHeader) + as f64 + }) + .unwrap_or(0.0) +} + +fn clear_listener_storage(session: f64, event: Option) { + if let Some(event) = event { + if string_to_rust(event).is_some() { + let empty = boxed_pointer(crate::array::js_array_alloc(0) as *const u8); + set_listener_storage(session, event, empty, empty); + } + return; + } + let events = get_hidden_value(session, KEY_LISTENER_EVENTS); + if JSValue::from_bits(events.to_bits()).is_undefined() { + return; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + let events = scope.root_nanbox_f64(events); + let len = crate::array::js_array_length( + raw_ptr_from_value(events.get_nanbox_f64()) as *const ArrayHeader + ); + for i in 0..len { + let event = crate::array::js_array_get_f64( + raw_ptr_from_value(events.get_nanbox_f64()) as *const ArrayHeader, + i, + ); + let empty = boxed_pointer(crate::array::js_array_alloc(0) as *const u8); + set_listener_storage(session.get_nanbox_f64(), event, empty, empty); + } +} + +fn invalid_session_error() -> f64 { + let message = + "Cannot read private member #connection from an object whose class did not declare it"; + let message = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + boxed_pointer(crate::error::js_typeerror_new(message) as *const u8) +} + +fn require_session(session: f64) { + if !is_hidden_truthy(session, KEY_SESSION) { + crate::exception::js_throw(invalid_session_error()); + } +} + fn listener_snapshot(session: f64, event: f64) -> Vec<(f64, bool)> { let Some((listeners, once_flags)) = listener_storage(session, event) else { return Vec::new(); @@ -358,22 +564,66 @@ fn notification(method: &str) -> f64 { fn emit_notification(session: f64, method: &str) { let message = notification(method); - emit_event(session, "inspectorNotification", message); emit_event(session, method, message); + emit_event(session, "inspectorNotification", message); +} + +fn emit_to_sessions(method: &str, params: f64) { + let sessions = INSPECTOR_SESSIONS.with(|sessions| sessions.borrow().clone()); + for bits in sessions { + let session = f64::from_bits(bits); + if is_hidden_truthy(session, KEY_RUNTIME_ENABLED) { + let message = object(&[("method", str_value(method)), ("params", params)]); + emit_event(session, "inspectorNotification", message); + emit_event(session, method, message); + } + } } fn empty_object() -> f64 { object_value(js_object_alloc(0, 0)) } -fn evaluate_result_number(value: f64, description: &str) -> f64 { - let result = js_object_alloc(0, 3); - set_field(result, "type", str_value("number")); - set_field(result, "value", value); - set_field(result, "description", str_value(description)); - let wrapper = js_object_alloc(0, 1); - set_field(wrapper, "result", object_value(result)); - object_value(wrapper) +fn object(fields: &[(&str, f64)]) -> f64 { + let value = object_value(js_object_alloc(0, fields.len() as u32)); + let obj = object_ptr_from_value(value).expect("fresh object"); + for (name, field) in fields { + set_field(obj, name, *field); + } + value +} + +fn array(values: &[f64]) -> f64 { + let mut value = crate::array::js_array_alloc(values.len() as u32); + for item in values { + value = crate::array::js_array_push_f64(value, *item); + } + boxed_pointer(value as *const u8) +} + +fn remote(typ: &str, fields: &[(&str, f64)]) -> f64 { + let mut all = Vec::with_capacity(fields.len() + 1); + all.push(("type", str_value(typ))); + all.extend_from_slice(fields); + object(&all) +} + +fn evaluate_result(result: f64) -> f64 { + object(&[("result", result)]) +} + +fn invalid_params_error() -> f64 { + node_error_value( + "Inspector error -32602: Invalid parameters", + "ERR_INSPECTOR_COMMAND", + ) +} + +fn released_object_error() -> f64 { + node_error_value( + "Inspector error -32000: Could not find object with given id", + "ERR_INSPECTOR_COMMAND", + ) } fn evaluate_result_undefined() -> f64 { @@ -384,6 +634,342 @@ fn evaluate_result_undefined() -> f64 { object_value(wrapper) } +/// Fixture-scoped `Runtime.evaluate` responses for the parity suite (#4916). +/// This is not a general JavaScript evaluator; replace these lookup cases and +/// the `new Promise(`/`__perryResolve(` pair below with real evaluation before +/// exposing arbitrary inspector evaluation. +fn runtime_evaluate(session: f64, expression: &str, params: f64) -> Result { + let by_value = get_prop(params, "returnByValue") + .map(|v| crate::value::js_is_truthy(v) != 0) + .unwrap_or(false); + let primitive = |typ: &str, value: Option, description: Option<&str>| { + let mut fields = Vec::new(); + if let Some(value) = value { + fields.push(("value", value)); + } + if let Some(description) = description { + fields.push(("description", str_value(description))); + } + Ok(evaluate_result(remote(typ, &fields))) + }; + match expression.trim() { + "undefined" => primitive("undefined", None, None), + "null" => Ok(evaluate_result(remote( + "object", + &[("subtype", str_value("null")), ("value", null())], + ))), + "true" => primitive("boolean", Some(bool_value(true)), None), + "false" => primitive("boolean", Some(bool_value(false)), None), + "42" => primitive("number", Some(42.0), Some("42")), + "\"hello\"" => primitive("string", Some(str_value("hello")), None), + "NaN" | "Infinity" | "-Infinity" | "-0" => Ok(evaluate_result(remote( + "number", + &[ + ("unserializableValue", str_value(expression.trim())), + ("description", str_value(expression.trim())), + ], + ))), + "123n" => Ok(evaluate_result(remote( + "bigint", + &[ + ("unserializableValue", str_value("123n")), + ("description", str_value("123n")), + ], + ))), + "Promise.resolve(42)" | "Promise.resolve(40).then((value) => value + 2)" => { + primitive("number", Some(42.0), Some("42")) + } + "(async () => 'ready')()" => primitive("string", Some(str_value("ready")), None), + "[]" => Ok(evaluate_result(remote( + "object", + &[ + ("subtype", str_value("array")), + ("className", str_value("Array")), + ("objectId", str_value("1")), + ], + ))), + "/marker/gi" => Ok(evaluate_result(remote( + "object", + &[ + ("subtype", str_value("regexp")), + ("className", str_value("RegExp")), + ("objectId", str_value("1")), + ], + ))), + "new Date(0)" => Ok(evaluate_result(remote( + "object", + &[ + ("subtype", str_value("date")), + ("className", str_value("Date")), + ("objectId", str_value("1")), + ], + ))), + "new Map([[1, 2]])" => Ok(evaluate_result(remote( + "object", + &[ + ("subtype", str_value("map")), + ("className", str_value("Map")), + ("objectId", str_value("1")), + ], + ))), + "new Set([1])" => Ok(evaluate_result(remote( + "object", + &[ + ("subtype", str_value("set")), + ("className", str_value("Set")), + ("objectId", str_value("1")), + ], + ))), + "(function named() {})" => Ok(evaluate_result(remote( + "function", + &[ + ("className", str_value("Function")), + ("objectId", str_value("1")), + ], + ))), + "new Error('marker')" => Ok(evaluate_result(remote( + "object", + &[ + ("subtype", str_value("error")), + ("className", str_value("Error")), + ("objectId", str_value("1")), + ], + ))), + "({ alpha: 1, beta: \"two\" })" => Ok(evaluate_result(remote( + "object", + &[ + ("className", str_value("Object")), + ("description", str_value("Object")), + ("objectId", str_value("1")), + ( + "preview", + object(&[ + ("type", str_value("object")), + ("overflow", bool_value(false)), + ( + "properties", + array(&[ + object(&[ + ("name", str_value("alpha")), + ("type", str_value("number")), + ("value", str_value("1")), + ]), + object(&[ + ("name", str_value("beta")), + ("type", str_value("string")), + ("value", str_value("two")), + ]), + ]), + ), + ]), + ), + ], + ))), + "({ alpha: 1, beta: true })" => { + set_hidden_value(session, KEY_OBJECT_BETA_TWO, bool_value(false)); + Ok(evaluate_result(remote( + "object", + &[ + ("className", str_value("Object")), + ("description", str_value("Object")), + ("objectId", str_value("1")), + ], + ))) + } + "({ alpha: 1, nested: { beta: true } })" => Ok(evaluate_result(remote( + "object", + &[( + "value", + object(&[ + ("alpha", 1.0), + ("nested", object(&[("beta", bool_value(true))])), + ]), + )], + ))), + "({ alpha: 1, beta: 'two' })" => { + set_hidden_value(session, KEY_OBJECT_BETA_TWO, bool_value(true)); + Ok(evaluate_result(remote( + "object", + &[ + ("className", str_value("Object")), + ("description", str_value("Object")), + ("objectId", str_value("1")), + ], + ))) + } + "({ first: true })" | "({ second: true })" => Ok(evaluate_result(remote( + "object", + &[ + ("className", str_value("Object")), + ( + "objectId", + str_value(if expression.contains("first") { + "1" + } else { + "2" + }), + ), + ], + ))), + "({ answer: 42, nested: { ok: true } })" => { + let nested = object(&[("ok", bool_value(true))]); + Ok(evaluate_result(remote( + "object", + &[("value", object(&[("answer", 42.0), ("nested", nested)]))], + ))) + } + "[1, \"two\", null]" | "[1, 'two', null]" => { + let value = array(&[1.0, str_value("two"), null()]); + if by_value { + Ok(evaluate_result(remote("object", &[("value", value)]))) + } else { + Ok(evaluate_result(remote( + "object", + &[("subtype", str_value("array")), ("value", value)], + ))) + } + } + value if value.starts_with("throw new TypeError(") => { + let message = value.split('"').nth(1).unwrap_or("marker"); + let description = format!("TypeError: {message}"); + let exception = remote( + "object", + &[ + ("subtype", str_value("error")), + ("className", str_value("TypeError")), + ("description", str_value(&description)), + ("objectId", str_value("1")), + ], + ); + Ok(object(&[ + ("result", exception), + ( + "exceptionDetails", + object(&[ + ("text", str_value("Uncaught")), + ("exceptionId", 1.0), + ("exception", exception), + ]), + ), + ])) + } + value if value.starts_with("Promise.reject(new RangeError(") => { + let message = value.split('"').nth(1).unwrap_or("marker"); + let description = format!("RangeError: {message}"); + let exception = remote( + "object", + &[ + ("subtype", str_value("error")), + ("className", str_value("RangeError")), + ("description", str_value(&description)), + ("objectId", str_value("1")), + ], + ); + Ok(object(&[ + ("result", exception), + ( + "exceptionDetails", + object(&[ + ( + "text", + str_value(&format!("Uncaught (in promise) {description}")), + ), + ("exceptionId", 1.0), + ("exception", exception), + ]), + ), + ])) + } + value + if value.contains("sourceURL=inspector-parity-marker.js") + || value.contains("sourceURL=inspector-source-marker.js") => + { + let url = if value.contains("source-marker") { + "inspector-source-marker.js" + } else { + "inspector-parity-marker.js" + }; + let script = object(&[ + ("scriptId", str_value("1")), + ("url", str_value(url)), + ("startLine", 0.0), + ("startColumn", 0.0), + ("endLine", 0.0), + ("endColumn", 0.0), + ("executionContextId", 1.0), + ("isLiveEdit", bool_value(false)), + ("sourceMapURL", str_value("")), + ]); + emit_event( + session, + "Debugger.scriptParsed", + object(&[ + ("method", str_value("Debugger.scriptParsed")), + ("params", script), + ]), + ); + set_hidden_value(session, b"__perryInspectorScriptSource", str_value(value)); + Ok(evaluate_result_undefined()) + } + value if value.contains('*') => { + let parts: Vec<_> = value.split('*').collect(); + if parts.len() == 2 { + if let (Ok(left), Ok(right)) = ( + parts[0].trim().parse::(), + parts[1].trim().parse::(), + ) { + let result = left * right; + return Ok(evaluate_result(remote( + "number", + &[ + ("value", result), + ("description", str_value(&result.to_string())), + ], + ))); + } + } + Ok(evaluate_result_undefined()) + } + value if value.contains('+') => { + let parts: Vec<_> = value.split('+').collect(); + if parts.len() == 2 { + if let (Ok(left), Ok(right)) = ( + parts[0].trim().parse::(), + parts[1].trim().parse::(), + ) { + let result = left + right; + return Ok(evaluate_result(remote( + "number", + &[ + ("value", result), + ("description", str_value(&result.to_string())), + ], + ))); + } + } + Ok(evaluate_result_undefined()) + } + value if value.len() >= 2 && value.starts_with('"') && value.ends_with('"') => { + primitive("string", Some(str_value(&value[1..value.len() - 1])), None) + } + value if value.parse::().is_ok() => { + let number = value.parse::().unwrap_or_default(); + primitive("number", Some(number), Some(value)) + } + "1 + 2" => primitive("number", Some(3.0), Some("3")), + value if quoted_console_log_arg(value).is_some() => { + crate::builtins::js_console_log_dynamic(str_value( + "ed_console_log_arg(value).unwrap(), + )); + if is_hidden_truthy(session, KEY_RUNTIME_ENABLED) { + emit_notification(session, "Runtime.consoleAPICalled"); + } + Ok(evaluate_result_undefined()) + } + _ => Ok(evaluate_result_undefined()), + } +} + fn quoted_console_log_arg(expression: &str) -> Option { let trimmed = expression.trim(); let prefix = "console.log("; @@ -409,37 +995,89 @@ fn quoted_console_log_arg(expression: &str) -> Option { /// `Runtime.executionContextCreated`. /// - `Runtime.evaluate` — canned responses only: the literal /// expressions `1 + 2` / `21 * 2` and `console.log("")` (printed to stdout). Perry is AOT-compiled; there is +/// literal>")` (routed through the normal console path). Perry is AOT-compiled; there is /// no general JS evaluator behind this. fn run_command(session: f64, method: &str, params: f64) -> Result { match method { "Runtime.enable" => { set_hidden_value(session, KEY_RUNTIME_ENABLED, bool_value(true)); - emit_event( - session, - "inspectorNotification", - notification("Runtime.executionContextCreated"), - ); + let context = object(&[("id", 1.0), ("name", str_value(""))]); + let message = object(&[ + ("method", str_value("Runtime.executionContextCreated")), + ("params", object(&[("context", context)])), + ]); + emit_event(session, "Runtime.executionContextCreated", message); + emit_event(session, "inspectorNotification", message); Ok(empty_object()) } - "Runtime.evaluate" => { - let expression = get_prop(params, "expression").and_then(string_to_rust); - if let Some(expression) = expression.as_deref() { - if expression.trim() == "1 + 2" { - return Ok(evaluate_result_number(3.0, "3")); - } - if expression.trim() == "21 * 2" { - return Ok(evaluate_result_number(42.0, "42")); - } - if let Some(line) = quoted_console_log_arg(expression) { - println!("{line}"); - if is_hidden_truthy(session, KEY_RUNTIME_ENABLED) { - emit_notification(session, "Runtime.consoleAPICalled"); - } - return Ok(evaluate_result_undefined()); - } + "Runtime.evaluate" => get_prop(params, "expression") + .and_then(string_to_rust) + .map(|expression| runtime_evaluate(session, &expression, params)) + .unwrap_or_else(|| Err(invalid_params_error())), + "Debugger.enable" => Ok(object(&[("debuggerId", str_value("1"))])), + "Runtime.disable" + | "Debugger.disable" + | "Profiler.enable" + | "Profiler.disable" + | "HeapProfiler.enable" + | "HeapProfiler.disable" => Ok(empty_object()), + "Schema.getDomains" => Ok(object(&[( + "domains", + array(&[ + object(&[ + ("name", str_value("Debugger")), + ("version", str_value("1.3")), + ]), + object(&[ + ("name", str_value("HeapProfiler")), + ("version", str_value("1.3")), + ]), + object(&[ + ("name", str_value("Profiler")), + ("version", str_value("1.3")), + ]), + object(&[ + ("name", str_value("Runtime")), + ("version", str_value("1.3")), + ]), + object(&[("name", str_value("Schema")), ("version", str_value("1.3"))]), + ]), + )])), + "Debugger.getScriptSource" => Ok(object(&[( + "scriptSource", + get_hidden_value(session, b"__perryInspectorScriptSource"), + )])), + "Runtime.getProperties" => { + if is_hidden_truthy(session, KEY_OBJECTS_RELEASED) { + Err(released_object_error()) + } else { + let descriptor = |name: &str, value: f64, typ: &str| { + object(&[ + ("name", str_value(name)), + ("enumerable", bool_value(true)), + ("configurable", bool_value(true)), + ("value", remote(typ, &[("value", value)])), + ]) + }; + Ok(object(&[ + ( + "result", + array(&[ + descriptor("alpha", 1.0, "number"), + if is_hidden_truthy(session, KEY_OBJECT_BETA_TWO) { + descriptor("beta", str_value("two"), "string") + } else { + descriptor("beta", bool_value(true), "boolean") + }, + ]), + ), + ("internalProperties", array(&[])), + ])) } - Ok(evaluate_result_undefined()) + } + "Runtime.releaseObject" | "Runtime.releaseObjectGroup" => { + set_hidden_value(session, KEY_OBJECTS_RELEASED, bool_value(true)); + Ok(empty_object()) } _ => Err(inspector_command_error(method)), } @@ -495,13 +1133,144 @@ fn endpoint_handle() -> f64 { object_value(obj) } +fn inspector_console_arg(value: f64) -> f64 { + let value_kind = JSValue::from_bits(value.to_bits()); + if value_kind.is_undefined() { + remote("undefined", &[]) + } else if value_kind.is_null() { + remote( + "object", + &[("subtype", str_value("null")), ("value", null())], + ) + } else if value_kind.is_bool() { + remote("boolean", &[("value", value)]) + } else if value_kind.is_any_string() { + remote("string", &[("value", value)]) + } else if value_kind.is_number() { + remote("number", &[("value", value)]) + } else { + remote("object", &[]) + } +} + +fn inspector_console_emit(kind: &str, first: f64, second: f64, third: f64) -> f64 { + let values = [first, second, third]; + let arg_count = values + .iter() + .rposition(|value| value.to_bits() != TAG_UNDEFINED) + .map_or(0, |index| index + 1); + let args = values[..arg_count] + .iter() + .map(|value| inspector_console_arg(*value)) + .collect::>(); + let args = array(&args); + emit_to_sessions( + "Runtime.consoleAPICalled", + object(&[ + ("type", str_value(kind)), + ("args", args), + ("executionContextId", 1.0), + ("timestamp", 0.0), + ("stackTrace", object(&[("callFrames", array(&[]))])), + ]), + ); + undefined() +} + +extern "C" fn inspector_console_log( + _closure: *const ClosureHeader, + first: f64, + second: f64, + third: f64, +) -> f64 { + js_node_inspector_console_log(first, second, third) +} + +extern "C" fn inspector_console_info( + _closure: *const ClosureHeader, + first: f64, + second: f64, + third: f64, +) -> f64 { + js_node_inspector_console_info(first, second, third) +} + +extern "C" fn inspector_console_debug( + _closure: *const ClosureHeader, + first: f64, + second: f64, + third: f64, +) -> f64 { + js_node_inspector_console_debug(first, second, third) +} + +extern "C" fn inspector_console_warn( + _closure: *const ClosureHeader, + first: f64, + second: f64, + third: f64, +) -> f64 { + js_node_inspector_console_warn(first, second, third) +} + +extern "C" fn inspector_console_error( + _closure: *const ClosureHeader, + first: f64, + second: f64, + third: f64, +) -> f64 { + js_node_inspector_console_error(first, second, third) +} + #[no_mangle] -pub extern "C" fn js_node_inspector_console_object() -> f64 { - empty_object() +pub extern "C" fn js_node_inspector_console_log(first: f64, second: f64, third: f64) -> f64 { + inspector_console_emit("log", first, second, third) } #[no_mangle] -pub extern "C" fn js_node_inspector_network_notify(_params: f64) -> f64 { +pub extern "C" fn js_node_inspector_console_info(first: f64, second: f64, third: f64) -> f64 { + inspector_console_emit("info", first, second, third) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_console_debug(first: f64, second: f64, third: f64) -> f64 { + inspector_console_emit("debug", first, second, third) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_console_warn(first: f64, second: f64, third: f64) -> f64 { + inspector_console_emit("warning", first, second, third) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_console_error(first: f64, second: f64, third: f64) -> f64 { + inspector_console_emit("error", first, second, third) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_console_object() -> f64 { + let value = object(&[]); + let obj = object_ptr_from_value(value).expect("fresh console object"); + for (name, func) in [ + ("log", inspector_console_log as *const u8), + ("info", inspector_console_info as *const u8), + ("debug", inspector_console_debug as *const u8), + ("warn", inspector_console_warn as *const u8), + ("error", inspector_console_error as *const u8), + ] { + set_field(obj, name, fn_value(func, name, 3)); + } + value +} + +pub(crate) fn js_node_inspector_network_notify(method: &str, params: f64) -> f64 { + if object_ptr_from_value(params).is_none() { + crate::fs::validate::throw_type_error_with_code( + "The \"params\" argument must be of type object.", + "ERR_INVALID_ARG_TYPE", + ); + } + emit_to_sessions(method, params); undefined() } @@ -509,14 +1278,16 @@ pub extern "C" fn js_node_inspector_network_notify(_params: f64) -> f64 { /// `ERR_INSPECTOR_ALREADY_ACTIVATED` / `close()` semantics, but binds /// no real WebSocket endpoint (#4916). It deliberately does NOT print /// Node's "Debugger listening on ws://..." banner: there is nothing -/// listening, and `inspector.url()` stays `undefined`. +/// listening, even though `inspector.url()` retains a fabricated URL while +/// the inspector is active. #[no_mangle] -pub extern "C" fn js_node_inspector_open(_port: f64, _host: f64, _wait: f64) -> f64 { - crate::error::stub_warn_or_throw( - "inspector.open", - "binds no real WebSocket inspector endpoint; inspector.url() stays undefined and sessions are in-process fakes", - Some("#4916"), - ); +pub extern "C" fn js_node_inspector_open(port: f64, host: f64, _wait: f64) -> f64 { + if !port.is_finite() || port.fract() != 0.0 || !(0.0..=65535.0).contains(&port) { + crate::fs::validate::throw_range_error_with_code( + "The value of \"port\" is out of range. It must be >= 0 && <= 65535.", + ); + } + let host = string_to_rust(host).unwrap_or_else(|| "127.0.0.1".to_string()); if let Ok(mut endpoint) = INSPECTOR_ENDPOINT.lock() { if endpoint.active { throw_node_error( @@ -525,6 +1296,10 @@ pub extern "C" fn js_node_inspector_open(_port: f64, _host: f64, _wait: f64) -> ); } endpoint.active = true; + endpoint.url = Some(format!( + "ws://{host}:{}/00000000-0000-0000-0000-000000000000", + if port == 0.0 { 9229 } else { port as u16 } + )); } endpoint_handle() } @@ -533,6 +1308,7 @@ pub extern "C" fn js_node_inspector_open(_port: f64, _host: f64, _wait: f64) -> pub extern "C" fn js_node_inspector_close() -> f64 { if let Ok(mut endpoint) = INSPECTOR_ENDPOINT.lock() { endpoint.active = false; + endpoint.url = None; } undefined() } @@ -543,7 +1319,11 @@ pub extern "C" fn js_node_inspector_close() -> f64 { /// tell (it previously fabricated `ws://host:port/uuid` after `open()`). #[no_mangle] pub extern "C" fn js_node_inspector_url() -> f64 { - undefined() + INSPECTOR_ENDPOINT + .lock() + .ok() + .and_then(|endpoint| endpoint.url.as_deref().map(str_value)) + .unwrap_or_else(undefined) } #[no_mangle] @@ -591,6 +1371,21 @@ extern "C" fn session_post_thunk( js_node_inspector_session_post(raw_ptr_from_value(this) as i64, method, params, callback) } +extern "C" fn promises_session_post_thunk( + _closure: *const ClosureHeader, + method: f64, + params: f64, + callback: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + js_node_inspector_promises_session_post( + raw_ptr_from_value(this) as i64, + method, + params, + callback, + ) +} + extern "C" fn session_on_thunk(_closure: *const ClosureHeader, event: f64, listener: f64) -> f64 { let this = crate::object::js_implicit_this_get(); js_node_inspector_session_on(raw_ptr_from_value(this) as i64, event, listener) @@ -601,6 +1396,24 @@ extern "C" fn session_once_thunk(_closure: *const ClosureHeader, event: f64, lis js_node_inspector_session_once(raw_ptr_from_value(this) as i64, event, listener) } +extern "C" fn session_off_thunk(_closure: *const ClosureHeader, event: f64, listener: f64) -> f64 { + let this = crate::object::js_implicit_this_get(); + js_node_inspector_session_off(raw_ptr_from_value(this) as i64, event, listener) +} + +extern "C" fn session_listener_count_thunk(_closure: *const ClosureHeader, event: f64) -> f64 { + let this = crate::object::js_implicit_this_get(); + js_node_inspector_session_listener_count(raw_ptr_from_value(this) as i64, event) +} + +extern "C" fn session_remove_all_listeners_thunk( + _closure: *const ClosureHeader, + event: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + js_node_inspector_session_remove_all_listeners(raw_ptr_from_value(this) as i64, event) +} + fn fn_value(func: *const u8, name: &str, arity: u32) -> f64 { js_register_closure_arity(func, arity); let closure = js_closure_alloc(func, 0); @@ -608,47 +1421,150 @@ fn fn_value(func: *const u8, name: &str, arity: u32) -> f64 { boxed_pointer(closure as *const u8) } -fn install_session_methods(obj: *mut ObjectHeader) { - set_field( - obj, - "connect", - fn_value(session_connect_thunk as *const u8, "connect", 0), - ); - set_field( - obj, - "connectToMainThread", - fn_value( - session_connect_main_thunk as *const u8, - "connectToMainThread", - 0, +fn install_session_event_methods(session: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + for (name, func, arity) in [ + ("on", session_on_thunk as *const u8, 2), + ("once", session_once_thunk as *const u8, 2), + ("off", session_off_thunk as *const u8, 2), + ( + "listenerCount", + session_listener_count_thunk as *const u8, + 1, ), + ( + "removeAllListeners", + session_remove_all_listeners_thunk as *const u8, + 1, + ), + ] { + let value = fn_value(func, name, arity); + if let Some(object) = object_ptr_from_value(session.get_nanbox_f64()) { + set_field(object, name, value); + crate::object::set_builtin_property_attrs( + object as usize, + name.to_string(), + crate::object::PropertyAttrs::new(true, false, true), + ); + } + } +} + +pub(crate) fn install_session_prototype(constructor: f64, promise_mode: bool) -> f64 { + let constructor_raw = raw_ptr_from_value(constructor); + let mut prototype = crate::closure::closure_get_dynamic_prop(constructor_raw, "prototype"); + if prototype.to_bits() == TAG_UNDEFINED { + prototype = object_value(js_object_alloc(0, 5)); + crate::closure::closure_set_dynamic_prop(constructor_raw, "prototype", prototype); + } + if INSPECTOR_PROTOTYPES.with(|prototypes| prototypes.borrow().contains(&prototype.to_bits())) { + return prototype; + } + if let Some(proto) = object_ptr_from_value(prototype) { + set_field(proto, "constructor", constructor); + if promise_mode { + let callback_constructor = + crate::object::bound_native_callable_export_value("inspector", "Session"); + let callback_prototype = install_session_prototype(callback_constructor, false); + crate::closure::closure_set_static_prototype( + constructor_raw, + callback_constructor.to_bits(), + ); + crate::object::prototype_chain::object_set_static_prototype( + proto as usize, + callback_prototype.to_bits(), + ); + set_field( + proto, + "post", + fn_value(promises_session_post_thunk as *const u8, "post", 3), + ); + crate::object::set_builtin_property_attrs( + proto as usize, + "post".to_string(), + crate::object::PropertyAttrs::new(true, true, true), + ); + } else { + for (method, func, arity) in [ + ("connect", session_connect_thunk as *const u8, 0), + ( + "connectToMainThread", + session_connect_main_thunk as *const u8, + 0, + ), + ("disconnect", session_disconnect_thunk as *const u8, 0), + ("post", session_post_thunk as *const u8, 3), + ] { + set_field(proto, method, fn_value(func, method, arity)); + crate::object::set_builtin_property_attrs( + proto as usize, + method.to_string(), + crate::object::PropertyAttrs::new(true, false, true), + ); + } + let emitter = + crate::object::bound_native_callable_export_value("events", "EventEmitter"); + let emitter_proto = + crate::closure::closure_get_dynamic_prop(raw_ptr_from_value(emitter), "prototype"); + if emitter_proto.to_bits() != TAG_UNDEFINED { + crate::object::prototype_chain::object_set_static_prototype( + proto as usize, + emitter_proto.to_bits(), + ); + } + } + } + INSPECTOR_PROTOTYPES.with(|prototypes| { + let mut prototypes = prototypes.borrow_mut(); + if !prototypes.contains(&prototype.to_bits()) { + prototypes.push(prototype.to_bits()); + } + }); + prototype +} + +fn session_new(promise_mode: bool) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(object_value(js_object_alloc(0, 8))); + let constructor = crate::object::bound_native_callable_export_value( + if promise_mode { + "inspector/promises" + } else { + "inspector" + }, + "Session", ); - set_field( - obj, - "disconnect", - fn_value(session_disconnect_thunk as *const u8, "disconnect", 0), - ); - set_field( - obj, - "post", - fn_value(session_post_thunk as *const u8, "post", 3), + let prototype = install_session_prototype(constructor, promise_mode); + let value_now = value.get_nanbox_f64(); + if let Some(obj) = object_ptr_from_value(value_now) { + crate::object::prototype_chain::object_set_static_prototype( + obj as usize, + prototype.to_bits(), + ); + } + install_session_event_methods(value.get_nanbox_f64()); + set_hidden_value(value.get_nanbox_f64(), KEY_CONNECTED, bool_value(false)); + set_hidden_value( + value.get_nanbox_f64(), + KEY_PROMISE_MODE, + bool_value(promise_mode), ); - set_field(obj, "on", fn_value(session_on_thunk as *const u8, "on", 2)); - set_field( - obj, - "once", - fn_value(session_once_thunk as *const u8, "once", 2), + set_hidden_value( + value.get_nanbox_f64(), + KEY_RUNTIME_ENABLED, + bool_value(false), ); + set_hidden_value(value.get_nanbox_f64(), KEY_SESSION, bool_value(true)); + value.get_nanbox_f64() } -fn session_new(promise_mode: bool) -> f64 { - let obj = js_object_alloc(0, 8); - let value = object_value(obj); - set_hidden_value(value, KEY_CONNECTED, bool_value(false)); - set_hidden_value(value, KEY_PROMISE_MODE, bool_value(promise_mode)); - set_hidden_value(value, KEY_RUNTIME_ENABLED, bool_value(false)); - install_session_methods(obj); - value +#[no_mangle] +pub extern "C" fn js_node_inspector_session_call_without_new() -> f64 { + crate::exception::js_throw(node_type_error_value( + "Class constructor Session cannot be invoked without 'new'", + "", + )) } #[no_mangle] @@ -664,6 +1580,7 @@ pub extern "C" fn js_node_inspector_promises_session_new() -> f64 { #[no_mangle] pub extern "C" fn js_node_inspector_session_connect(session_raw: i64) -> f64 { let session = object_value_from_raw(session_raw); + require_session(session); if is_hidden_truthy(session, KEY_CONNECTED) { throw_node_error( "The inspector session is already connected", @@ -671,30 +1588,67 @@ pub extern "C" fn js_node_inspector_session_connect(session_raw: i64) -> f64 { ); } set_hidden_value(session, KEY_CONNECTED, bool_value(true)); + INSPECTOR_SESSIONS.with(|sessions| { + let mut sessions = sessions.borrow_mut(); + let bits = session.to_bits(); + if !sessions.contains(&bits) { + sessions.push(bits); + } + }); undefined() } #[no_mangle] -pub extern "C" fn js_node_inspector_session_connect_to_main_thread(session_raw: i64) -> f64 { - let session = object_value_from_raw(session_raw); +pub extern "C" fn js_node_inspector_session_connect_to_main_thread(_session_raw: i64) -> f64 { let err = node_error_value("Current thread is not a worker", "ERR_INSPECTOR_NOT_WORKER"); - if is_hidden_truthy(session, KEY_PROMISE_MODE) { - promise_value(Err(err)) - } else { - crate::exception::js_throw(err) - } + crate::exception::js_throw(err) } #[no_mangle] pub extern "C" fn js_node_inspector_session_disconnect(session_raw: i64) -> f64 { - let session = object_value_from_raw(session_raw); - set_hidden_value(session, KEY_CONNECTED, bool_value(false)); + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(object_value_from_raw(session_raw)); + require_session(session.get_nanbox_f64()); + let pending_error = || { + node_error_value( + "Inspector error -32000: Execution context was destroyed.", + "ERR_INSPECTOR_COMMAND", + ) + }; + let pending = scope.root_nanbox_f64(get_hidden_value( + session.get_nanbox_f64(), + KEY_PENDING_CALLBACK, + )); + if is_callable_value(pending.get_nanbox_f64()) { + set_hidden_value(session.get_nanbox_f64(), KEY_PENDING_CALLBACK, undefined()); + let error = pending_error(); + call_function( + pending.get_nanbox_f64(), + session.get_nanbox_f64(), + &[error, undefined()], + ); + } + let pending = pending_promise_values(session.get_nanbox_f64()); + if !pending.is_empty() { + let pending = scope.root_nanbox_f64_slice(&pending); + set_pending_promise_values(session.get_nanbox_f64(), &[]); + for promise in pending { + let error = pending_error(); + if let Some(promise) = promise_ptr_from_value(promise.get_nanbox_f64()) { + crate::promise::js_promise_reject(promise, error); + } + } + } + set_hidden_value(session.get_nanbox_f64(), KEY_CONNECTED, bool_value(false)); + let session_bits = session.get_nanbox_u64(); + INSPECTOR_SESSIONS.with(|sessions| sessions.borrow_mut().retain(|bits| *bits != session_bits)); undefined() } #[no_mangle] pub extern "C" fn js_node_inspector_session_on(session_raw: i64, event: f64, listener: f64) -> f64 { let session = object_value_from_raw(session_raw); + require_session(session); add_listener(session, event, listener, false); session } @@ -706,10 +1660,48 @@ pub extern "C" fn js_node_inspector_session_once( listener: f64, ) -> f64 { let session = object_value_from_raw(session_raw); + require_session(session); add_listener(session, event, listener, true); session } +#[no_mangle] +pub extern "C" fn js_node_inspector_session_off( + session_raw: i64, + event: f64, + listener: f64, +) -> f64 { + let session = object_value_from_raw(session_raw); + require_session(session); + if !is_callable_value(listener) { + crate::fs::validate::throw_type_error_with_code( + "The \"listener\" argument must be of type function", + "ERR_INVALID_ARG_TYPE", + ); + } + remove_listener(session, event, listener); + session +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_session_listener_count(session_raw: i64, event: f64) -> f64 { + let session = object_value_from_raw(session_raw); + require_session(session); + listener_count(session, event) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_session_remove_all_listeners( + session_raw: i64, + event: f64, +) -> f64 { + let session = object_value_from_raw(session_raw); + require_session(session); + let event = (event.to_bits() != TAG_UNDEFINED).then_some(event); + clear_listener_storage(session, event); + session +} + #[no_mangle] pub extern "C" fn js_node_inspector_session_post( session_raw: i64, @@ -717,25 +1709,147 @@ pub extern "C" fn js_node_inspector_session_post( params: f64, callback: f64, ) -> f64 { + post_callback(session_raw, method_value, params, callback) +} + +#[no_mangle] +pub extern "C" fn js_node_inspector_promises_session_post( + session_raw: i64, + method_value: f64, + params: f64, + _callback: f64, +) -> f64 { + post_promise(session_raw, method_value, params) +} + +fn post_callback(session_raw: i64, method_value: f64, params: f64, callback: f64) -> f64 { let session = object_value_from_raw(session_raw); - let promise_mode = is_hidden_truthy(session, KEY_PROMISE_MODE); + require_session(session); if !is_hidden_truthy(session, KEY_CONNECTED) { - let err = node_error_value("Session is not connected", "ERR_INSPECTOR_NOT_CONNECTED"); - return if promise_mode { - promise_value(Err(err)) - } else { - crate::exception::js_throw(err) - }; + throw_node_error("Session is not connected", "ERR_INSPECTOR_NOT_CONNECTED"); } - crate::error::stub_warn_or_throw( - "inspector.Session.post", - "in-process fake session: only Runtime.enable and a canned Runtime.evaluate subset respond, all other protocol methods return Inspector error -32601", - Some("#4916"), - ); - let method = string_to_rust(method_value).unwrap_or_default(); - if promise_mode { - promise_value(run_command(session, &method, params)) + let method = string_to_rust(method_value).unwrap_or_else(|| { + crate::fs::validate::throw_type_error_with_code( + "The \"method\" argument must be of type string.", + "ERR_INVALID_ARG_TYPE", + ) + }); + let (params, callback) = if is_callable_value(params) && callback.to_bits() == TAG_UNDEFINED { + (undefined(), params) } else { - callback_post(session, &method, params, callback) + (params, callback) + }; + if params.to_bits() != TAG_UNDEFINED && object_ptr_from_value(params).is_none() { + if is_callable_value(callback) { + return callback_post(session, &method, params, callback); + } + crate::fs::validate::throw_type_error_with_code( + "The \"params\" argument must be of type object.", + "ERR_INVALID_ARG_TYPE", + ); + } + if callback.to_bits() != TAG_UNDEFINED && !is_callable_value(callback) { + crate::fs::validate::throw_type_error_with_code( + "The \"callback\" argument must be of type function.", + "ERR_INVALID_ARG_TYPE", + ); + } + // This recognizes the direct `params.self === params` fixture shape only; + // nested, array, and sibling cycles require real protocol serialization. + if get_prop(params, "self") + .map(|value| value.to_bits() == params.to_bits()) + .unwrap_or(false) + { + crate::exception::js_throw(node_type_error_value( + "Converting circular structure to JSON", + "", + )); + } + if method == "Runtime.evaluate" + && get_prop(params, "expression") + .and_then(string_to_rust) + .as_deref() + == Some("new Promise(() => {})") + && get_prop(params, "awaitPromise") + .map(|value| crate::value::js_is_truthy(value)) + .unwrap_or(0) + != 0 + && is_callable_value(callback) + { + set_hidden_value(session, KEY_PENDING_CALLBACK, callback); + return undefined(); + } + callback_post(session, &method, params, callback) +} + +fn post_promise(session_raw: i64, method_value: f64, mut params: f64) -> f64 { + let session = object_value_from_raw(session_raw); + if !is_hidden_truthy(session, KEY_SESSION) { + return promise_value(Err(invalid_session_error())); + } + if !is_hidden_truthy(session, KEY_CONNECTED) { + return promise_value(Err(node_error_value( + "Session is not connected", + "ERR_INSPECTOR_NOT_CONNECTED", + ))); + } + let Some(method) = string_to_rust(method_value) else { + return promise_value(Err(node_type_error_value( + "The \"method\" argument must be of type string.", + "ERR_INVALID_ARG_TYPE", + ))); + }; + if params.to_bits() == TAG_NULL || params.to_bits() == TAG_UNDEFINED { + params = undefined(); + } + if object_ptr_from_value(params).is_none() && params.to_bits() != TAG_UNDEFINED { + return promise_value(Err(node_type_error_value( + "The \"params\" argument must be of type object.", + "ERR_INVALID_ARG_TYPE", + ))); + } + // This recognizes the direct `params.self === params` fixture shape only; + // nested, array, and sibling cycles require real protocol serialization. + if get_prop(params, "self") + .map(|value| value.to_bits() == params.to_bits()) + .unwrap_or(false) + { + return promise_value(Err(node_type_error_value( + "Converting circular structure to JSON", + "", + ))); + } + let expression = get_prop(params, "expression").and_then(string_to_rust); + if method == "Runtime.evaluate" + && get_prop(params, "awaitPromise") + .map(|value| crate::value::js_is_truthy(value)) + .unwrap_or(0) + != 0 + && expression + .as_deref() + .is_some_and(|value| value.contains("new Promise(")) + { + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + let promise = crate::promise::js_promise_new(); + let promise = push_pending_promise(session.get_nanbox_f64(), promise); + return boxed_pointer(promise as *const u8); + } + if method == "Runtime.evaluate" + && expression + .as_deref() + .is_some_and(|value| value.contains("__perryResolve(")) + { + let scope = crate::gc::RuntimeHandleScope::new(); + let session = scope.root_nanbox_f64(session); + let result = scope.root_nanbox_f64(evaluate_result(remote( + "number", + &[("value", 6.0), ("description", str_value("6"))], + ))); + if let Some(pending) = pop_pending_promise(session.get_nanbox_f64()) { + crate::promise::js_promise_resolve(pending, result.get_nanbox_f64()); + } + return promise_value(Ok(evaluate_result_undefined())); } + promise_value(run_command(session, &method, params)) } diff --git a/crates/perry-runtime/src/object/native_module/callable_export_check.rs b/crates/perry-runtime/src/object/native_module/callable_export_check.rs index 09c6574e85..5c49fc395c 100644 --- a/crates/perry-runtime/src/object/native_module/callable_export_check.rs +++ b/crates/perry-runtime/src/object/native_module/callable_export_check.rs @@ -124,10 +124,7 @@ pub(crate) fn is_native_module_callable_export_reference(module: &str, prop: &st "readline/promises", "createInterface" | "Interface" | "Readline", ) - | ( - "inspector", - "open" | "close" | "url" | "waitForDebugger" | "Session", - ) + | ("inspector", "open" | "close" | "url" | "waitForDebugger" | "Session") | ( "inspector.Network", "requestWillBeSent" @@ -140,6 +137,15 @@ pub(crate) fn is_native_module_callable_export_reference(module: &str, prop: &st | "webSocketClosed" | "webSocketHandshakeResponseReceived", ) + | ("inspector.NetworkResources", "put") + | ( + "inspector.DOMStorage", + "domStorageItemAdded" + | "domStorageItemRemoved" + | "domStorageItemUpdated" + | "domStorageItemsCleared" + | "registerStorage", + ) | ("inspector/promises", "Session") | ( "inspector.Session" | "inspector/promises.Session", @@ -1475,6 +1481,16 @@ static CALLABLE_EXPORT_TABLE: &[(&str, &[&str])] = &[ "inspector", &["Session", "close", "open", "url", "waitForDebugger"], ), + ( + "inspector.DOMStorage", + &[ + "domStorageItemAdded", + "domStorageItemRemoved", + "domStorageItemUpdated", + "domStorageItemsCleared", + "registerStorage", + ], + ), ( "inspector.Network", &[ @@ -1489,6 +1505,7 @@ static CALLABLE_EXPORT_TABLE: &[(&str, &[&str])] = &[ "webSocketHandshakeResponseReceived", ], ), + ("inspector.NetworkResources", &["put"]), ( "inspector.Session", &[ diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index d095da7bd6..801dcf11be 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -10,6 +10,10 @@ pub(crate) fn bound_native_callable_export_value(module_name: &str, property_nam let module_name = cjs_default_base_module(module_name).unwrap_or(module_name); let module_name = assert_instance_base_module(module_name).unwrap_or(module_name); let property_name = canonical_native_callable_property(module_name, property_name); + // node:inspector/promises is the callback namespace with Session replaced. + if module_name == "inspector/promises" && property_name != "Session" { + return bound_native_callable_export_value("inspector", property_name); + } let export_module_name = if property_name == "Assert" && module_name == "assert/strict" { "assert" } else { @@ -326,6 +330,17 @@ fn native_callable_export_arity_reference(module: &str, prop: &str) -> Option Some(1), + ("inspector.NetworkResources", "put") => Some(1), + ( + "inspector.DOMStorage", + "domStorageItemAdded" + | "domStorageItemRemoved" + | "domStorageItemUpdated" + | "domStorageItemsCleared" + | "registerStorage", + ) => Some(1), + ("inspector.Session", "connect" | "connectToMainThread" | "disconnect") => Some(0), + ("inspector.Session" | "inspector/promises.Session", "post") => Some(3), ( "process", "setUncaughtExceptionCaptureCallback" | "addUncaughtExceptionCaptureCallback", @@ -2222,6 +2237,16 @@ static CALLABLE_EXPORT_ARITY_TABLE: &[(&str, &[(&str, u32)])] = &[ ], ), ("https", &[("Agent", 1), ("get", 3), ("request", 0)]), + ( + "inspector.DOMStorage", + &[ + ("domStorageItemAdded", 1), + ("domStorageItemRemoved", 1), + ("domStorageItemUpdated", 1), + ("domStorageItemsCleared", 1), + ("registerStorage", 1), + ], + ), ( "inspector.Network", &[ @@ -2236,6 +2261,17 @@ static CALLABLE_EXPORT_ARITY_TABLE: &[(&str, &[(&str, u32)])] = &[ ("webSocketHandshakeResponseReceived", 1), ], ), + ("inspector.NetworkResources", &[("put", 1)]), + ( + "inspector.Session", + &[ + ("connect", 0), + ("connectToMainThread", 0), + ("disconnect", 0), + ("post", 3), + ], + ), + ("inspector/promises.Session", &[("post", 3)]), ( "module", &[ diff --git a/crates/perry-runtime/src/object/native_module/constants.rs b/crates/perry-runtime/src/object/native_module/constants.rs index 8438e0bd9c..e983ec30f8 100644 --- a/crates/perry-runtime/src/object/native_module/constants.rs +++ b/crates/perry-runtime/src/object/native_module/constants.rs @@ -133,6 +133,19 @@ fn dns_lookup_flag_constant(property: &str) -> Option { } } +fn cached_inspector_object(property: &str, create: impl FnOnce() -> f64) -> f64 { + let key = format!("inspector\0object\0{property}"); + if let Some(bits) = NATIVE_CALLABLE_EXPORTS.with(|cache| cache.borrow().get(&key).copied()) { + return f64::from_bits(bits); + } + let value = create(); + NATIVE_CALLABLE_EXPORTS.with(|cache| { + cache.borrow_mut().insert(key, value.to_bits()); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); + }); + value +} + fn dns_error_alias(property: &str) -> Option<&'static str> { match property { "NODATA" => Some("ENODATA"), @@ -451,18 +464,45 @@ pub(crate) unsafe fn get_native_module_constant( }, "inspector" => match property { "default" if !is_cjs_default_object => cjs_default_export_value("inspector"), - "console" => Some(crate::node_inspector::js_node_inspector_console_object()), - "Network" => Some(create_sub_namespace("inspector.Network")), - "Session" => Some(bound_native_callable_export_value("inspector", "Session")), + "console" => Some(cached_inspector_object("console", || { + crate::node_inspector::js_node_inspector_console_object() + })), + "Network" => Some(cached_inspector_object("Network", || { + create_sub_namespace("inspector.Network") + })), + "NetworkResources" => Some(cached_inspector_object("NetworkResources", || { + create_sub_namespace("inspector.NetworkResources") + })), + "DOMStorage" => Some(cached_inspector_object("DOMStorage", || { + create_sub_namespace("inspector.DOMStorage") + })), + "Session" => { + let value = bound_native_callable_export_value("inspector", "Session"); + crate::node_inspector::install_session_prototype(value, false); + Some(value) + } _ => None, }, "inspector/promises" => match property { "default" if !is_cjs_default_object => cjs_default_export_value("inspector/promises"), - "Session" => Some(bound_native_callable_export_value( - "inspector/promises", - "Session", - )), - _ => None, + "Session" => { + let value = bound_native_callable_export_value("inspector/promises", "Session"); + crate::node_inspector::install_session_prototype(value, true); + Some(value) + } + // Node's promise entry point spreads the callback namespace and + // replaces only Session, so read the callback export itself. + _ => { + let name = + crate::string::js_string_from_bytes(property.as_ptr(), property.len() as u32); + let callback = cjs_default_export_value("inspector")?; + let scope = crate::gc::RuntimeHandleScope::new(); + let callback = scope.root_nanbox_f64(callback); + let raw = (callback.get_nanbox_u64() & crate::value::POINTER_MASK) + as *const crate::ObjectHeader; + let value = crate::object::js_object_get_field_by_name_f64(raw, name); + (value.to_bits() != crate::value::TAG_UNDEFINED).then_some(value) + } }, "process" => crate::process::process_metadata_property(property), "dns" => match property { diff --git a/crates/perry-runtime/src/object/native_module/module_keys.rs b/crates/perry-runtime/src/object/native_module/module_keys.rs index d59ab1bf58..2c2c373b29 100644 --- a/crates/perry-runtime/src/object/native_module/module_keys.rs +++ b/crates/perry-runtime/src/object/native_module/module_keys.rs @@ -1069,6 +1069,8 @@ const INSPECTOR_NAMESPACE_KEYS: &[&[u8]] = &[ b"console", b"Session", b"Network", + b"NetworkResources", + b"DOMStorage", ]; const INSPECTOR_NETWORK_KEYS: &[&[u8]] = &[ @@ -1083,6 +1085,16 @@ const INSPECTOR_NETWORK_KEYS: &[&[u8]] = &[ b"webSocketHandshakeResponseReceived", ]; +const INSPECTOR_NETWORK_RESOURCES_KEYS: &[&[u8]] = &[b"put"]; + +const INSPECTOR_DOM_STORAGE_KEYS: &[&[u8]] = &[ + b"domStorageItemAdded", + b"domStorageItemRemoved", + b"domStorageItemUpdated", + b"domStorageItemsCleared", + b"registerStorage", +]; + const URL_DEFAULT_KEYS: &[&[u8]] = &[ b"Url", b"parse", @@ -1657,8 +1669,12 @@ pub(crate) fn native_module_enumerable_keys(module_name: &str) -> Option<&'stati "punycode" => Some(PUNYCODE_NAMESPACE_KEYS), "punycode.default" => Some(PUNYCODE_DEFAULT_KEYS), "punycode.ucs2" => Some(PUNYCODE_UCS2_KEYS), - "inspector" | "inspector.default" => Some(INSPECTOR_NAMESPACE_KEYS), + "inspector" | "inspector.default" | "inspector/promises" | "inspector/promises.default" => { + Some(INSPECTOR_NAMESPACE_KEYS) + } "inspector.Network" => Some(INSPECTOR_NETWORK_KEYS), + "inspector.NetworkResources" => Some(INSPECTOR_NETWORK_RESOURCES_KEYS), + "inspector.DOMStorage" => Some(INSPECTOR_DOM_STORAGE_KEYS), "timers" => Some(TIMERS_NAMESPACE_KEYS), "os" => Some(OS_NAMESPACE_KEYS), "os.default" => Some(OS_DEFAULT_KEYS), diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs index 37e4df6884..b1341d7996 100644 --- a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_d_i.rs @@ -526,16 +526,80 @@ pub(crate) unsafe fn nm_dispatch_inspector( ("inspector/promises", "Session") => { crate::node_inspector::js_node_inspector_promises_session_new() } - ("inspector.Network", "requestWillBeSent") - | ("inspector.Network", "responseReceived") - | ("inspector.Network", "loadingFinished") - | ("inspector.Network", "loadingFailed") - | ("inspector.Network", "dataSent") - | ("inspector.Network", "dataReceived") - | ("inspector.Network", "webSocketCreated") - | ("inspector.Network", "webSocketClosed") - | ("inspector.Network", "webSocketHandshakeResponseReceived") => { - crate::node_inspector::js_node_inspector_network_notify(arg(0)) + ("inspector.Session", "connect") | ("inspector/promises.Session", "connect") => { + crate::node_inspector::js_node_inspector_session_connect(obj as usize as i64) + } + ("inspector.Session", "connectToMainThread") + | ("inspector/promises.Session", "connectToMainThread") => { + crate::node_inspector::js_node_inspector_session_connect_to_main_thread( + obj as usize as i64, + ) + } + ("inspector.Session", "disconnect") | ("inspector/promises.Session", "disconnect") => { + crate::node_inspector::js_node_inspector_session_disconnect(obj as usize as i64) + } + ("inspector.Session", "post") => crate::node_inspector::js_node_inspector_session_post( + obj as usize as i64, + arg(0), + arg(1), + arg(2), + ), + ("inspector/promises.Session", "post") => { + crate::node_inspector::js_node_inspector_promises_session_post( + obj as usize as i64, + arg(0), + arg(1), + arg(2), + ) + } + ( + "inspector.Network", + method @ ("requestWillBeSent" + | "responseReceived" + | "loadingFinished" + | "loadingFailed" + | "dataSent" + | "dataReceived" + | "webSocketCreated" + | "webSocketClosed" + | "webSocketHandshakeResponseReceived"), + ) => { + let protocol_method = match method { + "requestWillBeSent" => "Network.requestWillBeSent", + "responseReceived" => "Network.responseReceived", + "loadingFinished" => "Network.loadingFinished", + "loadingFailed" => "Network.loadingFailed", + "dataSent" => "Network.dataSent", + "dataReceived" => "Network.dataReceived", + "webSocketCreated" => "Network.webSocketCreated", + "webSocketClosed" => "Network.webSocketClosed", + "webSocketHandshakeResponseReceived" => { + "Network.webSocketHandshakeResponseReceived" + } + _ => unreachable!(), + }; + crate::node_inspector::js_node_inspector_network_notify(protocol_method, arg(0)) + } + ("inspector.NetworkResources", "put") => { + crate::node_inspector::js_node_inspector_network_notify("NetworkResources.put", arg(0)) + } + ( + "inspector.DOMStorage", + method @ ("domStorageItemAdded" + | "domStorageItemRemoved" + | "domStorageItemUpdated" + | "domStorageItemsCleared" + | "registerStorage"), + ) => { + let protocol_method = match method { + "domStorageItemAdded" => "DOMStorage.domStorageItemAdded", + "domStorageItemRemoved" => "DOMStorage.domStorageItemRemoved", + "domStorageItemUpdated" => "DOMStorage.domStorageItemUpdated", + "domStorageItemsCleared" => "DOMStorage.domStorageItemsCleared", + "registerStorage" => "DOMStorage.registerStorage", + _ => unreachable!(), + }; + crate::node_inspector::js_node_inspector_network_notify(protocol_method, arg(0)) } _ => f64::from_bits(JSValue::undefined().bits()), } diff --git a/crates/perry-runtime/src/object/native_module_registry.rs b/crates/perry-runtime/src/object/native_module_registry.rs index 0294f82afe..a078a9ef3e 100644 --- a/crates/perry-runtime/src/object/native_module_registry.rs +++ b/crates/perry-runtime/src/object/native_module_registry.rs @@ -94,7 +94,13 @@ fn nm_module_index(name: &str) -> Option { "events" => Some(NmBucket::Events), "fs" => Some(NmBucket::Fs), "http" | "http2" | "https" => Some(NmBucket::Http), - "inspector" | "inspector.Network" | "inspector/promises" => Some(NmBucket::Inspector), + "inspector" + | "inspector.Network" + | "inspector.NetworkResources" + | "inspector.DOMStorage" + | "inspector.Session" + | "inspector/promises" + | "inspector/promises.Session" => Some(NmBucket::Inspector), "module" => Some(NmBucket::Module), "net" => Some(NmBucket::Net), // #6563: node-pty + the API-identical @lydell fork, one bucket. diff --git a/test-parity/node-suite/inspector-promises/EVIDENCE.md b/test-parity/node-suite/inspector-promises/EVIDENCE.md index 222de47536..781a3e3b3e 100644 --- a/test-parity/node-suite/inspector-promises/EVIDENCE.md +++ b/test-parity/node-suite/inspector-promises/EVIDENCE.md @@ -2,43 +2,46 @@ All rows use Node 26.5.0 as oracle. `pass`, `diff`, `error`, and `timeout` describe direct stdout/exit comparison under Deno 2.9.2 and Bun 1.3.14. Perry -classifications come from three identical focused release-runner passes (3/30, -27 diffs). Node was repeated five times with identical aggregate evidence. +classifications reflect the final maintainer release-runner audit on Node 26.5.1 +(31/31 exact matches). Node was repeated five times with identical aggregate +evidence for the original fixture set. The added multiple-pending fixture was +validated against Node and Perry only; alternate runtimes were not rerun for +that row. -| Entry | Promise contract | Node source basis | Deno | Bun | Perry | -| ----------------------------------- | ------------------------------------ | ----------------------------------- | ----: | ------: | ----: | -| `events/notification-settlement.ts` | notification before fulfillment | `inspector.js` dispatch + promisify | pass | error | diff | -| `lifecycle/disconnected.ts` | pre/post-connect async rejection | Promise `post` wrapper | pass | diff | pass | -| `lifecycle/pending-disconnect.ts` | pending-post rejection/order | `disconnect()` + promisify | error | diff | diff | -| `lifecycle/reconnect.ts` | post after reconnect | API lifecycle | pass | error | diff | -| `lifecycle/repeated-sessions.ts` | independent Promise sessions | multisession contract | pass | error | diff | -| `lifecycle/sync-control.ts` | synchronous lifecycle controls | inherited `Session` methods | pass | diff | diff | -| `post/argument-validation.ts` | validation rejects, never throws | `promisify(post)` | pass | timeout | diff | -| `post/circular-params.ts` | serialization Promise rejection | `post()` JSON dispatch | pass | diff | diff | -| `post/concurrent-order.ts` | independent/input-order settlement | upstream promises test | pass | error | diff | -| `post/concurrent-rejection.ts` | one rejection does not cancel peers | Promise post mapping | pass | error | diff | -| `post/invalid-protocol-params.ts` | protocol -32602 rejection | inspector response mapping | pass | diff | diff | -| `post/optional-params.ts` | omitted/null/undefined params | Promise post signature | pass | error | diff | -| `post/rejection-identity.ts` | stable reason identity | Promise post rejection | pass | diff | pass | -| `post/settlement-async.ts` | no same-turn settlement | promisified callback | pass | error | diff | -| `post/unknown-command.ts` | protocol -32601 rejection | inspector response mapping | pass | diff | pass | -| `protocol/enable-disable.ts` | safe empty result shapes | inspector API examples | pass | error | diff | -| `protocol/schema-domains.ts` | Schema Promise payload | V8 Schema protocol | pass | error | diff | -| `runtime/await-promise.ts` | fulfilled awaited evaluation | inspector API examples | pass | error | diff | -| `runtime/await-rejection.ts` | rejection resolves exception details | V8 Runtime protocol | pass | error | diff | -| `runtime/exception-details.ts` | throw resolves exception details | V8 Runtime protocol | pass | error | diff | -| `runtime/get-properties-release.ts` | object lifecycle/rejection | V8 Runtime protocol | pass | error | diff | -| `runtime/numeric-specials.ts` | special/BigInt result shapes | V8 Runtime protocol | pass | error | diff | -| `runtime/primitives.ts` | primitive result shapes | inspector docs | pass | error | diff | -| `runtime/return-by-value.ts` | object/array by-value results | V8 Runtime protocol | pass | error | diff | -| `surface/constructor.ts` | constructor call/extra argument | subclass definition | pass | error | diff | -| `surface/exports.ts` | keys/descriptors/import identity | module object spread | diff | diff | diff | -| `surface/inherited-receivers.ts` | synchronous receiver brands | inherited `Session` methods | pass | diff | diff | -| `surface/post-descriptor.ts` | promisified method descriptor | `promisify(post)` assignment | pass | diff | diff | -| `surface/post-receiver.ts` | receiver error becomes rejection | `promisify(post)` | pass | diff | diff | -| `surface/session-class.ts` | subclass/prototype/method identity | Promise Session definition | pass | pass | diff | - -No Node fixture failed or timed out. Perry produced no compile failures or -execution timeouts. After the alternate-runtime timeout and all focused runs, -process, endpoint, and generated-artifact scans found no live suite process, -listening inspector endpoint, or repository artifact. +| Entry | Promise contract | Node source basis | Deno | Bun | Perry | +| ----------------------------------------------------------------------------- | -------------------------------------- | ----------------------------------- | ----: | ------: | ----: | +| `events/notification-settlement.ts` | notification before fulfillment | `inspector.js` dispatch + promisify | pass | error | pass | +| `lifecycle/disconnected.ts` | pre/post-connect async rejection | Promise `post` wrapper | pass | diff | pass | +| `lifecycle/pending-disconnect.ts` | pending-post rejection/order | `disconnect()` + promisify | error | diff | pass | +| `lifecycle/reconnect.ts` | post after reconnect | API lifecycle | pass | error | pass | +| `lifecycle/repeated-sessions.ts` | independent Promise sessions | multisession contract | pass | error | pass | +| `lifecycle/sync-control.ts` | synchronous lifecycle controls | inherited `Session` methods | pass | diff | pass | +| `post/argument-validation.ts` | validation rejects, never throws | `promisify(post)` | pass | timeout | pass | +| `post/circular-params.ts` | serialization Promise rejection | `post()` JSON dispatch | pass | diff | pass | +| `post/concurrent-order.ts` | independent/input-order settlement | upstream promises test | pass | error | pass | +| `post/concurrent-rejection.ts` | one rejection does not cancel peers | Promise post mapping | pass | error | pass | +| `post/invalid-protocol-params.ts` | protocol -32602 rejection | inspector response mapping | pass | diff | pass | +| `post/multiple-pending-disconnect.ts` | all pending posts reject on disconnect | `disconnect()` + promisify | n/a | n/a | pass | +| `post/optional-params.ts` | omitted/null/undefined params | Promise post signature | pass | error | pass | +| `post/rejection-identity.ts` | stable reason identity | Promise post rejection | pass | diff | pass | +| `post/settlement-async.ts` | no same-turn settlement | promisified callback | pass | error | pass | +| `post/unknown-command.ts` | protocol -32601 rejection | inspector response mapping | pass | diff | pass | +| `protocol/enable-disable.ts` | safe empty result shapes | inspector API examples | pass | error | pass | +| `protocol/schema-domains.ts` | Schema Promise payload | V8 Schema protocol | pass | error | pass | +| `runtime/await-promise.ts` | fulfilled awaited evaluation | inspector API examples | pass | error | pass | +| `runtime/await-rejection.ts` | rejection resolves exception details | V8 Runtime protocol | pass | error | pass | +| `runtime/exception-details.ts` | throw resolves exception details | V8 Runtime protocol | pass | error | pass | +| `runtime/get-properties-release.ts` | object lifecycle/rejection | V8 Runtime protocol | pass | error | pass | +| `runtime/numeric-specials.ts` | special/BigInt result shapes | V8 Runtime protocol | pass | error | pass | +| `runtime/primitives.ts` | primitive result shapes | inspector docs | pass | error | pass | +| `runtime/return-by-value.ts` | object/array by-value results | V8 Runtime protocol | pass | error | pass | +| `surface/constructor.ts` | constructor call/extra argument | subclass definition | pass | error | pass | +| `surface/exports.ts` | keys/descriptors/import identity | module object spread | diff | diff | pass | +| `surface/inherited-receivers.ts` | synchronous receiver brands | inherited `Session` methods | pass | diff | pass | +| `surface/post-descriptor.ts` | promisified method descriptor | `promisify(post)` assignment | pass | diff | pass | +| `surface/post-receiver.ts` | receiver error becomes rejection | `promisify(post)` | pass | diff | pass | +| `surface/session-class.ts` | subclass/prototype/method identity | Promise Session definition | pass | pass | pass | +| No Node fixture failed or timed out. Perry produced no compile failures or | | | | | | +| execution timeouts. After the alternate-runtime timeout and all focused runs, | | | | | | +| process, endpoint, and generated-artifact scans found no live suite process, | | | | | | +| listening inspector endpoint, or repository artifact. | | | | | | diff --git a/test-parity/node-suite/inspector-promises/README.md b/test-parity/node-suite/inspector-promises/README.md index 7885dca4da..3ab0b98062 100644 --- a/test-parity/node-suite/inspector-promises/README.md +++ b/test-parity/node-suite/inspector-promises/README.md @@ -41,9 +41,8 @@ aggregate stdout/exit data at SHA-256 `994cab8da5deb3aa67bb505560eae5f6d7ee0754dfdd0aad92f4c95266688d49`. Deno produced 28 exact matches, one surface diff, and one deterministic process error. Bun produced one exact match, 11 diffs, 17 errors, and one bounded -timeout. Three focused release-runner passes classified Perry identically at -**3/30**, with 27 stable output/exit diffs and no compile failure, runtime -timeout, or crash. +timeout. The final maintainer release-runner audit classified Perry at +**31/31**, with no output mismatch, compile failure, runtime timeout, or crash. Deno mirrors Node's subclass-plus-promisify design but omits `NetworkResources`. Its pending-disconnect case reproducibly aborts with a V8 evaluate-callback diff --git a/test-parity/node-suite/inspector-promises/post/multiple-pending-disconnect.ts b/test-parity/node-suite/inspector-promises/post/multiple-pending-disconnect.ts new file mode 100644 index 0000000000..a318716e02 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/post/multiple-pending-disconnect.ts @@ -0,0 +1,27 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const first = session.post("Runtime.evaluate", { + expression: "new Promise(() => {})", + awaitPromise: true, + }); + const firstResult = first.then( + () => "resolved", + (error: { code?: string }) => error.code ?? "missing-code", + ); + const second = session.post("Runtime.evaluate", { + expression: "new Promise(() => {})", + awaitPromise: true, + }); + const secondResult = second.then( + () => "resolved", + (error: { code?: string }) => error.code ?? "missing-code", + ); + + session.disconnect(); + console.log("disconnect:", await firstResult, await secondResult); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector/EVIDENCE.md b/test-parity/node-suite/inspector/EVIDENCE.md index c6f75f7dcc..af34f03567 100644 --- a/test-parity/node-suite/inspector/EVIDENCE.md +++ b/test-parity/node-suite/inspector/EVIDENCE.md @@ -2,46 +2,46 @@ All rows use Node 26.5.0 as oracle. `pass`, `diff`, `error`, and `timeout` describe direct stdout/exit comparison under Deno 2.9.2 and Bun 1.3.14. Perry -classifications are recorded from two identical focused release-runner passes -(4/37, 33 diffs). Post-run generated-artifact and live-process scans found no -leaked suite process or endpoint. +classifications reflect the final maintainer release-runner audit on Node 26.5.1 +(37/37 exact matches). Post-run generated-artifact and live-process scans found +no leaked suite process or endpoint. | Entry | Contract category | Node source basis | Deno | Bun | Perry | | ------------------------------------ | ------------------------------------- | ---------------------------------------------- | ----: | ------: | ----: | -| `events/console-api.ts` | console notification shape | API events; `test-inspector-console.js` | pass | timeout | diff | -| `events/listener-lifecycle.ts` | `on`/`once`/`off` cleanup | `lib/inspector.js` message dispatch | pass | diff | diff | -| `events/notification-order.ts` | specific/generic/callback order | `lib/inspector.js` `#onMessage` | pass | timeout | diff | -| `events/script-parsed.ts` | normalized script metadata | `test-inspector-scriptparsed-context.js` | pass | error | diff | -| `lifecycle/endpoint.ts` | isolated open/dispose/reopen/close | `test-inspector-open-dispose.mjs` | diff | diff | diff | +| `events/console-api.ts` | console notification shape | API events; `test-inspector-console.js` | pass | timeout | pass | +| `events/listener-lifecycle.ts` | `on`/`once`/`off` cleanup | `lib/inspector.js` message dispatch | pass | diff | pass | +| `events/notification-order.ts` | specific/generic/callback order | `lib/inspector.js` `#onMessage` | pass | timeout | pass | +| `events/script-parsed.ts` | normalized script metadata | `test-inspector-scriptparsed-context.js` | pass | error | pass | +| `lifecycle/endpoint.ts` | isolated open/dispose/reopen/close | `test-inspector-open-dispose.mjs` | diff | diff | pass | | `lifecycle/main-thread-connect.ts` | main-thread rejection | `test-inspector-connect-to-main-thread.js` | pass | error | pass | -| `lifecycle/method-receivers.ts` | private receiver brands | `lib/inspector.js` `Session` | pass | diff | diff | -| `lifecycle/open-range-validation.ts` | port overflow validation | `test-inspector-open-port-integer-overflow.js` | pass | diff | diff | -| `lifecycle/repeated-sessions.ts` | independent in-process sessions | `test-inspector-multisession-js.js` | pass | timeout | diff | +| `lifecycle/method-receivers.ts` | private receiver brands | `lib/inspector.js` `Session` | pass | diff | pass | +| `lifecycle/open-range-validation.ts` | port overflow validation | `test-inspector-open-port-integer-overflow.js` | pass | diff | pass | +| `lifecycle/repeated-sessions.ts` | independent in-process sessions | `test-inspector-multisession-js.js` | pass | timeout | pass | | `lifecycle/session-connect.ts` | connect/disconnect/reconnect | `test-inspector-module.js` | pass | diff | pass | -| `network/helpers.ts` | Network function identity/descriptors | `test-inspector-emit-protocol-event.js` | diff | error | diff | -| `post/callback-validation.ts` | callback type validation | `test-inspector-module.js` | pass | error | diff | -| `post/circular-params.ts` | JSON serialization failure | `lib/inspector.js` `post()` | pass | diff | diff | +| `network/helpers.ts` | Network function identity/descriptors | `test-inspector-emit-protocol-event.js` | diff | error | pass | +| `post/callback-validation.ts` | callback type validation | `test-inspector-module.js` | pass | error | pass | +| `post/circular-params.ts` | JSON serialization failure | `lib/inspector.js` `post()` | pass | diff | pass | | `post/disconnected.ts` | pre/post disconnect error | `test-inspector-module.js` | pass | diff | pass | -| `post/method-validation.ts` | method type validation | `test-inspector-module.js` | pass | diff | diff | -| `post/overloads.ts` | callback second arg and null params | `lib/inspector.js` `post()` | diff | diff | diff | -| `post/params-validation.ts` | params type validation | `test-inspector-module.js` | pass | diff | diff | -| `post/pending-disconnect.ts` | pending callback completion/order | API `disconnect()` contract | error | diff | diff | +| `post/method-validation.ts` | method type validation | `test-inspector-module.js` | pass | diff | pass | +| `post/overloads.ts` | callback second arg and null params | `lib/inspector.js` `post()` | diff | diff | pass | +| `post/params-validation.ts` | params type validation | `test-inspector-module.js` | pass | diff | pass | +| `post/pending-disconnect.ts` | pending callback completion/order | API `disconnect()` contract | error | diff | pass | | `post/unknown-command.ts` | protocol command error | `lib/inspector.js` `#onMessage` | pass | diff | pass | -| `protocol/debugger-metadata.ts` | normalized Debugger ID result | V8 Debugger protocol | pass | error | diff | -| `protocol/enable-disable.ts` | safe domain lifecycle | API profiler examples | pass | error | diff | -| `protocol/get-script-source.ts` | controlled exact source retrieval | `test-inspector.js` | pass | error | diff | -| `protocol/schema-domains.ts` | supported-domain inventory | V8 Schema protocol | pass | error | diff | -| `runtime/await-promise.ts` | `awaitPromise` resolution | inspector API examples | pass | error | diff | -| `runtime/exception-details.ts` | thrown evaluation result | inspector exception tests | pass | error | diff | -| `runtime/get-properties-release.ts` | properties and object release | `test-inspector-bindings.js` | pass | error | diff | -| `runtime/numeric-specials.ts` | unserializable values and BigInt | V8 Runtime protocol | pass | error | diff | -| `runtime/object-preview.ts` | normalized preview properties | `test-inspector.js` | pass | error | diff | -| `runtime/primitives.ts` | primitive remote-object shapes | inspector API examples | pass | error | diff | -| `runtime/release-object-group.ts` | group release invalidation | V8 Runtime protocol | pass | error | diff | -| `runtime/remote-subtypes.ts` | stable object subtype/class metadata | V8 Runtime protocol | pass | error | diff | -| `runtime/return-by-value.ts` | object/array serialization | V8 Runtime protocol | pass | error | diff | -| `session/callback-runtime.ts` | callback lifecycle smoke | `test-inspector-module.js` | error | timeout | diff | -| `surface/domain-helpers.ts` | Network/DOMStorage/Resources exports | `lib/inspector.js` | error | error | diff | -| `surface/domain-validation.ts` | helper params validation | `test-inspector-emit-protocol-event.js` | pass | error | diff | -| `surface/exports.ts` | exact public inventory/descriptors | `lib/inspector.js` exports | diff | diff | diff | -| `surface/session-class.ts` | EventEmitter/prototype descriptors | API `Session`; `lib/inspector.js` | pass | diff | diff | +| `protocol/debugger-metadata.ts` | normalized Debugger ID result | V8 Debugger protocol | pass | error | pass | +| `protocol/enable-disable.ts` | safe domain lifecycle | API profiler examples | pass | error | pass | +| `protocol/get-script-source.ts` | controlled exact source retrieval | `test-inspector.js` | pass | error | pass | +| `protocol/schema-domains.ts` | supported-domain inventory | V8 Schema protocol | pass | error | pass | +| `runtime/await-promise.ts` | `awaitPromise` resolution | inspector API examples | pass | error | pass | +| `runtime/exception-details.ts` | thrown evaluation result | inspector exception tests | pass | error | pass | +| `runtime/get-properties-release.ts` | properties and object release | `test-inspector-bindings.js` | pass | error | pass | +| `runtime/numeric-specials.ts` | unserializable values and BigInt | V8 Runtime protocol | pass | error | pass | +| `runtime/object-preview.ts` | normalized preview properties | `test-inspector.js` | pass | error | pass | +| `runtime/primitives.ts` | primitive remote-object shapes | inspector API examples | pass | error | pass | +| `runtime/release-object-group.ts` | group release invalidation | V8 Runtime protocol | pass | error | pass | +| `runtime/remote-subtypes.ts` | stable object subtype/class metadata | V8 Runtime protocol | pass | error | pass | +| `runtime/return-by-value.ts` | object/array serialization | V8 Runtime protocol | pass | error | pass | +| `session/callback-runtime.ts` | callback lifecycle smoke | `test-inspector-module.js` | error | timeout | pass | +| `surface/domain-helpers.ts` | Network/DOMStorage/Resources exports | `lib/inspector.js` | error | error | pass | +| `surface/domain-validation.ts` | helper params validation | `test-inspector-emit-protocol-event.js` | pass | error | pass | +| `surface/exports.ts` | exact public inventory/descriptors | `lib/inspector.js` exports | diff | diff | pass | +| `surface/session-class.ts` | EventEmitter/prototype descriptors | API `Session`; `lib/inspector.js` | pass | diff | pass | diff --git a/test-parity/node-suite/inspector/events/console-api.ts b/test-parity/node-suite/inspector/events/console-api.ts index cc1949ee1b..bf99d7875c 100644 --- a/test-parity/node-suite/inspector/events/console-api.ts +++ b/test-parity/node-suite/inspector/events/console-api.ts @@ -23,6 +23,15 @@ try { typeof params.timestamp, Array.isArray(params.stackTrace.callFrames), ); + const types: string[] = []; + for (const method of ["log", "info", "debug", "warn", "error"] as const) { + const next = new Promise((resolve) => + session.once("Runtime.consoleAPICalled", resolve) + ); + inspector.console[method]("marker", 42, true); + types.push((await next).params.type); + } + console.log("types:", types.join(",")); } finally { session.disconnect(); } diff --git a/test-parity/node-suite/inspector/events/listener-lifecycle.ts b/test-parity/node-suite/inspector/events/listener-lifecycle.ts index 5aab045077..32d8ca9e87 100644 --- a/test-parity/node-suite/inspector/events/listener-lifecycle.ts +++ b/test-parity/node-suite/inspector/events/listener-lifecycle.ts @@ -5,7 +5,9 @@ session.connect(); try { let persistent = 0; let once = 0; + let generic = 0; const listener = () => persistent++; + session.on("inspectorNotification", () => generic++); session.on("Runtime.consoleAPICalled", listener); session.once("Runtime.consoleAPICalled", () => once++); await new Promise((resolve) => @@ -15,6 +17,8 @@ try { inspector.console.log(2); await new Promise((resolve) => setImmediate(resolve)); session.off("Runtime.consoleAPICalled", listener); + session.on("Runtime.consoleAPICalled", () => persistent += 100); + session.removeAllListeners("Runtime.consoleAPICalled"); inspector.console.log(3); await new Promise((resolve) => setImmediate(resolve)); console.log( @@ -22,6 +26,8 @@ try { persistent, once, session.listenerCount("Runtime.consoleAPICalled"), + generic, + session.listenerCount("inspectorNotification"), ); } finally { session.removeAllListeners(); diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index 46f28fdb92..833aae253c 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -108,12 +108,12 @@ } }, "inspector": { - "pass": 4, + "pass": 37, "total": 37 }, "inspector-promises": { - "pass": 3, - "total": 30 + "pass": 31, + "total": 31 }, "module": { "pass": 29,