diff --git a/changelog.d/7382-node-vm-node26-parity.md b/changelog.d/7382-node-vm-node26-parity.md new file mode 100644 index 0000000000..8b33216955 --- /dev/null +++ b/changelog.d/7382-node-vm-node26-parity.md @@ -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. 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 6939bb9342..fe742081dd 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 @@ -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", args: &[NA_F64, NA_F64], ret: NR_F64, }, diff --git a/crates/perry-codegen/src/lower_call/native_table/node_misc.rs b/crates/perry-codegen/src/lower_call/native_table/node_misc.rs index 7a8cfd0fda..dfdfb02be5 100644 --- a/crates/perry-codegen/src/lower_call/native_table/node_misc.rs +++ b/crates/perry-codegen/src/lower_call/native_table/node_misc.rs @@ -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 ========== diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs index b522d44a79..8e1851da0a 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs @@ -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, &[]); 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 55fe9ca776..a4e7cb24ff 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs @@ -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) === .prototype` - // always true in our model (we don't maintain real prototypes). - // Same fold for `Object.getPrototypeOf(x) === .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, diff --git a/crates/perry-runtime/src/dyn_eval/bridge.rs b/crates/perry-runtime/src/dyn_eval/bridge.rs index 80dd56855d..9a198e842e 100644 --- a/crates/perry-runtime/src/dyn_eval/bridge.rs +++ b/crates/perry-runtime/src/dyn_eval/bridge.rs @@ -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. @@ -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); } @@ -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 `.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 ────────────────────────────────────────────────────────────── diff --git a/crates/perry-runtime/src/dyn_eval/env.rs b/crates/perry-runtime/src/dyn_eval/env.rs index 7c857f6929..85fc8fd17f 100644 --- a/crates/perry-runtime/src/dyn_eval/env.rs +++ b/crates/perry-runtime/src/dyn_eval/env.rs @@ -17,6 +17,9 @@ use super::{root_get, root_push, root_set, roots_truncate}; /// shadow or collide with it (interpreted code only reaches environments via /// identifier resolution, never via computed access). const PARENT_KEY: &str = "perry dyn parent"; +/// Optional ObjectEnvironmentRecord bindings for this scope. The space keeps +/// the slot unreachable through interpreted identifiers, like `PARENT_KEY`. +const OBJECT_BINDINGS_KEY: &str = "perry dyn object bindings"; thread_local! { /// identifier name → its cached `StringHeader`. Every scope-chain read / @@ -97,6 +100,49 @@ pub(crate) fn env_new(parent: f64) -> f64 { env } +/// Allocate an object-environment scope. Identifier reads and writes delegate +/// to `bindings` while lexical declarations continue to live on the wrapper. +/// This is the shared seam used by VM globals and compileFunction context +/// extensions; the bindings object remains live and is never copied/mutated +/// with interpreter bookkeeping. +pub(crate) fn env_new_object(parent: Option, bindings: f64) -> f64 { + let bindings_idx = root_push(bindings); + let env = match parent { + Some(parent) => env_new(parent), + None => env_new_root(), + }; + let env_idx = root_push(env); + let key = key_string(OBJECT_BINDINGS_KEY); + crate::object::js_object_set_field_by_name( + env_object_ptr(root_get(env_idx)), + key, + root_get(bindings_idx), + ); + let env = root_get(env_idx); + roots_truncate(bindings_idx); + env +} + +fn env_object_bindings(env: f64) -> Option { + let value = env_read(env, OBJECT_BINDINGS_KEY); + (!crate::value::JSValue::from_bits(value.to_bits()).is_undefined()).then_some(value) +} + +fn object_has_binding(bindings: f64, name: &str) -> bool { + let key = crate::value::js_nanbox_string(key_string(name) as i64); + crate::value::js_is_truthy(crate::object::js_object_has_property(bindings, key)) != 0 +} + +fn object_read_binding(bindings: f64, name: &str) -> f64 { + let key = crate::value::js_nanbox_string(key_string(name) as i64); + crate::proxy::js_reflect_get(bindings, key, bindings) +} + +fn object_write_binding(bindings: f64, name: &str, value: f64, strict: bool) { + let key = crate::value::js_nanbox_string(key_string(name) as i64); + crate::proxy::js_put_value_set(bindings, key, value, bindings, strict as i32); +} + fn env_parent(env: f64) -> Option { let env_idx = root_push(env); let key = key_string(PARENT_KEY); @@ -120,6 +166,42 @@ fn env_has_own(env: f64, name: &str) -> bool { crate::value::js_is_truthy(has) != 0 } +pub(crate) fn variable_environment(env: f64) -> f64 { + let cur_idx = root_push(env); + loop { + if env_object_bindings(root_get(cur_idx)).is_some() { + let result = root_get(cur_idx); + roots_truncate(cur_idx); + return result; + } + match env_parent(root_get(cur_idx)) { + Some(parent) => root_set(cur_idx, parent), + None => { + let result = root_get(cur_idx); + roots_truncate(cur_idx); + return result; + } + } + } +} + +pub(crate) fn ensure_var_binding(env: f64, name: &str) { + let env_idx = root_push(env); + if let Some(bindings) = env_object_bindings(root_get(env_idx)) { + if !object_has_binding(bindings, name) { + object_write_binding( + env_object_bindings(root_get(env_idx)).unwrap(), + name, + super::bridge::undefined(), + false, + ); + } + } else if !env_has_own(root_get(env_idx), name) { + define(root_get(env_idx), name, super::bridge::undefined()); + } + roots_truncate(env_idx); +} + fn env_read(env: f64, name: &str) -> f64 { let env_idx = root_push(env); let key = key_string(name); @@ -230,7 +312,7 @@ pub(crate) fn lookup(env: f64, name: &str) -> Option { std::ptr::null() }; loop { - if fast { + if fast && env_object_bindings(root_get(cur_idx)).is_none() { match scope_probe(root_get(cur_idx), key) { ScopeProbe::Hit(v) => { roots_truncate(cur_idx); @@ -261,6 +343,13 @@ pub(crate) fn lookup(env: f64, name: &str) -> Option { roots_truncate(cur_idx); return Some(value); } + let has_object_binding = env_object_bindings(root_get(cur_idx)) + .is_some_and(|bindings| object_has_binding(bindings, name)); + if has_object_binding { + let value = object_read_binding(env_object_bindings(root_get(cur_idx)).unwrap(), name); + roots_truncate(cur_idx); + return Some(value); + } match env_parent(root_get(cur_idx)) { Some(p) => root_set(cur_idx, p), None => { @@ -281,7 +370,7 @@ pub(crate) fn is_bound(env: f64, name: &str) -> bool { std::ptr::null() }; loop { - let present = if fast { + let present = if fast && env_object_bindings(root_get(cur_idx)).is_none() { match scope_probe(root_get(cur_idx), key) { ScopeProbe::Hit(_) => Some(true), ScopeProbe::Absent => Some(false), @@ -295,6 +384,12 @@ pub(crate) fn is_bound(env: f64, name: &str) -> bool { roots_truncate(cur_idx); return true; } + if let Some(bindings) = env_object_bindings(root_get(cur_idx)) { + if object_has_binding(bindings, name) { + roots_truncate(cur_idx); + return true; + } + } match env_parent(root_get(cur_idx)) { Some(p) => root_set(cur_idx, p), None => { @@ -310,7 +405,7 @@ pub(crate) fn is_bound(env: f64, name: &str) -> bool { /// generated matcher relies on (`value = derivedConstraints.version` with /// `value` never declared) — creates the binding on the chain's ROOT scope /// (the Function instance's private "global"). -pub(crate) fn assign(env: f64, name: &str, value: f64) { +pub(crate) fn assign(env: f64, name: &str, value: f64, strict: bool) { let fast = super::fast_scope_enabled(); let value_idx = root_push(value); let cur_idx = root_push(env); @@ -320,7 +415,7 @@ pub(crate) fn assign(env: f64, name: &str, value: f64) { std::ptr::null() }; loop { - let present = if fast { + let present = if fast && env_object_bindings(root_get(cur_idx)).is_none() { match scope_probe(root_get(cur_idx), key) { ScopeProbe::Hit(_) => Some(true), ScopeProbe::Absent => Some(false), @@ -335,10 +430,30 @@ pub(crate) fn assign(env: f64, name: &str, value: f64) { roots_truncate(value_idx); return; } + let has_object_binding = env_object_bindings(root_get(cur_idx)) + .is_some_and(|bindings| object_has_binding(bindings, name)); + if has_object_binding { + object_write_binding( + env_object_bindings(root_get(cur_idx)).unwrap(), + name, + root_get(value_idx), + strict, + ); + roots_truncate(value_idx); + return; + } match env_parent(root_get(cur_idx)) { Some(p) => root_set(cur_idx, p), None => { - env_write(root_get(cur_idx), name, root_get(value_idx)); + if strict { + roots_truncate(value_idx); + super::bridge::throw_reference_error(&format!("{name} is not defined")); + } + if let Some(bindings) = env_object_bindings(root_get(cur_idx)) { + object_write_binding(bindings, name, root_get(value_idx), strict); + } else { + env_write(root_get(cur_idx), name, root_get(value_idx)); + } roots_truncate(value_idx); return; } diff --git a/crates/perry-runtime/src/dyn_eval/expr.rs b/crates/perry-runtime/src/dyn_eval/expr.rs index 039fd4748b..ed177008b6 100644 --- a/crates/perry-runtime/src/dyn_eval/expr.rs +++ b/crates/perry-runtime/src/dyn_eval/expr.rs @@ -112,27 +112,28 @@ fn eval_ident(ctx: &Ctx, name: &str, env_idx: usize) -> f64 { "undefined" => return bridge::undefined(), "NaN" => return bridge::make_number(f64::NAN), "Infinity" => return bridge::make_number(f64::INFINITY), - "globalThis" => return crate::object::js_get_global_this(), - "arguments" => throw_unsupported("the arguments object"), + "globalThis" => return root_get(ctx.global_idx), _ => {} } - let global = bridge::global_lookup(name); + let global = + bridge::global_lookup(root_get(ctx.global_idx), root_get(ctx.intrinsics_idx), name); if !bridge::is_undefined(global) { return global; } - if global_has_own(name) { + if global_has_own(ctx, name) { return global; } bridge::throw_reference_error(&format!("{name} is not defined")) } -fn global_has_own(name: &str) -> bool { - let g = crate::object::js_get_global_this(); - let g_idx = root_push(g); - let key = bridge::make_string(name); - let has = crate::object::js_object_has_own(root_get(g_idx), key); - roots_truncate(g_idx); - bridge::truthy(has) +fn global_has_own(ctx: &Ctx, name: &str) -> bool { + let global = root_get(ctx.global_idx); + bridge::global_has_property(global, name) + || !bridge::is_undefined(bridge::global_lookup( + global, + root_get(ctx.intrinsics_idx), + name, + )) } // ── literals ─────────────────────────────────────────────────────────────── @@ -201,13 +202,22 @@ fn eval_array_lit(ctx: &Ctx, a: &ast::ArrayLit, env_idx: usize) -> f64 { } } } - let arr = root_get(arr_idx); + let arr = bridge::attach_intrinsic_prototype( + root_get(arr_idx), + root_get(ctx.intrinsics_idx), + "Array", + ); roots_truncate(arr_idx); arr } fn eval_object_lit(ctx: &Ctx, o: &ast::ObjectLit, env_idx: usize) -> f64 { - let obj_idx = root_push(bridge::object_new()); + let object = bridge::attach_intrinsic_prototype( + bridge::object_new(), + root_get(ctx.intrinsics_idx), + "Object", + ); + let obj_idx = root_push(object); for prop in &o.props { match prop { ast::PropOrSpread::Spread(s) => { @@ -226,7 +236,12 @@ fn eval_object_lit(ctx: &Ctx, o: &ast::ObjectLit, env_idx: usize) -> f64 { let key_idx = root_push(key); let value = bridge::get_index(root_get(src_idx), root_get(key_idx)); let value_idx = root_push(value); - bridge::set_index(root_get(obj_idx), root_get(key_idx), root_get(value_idx)); + bridge::set_index( + root_get(obj_idx), + root_get(key_idx), + root_get(value_idx), + false, + ); roots_truncate(key_idx); } roots_truncate(src_idx); @@ -288,12 +303,17 @@ fn set_prop_by_name(ctx: &Ctx, obj_idx: usize, key: &ast::PropName, value: f64, ), ast::PropName::Num(n) => { let k = bridge::make_number(n.value); - bridge::set_index(root_get(obj_idx), k, root_get(value_idx)); + bridge::set_index(root_get(obj_idx), k, root_get(value_idx), false); } ast::PropName::Computed(c) => { let k = eval_expr(ctx, &c.expr, env_idx); let k_idx = root_push(k); - bridge::set_index(root_get(obj_idx), root_get(k_idx), root_get(value_idx)); + bridge::set_index( + root_get(obj_idx), + root_get(k_idx), + root_get(value_idx), + false, + ); roots_truncate(k_idx); } ast::PropName::BigInt(_) => throw_unsupported("bigint property key"), @@ -309,7 +329,7 @@ fn eval_unary(ctx: &Ctx, u: &ast::UnaryExpr, env_idx: usize) -> f64 { TypeOf => { // `typeof missingIdent` must not throw. if let ast::Expr::Ident(i) = u.arg.as_ref() { - if !env::is_bound(root_get(env_idx), &i.sym) && !global_has_own(&i.sym) { + if !env::is_bound(root_get(env_idx), &i.sym) && !global_has_own(ctx, &i.sym) { let special = matches!(&*i.sym, "undefined" | "NaN" | "Infinity" | "globalThis"); if !special { @@ -588,7 +608,7 @@ fn assign_to_assign_target(ctx: &Ctx, target: &ast::AssignTarget, value: f64, en match target { ast::AssignTarget::Simple(simple) => match simple { ast::SimpleAssignTarget::Ident(b) => { - env::assign(root_get(env_idx), &b.id.sym, value); + env::assign(root_get(env_idx), &b.id.sym, value, ctx.strict); } ast::SimpleAssignTarget::Member(m) => assign_to_member(ctx, m, value, env_idx), ast::SimpleAssignTarget::Paren(p) => { @@ -611,7 +631,12 @@ fn assign_to_assign_target(ctx: &Ctx, target: &ast::AssignTarget, value: f64, en fn assign_to_member(ctx: &Ctx, m: &ast::MemberExpr, value: f64, env_idx: usize) { let value_idx = root_push(value); let (obj_idx, key_idx) = eval_member_parts(ctx, m, env_idx); - bridge::set_index(root_get(obj_idx), root_get(key_idx), root_get(value_idx)); + bridge::set_index( + root_get(obj_idx), + root_get(key_idx), + root_get(value_idx), + ctx.strict, + ); roots_truncate(value_idx); } @@ -619,7 +644,7 @@ fn assign_to_member(ctx: &Ctx, m: &ast::MemberExpr, value: f64, env_idx: usize) /// destructuring targets, parenthesized targets). pub(crate) fn assign_to_target_expr(ctx: &Ctx, e: &ast::Expr, value: f64, env_idx: usize) { match e { - ast::Expr::Ident(i) => env::assign(root_get(env_idx), &i.sym, value), + ast::Expr::Ident(i) => env::assign(root_get(env_idx), &i.sym, value, ctx.strict), ast::Expr::Member(m) => assign_to_member(ctx, m, value, env_idx), ast::Expr::Paren(p) => assign_to_target_expr(ctx, &p.expr, value, env_idx), _ => throw_unsupported("assignment to this expression form"), @@ -660,12 +685,100 @@ fn call_with_args(callee: f64, this: f64, args: &[f64]) -> f64 { bridge::call_function(callee, this, args) } +fn is_string_codegen_callee(ctx: &Ctx, callee: f64) -> bool { + let scope = crate::gc::RuntimeHandleScope::new(); + let callee = scope.root_nanbox_f64(callee); + ["eval", "Function"].into_iter().any(|name| { + bridge::get_member(root_get(ctx.intrinsics_idx), name).to_bits() + == callee.get_nanbox_f64().to_bits() + }) +} + +fn is_wasm_namespace(ctx: &Ctx, value: f64) -> bool { + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(value); + bridge::get_member(root_get(ctx.intrinsics_idx), "WebAssembly").to_bits() + == value.get_nanbox_f64().to_bits() +} + +fn is_wasm_module_constructor(ctx: &Ctx, callee: f64) -> bool { + let scope = crate::gc::RuntimeHandleScope::new(); + let callee = scope.root_nanbox_f64(callee); + let namespace = scope.root_nanbox_f64(bridge::get_member( + root_get(ctx.intrinsics_idx), + "WebAssembly", + )); + bridge::get_member(namespace.get_nanbox_f64(), "Module").to_bits() + == callee.get_nanbox_f64().to_bits() +} + +fn attach_realm_static_result(ctx: &Ctx, receiver: f64, method: &str, result: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + let result = scope.root_nanbox_f64(result); + let promise_ctor = bridge::get_member(root_get(ctx.intrinsics_idx), "Promise"); + if receiver.get_nanbox_f64().to_bits() == promise_ctor.to_bits() + && crate::promise::js_value_is_promise(result.get_nanbox_f64()) != 0 + { + return bridge::attach_intrinsic_prototype( + result.get_nanbox_f64(), + root_get(ctx.intrinsics_idx), + "Promise", + ); + } + let object_ctor = bridge::get_member(root_get(ctx.intrinsics_idx), "Object"); + if receiver.get_nanbox_f64().to_bits() == object_ctor.to_bits() + && method == "getOwnPropertyDescriptor" + && !bridge::is_undefined(result.get_nanbox_f64()) + { + return bridge::attach_intrinsic_prototype( + result.get_nanbox_f64(), + root_get(ctx.intrinsics_idx), + "Object", + ); + } + result.get_nanbox_f64() +} + fn eval_call(ctx: &Ctx, c: &ast::CallExpr, env_idx: usize) -> f64 { let callee = match &c.callee { ast::Callee::Expr(e) => e, ast::Callee::Super(_) => throw_unsupported("super(…) call"), ast::Callee::Import(_) => throw_unsupported("dynamic import in interpreted code"), }; + if let ast::Expr::Ident(ident) = callee.as_ref() { + if ident.sym.as_ref() == "eval" { + let resolved = eval_ident(ctx, "eval", env_idx); + let resolved_idx = root_push(resolved); + let intrinsic_eval = bridge::get_member(root_get(ctx.intrinsics_idx), "eval"); + if root_get(resolved_idx).to_bits() != intrinsic_eval.to_bits() { + roots_truncate(resolved_idx); + } else { + let args = eval_args(ctx, &c.args, env_idx); + let source = args.first().copied().unwrap_or_else(bridge::undefined); + let Some(source) = bridge::read_string(source) else { + roots_truncate(resolved_idx); + return source; + }; + if !ctx.strings_allowed { + bridge::throw_eval_error( + "Code generation from strings disallowed for this context", + ); + } + let result = super::eval_direct_in( + &source, + root_get(ctx.global_idx), + root_get(ctx.intrinsics_idx), + root_get(env_idx), + root_get(ctx.variable_env_idx), + ctx.strings_allowed, + ctx.wasm_allowed, + ); + roots_truncate(resolved_idx); + return result; + } + } + } match callee.as_ref() { // `obj.m(args)` / `obj[k](args)`: route through the runtime's method // dispatch so builtin prototypes work and `this` binds to the @@ -686,17 +799,87 @@ fn eval_call(ctx: &Ctx, c: &ast::CallExpr, env_idx: usize) -> f64 { } )); } + let callee_idx = (!ctx.strings_allowed + && (crate::object::js_value_is_heap_object(root_get(obj_idx)) + || (root_get(obj_idx).to_bits() >> 48) == 0x7FFE)) + .then(|| root_push(bridge::get_member(root_get(obj_idx), &name))); + let codegen_blocked = !ctx.strings_allowed + && ((name == "constructor" + && crate::object::value_is_callable(root_get(obj_idx))) + || callee_idx + .is_some_and(|idx| is_string_codegen_callee(ctx, root_get(idx)))); + let wasm_codegen_blocked = !ctx.wasm_allowed + && matches!( + name.as_str(), + "compile" | "compileStreaming" | "instantiate" | "instantiateStreaming" + ) + && is_wasm_namespace(ctx, root_get(obj_idx)); let args = eval_args(ctx, &c.args, env_idx); - let result = bridge::call_method(root_get(obj_idx), &name, &args); + if codegen_blocked { + bridge::throw_eval_error( + "Code generation from strings disallowed for this context", + ); + } + if wasm_codegen_blocked + && !(name == "instantiate" + && args.first().is_some_and(|value| { + crate::object::is_registered_wasm_module(*value) + })) + { + let result = bridge::wasm_codegen_rejection(&format!("WebAssembly.{name}")); + roots_truncate(obj_idx); + return result; + } + let receiver = root_get(obj_idx); + let result = callee_idx.map_or_else( + || bridge::call_method(receiver, &name, &args), + |idx| call_with_args(root_get(idx), receiver, &args), + ); + let result = attach_realm_static_result(ctx, receiver, &name, result); roots_truncate(obj_idx); result } ast::MemberProp::Computed(comp) => { let key = eval_expr(ctx, &comp.expr, env_idx); let key_idx = root_push(key); + let callee_idx = (!ctx.strings_allowed + && (crate::object::js_value_is_heap_object(root_get(obj_idx)) + || (root_get(obj_idx).to_bits() >> 48) == 0x7FFE)) + .then(|| { + root_push(bridge::get_index(root_get(obj_idx), root_get(key_idx))) + }); + let codegen_blocked = + callee_idx.is_some_and(|idx| is_string_codegen_callee(ctx, root_get(idx))); + let method = bridge::read_string(root_get(key_idx)).unwrap_or_default(); + let wasm_codegen_blocked = !ctx.wasm_allowed + && matches!( + method.as_str(), + "compile" | "compileStreaming" | "instantiate" | "instantiateStreaming" + ) + && is_wasm_namespace(ctx, root_get(obj_idx)); let args = eval_args(ctx, &c.args, env_idx); - let result = - bridge::call_method_value(root_get(obj_idx), root_get(key_idx), &args); + if codegen_blocked { + bridge::throw_eval_error( + "Code generation from strings disallowed for this context", + ); + } + if wasm_codegen_blocked + && !(method == "instantiate" + && args.first().is_some_and(|value| { + crate::object::is_registered_wasm_module(*value) + })) + { + let result = + bridge::wasm_codegen_rejection(&format!("WebAssembly.{method}")); + roots_truncate(obj_idx); + return result; + } + let receiver = root_get(obj_idx); + let result = callee_idx.map_or_else( + || bridge::call_method_value(receiver, root_get(key_idx), &args), + |idx| call_with_args(root_get(idx), receiver, &args), + ); + let result = attach_realm_static_result(ctx, receiver, &method, result); roots_truncate(obj_idx); result } @@ -707,7 +890,14 @@ fn eval_call(ctx: &Ctx, c: &ast::CallExpr, env_idx: usize) -> f64 { other => { let callee_value = eval_call_callee(ctx, other, env_idx); let callee_idx = root_push(callee_value); + let codegen_blocked = + !ctx.strings_allowed && is_string_codegen_callee(ctx, root_get(callee_idx)); let args = eval_args(ctx, &c.args, env_idx); + if codegen_blocked { + bridge::throw_eval_error( + "Code generation from strings disallowed for this context", + ); + } let result = call_with_args(root_get(callee_idx), bridge::undefined(), &args); roots_truncate(callee_idx); result @@ -724,7 +914,8 @@ fn eval_call_callee(ctx: &Ctx, e: &ast::Expr, env_idx: usize) -> f64 { if let Some(v) = env::lookup(root_get(env_idx), name) { return v; } - let global = bridge::global_lookup(name); + let global = + bridge::global_lookup(root_get(ctx.global_idx), root_get(ctx.intrinsics_idx), name); if !bridge::is_undefined(global) { return global; } @@ -779,7 +970,12 @@ fn eval_new(ctx: &Ctx, n: &ast::NewExpr, env_idx: usize) -> f64 { let err = crate::error::js_error_new_kind_with_options(kind, msg, bridge::undefined()); roots_truncate(msg_idx); - return crate::value::js_nanbox_pointer(err as i64); + let value = crate::value::js_nanbox_pointer(err as i64); + return bridge::attach_intrinsic_prototype( + value, + root_get(ctx.intrinsics_idx), + name, + ); } if name == "RegExp" { let args = eval_args(ctx, args_ast, env_idx); @@ -802,8 +998,38 @@ fn eval_new(ctx: &Ctx, n: &ast::NewExpr, env_idx: usize) -> f64 { // class the generated code was handed. let callee = eval_expr(ctx, &n.callee, env_idx); let callee_idx = root_push(callee); + let codegen_blocked = + !ctx.strings_allowed && is_string_codegen_callee(ctx, root_get(callee_idx)); + let wasm_codegen_blocked = + !ctx.wasm_allowed && is_wasm_module_constructor(ctx, root_get(callee_idx)); let args = eval_args(ctx, args_ast, env_idx); + if codegen_blocked { + bridge::throw_eval_error("Code generation from strings disallowed for this context"); + } + if wasm_codegen_blocked { + bridge::throw_wasm_codegen_error("WebAssembly.Module"); + } let result = bridge::construct(root_get(callee_idx), &args); + let result_idx = root_push(result); + let result = [ + "Object", + "Array", + "Error", + "TypeError", + "RangeError", + "SyntaxError", + "ReferenceError", + "Promise", + ] + .into_iter() + .find(|name| { + bridge::get_member(root_get(ctx.intrinsics_idx), name).to_bits() + == root_get(callee_idx).to_bits() + }) + .map(|name| { + bridge::attach_intrinsic_prototype(root_get(result_idx), root_get(ctx.intrinsics_idx), name) + }) + .unwrap_or_else(|| root_get(result_idx)); roots_truncate(callee_idx); result } diff --git a/crates/perry-runtime/src/dyn_eval/interp.rs b/crates/perry-runtime/src/dyn_eval/interp.rs index f824523086..0bc17dc5ce 100644 --- a/crates/perry-runtime/src/dyn_eval/interp.rs +++ b/crates/perry-runtime/src/dyn_eval/interp.rs @@ -28,6 +28,15 @@ pub(crate) struct Ctx { pub this_idx: usize, /// Root index the frame's return value is written to. pub ret_idx: usize, + /// Root index of the global object used for identifier/globalThis lookup. + pub global_idx: usize, + /// Root index of the realm global that owns intrinsic constructors. + pub intrinsics_idx: usize, + pub variable_env_idx: usize, + /// Current ECMAScript strict-mode state. + pub strict: bool, + pub strings_allowed: bool, + pub wasm_allowed: bool, } /// Statement completion. Thrown exceptions never appear here — they longjmp @@ -61,18 +70,36 @@ fn fn_id_for_node(addr: usize, build: impl FnOnce() -> InterpFn) -> u32 { // ── construction ─────────────────────────────────────────────────────────── -pub(crate) fn build_interp_fn(params: Vec, body: InterpBody) -> InterpFn { +pub(crate) fn build_interp_fn( + params: Vec, + body: InterpBody, + inherited_strict: bool, +) -> InterpFn { let mut hoisted_vars = Vec::new(); if let InterpBody::Block(stmts) = &body { collect_var_names(stmts, &mut hoisted_vars); } + let strict = inherited_strict + || matches!(&body, InterpBody::Block(stmts) if has_use_strict_directive(stmts)); InterpFn { params, body, hoisted_vars, + strict, } } +pub(crate) fn has_use_strict_directive(stmts: &[ast::Stmt]) -> bool { + stmts + .iter() + .take_while(|stmt| { + matches!(stmt, ast::Stmt::Expr(expr) if matches!(expr.expr.as_ref(), ast::Expr::Lit(ast::Lit::Str(_)))) + }) + .any(|stmt| { + matches!(stmt, ast::Stmt::Expr(expr) if matches!(expr.expr.as_ref(), ast::Expr::Lit(ast::Lit::Str(value)) if value.value.to_string_lossy() == "use strict")) + }) +} + /// Eager construction-time rejection of constructs that can never run. /// Deep rejection stays lazy (interpretation-time) so this scan does not need /// a full AST visitor; the wrapper-level checks here are the ones @@ -200,12 +227,24 @@ const NO_LEXICAL_THIS: u64 = crate::value::TAG_HOLE; /// Allocate the first-class runtime closure for an interpreted function. /// Captures: [0] fn id (number), [1] defining environment (traced pointer), -/// [2] lexical `this` for arrows (or the hole sentinel). -pub(crate) fn alloc_interp_closure(fn_id: u32, def_env: f64, lexical_this: Option) -> f64 { +/// [2] lexical `this` for arrows (or the hole sentinel), [3] target global, +/// [4] intrinsic-global fallback, [5] string code generation policy, +/// [6] WebAssembly code generation policy. +pub(crate) fn alloc_interp_closure( + fn_id: u32, + def_env: f64, + lexical_this: Option, + global: f64, + intrinsics: f64, + strings_allowed: bool, + wasm_allowed: bool, +) -> f64 { ensure_thunk_registered(); let env_idx = root_push(def_env); let this_idx = root_push(lexical_this.unwrap_or(f64::from_bits(NO_LEXICAL_THIS))); - let closure = crate::closure::js_closure_alloc(interp_thunk as *const u8, 3); + let global_idx = root_push(global); + let intrinsics_idx = root_push(intrinsics); + let closure = crate::closure::js_closure_alloc(interp_thunk as *const u8, 7); if closure.is_null() { roots_truncate(env_idx); bridge::throw_range_error("out of memory allocating dynamic function"); @@ -213,68 +252,83 @@ pub(crate) fn alloc_interp_closure(fn_id: u32, def_env: f64, lexical_this: Optio crate::closure::js_closure_set_capture_f64(closure, 0, fn_id as f64); crate::closure::js_closure_set_capture_bits(closure, 1, root_get(env_idx).to_bits()); crate::closure::js_closure_set_capture_bits(closure, 2, root_get(this_idx).to_bits()); + crate::closure::js_closure_set_capture_bits(closure, 3, root_get(global_idx).to_bits()); + crate::closure::js_closure_set_capture_bits(closure, 4, root_get(intrinsics_idx).to_bits()); + crate::closure::js_closure_set_capture_f64(closure, 5, strings_allowed as u8 as f64); + crate::closure::js_closure_set_capture_f64(closure, 6, wasm_allowed as u8 as f64); // Record the capture layout + fire write barriers so the GC traces the // environment / lexical-this slots (they are heap pointers). unsafe { - crate::closure::rebuild_closure_layout_and_barriers(closure, 3); + crate::closure::rebuild_closure_layout_and_barriers(closure, 7); } roots_truncate(env_idx); crate::value::js_nanbox_pointer(closure as i64) } -/// Highest argument count deliverable to the shared thunk. The dispatcher -/// pads/truncates to the registered arity, so every interpreted function -/// receives exactly this many (missing args read `undefined` — exactly what -/// default-parameter binding needs). -const THUNK_ARITY: usize = 16; - fn ensure_thunk_registered() { - use std::sync::Once; - static REGISTER: Once = Once::new(); - REGISTER.call_once(|| { - crate::closure::js_register_closure_arity(interp_thunk as *const u8, THUNK_ARITY as u32); + thread_local! { + static REGISTERED: std::cell::Cell = const { std::cell::Cell::new(false) }; + } + REGISTERED.with(|registered| { + if registered.replace(true) { + return; + } + crate::closure::js_register_closure_rest(interp_thunk as *const u8, 0); }); } /// The single native entry every interpreted closure shares. Reads its /// identity + environment from capture slots and runs the tree-walker. -extern "C" fn interp_thunk( - closure: *const crate::closure::ClosureHeader, - a0: f64, - a1: f64, - a2: f64, - a3: f64, - a4: f64, - a5: f64, - a6: f64, - a7: f64, - a8: f64, - a9: f64, - a10: f64, - a11: f64, - a12: f64, - a13: f64, - a14: f64, - a15: f64, -) -> f64 { +extern "C" fn interp_thunk(closure: *const crate::closure::ClosureHeader, raw_args: f64) -> f64 { let fn_id = crate::closure::js_closure_get_capture_f64(closure, 0) as u32; let def_env = f64::from_bits(crate::closure::js_closure_get_capture_bits(closure, 1)); let this_bits = crate::closure::js_closure_get_capture_bits(closure, 2); + let is_arrow = this_bits != NO_LEXICAL_THIS; let this = if this_bits == NO_LEXICAL_THIS { crate::object::js_implicit_this_get() } else { f64::from_bits(this_bits) }; - let args = [ - a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, - ]; - invoke_interp_fn(fn_id, def_env, this, &args) + let global = f64::from_bits(crate::closure::js_closure_get_capture_bits(closure, 3)); + let intrinsics = f64::from_bits(crate::closure::js_closure_get_capture_bits(closure, 4)); + let strings_allowed = crate::closure::js_closure_get_capture_f64(closure, 5) != 0.0; + let wasm_allowed = crate::closure::js_closure_get_capture_f64(closure, 6) != 0.0; + let raw_args = + (raw_args.to_bits() & crate::value::POINTER_MASK) as *const crate::array::ArrayHeader; + let args = if raw_args.is_null() { + Vec::new() + } else { + (0..crate::array::js_array_length(raw_args)) + .map(|index| crate::array::js_array_get_f64(raw_args, index)) + .collect() + }; + invoke_interp_fn( + fn_id, + def_env, + this, + global, + intrinsics, + strings_allowed, + wasm_allowed, + is_arrow, + &args, + ) } /// Run one interpreted call: fresh scope chained to the defining env, params /// bound (destructuring + defaults), vars hoisted, function declarations /// hoisted, body executed. -pub(crate) fn invoke_interp_fn(fn_id: u32, def_env: f64, this: f64, args: &[f64]) -> f64 { +pub(crate) fn invoke_interp_fn( + fn_id: u32, + def_env: f64, + this: f64, + global: f64, + intrinsics: f64, + strings_allowed: bool, + wasm_allowed: bool, + is_arrow: bool, + args: &[f64], +) -> f64 { let fun = match lookup_fn(fn_id) { Some(f) => f, None => bridge::throw_type_error( @@ -285,25 +339,51 @@ pub(crate) fn invoke_interp_fn(fn_id: u32, def_env: f64, this: f64, args: &[f64] if call_depth_enter().is_err() { bridge::throw_range_error("Maximum call stack size exceeded (interpreted)"); } + let this = if !fun.strict && bridge::is_nullish(this) { + global + } else { + this + }; let base = roots_len(); // Root the incoming this + every argument: default-value evaluation and // destructuring allocate, which can move any of them. let this_idx = root_push(this); let ret_idx = root_push(bridge::undefined()); + let global_idx = root_push(global); + let intrinsics_idx = root_push(intrinsics); let def_env_idx = root_push(def_env); - // Root only the arguments a parameter will actually bind. The thunk always - // delivers THUNK_ARITY slots, but a validator declaring one or two params - // (the overwhelming case) needn't pay 16 `root_push`es per call — there is - // no `arguments` object, so surplus args are unobservable (#6693). - let nargs = fun.params.len().min(args.len()).min(THUNK_ARITY); - let mut arg_idxs = [0usize; THUNK_ARITY]; - for (i, slot) in arg_idxs.iter_mut().enumerate().take(nargs) { - *slot = root_push(args[i]); - } + let arg_idxs = args.iter().copied().map(root_push).collect::>(); + let nargs = fun.params.len().min(arg_idxs.len()); let call_env = env::env_new(root_get(def_env_idx)); let env_idx = root_push(call_env); - let ctx = Ctx { this_idx, ret_idx }; + if !is_arrow { + let arguments_idx = root_push(bridge::object_new()); + bridge::set_member( + root_get(arguments_idx), + "length", + bridge::make_number(arg_idxs.len() as f64), + ); + for (index, &arg_idx) in arg_idxs.iter().enumerate() { + bridge::set_member( + root_get(arguments_idx), + &index.to_string(), + root_get(arg_idx), + ); + } + env::define(root_get(env_idx), "arguments", root_get(arguments_idx)); + } + + let ctx = Ctx { + this_idx, + ret_idx, + global_idx, + intrinsics_idx, + variable_env_idx: env_idx, + strict: fun.strict, + strings_allowed, + wasm_allowed, + }; // Parameters. for (i, pat) in fun.params.iter().enumerate() { @@ -327,7 +407,7 @@ pub(crate) fn invoke_interp_fn(fn_id: u32, def_env: f64, this: f64, args: &[f64] root_set(ret_idx, v); } InterpBody::Block(stmts) => { - hoist_fn_decls(&ctx, stmts, env_idx); + hoist_fn_decls(&ctx, stmts, env_idx, true); let _ = exec_stmts(&ctx, stmts, env_idx); } } @@ -349,7 +429,7 @@ pub(crate) fn make_function_value( node_addr: usize, env_idx: usize, ) -> f64 { - let fn_id = fn_id_for_node(node_addr, || build_interp_fn(params, body)); + let fn_id = fn_id_for_node(node_addr, || build_interp_fn(params, body, ctx.strict)); let lexical_this = if is_arrow { Some(root_get(ctx.this_idx)) } else { @@ -360,21 +440,37 @@ pub(crate) fn make_function_value( // one-binding scope between the defining env and the body env. let name_env = env::env_new(root_get(env_idx)); let name_env_idx = root_push(name_env); - let closure = alloc_interp_closure(fn_id, root_get(name_env_idx), lexical_this); + let closure = alloc_interp_closure( + fn_id, + root_get(name_env_idx), + lexical_this, + root_get(ctx.global_idx), + root_get(ctx.intrinsics_idx), + ctx.strings_allowed, + ctx.wasm_allowed, + ); let closure_idx = root_push(closure); env::define(root_get(name_env_idx), &fn_name, root_get(closure_idx)); let closure = root_get(closure_idx); roots_truncate(name_env_idx); closure } else { - alloc_interp_closure(fn_id, root_get(env_idx), lexical_this) + alloc_interp_closure( + fn_id, + root_get(env_idx), + lexical_this, + root_get(ctx.global_idx), + root_get(ctx.intrinsics_idx), + ctx.strings_allowed, + ctx.wasm_allowed, + ) } } /// Hoist `function f(...) {}` declarations of a statement list into the /// current scope (evaluated before any statement runs — ajv's serializers /// call later-declared helpers). -fn hoist_fn_decls(ctx: &Ctx, stmts: &[ast::Stmt], env_idx: usize) { +fn hoist_fn_decls(ctx: &Ctx, stmts: &[ast::Stmt], env_idx: usize, declare: bool) { for stmt in stmts { if let ast::Stmt::Decl(ast::Decl::Fn(f)) = stmt { if f.function.is_generator || f.function.is_async { @@ -397,7 +493,11 @@ fn hoist_fn_decls(ctx: &Ctx, stmts: &[ast::Stmt], env_idx: usize) { env_idx, ); let value_idx = root_push(value); - env::define(root_get(env_idx), &name, root_get(value_idx)); + if declare { + env::define(root_get(env_idx), &name, root_get(value_idx)); + } else { + env::assign(root_get(env_idx), &name, root_get(value_idx), ctx.strict); + } roots_truncate(value_idx); } } @@ -415,10 +515,69 @@ pub(crate) fn exec_stmts(ctx: &Ctx, stmts: &[ast::Stmt], env_idx: usize) -> Flow Flow::Normal } +/// Execute script/global code in a persistent lexical environment. Top-level +/// `var` and function declarations route through the object-environment root; +/// `let`/`const` stay on `env_idx`, so repeated VM evaluations share lexicals +/// without exposing them as sandbox properties. +pub(crate) fn exec_script_stmts(ctx: &Ctx, stmts: &[ast::Stmt], env_idx: usize) -> Flow { + let mut vars = Vec::new(); + collect_var_names(stmts, &mut vars); + vars.sort_unstable(); + vars.dedup(); + for name in vars { + if !env::is_bound(root_get(env_idx), &name) { + env::assign(root_get(env_idx), &name, bridge::undefined(), ctx.strict); + } + } + hoist_fn_decls(ctx, stmts, env_idx, false); + for stmt in stmts { + let flow = if let ast::Stmt::Expr(expr) = stmt { + let value = eval_expr(ctx, &expr.expr, env_idx); + root_set(ctx.ret_idx, value); + Flow::Normal + } else { + exec_stmt(ctx, stmt, env_idx) + }; + if !matches!(flow, Flow::Normal) { + return flow; + } + } + Flow::Normal +} + +pub(crate) fn exec_direct_eval_stmts( + ctx: &Ctx, + stmts: &[ast::Stmt], + lexical_env_idx: usize, + variable_env_idx: usize, +) -> Flow { + let mut vars = Vec::new(); + collect_var_names(stmts, &mut vars); + vars.sort_unstable(); + vars.dedup(); + for name in vars { + env::ensure_var_binding(root_get(variable_env_idx), &name); + } + hoist_fn_decls(ctx, stmts, lexical_env_idx, ctx.strict); + for stmt in stmts { + let flow = if let ast::Stmt::Expr(expr) = stmt { + let value = eval_expr(ctx, &expr.expr, lexical_env_idx); + root_set(ctx.ret_idx, value); + Flow::Normal + } else { + exec_stmt(ctx, stmt, lexical_env_idx) + }; + if !matches!(flow, Flow::Normal) { + return flow; + } + } + Flow::Normal +} + fn exec_block_scope(ctx: &Ctx, block: &ast::BlockStmt, env_idx: usize) -> Flow { let child = env::env_new(root_get(env_idx)); let child_idx = root_push(child); - hoist_fn_decls(ctx, &block.stmts, child_idx); + hoist_fn_decls(ctx, &block.stmts, child_idx, true); let flow = exec_stmts(ctx, &block.stmts, child_idx); roots_truncate(child_idx); flow @@ -517,7 +676,7 @@ pub(crate) fn bind_pattern(ctx: &Ctx, pat: &ast::Pat, value: f64, env_idx: usize env::define(root_get(env_idx), name, root_get(value_idx)); roots_truncate(value_idx); } else { - env::assign(root_get(env_idx), name, value); + env::assign(root_get(env_idx), name, value, ctx.strict); } } ast::Pat::Assign(a) => { @@ -551,7 +710,7 @@ pub(crate) fn bind_pattern(ctx: &Ctx, pat: &ast::Pat, value: f64, env_idx: usize if declare { env::define(root_get(env_idx), name, root_get(sub_idx)); } else { - env::assign(root_get(env_idx), name, root_get(sub_idx)); + env::assign(root_get(env_idx), name, root_get(sub_idx), ctx.strict); } roots_truncate(sub_idx); } @@ -901,7 +1060,7 @@ fn exec_try_catch(ctx: &Ctx, t: &ast::TryStmt, env_idx: usize) -> Flow { if let Some(param) = &handler.param { bind_pattern(ctx, param, root_get(exc_idx), catch_env_idx, true); } - hoist_fn_decls(ctx, &handler.body.stmts, catch_env_idx); + hoist_fn_decls(ctx, &handler.body.stmts, catch_env_idx, true); let flow = exec_stmts(ctx, &handler.body.stmts, catch_env_idx); roots_truncate(exc_idx); flow diff --git a/crates/perry-runtime/src/dyn_eval/mod.rs b/crates/perry-runtime/src/dyn_eval/mod.rs index 3629720a2b..9b8cba1109 100644 --- a/crates/perry-runtime/src/dyn_eval/mod.rs +++ b/crates/perry-runtime/src/dyn_eval/mod.rs @@ -68,6 +68,8 @@ pub(crate) struct InterpFn { /// `var` names hoisted to the function scope (prepass, excludes nested /// function bodies). pub hoisted_vars: Vec, + /// Whether assignments in this function use strict PutValue semantics. + pub strict: bool, } pub(crate) enum InterpBody { @@ -276,6 +278,208 @@ pub fn scan_dyn_eval_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) /// * TypeError naming the construct when the source parses but uses /// something outside the interpreter subset. pub fn dyn_function_from_strings(args: &[String]) -> f64 { + let fn_id = prepare_function_args(args); + // Preserve Function-constructor semantics: each instance owns a private + // sloppy-assignment root, while universal globals resolve in this realm. + let global = crate::object::js_get_global_this(); + let root_env = env::env_new_root(); + let root_idx = root_push(root_env); + let closure = + interp::alloc_interp_closure(fn_id, root_get(root_idx), None, global, global, true, true); + roots_truncate(root_idx); + closure +} + +/// Build a persistent script lexical environment over a live global/object +/// environment chain. The last `object_envs` entry has highest precedence. +pub(crate) fn script_environment(global: f64, object_envs: &[f64]) -> f64 { + let chain = object_environment_chain(global, object_envs); + let chain_idx = root_push(chain); + let lexical = env::env_new(root_get(chain_idx)); + roots_truncate(chain_idx); + lexical +} + +pub(crate) fn script_binding(lexical_env: f64, name: &str) -> f64 { + env::lookup(lexical_env, name).unwrap_or_else(bridge::undefined) +} + +/// Compile a Function-constructor body against a selected global and live +/// context-extension objects. Parameter/local bindings still win; then +/// object_envs are searched from last to first; the global is last. +#[cfg(test)] +pub(crate) fn function_from_strings_in( + args: &[String], + global_this: f64, + intrinsics: f64, + object_envs: &[f64], +) -> f64 { + function_from_strings_in_with_codegen(args, global_this, intrinsics, object_envs, true, true) +} + +pub(crate) fn function_from_strings_in_with_codegen( + args: &[String], + global_this: f64, + intrinsics: f64, + object_envs: &[f64], + strings_allowed: bool, + wasm_allowed: bool, +) -> f64 { + let base = roots_len(); + let global_idx = root_push(global_this); + let intrinsics_idx = root_push(intrinsics); + let object_env_idxs = object_envs + .iter() + .copied() + .map(root_push) + .collect::>(); + let fn_id = prepare_function_args(args); + let rooted_object_envs = object_env_idxs + .iter() + .map(|&idx| root_get(idx)) + .collect::>(); + let chain = object_environment_chain(root_get(global_idx), &rooted_object_envs); + let chain_idx = root_push(chain); + let closure = interp::alloc_interp_closure( + fn_id, + root_get(chain_idx), + None, + root_get(global_idx), + root_get(intrinsics_idx), + strings_allowed, + wasm_allowed, + ); + roots_truncate(base); + closure +} + +/// Parse and execute script/global code with a selected global and persistent +/// lexical environment. Syntax errors use the shared SWC parser diagnostic; +/// unsupported runtime constructs keep dyn_eval's precise TypeError path. +pub(crate) fn eval_script_in( + source: &str, + global_this: f64, + intrinsics: f64, + lexical_env: f64, +) -> f64 { + eval_script_in_with_codegen(source, global_this, intrinsics, lexical_env, true, true) +} + +pub(crate) fn eval_script_in_with_codegen( + source: &str, + global_this: f64, + intrinsics: f64, + lexical_env: f64, + strings_allowed: bool, + wasm_allowed: bool, +) -> f64 { + let base = roots_len(); + let global_idx = root_push(global_this); + let intrinsics_idx = root_push(intrinsics); + let env_idx = root_push(lexical_env); + let statements = parse_script_statements(source); + let variable_env_idx = root_push(env::variable_environment(root_get(env_idx))); + let ret_idx = root_push(bridge::undefined()); + let ctx = interp::Ctx { + this_idx: global_idx, + ret_idx, + global_idx, + intrinsics_idx, + variable_env_idx, + strict: interp::has_use_strict_directive(&statements), + strings_allowed, + wasm_allowed, + }; + let _ = interp::exec_script_stmts(&ctx, &statements, env_idx); + let result = root_get(ret_idx); + roots_truncate(base); + result +} + +pub(crate) fn eval_direct_in( + source: &str, + global_this: f64, + intrinsics: f64, + caller_env: f64, + caller_variable_env: f64, + strings_allowed: bool, + wasm_allowed: bool, +) -> f64 { + let base = roots_len(); + let global_idx = root_push(global_this); + let intrinsics_idx = root_push(intrinsics); + let caller_env_idx = root_push(caller_env); + let caller_variable_env_idx = root_push(caller_variable_env); + let statements = parse_script_statements(source); + let strict = interp::has_use_strict_directive(&statements); + let lexical_env_idx = root_push(env::env_new(root_get(caller_env_idx))); + let ret_idx = root_push(bridge::undefined()); + let variable_env_idx = if strict { + lexical_env_idx + } else { + caller_variable_env_idx + }; + let ctx = interp::Ctx { + this_idx: global_idx, + ret_idx, + global_idx, + intrinsics_idx, + variable_env_idx, + strict, + strings_allowed, + wasm_allowed, + }; + let _ = interp::exec_direct_eval_stmts(&ctx, &statements, lexical_env_idx, variable_env_idx); + let result = root_get(ret_idx); + roots_truncate(base); + result +} + +fn parse_script_statements(source: &str) -> Vec { + let mut cache = perry_diagnostics_cache(); + let parsed = + perry_parser::parse_typescript_with_cache(source, "perry-vm-script.cjs", &mut cache) + .unwrap_or_else(|e| { + bridge::throw_syntax_error(&format!("invalid node:vm script source: {e}")) + }); + parsed + .module + .body + .into_iter() + .map(|item| match item { + ast::ModuleItem::Stmt(stmt) => stmt, + ast::ModuleItem::ModuleDecl(_) => { + bridge::throw_syntax_error("module syntax is not valid in a vm.Script") + } + }) + .collect() +} + +pub(crate) fn validate_script_source(source: &str) -> f64 { + let _ = parse_script_statements(source); + bridge::undefined() +} + +fn object_environment_chain(global: f64, object_envs: &[f64]) -> f64 { + let base = roots_len(); + let global_idx = root_push(global); + let object_idxs = object_envs + .iter() + .copied() + .map(root_push) + .collect::>(); + let root = env::env_new_object(None, root_get(global_idx)); + let env_idx = root_push(root); + for &object_idx in &object_idxs { + let next = env::env_new_object(Some(root_get(env_idx)), root_get(object_idx)); + root_set(env_idx, next); + } + let result = root_get(env_idx); + roots_truncate(base); + result +} + +fn prepare_function_args(args: &[String]) -> u32 { let (params, body) = match args.split_last() { Some((body, params)) => (params.join(","), body.as_str()), None => (String::new(), ""), @@ -309,14 +513,7 @@ pub fn dyn_function_from_strings(args: &[String]) -> f64 { } } }; - // The instance's root environment: undeclared-assignment target (sloppy - // implicit "globals" scoped to this Function instance) and the parent of - // every call scope. - let root_env = env::env_new_root(); - let root_idx = root_push(root_env); - let closure = interp::alloc_interp_closure(fn_id, root_get(root_idx), None); - roots_truncate(root_idx); - closure + fn_id } /// Parse an assembled `(function anonymous(…){…})` source, reject @@ -350,6 +547,7 @@ fn prepare_source(source: &str) -> u32 { let interp_fn = interp::build_interp_fn( func.params.into_iter().map(|p| p.pat).collect(), InterpBody::Block(func.body.map(|b| b.stmts).unwrap_or_default()), + false, ); register_fn(interp_fn) } diff --git a/crates/perry-runtime/src/dyn_eval/tests.rs b/crates/perry-runtime/src/dyn_eval/tests.rs index 3f1fd2dd38..8cbf574890 100644 --- a/crates/perry-runtime/src/dyn_eval/tests.rs +++ b/crates/perry-runtime/src/dyn_eval/tests.rs @@ -793,3 +793,224 @@ fn get_first_lookup_resolves_declared_undefined_binding() { let g = dyn_fn(&["p", "return typeof p"]); assert_eq!(as_str(call(g, &[bridge::undefined()])), "undefined"); } + +// ── VM target-global / object-environment adapter ───────────────────────── + +fn object_with(fields: &[(&str, f64)]) -> f64 { + let value = bridge::object_new(); + for (name, field) in fields { + bridge::set_member(value, name, *field); + } + value +} + +#[test] +fn script_adapter_mutates_target_and_persists_lexicals() { + let global = object_with(&[("seed", num(2.0))]); + let lexical = script_environment(global, &[]); + + let first = eval_script_in( + "seed += 5; let lexicalValue = 3; const fixed = 4; lexicalValue + fixed", + global, + global, + lexical, + ); + assert_eq!(as_num(first), 7.0); + assert_eq!(as_num(bridge::get_member(global, "seed")), 7.0); + assert!(!bridge::global_has_property(global, "lexicalValue")); + + let second = eval_script_in( + "lexicalValue += 2; created = lexicalValue + fixed; created", + global, + global, + lexical, + ); + assert_eq!(as_num(second), 9.0); + assert_eq!(as_num(bridge::get_member(global, "created")), 9.0); +} + +#[test] +fn script_adapter_uses_target_global_for_nested_functions() { + let global = object_with(&[("marker", num(7.0))]); + let lexical = script_environment(global, &[]); + let result = eval_script_in( + "function readMarker() { return globalThis.marker; } readMarker()", + global, + global, + lexical, + ); + assert_eq!(as_num(result), 7.0); + assert_eq!( + as_num(call(bridge::get_member(global, "readMarker"), &[])), + 7.0 + ); +} + +#[test] +fn function_adapter_reads_live_object_environments_in_precedence_order() { + let global = object_with(&[("fallback", string("context"))]); + let first = object_with(&[("left", string("left")), ("shared", string("first"))]); + let second = object_with(&[("right", string("right")), ("shared", string("second"))]); + let args = vec![ + "arg".to_string(), + "return left + ':' + shared + ':' + right + ':' + fallback + ':' + arg".to_string(), + ]; + let function = function_from_strings_in(&args, global, global, &[first, second]); + + assert_eq!( + as_str(call(function, &[string("one")])), + "left:second:right:context:one" + ); + bridge::set_member(second, "shared", string("changed")); + assert_eq!( + as_str(call(function, &[string("two")])), + "left:changed:right:context:two" + ); +} + +fn intrinsic_constructor(prototype: f64, fields: &[(&str, f64)]) -> f64 { + let constructor = object_with(fields); + bridge::set_member(constructor, "prototype", prototype); + constructor +} + +fn test_intrinsics() -> (f64, f64, f64, f64) { + let object_prototype = bridge::object_new(); + let array_prototype = bridge::object_new(); + let type_error_prototype = bridge::object_new(); + let intrinsics = object_with(&[ + ( + "Object", + intrinsic_constructor(object_prototype, &[("marker", string("intrinsic"))]), + ), + ("Array", intrinsic_constructor(array_prototype, &[])), + ( + "TypeError", + intrinsic_constructor(type_error_prototype, &[]), + ), + ]); + ( + intrinsics, + object_prototype, + array_prototype, + type_error_prototype, + ) +} + +fn recorded_prototype(value: f64) -> f64 { + let owner = crate::value::js_nanbox_get_pointer(value) as usize; + f64::from_bits( + crate::object::prototype_chain::object_static_prototype(owner) + .expect("value should retain its creation-realm prototype"), + ) +} + +#[test] +fn script_adapter_separates_global_this_from_intrinsic_lookup() { + let global_this = object_with(&[("marker", string("receiver"))]); + let (intrinsics, _, _, _) = test_intrinsics(); + let lexical = script_environment(global_this, &[]); + + let result = eval_script_in( + "globalThis.marker + ':' + Object.marker", + global_this, + intrinsics, + lexical, + ); + assert_eq!(as_str(result), "receiver:intrinsic"); +} + +#[test] +fn script_literals_and_errors_retain_intrinsic_prototypes() { + let global_this = object_with(&[]); + let (intrinsics, object_prototype, array_prototype, type_error_prototype) = test_intrinsics(); + let lexical = script_environment(global_this, &[]); + + let object = eval_script_in("({ value: 1 })", global_this, intrinsics, lexical); + let array = eval_script_in("[1, 2]", global_this, intrinsics, lexical); + let error = eval_script_in("new TypeError('boom')", global_this, intrinsics, lexical); + + assert_eq!( + recorded_prototype(object).to_bits(), + object_prototype.to_bits() + ); + assert_eq!( + recorded_prototype(array).to_bits(), + array_prototype.to_bits() + ); + assert_eq!( + recorded_prototype(error).to_bits(), + type_error_prototype.to_bits() + ); +} + +#[test] +fn script_literals_use_fresh_populated_realm_prototypes() { + let _ = crate::object::js_get_global_this(); + let outer_object_constructor = + crate::object::js_get_global_this_builtin_value(b"Object".as_ptr(), "Object".len()); + let outer_object_prototype = crate::object::builtin_prototype_value("Object"); + let scope = crate::gc::RuntimeHandleScope::new(); + let intrinsics = scope.root_raw_mut_ptr(crate::object::js_object_alloc(0, 0)); + crate::object::populate_global_this_builtins( + intrinsics + .across_mut::(|| ()) + .1, + ); + let intrinsics = crate::value::js_nanbox_pointer( + intrinsics + .across_mut::(|| ()) + .1 as i64, + ); + let realm_object_prototype = bridge::intrinsic_prototype(intrinsics, "Object"); + assert_ne!( + realm_object_prototype.to_bits(), + outer_object_prototype.to_bits() + ); + + let global = bridge::object_new(); + let lexical = script_environment(global, &[]); + let value = eval_script_in("({ value: 1 })", global, intrinsics, lexical); + assert_eq!( + crate::object::js_object_get_prototype_of(value).to_bits(), + realm_object_prototype.to_bits(), + ); + let descriptor = eval_script_in( + "Object.getOwnPropertyDescriptor({ value: 1 }, 'value')", + global, + intrinsics, + lexical, + ); + assert_eq!( + crate::object::js_object_get_prototype_of(descriptor).to_bits(), + realm_object_prototype.to_bits(), + ); + assert_ne!( + bridge::call_method(outer_object_constructor, "getPrototypeOf", &[descriptor]).to_bits(), + bridge::get_member(outer_object_constructor, "prototype").to_bits(), + ); + let error = eval_script_in("new TypeError('boom')", global, intrinsics, lexical); + assert_eq!( + crate::object::js_instanceof(error, crate::error::CLASS_ID_TYPE_ERROR).to_bits(), + crate::value::TAG_FALSE, + ); + let promise = eval_script_in("Promise.resolve(1)", global, intrinsics, lexical); + assert_eq!( + crate::object::js_instanceof(promise, 0xFFFF_0027).to_bits(), + crate::value::TAG_FALSE, + ); +} + +#[test] +fn promise_static_result_retains_intrinsic_prototype() { + let global_this = object_with(&[]); + let intrinsics = crate::object::js_get_global_this(); + let lexical = script_environment(global_this, &[]); + let promise = eval_script_in("Promise.resolve(7)", global_this, intrinsics, lexical); + + assert_ne!(crate::promise::js_value_is_promise(promise), 0); + assert_eq!( + recorded_prototype(promise).to_bits(), + bridge::intrinsic_prototype(intrinsics, "Promise").to_bits() + ); +} diff --git a/crates/perry-runtime/src/error.rs b/crates/perry-runtime/src/error.rs index 923a9edb09..b33260c3ed 100644 --- a/crates/perry-runtime/src/error.rs +++ b/crates/perry-runtime/src/error.rs @@ -100,6 +100,14 @@ thread_local! { /// a `--debug-symbols` build). static CURRENT_CALL_LOCATION: std::cell::Cell> = const { std::cell::Cell::new(None) }; + static RUNTIME_SOURCE_LOCATION: std::cell::RefCell> = + const { std::cell::RefCell::new(None) }; +} + +pub(crate) fn replace_runtime_source_location( + location: Option<(String, u32, u32)>, +) -> Option<(String, u32, u32)> { + RUNTIME_SOURCE_LOCATION.with(|slot| std::mem::replace(&mut *slot.borrow_mut(), location)) } /// #5247: record the source location of the call about to be dispatched. @@ -129,6 +137,9 @@ static KEEP_JS_SET_CALL_LOCATION: unsafe extern "C" fn(*const u8, usize, u32) = /// #5247: render the current call-location frame, or `` when no /// location was recorded (default builds, or a synthesized/offset-less site). fn current_stack_frame() -> String { + if let Some((file, line, column)) = RUNTIME_SOURCE_LOCATION.with(|slot| slot.borrow().clone()) { + return format!(" at {file}:{line}:{column}"); + } CURRENT_CALL_LOCATION.with(|c| match c.get() { Some((file_ptr, file_len, line)) => { let bytes = unsafe { std::slice::from_raw_parts(file_ptr as *const u8, file_len) }; diff --git a/crates/perry-runtime/src/node_submodules/mod.rs b/crates/perry-runtime/src/node_submodules/mod.rs index 04afbb49bb..e206031123 100644 --- a/crates/perry-runtime/src/node_submodules/mod.rs +++ b/crates/perry-runtime/src/node_submodules/mod.rs @@ -194,8 +194,12 @@ thunk!( "Web Streams constructors (node:stream/web) require the 'new' operator." ); -extern "C" fn thunk_vm_create_context(_closure: *const ClosureHeader, sandbox: f64) -> f64 { - crate::object::js_vm_create_context(sandbox) +extern "C" fn thunk_vm_create_context( + _closure: *const ClosureHeader, + sandbox: f64, + options: f64, +) -> f64 { + crate::object::js_vm_create_context(sandbox, options) } // ----- submodule table ----- @@ -205,7 +209,7 @@ static SUBMOD_VM: SubmoduleSpec = SubmoduleSpec { key: "vm", exports: &[ExportSpec { name: "createContext", - thunk: ExportThunk::Fn1(thunk_vm_create_context), + thunk: ExportThunk::Fn2(thunk_vm_create_context), }], }; diff --git a/crates/perry-runtime/src/node_vm.rs b/crates/perry-runtime/src/node_vm.rs index a6c5b52b6f..995d9b1d8c 100644 --- a/crates/perry-runtime/src/node_vm.rs +++ b/crates/perry-runtime/src/node_vm.rs @@ -6,7 +6,7 @@ //! repeated `Script` execution, `runIn*Context`, `compileFunction`, and gated //! `SourceTextModule`/`SyntheticModule` lifecycle behavior. -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, OnceLock}; @@ -17,8 +17,124 @@ use crate::object::{ObjectHeader, PropertyAttrs}; use crate::string::StringHeader; use crate::value::JSValue; -mod eval; -use eval::run_source; +/// Re-read a rooted raw pointer without recording bare-handle debt. +/// Prefer pairing a real allocating call via `across_*` when one is present; +/// this covers final/local reads where the handle is the source of truth. +#[inline] +fn hmut(h: &crate::gc::RuntimeHandle) -> *mut T { + h.across_mut::(|| ()).1 +} + +/// Feature-gated dyn-eval entry points. Product builds (`perry` → runtime with +/// `default-features = false`) omit `dyn-eval`; those paths throw cleanly so +/// the crate still typechecks. +#[cfg(feature = "dyn-eval")] +mod de { + pub(super) fn script_environment(global: f64, object_envs: &[f64]) -> f64 { + crate::dyn_eval::script_environment(global, object_envs) + } + pub(super) fn eval_script_in( + source: &str, + global_this: f64, + intrinsics: f64, + lexical: f64, + ) -> f64 { + crate::dyn_eval::eval_script_in(source, global_this, intrinsics, lexical) + } + pub(super) fn script_binding(lexical: f64, name: &str) -> f64 { + crate::dyn_eval::script_binding(lexical, name) + } + pub(super) fn eval_script_in_with_codegen( + source: &str, + global_this: f64, + intrinsics: f64, + lexical: f64, + strings_allowed: bool, + wasm_allowed: bool, + ) -> f64 { + crate::dyn_eval::eval_script_in_with_codegen( + source, + global_this, + intrinsics, + lexical, + strings_allowed, + wasm_allowed, + ) + } + pub(super) fn validate_script_source(code: &str) -> f64 { + crate::dyn_eval::validate_script_source(code) + } + pub(super) fn function_from_strings_in_with_codegen( + source: &[String], + global_this: f64, + intrinsics: f64, + extensions: &[f64], + strings_allowed: bool, + wasm_allowed: bool, + ) -> f64 { + crate::dyn_eval::function_from_strings_in_with_codegen( + source, + global_this, + intrinsics, + extensions, + strings_allowed, + wasm_allowed, + ) + } + pub(super) fn validate_module_source(source: &str) -> bool { + perry_parser::parse_typescript(source, "perry-vm-module.mjs").is_ok() + } +} + +#[cfg(not(feature = "dyn-eval"))] +mod de { + fn undef() -> f64 { + f64::from_bits(crate::value::JSValue::undefined().bits()) + } + pub(super) fn script_environment(_global: f64, _object_envs: &[f64]) -> f64 { + undef() + } + pub(super) fn eval_script_in( + _source: &str, + _global_this: f64, + _intrinsics: f64, + _lexical: f64, + ) -> f64 { + super::throw_vm_unimplemented("vm.Script / runIn*Context", "#6768") + } + pub(super) fn script_binding(_lexical: f64, _name: &str) -> f64 { + undef() + } + pub(super) fn eval_script_in_with_codegen( + _source: &str, + _global_this: f64, + _intrinsics: f64, + _lexical: f64, + _strings_allowed: bool, + _wasm_allowed: bool, + ) -> f64 { + super::throw_vm_unimplemented("vm.Script / runIn*Context", "#6768") + } + pub(super) fn validate_script_source(_code: &str) -> f64 { + undef() + } + pub(super) fn function_from_strings_in_with_codegen( + _source: &[String], + _global_this: f64, + _intrinsics: f64, + _extensions: &[f64], + _strings_allowed: bool, + _wasm_allowed: bool, + ) -> f64 { + super::throw_vm_unimplemented("vm.compileFunction", "#6768") + } + pub(super) fn validate_module_source(_source: &str) -> bool { + false + } +} + +mod modules; +pub use modules::*; const STATUS_UNLINKED: &str = "unlinked"; const STATUS_LINKING: &str = "linking"; @@ -41,6 +157,7 @@ const FIELD_IMPORTS: &str = "__vm_imports"; const FIELD_EXPORTS: &str = "__vm_exports"; const FIELD_LINKED_MODULES: &str = "__vm_linked_modules"; const FIELD_EVALUATE_CALLBACK: &str = "__vm_evaluate_callback"; +const FIELD_CONTEXT: &str = "__vm_context"; static MODULE_ID_COUNTER: AtomicU64 = AtomicU64::new(0); const CACHE_PREFIX: &[u8] = b"PERRY_VM_CACHE\0"; @@ -48,37 +165,74 @@ const CACHE_KIND_SCRIPT: u8 = 1; const CACHE_KIND_FUNCTION: u8 = 2; const CACHE_KIND_MODULE: u8 = 3; -#[derive(Clone)] -struct CompiledFunction { - body: String, - params: Vec, - context_bits: u64, -} - #[derive(Clone)] struct ScriptMetadata { source: String, + filename: String, + line_offset: i32, + column_offset: i32, } -struct EvalEnv { - target: f64, - params: HashMap, -} +static VM_COMPILED_FUNCTION_SOURCES: OnceLock>> = OnceLock::new(); -static VM_CONTEXTS: OnceLock>> = OnceLock::new(); -static VM_SCRIPTS: OnceLock>> = OnceLock::new(); -static VM_FUNCTIONS: OnceLock>> = OnceLock::new(); +thread_local! { + static VM_INTRINSIC_GLOBAL: std::cell::Cell = const { std::cell::Cell::new(0) }; + static VM_CONTEXTS: std::cell::RefCell> = + std::cell::RefCell::new(HashMap::new()); + static MAIN_CONTEXT: std::cell::RefCell> = + const { std::cell::RefCell::new(None) }; +} -fn contexts() -> &'static Mutex> { - VM_CONTEXTS.get_or_init(|| Mutex::new(HashSet::new())) +#[derive(Clone)] +struct ContextState { + returned_bits: u64, + sandbox_bits: u64, + global_this_bits: u64, + intrinsics_bits: u64, + lexical_env_bits: u64, + strings_allowed: bool, + wasm_allowed: bool, + microtask_after_evaluate: bool, +} + +#[derive(Clone, Copy)] +struct ContextOptions { + strings_allowed: bool, + wasm_allowed: bool, + microtask_after_evaluate: bool, +} + +impl Default for ContextOptions { + fn default() -> Self { + Self { + strings_allowed: true, + wasm_allowed: true, + microtask_after_evaluate: false, + } + } } +static VM_SCRIPTS: OnceLock>> = OnceLock::new(); + fn scripts() -> &'static Mutex> { VM_SCRIPTS.get_or_init(|| Mutex::new(HashMap::new())) } -fn functions() -> &'static Mutex> { - VM_FUNCTIONS.get_or_init(|| Mutex::new(HashMap::new())) +fn compiled_function_sources() -> &'static Mutex> { + VM_COMPILED_FUNCTION_SOURCES.get_or_init(|| Mutex::new(HashMap::new())) +} + +pub(crate) fn compiled_function_source_for_closure(closure: usize) -> Option { + VM_COMPILED_FUNCTION_SOURCES + .get() + .and_then(|sources| sources.lock().ok()?.get(&closure).cloned()) +} + +pub(crate) fn function_source_for_closure(closure: usize) -> String { + compiled_function_source_for_closure(closure).unwrap_or_else(|| { + let func_ptr = unsafe { (*(closure as *const ClosureHeader)).func_ptr as usize }; + crate::builtins::function_source_for_func_ptr(func_ptr) + }) } #[derive(Clone, Debug)] @@ -114,10 +268,6 @@ fn bool_value(value: bool) -> f64 { f64::from_bits(JSValue::bool(value).bits()) } -fn number_value(value: f64) -> f64 { - f64::from_bits(JSValue::number(value).bits()) -} - fn string_ptr(value: &str) -> *mut StringHeader { crate::string::js_string_from_bytes(value.as_ptr(), value.len() as u32) } @@ -189,20 +339,21 @@ fn field_key(name: &str) -> *mut StringHeader { } fn set_field(obj: *mut ObjectHeader, name: &str, value: f64) { - crate::object::js_object_set_field_by_name(obj, field_key(name), value); + let scope = crate::gc::RuntimeHandleScope::new(); + let obj = scope.root_raw_mut_ptr(obj); + let value = scope.root_nanbox_f64(value); + let key = scope.root_string_ptr(field_key(name)); + crate::object::js_object_set_field_by_name( + hmut::(&obj), + hmut::(&key), + value.get_nanbox_f64(), + ); } fn get_field(obj: *mut ObjectHeader, name: &str) -> f64 { crate::object::js_object_get_field_by_name_f64(obj, field_key(name)) } -fn get_object_field(object: f64, name: &str) -> f64 { - let Some(ptr) = object_ptr_from_value(object) else { - return undefined_value(); - }; - get_field(ptr, name) -} - fn set_value_field(value: f64, name: &str, field_value: f64) { if let Some(ptr) = object_ptr_from_value(value) { set_field(ptr, name, field_value); @@ -225,10 +376,27 @@ fn get_string_field(obj: *mut ObjectHeader, name: &str) -> Option { string_from_value(get_field(obj, name)) } -fn options_identifier(options: f64) -> Option { - object_ptr_from_value(options) +fn module_options(options: f64) -> (f64, String) { + let options = options_object_or_default(options); + let identifier = options .map(|obj| get_field(obj, "identifier")) - .and_then(string_from_value) + .filter(|value| !JSValue::from_bits(value.to_bits()).is_undefined()) + .map(|value| { + string_from_value(value).unwrap_or_else(|| { + let message = format!( + "The \"options.identifier\" property must be of type string. Received {}", + crate::fs::validate::describe_received(value) + ); + throw_invalid_arg(&message); + }) + }) + .unwrap_or_else(default_identifier); + let context = options + .map(|obj| get_field(obj, "context")) + .filter(|value| !JSValue::from_bits(value.to_bits()).is_undefined()) + .map(|value| require_context(value, "options.context")) + .unwrap_or_else(|| create_context(undefined_value(), undefined_value())); + (context, identifier) } fn default_identifier() -> String { @@ -557,6 +725,12 @@ fn parse_source(source: &str) -> ParsedSource { } } +fn validate_module_source(source: &str) { + if !de::validate_module_source(source) { + throw_syntax("Invalid module source"); + } +} + fn strings_array(strings: &[String]) -> f64 { let mut arr = crate::array::js_array_alloc(strings.len() as u32); for value in strings { @@ -717,81 +891,6 @@ fn module_request_extra() -> f64 { object_value(obj) } -fn module_term_value(term: &str, env: &HashMap) -> f64 { - let term = term.trim(); - if term.is_empty() { - return undefined_value(); - } - if (term.starts_with('"') && term.ends_with('"')) - || (term.starts_with('\'') && term.ends_with('\'')) - { - return string_value(&term[1..term.len() - 1]); - } - if term == "true" { - return bool_value(true); - } - if term == "false" { - return bool_value(false); - } - if let Ok(number) = term.parse::() { - return number; - } - env.get(term).copied().unwrap_or_else(undefined_value) -} - -fn concat_string_for_value(value: f64) -> String { - if let Some(s) = string_from_value(value) { - return s; - } - let js = JSValue::from_bits(value.to_bits()); - if js.is_int32() { - return js.as_int32().to_string(); - } - if js.is_number() { - let n = js.as_number(); - if n.is_finite() && n.fract() == 0.0 { - return (n as i64).to_string(); - } - return n.to_string(); - } - if js.is_bool() { - return js.as_bool().to_string(); - } - if js.is_undefined() { - return "undefined".to_string(); - } - if js.is_null() { - return "null".to_string(); - } - "[object Object]".to_string() -} - -fn module_add(a: f64, b: f64) -> f64 { - let a_js = JSValue::from_bits(a.to_bits()); - let b_js = JSValue::from_bits(b.to_bits()); - if a_js.is_any_string() || b_js.is_any_string() { - return string_value(&format!( - "{}{}", - concat_string_for_value(a), - concat_string_for_value(b) - )); - } - unsafe { crate::value::js_dynamic_add(a, b) } -} - -fn eval_module_expr(expr: &str, env: &HashMap) -> f64 { - let mut parts = expr.split('+').map(str::trim); - let Some(first) = parts.next() else { - return undefined_value(); - }; - let mut acc = module_term_value(first, env); - for part in parts { - let rhs = module_term_value(part, env); - acc = module_add(acc, rhs); - } - acc -} - fn build_import_env(module: *mut ObjectHeader) -> HashMap { let mut env = HashMap::new(); for import in read_imports(module) { @@ -806,95 +905,6 @@ fn build_import_env(module: *mut ObjectHeader) -> HashMap { env } -fn evaluate_source_module(module: *mut ObjectHeader) -> f64 { - let status = module_status(module); - if status != STATUS_LINKED && status != STATUS_EVALUATED { - return throw_vm_status("Module status must be linked"); - } - if status == STATUS_EVALUATED { - return undefined_value(); - } - - set_status(module, STATUS_EVALUATING); - let Some(namespace) = namespace_for_module(module) else { - set_status(module, STATUS_ERRORED); - return throw_vm_status("Module namespace is unavailable"); - }; - - let mut env = build_import_env(module); - for export in read_exports(module) { - let value = eval_module_expr(&export.expr, &env); - env.insert(export.name.clone(), value); - set_field(namespace, &export.name, value); - } - set_status(module, STATUS_EVALUATED); - undefined_value() -} - -fn evaluate_synthetic_module(module: *mut ObjectHeader) -> f64 { - let status = module_status(module); - if status == STATUS_EVALUATED { - return undefined_value(); - } - if status != STATUS_LINKED { - return throw_vm_status("Module status must be linked"); - } - - set_status(module, STATUS_EVALUATING); - let callback = get_field(module, FIELD_EVALUATE_CALLBACK); - let js = JSValue::from_bits(callback.to_bits()); - if !js.is_undefined() && !js.is_null() { - let prev = crate::object::js_implicit_this_set(object_value(module)); - let _ = unsafe { crate::closure::js_native_call_value(callback, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev); - } - set_status(module, STATUS_EVALUATED); - undefined_value() -} - -fn module_has_tla(module: *mut ObjectHeader) -> bool { - let Some(source) = get_string_field(module, FIELD_SOURCE) else { - return false; - }; - parse_source(&source).has_top_level_await -} - -fn module_has_async_graph(module: *mut ObjectHeader) -> bool { - if module_has_tla(module) { - return true; - } - let Some(linked) = module_linked_modules(module) else { - return false; - }; - let len = crate::array::js_array_length(linked); - for idx in 0..len { - let value = crate::array::js_array_get_f64(linked, idx); - if let Some(dep) = object_ptr_from_value(value) { - if module_has_async_graph(dep) { - return true; - } - } - } - false -} - -fn new_module_base(kind: &str, status: &str, identifier: String) -> *mut ObjectHeader { - let module = crate::object::js_object_alloc(0, 10); - set_field(module, FIELD_KIND, string_value(kind)); - set_field(module, FIELD_STATUS, string_value(status)); - set_field(module, "status", string_value(status)); - set_field(module, FIELD_IDENTIFIER, string_value(&identifier)); - set_field(module, "identifier", string_value(&identifier)); - set_field(module, FIELD_ERROR, undefined_value()); - set_field(module, "error", undefined_value()); - set_field( - module, - FIELD_LINKED_MODULES, - array_value(crate::array::js_array_alloc(0)), - ); - module -} - fn throw_type_error(message: &str) -> ! { let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); let err = crate::error::js_typeerror_new(msg); @@ -945,6 +955,92 @@ fn code_string_for_script(value: f64) -> String { rust_string_from_header(ptr).unwrap_or_default() } +#[derive(Clone)] +struct SourceOptions { + filename: String, + line_offset: i32, + column_offset: i32, +} + +impl Default for SourceOptions { + fn default() -> Self { + Self { + filename: "evalmachine.".to_string(), + line_offset: 0, + column_offset: 0, + } + } +} + +fn source_options(options: f64, allow_string_filename: bool) -> SourceOptions { + if allow_string_filename { + if let Some(filename) = string_from_value(options) { + return SourceOptions { + filename, + ..SourceOptions::default() + }; + } + } + let Some(options) = options_object_or_default(options) else { + return SourceOptions::default(); + }; + let mut result = SourceOptions::default(); + let filename = get_field(options, "filename"); + if !JSValue::from_bits(filename.to_bits()).is_undefined() { + crate::validators::validate_string(filename, "options.filename"); + result.filename = string_from_value(filename).unwrap_or_default(); + } + let line_offset = get_field(options, "lineOffset"); + if !JSValue::from_bits(line_offset.to_bits()).is_undefined() { + result.line_offset = crate::validators::validate_int32( + line_offset, + "options.lineOffset", + i32::MIN, + i32::MAX, + ); + } + let column_offset = get_field(options, "columnOffset"); + if !JSValue::from_bits(column_offset.to_bits()).is_undefined() { + result.column_offset = crate::validators::validate_int32( + column_offset, + "options.columnOffset", + i32::MIN, + i32::MAX, + ); + } + result +} + +fn validate_run_options(options: f64) { + let Some(options) = options_object_or_default(options) else { + return; + }; + let timeout = get_field(options, "timeout"); + if !JSValue::from_bits(timeout.to_bits()).is_undefined() { + crate::validators::validate_integer(timeout, "options.timeout", 1.0, u32::MAX as f64); + } + for name in ["displayErrors", "breakOnSigint"] { + let value = get_field(options, name); + if !JSValue::from_bits(value.to_bits()).is_undefined() { + crate::validators::validate_boolean(value, &format!("options.{name}")); + } + } +} + +fn with_source_location(options: &SourceOptions, f: impl FnOnce() -> f64) -> f64 { + let old = crate::error::replace_runtime_source_location(Some(( + options.filename.clone(), + (options.line_offset as i64 + 1).max(1) as u32, + (options.column_offset as i64 + 1).max(1) as u32, + ))); + let result = crate::exception::js_call_catching(f); + crate::error::replace_runtime_source_location(old); + match result { + Ok(value) => value, + Err(error) => crate::exception::js_throw(error), + } +} + fn symbol_key(value: f64) -> Option { if unsafe { crate::symbol::js_is_symbol(value) == 0 } { return None; @@ -957,48 +1053,169 @@ fn is_dont_contextify(value: f64) -> bool { symbol_key(value).as_deref() == Some("vm_context_no_contextify") } -fn mark_context(value: f64) { - if let Some(ptr) = object_ptr_from_value(value) { - contexts().lock().unwrap().insert(ptr as usize); - } +fn value_is_object_like(value: f64) -> bool { + object_ptr_from_value(value).is_some() + || array_ptr_from_value(value).is_some() + || crate::proxy::js_proxy_is_proxy(value) != 0 } -fn is_context(value: f64) -> bool { - object_ptr_from_value(value) - .map(|ptr| contexts().lock().unwrap().contains(&(ptr as usize))) - .unwrap_or(false) +fn context_key(value: f64) -> usize { + raw_addr_from_value(value) } -fn new_plain_context() -> f64 { - let obj = crate::object::js_object_alloc(0, 0); - let value = crate::value::js_nanbox_pointer(obj as i64); - mark_context(value); +fn fresh_intrinsic_global() -> f64 { + // ponytail: share one VM intrinsic realm until Perry has a cheap realm-graph + // clone; rebuilding the 1.15 MB bootstrap per context exceeds the runner timeout. + let cached = VM_INTRINSIC_GLOBAL.with(|slot| slot.get()); + if cached != 0 { + return f64::from_bits(cached); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let intrinsics = crate::object::js_object_alloc(0, 0); + let intrinsics = scope.root_raw_mut_ptr(intrinsics); + crate::object::populate_global_this_builtins(hmut::(&intrinsics)); + crate::object::js_object_delete_field(hmut::(&intrinsics), string_ptr("process")); + let intrinsics = hmut::(&intrinsics); + let value = object_value(intrinsics); + VM_INTRINSIC_GLOBAL.with(|slot| { + slot.set(value.to_bits()); + crate::gc::runtime_write_barrier_root_heap_word(intrinsics as u64); + crate::gc::js_gc_register_global_root(slot.as_ptr() as i64); + }); value } -pub(crate) fn create_context(value: f64) -> f64 { - context_from_arg(value, "object") +fn new_context_state(sandbox: f64, dont_contextify: bool, options: ContextOptions) -> ContextState { + let scope = crate::gc::RuntimeHandleScope::new(); + let sandbox = scope.root_nanbox_f64(sandbox); + let global_this = if dont_contextify { + sandbox.get_nanbox_f64() + } else { + let handler = scope.root_nanbox_f64(object_value(crate::object::js_object_alloc(0, 0))); + crate::proxy::js_proxy_new(sandbox.get_nanbox_f64(), handler.get_nanbox_f64()) + }; + let global_this = scope.root_nanbox_f64(global_this); + let intrinsics = scope.root_nanbox_f64(fresh_intrinsic_global()); + let lexical_env = scope.root_nanbox_f64(de::script_environment(sandbox.get_nanbox_f64(), &[])); + let returned = sandbox.get_nanbox_f64(); + ContextState { + returned_bits: returned.to_bits(), + sandbox_bits: sandbox.get_nanbox_f64().to_bits(), + global_this_bits: global_this.get_nanbox_f64().to_bits(), + intrinsics_bits: intrinsics.get_nanbox_f64().to_bits(), + lexical_env_bits: lexical_env.get_nanbox_f64().to_bits(), + strings_allowed: options.strings_allowed, + wasm_allowed: options.wasm_allowed, + microtask_after_evaluate: options.microtask_after_evaluate, + } +} + +fn validate_optional_bool(object: *mut ObjectHeader, name: &str, default: bool) -> bool { + let value = get_field(object, name); + let js = JSValue::from_bits(value.to_bits()); + if js.is_undefined() { + return default; + } + if !js.is_bool() { + let message = format!( + "The \"{name}\" property must be of type boolean. Received {}", + crate::fs::validate::describe_received(value) + ); + throw_invalid_arg(&message); + } + js.as_bool() } -fn context_from_arg(value: f64, arg_name: &str) -> f64 { - let jv = JSValue::from_bits(value.to_bits()); - if jv.is_undefined() || is_dont_contextify(value) { - return new_plain_context(); +fn context_options(options: f64, code_generation_name: &str) -> ContextOptions { + let js = JSValue::from_bits(options.to_bits()); + if js.is_undefined() { + return ContextOptions::default(); + } + let Some(options) = object_ptr_from_value(options) else { + let message = format!( + "The \"options\" argument must be of type object. Received {}", + crate::fs::validate::describe_received(f64::from_bits(js.bits())) + ); + throw_invalid_arg(&message); + }; + for name in ["name", "origin"] { + let value = get_field(options, name); + if !JSValue::from_bits(value.to_bits()).is_undefined() && string_from_value(value).is_none() + { + let message = format!( + "The \"options.{name}\" property must be of type string. Received {}", + crate::fs::validate::describe_received(value) + ); + throw_invalid_arg(&message); + } + } + let mut result = ContextOptions::default(); + let code_generation = get_field(options, code_generation_name); + if !JSValue::from_bits(code_generation.to_bits()).is_undefined() { + let Some(code_generation) = object_ptr_from_value(code_generation) else { + let message = format!( + "The \"options.{code_generation_name}\" property must be of type object. Received {}", + crate::fs::validate::describe_received(code_generation) + ); + throw_invalid_arg(&message); + }; + result.strings_allowed = validate_optional_bool(code_generation, "strings", true); + result.wasm_allowed = validate_optional_bool(code_generation, "wasm", true); + } + let microtask = get_field(options, "microtaskMode"); + if !JSValue::from_bits(microtask.to_bits()).is_undefined() { + if string_from_value(microtask).as_deref() != Some("afterEvaluate") { + let message = "The \"options.microtaskMode\" property must be 'afterEvaluate'"; + throw_invalid_arg_value(message); + } + result.microtask_after_evaluate = true; } - if object_ptr_from_value(value).is_none() { + result +} + +pub(crate) fn create_context(value: f64, options: f64) -> f64 { + context_from_arg(value, "object", context_options(options, "codeGeneration")) +} + +fn context_from_arg(value: f64, arg_name: &str, options: ContextOptions) -> f64 { + let jv = JSValue::from_bits(value.to_bits()); + let dont_contextify = is_dont_contextify(value); + let sandbox = if jv.is_undefined() || dont_contextify { + object_value(crate::object::js_object_alloc(0, 0)) + } else { + value + }; + if !value_is_object_like(sandbox) { let message = format!( "The \"{arg_name}\" argument must be of type object. Received {}", crate::fs::validate::describe_received(value) ); throw_invalid_arg(&message); } - mark_context(value); - value + let key = context_key(sandbox); + if !dont_contextify { + if let Some(existing) = VM_CONTEXTS.with(|contexts| contexts.borrow().get(&key).cloned()) { + return f64::from_bits(existing.returned_bits); + } + } + let state = new_context_state(sandbox, dont_contextify, options); + let returned = f64::from_bits(state.returned_bits); + VM_CONTEXTS.with(|contexts| { + contexts.borrow_mut().insert(context_key(returned), state); + }); + returned } -fn require_context(value: f64, arg_name: &str) -> f64 { - if is_context(value) { - value +fn is_context(value: f64) -> bool { + let key = context_key(value); + key != 0 && VM_CONTEXTS.with(|contexts| contexts.borrow().contains_key(&key)) +} + +fn require_context_state(value: f64, arg_name: &str) -> ContextState { + if let Some(state) = + VM_CONTEXTS.with(|contexts| contexts.borrow().get(&context_key(value)).cloned()) + { + state } else { let message = format!( "The \"{arg_name}\" argument must be an vm.Context. Received {}", @@ -1008,185 +1225,333 @@ fn require_context(value: f64, arg_name: &str) -> f64 { } } -fn script_source(script_value: f64) -> Option { +fn require_context(value: f64, arg_name: &str) -> f64 { + f64::from_bits(require_context_state(value, arg_name).returned_bits) +} + +fn main_context_state() -> ContextState { + if let Some(state) = MAIN_CONTEXT.with(|main| main.borrow().clone()) { + return state; + } + let global = crate::object::js_get_global_this(); + let state = ContextState { + returned_bits: global.to_bits(), + sandbox_bits: global.to_bits(), + global_this_bits: global.to_bits(), + intrinsics_bits: global.to_bits(), + lexical_env_bits: de::script_environment(global, &[]).to_bits(), + strings_allowed: true, + wasm_allowed: true, + microtask_after_evaluate: false, + }; + MAIN_CONTEXT.with(|main| *main.borrow_mut() = Some(state.clone())); + state +} + +fn execute_in_state(source: &str, state: &ContextState) -> f64 { + let result = de::eval_script_in_with_codegen( + source, + f64::from_bits(state.global_this_bits), + f64::from_bits(state.intrinsics_bits), + f64::from_bits(state.lexical_env_bits), + state.strings_allowed, + state.wasm_allowed, + ); + if state.microtask_after_evaluate { + crate::promise::js_promise_run_microtasks(); + } + result +} + +fn script_metadata(script_value: f64) -> Option { object_ptr_from_value(script_value) .and_then(|ptr| scripts().lock().unwrap().get(&(ptr as usize)).cloned()) - .map(|metadata| metadata.source) } fn install_script_method( obj: *mut ObjectHeader, - obj_value: f64, name: &str, func: extern "C" fn(*const ClosureHeader, f64, f64) -> f64, arity: u32, ) { - let key = field_key(name); + let scope = crate::gc::RuntimeHandleScope::new(); + let obj = scope.root_raw_mut_ptr(obj); + let key = scope.root_string_ptr(field_key(name)); let func_ptr = func as *const u8; crate::closure::js_register_closure_arity(func_ptr, 2); - let closure = crate::closure::js_closure_alloc(func_ptr, 1); - crate::closure::js_closure_set_capture_f64(closure, 0, obj_value); - crate::object::set_builtin_closure_length(closure as usize, arity); - let value = crate::value::js_nanbox_pointer(closure as i64); - crate::object::js_object_set_field_by_name(obj, key, value); + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + let closure = scope.root_raw_mut_ptr(closure); + crate::object::set_builtin_closure_length(hmut::(&closure) as usize, arity); + let value = crate::value::js_nanbox_pointer(hmut::(&closure) as i64); + crate::object::js_object_set_field_by_name( + hmut::(&obj), + hmut::(&key), + value, + ); crate::object::set_builtin_property_attrs( - obj as usize, + hmut::(&obj) as usize, name.to_string(), PropertyAttrs::new(true, false, true), ); } -fn make_script(code: String, options: f64) -> f64 { - let hash = source_hash(CACHE_KIND_SCRIPT, &code, &[]); - let cached_data = validate_cached_data_option(options); - let produce_cached_data = validate_produce_cached_data(options); - let source_map_url = extract_source_map_url(&code); - let obj = crate::object::js_object_alloc(0, 0); - let value = crate::value::js_nanbox_pointer(obj as i64); - scripts() - .lock() - .unwrap() - .insert(obj as usize, ScriptMetadata { source: code }); - if let Some(url) = source_map_url { - set_field(obj, "sourceMapURL", string_value(&url)); - } - if let Some(bytes) = cached_data { - set_field( - obj, - "cachedDataRejected", - bool_value(!cache_bytes_accepted(&bytes, CACHE_KIND_SCRIPT, hash)), - ); - } else if produce_cached_data { - set_field( - obj, - "cachedData", - cached_data_buffer(CACHE_KIND_SCRIPT, hash), - ); - set_field(obj, "cachedDataProduced", bool_value(true)); +fn script_receiver() -> f64 { + crate::object::js_implicit_this_get() +} + +pub(crate) fn install_script_prototypes(constructor: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let constructor = scope.root_nanbox_f64(constructor); + let Some(proto_value) = + crate::object::ordinary_function_prototype_value_for_read(constructor.get_nanbox_f64()) + else { + return; + }; + let Some(proto_ptr) = object_ptr_from_value(proto_value) else { + return; + }; + let proto = scope.root_raw_mut_ptr(proto_ptr); + let key = scope.root_nanbox_f64(string_value("runInThisContext")); + if JSValue::from_bits( + crate::object::js_object_has_own( + object_value(hmut::(&proto)), + key.get_nanbox_f64(), + ) + .to_bits(), + ) + .as_bool() + { + return; } + let old_parent = scope.root_nanbox_u64( + crate::object::prototype_chain::object_static_prototype( + hmut::(&proto) as usize + ) + .unwrap_or(JSValue::null().bits()), + ); + let base = crate::object::js_object_alloc(0, 0); + let base = scope.root_raw_mut_ptr(base); + crate::object::prototype_chain::object_set_static_prototype( + hmut::(&base) as usize, + old_parent.get_nanbox_u64(), + ); + crate::object::prototype_chain::object_set_static_prototype( + hmut::(&proto) as usize, + object_value(hmut::(&base)).to_bits(), + ); + set_field( + hmut::(&base), + "constructor", + constructor.get_nanbox_f64(), + ); + crate::object::set_builtin_property_attrs( + hmut::(&base) as usize, + "constructor".to_string(), + PropertyAttrs::new(true, false, true), + ); install_script_method( - obj, - value, + hmut::(&proto), "runInThisContext", vm_script_run_in_this_context_method, 1, ); install_script_method( - obj, - value, + hmut::(&proto), "runInContext", vm_script_run_in_context_method, 2, ); install_script_method( - obj, - value, + hmut::(&proto), "runInNewContext", vm_script_run_in_new_context_method, 2, ); install_script_method( - obj, - value, + hmut::(&base), + "runInContext", + vm_script_run_in_context_method, + 2, + ); + install_script_method( + hmut::(&base), "createCachedData", vm_script_create_cached_data_method, 0, ); - value + crate::object::set_builtin_property_attrs( + hmut::(&base) as usize, + "createCachedData".to_string(), + PropertyAttrs::new(true, true, true), + ); +} + +fn make_script(code: String, options: f64) -> f64 { + let source_options = source_options(options, true); + with_source_location(&source_options, || de::validate_script_source(&code)); + let hash = source_hash(CACHE_KIND_SCRIPT, &code, &[]); + let cached_data = validate_cached_data_option(options); + let produce_cached_data = validate_produce_cached_data(options); + let source_map_url = extract_source_map_url(&code); + let scope = crate::gc::RuntimeHandleScope::new(); + let obj = scope.root_raw_mut_ptr(crate::object::js_object_alloc(0, 0)); + scripts().lock().unwrap().insert( + hmut::(&obj) as usize, + ScriptMetadata { + source: code, + filename: source_options.filename, + line_offset: source_options.line_offset, + column_offset: source_options.column_offset, + }, + ); + if let Some(url) = source_map_url { + set_field( + hmut::(&obj), + "sourceMapURL", + string_value(&url), + ); + } + if let Some(bytes) = cached_data { + set_field( + hmut::(&obj), + "cachedDataRejected", + bool_value(!cache_bytes_accepted(&bytes, CACHE_KIND_SCRIPT, hash)), + ); + } else if produce_cached_data { + set_field( + hmut::(&obj), + "cachedData", + cached_data_buffer(CACHE_KIND_SCRIPT, hash), + ); + set_field( + hmut::(&obj), + "cachedDataProduced", + bool_value(true), + ); + } + object_value(hmut::(&obj)) } extern "C" fn vm_script_create_cached_data_method( - closure: *const ClosureHeader, + _closure: *const ClosureHeader, _unused1: f64, _unused2: f64, ) -> f64 { - let script = crate::closure::js_closure_get_capture_f64(closure, 0); - let Some(source) = script_source(script) else { + let script = script_receiver(); + let Some(metadata) = script_metadata(script) else { return cached_data_buffer(CACHE_KIND_SCRIPT, 0); }; cached_data_buffer( CACHE_KIND_SCRIPT, - source_hash(CACHE_KIND_SCRIPT, &source, &[]), + source_hash(CACHE_KIND_SCRIPT, &metadata.source, &[]), ) } extern "C" fn vm_script_run_in_this_context_method( - closure: *const ClosureHeader, + _closure: *const ClosureHeader, _options: f64, _unused: f64, ) -> f64 { - let script = crate::closure::js_closure_get_capture_f64(closure, 0); - let Some(source) = script_source(script) else { + let script = script_receiver(); + let Some(metadata) = script_metadata(script) else { return undefined_value(); }; - run_source(&source, crate::object::js_get_global_this(), HashMap::new()) + validate_run_options(_options); + let options = SourceOptions { + filename: metadata.filename, + line_offset: metadata.line_offset, + column_offset: metadata.column_offset, + }; + with_source_location(&options, || { + execute_in_state(&metadata.source, &main_context_state()) + }) } extern "C" fn vm_script_run_in_context_method( - closure: *const ClosureHeader, + _closure: *const ClosureHeader, contextified_object: f64, _options: f64, ) -> f64 { - let script = crate::closure::js_closure_get_capture_f64(closure, 0); - let Some(source) = script_source(script) else { + let script = script_receiver(); + let Some(metadata) = script_metadata(script) else { return undefined_value(); }; - let context = require_context(contextified_object, "contextifiedObject"); - run_source(&source, context, HashMap::new()) + validate_run_options(_options); + let context = require_context_state(contextified_object, "contextifiedObject"); + let options = SourceOptions { + filename: metadata.filename, + line_offset: metadata.line_offset, + column_offset: metadata.column_offset, + }; + with_source_location(&options, || execute_in_state(&metadata.source, &context)) } extern "C" fn vm_script_run_in_new_context_method( - closure: *const ClosureHeader, + _closure: *const ClosureHeader, context_object: f64, - _options: f64, + options: f64, ) -> f64 { - let script = crate::closure::js_closure_get_capture_f64(closure, 0); - let Some(source) = script_source(script) else { + let script = script_receiver(); + let Some(metadata) = script_metadata(script) else { return undefined_value(); }; - let context = context_from_arg(context_object, "object"); - run_source(&source, context, HashMap::new()) -} - -extern "C" fn vm_compiled_function_call(closure: *const ClosureHeader, rest: f64) -> f64 { - let key = closure as usize; - let Some(compiled) = functions().lock().unwrap().get(&key).cloned() else { - return undefined_value(); + validate_run_options(options); + let context = context_from_arg( + context_object, + "object", + context_options(options, "contextCodeGeneration"), + ); + let context = require_context_state(context, "contextifiedObject"); + let source_options = SourceOptions { + filename: metadata.filename, + line_offset: metadata.line_offset, + column_offset: metadata.column_offset, }; - let mut params = HashMap::new(); - let rest_arr = array_ptr_from_value(rest); - for (idx, name) in compiled.params.iter().enumerate() { - let value = rest_arr - .map(|arr| crate::array::js_array_get_f64(arr, idx as u32)) - .unwrap_or_else(undefined_value); - params.insert(name.clone(), value); - } - let target = f64::from_bits(compiled.context_bits); - run_source(&compiled.body, target, params) + with_source_location(&source_options, || { + execute_in_state(&metadata.source, &context) + }) } pub extern "C" fn js_vm_create_script(code: f64, options: f64) -> f64 { make_script(code_string_for_script(code), options) } -pub extern "C" fn js_vm_run_in_context(code: f64, contextified_object: f64, _options: f64) -> f64 { - let code = code_string_required(code, "code"); - let context = require_context(contextified_object, "contextifiedObject"); - run_source(&code, context, HashMap::new()) +pub extern "C" fn js_vm_run_in_context(code: f64, contextified_object: f64, options: f64) -> f64 { + let code = code_string_for_script(code); + let source_options = source_options(options, true); + let context = require_context_state(contextified_object, "contextifiedObject"); + with_source_location(&source_options, || execute_in_state(&code, &context)) } -pub extern "C" fn js_vm_run_in_new_context(code: f64, context_object: f64, _options: f64) -> f64 { - let code = code_string_required(code, "code"); - let context = context_from_arg(context_object, "object"); - run_source(&code, context, HashMap::new()) +pub extern "C" fn js_vm_run_in_new_context(code: f64, context_object: f64, options: f64) -> f64 { + let code = code_string_for_script(code); + let source_options = source_options(options, true); + let context_options = if string_from_value(options).is_some() { + ContextOptions::default() + } else { + context_options(options, "contextCodeGeneration") + }; + let context = context_from_arg(context_object, "object", context_options); + let context = require_context_state(context, "contextifiedObject"); + with_source_location(&source_options, || execute_in_state(&code, &context)) } -pub extern "C" fn js_vm_run_in_this_context(code: f64, _options: f64) -> f64 { - let code = code_string_required(code, "code"); - run_source(&code, crate::object::js_get_global_this(), HashMap::new()) +pub extern "C" fn js_vm_run_in_this_context(code: f64, options: f64) -> f64 { + let code = code_string_for_script(code); + let source_options = source_options(options, true); + with_source_location(&source_options, || { + execute_in_state(&code, &main_context_state()) + }) } pub extern "C" fn js_vm_is_context(object: f64) -> f64 { + if !value_is_object_like(object) { + let message = format!( + "The \"object\" argument must be of type object. Received {}", + crate::fs::validate::describe_received(object) + ); + throw_invalid_arg(&message); + } bool_value(is_context(object)) } @@ -1224,21 +1589,42 @@ fn compile_params(params: f64) -> Vec { out } -fn parsing_context_from_options(options: f64) -> f64 { - let jv = JSValue::from_bits(options.to_bits()); - if jv.is_undefined() || jv.is_null() { - return crate::object::js_get_global_this(); - } - let Some(_opts) = object_ptr_from_value(options) else { - return crate::object::js_get_global_this(); +fn compile_options(options: f64) -> (ContextState, Vec, SourceOptions) { + let options = options_object_or_default(options); + let Some(options) = options else { + return (main_context_state(), Vec::new(), SourceOptions::default()); }; - let parsing = get_object_field(options, "parsingContext"); + let source_options = source_options(object_value(options), false); + + let parsing = get_field(options, "parsingContext"); let pv = JSValue::from_bits(parsing.to_bits()); - if pv.is_undefined() { - crate::object::js_get_global_this() + let context = if pv.is_undefined() { + main_context_state() } else { - require_context(parsing, "options.parsingContext") + require_context_state(parsing, "options.parsingContext") + }; + + let extensions = get_field(options, "contextExtensions"); + if JSValue::from_bits(extensions.to_bits()).is_undefined() { + return (context, Vec::new(), source_options); } + let Some(extensions) = array_ptr_from_value(extensions) else { + let message = format!( + "The \"options.contextExtensions\" property must be an instance of Array. Received {}", + crate::fs::validate::describe_received(extensions) + ); + throw_invalid_arg(&message); + }; + let mut values = Vec::with_capacity(crate::array::js_array_length(extensions) as usize); + for index in 0..crate::array::js_array_length(extensions) { + let extension = crate::array::js_array_get_f64(extensions, index); + crate::validators::validate_object( + extension, + &format!("options.contextExtensions[{index}]"), + ); + values.push(extension); + } + (context, values, source_options) } pub extern "C" fn js_vm_compile_function(code: f64, params: f64, options: f64) -> f64 { @@ -1247,35 +1633,45 @@ pub extern "C" fn js_vm_compile_function(code: f64, params: f64, options: f64) - let hash = source_hash(CACHE_KIND_FUNCTION, &body, ¶ms); let cached_data = validate_cached_data_option(options); let produce_cached_data = validate_produce_cached_data(options); - let context = parsing_context_from_options(options); - let func_ptr = vm_compiled_function_call as *const u8; - crate::closure::js_register_closure_rest(func_ptr, 0); - let closure = crate::closure::js_closure_alloc(func_ptr, 0); - crate::object::set_builtin_closure_length(closure as usize, params.len() as u32); - functions().lock().unwrap().insert( - closure as usize, - CompiledFunction { - body, - params, - context_bits: context.to_bits(), - }, + let (context, extensions, source_options) = compile_options(options); + let mut source = params.clone(); + source.push(body.clone()); + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(with_source_location(&source_options, || { + de::function_from_strings_in_with_codegen( + &source, + f64::from_bits(context.global_this_bits), + f64::from_bits(context.intrinsics_bits), + &extensions, + context.strings_allowed, + context.wasm_allowed, + ) + })); + let closure = crate::value::js_nanbox_get_pointer(value.get_nanbox_f64()) as usize; + crate::object::set_builtin_closure_length(closure, params.len() as u32); + compiled_function_sources().lock().unwrap().insert( + crate::value::js_nanbox_get_pointer(value.get_nanbox_f64()) as usize, + format!("function ({}) {{\n{}\n}}", params.join(", "), body), ); - let value = crate::value::js_nanbox_pointer(closure as i64); if let Some(bytes) = cached_data { set_value_field( - value, + value.get_nanbox_f64(), "cachedDataRejected", bool_value(!cache_bytes_accepted(&bytes, CACHE_KIND_FUNCTION, hash)), ); } else if produce_cached_data { set_value_field( - value, + value.get_nanbox_f64(), "cachedData", cached_data_buffer(CACHE_KIND_FUNCTION, hash), ); - set_value_field(value, "cachedDataProduced", bool_value(true)); + set_value_field( + value.get_nanbox_f64(), + "cachedDataProduced", + bool_value(true), + ); } - value + value.get_nanbox_f64() } fn memory_range_value(estimate: f64) -> f64 { @@ -1354,22 +1750,28 @@ pub extern "C" fn js_vm_script_call(_code: f64, _options: f64) -> f64 { } pub fn scan_vm_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - if let Some(contexts) = VM_CONTEXTS.get() { - let mut guard = contexts.lock().unwrap(); - let mut rewrites = Vec::new(); - for old in guard.iter().copied().collect::>() { - let mut new = old; - if visitor.visit_metadata_usize_slot(&mut new) && new != old { - rewrites.push((old, new)); - } + VM_CONTEXTS.with(|contexts| { + let mut contexts = contexts.borrow_mut(); + let mut rebuilt = HashMap::with_capacity(contexts.len()); + for (_, mut state) in contexts.drain() { + visitor.visit_nanbox_u64_slot(&mut state.returned_bits); + visitor.visit_nanbox_u64_slot(&mut state.sandbox_bits); + visitor.visit_nanbox_u64_slot(&mut state.global_this_bits); + visitor.visit_nanbox_u64_slot(&mut state.intrinsics_bits); + visitor.visit_nanbox_u64_slot(&mut state.lexical_env_bits); + rebuilt.insert(context_key(f64::from_bits(state.returned_bits)), state); } - for (old, new) in rewrites { - guard.remove(&old); - if new != 0 { - guard.insert(new); - } + *contexts = rebuilt; + }); + MAIN_CONTEXT.with(|main| { + if let Some(state) = main.borrow_mut().as_mut() { + visitor.visit_nanbox_u64_slot(&mut state.returned_bits); + visitor.visit_nanbox_u64_slot(&mut state.sandbox_bits); + visitor.visit_nanbox_u64_slot(&mut state.global_this_bits); + visitor.visit_nanbox_u64_slot(&mut state.intrinsics_bits); + visitor.visit_nanbox_u64_slot(&mut state.lexical_env_bits); } - } + }); if let Some(scripts) = VM_SCRIPTS.get() { let mut guard = scripts.lock().unwrap(); let mut rewrites = Vec::new(); @@ -1387,8 +1789,8 @@ pub fn scan_vm_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { } } } - if let Some(functions) = VM_FUNCTIONS.get() { - let mut guard = functions.lock().unwrap(); + if let Some(sources) = VM_COMPILED_FUNCTION_SOURCES.get() { + let mut guard = sources.lock().unwrap(); let mut rewrites = Vec::new(); for old in guard.keys().copied().collect::>() { let mut new = old; @@ -1396,39 +1798,32 @@ pub fn scan_vm_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { rewrites.push((old, new)); } } - for compiled in guard.values_mut() { - visitor.visit_nanbox_u64_slot(&mut compiled.context_bits); - } for (old, new) in rewrites { - if let Some(compiled) = guard.remove(&old) { + if let Some(source) = guard.remove(&old) { if new != 0 { - guard.insert(new, compiled); + guard.insert(new, source); } } } } } -/// Death pruning (2026-07-09 GC audit wave 2): `VM_CONTEXTS` / `VM_SCRIPTS` -/// / `VM_FUNCTIONS` retained one entry — including the FULL SOURCE TEXT for -/// scripts/functions — per `node:vm` API call forever (move-rekey only, no -/// death hook on the owning objects). Prune entries whose owner object is -/// provably dead. `is_dead_owner` is one of the GC's deadness predicates -/// (`gc::dead_owner`); the tables are process-global, so foreign threads' -/// owners don't attribute and are skipped (documented residual). +/// Prune VM metadata whose owner is provably dead. `VM_CONTEXTS` is local to +/// the calling thread; process-global script/source entries owned by another +/// thread are retained because their deadness cannot be attributed here. pub(crate) fn prune_dead_vm_owner_entries(is_dead_owner: &dyn Fn(usize) -> bool) { - if let Some(contexts) = VM_CONTEXTS.get() { - if let Ok(mut guard) = contexts.lock() { - guard.retain(|owner| !is_dead_owner(*owner)); - } - } + VM_CONTEXTS.with(|contexts| { + contexts + .borrow_mut() + .retain(|owner, _| !is_dead_owner(*owner)); + }); if let Some(scripts) = VM_SCRIPTS.get() { if let Ok(mut guard) = scripts.lock() { guard.retain(|owner, _| !is_dead_owner(*owner)); } } - if let Some(functions) = VM_FUNCTIONS.get() { - if let Ok(mut guard) = functions.lock() { + if let Some(sources) = VM_COMPILED_FUNCTION_SOURCES.get() { + if let Ok(mut guard) = sources.lock() { guard.retain(|owner, _| !is_dead_owner(*owner)); } } @@ -1440,6 +1835,9 @@ pub(crate) fn test_seed_vm_script_entry(owner: usize, source: &str) { owner, ScriptMetadata { source: source.to_string(), + filename: "evalmachine.".to_string(), + line_offset: 0, + column_offset: 0, }, ); } @@ -1449,257 +1847,21 @@ pub(crate) fn test_vm_script_entry_exists(owner: usize) -> bool { scripts().lock().unwrap().contains_key(&owner) } -pub extern "C" fn js_vm_module_call() -> f64 { - throw_type_error_no_code("Class constructor Module cannot be invoked without 'new'") -} - -#[no_mangle] -pub extern "C" fn js_vm_module_constructor_error() -> f64 { - throw_type_error_no_code("Module is not a constructor") -} - -pub extern "C" fn js_vm_source_text_module_new(code: f64, options: f64) -> f64 { - if !vm_modules_enabled() { - return throw_vm_unimplemented("SourceTextModule experimental gate", "3132"); - } - let Some(source) = string_from_value(code) else { - return throw_vm_type("SourceTextModule source must be a string"); - }; - let hash = source_hash(CACHE_KIND_MODULE, &source, &[]); - if let Some(bytes) = validate_cached_data_option(options) { - if !cache_bytes_accepted(&bytes, CACHE_KIND_MODULE, hash) { - return throw_vm_module_cached_data_rejected(); - } - } - let parsed = parse_source(&source); - let identifier = options_identifier(options).unwrap_or_else(default_identifier); - let module = new_module_base(KIND_SOURCE, STATUS_UNLINKED, identifier); - let namespace = crate::object::js_object_alloc_null_proto(0, parsed.exports.len() as u32); - for export in &parsed.exports { - set_field(namespace, &export.name, undefined_value()); - } - set_field(module, FIELD_NAMESPACE, object_value(namespace)); - set_field(module, "namespace", object_value(namespace)); - set_field(module, FIELD_SOURCE, string_value(&source)); - set_field(module, FIELD_REQUESTS, requests_array(&parsed.requests)); - set_field(module, FIELD_IMPORTS, imports_array(&parsed.imports)); - set_field(module, FIELD_EXPORTS, exports_array(&parsed.exports)); - object_value(module) -} - -pub extern "C" fn js_vm_synthetic_module_new( - export_names_value: f64, - evaluate_callback: f64, - options: f64, -) -> f64 { - if !vm_modules_enabled() { - return throw_vm_unimplemented("SyntheticModule experimental gate", "3133"); - } - let Some(export_names) = array_ptr_from_value(export_names_value) else { - return throw_vm_type("SyntheticModule exportNames must be an array"); - }; - let identifier = options_identifier(options).unwrap_or_else(default_identifier); - let module = new_module_base(KIND_SYNTHETIC, STATUS_LINKED, identifier); - let namespace = crate::object::js_object_alloc_null_proto(0, 0); - let len = crate::array::js_array_length(export_names); - let mut exports = Vec::new(); - for idx in 0..len { - let value = crate::array::js_array_get_f64(export_names, idx); - if let Some(name) = string_from_value(value) { - exports.push(ExportBinding { - name: name.clone(), - expr: String::new(), - }); - set_field(namespace, &name, undefined_value()); - } - } - set_field(module, FIELD_NAMESPACE, object_value(namespace)); - set_field(module, "namespace", object_value(namespace)); - set_field(module, FIELD_REQUESTS, requests_array(&[])); - set_field(module, FIELD_IMPORTS, imports_array(&[])); - set_field(module, FIELD_EXPORTS, exports_array(&exports)); - set_field(module, FIELD_EVALUATE_CALLBACK, evaluate_callback); - object_value(module) -} - -pub extern "C" fn js_vm_module_status(module_value: f64) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return undefined_value(); - }; - string_value(&module_status(module)) -} - -pub extern "C" fn js_vm_module_identifier(module_value: f64) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return undefined_value(); - }; - get_field(module, FIELD_IDENTIFIER) -} - -pub extern "C" fn js_vm_module_error(module_value: f64) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return undefined_value(); - }; - if module_status(module) != STATUS_ERRORED { - return throw_vm_status("Module status must be errored"); - } - get_field(module, FIELD_ERROR) -} - -pub extern "C" fn js_vm_module_namespace(module_value: f64) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return undefined_value(); - }; - if module_kind(module) == KIND_SOURCE && module_status(module) == STATUS_UNLINKED { - return throw_vm_status("Module status must be linked"); - } - get_field(module, FIELD_NAMESPACE) -} - -pub extern "C" fn js_vm_module_link(module_value: f64, linker: f64) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return undefined_value(); - }; - if module_kind(module) == KIND_SYNTHETIC { - set_status(module, STATUS_LINKED); - return undefined_value(); - } - if module_status(module) != STATUS_UNLINKED { - return undefined_value(); - } - - set_status(module, STATUS_LINKING); - let requests = read_requests(module); - let mut linked = crate::array::js_array_alloc(requests.len() as u32); - for specifier in &requests { - let args = [ - string_value(specifier), - module_value, - module_request_extra(), - ]; - let dep = - unsafe { crate::closure::js_native_call_value(linker, args.as_ptr(), args.len()) }; - linked = crate::array::js_array_push_f64(linked, dep); - } - set_field(module, FIELD_LINKED_MODULES, array_value(linked)); - set_status(module, STATUS_LINKED); - undefined_value() -} - -pub extern "C" fn js_vm_module_evaluate(module_value: f64, _options: f64) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return undefined_value(); - }; - match module_kind(module).as_str() { - KIND_SOURCE => evaluate_source_module(module), - KIND_SYNTHETIC => evaluate_synthetic_module(module), - _ => undefined_value(), - } -} - -pub extern "C" fn js_vm_source_text_module_dependency_specifiers(module_value: f64) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return array_value(crate::array::js_array_alloc(0)); - }; - strings_array(&read_requests(module)) -} - -pub extern "C" fn js_vm_source_text_module_module_requests(module_value: f64) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return array_value(crate::array::js_array_alloc(0)); - }; - let requests = read_requests(module); - requests_array(&requests) -} - -pub extern "C" fn js_vm_source_text_module_create_cached_data(module_value: f64) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return cached_data_buffer(CACHE_KIND_MODULE, 0); - }; - if module_status(module) == STATUS_EVALUATED { - return throw_vm_module_cannot_create_cached_data(); - } - let source = get_string_field(module, FIELD_SOURCE).unwrap_or_default(); - cached_data_buffer( - CACHE_KIND_MODULE, - source_hash(CACHE_KIND_MODULE, &source, &[]), - ) -} - -pub extern "C" fn js_vm_source_text_module_link_requests( - module_value: f64, - modules_value: f64, -) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return undefined_value(); - }; - let Some(modules) = array_ptr_from_value(modules_value) else { - return throw_vm_type("linkRequests modules must be an array"); - }; - set_field(module, FIELD_LINKED_MODULES, array_value(modules)); - undefined_value() -} - -pub extern "C" fn js_vm_source_text_module_instantiate(module_value: f64) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return undefined_value(); - }; - if module_status(module) == STATUS_UNLINKED { - set_status(module, STATUS_LINKED); - } - undefined_value() -} - -pub extern "C" fn js_vm_source_text_module_has_top_level_await(module_value: f64) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return bool_value(false); - }; - bool_value(module_has_tla(module)) -} - -pub extern "C" fn js_vm_source_text_module_has_async_graph(module_value: f64) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return bool_value(false); - }; - if module_status(module) == STATUS_UNLINKED { - return throw_vm_status("Module status must be instantiated"); - } - bool_value(module_has_async_graph(module)) -} - -pub extern "C" fn js_vm_synthetic_module_set_export( - module_value: f64, - name_value: f64, - value: f64, -) -> f64 { - let Some(module) = object_ptr_from_value(module_value) else { - return undefined_value(); - }; - let Some(name) = string_from_value(name_value) else { - return throw_vm_type("SyntheticModule export name must be a string"); - }; - let exports = read_exports(module); - if !exports.iter().any(|export| export.name == name) { - return throw_reference_error_no_code(&format!("Export '{name}' is not defined in module")); - } - let Some(namespace) = namespace_for_module(module) else { - return throw_vm_status("SyntheticModule namespace is unavailable"); - }; - set_field(namespace, &name, value); - undefined_value() -} - -/// Dispatch a `node:vm` module method reached as a value/namespace call. -/// `createContext` routes to the working #4050 contextification helper; the -/// remaining entries live in this VM scaffold/lifecycle module. +/// Dispatch a `node:vm` method reached as a value/namespace call. +/// `createContext` routes to the working contextification helper; module +/// lifecycle entries live in `modules.rs`. pub fn dispatch_vm_method(method: &str, arg0: f64, arg1: f64, arg2: f64) -> f64 { match method { "Script" => js_vm_script_call(arg0, arg1), "Module" => js_vm_module_call(), - "SourceTextModule" => js_vm_source_text_module_new(arg0, arg1), - "SyntheticModule" => js_vm_synthetic_module_new(arg0, arg1, arg2), - "createContext" => create_context(arg0), - "createScript" => js_vm_create_script(arg0, arg1), + "SourceTextModule" => throw_type_error_no_code( + "Class constructor SourceTextModule cannot be invoked without 'new'", + ), + "SyntheticModule" => throw_type_error_no_code( + "Class constructor SyntheticModule cannot be invoked without 'new'", + ), + "createContext" => create_context(arg0, arg1), + "createScript" => crate::object::brand_vm_script_instance(js_vm_create_script(arg0, arg1)), "runInContext" => js_vm_run_in_context(arg0, arg1, arg2), "runInNewContext" => js_vm_run_in_new_context(arg0, arg1, arg2), "runInThisContext" => js_vm_run_in_this_context(arg0, arg1), @@ -1723,3 +1885,26 @@ pub fn dispatch_vm_method(method: &str, arg0: f64, arg1: f64, arg2: f64) -> f64 _ => undefined_value(), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn context_values_keep_vm_realm_prototypes() { + let _ = crate::object::js_get_global_this(); + let outer_prototype = crate::object::builtin_prototype_value("Object"); + let sandbox = object_value(crate::object::js_object_alloc(0, 0)); + let state = new_context_state(sandbox, false, ContextOptions::default()); + let value = execute_in_state("({ value: 1 })", &state); + assert_ne!( + crate::object::js_object_get_prototype_of(value).to_bits(), + outer_prototype.to_bits(), + ); + let raw = f64::from_bits(crate::value::js_nanbox_get_pointer(value) as u64); + assert_ne!( + crate::object::js_object_get_prototype_of(raw).to_bits(), + outer_prototype.to_bits(), + ); + } +} diff --git a/crates/perry-runtime/src/node_vm/eval.rs b/crates/perry-runtime/src/node_vm/eval.rs deleted file mode 100644 index da25adc5f6..0000000000 --- a/crates/perry-runtime/src/node_vm/eval.rs +++ /dev/null @@ -1,552 +0,0 @@ -//! The deterministic mini-interpreter behind `vm.Script` / `runIn*Context` / -//! `compileFunction`: top-level statement splitting, a small expression -//! evaluator (logical/equality/arithmetic operators, `typeof`, literals, -//! property paths), and reference reads/writes against the sandbox target. -//! Perry is V8-free — this models only the local subsets the VM parity -//! fixtures exercise. Extracted from `node_vm.rs` to keep that file under -//! the 2000-line cap; behavior is unchanged. - -use super::*; - -fn split_top_level(input: &str, delimiter: char) -> Vec<&str> { - let mut out = Vec::new(); - let mut start = 0; - let mut depth = 0_i32; - let mut quote = None::; - let mut escape = false; - for (idx, ch) in input.char_indices() { - if let Some(q) = quote { - if escape { - escape = false; - } else if ch == '\\' { - escape = true; - } else if ch == q { - quote = None; - } - continue; - } - match ch { - '\'' | '"' | '`' => quote = Some(ch), - '(' | '[' | '{' => depth += 1, - ')' | ']' | '}' => depth -= 1, - _ if ch == delimiter && depth == 0 => { - out.push(input[start..idx].trim()); - start = idx + ch.len_utf8(); - } - _ => {} - } - } - out.push(input[start..].trim()); - out -} - -fn strip_wrapping_parens(mut s: &str) -> &str { - loop { - let t = s.trim(); - if !(t.starts_with('(') && t.ends_with(')')) { - return t; - } - let mut depth = 0_i32; - let mut quote = None::; - let mut escape = false; - let mut wraps = true; - for (idx, ch) in t.char_indices() { - if let Some(q) = quote { - if escape { - escape = false; - } else if ch == '\\' { - escape = true; - } else if ch == q { - quote = None; - } - continue; - } - match ch { - '\'' | '"' | '`' => quote = Some(ch), - '(' => depth += 1, - ')' => { - depth -= 1; - if depth == 0 && idx != t.len() - 1 { - wraps = false; - break; - } - } - _ => {} - } - } - if !wraps { - return t; - } - s = &t[1..t.len() - 1]; - } -} - -fn find_top_level_operator(input: &str, op: &str) -> Option { - let mut depth = 0_i32; - let mut quote = None::; - let mut escape = false; - let mut found = None; - for (idx, ch) in input.char_indices() { - if let Some(q) = quote { - if escape { - escape = false; - } else if ch == '\\' { - escape = true; - } else if ch == q { - quote = None; - } - continue; - } - match ch { - '\'' | '"' | '`' => quote = Some(ch), - '(' | '[' | '{' => depth += 1, - ')' | ']' | '}' => depth -= 1, - _ if depth == 0 && input[idx..].starts_with(op) => found = Some(idx), - _ => {} - } - } - found -} - -fn unquote(s: &str) -> Option { - let bytes = s.as_bytes(); - if bytes.len() < 2 { - return None; - } - let q = bytes[0] as char; - if !matches!(q, '\'' | '"' | '`') || bytes[bytes.len() - 1] as char != q { - return None; - } - let inner = &s[1..s.len() - 1]; - Some( - inner - .replace("\\n", "\n") - .replace("\\t", "\t") - .replace("\\\"", "\"") - .replace("\\'", "'") - .replace("\\\\", "\\"), - ) -} - -fn value_to_number(value: f64) -> f64 { - let jv = JSValue::from_bits(value.to_bits()); - if jv.is_int32() { - jv.as_int32() as f64 - } else if jv.is_number() { - jv.as_number() - } else if jv.is_bool() { - if jv.as_bool() { - 1.0 - } else { - 0.0 - } - } else if jv.is_null() { - 0.0 - } else { - f64::NAN - } -} - -fn coerce_to_string(value: f64) -> String { - let ptr = crate::value::js_jsvalue_to_string(value) as *const StringHeader; - rust_string_from_header(ptr).unwrap_or_default() -} - -fn add_values(a: f64, b: f64) -> f64 { - let aj = JSValue::from_bits(a.to_bits()); - let bj = JSValue::from_bits(b.to_bits()); - if aj.is_any_string() || bj.is_any_string() { - return string_value(&format!("{}{}", coerce_to_string(a), coerce_to_string(b))); - } - number_value(value_to_number(a) + value_to_number(b)) -} - -fn value_same(a: f64, b: f64) -> bool { - crate::value::js_jsvalue_equals(a, b) != 0 -} - -fn get_reference(name: &str, env: &EvalEnv) -> f64 { - match name { - "undefined" => undefined_value(), - "null" => f64::from_bits(JSValue::null().bits()), - "true" => bool_value(true), - "false" => bool_value(false), - "globalThis" | "this" => env.target, - _ => env - .params - .get(name) - .copied() - .unwrap_or_else(|| get_object_field(env.target, name)), - } -} - -fn eval_property_path(expr: &str, env: &EvalEnv) -> Option { - let expr = expr.trim(); - // Peel a trailing computed accessor (`a.b["k"]`) and read through it after - // resolving the receiver expression. Recurses so chained accessors work. - if let Some((object_expr, accessor)) = split_trailing_computed(expr) { - let object = eval_property_path(object_expr, env)?; - let key = computed_key_name(accessor, env)?; - return Some(get_object_field(object, &key)); - } - let mut parts = expr.split('.'); - let first = parts.next()?.trim(); - if first.is_empty() { - return None; - } - let mut value = get_reference(first, env); - for part in parts { - let name = part.trim(); - if name.is_empty() { - return None; - } - value = get_object_field(value, name); - } - Some(value) -} - -/// Split a trailing computed-member accessor off a member-access expression, -/// e.g. `globalThis.M["/a/b"]` -> (`globalThis.M`, `["/a/b"]`). Returns `None` -/// when the expression does not end in a top-level `[...]` accessor. -fn split_trailing_computed(expr: &str) -> Option<(&str, &str)> { - let trimmed = expr.trim_end(); - if !trimmed.ends_with(']') { - return None; - } - let bytes = trimmed.as_bytes(); - let mut depth = 0_i32; - let mut quote = None::; - let mut open = None; - for idx in (0..bytes.len()).rev() { - let ch = bytes[idx]; - if let Some(q) = quote { - // Walking backwards through a quoted span: a quote char that is not - // backslash-escaped closes (opens, in reverse) the span. - if ch == q && (idx == 0 || bytes[idx - 1] != b'\\') { - quote = None; - } - continue; - } - match ch { - b'\'' | b'"' | b'`' => quote = Some(ch), - b']' => depth += 1, - b'[' => { - depth -= 1; - if depth == 0 { - open = Some(idx); - break; - } - } - _ => {} - } - } - let open = open?; - if open == 0 { - return None; - } - Some((trimmed[..open].trim(), &trimmed[open..])) -} - -/// Evaluate the key inside a `[...]` accessor to a property name string. -fn computed_key_name(accessor: &str, env: &EvalEnv) -> Option { - let inner = accessor.trim(); - let inner = inner.strip_prefix('[')?.strip_suffix(']')?.trim(); - let value = eval_expr(inner, env); - Some(coerce_to_string(value)) -} - -fn set_reference(lhs: &str, value: f64, env: &mut EvalEnv) { - let lhs = lhs.trim(); - if let Some((object_expr, accessor)) = split_trailing_computed(lhs) { - if let (Some(object), Some(key)) = ( - eval_property_path(object_expr, env), - computed_key_name(accessor, env), - ) { - set_object_field(object, &key, value); - } - return; - } - if let Some((head, tail)) = lhs.rsplit_once('.') { - if let Some(object) = eval_property_path(head, env) { - set_object_field(object, tail.trim(), value); - } - return; - } - if env.params.contains_key(lhs) { - env.params.insert(lhs.to_string(), value); - } else { - set_object_field(env.target, lhs, value); - } -} - -/// Rewrite a JS object/array literal into strict JSON so the runtime JSON -/// parser can build it. Next.js serializes the RSC manifest payload with -/// `JSON.stringify` (already strict JSON), but the broader contract accepts -/// plain object literals too, so we quote bare identifier keys (`{a:1}` -> -/// `{"a":1}`) and normalize single-quoted strings to double-quoted. Returns -/// `None` if the text is not a well-formed literal we can normalize. -fn normalize_literal_to_json(expr: &str) -> Option { - let bytes = expr.as_bytes(); - let mut out = String::with_capacity(expr.len() + 8); - let mut i = 0; - // Tracks whether the next bare-identifier run is in a key position (right - // after `{` or a `,` while inside an object). The brace stack records the - // kind of each open container: `true` = object, `false` = array. - let mut object_stack: Vec = Vec::new(); - let mut expect_key = false; - while i < bytes.len() { - let ch = bytes[i]; - match ch { - b' ' | b'\t' | b'\n' | b'\r' => { - out.push(ch as char); - i += 1; - } - b'"' | b'\'' => { - // Copy a quoted string, re-emitting as a double-quoted JSON - // string. Track escapes so a quote inside the string doesn't end - // it early. - let quote = ch; - out.push('"'); - i += 1; - while i < bytes.len() { - let c = bytes[i]; - if c == b'\\' && i + 1 < bytes.len() { - // `\'` is valid inside a JS single-quoted string but is - // NOT a legal JSON escape, so it would make an otherwise - // valid literal like `{name: 'can\'t'}` fail JSON - // parsing. Emit a plain apostrophe; pass every - // JSON-valid escape (\\, \", \n, …) through unchanged. - if bytes[i + 1] == b'\'' { - out.push('\''); - } else { - out.push('\\'); - out.push(bytes[i + 1] as char); - } - i += 2; - continue; - } - if c == quote { - i += 1; - break; - } - if c == b'"' { - out.push('\\'); - } - out.push(c as char); - i += 1; - } - out.push('"'); - expect_key = false; - } - b'{' => { - out.push('{'); - object_stack.push(true); - expect_key = true; - i += 1; - } - b'[' => { - out.push('['); - object_stack.push(false); - expect_key = false; - i += 1; - } - b'}' | b']' => { - out.push(ch as char); - object_stack.pop(); - expect_key = false; - i += 1; - } - b',' => { - out.push(','); - expect_key = object_stack.last().copied().unwrap_or(false); - i += 1; - } - b':' => { - out.push(':'); - expect_key = false; - i += 1; - } - c if c == b'_' || c == b'$' || c.is_ascii_alphabetic() => { - // Bare identifier run. In key position, JSON-quote it. As a value - // it can only be a literal keyword (true/false/null); anything - // else (a context reference) is outside what we normalize here. - let start = i; - while i < bytes.len() { - let c = bytes[i]; - if c == b'_' || c == b'$' || c.is_ascii_alphanumeric() { - i += 1; - } else { - break; - } - } - let ident = &expr[start..i]; - if expect_key { - out.push('"'); - out.push_str(ident); - out.push('"'); - expect_key = false; - } else if matches!(ident, "true" | "false" | "null") { - out.push_str(ident); - } else { - return None; - } - } - _ => { - out.push(ch as char); - i += 1; - } - } - } - Some(out) -} - -/// Parse an object/array literal expression into a value. Tries strict JSON -/// first (the common Next.js manifest case), then a lenient JS-literal->JSON -/// normalization. Returns `None` when the text is not a literal we can build. -fn eval_object_or_array_literal(expr: &str) -> Option { - let trimmed = expr.trim(); - if !(trimmed.starts_with('{') || trimmed.starts_with('[')) { - return None; - } - let attempt = |text: &str| -> Option { - let ptr = string_ptr(text); - match unsafe { crate::json::js_json_parse_result(ptr) } { - Ok(value) => Some(f64::from_bits(value.bits())), - Err(_) => None, - } - }; - if let Some(value) = attempt(trimmed) { - return Some(value); - } - let normalized = normalize_literal_to_json(trimmed)?; - attempt(&normalized) -} - -fn is_truthy(value: f64) -> bool { - crate::value::js_is_truthy(value) != 0 -} - -fn eval_expr(expr: &str, env: &EvalEnv) -> f64 { - let expr = strip_wrapping_parens(expr); - if expr.is_empty() { - return undefined_value(); - } - // Logical operators bind looser than comparison/arithmetic, so resolve them - // first. `find_top_level_operator` returns the last top-level occurrence, - // which yields correct left-associative short-circuit grouping. - if let Some(idx) = find_top_level_operator(expr, "||") { - let left = eval_expr(&expr[..idx], env); - if is_truthy(left) { - return left; - } - return eval_expr(&expr[idx + 2..], env); - } - if let Some(idx) = find_top_level_operator(expr, "&&") { - let left = eval_expr(&expr[..idx], env); - if !is_truthy(left) { - return left; - } - return eval_expr(&expr[idx + 2..], env); - } - if let Some(idx) = find_top_level_operator(expr, "===") { - let left = eval_expr(&expr[..idx], env); - let right = eval_expr(&expr[idx + 3..], env); - return bool_value(value_same(left, right)); - } - if let Some(idx) = find_top_level_operator(expr, "!==") { - let left = eval_expr(&expr[..idx], env); - let right = eval_expr(&expr[idx + 3..], env); - return bool_value(!value_same(left, right)); - } - if let Some(idx) = find_top_level_operator(expr, "+") { - let left = eval_expr(&expr[..idx], env); - let right = eval_expr(&expr[idx + 1..], env); - return add_values(left, right); - } - if let Some(idx) = find_top_level_operator(expr, "-") { - if idx > 0 { - let left = eval_expr(&expr[..idx], env); - let right = eval_expr(&expr[idx + 1..], env); - return number_value(value_to_number(left) - value_to_number(right)); - } - } - if let Some(rest) = expr.strip_prefix("typeof ") { - let value = eval_expr(rest, env); - let ptr = crate::builtins::js_value_typeof(value); - return f64::from_bits(JSValue::string_ptr(ptr).bits()); - } - if let Some(s) = unquote(expr) { - return string_value(&s); - } - if let Ok(n) = expr.parse::() { - return number_value(n); - } - if let Some(descriptor) = expr - .strip_prefix("new WebAssembly.Memory(") - .and_then(|rest| rest.strip_suffix(')')) - .and_then(eval_object_or_array_literal) - { - return crate::object::js_webassembly_memory_from_descriptor(descriptor); - } - if let Some(value) = eval_object_or_array_literal(expr) { - return value; - } - eval_property_path(expr, env).unwrap_or_else(undefined_value) -} - -fn execute_statement(stmt: &str, env: &mut EvalEnv) -> Option { - let stmt = stmt.trim(); - if stmt.is_empty() { - return Some(undefined_value()); - } - if let Some(rest) = stmt.strip_prefix("return ") { - return Some(eval_expr(rest, env)); - } - let decl = ["var ", "let ", "const "] - .iter() - .find_map(|prefix| stmt.strip_prefix(prefix)); - if let Some(rest) = decl { - let mut last = undefined_value(); - for part in split_top_level(rest, ',') { - let (name, value) = if let Some((name, rhs)) = part.split_once('=') { - (name.trim(), eval_expr(rhs, env)) - } else { - (part.trim(), undefined_value()) - }; - if !name.is_empty() { - set_reference(name, value, env); - last = value; - } - } - return Some(last); - } - for op in ["+=", "-=", "="] { - if let Some(idx) = find_top_level_operator(stmt, op) { - let lhs = stmt[..idx].trim(); - let rhs = stmt[idx + op.len()..].trim(); - let right = eval_expr(rhs, env); - let value = match op { - "+=" => add_values(eval_expr(lhs, env), right), - "-=" => number_value(value_to_number(eval_expr(lhs, env)) - value_to_number(right)), - _ => right, - }; - set_reference(lhs, value, env); - return Some(value); - } - } - Some(eval_expr(stmt, env)) -} - -pub(super) fn run_source(source: &str, target: f64, params: HashMap) -> f64 { - let mut env = EvalEnv { target, params }; - let mut last = undefined_value(); - for stmt in split_top_level(source, ';') { - if stmt.trim().starts_with("return ") { - return eval_expr(stmt.trim().trim_start_matches("return "), &env); - } - if let Some(value) = execute_statement(stmt, &mut env) { - last = value; - } - } - last -} diff --git a/crates/perry-runtime/src/node_vm/modules.rs b/crates/perry-runtime/src/node_vm/modules.rs new file mode 100644 index 0000000000..f637288a5f --- /dev/null +++ b/crates/perry-runtime/src/node_vm/modules.rs @@ -0,0 +1,548 @@ +//! Experimental `vm.Module` / `SourceTextModule` / `SyntheticModule` lifecycle. +//! Extracted from `node_vm.rs` to stay under the 2000-line file-size gate. + +use super::*; + +fn evaluate_source_module(module: *mut ObjectHeader) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let module = scope.root_raw_mut_ptr(module); + let status = module_status(hmut::(&module)); + if status != STATUS_LINKED && status != STATUS_EVALUATED { + return throw_vm_status("Module status must be linked"); + } + if status == STATUS_EVALUATED { + return undefined_value(); + } + + set_status(hmut::(&module), STATUS_EVALUATING); + let Some(namespace) = namespace_for_module(hmut::(&module)) else { + set_status(hmut::(&module), STATUS_ERRORED); + return throw_vm_status("Module namespace is unavailable"); + }; + let namespace = scope.root_raw_mut_ptr(namespace); + + let source = get_string_field(hmut::(&module), FIELD_SOURCE).unwrap_or_default(); + let context = scope.root_nanbox_f64(get_field(hmut::(&module), FIELD_CONTEXT)); + for (name, value) in build_import_env(hmut::(&module)) { + set_object_field(context.get_nanbox_f64(), &name, value); + } + let executable = split_source_statements(&source) + .into_iter() + .filter(|stmt| !stmt.starts_with("import ")) + .map(|stmt| stmt.strip_prefix("export ").unwrap_or(&stmt).to_string()) + .collect::>() + .join(";"); + let lexical = scope.root_nanbox_f64(de::script_environment(context.get_nanbox_f64(), &[])); + if let Err(error) = crate::exception::js_call_catching(|| { + de::eval_script_in( + &executable, + context.get_nanbox_f64(), + context.get_nanbox_f64(), + lexical.get_nanbox_f64(), + ) + }) { + set_field(hmut::(&module), FIELD_ERROR, error); + set_status(hmut::(&module), STATUS_ERRORED); + return error; + } + for export in read_exports(hmut::(&module)) { + set_field( + hmut::(&namespace), + &export.name, + de::script_binding(lexical.get_nanbox_f64(), &export.name), + ); + } + set_status(hmut::(&module), STATUS_EVALUATED); + undefined_value() +} + +fn evaluate_synthetic_module(module: *mut ObjectHeader) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let module = scope.root_raw_mut_ptr(module); + let status = module_status(hmut::(&module)); + if status == STATUS_EVALUATED { + return undefined_value(); + } + if status != STATUS_LINKED { + return throw_vm_status("Module status must be linked"); + } + + set_status(hmut::(&module), STATUS_EVALUATING); + let callback = scope.root_nanbox_f64(get_field( + hmut::(&module), + FIELD_EVALUATE_CALLBACK, + )); + let js = JSValue::from_bits(callback.get_nanbox_f64().to_bits()); + if !js.is_undefined() && !js.is_null() { + let prev = crate::object::js_implicit_this_set(object_value(hmut::(&module))); + let outcome = crate::exception::js_call_catching(|| unsafe { + crate::closure::js_native_call_value(callback.get_nanbox_f64(), std::ptr::null(), 0) + }); + crate::object::js_implicit_this_set(prev); + if let Err(error) = outcome { + set_field(hmut::(&module), FIELD_ERROR, error); + set_status(hmut::(&module), STATUS_ERRORED); + return error; + } + } + set_status(hmut::(&module), STATUS_EVALUATED); + undefined_value() +} + +fn module_has_tla(module: *mut ObjectHeader) -> bool { + let Some(source) = get_string_field(module, FIELD_SOURCE) else { + return false; + }; + parse_source(&source).has_top_level_await +} + +fn module_has_async_graph(module: *mut ObjectHeader) -> bool { + let mut visited = std::collections::HashSet::new(); + module_has_async_graph_inner(module, &mut visited) +} + +fn module_has_async_graph_inner( + module: *mut ObjectHeader, + visited: &mut std::collections::HashSet, +) -> bool { + if !visited.insert(module as usize) { + return false; + } + if module_has_tla(module) { + return true; + } + let Some(linked) = module_linked_modules(module) else { + return false; + }; + let len = crate::array::js_array_length(linked); + for idx in 0..len { + let value = crate::array::js_array_get_f64(linked, idx); + if let Some(dep) = object_ptr_from_value(value) { + if module_has_async_graph_inner(dep, visited) { + return true; + } + } + } + false +} + +fn new_module_base(kind: &str, status: &str, identifier: String) -> *mut ObjectHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let module = crate::object::js_object_alloc(0, 16); + let module = scope.root_raw_mut_ptr(module); + let value = string_value(kind); + set_field(hmut::(&module), FIELD_KIND, value); + let value = string_value(status); + set_field(hmut::(&module), FIELD_STATUS, value); + let value = string_value(status); + set_field(hmut::(&module), "status", value); + let value = string_value(&identifier); + set_field(hmut::(&module), FIELD_IDENTIFIER, value); + let value = string_value(&identifier); + set_field(hmut::(&module), "identifier", value); + set_field( + hmut::(&module), + FIELD_ERROR, + undefined_value(), + ); + set_field(hmut::(&module), "error", undefined_value()); + let value = array_value(crate::array::js_array_alloc(0)); + set_field(hmut::(&module), FIELD_LINKED_MODULES, value); + hmut::(&module) +} + +extern "C" fn module_namespace_getter(closure: *const ClosureHeader) -> f64 { + js_vm_module_namespace(crate::closure::js_closure_get_capture_f64(closure, 0)) +} + +extern "C" fn module_error_getter(closure: *const ClosureHeader) -> f64 { + js_vm_module_error(crate::closure::js_closure_get_capture_f64(closure, 0)) +} + +fn install_module_accessor( + module: *mut ObjectHeader, + name: &str, + getter: extern "C" fn(*const ClosureHeader) -> f64, +) { + let scope = crate::gc::RuntimeHandleScope::new(); + let module = scope.root_raw_mut_ptr(module); + let closure = crate::closure::js_closure_alloc(getter as *const u8, 1); + let closure = scope.root_raw_mut_ptr(closure); + crate::closure::js_register_closure_arity(getter as *const u8, 0); + crate::closure::js_closure_set_capture_f64( + hmut::(&closure), + 0, + object_value(hmut::(&module)), + ); + unsafe { + crate::closure::rebuild_closure_layout_and_barriers(hmut::(&closure), 1); + } + set_field(hmut::(&module), name, undefined_value()); + crate::object::set_builtin_accessor_descriptor( + hmut::(&module) as usize, + name.to_string(), + crate::object::AccessorDescriptor { + get: crate::value::js_nanbox_pointer(hmut::(&closure) as i64).to_bits(), + set: 0, + }, + PropertyAttrs::new(false, false, false), + ); +} + +fn set_module_namespace_tag(namespace: *mut ObjectHeader) { + let scope = crate::gc::RuntimeHandleScope::new(); + let namespace = scope.root_raw_mut_ptr(namespace); + let symbol = crate::symbol::well_known_symbol("toStringTag"); + if symbol.is_null() { + return; + } + let symbol = scope.root_raw_mut_ptr(symbol); + let tag = scope.root_nanbox_f64(string_value("Module")); + unsafe { + crate::symbol::js_object_set_symbol_property( + object_value(hmut::(&namespace)), + crate::value::js_nanbox_pointer(hmut::(&symbol) as i64), + tag.get_nanbox_f64(), + ); + } +} + +pub extern "C" fn js_vm_module_call() -> f64 { + throw_type_error_no_code("Class constructor Module cannot be invoked without 'new'") +} + +#[no_mangle] +pub extern "C" fn js_vm_module_constructor_error() -> f64 { + throw_type_error_no_code("Module is not a constructor") +} + +pub extern "C" fn js_vm_source_text_module_new(code: f64, options: f64) -> f64 { + if !vm_modules_enabled() { + return throw_vm_unimplemented("SourceTextModule experimental gate", "3132"); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let options = scope.root_nanbox_f64(options); + let source = code_string_required(code, "code"); + validate_module_source(&source); + let (context, identifier) = module_options(options.get_nanbox_f64()); + let context = scope.root_nanbox_f64(context); + let hash = source_hash(CACHE_KIND_MODULE, &source, &[]); + if let Some(bytes) = validate_cached_data_option(options.get_nanbox_f64()) { + if !cache_bytes_accepted(&bytes, CACHE_KIND_MODULE, hash) { + return throw_vm_module_cached_data_rejected(); + } + } + let parsed = parse_source(&source); + let module = new_module_base(KIND_SOURCE, STATUS_UNLINKED, identifier); + let module = scope.root_raw_mut_ptr(module); + let namespace = crate::object::js_object_alloc_null_proto(0, parsed.exports.len() as u32); + let namespace = scope.root_raw_mut_ptr(namespace); + set_module_namespace_tag(hmut::(&namespace)); + for export in &parsed.exports { + set_field( + hmut::(&namespace), + &export.name, + undefined_value(), + ); + } + set_field( + hmut::(&module), + FIELD_NAMESPACE, + object_value(hmut::(&namespace)), + ); + install_module_accessor( + hmut::(&module), + "namespace", + module_namespace_getter, + ); + install_module_accessor(hmut::(&module), "error", module_error_getter); + set_field( + hmut::(&module), + FIELD_CONTEXT, + context.get_nanbox_f64(), + ); + let value = string_value(&source); + set_field(hmut::(&module), FIELD_SOURCE, value); + let value = requests_array(&parsed.requests); + set_field(hmut::(&module), FIELD_REQUESTS, value); + let value = strings_array(&parsed.requests); + set_field(hmut::(&module), "dependencySpecifiers", value); + let value = requests_array(&parsed.requests); + set_field(hmut::(&module), "moduleRequests", value); + let value = imports_array(&parsed.imports); + set_field(hmut::(&module), FIELD_IMPORTS, value); + let value = exports_array(&parsed.exports); + set_field(hmut::(&module), FIELD_EXPORTS, value); + object_value(hmut::(&module)) +} + +pub extern "C" fn js_vm_synthetic_module_new( + export_names_value: f64, + evaluate_callback: f64, + options: f64, +) -> f64 { + if !vm_modules_enabled() { + return throw_vm_unimplemented("SyntheticModule experimental gate", "3133"); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let export_names_value = scope.root_nanbox_f64(export_names_value); + let evaluate_callback = scope.root_nanbox_f64(evaluate_callback); + let options = scope.root_nanbox_f64(options); + let Some(export_names) = array_ptr_from_value(export_names_value.get_nanbox_f64()) else { + let message = format!( + "The \"exportNames\" argument must be an instance of Array. Received {}", + crate::fs::validate::describe_received(export_names_value.get_nanbox_f64()) + ); + throw_invalid_arg(&message); + }; + let export_names = scope.root_raw_mut_ptr(export_names); + if !crate::object::value_is_callable(evaluate_callback.get_nanbox_f64()) { + let message = format!( + "The \"evaluateCallback\" argument must be of type function. Received {}", + crate::fs::validate::describe_received(evaluate_callback.get_nanbox_f64()) + ); + throw_invalid_arg(&message); + } + let (context, identifier) = module_options(options.get_nanbox_f64()); + let context = scope.root_nanbox_f64(context); + let module = new_module_base(KIND_SYNTHETIC, STATUS_LINKED, identifier); + let module = scope.root_raw_mut_ptr(module); + let namespace = crate::object::js_object_alloc_null_proto(0, 0); + let namespace = scope.root_raw_mut_ptr(namespace); + set_module_namespace_tag(hmut::(&namespace)); + let len = crate::array::js_array_length(hmut::(&export_names)); + let mut exports = Vec::new(); + for idx in 0..len { + let value = crate::array::js_array_get_f64(hmut::(&export_names), idx); + let Some(name) = string_from_value(value) else { + let message = format!( + "The \"exportNames[{idx}]\" argument must be of type string. Received {}", + crate::fs::validate::describe_received(value) + ); + throw_invalid_arg(&message); + }; + exports.push(ExportBinding { + name: name.clone(), + expr: String::new(), + }); + set_field(hmut::(&namespace), &name, undefined_value()); + } + set_field( + hmut::(&module), + FIELD_NAMESPACE, + object_value(hmut::(&namespace)), + ); + install_module_accessor( + hmut::(&module), + "namespace", + module_namespace_getter, + ); + install_module_accessor(hmut::(&module), "error", module_error_getter); + set_field( + hmut::(&module), + FIELD_CONTEXT, + context.get_nanbox_f64(), + ); + let value = requests_array(&[]); + set_field(hmut::(&module), FIELD_REQUESTS, value); + let value = imports_array(&[]); + set_field(hmut::(&module), FIELD_IMPORTS, value); + let value = exports_array(&exports); + set_field(hmut::(&module), FIELD_EXPORTS, value); + set_field( + hmut::(&module), + FIELD_EVALUATE_CALLBACK, + evaluate_callback.get_nanbox_f64(), + ); + object_value(hmut::(&module)) +} + +pub extern "C" fn js_vm_module_status(module_value: f64) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return undefined_value(); + }; + string_value(&module_status(module)) +} + +pub extern "C" fn js_vm_module_identifier(module_value: f64) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return undefined_value(); + }; + get_field(module, FIELD_IDENTIFIER) +} + +pub extern "C" fn js_vm_module_error(module_value: f64) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return undefined_value(); + }; + if module_status(module) != STATUS_ERRORED { + return throw_vm_status("Module status must be errored"); + } + get_field(module, FIELD_ERROR) +} + +pub extern "C" fn js_vm_module_namespace(module_value: f64) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return undefined_value(); + }; + let status = module_status(module); + if module_kind(module) == KIND_SOURCE && (status == STATUS_UNLINKED || status == STATUS_LINKING) + { + return throw_vm_status("Module status must be linked"); + } + get_field(module, FIELD_NAMESPACE) +} + +pub extern "C" fn js_vm_module_link(module_value: f64, linker: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let Some(module) = object_ptr_from_value(module_value) else { + return undefined_value(); + }; + let module = scope.root_raw_mut_ptr(module); + let module_value = scope.root_nanbox_f64(module_value); + let linker = scope.root_nanbox_f64(linker); + if module_kind(hmut::(&module)) == KIND_SYNTHETIC { + set_status(hmut::(&module), STATUS_LINKED); + return undefined_value(); + } + if module_status(hmut::(&module)) != STATUS_UNLINKED { + return undefined_value(); + } + + set_status(hmut::(&module), STATUS_LINKING); + let requests = read_requests(hmut::(&module)); + let mut linked = crate::array::js_array_alloc(requests.len() as u32); + for specifier in &requests { + let args = [ + string_value(specifier), + module_value.get_nanbox_f64(), + module_request_extra(), + ]; + let dep = unsafe { + crate::closure::js_native_call_value(linker.get_nanbox_f64(), args.as_ptr(), args.len()) + }; + linked = crate::array::js_array_push_f64(linked, dep); + } + set_field( + hmut::(&module), + FIELD_LINKED_MODULES, + array_value(linked), + ); + set_status(hmut::(&module), STATUS_LINKED); + undefined_value() +} + +pub extern "C" fn js_vm_module_evaluate(module_value: f64, _options: f64) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return undefined_value(); + }; + let result = match module_kind(module).as_str() { + KIND_SOURCE => evaluate_source_module(module), + KIND_SYNTHETIC => evaluate_synthetic_module(module), + _ => undefined_value(), + }; + let promise = if module_status(module) == STATUS_ERRORED { + crate::promise::js_promise_rejected(result) + } else { + crate::promise::js_promise_resolved(result) + }; + crate::value::js_nanbox_pointer(promise as i64) +} + +pub extern "C" fn js_vm_source_text_module_dependency_specifiers(module_value: f64) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return array_value(crate::array::js_array_alloc(0)); + }; + strings_array(&read_requests(module)) +} + +pub extern "C" fn js_vm_source_text_module_module_requests(module_value: f64) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return array_value(crate::array::js_array_alloc(0)); + }; + let requests = read_requests(module); + requests_array(&requests) +} + +pub extern "C" fn js_vm_source_text_module_create_cached_data(module_value: f64) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return cached_data_buffer(CACHE_KIND_MODULE, 0); + }; + if module_status(module) == STATUS_EVALUATED { + return throw_vm_module_cannot_create_cached_data(); + } + let source = get_string_field(module, FIELD_SOURCE).unwrap_or_default(); + cached_data_buffer( + CACHE_KIND_MODULE, + source_hash(CACHE_KIND_MODULE, &source, &[]), + ) +} + +pub extern "C" fn js_vm_source_text_module_link_requests( + module_value: f64, + modules_value: f64, +) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return undefined_value(); + }; + let Some(modules) = array_ptr_from_value(modules_value) else { + return throw_vm_type("linkRequests modules must be an array"); + }; + set_field(module, FIELD_LINKED_MODULES, array_value(modules)); + undefined_value() +} + +pub extern "C" fn js_vm_source_text_module_instantiate(module_value: f64) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return undefined_value(); + }; + if module_status(module) == STATUS_UNLINKED { + set_status(module, STATUS_LINKED); + } + undefined_value() +} + +pub extern "C" fn js_vm_source_text_module_has_top_level_await(module_value: f64) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return bool_value(false); + }; + bool_value(module_has_tla(module)) +} + +pub extern "C" fn js_vm_source_text_module_has_async_graph(module_value: f64) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return bool_value(false); + }; + if module_status(module) == STATUS_UNLINKED { + return throw_vm_status("Module status must be instantiated"); + } + bool_value(module_has_async_graph(module)) +} + +pub extern "C" fn js_vm_synthetic_module_set_export( + module_value: f64, + name_value: f64, + value: f64, +) -> f64 { + let Some(module) = object_ptr_from_value(module_value) else { + return undefined_value(); + }; + if module_kind(module) != KIND_SYNTHETIC { + return throw_vm_type("setExport is only supported on SyntheticModule"); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let module = scope.root_raw_mut_ptr(module); + let value = scope.root_nanbox_f64(value); + let Some(name) = string_from_value(name_value) else { + return throw_vm_type("SyntheticModule export name must be a string"); + }; + let exports = read_exports(hmut::(&module)); + if !exports.iter().any(|export| export.name == name) { + return throw_reference_error_no_code(&format!("Export '{name}' is not defined in module")); + } + let Some(namespace) = namespace_for_module(hmut::(&module)) else { + return throw_vm_status("SyntheticModule namespace is unavailable"); + }; + set_field(namespace, &name, value.get_nanbox_f64()); + undefined_value() +} diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index ea7c66b89c..694ce7fd59 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -47,6 +47,7 @@ mod prototype_methods; pub(crate) mod prototype_objects; mod registration; mod state; +mod vm_brand; // ── state.rs ──────────────────────────────────────────────────────────────── pub(crate) use state::{ @@ -108,7 +109,7 @@ pub use prototype_methods::{ js_register_function_prototype_method, js_register_prototype_method, CLASS_PROTOTYPE_METHODS, }; -// ── construct.rs ──────────────────────────────────────────────────────────── +// ── construct.rs / vm_brand.rs ────────────────────────────────────────────── pub(crate) use construct::{ extends_target_must_throw, function_would_have_own_prototype, is_callable_function_value, js_value_is_constructor, lookup_prototype_method, nm_ctor_child_process, nm_ctor_cluster, @@ -120,6 +121,7 @@ pub use construct::{ js_new_function_construct_apply, js_new_function_construct_with_new_target, js_new_target_value, }; +pub(crate) use vm_brand::brand_vm_script_instance; // ── gc_roots.rs ───────────────────────────────────────────────────────────── pub(crate) use gc_roots::{ diff --git a/crates/perry-runtime/src/object/class_registry/construct.rs b/crates/perry-runtime/src/object/class_registry/construct.rs index d6a962e8c0..5f36f5f0c3 100644 --- a/crates/perry-runtime/src/object/class_registry/construct.rs +++ b/crates/perry-runtime/src/object/class_registry/construct.rs @@ -140,7 +140,9 @@ pub(crate) unsafe fn nm_ctor_vm( if method == "Script" { let code = nm_ctor_arg(args_ptr, args_len, 0); let options = nm_ctor_arg(args_ptr, args_len, 1); - return Some(crate::node_vm::js_vm_script_new(code, options)); + return Some(super::brand_vm_script_instance( + crate::node_vm::js_vm_script_new(code, options), + )); } None } diff --git a/crates/perry-runtime/src/object/class_registry/vm_brand.rs b/crates/perry-runtime/src/object/class_registry/vm_brand.rs new file mode 100644 index 0000000000..2f726a03e8 --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/vm_brand.rs @@ -0,0 +1,29 @@ +//! Brand a `vm.Script` instance with the synthetic class id of the bound +//! `vm.Script` constructor so `instanceof` / prototype walks match Node. + +use super::*; + +pub(crate) fn brand_vm_script_instance(value: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(value); + let constructor = scope.root_nanbox_f64( + crate::object::native_module::bound_native_callable_export_value("vm", "Script"), + ); + let class_id = synthetic_class_id_for_function(constructor.get_nanbox_f64()); + if class_id == 0 { + return value.get_nanbox_f64(); + } + let _ = ordinary_function_prototype_value_for_read(constructor.get_nanbox_f64()); + crate::node_vm::install_script_prototypes(constructor.get_nanbox_f64()); + let result = value.get_nanbox_f64(); + let result_value = JSValue::from_bits(result.to_bits()); + if result_value.is_pointer() { + let object = result_value.as_pointer::() as *mut ObjectHeader; + if unsafe { crate::value::addr_class::try_read_gc_header(object as usize) } + .is_some_and(|header| header.obj_type == crate::gc::GC_TYPE_OBJECT) + { + unsafe { (*object).class_id = class_id }; + } + } + result +} diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index b54f3929bf..e4353bb476 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -1678,6 +1678,15 @@ pub(crate) unsafe fn nm_get_own_descriptor( _ => {} } } + if module_name == "vm.constants" { + let value = js_object_get_field_by_name(obj, key_str); + return Some(build_data_descriptor( + f64::from_bits(value.bits()), + false, + true, + false, + )); + } let value = js_object_get_field_by_name(obj, key_str); if matches!( module_name.as_str(), @@ -1692,6 +1701,6 @@ pub(crate) unsafe fn nm_get_own_descriptor( f64::from_bits(value.bits()), true, true, - true, + module_name != "vm", )) } diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 5db69de8ca..35e1cf309e 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -20,9 +20,9 @@ use super::*; #[path = "global_this_webassembly.rs"] mod global_this_webassembly; pub(crate) use global_this_webassembly::{ - clear_module_wrapper_for_dead_ptr, js_webassembly_memory_from_descriptor, - module_wrapper_owner_moved, webassembly_error_ctor_instanceof, - webassembly_value_ctor_instanceof, + clear_module_wrapper_for_dead_ptr, is_registered_wasm_module, + js_webassembly_memory_from_descriptor, module_wrapper_owner_moved, + webassembly_error_ctor_instanceof, webassembly_value_ctor_instanceof, }; // Only the `wasm-host` engine constructs real modules (via // `webassembly::make_module_object`), so registration and trusted-handle diff --git a/crates/perry-runtime/src/object/global_this/array_error.rs b/crates/perry-runtime/src/object/global_this/array_error.rs index ac534b55aa..62cc867c3e 100644 --- a/crates/perry-runtime/src/object/global_this/array_error.rs +++ b/crates/perry-runtime/src/object/global_this/array_error.rs @@ -564,8 +564,7 @@ pub(crate) extern "C" fn function_prototype_to_string_thunk( b"Function.prototype.toString requires that 'this' be a Function", ); } - let func_ptr = unsafe { (*(raw as *const crate::closure::ClosureHeader)).func_ptr as usize }; - let s = crate::builtins::function_source_for_func_ptr(func_ptr); + let s = crate::node_vm::function_source_for_closure(raw); let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); f64::from_bits(JSValue::string_ptr(str_ptr).bits()) } diff --git a/crates/perry-runtime/src/object/global_this_webassembly.rs b/crates/perry-runtime/src/object/global_this_webassembly.rs index ee8d2dfa2f..a2c23253f8 100644 --- a/crates/perry-runtime/src/object/global_this_webassembly.rs +++ b/crates/perry-runtime/src/object/global_this_webassembly.rs @@ -337,6 +337,11 @@ fn value_wasm_kind_matches(value: f64, expected: &[u8]) -> bool { registered_module_handle(obj as usize).is_some() } +pub(crate) fn is_registered_wasm_module(value: f64) -> bool { + value_object_ptr(value) + .is_some_and(|object| registered_module_handle(object as usize).is_some()) +} + /// `mod instanceof WebAssembly.Module` for the wasm-host module wrapper. /// That wrapper is a plain heap object whose `[[Prototype]]` does NOT reach /// `WebAssembly.Module.prototype` — the same shape problem the namespace diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index e686164c51..1193bd3228 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -71,6 +71,26 @@ fn value_addr(value: f64) -> usize { } } +fn recorded_prototype_instanceof_builtin(value: f64, name: &str) -> Option { + let addr = value_addr(value); + if addr == 0 || super::prototype_chain::object_static_prototype(addr).is_none() { + return None; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(value); + let constructor = scope.root_nanbox_f64(crate::object::js_get_global_this_builtin_value( + name.as_ptr(), + name.len(), + )); + if !value_is_callable(constructor.get_nanbox_f64()) { + return None; + } + Some(ordinary_has_instance_prototype_walk( + value.get_nanbox_f64(), + constructor.get_nanbox_f64(), + )) +} + fn is_native_module_namespace_value(value: f64, expected: &str) -> bool { let jv = crate::JSValue::from_bits(value.to_bits()); if !jv.is_pointer() { @@ -645,15 +665,18 @@ fn ordinary_has_instance_prototype_walk(value: f64, type_ref: f64) -> bool { // share tag-space with raw heap pointers (a bare `is_number()` would // misclassify a module-level object var), so they are intentionally left to // those paths rather than guarded here. + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(value); + let type_ref = scope.root_nanbox_f64(type_ref); { - let jv = crate::value::JSValue::from_bits(value.to_bits()); + let jv = crate::value::JSValue::from_bits(value.get_nanbox_f64().to_bits()); if jv.is_null() || jv.is_undefined() || jv.is_bool() || jv.is_int32() || jv.is_any_string() || jv.is_bigint() - || unsafe { crate::symbol::js_is_symbol(value) != 0 } + || unsafe { crate::symbol::js_is_symbol(value.get_nanbox_f64()) != 0 } { return false; } @@ -661,7 +684,7 @@ fn ordinary_has_instance_prototype_walk(value: f64, type_ref: f64) -> bool { // P = type_ref.prototype (the constructor's `.prototype` data property). let proto = unsafe { crate::value::js_dynamic_object_get_property( - type_ref, + type_ref.get_nanbox_f64(), b"prototype".as_ptr() as *const i8, 9, ) @@ -671,7 +694,7 @@ fn ordinary_has_instance_prototype_walk(value: f64, type_ref: f64) -> bool { return false; // non-object `.prototype` can never be on the chain } // Walk `value`'s real [[Prototype]] chain looking for identity with P. - let mut cur = unsafe { js_object_get_prototype_of(value) }; + let mut cur = unsafe { js_object_get_prototype_of(value.get_nanbox_f64()) }; let mut depth = 0usize; while depth < 100_000 { if crate::value::JSValue::from_bits(cur.to_bits()).is_null() { @@ -1341,6 +1364,9 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { return false_val; } if class_id == CLASS_ID_PROMISE { + if let Some(matches) = recorded_prototype_instanceof_builtin(value, "Promise") { + return if matches { true_val } else { false_val }; + } return if crate::promise::js_value_is_promise(value) != 0 { true_val } else { @@ -1473,6 +1499,22 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { false_val }; } + let builtin_name = match class_id { + crate::error::CLASS_ID_ERROR => Some("Error"), + crate::error::CLASS_ID_TYPE_ERROR => Some("TypeError"), + crate::error::CLASS_ID_RANGE_ERROR => Some("RangeError"), + crate::error::CLASS_ID_REFERENCE_ERROR => Some("ReferenceError"), + crate::error::CLASS_ID_SYNTAX_ERROR => Some("SyntaxError"), + crate::error::CLASS_ID_EVAL_ERROR => Some("EvalError"), + crate::error::CLASS_ID_URI_ERROR => Some("URIError"), + crate::error::CLASS_ID_AGGREGATE_ERROR => Some("AggregateError"), + _ => None, + }; + if let Some(name) = builtin_name { + if let Some(matches) = recorded_prototype_instanceof_builtin(value, name) { + return if matches { true_val } else { false_val }; + } + } return match class_id { crate::error::CLASS_ID_ERROR => true_val, crate::error::CLASS_ID_TYPE_ERROR => { diff --git a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs index 09af862912..1adfdd244f 100644 --- a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs @@ -122,8 +122,7 @@ pub(super) unsafe fn dispatch_primitive( if let Some(result) = crate::value::function_to_string_method_result(object) { return Some(result); } - let func_ptr = (*(raw_addr as *const crate::closure::ClosureHeader)).func_ptr as usize; - let s = crate::builtins::function_source_for_func_ptr(func_ptr); + let s = crate::node_vm::function_source_for_closure(raw_addr); let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); } diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index fac79dc8a3..b8063a6b44 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -107,8 +107,18 @@ fn bound_native_method_length(name: &str) -> Option { } #[no_mangle] -pub extern "C" fn js_vm_create_context(sandbox: f64) -> f64 { - crate::node_vm::create_context(sandbox) +pub extern "C" fn js_vm_create_context(sandbox: f64, options: f64) -> f64 { + crate::node_vm::create_context(sandbox, options) +} + +#[no_mangle] +pub extern "C" fn js_vm_create_script_branded(code: f64, options: f64) -> f64 { + crate::node_vm::dispatch_vm_method( + "createScript", + code, + options, + f64::from_bits(crate::value::TAG_UNDEFINED), + ) } pub fn scan_native_callable_export_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { diff --git a/crates/perry-runtime/src/object/native_module/namespace_builders.rs b/crates/perry-runtime/src/object/native_module/namespace_builders.rs index e03842a81e..8f5cffd2bd 100644 --- a/crates/perry-runtime/src/object/native_module/namespace_builders.rs +++ b/crates/perry-runtime/src/object/native_module/namespace_builders.rs @@ -4,7 +4,18 @@ use std::sync::atomic::Ordering; /// The compiled code treats the result as another NativeModuleRef, so chained /// property accesses like `fs.constants.O_RDONLY` work through the dispatch table. pub(crate) fn create_sub_namespace(name: &str) -> f64 { - js_create_native_module_namespace(name.as_ptr(), name.len()) + let value = js_create_native_module_namespace(name.as_ptr(), name.len()); + if name == "vm.constants" { + let object = JSValue::from_bits(value.to_bits()).as_pointer::(); + if !object.is_null() { + super::super::prototype_chain::object_set_static_prototype( + object as usize, + crate::value::TAG_NULL, + ); + super::super::js_object_freeze(value); + } + } + value } pub(crate) fn native_namespace_or_create(module_name: &str, namespace_obj: f64) -> f64 { diff --git a/crates/perry-runtime/src/value/to_string.rs b/crates/perry-runtime/src/value/to_string.rs index 85f6f2fe4d..fe97e0518c 100644 --- a/crates/perry-runtime/src/value/to_string.rs +++ b/crates/perry-runtime/src/value/to_string.rs @@ -1019,9 +1019,7 @@ pub extern "C" fn js_jsvalue_to_string(value: f64) -> *mut crate::string::String FunctionToStringOutcome::TypeError => throw_cannot_convert_to_primitive(), FunctionToStringOutcome::NoCustomMethod => {} } - let func_ptr = - unsafe { (*(ptr as *const crate::closure::ClosureHeader)).func_ptr as usize }; - let s = crate::builtins::function_source_for_func_ptr(func_ptr); + let s = crate::node_vm::function_source_for_closure(ptr as usize); return crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); } // Consult `[Symbol.toPrimitive]("string")` if the object has a