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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.d/7382-node-vm-node26-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Fixed

- **Completed Node.js 26.5.0 parity for `node:vm`.** Contexts now preserve
sandbox, lexical, descriptor, strict-write, code-generation, microtask, and
cross-realm behavior; `Script`, `compileFunction`, cached-data metadata, and
experimental VM modules now match the Node oracle across the full 64-case
module suite.
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ pub(crate) const NODE_CORE_INSPECTOR_VM_ROWS: &[NativeModSig] = &[
has_receiver: false,
method: "createScript",
class_filter: None,
runtime: "js_vm_create_script",
runtime: "js_vm_create_script_branded",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. LLVM declaration for the new symbol.
rg -n 'js_vm_create_script' crates/perry-codegen/src
# 2. Runtime definition and export.
rg -n -C3 'fn js_vm_create_script' crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 1302


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== declarations matching create_script =="
rg -n 'js_vm_create_script|js_vm_create_script_branded|declare_function' crates -g '*.rs'

echo
echo "== inspector_vm native_table excerpt =="
sed -n '280,330p' crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs

echo
echo "== linked runtime exports around native_module =="
rg -n -C6 '#\[no_mangle\]|pub extern "C" fn js_vm_create_script_branded|fn js_vm_create_script|declare_function' crates/perry-runtime/src/object/native_module.rs crates/perry-runtime/src/node_vm.rs

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== exact declaration/query occurrences =="
git -C crates/perry-codegen ls-files src | awk '/runtime_decls/ || /inspector_vm.rs/' | while read -r f; do
  echo "--- $f"
  rg -n 'js_vm_create_script(branded)?' "$f" || true
done

echo
echo "== focused runtime occurrences =="
rg -n -C3 'pub extern "C" fn js_vm_create_script|js_vm_create_script' crates/perry-runtime/src/object/native_module.rs crates/perry-runtime/src/node_vm.rs crates/perry-runtime/ || true

echo
echo "== tracked files containing create_script =="
git ls-files | xargs rg -n 'js_vm_create_script' || true

Repository: PerryTS/perry

Length of output: 8657


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== declaration file locations =="
fd -a '^(runtime_decls|vm)' crates/perry-codegen crates/perry-stdlib crates/perry-runtime 2>/dev/null | head -100

echo
echo "== exact js_vm_create_script occurrences in repository =="
git ls-files | xargs rg -n 'js_vm_create_script(branded)?' || true

echo
echo "== object/native_module.rs around branded wrapper =="
sed -n '90,130p' crates/perry-runtime/src/object/native_module.rs

echo
echo "== low-level native export declarations in inspector_vm.rs =="
sed -n '290,325p' crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs

Repository: PerryTS/perry

Length of output: 4069


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== runtime_declaration / fn_decl type definitions =="
rg -n 'type RuntimeDecl|struct RuntimeDecl|declare_function|fn_decl' crates/perry-codegen/src -g '*.rs' -C 4 | head -220 || true

echo
echo "== runtime_decls top-level declaration definitions =="
sed -n '1,120p' crates/perry-codegen/src/runtime_decls/mod.rs
sed -n '1,120p' crates/perry-codegen/src/runtime_decls/objects.rs

Repository: PerryTS/perry

Length of output: 33469


Add an LLVM declaration for js_vm_create_script_branded.

crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs now emits a native call to js_vm_create_script_branded, the runtime provides #[no_mangle] pub extern "C" fn js_vm_create_script_branded, but no LLVM declaration for it exists yet. Add the matching declaration, e.g. DOUBLE return with [DOUBLE, DOUBLE] params, so lowering has a declared FFI import.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs`
at line 313, Add an LLVM declaration entry alongside the existing native runtime
declarations in inspector_vm.rs for the js_vm_create_script_branded runtime
symbol, matching its external C signature with a DOUBLE return type and two
DOUBLE parameters so the emitted call has a declared FFI import.

args: &[NA_F64, NA_F64],
ret: NR_F64,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,13 @@ pub(super) const NODE_MISC_ROWS: &[NativeModSig] = &[
ret: NR_F64,
},
// ========== node:vm ==========
// Minimal contextification surface for APIs that require a vm context
// object but do not execute code inside it yet.
NativeModSig {
module: "vm",
has_receiver: false,
method: "createContext",
class_filter: None,
runtime: "js_vm_create_context",
args: &[NA_F64],
args: &[NA_F64, NA_F64],
ret: NR_F64,
},
// ========== node:repl ==========
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use crate::types::{DOUBLE, I32, I64, VOID};

pub(crate) fn declare_net_http(module: &mut LlModule) {
// ========== node:vm ==========
module.declare_function("js_vm_create_context", DOUBLE, &[DOUBLE]);
module.declare_function("js_vm_create_context", DOUBLE, &[DOUBLE, DOUBLE]);
module.declare_function("js_vm_create_script_branded", DOUBLE, &[DOUBLE, DOUBLE]);
module.declare_function("js_vm_module_call", DOUBLE, &[]);
module.declare_function("js_vm_module_constructor_error", DOUBLE, &[]);

Expand Down
41 changes: 10 additions & 31 deletions crates/perry-hir/src/lower/lower_expr/arm_bin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,37 +246,16 @@ pub(crate) fn lower_bin_expr(ctx: &mut LoweringContext, bin: &ast::BinExpr) -> R
}),

// Comparison (treat == same as === for typed code)
ast::BinaryOp::EqEq => {
// Proxy/Reflect fold: `Reflect.getPrototypeOf(x) === <Class>.prototype`
// always true in our model (we don't maintain real prototypes).
// Same fold for `Object.getPrototypeOf(x) === <Class>.prototype`.
if matches!(
&*left,
Expr::ReflectGetPrototypeOf(_) | Expr::ObjectGetPrototypeOf(_)
) && matches!(&*right, Expr::PropertyGet { property, .. } if property == "prototype")
{
return Ok(Expr::Bool(true));
}
Ok(Expr::Compare {
op: CompareOp::LooseEq,
left,
right,
})
}
ast::BinaryOp::EqEqEq => {
if matches!(
&*left,
Expr::ReflectGetPrototypeOf(_) | Expr::ObjectGetPrototypeOf(_)
) && matches!(&*right, Expr::PropertyGet { property, .. } if property == "prototype")
{
return Ok(Expr::Bool(true));
}
Ok(Expr::Compare {
op: CompareOp::Eq,
left,
right,
})
}
ast::BinaryOp::EqEq => Ok(Expr::Compare {
op: CompareOp::LooseEq,
left,
right,
}),
ast::BinaryOp::EqEqEq => Ok(Expr::Compare {
op: CompareOp::Eq,
left,
right,
}),
ast::BinaryOp::NotEq => Ok(Expr::Compare {
op: CompareOp::LooseNe,
left,
Expand Down
93 changes: 87 additions & 6 deletions crates/perry-runtime/src/dyn_eval/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,26 @@ pub(crate) fn throw_syntax_error(message: &str) -> ! {
throw_error_kind(crate::error::ERROR_KIND_SYNTAX_ERROR, message)
}

pub(crate) fn throw_eval_error(message: &str) -> ! {
throw_error_kind(crate::error::ERROR_KIND_EVAL_ERROR, message)
}

fn wasm_codegen_error(api: &str) -> f64 {
let message = format!("{api}(): Wasm code generation disallowed by embedder");
let message = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32);
let error = crate::error::js_error_new_with_name_message_bytes(b"CompileError", message);
crate::value::js_nanbox_pointer(error as i64)
}

pub(crate) fn wasm_codegen_rejection(api: &str) -> f64 {
let promise = crate::promise::js_promise_rejected(wasm_codegen_error(api));
crate::value::js_nanbox_pointer(promise as i64)
}

pub(crate) fn throw_wasm_codegen_error(api: &str) -> ! {
crate::exception::js_throw(wasm_codegen_error(api))
}

/// The diagnostic contract of #6559: anything outside the interpreter subset
/// throws a TypeError that NAMES the construct, so gaps met in the wild show
/// up as actionable errors, never as silent miscomputation.
Expand Down Expand Up @@ -214,15 +234,15 @@ pub(crate) fn get_index(base: f64, key: f64) -> f64 {
}

/// `base[key] = value` (also used for `base.name = value` with a string key).
pub(crate) fn set_index(base: f64, key: f64, value: f64) {
crate::value::js_dyn_index_set(base, key, value);
pub(crate) fn set_index(base: f64, key: f64, value: f64, strict: bool) {
crate::proxy::js_put_value_set(base, key, value, base, strict as i32);
}

pub(crate) fn set_member(base: f64, name: &str, value: f64) {
let base_idx = root_push(base);
let value_idx = root_push(value);
let key = make_string(name);
set_index(root_get(base_idx), key, root_get(value_idx));
set_index(root_get(base_idx), key, root_get(value_idx), false);
roots_truncate(base_idx);
}

Expand Down Expand Up @@ -272,9 +292,70 @@ pub(crate) fn construct(callee: f64, args: &[f64]) -> f64 {

// ── globals ────────────────────────────────────────────────────────────────

/// Look `name` up on the real `globalThis` (Math, JSON, Array, isNaN, …).
pub(crate) fn global_lookup(name: &str) -> f64 {
crate::object::js_get_global_this_builtin_value(name.as_ptr(), name.len())
/// Whether the selected global (including its prototype chain) binds `name`.
pub(crate) fn global_has_property(global: f64, name: &str) -> bool {
let global = if crate::proxy::js_proxy_is_proxy(global) != 0 {
crate::proxy::js_proxy_target(global)
} else {
global
};
let global_idx = root_push(global);
let key = make_string(name);
let present = crate::object::js_object_has_property(root_get(global_idx), key);
roots_truncate(global_idx);
truthy(present)
}

/// Look `name` up on the selected global receiver, then on the selected realm's
/// intrinsic-global backing. VM contexts pass distinct values; ordinary
/// dynamic Function calls pass the process global for both.
pub(crate) fn global_lookup(global_this: f64, intrinsics: f64, name: &str) -> f64 {
let lookup_target = if crate::proxy::js_proxy_is_proxy(global_this) != 0 {
crate::proxy::js_proxy_target(global_this)
} else {
global_this
};
if global_has_property(lookup_target, name) {
return get_member(lookup_target, name);
}
let builtin = crate::object::GLOBAL_THIS_BUILTIN_CONSTRUCTORS.contains(&name)
|| crate::object::GLOBAL_THIS_BUILTIN_NAMESPACES.contains(&name)
|| crate::object::GLOBAL_THIS_BUILTIN_FUNCTIONS.contains(&name);
if builtin {
get_member(intrinsics, name)
} else {
undefined()
}
}

/// The selected realm's `<Constructor>.prototype` value.
pub(crate) fn intrinsic_prototype(intrinsics: f64, name: &str) -> f64 {
let intrinsics_idx = root_push(intrinsics);
let constructor = get_member(root_get(intrinsics_idx), name);
let constructor_idx = root_push(constructor);
let prototype = get_member(root_get(constructor_idx), "prototype");
roots_truncate(intrinsics_idx);
prototype
}

/// Link a freshly-created value to the actual prototype of its creation realm.
/// The value is returned through a handle because creating object metadata may
/// collect and move it.
pub(crate) fn attach_intrinsic_prototype(value: f64, intrinsics: f64, name: &str) -> f64 {
let scope = crate::gc::RuntimeHandleScope::new();
let value_handle = scope.root_nanbox_f64(value);
let prototype = intrinsic_prototype(intrinsics, name);
let prototype_handle = scope.root_nanbox_f64(prototype);
let prototype = prototype_handle.get_nanbox_f64();
if !crate::value::JSValue::from_bits(prototype.to_bits()).is_pointer() {
return value_handle.get_nanbox_f64();
}
let value = value_handle.get_nanbox_f64();
let raw = crate::value::js_nanbox_get_pointer(value) as usize;
if raw != 0 {
crate::object::prototype_chain::object_set_static_prototype(raw, prototype.to_bits());
}
value_handle.get_nanbox_f64()
}

// ── operators ──────────────────────────────────────────────────────────────
Expand Down
Loading