From 1c6983611be500e7ab13a38f8b65575d0e4ce240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 15:34:40 +0200 Subject: [PATCH 1/6] fix: restore method sources and locale date defaults --- Cargo.lock | 1 + changelog.d/9451-9468-intl-method-source.md | 15 + .../src/codegen/artifact_source_text.rs | 117 +++ crates/perry-codegen/src/codegen/artifacts.rs | 10 + crates/perry-codegen/src/codegen/mod.rs | 1 + .../perry-hir/src/destructuring/var_decl.rs | 20 +- crates/perry-hir/src/lower/expr_assign.rs | 2 +- crates/perry-hir/src/lower/expr_object.rs | 13 + crates/perry-hir/src/lower_decl/class_decl.rs | 22 + .../perry-hir/src/lower_decl/class_members.rs | 45 +- crates/perry-runtime/Cargo.toml | 4 +- crates/perry-runtime/src/closure/dispatch.rs | 5 +- .../src/closure/dispatch/bound.rs | 51 ++ crates/perry-runtime/src/closure/mod.rs | 8 +- .../perry-runtime/src/intl/date_collator.rs | 134 +-- .../src/intl/date_collator/icu.rs | 167 ++++ crates/perry-runtime/src/intl/icu_dtf.rs | 278 ++++++- .../intl/icu_dtf/default_numeric_patterns.rs | 769 ++++++++++++++++++ crates/perry-runtime/src/node_vm.rs | 7 +- .../src/object/class_registry.rs | 4 +- .../src/object/class_registry/registration.rs | 19 + test-files/test_class_name_and_source_9413.ts | 34 + test-files/test_class_name_cjs_9413.cts | 15 +- test-files/test_gap_intl_component_locale.ts | 40 +- 24 files changed, 1631 insertions(+), 150 deletions(-) create mode 100644 changelog.d/9451-9468-intl-method-source.md create mode 100644 crates/perry-codegen/src/codegen/artifact_source_text.rs create mode 100644 crates/perry-runtime/src/intl/date_collator/icu.rs create mode 100644 crates/perry-runtime/src/intl/icu_dtf/default_numeric_patterns.rs diff --git a/Cargo.lock b/Cargo.lock index d6c4beedb8..f02b6d6e1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6364,6 +6364,7 @@ dependencies = [ "unicode-segmentation", "url", "windows-sys 0.61.2", + "writeable", "x509-cert", ] diff --git a/changelog.d/9451-9468-intl-method-source.md b/changelog.d/9451-9468-intl-method-source.md new file mode 100644 index 0000000000..033cf88709 --- /dev/null +++ b/changelog.d/9451-9468-intl-method-source.md @@ -0,0 +1,15 @@ +### Fixed + +- **Class methods and accessors now retain their original source text.** + `String(C.prototype.method)`, direct `.toString()`, template coercion, and + reflected getter/setter functions return the source MethodDefinition rather + than a synthesized native-function body. Object-literal accessors also + receive their specified `get name` / `set name` function names. CommonJS + class expressions keep assignment-inferred names without exposing Perry's + internal anonymous-default registration key. Fixes #9468. + +- **Default `Intl.DateTimeFormat` dates now use the locale's CLDR numeric + pattern.** The implicit numeric year/month/day field set, its + `formatToParts()` output, and `Date.prototype.toLocaleDateString()` now agree + on locale order, separators, and padding instead of falling back to the + hard-coded US layout. Fixes #9451. diff --git a/crates/perry-codegen/src/codegen/artifact_source_text.rs b/crates/perry-codegen/src/codegen/artifact_source_text.rs new file mode 100644 index 0000000000..794eed3ba7 --- /dev/null +++ b/crates/perry-codegen/src/codegen/artifact_source_text.rs @@ -0,0 +1,117 @@ +//! Source-text registration for raw class method and accessor symbols. +//! +//! Class members are not ordinary closure wrappers, so their retained source +//! must be paired with the LLVM body symbol codegen actually emitted. Kept out +//! of `artifacts.rs` so that file remains below the repository's 2,000-line +//! limit. + +use std::collections::HashSet; + +use perry_hir::types::FuncId; +use perry_hir::Module as HirModule; + +use crate::module::LlModule; + +use super::helpers::{scoped_method_name, scoped_static_method_name}; + +pub(super) fn extend_class_method_source_text( + hir: &HirModule, + module_prefix: &str, + llmod: &LlModule, + user_fn_source: &mut Vec<(String, String)>, +) { + // An HIR registry entry is not proof that this module emitted the body: a + // cross-module or typed-only accessor can remain present without a local + // definition. Referencing such a symbol from module initialization makes + // LLVM reject the module, so `has_function` is the final authority. + let mut seen: HashSet = user_fn_source + .iter() + .map(|(symbol, _)| symbol.clone()) + .collect(); + let mut push_defined = |func_id: FuncId, symbol: String| { + let Some(source) = hir.closure_source_text.get(&func_id) else { + return; + }; + if symbol.is_empty() || !llmod.has_function(&symbol) || !seen.insert(symbol.clone()) { + return; + } + user_fn_source.push((symbol, source.clone())); + }; + + for class in &hir.classes { + if class.id == 0 { + continue; + } + for method in &class.methods { + push_defined( + method.id, + scoped_method_name(module_prefix, &class.name, &method.name), + ); + } + for member in class + .computed_members + .iter() + .filter(|member| !member.is_static) + { + push_defined( + member.function.id, + scoped_method_name(module_prefix, &class.name, &member.function.name), + ); + } + for (prop, getter) in &class.getters { + let symbol = if class.static_accessor_fn_ids.contains(&getter.id) { + scoped_static_method_name( + module_prefix, + class.id, + &class.name, + &format!("__get_{prop}"), + ) + } else { + scoped_method_name( + module_prefix, + &class.name, + &format!("__get_{}", getter.name), + ) + }; + push_defined(getter.id, symbol); + } + for (prop, setter) in &class.setters { + let symbol = if class.static_accessor_fn_ids.contains(&setter.id) { + scoped_static_method_name( + module_prefix, + class.id, + &class.name, + &format!("__set_{prop}"), + ) + } else { + scoped_method_name( + module_prefix, + &class.name, + &format!("__set_{}", setter.name), + ) + }; + push_defined(setter.id, symbol); + } + for method in &class.static_methods { + push_defined( + method.id, + scoped_static_method_name(module_prefix, class.id, &class.name, &method.name), + ); + } + for member in class + .computed_members + .iter() + .filter(|member| member.is_static) + { + push_defined( + member.function.id, + scoped_static_method_name( + module_prefix, + class.id, + &class.name, + &member.function.name, + ), + ); + } + } +} diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index 4e69575e47..6f845e6276 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -1854,6 +1854,16 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { user_fn_source.push((sym, src.clone())); } + // #9468: method/accessor bodies are raw symbols rather than closure + // wrappers. Pair retained MethodDefinition text only with symbols this + // module actually emitted; the helper also preserves the file-size gate. + super::artifact_source_text::extend_class_method_source_text( + hir, + module_prefix, + llmod, + &mut user_fn_source, + ); + // Wall 51: the standalone-ctor arity registered into CLASS_CONSTRUCTORS must // match the arity of the ctor function actually emitted above (which, for a // no-own-ctor class with heritage, is the synthesized `super(...args)` diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 0c6d7b52d9..ba4899a9ef 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -179,6 +179,7 @@ mod argument_shape_clone_tests; pub(crate) mod arguments; mod artifact_context; mod artifact_display_names; +mod artifact_source_text; mod artifacts; mod boxed_locals; #[cfg(test)] diff --git a/crates/perry-hir/src/destructuring/var_decl.rs b/crates/perry-hir/src/destructuring/var_decl.rs index 072b1b5e5e..81e30a2d9b 100644 --- a/crates/perry-hir/src/destructuring/var_decl.rs +++ b/crates/perry-hir/src/destructuring/var_decl.rs @@ -105,7 +105,25 @@ pub(crate) fn lower_var_decl_with_destructuring( if let Some(init_ast) = decl.init.as_ref() { result.extend(predeclare_implicit_assignment_targets(ctx, init_ast)); } - let init = decl.init.as_ref().map(|e| lower_expr(ctx, e)).transpose()?; + // A simple binding performs NamedEvaluation for an anonymous + // function/class initializer (`const C = class {}`). Most class + // expressions take stmt.rs's direct class fast path, but a + // pre-existing class-registry entry can deliberately divert one + // through this generic path (notably the CommonJS factory's + // function-scope pre-registration). Preserve the binding name in + // that path too; the helper filters out named definitions and + // non-NamedEvaluation expressions before installing the context. + let init = decl + .init + .as_ref() + .map(|e| { + crate::lower::expr_assign::lower_rhs_with_assignment_name( + ctx, + e, + Some(name.clone()), + ) + }) + .transpose()?; if matches!(ty, Type::Any) { match &init { Some(Expr::NativeMethodCall { module, method, .. }) => { diff --git a/crates/perry-hir/src/lower/expr_assign.rs b/crates/perry-hir/src/lower/expr_assign.rs index a34f3055f1..d593a91d69 100644 --- a/crates/perry-hir/src/lower/expr_assign.rs +++ b/crates/perry-hir/src/lower/expr_assign.rs @@ -56,7 +56,7 @@ pub(crate) fn rhs_accepts_assignment_name(expr: &ast::Expr) -> bool { } } -fn lower_rhs_with_assignment_name( +pub(crate) fn lower_rhs_with_assignment_name( ctx: &mut LoweringContext, rhs: &ast::Expr, name: Option, diff --git a/crates/perry-hir/src/lower/expr_object.rs b/crates/perry-hir/src/lower/expr_object.rs index 4bf17bb602..dedfdd6c15 100644 --- a/crates/perry-hir/src/lower/expr_object.rs +++ b/crates/perry-hir/src/lower/expr_object.rs @@ -501,6 +501,19 @@ fn lower_accessor_prop( }; let func_id = ctx.fresh_func(); + // #9468: accessor definitions run SetFunctionName with the `get`/`set` + // prefix. Class accessors already acquire this name when reflected from a + // descriptor; object-literal accessors are ordinary closure values and + // therefore need the same metadata recorded at lowering. + if let MethodKeyKind::Static(key) = &accessor_key { + let prefix = if setter_param.is_some() { + "set " + } else { + "get " + }; + ctx.closure_display_names + .insert(func_id, format!("{prefix}{key}")); + } let outer_locals: Vec<(String, LocalId)> = ctx .locals .iter() diff --git a/crates/perry-hir/src/lower_decl/class_decl.rs b/crates/perry-hir/src/lower_decl/class_decl.rs index 6daacccc90..d7d0080aa8 100644 --- a/crates/perry-hir/src/lower_decl/class_decl.rs +++ b/crates/perry-hir/src/lower_decl/class_decl.rs @@ -226,6 +226,28 @@ pub fn lower_class_decl( .insert(class_id, class_decl.ident.sym.to_string()); } capture_class_source(ctx, class_id, &class_decl.class); + // cjs_wrap rewrites a sole `module.exports = class { ... }` into a + // declaration under this reserved key so Perry can hoist and register the + // class. The key is compiler-only: the original class expression had no + // NamedEvaluation context, therefore its observable `.name` is empty and + // its retained source must not expose the injected identifier. + const CJS_ANONYMOUS_DEFAULT: &str = "__perry_cjs_default__"; + if class_decl.ident.sym.as_ref() == CJS_ANONYMOUS_DEFAULT { + ctx.class_display_names.insert(class_id, String::new()); + if let Some(source) = ctx.class_source_text.get_mut(&class_id) { + if let Some(after_class) = source.strip_prefix("class") { + let trimmed = after_class.trim_start(); + if let Some(after_name) = trimmed.strip_prefix(CJS_ANONYMOUS_DEFAULT) { + let name_is_complete = after_name.as_bytes().first().is_none_or(|byte| { + !byte.is_ascii_alphanumeric() && *byte != b'_' && *byte != b'$' + }); + if name_is_complete { + *source = format!("class{after_name}"); + } + } + } + } + } if let Some(ast::Expr::Ident(parent)) = class_decl.class.super_class.as_deref() { if let Some(crate::lower::fn_ctor_env::FnCtorShape::DynCtor(kind)) = ctx.fn_ctor_env.entries.get(parent.sym.as_ref()).cloned() diff --git a/crates/perry-hir/src/lower_decl/class_members.rs b/crates/perry-hir/src/lower_decl/class_members.rs index d830af083a..2e9f0b13fb 100644 --- a/crates/perry-hir/src/lower_decl/class_members.rs +++ b/crates/perry-hir/src/lower_decl/class_members.rs @@ -9,6 +9,40 @@ use crate::lower_types::*; use super::*; +/// #9468: retain a class member's MethodDefinition source under the FuncId of +/// its compiled method/accessor body. Unlike ordinary functions, class members +/// are emitted as raw `perry_method_*` / `perry_static_*` symbols, but the +/// source registry can still use the same FuncId-to-source handoff. +/// +/// SWC's ClassMethod span includes the class-only `static` modifier. That +/// modifier is not part of the function object's [[SourceText]] (`String(C.m)` +/// starts at the method definition itself), so remove it while preserving the +/// rest byte-for-byte, including `get`/`set`, `async`, `*`, and comments. +fn capture_class_method_source( + ctx: &mut LoweringContext, + func_id: crate::types::FuncId, + method: &ast::ClassMethod, +) { + let Some(mut src) = crate::ir::current_module_source_slice(method.span.lo.0, method.span.hi.0) + else { + return; + }; + if method.is_static { + let leading_ws = src.len() - src.trim_start().len(); + let candidate = &src[leading_ws..]; + if let Some(after_static) = candidate.strip_prefix("static") { + if after_static + .as_bytes() + .first() + .is_some_and(u8::is_ascii_whitespace) + { + src = after_static.trim_start().to_string(); + } + } + } + ctx.closure_source_text.insert(func_id, src); +} + pub fn lower_constructor( ctx: &mut LoweringContext, class_name: &str, @@ -670,6 +704,7 @@ pub fn lower_class_method_with_name( ctx.exit_type_param_scope(); let func_id = ctx.fresh_func(); + capture_class_method_source(ctx, func_id, method); // Record the param-prologue length for generator methods so the generator // transform runs param binding (default guards + destructuring) synchronously // at call time per spec FunctionDeclarationInstantiation order. Without this, @@ -799,8 +834,11 @@ pub fn lower_getter_method_with_name( ctx.exit_scope(scope_mark); ctx.in_nonarrow_fn = saved_in_nonarrow_fn; + let func_id = ctx.fresh_func(); + capture_class_method_source(ctx, func_id, method); + Ok(Function { - id: ctx.fresh_func(), + id: func_id, name, type_params: Vec::new(), params: Vec::new(), @@ -967,8 +1005,11 @@ pub fn lower_setter_method_with_name( ctx.exit_scope(scope_mark); ctx.in_nonarrow_fn = saved_in_nonarrow_fn; + let func_id = ctx.fresh_func(); + capture_class_method_source(ctx, func_id, method); + Ok(Function { - id: ctx.fresh_func(), + id: func_id, name, type_params: Vec::new(), params, diff --git a/crates/perry-runtime/Cargo.toml b/crates/perry-runtime/Cargo.toml index 0f83c76603..f2546c6af5 100644 --- a/crates/perry-runtime/Cargo.toml +++ b/crates/perry-runtime/Cargo.toml @@ -212,7 +212,7 @@ proc-ipc = [] intl-locale = ["dep:icu_locale", "dep:icu_locale_core"] # CLDR-accurate Intl.DateTimeFormat / toLocaleString date-time patterns and a # compiled IANA database for explicit named `timeZone` options. -intl-datetime = ["dep:icu_datetime", "dep:icu_time", "dep:icu_calendar", "dep:icu_locale_core", "dep:timezone_provider"] +intl-datetime = ["dep:icu_datetime", "dep:icu_time", "dep:icu_calendar", "dep:icu_locale_core", "dep:timezone_provider", "dep:writeable"] # `full` only opt-ins the small Node-API helpers (os.hostname / os.homedir). # `postgres`, `redis`, `whoami` were previously listed here but were either # unimported (postgres, whoami) or only used by a now-deleted `redis_client.rs` @@ -351,6 +351,8 @@ icu_locale_core = { version = "2", optional = true } icu_datetime = { version = "2", default-features = false, features = ["compiled_data"], optional = true } icu_time = { version = "2", default-features = false, features = ["compiled_data"], optional = true } icu_calendar = { version = "2", default-features = false, features = ["compiled_data"], optional = true } +# ICU's semantic field annotations drive Intl.DateTimeFormat#formatToParts. +writeable = { version = "0.6", optional = true } idna = { version = "1", optional = true } url = { version = "2", optional = true } # #4911: real node:dns resolve*/reverse. hickory-proto provides DNS wire-format diff --git a/crates/perry-runtime/src/closure/dispatch.rs b/crates/perry-runtime/src/closure/dispatch.rs index c19fca9c77..bdfb309264 100644 --- a/crates/perry-runtime/src/closure/dispatch.rs +++ b/crates/perry-runtime/src/closure/dispatch.rs @@ -20,7 +20,10 @@ mod errors; mod validate; mod value_call; -pub(crate) use bound::{coerce_call_this, rebind_explicit_this, reify_function_method_value}; +pub(crate) use bound::{ + bound_method_source_func_ptr, coerce_call_this, rebind_explicit_this, + reify_function_method_value, +}; pub use bound::{dispatch_bound_function, dispatch_bound_method, js_function_bind}; pub(crate) use errors::reset_throw_not_callable_counter; diff --git a/crates/perry-runtime/src/closure/dispatch/bound.rs b/crates/perry-runtime/src/closure/dispatch/bound.rs index 74d753cc2b..fffb633467 100644 --- a/crates/perry-runtime/src/closure/dispatch/bound.rs +++ b/crates/perry-runtime/src/closure/dispatch/bound.rs @@ -3,6 +3,57 @@ use super::*; +/// Resolve a bound class-method value to the raw method body whose source was +/// registered by codegen. Ordinary instance methods carry an owner prototype +/// ref plus their string name; statics carry a constructor ref; symbol-keyed +/// methods carry the already-resolved function pointer directly. +/// +/// Returns `None` for the many non-class users of the BOUND_METHOD sentinel +/// (native module methods, `Function.prototype.call` reifications, and so on), +/// which correctly retain synthesized native source text. +pub(crate) unsafe fn bound_method_source_func_ptr(closure: *const ClosureHeader) -> Option { + if closure.is_null() + || (*closure).func_ptr != BOUND_METHOD_FUNC_PTR + || crate::closure::real_capture_count((*closure).capture_count) < 3 + { + return None; + } + + let method_name_ptr = js_closure_get_capture_ptr(closure, 1) as *const u8; + if method_name_ptr == crate::object::SYMBOL_BOUND_METHOD_NAME.as_ptr() { + if crate::closure::real_capture_count((*closure).capture_count) < 5 { + return None; + } + return (js_closure_get_capture_ptr(closure, 3) as usize != 0) + .then(|| js_closure_get_capture_ptr(closure, 3) as usize); + } + + let method_name_len = js_closure_get_capture_ptr(closure, 2) as usize; + if method_name_ptr.is_null() || method_name_len == 0 { + return None; + } + let name = + std::str::from_utf8(std::slice::from_raw_parts(method_name_ptr, method_name_len)).ok()?; + let receiver = js_closure_get_capture_f64(closure, 0); + + if let Some(class_id) = crate::object::class_prototype_ref_id(receiver) { + return crate::object::lookup_class_method_in_chain(class_id, name) + .map(|(func_ptr, ..)| func_ptr); + } + + let static_class_id = crate::object::class_ref_id(receiver).or_else(|| { + crate::object::is_class_object_value(receiver).then(|| { + let obj = crate::value::JSValue::from_bits(receiver.to_bits()) + .as_pointer::(); + crate::object::js_object_get_class_id(obj) + }) + }); + static_class_id + .filter(|class_id| *class_id != 0) + .and_then(|class_id| crate::object::lookup_static_method_in_chain(class_id, name)) + .map(|(func_ptr, ..)| func_ptr) +} + /// Dispatch a bound method call with the given arguments. /// Extracts the namespace object and method name from the closure captures, /// then calls js_native_call_method with the packed arguments. diff --git a/crates/perry-runtime/src/closure/mod.rs b/crates/perry-runtime/src/closure/mod.rs index e41cb4d81c..0708298460 100644 --- a/crates/perry-runtime/src/closure/mod.rs +++ b/crates/perry-runtime/src/closure/mod.rs @@ -44,6 +44,10 @@ pub use registry::{ CLOSURE_MAGIC, NO_THIS_REBIND_FLAG, }; +pub(crate) use dispatch::{ + bound_method_source_func_ptr, coerce_call_this, rebind_explicit_this, + reify_function_method_value, reset_throw_not_callable_counter, +}; pub use dispatch::{ clean_closure_ptr, dispatch_bound_function, dispatch_bound_method, get_valid_func_ptr, js_closure_call0, js_closure_call1, js_closure_call10, js_closure_call11, js_closure_call12, @@ -53,10 +57,6 @@ pub use dispatch::{ js_closure_call_apply_with_spread, js_closure_call_array, js_function_bind, js_native_call_value, throw_not_callable, DirectCall1, DirectCall2, DirectCall3, DirectCall4, }; -pub(crate) use dispatch::{ - coerce_call_this, rebind_explicit_this, reify_function_method_value, - reset_throw_not_callable_counter, -}; pub use unbox::{js_closure_unbox_callee_checked, js_closure_unbox_callee_checked_rebind}; #[cfg(test)] diff --git a/crates/perry-runtime/src/intl/date_collator.rs b/crates/perry-runtime/src/intl/date_collator.rs index 948e0e6c3b..396c1fc7ad 100644 --- a/crates/perry-runtime/src/intl/date_collator.rs +++ b/crates/perry-runtime/src/intl/date_collator.rs @@ -7,6 +7,8 @@ use crate::value::{js_nanbox_pointer, JSValue}; mod compare; use compare::{collator_compare_order, CollatorCompareOptions}; +mod icu; +use icu::{icu_component_parts, icu_components, icu_style}; /// ECMA-402 FormatDateTime / HandleDateTimeValue step 1: coerce the /// `format`/`formatToParts` argument to a TimeClip'd integer-millisecond value. @@ -139,6 +141,7 @@ fn format_parts_with_dtf_obj( let secs = dtf_zone_local_secs(obj, (ms as i64).div_euclid(1000), temporal_kind); let (year, month, day, hour, minute, second) = crate::date::timestamp_to_components(secs); let mi = month.saturating_sub(1).min(11) as usize; + let locale = get_string_field(obj, KEY_LOCALE).unwrap_or_else(|| "en-US".to_string()); let date_style = get_string_field(obj, KEY_DATE_STYLE); let time_style = get_string_field(obj, KEY_TIME_STYLE); @@ -415,6 +418,26 @@ fn format_parts_with_dtf_obj( let day_period_opt = get_string_field(obj, KEY_DAY_PERIOD); let fractional_digits = get_number_field(obj, KEY_FRACTIONAL).map(|n| (n as u8).clamp(1, 3)); + if hour_opt.is_none() + && minute_opt.is_none() + && second_opt.is_none() + && day_period_opt.is_none() + && era_opt.is_none() + && fractional_digits.is_none() + { + if let Some(parts) = icu_component_parts( + &locale, + year, + month, + day, + year_opt.as_deref(), + month_opt.as_deref(), + day_opt.as_deref(), + weekday_opt.as_deref(), + ) { + return parts; + } + } build_parts_from_components( year, month, @@ -845,117 +868,6 @@ fn era_string(year: i32, style: &str) -> &'static str { } } -/// Format a `dateStyle`/`timeStyle` combination via icu4x (CLDR patterns). -/// Returns `None` when the icu feature is off, the caller opted out (`enabled` -/// = false, e.g. a Temporal partial), or the option combination is unmapped -/// (notably a `long`/`full` timeStyle, which carries a localized time-zone -/// name) — the caller then falls back to the bespoke formatters below. -#[cfg(feature = "intl-datetime")] -#[allow(clippy::too_many_arguments)] -fn icu_style( - enabled: bool, - locale: &str, - year: i32, - month: u32, - day: u32, - hour: u32, - minute: u32, - second: u32, - date_style: Option<&str>, - time_style: Option<&str>, - hour_cycle: Option<&str>, - hour12: Option, -) -> Option { - use super::icu_dtf::{self, Len, Req}; - if !enabled { - return None; - } - icu_dtf::format(&Req { - locale, - year, - month: month as u8, - day: day as u8, - hour: hour as u8, - minute: minute as u8, - second: second as u8, - date_style: date_style.and_then(Len::parse), - time_style: time_style.and_then(Len::parse), - hour_cycle, - hour12, - }) -} - -#[cfg(not(feature = "intl-datetime"))] -#[allow(clippy::too_many_arguments)] -fn icu_style( - _enabled: bool, - _locale: &str, - _year: i32, - _month: u32, - _day: u32, - _hour: u32, - _minute: u32, - _second: u32, - _date_style: Option<&str>, - _time_style: Option<&str>, - _hour_cycle: Option<&str>, - _hour12: Option, -) -> Option { - None -} - -/// Format a date-only, name-bearing component set via icu4x. `None` when the -/// feature is off or icu can't reproduce the combo (numeric-only, narrow, -/// structurally inexpressible), so the caller falls back. -#[cfg(feature = "intl-datetime")] -#[allow(clippy::too_many_arguments)] -fn icu_components( - locale: &str, - year: i32, - month: u32, - day: u32, - year_opt: Option<&str>, - month_opt: Option<&str>, - day_opt: Option<&str>, - weekday_opt: Option<&str>, -) -> Option { - use super::icu_dtf::{self, CompReq}; - icu_dtf::format_components(&CompReq { - locale, - year, - month: month as u8, - day: day as u8, - hour: 0, - minute: 0, - second: 0, - has_year: year_opt.is_some(), - has_month: month_opt.is_some(), - has_day: day_opt.is_some(), - month_style: month_opt, - weekday_style: weekday_opt, - has_hour: false, - has_minute: false, - has_second: false, - hour_cycle: None, - hour12: None, - }) -} - -#[cfg(not(feature = "intl-datetime"))] -#[allow(clippy::too_many_arguments)] -fn icu_components( - _locale: &str, - _year: i32, - _month: u32, - _day: u32, - _year_opt: Option<&str>, - _month_opt: Option<&str>, - _day_opt: Option<&str>, - _weekday_opt: Option<&str>, -) -> Option { - None -} - fn format_date_style(year: i32, month: u32, day: u32, secs: i64, style: &str) -> String { let mi = month.saturating_sub(1).min(11) as usize; match style { diff --git a/crates/perry-runtime/src/intl/date_collator/icu.rs b/crates/perry-runtime/src/intl/date_collator/icu.rs new file mode 100644 index 0000000000..3781c87b2e --- /dev/null +++ b/crates/perry-runtime/src/intl/date_collator/icu.rs @@ -0,0 +1,167 @@ +//! Thin feature-gated adapters from DateTimeFormat state to `icu_dtf`. +//! +//! Kept separate from `date_collator.rs` so the primary implementation stays +//! below the repository's 2,000-line limit. + +/// Format a `dateStyle`/`timeStyle` combination via icu4x (CLDR patterns). +/// Returns `None` when the icu feature is off, the caller opted out (`enabled` +/// = false, e.g. a Temporal partial), or the option combination is unmapped. +#[cfg(feature = "intl-datetime")] +#[allow(clippy::too_many_arguments)] +pub(super) fn icu_style( + enabled: bool, + locale: &str, + year: i32, + month: u32, + day: u32, + hour: u32, + minute: u32, + second: u32, + date_style: Option<&str>, + time_style: Option<&str>, + hour_cycle: Option<&str>, + hour12: Option, +) -> Option { + use super::super::icu_dtf::{self, Len, Req}; + if !enabled { + return None; + } + icu_dtf::format(&Req { + locale, + year, + month: month as u8, + day: day as u8, + hour: hour as u8, + minute: minute as u8, + second: second as u8, + date_style: date_style.and_then(Len::parse), + time_style: time_style.and_then(Len::parse), + hour_cycle, + hour12, + }) +} + +#[cfg(not(feature = "intl-datetime"))] +#[allow(clippy::too_many_arguments)] +pub(super) fn icu_style( + _enabled: bool, + _locale: &str, + _year: i32, + _month: u32, + _day: u32, + _hour: u32, + _minute: u32, + _second: u32, + _date_style: Option<&str>, + _time_style: Option<&str>, + _hour_cycle: Option<&str>, + _hour12: Option, +) -> Option { + None +} + +/// Format a date-only component set via icu4x. `None` when the feature is off +/// or icu cannot reproduce the combination. +#[cfg(feature = "intl-datetime")] +#[allow(clippy::too_many_arguments)] +pub(super) fn icu_components( + locale: &str, + year: i32, + month: u32, + day: u32, + year_opt: Option<&str>, + month_opt: Option<&str>, + day_opt: Option<&str>, + weekday_opt: Option<&str>, +) -> Option { + use super::super::icu_dtf::{self, CompReq}; + icu_dtf::format_components(&CompReq { + locale, + year, + month: month as u8, + day: day as u8, + hour: 0, + minute: 0, + second: 0, + has_year: year_opt.is_some(), + has_month: month_opt.is_some(), + has_day: day_opt.is_some(), + year_style: year_opt, + month_style: month_opt, + day_style: day_opt, + weekday_style: weekday_opt, + has_hour: false, + has_minute: false, + has_second: false, + hour_cycle: None, + hour12: None, + }) +} + +#[cfg(not(feature = "intl-datetime"))] +#[allow(clippy::too_many_arguments)] +pub(super) fn icu_components( + _locale: &str, + _year: i32, + _month: u32, + _day: u32, + _year_opt: Option<&str>, + _month_opt: Option<&str>, + _day_opt: Option<&str>, + _weekday_opt: Option<&str>, +) -> Option { + None +} + +/// Semantic counterpart of `icu_components`, used by `formatToParts` for the +/// default numeric Y/M/D field set so its order and punctuation match CLDR. +#[cfg(feature = "intl-datetime")] +#[allow(clippy::too_many_arguments)] +pub(super) fn icu_component_parts( + locale: &str, + year: i32, + month: u32, + day: u32, + year_opt: Option<&str>, + month_opt: Option<&str>, + day_opt: Option<&str>, + weekday_opt: Option<&str>, +) -> Option> { + use super::super::icu_dtf::{self, CompReq}; + icu_dtf::format_components_parts(&CompReq { + locale, + year, + month: month as u8, + day: day as u8, + hour: 0, + minute: 0, + second: 0, + has_year: year_opt.is_some(), + has_month: month_opt.is_some(), + has_day: day_opt.is_some(), + year_style: year_opt, + month_style: month_opt, + day_style: day_opt, + weekday_style: weekday_opt, + has_hour: false, + has_minute: false, + has_second: false, + hour_cycle: None, + hour12: None, + }) +} + +#[cfg(not(feature = "intl-datetime"))] +#[allow(clippy::too_many_arguments)] +pub(super) fn icu_component_parts( + _locale: &str, + _year: i32, + _month: u32, + _day: u32, + _year_opt: Option<&str>, + _month_opt: Option<&str>, + _day_opt: Option<&str>, + _weekday_opt: Option<&str>, +) -> Option> { + None +} diff --git a/crates/perry-runtime/src/intl/icu_dtf.rs b/crates/perry-runtime/src/intl/icu_dtf.rs index 4b0627cd8b..b9780265b7 100644 --- a/crates/perry-runtime/src/intl/icu_dtf.rs +++ b/crates/perry-runtime/src/intl/icu_dtf.rs @@ -10,11 +10,15 @@ use icu_datetime::fieldsets; use icu_datetime::fieldsets::builder::{DateFields, FieldSetBuilder}; use icu_datetime::input::{Date, DateTime, Time}; -use icu_datetime::options::{Length, TimePrecision}; +use icu_datetime::options::{Alignment, Length, TimePrecision, YearStyle}; use icu_datetime::preferences::HourCycle; use icu_datetime::DateTimeFormatter; use icu_datetime::DateTimeFormatterPreferences; use icu_locale_core::Locale; +use writeable::{Part, PartsWrite, Writeable}; + +mod default_numeric_patterns; +use default_numeric_patterns::YMD_PATTERNS; /// dateStyle / timeStyle length. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -177,9 +181,13 @@ pub(crate) struct CompReq<'a> { pub has_year: bool, pub has_month: bool, pub has_day: bool, + /// `year` option value (`numeric`/`2-digit`), or `None` when absent. + pub year_style: Option<&'a str>, /// `month` option value (`numeric`/`2-digit`/`short`/`long`/`narrow`), or /// `None` when month is absent. pub month_style: Option<&'a str>, + /// `day` option value (`numeric`/`2-digit`), or `None` when absent. + pub day_style: Option<&'a str>, /// `weekday` option value (`short`/`long`/`narrow`), or `None` when absent. pub weekday_style: Option<&'a str>, pub has_hour: bool, @@ -191,17 +199,21 @@ pub(crate) struct CompReq<'a> { /// Format an explicit-component request via icu4x's dynamic `FieldSetBuilder`. /// -/// Only combos icu's *semantic* field sets reproduce faithfully are handled: -/// a date part must carry a spelled month (`short`/`long`) or a weekday (icu -/// gets the localized name + field order right). A **purely numeric** date is -/// deliberately rejected (returns `None`): its minimal-digit CLDR pattern -/// (`5.1.2026` for de) can't be expressed by icu's `Short` length, which pads -/// and truncates the year (`05.01.26`) — the caller's numeric assembly owns -/// that. `narrow` and structurally-inexpressible field combos also return -/// `None` for the fallback. +/// Only combos icu's semantic field sets reproduce faithfully are handled: a +/// date part must carry a spelled month (`short`/`long`), a weekday, or the +/// ECMA-402 default numeric Y/M/D set. The latter uses CLDR's classical `yMd` +/// pattern (see `format_default_numeric_parts`); semantic `Short` is not +/// equivalent because it pads German/Japanese fields. `narrow` and +/// structurally-inexpressible field combos still return `None` for fallback. pub(crate) fn format_components(req: &CompReq) -> Option { let has_weekday = req.weekday_style.is_some(); let has_date = req.has_year || req.has_month || req.has_day || has_weekday; + let has_time = req.has_hour || req.has_minute || req.has_second; + let numeric_ymd = req.year_style == Some("numeric") + && req.month_style == Some("numeric") + && req.day_style == Some("numeric") + && !has_weekday + && !has_time; // Name-bearing = a spelled month or a weekday; only these route to icu. let month_len = match req.month_style { @@ -216,10 +228,19 @@ pub(crate) fn format_components(req: &CompReq) -> Option { }; let name_bearing = month_len.is_some() || weekday_len.is_some(); - // Reject narrow (no semantic-fieldset equivalent) and purely numeric dates. + // Reject narrow (no semantic-fieldset equivalent) and numeric component + // combinations other than the default Y/M/D set. if matches!(req.month_style, Some("narrow")) || matches!(req.weekday_style, Some("narrow")) { return None; } + if numeric_ymd { + return format_default_numeric_parts(req).map(|parts| { + parts + .into_iter() + .map(|(_, value)| value) + .collect::() + }); + } if has_date && !name_bearing { return None; } @@ -289,6 +310,204 @@ pub(crate) fn format_components(req: &CompReq) -> Option { Some(normalize(&formatted)) } +#[derive(Default)] +struct DatePartsSink { + active: Vec, + parts: Vec<(&'static str, String)>, +} + +impl std::fmt::Write for DatePartsSink { + fn write_str(&mut self, value: &str) -> std::fmt::Result { + let kind = self + .active + .iter() + .rev() + .find(|part| part.category == "datetime") + .map(|part| part.value) + .unwrap_or("literal"); + if let Some((last_kind, last_value)) = self.parts.last_mut() { + if *last_kind == kind { + last_value.push_str(value); + return Ok(()); + } + } + self.parts.push((kind, value.to_string())); + Ok(()) + } +} + +impl PartsWrite for DatePartsSink { + type SubPartsWrite = Self; + + fn with_part( + &mut self, + part: Part, + mut write: impl FnMut(&mut Self::SubPartsWrite) -> std::fmt::Result, + ) -> std::fmt::Result { + self.active.push(part); + let result = write(self); + self.active.pop(); + result + } +} + +fn ymd_pattern(locale: &Locale) -> Option<&'static str> { + // The table contains canonical CLDR locale identifiers without Unicode or + // private-use extensions. Fall back one subtag at a time for structurally + // valid locales not materialized as their own CLDR data locale. + let id = locale.id.to_string(); + let mut candidate = id.as_str(); + loop { + if let Ok(index) = YMD_PATTERNS.binary_search_by(|entry| entry.0.cmp(candidate)) { + return Some(YMD_PATTERNS[index].1); + } + candidate = candidate.rsplit_once('-').map(|(parent, _)| parent)?; + } +} + +fn push_part(parts: &mut Vec<(&'static str, String)>, kind: &'static str, value: impl AsRef) { + let value = value.as_ref(); + if value.is_empty() { + return; + } + if let Some((last_kind, last_value)) = parts.last_mut() { + if *last_kind == kind { + last_value.push_str(value); + return; + } + } + parts.push((kind, value.to_string())); +} + +fn without_numeric_padding(value: &str, number: u8) -> &str { + if number >= 10 { + return value; + } + // `Alignment::Column` makes a one-digit month/day two localized digits. + // Decimal digits are single Unicode scalars; discard that leading zero. + value + .char_indices() + .nth(1) + .map_or(value, |(index, _)| &value[index..]) +} + +/// Render the ECMA-402 default numeric fields with CLDR's classical `yMd` +/// skeleton. ICU4X 2.x compiles semantic field sets but no longer exposes its +/// classical skeleton matcher at runtime, and semantic `YMD::short()` is not +/// the same pattern: for example, it yields `05.01.2026` in German and +/// `2026/01/05` in Japanese. The small generated table carries just `yMd`'s +/// localized pattern; ICU still supplies calendar conversion and digits. +fn format_default_numeric_parts(req: &CompReq) -> Option> { + let locale: Locale = req.locale.parse().ok()?; + let pattern = ymd_pattern(&locale)?; + let mut builder = FieldSetBuilder::default(); + builder.date_fields = Some(DateFields::YMD); + builder.length = Some(Length::Short); + builder.alignment = Some(Alignment::Column); + builder.year_style = Some(YearStyle::Full); + + let date = Date::try_new_iso(req.year, req.month.into(), req.day.into()).ok()?; + let dtf = DateTimeFormatter::try_new((&locale).into(), builder.build_date().ok()?).ok()?; + let mut sink = DatePartsSink::default(); + dtf.format(&date).write_to_parts(&mut sink).ok()?; + + let year = sink + .parts + .iter() + .find(|(kind, _)| *kind == "year" || *kind == "relatedYear")? + .1 + .as_str(); + let month = sink + .parts + .iter() + .find(|(kind, _)| *kind == "month")? + .1 + .as_str(); + let day = sink + .parts + .iter() + .find(|(kind, _)| *kind == "day")? + .1 + .as_str(); + + let mut parts = Vec::with_capacity(5); + let mut chars = pattern.chars().peekable(); + let mut literal = String::new(); + let mut quoted = false; + while let Some(ch) = chars.next() { + if ch == '\'' { + if chars.peek() == Some(&'\'') { + chars.next(); + literal.push('\''); + } else { + quoted = !quoted; + } + continue; + } + if !quoted && matches!(ch, 'y' | 'M' | 'd') { + push_part(&mut parts, "literal", &literal); + literal.clear(); + let mut width = 1; + while chars.peek() == Some(&ch) { + chars.next(); + width += 1; + } + match ch { + 'y' => push_part(&mut parts, "year", year), + 'M' => push_part( + &mut parts, + "month", + if width == 1 { + without_numeric_padding(month, req.month) + } else { + month + }, + ), + 'd' => push_part( + &mut parts, + "day", + if width == 1 { + without_numeric_padding(day, req.day) + } else { + day + }, + ), + _ => unreachable!(), + } + } else { + literal.push(ch); + } + } + if quoted { + return None; + } + push_part(&mut parts, "literal", literal); + Some( + parts + .into_iter() + .map(|(kind, value)| (kind, normalize(&value))) + .collect(), + ) +} + +/// CLDR-ordered semantic parts for the default numeric Y/M/D component set. +/// ICU annotates fields before localized punctuation is written, so this stays +/// correct even when a locale changes both order and separators. +pub(crate) fn format_components_parts(req: &CompReq) -> Option> { + let numeric_ymd = req.year_style == Some("numeric") + && req.month_style == Some("numeric") + && req.day_style == Some("numeric") + && req.weekday_style.is_none() + && !req.has_hour + && !req.has_minute + && !req.has_second; + if !numeric_ymd { + return None; + } + + format_default_numeric_parts(req) +} + #[cfg(test)] mod tests { use super::*; @@ -419,7 +638,9 @@ mod tests { has_year: year.is_some(), has_month: month.is_some(), has_day: day.is_some(), + year_style: year, month_style: month, + day_style: day, weekday_style: weekday, has_hour: false, has_minute: false, @@ -468,16 +689,39 @@ mod tests { } #[test] - fn numeric_and_narrow_components_defer() { - // Pure-numeric date → None (numeric locale pattern owns it). - assert_eq!( - comp( - "de", + fn default_numeric_components_match_node() { + let cases = [ + ("de-DE", "5.1.2026"), + ("fr-FR", "05/01/2026"), + ("ja-JP", "2026/1/5"), + ("en-GB", "05/01/2026"), + ("en-US", "1/5/2026"), + ]; + for (locale, want) in cases { + let got = comp( + locale, Some("numeric"), Some("numeric"), Some("numeric"), - None - ), + None, + ); + assert_eq!(got.as_deref(), Some(want), "locale {locale}"); + } + } + + #[test] + fn default_numeric_pattern_table_is_strictly_sorted() { + assert!( + YMD_PATTERNS.windows(2).all(|pair| pair[0].0 < pair[1].0), + "YMD_PATTERNS must stay sorted and unique for binary search" + ); + } + + #[test] + fn unsupported_numeric_and_narrow_components_defer() { + // Numeric subsets retain the bespoke fallback. + assert_eq!( + comp("de", None, Some("numeric"), Some("numeric"), None), None ); // Narrow → None. diff --git a/crates/perry-runtime/src/intl/icu_dtf/default_numeric_patterns.rs b/crates/perry-runtime/src/intl/icu_dtf/default_numeric_patterns.rs new file mode 100644 index 0000000000..ee75d93cd4 --- /dev/null +++ b/crates/perry-runtime/src/intl/icu_dtf/default_numeric_patterns.rs @@ -0,0 +1,769 @@ +// @generated from Unicode CLDR 48.2.1 cldr-dates-full. +// The default ECMA-402 date request is the classical `yMd` skeleton. +pub(super) static YMD_PATTERNS: &[(&str, &str)] = &[ + ("aa", "y-MM-dd"), + ("aa-DJ", "y-MM-dd"), + ("aa-ER", "y-MM-dd"), + ("ab", "y-MM-dd"), + ("af", "y-MM-dd"), + ("af-NA", "y-MM-dd"), + ("agq", "d/M/y"), + ("ak", "y/M/d"), + ("am", "d/M/y"), + ("an", "y-MM-dd"), + ("ann", "y-MM-dd"), + ("apc", "y-MM-dd"), + ("ar", "d‏/M‏/y"), + ("ar-AE", "d‏/M‏/y"), + ("ar-BH", "d‏/M‏/y"), + ("ar-DJ", "d‏/M‏/y"), + ("ar-DZ", "d‏/M‏/y"), + ("ar-EG", "d‏/M‏/y"), + ("ar-EH", "d‏/M‏/y"), + ("ar-ER", "d‏/M‏/y"), + ("ar-IL", "d‏/M‏/y"), + ("ar-IQ", "d‏/M‏/y"), + ("ar-JO", "d‏/M‏/y"), + ("ar-KM", "d‏/M‏/y"), + ("ar-KW", "d‏/M‏/y"), + ("ar-LB", "d‏/M‏/y"), + ("ar-LY", "d‏/M‏/y"), + ("ar-MA", "d‏/M‏/y"), + ("ar-MR", "d‏/M‏/y"), + ("ar-OM", "d‏/M‏/y"), + ("ar-PS", "d‏/M‏/y"), + ("ar-QA", "d‏/M‏/y"), + ("ar-SA", "d‏/M‏/y"), + ("ar-SD", "d‏/M‏/y"), + ("ar-SO", "d‏/M‏/y"), + ("ar-SS", "d‏/M‏/y"), + ("ar-SY", "d‏/M‏/y"), + ("ar-TD", "d‏/M‏/y"), + ("ar-TN", "d‏/M‏/y"), + ("ar-YE", "d‏/M‏/y"), + ("arn", "y-MM-dd"), + ("as", "dd-MM-y"), + ("asa", "d/M/y"), + ("ast", "d/M/y"), + ("az", "dd.MM.y"), + ("az-Arab", "y-MM-dd"), + ("az-Arab-IQ", "y-MM-dd"), + ("az-Arab-TR", "y-MM-dd"), + ("az-Cyrl", "dd.MM.y"), + ("az-Latn", "dd.MM.y"), + ("ba", "dd.MM.y"), + ("bal", "dd-MM-y"), + ("bal-Arab", "dd-MM-y"), + ("bal-Latn", "d/M/y"), + ("bas", "d/M/y"), + ("be", "d.M.y"), + ("be-tarask", "d.M.y"), + ("bem", "d/M/y"), + ("bew", "y-MM-dd"), + ("bez", "d/M/y"), + ("bg", "d.MM.y 'г'."), + ("bgc", "y-MM-dd"), + ("bgn", "y-MM-dd"), + ("bgn-AE", "y-MM-dd"), + ("bgn-AF", "y-MM-dd"), + ("bgn-IR", "y-MM-dd"), + ("bgn-OM", "y-MM-dd"), + ("bho", "y-MM-dd"), + ("blo", "M/d/y"), + ("blt", "y-MM-dd"), + ("bm", "d/M/y"), + ("bm-Nkoo", "y-MM-dd"), + ("bn", "d/M/y"), + ("bn-IN", "d/M/y"), + ("bo", "y-MM-dd"), + ("bo-IN", "y-MM-dd"), + ("bqi", "y-MM-dd"), + ("br", "dd/MM/y"), + ("brx", "dd-MM-y"), + ("bs", "d. M. y."), + ("bs-Cyrl", "dd.MM.y."), + ("bs-Latn", "d. M. y."), + ("bss", "y-MM-dd"), + ("bua", "dd.MM.y"), + ("byn", "y-MM-dd"), + ("ca", "d/M/y"), + ("ca-AD", "d/M/y"), + ("ca-ES-valencia", "d/M/y"), + ("ca-FR", "d/M/y"), + ("ca-IT", "d/M/y"), + ("cad", "y-MM-dd"), + ("cch", "y-MM-dd"), + ("ccp", "d/M/y"), + ("ccp-IN", "d/M/y"), + ("ce", "y-MM-dd"), + ("ceb", "M/d/y"), + ("cgg", "d/M/y"), + ("cho", "y-MM-dd"), + ("chr", "M/d/y"), + ("cic", "y-MM-dd"), + ("ckb", "d/M/y"), + ("ckb-IR", "d/M/y"), + ("co", "y-MM-dd"), + ("cop", "y-MM-dd"), + ("cs", "d. M. y"), + ("csw", "y-MM-dd"), + ("cu", "y-MM-dd"), + ("cv", "y.MM.dd"), + ("cy", "d/M/y"), + ("da", "d.M.y"), + ("da-GL", "d.M.y"), + ("dav", "d/M/y"), + ("de", "d.M.y"), + ("de-AT", "d.M.y"), + ("de-BE", "d.M.y"), + ("de-CH", "d.M.y"), + ("de-IT", "d.M.y"), + ("de-LI", "d.M.y"), + ("de-LU", "d.M.y"), + ("dje", "d/M/y"), + ("doi", "d/M/y"), + ("dsb", "d.M.y"), + ("dua", "d/M/y"), + ("dv", "y-MM-dd"), + ("dyo", "d/M/y"), + ("dz", "y-M-d"), + ("ebu", "d/M/y"), + ("ee", "M/d/y"), + ("ee-TG", "M/d/y"), + ("el", "d/M/y"), + ("el-CY", "d/M/y"), + ("el-polyton", "d/M/y"), + ("en", "M/d/y"), + ("en-001", "dd/MM/y"), + ("en-150", "dd/MM/y"), + ("en-AE", "dd/MM/y"), + ("en-AG", "dd/MM/y"), + ("en-AI", "dd/MM/y"), + ("en-AS", "M/d/y"), + ("en-AT", "dd/MM/y"), + ("en-AU", "dd/MM/y"), + ("en-BB", "dd/MM/y"), + ("en-BE", "d/M/y"), + ("en-BI", "M/d/y"), + ("en-BM", "dd/MM/y"), + ("en-BS", "dd/MM/y"), + ("en-BW", "dd/MM/y"), + ("en-BZ", "dd/MM/y"), + ("en-CA", "y-MM-dd"), + ("en-CC", "dd/MM/y"), + ("en-CH", "dd.MM.y"), + ("en-CK", "dd/MM/y"), + ("en-CM", "dd/MM/y"), + ("en-CX", "dd/MM/y"), + ("en-CY", "dd/MM/y"), + ("en-CZ", "dd/MM/y"), + ("en-DE", "dd/MM/y"), + ("en-DG", "dd/MM/y"), + ("en-DK", "dd/MM/y"), + ("en-DM", "dd/MM/y"), + ("en-Dsrt", "y-MM-dd"), + ("en-EE", "dd/MM/y"), + ("en-ER", "dd/MM/y"), + ("en-ES", "dd/MM/y"), + ("en-FI", "dd/MM/y"), + ("en-FJ", "dd/MM/y"), + ("en-FK", "dd/MM/y"), + ("en-FM", "dd/MM/y"), + ("en-FR", "dd/MM/y"), + ("en-GB", "dd/MM/y"), + ("en-GD", "dd/MM/y"), + ("en-GE", "dd/MM/y"), + ("en-GG", "dd/MM/y"), + ("en-GH", "dd/MM/y"), + ("en-GI", "dd/MM/y"), + ("en-GM", "dd/MM/y"), + ("en-GS", "dd/MM/y"), + ("en-GU", "M/d/y"), + ("en-GY", "dd/MM/y"), + ("en-HK", "d/M/y"), + ("en-HU", "dd/MM/y"), + ("en-ID", "dd/MM/y"), + ("en-IE", "d/M/y"), + ("en-IL", "dd/MM/y"), + ("en-IM", "dd/MM/y"), + ("en-IN", "d/M/y"), + ("en-IO", "dd/MM/y"), + ("en-IT", "dd/MM/y"), + ("en-JE", "dd/MM/y"), + ("en-JM", "dd/MM/y"), + ("en-JP", "y/MM/dd"), + ("en-KE", "dd/MM/y"), + ("en-KI", "dd/MM/y"), + ("en-KN", "dd/MM/y"), + ("en-KY", "dd/MM/y"), + ("en-LC", "dd/MM/y"), + ("en-LR", "dd/MM/y"), + ("en-LS", "dd/MM/y"), + ("en-LT", "dd/MM/y"), + ("en-LV", "dd/MM/y"), + ("en-MG", "dd/MM/y"), + ("en-MH", "M/d/y"), + ("en-MO", "dd/MM/y"), + ("en-MP", "M/d/y"), + ("en-MS", "dd/MM/y"), + ("en-MT", "dd/MM/y"), + ("en-MU", "dd/MM/y"), + ("en-MV", "dd/MM/y"), + ("en-MW", "dd/MM/y"), + ("en-MY", "dd/MM/y"), + ("en-NA", "dd/MM/y"), + ("en-NF", "dd/MM/y"), + ("en-NG", "dd/MM/y"), + ("en-NL", "dd/MM/y"), + ("en-NO", "dd/MM/y"), + ("en-NR", "dd/MM/y"), + ("en-NU", "dd/MM/y"), + ("en-NZ", "d/MM/y"), + ("en-PG", "dd/MM/y"), + ("en-PH", "M/d/y"), + ("en-PK", "dd/MM/y"), + ("en-PL", "dd/MM/y"), + ("en-PN", "dd/MM/y"), + ("en-PR", "M/d/y"), + ("en-PT", "dd/MM/y"), + ("en-PW", "dd/MM/y"), + ("en-RO", "dd/MM/y"), + ("en-RW", "dd/MM/y"), + ("en-SB", "dd/MM/y"), + ("en-SC", "dd/MM/y"), + ("en-SD", "dd/MM/y"), + ("en-SE", "y-MM-dd"), + ("en-SG", "dd/MM/y"), + ("en-SH", "dd/MM/y"), + ("en-SI", "dd/MM/y"), + ("en-SK", "dd/MM/y"), + ("en-SL", "dd/MM/y"), + ("en-SS", "dd/MM/y"), + ("en-SX", "dd/MM/y"), + ("en-SZ", "dd/MM/y"), + ("en-Shaw", "y-MM-dd"), + ("en-TC", "dd/MM/y"), + ("en-TK", "dd/MM/y"), + ("en-TO", "dd/MM/y"), + ("en-TT", "dd/MM/y"), + ("en-TV", "dd/MM/y"), + ("en-TZ", "dd/MM/y"), + ("en-UA", "dd/MM/y"), + ("en-UG", "dd/MM/y"), + ("en-UM", "M/d/y"), + ("en-VC", "dd/MM/y"), + ("en-VG", "dd/MM/y"), + ("en-VI", "M/d/y"), + ("en-VU", "dd/MM/y"), + ("en-WS", "dd/MM/y"), + ("en-ZA", "y/MM/dd"), + ("en-ZM", "dd/MM/y"), + ("en-ZW", "d/M/y"), + ("eo", "y-MM-dd"), + ("es", "d/M/y"), + ("es-419", "d/M/y"), + ("es-AR", "d/M/y"), + ("es-BO", "d/M/y"), + ("es-BR", "d/M/y"), + ("es-BZ", "d/M/y"), + ("es-CL", "dd-MM-y"), + ("es-CO", "d/M/y"), + ("es-CR", "d/M/y"), + ("es-CU", "d/M/y"), + ("es-DO", "d/M/y"), + ("es-EA", "d/M/y"), + ("es-EC", "d/M/y"), + ("es-GQ", "d/M/y"), + ("es-GT", "d/M/y"), + ("es-HN", "d/M/y"), + ("es-IC", "d/M/y"), + ("es-MX", "d/M/y"), + ("es-NI", "d/M/y"), + ("es-PA", "MM/dd/y"), + ("es-PE", "d/M/y"), + ("es-PH", "d/M/y"), + ("es-PR", "MM/dd/y"), + ("es-PY", "d/M/y"), + ("es-SV", "d/M/y"), + ("es-US", "d/M/y"), + ("es-UY", "d/M/y"), + ("es-VE", "d/M/y"), + ("et", "d.M.y"), + ("eu", "y/M/d"), + ("ewo", "d/M/y"), + ("fa", "y/M/d"), + ("fa-AF", "M/d/y"), + ("ff", "y-MM-dd"), + ("ff-Adlm", "d-M-y"), + ("ff-Adlm-BF", "d-M-y"), + ("ff-Adlm-CM", "d-M-y"), + ("ff-Adlm-GH", "d-M-y"), + ("ff-Adlm-GM", "d-M-y"), + ("ff-Adlm-GW", "d-M-y"), + ("ff-Adlm-LR", "d-M-y"), + ("ff-Adlm-MR", "d-M-y"), + ("ff-Adlm-NE", "d-M-y"), + ("ff-Adlm-NG", "d-M-y"), + ("ff-Adlm-SL", "d-M-y"), + ("ff-Adlm-SN", "d-M-y"), + ("ff-Latn", "y-MM-dd"), + ("ff-Latn-BF", "y-MM-dd"), + ("ff-Latn-CM", "y-MM-dd"), + ("ff-Latn-GH", "y-MM-dd"), + ("ff-Latn-GM", "y-MM-dd"), + ("ff-Latn-GN", "y-MM-dd"), + ("ff-Latn-GW", "y-MM-dd"), + ("ff-Latn-LR", "y-MM-dd"), + ("ff-Latn-MR", "y-MM-dd"), + ("ff-Latn-NE", "y-MM-dd"), + ("ff-Latn-NG", "y-MM-dd"), + ("ff-Latn-SL", "y-MM-dd"), + ("fi", "d.M.y"), + ("fil", "M/d/y"), + ("fo", "dd.MM.y"), + ("fo-DK", "dd.MM.y"), + ("fr", "dd/MM/y"), + ("fr-BE", "dd/MM/y"), + ("fr-BF", "dd/MM/y"), + ("fr-BI", "dd/MM/y"), + ("fr-BJ", "dd/MM/y"), + ("fr-BL", "dd/MM/y"), + ("fr-CA", "y-MM-dd"), + ("fr-CD", "dd/MM/y"), + ("fr-CF", "dd/MM/y"), + ("fr-CG", "dd/MM/y"), + ("fr-CH", "dd.MM.y"), + ("fr-CI", "dd/MM/y"), + ("fr-CM", "dd/MM/y"), + ("fr-DJ", "dd/MM/y"), + ("fr-DZ", "dd/MM/y"), + ("fr-GA", "dd/MM/y"), + ("fr-GF", "dd/MM/y"), + ("fr-GN", "dd/MM/y"), + ("fr-GP", "dd/MM/y"), + ("fr-GQ", "dd/MM/y"), + ("fr-HT", "dd/MM/y"), + ("fr-KM", "dd/MM/y"), + ("fr-LU", "dd/MM/y"), + ("fr-MA", "dd/MM/y"), + ("fr-MC", "dd/MM/y"), + ("fr-MF", "dd/MM/y"), + ("fr-MG", "dd/MM/y"), + ("fr-ML", "dd/MM/y"), + ("fr-MQ", "dd/MM/y"), + ("fr-MR", "dd/MM/y"), + ("fr-MU", "dd/MM/y"), + ("fr-NC", "dd/MM/y"), + ("fr-NE", "dd/MM/y"), + ("fr-PF", "dd/MM/y"), + ("fr-PM", "dd/MM/y"), + ("fr-RE", "dd/MM/y"), + ("fr-RW", "dd/MM/y"), + ("fr-SC", "dd/MM/y"), + ("fr-SN", "dd/MM/y"), + ("fr-SY", "dd/MM/y"), + ("fr-TD", "dd/MM/y"), + ("fr-TG", "dd/MM/y"), + ("fr-TN", "dd/MM/y"), + ("fr-VU", "dd/MM/y"), + ("fr-WF", "dd/MM/y"), + ("fr-YT", "dd/MM/y"), + ("frr", "y-MM-dd"), + ("fur", "y-MM-dd"), + ("fy", "d-M-y"), + ("ga", "dd/MM/y"), + ("ga-GB", "dd/MM/y"), + ("gaa", "M/d/y"), + ("gd", "d/M/y"), + ("gez", "y-MM-dd"), + ("gez-ER", "y-MM-dd"), + ("gl", "d/M/y"), + ("gn", "y-MM-dd"), + ("gsw", "y-MM-dd"), + ("gsw-FR", "y-MM-dd"), + ("gsw-LI", "y-MM-dd"), + ("gu", "d/M/y"), + ("guz", "y-MM-dd"), + ("gv", "y-MM-dd"), + ("ha", "y-MM-dd"), + ("ha-Arab", "y-MM-dd"), + ("ha-Arab-SD", "y-MM-dd"), + ("ha-GH", "y-MM-dd"), + ("ha-NE", "y-MM-dd"), + ("he", "d.M.y"), + ("hi", "d/M/y"), + ("hi-Latn", "d/M/y"), + ("hnj", "y-MM-dd"), + ("hnj-Hmnp", "y-MM-dd"), + ("hr", "dd. MM. y."), + ("hr-BA", "dd. MM. y."), + ("hsb", "d.M.y"), + ("ht", "dd/MM/y"), + ("hu", "y. MM. dd."), + ("hy", "dd.MM.y"), + ("ia", "dd-MM-y"), + ("id", "d/M/y"), + ("ie", "d.M.y"), + ("ig", "d/M/y"), + ("ii", "y-MM-dd"), + ("io", "y-MM-dd"), + ("is", "d.M.y"), + ("it", "dd/MM/y"), + ("it-CH", "dd/MM/y"), + ("it-SM", "dd/MM/y"), + ("it-VA", "dd/MM/y"), + ("iu", "MM/dd/y"), + ("iu-Latn", "y-MM-dd"), + ("ja", "y/M/d"), + ("jbo", "y-MM-dd"), + ("jgo", "M.d.y"), + ("jmc", "y-MM-dd"), + ("jv", "dd-MM-y"), + ("ka", "d.M.y"), + ("kaa", "y-MM-dd"), + ("kaa-Cyrl", "y-MM-dd"), + ("kaa-Latn", "y-MM-dd"), + ("kab", "y-MM-dd"), + ("kaj", "y-MM-dd"), + ("kam", "y-MM-dd"), + ("kcg", "y-MM-dd"), + ("kde", "y-MM-dd"), + ("kea", "dd/MM/y"), + ("kek", "dd-MM-y"), + ("ken", "y-MM-dd"), + ("kgp", "dd/MM/y"), + ("khq", "y-MM-dd"), + ("ki", "y-MM-dd"), + ("kk", "dd.MM.y"), + ("kk-Arab", "y-d-M"), + ("kk-Cyrl", "dd.MM.y"), + ("kk-KZ", "dd.MM.y"), + ("kkj", "dd/MM y"), + ("kl", "y-MM-dd"), + ("kln", "y-MM-dd"), + ("km", "d/M/y"), + ("kn", "d/M/y"), + ("ko", "y. M. d."), + ("ko-CN", "y. M. d."), + ("ko-KP", "y. M. d."), + ("kok", "d-M-y"), + ("kok-Deva", "d-M-y"), + ("kok-Latn", "d-M-y"), + ("kpe", "y-MM-dd"), + ("kpe-GN", "y-MM-dd"), + ("ks", "M/d/y"), + ("ks-Arab", "M/d/y"), + ("ks-Deva", "M/d/y"), + ("ksb", "y-MM-dd"), + ("ksf", "d/M/y"), + ("ksh", "y-MM-dd"), + ("ku", "dd.MM.y"), + ("ku-Arab", "y-MM-dd"), + ("ku-Arab-IR", "y-MM-dd"), + ("ku-Latn", "dd.MM.y"), + ("ku-Latn-IQ", "dd.MM.y"), + ("ku-Latn-SY", "dd.MM.y"), + ("ku-TR", "dd.MM.y"), + ("kw", "y-MM-dd"), + ("kxv", "d/M/y"), + ("kxv-Deva", "d/M/y"), + ("kxv-Latn", "d/M/y"), + ("kxv-Orya", "d/M/y"), + ("kxv-Telu", "d/M/y"), + ("ky", "y-dd-MM"), + ("la", "y-MM-dd"), + ("lag", "y-MM-dd"), + ("lb", "d.M.y"), + ("lg", "y-MM-dd"), + ("lij", "d/M/y"), + ("lkt", "y-MM-dd"), + ("lld", "d.M.y"), + ("lmo", "y-MM-dd"), + ("ln", "d/M/y"), + ("ln-AO", "d/M/y"), + ("ln-CF", "d/M/y"), + ("ln-CG", "d/M/y"), + ("lo", "d/M/y"), + ("lrc", "y-MM-dd"), + ("lrc-IQ", "y-MM-dd"), + ("lt", "y-MM-dd"), + ("ltg", "y-MM-dd"), + ("lu", "d/M/y"), + ("luo", "y-MM-dd"), + ("luy", "y-MM-dd"), + ("lv", "d.MM.y."), + ("lzz", "y-MM-dd"), + ("mai", "d/M/y"), + ("mas", "y-MM-dd"), + ("mas-TZ", "y-MM-dd"), + ("mdf", "y-MM-dd"), + ("mer", "y-MM-dd"), + ("mfe", "y-MM-dd"), + ("mg", "y-MM-dd"), + ("mgh", "d/M/y"), + ("mgo", "y-MM-dd"), + ("mhn", "y-MM-dd"), + ("mi", "dd-MM-y"), + ("mic", "y-MM-dd"), + ("mk", "d.M.y 'г'."), + ("ml", "d/M/y"), + ("mn", "y.MM.dd"), + ("mn-Mong", "y-MM-dd"), + ("mn-Mong-MN", "y-MM-dd"), + ("mni", "d/M/y"), + ("mni-Beng", "d/M/y"), + ("mni-Mtei", "y-MM-dd"), + ("moh", "y-MM-dd"), + ("mr", "d/M/y"), + ("ms", "d/M/y"), + ("ms-Arab", "d/M/y"), + ("ms-Arab-BN", "d/M/y"), + ("ms-BN", "d/M/y"), + ("ms-ID", "d/M/y"), + ("ms-SG", "d/M/y"), + ("mt", "M/d/y"), + ("mua", "d/M/y"), + ("mus", "y-MM-dd"), + ("mww", "y-MM-dd"), + ("mww-Hmnp", "y-MM-dd"), + ("my", "d/M/y"), + ("myv", "y-MM-dd"), + ("mzn", "y-MM-dd"), + ("naq", "y-MM-dd"), + ("nb", "d.M.y"), + ("nb-SJ", "d.M.y"), + ("nd", "y-MM-dd"), + ("nds", "d.M.y"), + ("nds-NL", "d.M.y"), + ("ne", "y-MM-dd"), + ("ne-IN", "y-MM-dd"), + ("nl", "d-M-y"), + ("nl-AW", "d-M-y"), + ("nl-BE", "d/M/y"), + ("nl-BQ", "d-M-y"), + ("nl-CW", "d-M-y"), + ("nl-SR", "d-M-y"), + ("nl-SX", "d-M-y"), + ("nmg", "d/M/y"), + ("nn", "d.M.y"), + ("nnh", "d/M/y"), + ("no", "d.M.y"), + ("nqo", "y / dd / MM"), + ("nr", "y-MM-dd"), + ("nso", "dd-MM-y"), + ("nus", "d/M/y"), + ("nv", "y-MM-dd"), + ("ny", "y-MM-dd"), + ("nyn", "y-MM-dd"), + ("oc", "dd/MM/y"), + ("oc-ES", "dd/MM/y"), + ("oka", "y-MM-dd"), + ("oka-US", "y-MM-dd"), + ("om", "M/d/y"), + ("om-KE", "M/d/y"), + ("or", "M/d/y"), + ("os", "y-MM-dd"), + ("os-RU", "y-MM-dd"), + ("osa", "y-MM-dd"), + ("pa", "d/M/y"), + ("pa-Arab", "y-MM-dd"), + ("pa-Guru", "d/M/y"), + ("pap", "y-MM-dd"), + ("pap-AW", "y-MM-dd"), + ("pcm", "d/M/y"), + ("pi", "y-MM-dd"), + ("pi-Latn", "y-MM-dd"), + ("pis", "y-MM-dd"), + ("pl", "d.MM.y"), + ("pms", "dd/MM/y"), + ("prg", "d.M.y"), + ("ps", "y-MM-dd"), + ("ps-PK", "y-MM-dd"), + ("pt", "dd/MM/y"), + ("pt-AO", "dd/MM/y"), + ("pt-CH", "dd/MM/y"), + ("pt-CV", "dd/MM/y"), + ("pt-GQ", "dd/MM/y"), + ("pt-GW", "dd/MM/y"), + ("pt-LU", "dd/MM/y"), + ("pt-MO", "dd/MM/y"), + ("pt-MZ", "dd/MM/y"), + ("pt-PT", "dd/MM/y"), + ("pt-ST", "dd/MM/y"), + ("pt-TL", "dd/MM/y"), + ("qu", "dd-MM-y"), + ("qu-BO", "dd-MM-y"), + ("qu-EC", "dd-MM-y"), + ("quc", "y-MM-dd"), + ("raj", "y-MM-dd"), + ("rhg", "y-MM-dd"), + ("rhg-Rohg", "y-MM-dd"), + ("rhg-Rohg-BD", "y-MM-dd"), + ("rif", "y-MM-dd"), + ("rm", "d-M-y"), + ("rn", "d/M/y"), + ("ro", "dd.MM.y"), + ("ro-MD", "dd.MM.y"), + ("rof", "y-MM-dd"), + ("ru", "dd.MM.y"), + ("ru-BY", "dd.MM.y"), + ("ru-KG", "dd.MM.y"), + ("ru-KZ", "dd.MM.y"), + ("ru-MD", "dd.MM.y"), + ("ru-UA", "dd.MM.y"), + ("rw", "dd-MM-y"), + ("rwk", "y-MM-dd"), + ("sa", "d/M/y"), + ("sah", "y-MM-dd"), + ("saq", "y-MM-dd"), + ("sat", "y-MM-dd"), + ("sat-Deva", "y-MM-dd"), + ("sat-Olck", "y-MM-dd"), + ("sbp", "M/d/y"), + ("sc", "dd/MM/y"), + ("scn", "d/M/y"), + ("sd", "y-MM-dd"), + ("sd-Arab", "y-MM-dd"), + ("sd-Deva", "M/d/y"), + ("sdh", "y-MM-dd"), + ("sdh-IQ", "y-MM-dd"), + ("se", "y-MM-dd"), + ("se-FI", "dd.MM.y"), + ("se-SE", "y-MM-dd"), + ("seh", "y-MM-dd"), + ("ses", "y-MM-dd"), + ("sg", "y-MM-dd"), + ("sgs", "y-MM-dd"), + ("shi", "y-MM-dd"), + ("shi-Latn", "y-MM-dd"), + ("shi-Tfng", "y-MM-dd"), + ("shn", "y-MM-dd"), + ("shn-TH", "y-MM-dd"), + ("si", "y-M-d"), + ("sid", "y-MM-dd"), + ("sk", "d. M. y"), + ("skr", "y-MM-dd"), + ("sl", "d. M. y"), + ("sma", "y-MM-dd"), + ("sma-NO", "y-MM-dd"), + ("smj", "y-MM-dd"), + ("smj-NO", "y-MM-dd"), + ("smn", "d.M.y"), + ("sms", "y-MM-dd"), + ("sn", "y-MM-dd"), + ("so", "M/d/y"), + ("so-DJ", "M/d/y"), + ("so-ET", "M/d/y"), + ("so-KE", "M/d/y"), + ("sq", "d.M.y"), + ("sq-MK", "d.M.y"), + ("sq-XK", "d.M.y"), + ("sr", "d. M. y."), + ("sr-Cyrl", "d. M. y."), + ("sr-Cyrl-BA", "d. M. y."), + ("sr-Cyrl-ME", "d. M. y."), + ("sr-Cyrl-XK", "d. M. y."), + ("sr-Latn", "d. M. y."), + ("sr-Latn-BA", "d. M. y."), + ("sr-Latn-ME", "d. M. y."), + ("sr-Latn-XK", "d. M. y."), + ("ss", "y-MM-dd"), + ("ss-SZ", "y-MM-dd"), + ("ssy", "y-MM-dd"), + ("st", "y-MM-dd"), + ("st-LS", "y-MM-dd"), + ("su", "d/M/y"), + ("su-Latn", "d/M/y"), + ("suz", "y-MM-dd"), + ("suz-Deva", "y-MM-dd"), + ("suz-Sunu", "y-MM-dd"), + ("sv", "y-MM-dd"), + ("sv-AX", "y-MM-dd"), + ("sv-FI", "d.M.y"), + ("sw", "d/M/y"), + ("sw-CD", "d/M/y"), + ("sw-KE", "d/M/y"), + ("sw-UG", "d/M/y"), + ("syr", "d/M/y"), + ("syr-SY", "d/M/y"), + ("szl", "dd.MM.y"), + ("ta", "d/M/y"), + ("ta-LK", "d/M/y"), + ("ta-MY", "d/M/y"), + ("ta-SG", "d/M/y"), + ("te", "d/M/y"), + ("teo", "y-MM-dd"), + ("teo-KE", "y-MM-dd"), + ("tg", "d.M.y"), + ("th", "d/M/y"), + ("ti", "M/d/y"), + ("ti-ER", "M/d/y"), + ("tig", "y-MM-dd"), + ("tk", "dd.MM.y"), + ("tn", "y-MM-dd"), + ("tn-BW", "y-MM-dd"), + ("to", "d/M/y"), + ("tok", "#y)#M)#d"), + ("tpi", "d/M/y"), + ("tr", "dd.MM.y"), + ("tr-CY", "dd.MM.y"), + ("trv", "y-MM-dd"), + ("trw", "y-MM-dd"), + ("ts", "y-MM-dd"), + ("tt", "dd.MM.y"), + ("twq", "d/M/y"), + ("tyv", "y-MM-dd"), + ("tzm", "y-MM-dd"), + ("ug", "y-d-M"), + ("uk", "dd.MM.y"), + ("und", "y-MM-dd"), + ("ur", "d/M/y"), + ("ur-IN", "d/M/y"), + ("uz", "dd/MM/y"), + ("uz-Arab", "y-MM-dd"), + ("uz-Cyrl", "dd/MM/y"), + ("uz-Latn", "dd/MM/y"), + ("vai", "y-MM-dd"), + ("vai-Latn", "M/d/y"), + ("vai-Vaii", "y-MM-dd"), + ("ve", "y-MM-dd"), + ("vec", "dd/MM/y"), + ("vi", "d/M/y"), + ("vmw", "d/M/y"), + ("vo", "y-MM-dd"), + ("vun", "y-MM-dd"), + ("wa", "y-MM-dd"), + ("wae", "y-MM-dd"), + ("wal", "y-MM-dd"), + ("wbp", "y-MM-dd"), + ("wo", "dd-MM-y"), + ("xh", "M/d/y"), + ("xnr", "d/M/y"), + ("xog", "y-MM-dd"), + ("yav", "d/M/y"), + ("yi", "d-M-y"), + ("yo", "d/M/y"), + ("yo-BJ", "d/M/y"), + ("yrl", "dd/MM/y"), + ("yrl-CO", "dd/MM/y"), + ("yrl-VE", "dd/MM/y"), + ("yue", "y/M/d"), + ("yue-Hans", "y/M/d"), + ("yue-Hant", "y/M/d"), + ("yue-Hant-CN", "y/M/d"), + ("yue-Hant-MO", "y/M/d"), + ("za", "y/M/d"), + ("zgh", "y-MM-dd"), + ("zh", "y/M/d"), + ("zh-Hans", "y/M/d"), + ("zh-Hans-HK", "d/M/y"), + ("zh-Hans-MO", "y年M月d日"), + ("zh-Hans-MY", "y/M/d"), + ("zh-Hans-SG", "y年M月d日"), + ("zh-Hant", "y/M/d"), + ("zh-Hant-HK", "d/M/y"), + ("zh-Hant-MO", "d/M/y"), + ("zh-Hant-MY", "y/M/d"), + ("zh-Latn", "y-MM-dd"), + ("zu", "y-MM-dd"), +]; diff --git a/crates/perry-runtime/src/node_vm.rs b/crates/perry-runtime/src/node_vm.rs index 92efa9495b..ad6ea8374e 100644 --- a/crates/perry-runtime/src/node_vm.rs +++ b/crates/perry-runtime/src/node_vm.rs @@ -230,7 +230,12 @@ pub(crate) fn compiled_function_source_for_closure(closure: usize) -> Option String { compiled_function_source_for_closure(closure).unwrap_or_else(|| { - let func_ptr = unsafe { (*(closure as *const ClosureHeader)).func_ptr as usize }; + let closure_ptr = closure as *const ClosureHeader; + let func_ptr = unsafe { + crate::closure::bound_method_source_func_ptr(closure_ptr) + .or_else(|| crate::object::class_accessor_source_func_ptr(closure_ptr)) + .unwrap_or((*closure_ptr).func_ptr as usize) + }; crate::builtins::function_source_for_func_ptr(func_ptr) }) } diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index d711b8ca8a..074c00871c 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -171,8 +171,8 @@ pub(crate) use gc_roots::{ // ── registration.rs ───────────────────────────────────────────────────────── pub(crate) use registration::{ - class_accessor_function_value, class_own_accessor_ptrs, class_own_static_accessor_ptrs, - invalidate_class_string_member_order, + class_accessor_function_value, class_accessor_source_func_ptr, class_own_accessor_ptrs, + class_own_static_accessor_ptrs, invalidate_class_string_member_order, }; pub use registration::{ is_class_id_registered, js_register_class_getter, js_register_class_method, diff --git a/crates/perry-runtime/src/object/class_registry/registration.rs b/crates/perry-runtime/src/object/class_registry/registration.rs index a0158f6a92..a6dcfb9deb 100644 --- a/crates/perry-runtime/src/object/class_registry/registration.rs +++ b/crates/perry-runtime/src/object/class_registry/registration.rs @@ -236,6 +236,25 @@ extern "C" fn class_accessor_setter_thunk( f(this, value) } +/// Return the raw getter/setter body wrapped by a descriptor-reflection +/// accessor closure. The wrapper has its own calling-convention thunk, while +/// Function.prototype.toString must consult the original MethodDefinition. +pub(crate) unsafe fn class_accessor_source_func_ptr( + closure: *const crate::closure::ClosureHeader, +) -> Option { + if closure.is_null() || crate::closure::real_capture_count((*closure).capture_count) < 1 { + return None; + } + let thunk = (*closure).func_ptr; + if thunk != class_accessor_getter_thunk as *const u8 + && thunk != class_accessor_setter_thunk as *const u8 + { + return None; + } + let raw = crate::closure::js_closure_get_capture_ptr(closure, 0) as usize; + (raw != 0).then_some(raw) +} + /// Wrap a raw class accessor func_ptr as a callable function VALUE for /// descriptor reflection (`Object.getOwnPropertyDescriptor(C.prototype, /// "x").get`). Built-in-shaped: `.length` 0/1, no `.prototype`, native diff --git a/test-files/test_class_name_and_source_9413.ts b/test-files/test_class_name_and_source_9413.ts index 57133699e3..5789443aba 100644 --- a/test-files/test_class_name_and_source_9413.ts +++ b/test-files/test_class_name_and_source_9413.ts @@ -66,6 +66,40 @@ console.log("expr-toString:", String(class Anon { y = 2; })); console.log("subclass-toString:", String(class ExtNamed extends Named {})); console.log("objmethod-toString:", String(({ m() { return 1; } }).m)); +// #9468: class member values retain their exact MethodDefinition source even +// though Perry compiles them as raw vtable/static/accessor symbols rather than +// ordinary closure bodies. +class MemberSource { + m() { return 1; } + static sm() { return 2; } + get value() { return 3; } + set value(v) { void v; } + async am() { return 4; } + *gm() { yield 5; } + async *agm() { yield 6; } +} +const memberDescriptor = Object.getOwnPropertyDescriptor(MemberSource.prototype, "value")!; +console.log("method-String:", String(MemberSource.prototype.m)); +console.log("method-toString:", MemberSource.prototype.m.toString()); +console.log("method-template:", `${MemberSource.prototype.m}`); +console.log("static-method:", String(MemberSource.sm)); +console.log("getter-source:", String(memberDescriptor.get)); +console.log("setter-source:", String(memberDescriptor.set)); +console.log("async-method:", String(MemberSource.prototype.am)); +console.log("generator-method:", String(MemberSource.prototype.gm)); +console.log("async-generator-method:", String(MemberSource.prototype.agm)); + +// Object-literal accessors use SetFunctionName with the `get`/`set` prefix. +const objectAccessors = { + get g() { return 1; }, + set s(v) { void v; }, +}; +console.log( + "object-accessor-names:", + Object.getOwnPropertyDescriptor(objectAccessors, "g")!.get!.name, + Object.getOwnPropertyDescriptor(objectAccessors, "s")!.set!.name, +); + // util.inspect / console.log of a class object. console.log("direct:", Klass); console.log("inspect:", inspect(Klass)); diff --git a/test-files/test_class_name_cjs_9413.cts b/test-files/test_class_name_cjs_9413.cts index ed84776d7c..06c9d525f1 100644 --- a/test-files/test_class_name_cjs_9413.cts +++ b/test-files/test_class_name_cjs_9413.cts @@ -3,16 +3,10 @@ // ESM (`"type": "module"`), so this file is the only place the CJS lowering // path is exercised. // -// Deliberately NOT covered here: `module.exports = class {}` and -// `exports.Foo = class {}`. Both are member assignments, which per spec get no -// NamedEvaluation (node: `""`), but perry's CJS source rewrite turns the first -// into a NAMED declaration (`__perry_cjs_default__`) before parsing, and drops -// the binding-name inference for the local-`const` form. Those are defects of -// `crates/perry/src/commands/compile/cjs_wrap/hoist_classes.rs`, upstream of -// anything class metadata can reach — reported separately. class Named {} function scopeA() { class Made { } return Made.name; } class Made {} +const LocalAnon = class {}; console.log("decl:", Named.name); console.log("ctor:", new Named().constructor.name); @@ -21,3 +15,10 @@ console.log("new-anon:", new (class {})().constructor.name); console.log("new-named:", new (class Zed {})().constructor.name); console.log("String:", String(Named)); console.log("inspect:", Named); +console.log("local-anon:", LocalAnon.name); + +// #9468: a member assignment is not a NamedEvaluation context. The CJS +// pre-parse rewrite may give this class a synthetic registration key, but that +// key must not become the constructor's observable name. +module.exports = class {}; +console.log("module-exports-anon:", module.exports.name); diff --git a/test-files/test_gap_intl_component_locale.ts b/test-files/test_gap_intl_component_locale.ts index 9f85a40acb..1865bfb464 100644 --- a/test-files/test_gap_intl_component_locale.ts +++ b/test-files/test_gap_intl_component_locale.ts @@ -2,13 +2,49 @@ // spelled `month` or a `weekday`) must localize the field names AND the field // order — `5. Januar 2026`, `2026年1月5日`, `lundi 5 janvier` — not the old // US-hardcoded `January 5, 2026`. Perry routes these name-bearing combos -// through icu4x's dynamic FieldSetBuilder; numeric-only combos keep the -// existing assembly and aren't asserted here. Both entry points are covered: +// through icu4x's dynamic FieldSetBuilder. #9451 extends the same CLDR route +// to the default numeric Y/M/D field set, including semantic parts. Both entry +// points are covered: // `Intl.DateTimeFormat(...).format()` and `Date.prototype.toLocaleDateString`. // // Compared byte-for-byte against `node --experimental-strip-types`. const d = new Date(Date.UTC(2026, 0, 5, 14, 37, 9)); +// The ECMA-402 no-fields default is numeric year/month/day. Cover both sides +// of the padding boundary: January 5 has single-digit month/day, while +// November 15 has double-digit month/day. +const defaultDates = [ + new Date(Date.UTC(2026, 0, 5, 14, 37, 9)), + new Date(Date.UTC(2026, 10, 15, 14, 37, 9)), +]; +for (const loc of ["de-DE", "fr-FR", "ja-JP", "en-GB", "en-US"]) { + for (const date of defaultDates) { + const dtf = new Intl.DateTimeFormat(loc, { timeZone: "UTC" }); + console.log(loc + " | default | " + dtf.format(date)); + console.log(loc + " | parts | " + JSON.stringify(dtf.formatToParts(date))); + console.log( + loc + " | method | " + date.toLocaleDateString(loc, { timeZone: "UTC" }), + ); + } +} + +// Controls: style-driven requests already use ICU's CLDR patterns and must +// remain unchanged when the numeric component path is enabled. +const styleControls: Array<[string, Opt]> = [ + ["de-DE", { dateStyle: "short" }], + ["fr-FR", { dateStyle: "long" }], + ["ja-JP", { timeStyle: "short" }], + ["en-GB", { dateStyle: "medium", timeStyle: "short" }], + ["en-US", { dateStyle: "full" }], +]; +for (const [loc, opt] of styleControls) { + console.log( + loc + + " | style | " + + new Intl.DateTimeFormat(loc, { ...opt, timeZone: "UTC" }).format(d), + ); +} + type Opt = Intl.DateTimeFormatOptions; const cases: Array<[string, Opt]> = [ ["de", { year: "numeric", month: "long", day: "numeric" }], From f9d68d53ef2b6153e38ef5aebbf7e62b21972e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 14:18:05 +0200 Subject: [PATCH 2/6] fix(runtime): root every saved implicit-`this` across the user code it brackets (#9445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every `let prev = js_implicit_this_set(receiver); …user code…; js_implicit_this_set(prev)` in the runtime held the caller's receiver in a bare Rust local across a call that allocates. An evacuating young-gen minor inside the window moves that object; the restore then installed a retired from-space address as the caller's `this`, which reads as `undefined` on the next member access (the #9417 shape). Root the saved value in a RuntimeHandleScope and re-read it at the restore — the idiom PR #9444 used for the accessor sites — at all 121 remaining sites, plus the receivers that three of those sites consume again after the call. Claude-Session: https://claude.ai/code/session_01L11XMMWrR9Wz11dHpq4gXS --- crates/perry-runtime/src/array/iterator.rs | 48 ++++++++++++------- crates/perry-runtime/src/async_hooks.rs | 7 ++- .../src/child_process/emitter.rs | 15 +++--- .../src/closure/dispatch/bound.rs | 10 ++-- .../src/closure/dynamic_props.rs | 22 +++++---- crates/perry-runtime/src/cluster.rs | 10 ++-- crates/perry-runtime/src/collection_iter.rs | 5 +- crates/perry-runtime/src/dgram.rs | 5 +- crates/perry-runtime/src/disposable.rs | 5 +- crates/perry-runtime/src/event_target.rs | 6 ++- .../src/fs/dir_glob_watch/watch.rs | 21 +++++--- .../src/fs/stream/write_file_input.rs | 5 +- crates/perry-runtime/src/intl/subclass.rs | 10 ++-- crates/perry-runtime/src/json/replacer.rs | 5 +- crates/perry-runtime/src/json/reviver.rs | 5 +- crates/perry-runtime/src/json/stringify.rs | 12 +++-- .../src/json/stringify_scalars.rs | 5 +- crates/perry-runtime/src/map.rs | 5 +- crates/perry-runtime/src/messaging.rs | 6 ++- .../src/node_api_host/functions.rs | 7 ++- crates/perry-runtime/src/node_inspector.rs | 5 +- crates/perry-runtime/src/node_repl.rs | 5 +- crates/perry-runtime/src/node_stream.rs | 15 +++--- .../src/node_stream_destroy_state.rs | 5 +- .../src/node_stream_event_emitter.rs | 5 +- .../src/node_stream_readwrite.rs | 20 ++++---- .../src/node_submodules/consumers.rs | 5 +- .../src/node_submodules/diagnostics.rs | 5 +- .../src/node_submodules/stream_promises.rs | 5 +- .../perry-runtime/src/node_submodules/test.rs | 4 +- .../src/node_submodules/trace_events.rs | 7 ++- crates/perry-runtime/src/node_vm/modules.rs | 8 +++- .../src/object/class_constructors.rs | 6 ++- .../object/class_registry/parent_static.rs | 35 +++++++++----- .../parent_static/private_and_dynamic.rs | 25 ++++++---- .../class_registry/prototype_objects.rs | 5 +- .../src/object/date_proto_thunks.rs | 5 +- .../src/object/descriptor_state.rs | 5 +- .../src/object/field_set_by_name/tail.rs | 12 +++-- .../src/object/global_this/bigint_promise.rs | 5 +- .../src/object/global_this/fetch_globals.rs | 15 ++++-- .../src/object/native_call_method.rs | 18 ++++--- .../native_call_method/handle_methods.rs | 5 +- .../object/native_call_method/object_proto.rs | 5 +- .../perry-runtime/src/object/property_key.rs | 22 +++++---- .../perry-runtime/src/object/this_binding.rs | 25 ++++++++++ .../perry-runtime/src/os_process_streams.rs | 31 +++++++----- .../perry-runtime/src/promise/assimilate.rs | 10 ++-- .../perry-runtime/src/promise/async_step.rs | 5 +- .../src/promise/checked_dispatch.rs | 20 ++++---- .../src/promise/spec_combinators.rs | 5 +- crates/perry-runtime/src/promise/then.rs | 5 +- crates/perry-runtime/src/proxy.rs | 21 ++++---- .../src/proxy/apply_construct.rs | 15 +++--- crates/perry-runtime/src/proxy/reflect.rs | 15 +++--- crates/perry-runtime/src/pty/mod.rs | 5 +- crates/perry-runtime/src/regex/replace_fn.rs | 7 ++- crates/perry-runtime/src/set.rs | 5 +- crates/perry-runtime/src/symbol/accessors.rs | 5 +- crates/perry-runtime/src/symbol/iterator.rs | 19 +++++--- crates/perry-runtime/src/timer.rs | 19 +++++--- crates/perry-runtime/src/typedarray_props.rs | 10 ++-- crates/perry-runtime/src/url/search_params.rs | 6 ++- crates/perry-runtime/src/util_promisify.rs | 5 +- crates/perry-runtime/src/value/to_string.rs | 20 ++++---- .../src/value/to_string_class_ref.rs | 5 +- 66 files changed, 467 insertions(+), 257 deletions(-) diff --git a/crates/perry-runtime/src/array/iterator.rs b/crates/perry-runtime/src/array/iterator.rs index d479cabfdc..1b3ffffd88 100644 --- a/crates/perry-runtime/src/array/iterator.rs +++ b/crates/perry-runtime/src/array/iterator.rs @@ -481,8 +481,9 @@ fn async_from_sync_call_raw(iter: f64, method: &[u8], args: &[f64]) -> Result Result f64 { if !is_callable_value(method) { throw_iterator_method_not_callable(); } - let prev_this = crate::object::js_implicit_this_set(value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(value)); let iterator = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // GetIterator step 5: the result must be an Object. if !is_async_iterator_object(iterator) { throw_iterator_result_not_object(); @@ -1301,9 +1304,10 @@ fn throw_iterator_result_not_object() -> ! { pub extern "C" fn js_iterator_next_result(iter_f64: f64) -> f64 { let next = named_field(iter_f64, b"next"); let result = if is_callable_value(next) { - let prev_this = crate::object::js_implicit_this_set(iter_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_f64)); let result = unsafe { crate::closure::js_native_call_value(next, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } else if next.to_bits() == crate::value::TAG_UNDEFINED && is_builtin_iterator_class_id(crate::value::js_nanbox_get_pointer(iter_f64) as usize) @@ -1363,9 +1367,10 @@ pub extern "C" fn js_iterator_close_if_not_done(iter_f64: f64, done_f64: f64) -> crate::closure::throw_not_callable(); } - let prev_this = crate::object::js_implicit_this_set(iter_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_f64)); let result = unsafe { crate::closure::js_native_call_value(ret, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if !is_object_like_value(result) { throw_iterator_result_not_object(); } @@ -1439,9 +1444,11 @@ pub(crate) fn sync_iterator_to_array_if_not_async(iter_f64: f64) -> Option<*mut } else { // Call(next, iterator) — bind `this` like `js_iterator_to_array` // does for its stored-closure path (#9019). - let prev_this = crate::object::js_implicit_this_set(iter_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_f64)); let r = closure::js_closure_call1(next_ptr, f64::from_bits(TAG_UNDEFINED)); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); r }; if crate::promise::js_value_is_promise(step) != 0 { @@ -1477,9 +1484,10 @@ pub(crate) fn call_symbol_async_iterator(value: f64) -> Option { if !is_callable_value(method) { return None; } - let prev_this = crate::object::js_implicit_this_set(value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(value)); let iterator = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if iterator.to_bits() == crate::value::TAG_UNDEFINED { None } else { @@ -1633,12 +1641,14 @@ pub extern "C" fn js_iterator_to_array(iter_f64: f64) -> *mut ArrayHeader { // Call(next, iterator): bind `this` for the stored-closure path // exactly like `js_iterator_next_result` — a user-assigned // `it.next = function () { … }` may read `this` (#9019). - let prev_this = crate::object::js_implicit_this_set(iter_h.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope + .root_nanbox_f64(crate::object::js_implicit_this_set(iter_h.get_nanbox_f64())); let r = closure::js_closure_call1( js_nanbox_get_pointer(next_h.get_nanbox_f64()) as *const closure::ClosureHeader, f64::from_bits(TAG_UNDEFINED), ); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); r }; // IteratorNext (ECMA-262 §7.4.2 step 3): if Type(result) is not @@ -1755,9 +1765,11 @@ fn js_async_iterator_to_array(iter_f64: f64) -> *mut ArrayHeader { } else { // Call(next, iterator) — bind `this` for the stored-closure path // (#9019), mirroring `js_iterator_to_array`. - let prev_this = crate::object::js_implicit_this_set(iter_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_f64)); let r = closure::js_closure_call1(next_ptr, f64::from_bits(TAG_UNDEFINED)); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); r }; let Some(step_result) = settled_promise_value(step) else { diff --git a/crates/perry-runtime/src/async_hooks.rs b/crates/perry-runtime/src/async_hooks.rs index e73c8985e1..28703c7f53 100644 --- a/crates/perry-runtime/src/async_hooks.rs +++ b/crates/perry-runtime/src/async_hooks.rs @@ -1811,7 +1811,10 @@ fn call_callback_with_rest(callback_value: f64, this_arg: f64, rest: f64) -> f64 } let args_array = ptr_from_nanboxed(rest) as *const ArrayHeader; let args_array_handle = scope.root_raw_const_ptr(args_array); - let prev_this = crate::object::js_implicit_this_set(this_arg_handle.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + this_arg_handle.get_nanbox_f64(), + )); let result = if args_array.is_null() { unsafe { js_closure_call_array(callback as i64, ptr::null(), 0) } } else { @@ -1824,7 +1827,7 @@ fn call_callback_with_rest(callback_value: f64, this_arg: f64, rest: f64) -> f64 }; unsafe { js_closure_call_array(callback as i64, data, len) } }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/child_process/emitter.rs b/crates/perry-runtime/src/child_process/emitter.rs index 5a3768e0fb..aca68e59b4 100644 --- a/crates/perry-runtime/src/child_process/emitter.rs +++ b/crates/perry-runtime/src/child_process/emitter.rs @@ -61,11 +61,12 @@ pub(crate) fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool { break; } let cb = crate::array::js_array_get_f64(arr, i); - let prev = js_implicit_this_set(target); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(target)); unsafe { let _ = js_native_call_value(cb, args.as_ptr(), args.len()); } - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); fired = true; i += 1; } @@ -255,12 +256,13 @@ pub(crate) extern "C" fn cp_pipe_data_thunk(closure: *const ClosureHeader, chunk let dest = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); let write = cp_get_field(dest, b"write"); if !crate::fs::extract_closure_ptr(write).is_null() { - let prev = js_implicit_this_set(dest); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(dest)); let args = [chunk]; unsafe { let _ = js_native_call_value(write, args.as_ptr(), args.len()); } - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); } cp_undefined() } @@ -272,12 +274,13 @@ pub(crate) extern "C" fn cp_pipe_end_thunk(closure: *const ClosureHeader) -> f64 let dest = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); let end = cp_get_field(dest, b"end"); if !crate::fs::extract_closure_ptr(end).is_null() { - let prev = js_implicit_this_set(dest); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(dest)); let args = [cp_undefined()]; unsafe { let _ = js_native_call_value(end, args.as_ptr(), 0); } - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); } cp_undefined() } diff --git a/crates/perry-runtime/src/closure/dispatch/bound.rs b/crates/perry-runtime/src/closure/dispatch/bound.rs index fffb633467..f1c0e8d27d 100644 --- a/crates/perry-runtime/src/closure/dispatch/bound.rs +++ b/crates/perry-runtime/src/closure/dispatch/bound.rs @@ -265,7 +265,8 @@ unsafe fn dispatch_symbol_bound_method( // the direct-call path. The one-shot static-`this` override (armed by // the Function.prototype call/apply arms for a static bound-method // value) still wins in the static-method prologue. - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::object::static_private_owner_push(receiver); let result = crate::object::call_registered_static_method( func_ptr, @@ -275,7 +276,7 @@ unsafe fn dispatch_symbol_bound_method( has_rest, ); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } else { // Computed symbol methods never synthesize an `arguments` object but @@ -318,14 +319,15 @@ pub unsafe fn dispatch_bound_function(closure: *const ClosureHeader, args: &[f64 // slot, not IMPLICIT_THIS — rebind it to the bound receiver so the bound // `this` is honored (arrows/non-captures_this targets are returned as-is). let target = rebind_explicit_this(target, bound_this); - let prev_this = crate::object::js_implicit_this_set(bound_this); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(bound_this)); let (call_ptr, call_len) = if combined.is_empty() { (std::ptr::null::(), 0usize) } else { (combined.as_ptr(), combined.len()) }; let result = js_native_call_value(target, call_ptr, call_len); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index 40292d97a8..b6b804d9bd 100644 --- a/crates/perry-runtime/src/closure/dynamic_props.rs +++ b/crates/perry-runtime/src/closure/dynamic_props.rs @@ -600,9 +600,10 @@ pub fn closure_get_dynamic_prop(ptr: usize, prop: &str) -> f64 { return f64::from_bits(crate::value::TAG_UNDEFINED); } let receiver = crate::value::js_nanbox_pointer(ptr as i64); - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } @@ -661,9 +662,11 @@ pub fn closure_get_dynamic_prop(ptr: usize, prop: &str) -> f64 { if getter.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(getter); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } if let Ok(props) = get_closure_props().lock() { @@ -692,9 +695,10 @@ pub fn closure_get_dynamic_prop(ptr: usize, prop: &str) -> f64 { if getter.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(getter); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } { @@ -724,9 +728,11 @@ pub fn closure_get_dynamic_prop(ptr: usize, prop: &str) -> f64 { (acc.get & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if !getter.is_null() { let receiver = crate::value::js_nanbox_pointer(ptr as i64); - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(getter); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } } diff --git a/crates/perry-runtime/src/cluster.rs b/crates/perry-runtime/src/cluster.rs index aa4284e025..dee5698e39 100644 --- a/crates/perry-runtime/src/cluster.rs +++ b/crates/perry-runtime/src/cluster.rs @@ -305,11 +305,12 @@ pub(crate) fn cluster_emit_event(event: &str, args: &[f64]) -> bool { } for listener in listeners { let cb = f64::from_bits(listener.callback_bits); - let prev = js_implicit_this_set(cluster_default_value()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(cluster_default_value())); unsafe { let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); } - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); } true } @@ -1423,11 +1424,12 @@ fn emit(target: f64, event: &str, args: &[f64]) -> bool { break; } let cb = crate::array::js_array_get_f64(arr, i); - let prev = js_implicit_this_set(target); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(target)); unsafe { let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); } - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); fired = true; i += 1; } diff --git a/crates/perry-runtime/src/collection_iter.rs b/crates/perry-runtime/src/collection_iter.rs index 852ae7cea5..44d5c801b4 100644 --- a/crates/perry-runtime/src/collection_iter.rs +++ b/crates/perry-runtime/src/collection_iter.rs @@ -276,11 +276,12 @@ pub(crate) fn call_with_this_capturing_throw( this_value: f64, args: &[f64], ) -> Result { - let prev_this = crate::object::js_implicit_this_set(this_value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_value)); let result = call_capturing_throw(|| unsafe { crate::closure::js_native_call_value(callee, args.as_ptr(), args.len()) }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/dgram.rs b/crates/perry-runtime/src/dgram.rs index 17b151ee4d..67cc2a3115 100644 --- a/crates/perry-runtime/src/dgram.rs +++ b/crates/perry-runtime/src/dgram.rs @@ -752,10 +752,11 @@ pub(crate) fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { if !is_callable_value(callback) { return undefined_value(); } - let prev = crate::object::js_implicit_this_set(this); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); let result = unsafe { crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/disposable.rs b/crates/perry-runtime/src/disposable.rs index e07de3529c..225ad6cc64 100644 --- a/crates/perry-runtime/src/disposable.rs +++ b/crates/perry-runtime/src/disposable.rs @@ -103,9 +103,10 @@ extern "C" fn bound_dispose_thunk(closure: *const ClosureHeader) -> f64 { if !is_callable_value(method) { return undefined(); } - let prev = js_implicit_this_set(resource); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(resource)); let result = unsafe { js_native_call_value(method, std::ptr::null(), 0) }; - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/event_target.rs b/crates/perry-runtime/src/event_target.rs index e31860cf8b..fabb6e1e25 100644 --- a/crates/perry-runtime/src/event_target.rs +++ b/crates/perry-runtime/src/event_target.rs @@ -986,9 +986,11 @@ pub unsafe extern "C" fn js_event_target_dispatch_event( if once { remove_event_listener_value_with_capture(target, event_name_ptr, callback, capture); } - let prev_this = crate::object::js_implicit_this_set(target_value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(target_value)); let _ = crate::closure::js_native_call_value(callable, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if event_bool_field(event_ptr, b"_immediateStopped") { break; } diff --git a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs index 4b392a3571..d833dd306b 100644 --- a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs +++ b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs @@ -518,11 +518,14 @@ fn emit_listener0(object_value: f64, callback: f64) { if cb.is_null() { return; } - let prev_this = crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + object_handle.get_nanbox_f64(), + )); with_watcher_uncaught_trap(|| { crate::closure::js_closure_call0(cb); }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } fn emit_fs_watch_event( @@ -551,11 +554,14 @@ fn emit_fs_watch_event( if cb.is_null() { continue; } - let prev_this = crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + object_handle.get_nanbox_f64(), + )); with_watcher_uncaught_trap(|| { crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } @@ -586,11 +592,14 @@ fn emit_watch_file_change( if cb.is_null() { continue; } - let prev_this = crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + object_handle.get_nanbox_f64(), + )); with_watcher_uncaught_trap(|| { crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } diff --git a/crates/perry-runtime/src/fs/stream/write_file_input.rs b/crates/perry-runtime/src/fs/stream/write_file_input.rs index 95fa53d0d8..600aae4ccd 100644 --- a/crates/perry-runtime/src/fs/stream/write_file_input.rs +++ b/crates/perry-runtime/src/fs/stream/write_file_input.rs @@ -150,9 +150,10 @@ fn well_known_iterator_method(value: f64, name: &str) -> Option { fn call_well_known_iterator(value: f64, name: &str) -> Option { let method = well_known_iterator_method(value, name)?; - let prev_this = crate::object::js_implicit_this_set(value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(value)); let iterator = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if iterator.to_bits() == crate::value::TAG_UNDEFINED { None } else { diff --git a/crates/perry-runtime/src/intl/subclass.rs b/crates/perry-runtime/src/intl/subclass.rs index 45b7886527..d2f397e50e 100644 --- a/crates/perry-runtime/src/intl/subclass.rs +++ b/crates/perry-runtime/src/intl/subclass.rs @@ -147,13 +147,17 @@ pub(crate) unsafe fn intl_subclass_super( if !is_intl_constructor_value(parent_val) { return false; } - let prev_this = crate::object::js_implicit_this_set(this_box); + // #9445: the parent constructor is user-reachable code; root the displaced + // receiver AND `this_box`, which is consumed again after the call. + let this_scope = crate::gc::RuntimeHandleScope::new(); + let this_h = this_scope.root_nanbox_f64(this_box); + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_box)); let prev_nt = crate::object::js_new_target_set(parent_val); let instance = crate::closure::js_native_call_value(parent_val, args_ptr, args_len); crate::object::js_new_target_set(prev_nt); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // Re-home the freshly-built instance's brand + bound methods onto `this`. - let this_bits = this_box.to_bits(); + let this_bits = this_h.get_nanbox_f64().to_bits(); if (this_bits >> 48) == 0x7FFD { let dst = (this_bits & 0x0000_FFFF_FFFF_FFFF) as i64; if dst >= 0x10000 { diff --git a/crates/perry-runtime/src/json/replacer.rs b/crates/perry-runtime/src/json/replacer.rs index 8afd759808..b98651c75c 100644 --- a/crates/perry-runtime/src/json/replacer.rs +++ b/crates/perry-runtime/src/json/replacer.rs @@ -66,9 +66,10 @@ pub(crate) unsafe fn call_replacer( value_f64: f64, holder_f64: f64, ) -> f64 { - let prev_this = crate::object::js_implicit_this_set(holder_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(holder_f64)); let result = crate::js_closure_call2(replacer, key_f64, value_f64); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // The user callback may have installed/removed `Object.prototype.toJSON` // (#6009 fast-probe cache). super::invalidate_object_proto_tojson_state(); diff --git a/crates/perry-runtime/src/json/reviver.rs b/crates/perry-runtime/src/json/reviver.rs index 3894f931b2..6e74da888d 100644 --- a/crates/perry-runtime/src/json/reviver.rs +++ b/crates/perry-runtime/src/json/reviver.rs @@ -715,10 +715,11 @@ unsafe fn call_reviver( let holder_arg = holder_handle.get_nanbox_f64(); let key_arg = key_handle.get_nanbox_f64(); let value_arg = value_handle.get_nanbox_f64(); - let prev_this = crate::object::js_implicit_this_set(holder_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(holder_arg)); let result = crate::js_closure_call3(reviver, key_arg, value_arg, context_handle.get_nanbox_f64()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); let result_bits = result.to_bits(); let revived_bits = if result_bits == value_arg.to_bits() { value_handle.get_nanbox_u64() diff --git a/crates/perry-runtime/src/json/stringify.rs b/crates/perry-runtime/src/json/stringify.rs index 44b957d0b8..f3db707484 100644 --- a/crates/perry-runtime/src/json/stringify.rs +++ b/crates/perry-runtime/src/json/stringify.rs @@ -309,9 +309,10 @@ pub(crate) unsafe fn object_get_to_json(ptr: *const u8) -> Option { // record it in `TO_JSON_KEY` before recursing here. let key_f64_arg = current_to_json_key_arg(); - let prev_this = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let result = crate::closure::js_native_call_value(f64::from_bits(bound), &key_f64_arg, 1); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // The user callback may have installed/removed `Object.prototype.toJSON`. invalidate_object_proto_tojson_state(); Some(result) @@ -346,9 +347,12 @@ pub(crate) unsafe fn array_get_to_json(arr: *const crate::ArrayHeader) -> Option let recv_handle = scope.root_nanbox_f64(recv); // `toJSON(key)` receives the property key of this array value (#5909). let key_f64_arg = current_to_json_key_arg(); - let prev_this = crate::object::js_implicit_this_set(recv_handle.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + recv_handle.get_nanbox_f64(), + )); let result = crate::closure::js_native_call_value(f64::from_bits(method_bits), &key_f64_arg, 1); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // The user callback may have installed/removed `Object.prototype.toJSON`. invalidate_object_proto_tojson_state(); Some(result) diff --git a/crates/perry-runtime/src/json/stringify_scalars.rs b/crates/perry-runtime/src/json/stringify_scalars.rs index 18fa1ff38b..ab6083939a 100644 --- a/crates/perry-runtime/src/json/stringify_scalars.rs +++ b/crates/perry-runtime/src/json/stringify_scalars.rs @@ -231,9 +231,10 @@ pub(crate) unsafe fn bigint_apply_to_json(value: f64) -> Option { let recv = value_handle.get_nanbox_f64(); // `toJSON(key)` receives the property key of this BigInt value (#5909). let key_f64_arg = super::stringify_tojson_probe::current_to_json_key_arg(); - let prev_this = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let result = crate::closure::js_native_call_value(f64::from_bits(method_bits), &key_f64_arg, 1); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // The user callback may have installed/removed `Object.prototype.toJSON`. invalidate_object_proto_tojson_state(); Some(result) diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index 6d2f5beffd..3a7b790383 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -3154,9 +3154,10 @@ fn js_map_foreach_impl( // Bind `thisArg` for the duration of the call (matches the // URLSearchParams.forEach pattern); `js_native_call_value` // dispatches the NaN-boxed callback with the full arg vector. - let prev_this = crate::object::js_implicit_this_set(this_v); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_v)); let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } let map = map_handle.get_raw_const_ptr::(); diff --git a/crates/perry-runtime/src/messaging.rs b/crates/perry-runtime/src/messaging.rs index a55a560344..e87b5f3f1a 100644 --- a/crates/perry-runtime/src/messaging.rs +++ b/crates/perry-runtime/src/messaging.rs @@ -279,7 +279,9 @@ fn invoke_message_handler(handler: f64, event: f64, port_box: f64) { let handler_h = scope.root_nanbox_f64(handler); let event_h = scope.root_nanbox_f64(event); let port_h = scope.root_nanbox_f64(port_box); - let prev_this = object::js_implicit_this_set(port_h.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(object::js_implicit_this_set(port_h.get_nanbox_f64())); let args = [event_h.get_nanbox_f64()]; unsafe { let _ = crate::closure::js_native_call_value( @@ -288,7 +290,7 @@ fn invoke_message_handler(handler: f64, event: f64, port_box: f64) { args.len(), ); } - object::js_implicit_this_set(prev_this); + object::js_implicit_this_set(prev_this.get_nanbox_f64()); } /// Macrotask body: deliver exactly one queued message to `port_box`'s port. diff --git a/crates/perry-runtime/src/node_api_host/functions.rs b/crates/perry-runtime/src/node_api_host/functions.rs index 4de6d8bad3..db56b1514d 100644 --- a/crates/perry-runtime/src/node_api_host/functions.rs +++ b/crates/perry-runtime/src/node_api_host/functions.rs @@ -277,7 +277,10 @@ pub unsafe extern "C" fn napi_call_function( }; arguments.push(f64::from_bits(bits)); } - let previous_this = crate::object::js_implicit_this_set(f64::from_bits(receiver_bits)); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let previous_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + f64::from_bits(receiver_bits), + )); let call_result = catch_value_call(env, || { crate::closure::js_native_call_value( f64::from_bits(function_bits), @@ -285,7 +288,7 @@ pub unsafe extern "C" fn napi_call_function( arguments.len(), ) }); - crate::object::js_implicit_this_set(previous_this); + crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); match call_result { Ok(value) => { if !result.is_null() { diff --git a/crates/perry-runtime/src/node_inspector.rs b/crates/perry-runtime/src/node_inspector.rs index e147f62996..f7c6fb4ca8 100644 --- a/crates/perry-runtime/src/node_inspector.rs +++ b/crates/perry-runtime/src/node_inspector.rs @@ -255,10 +255,11 @@ fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { if !is_callable_value(callback) { return undefined(); } - let prev = crate::object::js_implicit_this_set(this); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); let result = unsafe { crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/node_repl.rs b/crates/perry-runtime/src/node_repl.rs index 1baf8f4b23..9b8656ae9f 100644 --- a/crates/perry-runtime/src/node_repl.rs +++ b/crates/perry-runtime/src/node_repl.rs @@ -168,10 +168,11 @@ fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { callback.to_bits(), this, )); - let prev = crate::object::js_implicit_this_set(this); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); let result = unsafe { crate::closure::js_native_call_value(rebound, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/node_stream.rs b/crates/perry-runtime/src/node_stream.rs index 5ffe59e656..722648d1a6 100644 --- a/crates/perry-runtime/src/node_stream.rs +++ b/crates/perry-runtime/src/node_stream.rs @@ -1163,11 +1163,12 @@ fn invoke_writable_write(stream: f64, chunk: f64, enc: f64, len: f64, callback: js_closure_set_capture_f64(cb, 2, callback); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); let args = [chunk, enc, cb_value]; - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(write, args.as_ptr(), args.len()); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } else { throw_missing_stream_method("The _write() method is not implemented"); } @@ -1178,11 +1179,12 @@ fn invoke_writable_writev(stream: f64, chunks: f64) { let cb = js_closure_alloc(writable_write_callback_noop as *const u8, 0); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); let args = [chunks, cb_value]; - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(writev, args.as_ptr(), args.len()); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } @@ -1222,11 +1224,12 @@ fn invoke_transform_write(stream: f64, chunk: f64, enc: f64, len: f64, callback: js_closure_set_capture_f64(cb, 2, callback); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); let args = [chunk, enc, cb_value]; - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(transform, args.as_ptr(), args.len()); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return; } throw_missing_stream_method("The _transform() method is not implemented"); diff --git a/crates/perry-runtime/src/node_stream_destroy_state.rs b/crates/perry-runtime/src/node_stream_destroy_state.rs index 3b591207a2..71a83596b9 100644 --- a/crates/perry-runtime/src/node_stream_destroy_state.rs +++ b/crates/perry-runtime/src/node_stream_destroy_state.rs @@ -66,11 +66,12 @@ pub(super) fn destroy_stream(stream: f64, err: f64) { err }; let args = [destroy_arg, cb_value]; - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(destroy, args.as_ptr(), args.len()); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return; } } diff --git a/crates/perry-runtime/src/node_stream_event_emitter.rs b/crates/perry-runtime/src/node_stream_event_emitter.rs index a4b3a40bd8..54a19a3eca 100644 --- a/crates/perry-runtime/src/node_stream_event_emitter.rs +++ b/crates/perry-runtime/src/node_stream_event_emitter.rs @@ -670,10 +670,11 @@ pub(super) fn call_listener_args(stream: f64, listener: f64, args: &[f64]) -> f6 if !is_callable_value(listener) { return f64::from_bits(super::TAG_UNDEFINED); } - let prev = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); let result = unsafe { crate::closure::js_native_call_value(listener, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/node_stream_readwrite.rs b/crates/perry-runtime/src/node_stream_readwrite.rs index cfec091967..0af8609b77 100644 --- a/crates/perry-runtime/src/node_stream_readwrite.rs +++ b/crates/perry-runtime/src/node_stream_readwrite.rs @@ -710,12 +710,13 @@ pub(super) fn schedule_writable_finish(stream: f64, callback: Option) { callback.unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)), ); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(final_callback, [cb_value].as_ptr(), 1); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return; } } @@ -1036,11 +1037,12 @@ pub(super) fn finish_transform_stream(stream: f64, callback: Option) -> boo callback.unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)), ); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(flush, [cb_value].as_ptr(), 1); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); true } @@ -1312,11 +1314,12 @@ pub(super) fn invoke_construct_callback(stream: f64, opts: f64) { let cb = js_closure_alloc(ns_construct_callback_done as *const u8, 1); js_closure_set_capture_f64(cb, 0, stream); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(construct, [cb_value].as_ptr(), 1); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } pub(super) fn invoke_read_once(stream: f64) { @@ -1346,11 +1349,12 @@ fn invoke_read_once_inner(stream: f64, emit_default_error: bool) { } set_hidden_value(stream, hidden_read_invoked_key(), f64::from_bits(TAG_TRUE)); let size = get_hidden_value(stream, hidden_hwm_key()).unwrap_or_else(|| default_hwm(false)); - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(read, [size].as_ptr(), 1); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } pub(super) fn maybe_emit_default_read_error(stream: f64) { diff --git a/crates/perry-runtime/src/node_submodules/consumers.rs b/crates/perry-runtime/src/node_submodules/consumers.rs index d21fe115ce..f91bb9a339 100644 --- a/crates/perry-runtime/src/node_submodules/consumers.rs +++ b/crates/perry-runtime/src/node_submodules/consumers.rs @@ -507,9 +507,10 @@ fn call_symbol_async_iterator(stream: f64) -> Option { if !is_callable_value(method) { return None; } - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); let iterator = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if iterator.to_bits() == crate::value::TAG_UNDEFINED { None } else { diff --git a/crates/perry-runtime/src/node_submodules/diagnostics.rs b/crates/perry-runtime/src/node_submodules/diagnostics.rs index a0268b8124..b053a3b6b3 100644 --- a/crates/perry-runtime/src/node_submodules/diagnostics.rs +++ b/crates/perry-runtime/src/node_submodules/diagnostics.rs @@ -842,9 +842,10 @@ pub(crate) fn suppress_uncaught_drain f64>(f: F) -> f64 { } pub(crate) fn with_implicit_this f64>(this_arg: f64, f: F) -> f64 { - let prev = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let result = f(); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/node_submodules/stream_promises.rs b/crates/perry-runtime/src/node_submodules/stream_promises.rs index ef1e96ff47..ecf4a3ba2e 100644 --- a/crates/perry-runtime/src/node_submodules/stream_promises.rs +++ b/crates/perry-runtime/src/node_submodules/stream_promises.rs @@ -442,9 +442,10 @@ fn invoke_destination_method(destination: f64, method: &[u8], args: &[f64]) -> f let Some(func) = get_object_property(destination, method) else { return undefined_value(); }; - let prev_this = crate::object::js_implicit_this_set(destination); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(destination)); let result = unsafe { crate::closure::js_native_call_value(func, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/node_submodules/test.rs b/crates/perry-runtime/src/node_submodules/test.rs index 956434e867..8b3b032340 100644 --- a/crates/perry-runtime/src/node_submodules/test.rs +++ b/crates/perry-runtime/src/node_submodules/test.rs @@ -805,7 +805,7 @@ extern "C" fn mock_function_invoke(closure: *const ClosureHeader, rest: f64) -> let rest_handle = scope.root_nanbox_f64(rest); let arg_handles = scope.root_nanbox_f64_slice(&args); let call_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - let previous_this = crate::object::js_implicit_this_set(this_value); + let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_value)); // #9445 let call_result = catch_js(|| unsafe { crate::closure::js_native_call_value( implementation_handle.get_nanbox_f64(), @@ -813,7 +813,7 @@ extern "C" fn mock_function_invoke(closure: *const ClosureHeader, rest: f64) -> call_args.len(), ) }); - crate::object::js_implicit_this_set(previous_this); + crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); match call_result { Ok(result) => { diff --git a/crates/perry-runtime/src/node_submodules/trace_events.rs b/crates/perry-runtime/src/node_submodules/trace_events.rs index 9b7bfc0e26..af52b7b484 100644 --- a/crates/perry-runtime/src/node_submodules/trace_events.rs +++ b/crates/perry-runtime/src/node_submodules/trace_events.rs @@ -611,7 +611,10 @@ fn emit_enabled_trace_warning() { let process = scope.root_nanbox_f64(process); let callback = scope.root_nanbox_f64(callback); let warning = scope.root_nanbox_f64(warning); - let previous = crate::object::js_implicit_this_set(process.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let previous = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + process.get_nanbox_f64(), + )); unsafe { crate::closure::js_native_call_value( callback.get_nanbox_f64(), @@ -619,7 +622,7 @@ fn emit_enabled_trace_warning() { 1, ); } - crate::object::js_implicit_this_set(previous); + crate::object::js_implicit_this_set(previous.get_nanbox_f64()); } #[cfg(test)] diff --git a/crates/perry-runtime/src/node_vm/modules.rs b/crates/perry-runtime/src/node_vm/modules.rs index 731856e47f..a0f94f56e4 100644 --- a/crates/perry-runtime/src/node_vm/modules.rs +++ b/crates/perry-runtime/src/node_vm/modules.rs @@ -75,11 +75,15 @@ fn evaluate_synthetic_module(module: *mut ObjectHeader) -> f64 { })); 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(with_hmut(&module, object_value)); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(with_hmut( + &module, + object_value, + ))); 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); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); if let Err(error) = outcome { with_hmut(&module, |module| set_field(module, FIELD_ERROR, error)); with_hmut(&module, |module| set_status(module, STATUS_ERRORED)); diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index 878542fba5..0ab60a0bb3 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -677,7 +677,9 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( if let Some((func_ptr, param_count, has_rest)) = super::class_registry::lookup_static_method_in_chain(parent_cid, name) { - let prev_this = crate::object::js_implicit_this_set(this_value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_value)); crate::object::static_this_arm_if_unarmed(this_value); let result = if has_rest { // Mirror `js_class_static_method_call`'s rest bundling: fixed @@ -709,7 +711,7 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( super::class_registry::call_static_method(func_ptr, args_ptr, args_len, param_count) }; crate::object::static_this_disarm(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } diff --git a/crates/perry-runtime/src/object/class_registry/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs index 09b80f6f7f..ae44447026 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -1007,12 +1007,14 @@ pub(crate) unsafe fn class_symbol_getter_value( return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); } let result = if is_static { - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::object::static_private_owner_push(receiver); let f: extern "C" fn() -> f64 = std::mem::transmute(getter); let result = f(); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } else { let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter); @@ -1053,12 +1055,14 @@ pub(crate) unsafe fn class_symbol_setter_apply( if let Some(&(_, setter)) = map.get(&(cid, sym_key, is_static)) { if setter != 0 { if is_static { - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope + .root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::object::static_private_owner_push(receiver); let f: extern "C" fn(f64) -> f64 = std::mem::transmute(setter); let _ = f(value); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } else { let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(setter); let _ = f(receiver, value); @@ -1575,7 +1579,8 @@ pub unsafe extern "C" fn js_class_static_method_call( return receiver; } if let Some((func_ptr, param_count, has_rest)) = lookup_static_method_in_chain(class_id, name) { - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::object::static_private_owner_push(receiver); // Receiver-sensitive static `this`: arm the one-shot override so the // method prologue (`js_static_this_resolve`) sees the DYNAMIC receiver @@ -1613,7 +1618,7 @@ pub unsafe extern "C" fn js_class_static_method_call( }; crate::object::static_this_disarm(); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } // #1787 / #321: not a static METHOD — try a static FIELD holding a @@ -1667,9 +1672,11 @@ pub unsafe extern "C" fn js_class_static_method_call( let member = f64::from_bits(member.bits()); let mv = crate::value::JSValue::from_bits(member.to_bits()); if !mv.is_undefined() && !mv.is_null() { - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(member, args_ptr, args_len); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } @@ -1694,9 +1701,11 @@ pub unsafe extern "C" fn js_class_static_method_call( // implicit-this slot, so bind it to the subclass receiver for the // duration of the call — `NewPromiseCapability(receiver)` then // constructs the subclass. - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(static_val, args_ptr, args_len); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } @@ -1771,9 +1780,11 @@ pub unsafe extern "C" fn js_class_static_method_call( // not a real inherited member. && member.to_bits() != closure_val.to_bits() { - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(member, args_ptr, args_len); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs index 4b74d9ed05..ae568602a3 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs @@ -120,17 +120,20 @@ pub(crate) fn register_class_dynamic_static_accessor( owner, key.clone(), crate::object::AccessorDescriptor { - get: get_bits.map(|_| get.get_nanbox_u64()).unwrap_or(existing.get), - set: set_bits.map(|_| set.get_nanbox_u64()).unwrap_or(existing.set), + get: get_bits + .map(|_| get.get_nanbox_u64()) + .unwrap_or(existing.get), + set: set_bits + .map(|_| set.get_nanbox_u64()) + .unwrap_or(existing.set), }, ); let existing_attrs = if is_class_object_ptr(owner as *const u8) { crate::object::get_property_attrs(owner, &key) .map(|attrs| (attrs.enumerable(), attrs.configurable())) } else { - class_static_defined_attrs(class_id, name).map(|(_, enumerable, configurable)| { - (enumerable, configurable) - }) + class_static_defined_attrs(class_id, name) + .map(|(_, enumerable, configurable)| (enumerable, configurable)) }; let enumerable = enumerable .or_else(|| existing_attrs.map(|attrs| attrs.0)) @@ -153,7 +156,10 @@ pub(crate) fn class_dynamic_static_accessor_descriptor( class_id: u32, name: &str, receiver: f64, -) -> Option<(crate::object::AccessorDescriptor, crate::object::PropertyAttrs)> { +) -> Option<( + crate::object::AccessorDescriptor, + crate::object::PropertyAttrs, +)> { let scope = crate::gc::RuntimeHandleScope::new(); let receiver = scope.root_nanbox_f64(receiver); let owner = dynamic_static_accessor_owner(class_id, receiver.get_nanbox_f64()); @@ -289,7 +295,10 @@ pub(crate) unsafe fn call_private_static_method_for_owner( let scope = crate::gc::RuntimeHandleScope::new(); let this_value = scope.root_nanbox_f64(this_value); let private_brand = scope.root_nanbox_f64(private_brand); - let previous_this = crate::object::js_implicit_this_set(this_value.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let previous_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + this_value.get_nanbox_f64(), + )); crate::object::static_private_owner_push(private_brand.get_nanbox_f64()); crate::object::private_lexical_brand_push(private_brand.get_nanbox_f64()); crate::object::static_this_arm_if_unarmed(this_value.get_nanbox_f64()); @@ -297,6 +306,6 @@ pub(crate) unsafe fn call_private_static_method_for_owner( crate::object::static_this_disarm(); crate::object::private_lexical_brand_pop(); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(previous_this); + crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); Some(result) } diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index ec638bb7dd..047a4d3e90 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -547,7 +547,8 @@ unsafe fn resolve_proto_chain_field_inner( } } let field_val = if let Some(receiver) = receiver { - let previous_this = js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let previous_this = this_scope.root_nanbox_f64(js_implicit_this_set(receiver)); // The recursive `get_field(proto_obj, key)` re-derives a class // getter's `this` from `proto_obj`; stash the real instance so an // inherited getter (object-literal `get x()` on an @@ -556,7 +557,7 @@ unsafe fn resolve_proto_chain_field_inner( super::super::field_get_set::accessor_receiver_override_begin(receiver); let value = js_object_get_field_by_name(proto_obj as *const _, key); super::super::field_get_set::accessor_receiver_override_end(prev_override); - js_implicit_this_set(previous_this); + js_implicit_this_set(previous_this.get_nanbox_f64()); value } else { js_object_get_field_by_name(proto_obj as *const _, key) diff --git a/crates/perry-runtime/src/object/date_proto_thunks.rs b/crates/perry-runtime/src/object/date_proto_thunks.rs index 9c45dd9b23..1997457e38 100644 --- a/crates/perry-runtime/src/object/date_proto_thunks.rs +++ b/crates/perry-runtime/src/object/date_proto_thunks.rs @@ -182,9 +182,10 @@ extern "C" fn date_to_json(_closure: *const crate::closure::ClosureHeader) -> f6 { // `Call(func, O, «»)` — toJSON's `key` argument is intentionally not // forwarded (Invoke passes an empty argument list). - let prev = crate::object::js_implicit_this_set(o); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(o)); let r = crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return r; } super::object_ops::throw_object_type_error(b"toISOString is not a function") diff --git a/crates/perry-runtime/src/object/descriptor_state.rs b/crates/perry-runtime/src/object/descriptor_state.rs index 1ec8bd19dd..909002a9aa 100644 --- a/crates/perry-runtime/src/object/descriptor_state.rs +++ b/crates/perry-runtime/src/object/descriptor_state.rs @@ -960,9 +960,10 @@ pub(crate) unsafe fn json_object_getter_value( return Some(f64::from_bits(TAG_UNDEFINED)); } let receiver = crate::value::js_nanbox_pointer(obj as i64); - let prev = js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(closure); - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); Some(result) } diff --git a/crates/perry-runtime/src/object/field_set_by_name/tail.rs b/crates/perry-runtime/src/object/field_set_by_name/tail.rs index 26d7937a9c..ee6a361400 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/tail.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/tail.rs @@ -125,9 +125,11 @@ pub(crate) fn set_field_by_name_object_tail( as *const crate::closure::ClosureHeader; if !closure.is_null() { let receiver = crate::value::js_nanbox_pointer(obj as i64); - let previous_this = super::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let previous_this = + this_scope.root_nanbox_f64(super::js_implicit_this_set(receiver)); crate::closure::js_closure_call1(closure, value); - super::js_implicit_this_set(previous_this); + super::js_implicit_this_set(previous_this.get_nanbox_f64()); } } else { crate::error::throw_immutable_write(0, name); @@ -686,9 +688,11 @@ pub(crate) fn set_field_by_name_object_tail( as *const crate::closure::ClosureHeader; if !closure.is_null() { let receiver = crate::value::js_nanbox_pointer(obj as i64); - let previous_this = super::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let previous_this = + this_scope.root_nanbox_f64(super::js_implicit_this_set(receiver)); crate::closure::js_closure_call1(closure, value); - super::js_implicit_this_set(previous_this); + super::js_implicit_this_set(previous_this.get_nanbox_f64()); } } else { crate::error::throw_immutable_write(0, k); diff --git a/crates/perry-runtime/src/object/global_this/bigint_promise.rs b/crates/perry-runtime/src/object/global_this/bigint_promise.rs index 907f295314..c15889a8ab 100644 --- a/crates/perry-runtime/src/object/global_this/bigint_promise.rs +++ b/crates/perry-runtime/src/object/global_this/bigint_promise.rs @@ -696,9 +696,10 @@ pub(crate) extern "C" fn typed_array_from_thunk( if map_closure.is_null() { return v; } - let prev = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let r = crate::closure::js_closure_call2(map_closure, v, k as f64); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); r }; if let Some(kind) = kind_opt { diff --git a/crates/perry-runtime/src/object/global_this/fetch_globals.rs b/crates/perry-runtime/src/object/global_this/fetch_globals.rs index 5d0e3f6f62..867d43e1fe 100644 --- a/crates/perry-runtime/src/object/global_this/fetch_globals.rs +++ b/crates/perry-runtime/src/object/global_this/fetch_globals.rs @@ -449,13 +449,17 @@ pub(crate) unsafe fn temporal_subclass_super( // to the parent ctor for the duration of the call (the cell it returns is // re-homed onto the subclass `this`; the exact new.target identity is not // observable to these native ctors beyond being defined). Restore after. - let prev_this = crate::object::js_implicit_this_set(this_box); + // #9445: root the displaced receiver and `this_box` (consumed again below) + // across the parent constructor call. + let this_scope = crate::gc::RuntimeHandleScope::new(); + let this_h = this_scope.root_nanbox_f64(this_box); + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_box)); let prev_nt = crate::object::js_new_target_set(parent_val); let cell = crate::closure::js_native_call_value(parent_val, args_ptr, args_len); crate::object::js_new_target_set(prev_nt); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if crate::temporal::is_temporal_value(cell) { - attach_temporal_cell_to_this(this_box, cell); + attach_temporal_cell_to_this(this_h.get_nanbox_f64(), cell); } true } @@ -959,9 +963,10 @@ pub unsafe extern "C" fn js_fetch_or_value_super( } } } - let prev = crate::object::js_implicit_this_set(this_box); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_box)); let r = crate::closure::js_native_call_value(callee, args_ptr, args_len); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); r } } diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index b54754a493..fe73a62449 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -677,7 +677,9 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( if let Some((func_ptr, param_count, has_rest)) = lookup_class_symbol_method_in_chain(class_id, sym_key, true) { - let prev_this = crate::object::js_implicit_this_set(object); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object)); let result = call_registered_static_method( func_ptr, args_ptr, @@ -685,7 +687,7 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( param_count, has_rest, ); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } @@ -695,7 +697,9 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( if let Some((func_ptr, param_count, has_rest)) = lookup_class_symbol_method_in_chain(class_id, sym_key, true) { - let prev_this = crate::object::js_implicit_this_set(object); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object)); let result = call_registered_static_method( func_ptr, args_ptr, @@ -703,7 +707,7 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( param_count, has_rest, ); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } else if key_jsval.is_pointer() || JSValue::from_bits(bits).is_pointer() { @@ -2506,9 +2510,11 @@ pub unsafe extern "C-unwind" fn js_native_call_method( if crate::promise::subclass_backing_promise(object()).is_some() { if let Some(m) = crate::promise::promise_proto_method(method_name) { let args = refreshed_args(); - let prev_this = crate::object::js_implicit_this_set(object()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object())); let result = crate::closure::js_native_call_value(m, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } diff --git a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs index 613d7968fe..8f63bf62bb 100644 --- a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs @@ -45,9 +45,10 @@ unsafe fn dispatch_handle_proto_method( // `this` and for non-closure values. Mirrors the class-prototype fallback. let _ = closure_ptr; let bound = crate::closure::clone_closure_rebind_this(resolved_bits, object); - let prev = crate::object::js_implicit_this_set(object); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object)); let result = crate::closure::js_native_call_value(f64::from_bits(bound), args_ptr, args_len); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); Some(result) } diff --git a/crates/perry-runtime/src/object/native_call_method/object_proto.rs b/crates/perry-runtime/src/object/native_call_method/object_proto.rs index 645705f093..8a88e9f98c 100644 --- a/crates/perry-runtime/src/object/native_call_method/object_proto.rs +++ b/crates/perry-runtime/src/object/native_call_method/object_proto.rs @@ -33,9 +33,10 @@ pub(super) unsafe fn call_object_to_string_method(object: f64) -> Option { throw_object_to_string_not_function(); } let bound = crate::closure::clone_closure_rebind_this(method_bits, receiver); - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); Some(result) } diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index cada3adae4..d1d360a125 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -129,10 +129,11 @@ unsafe fn ordinary_to_primitive_string_key(value: f64) -> Option { continue; } let bound = crate::closure::clone_closure_rebind_this(method_bits, receiver); - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if js_value_is_not_object(result) { return Some(result); } @@ -373,9 +374,11 @@ pub unsafe extern "C" fn js_super_accessor_get( { if getter_ptr != 0 { let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter_ptr); - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope + .root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let r = f(receiver); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return r; } } @@ -468,9 +471,11 @@ pub unsafe extern "C" fn js_super_accessor_get( .or_else(|| vtable.getters.get(&getter_alias)) { let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter_ptr); - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope + .root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let r = f(receiver); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return r; } } @@ -565,13 +570,14 @@ pub unsafe extern "C" fn js_object_super_call( let bound = crate::closure::clone_closure_rebind_this(callee_handle.get_nanbox_u64(), receiver); let bound_handle = scope.root_nanbox_u64(bound); let receiver = f64::from_bits(receiver_handle.get_heap_word_u64()); - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value( f64::from_bits(bound_handle.get_nanbox_u64()), args_ptr, args_len, ); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/object/this_binding.rs b/crates/perry-runtime/src/object/this_binding.rs index 6ece8c5ce0..3c9cc7f096 100644 --- a/crates/perry-runtime/src/object/this_binding.rs +++ b/crates/perry-runtime/src/object/this_binding.rs @@ -210,6 +210,31 @@ pub extern "C" fn js_implicit_this_get_sloppy() -> f64 { /// Callers must restore the previous value to scope the binding to the /// duration of a single method-style call. /// +/// **The returned previous value is a GC-managed pointer held in a bare Rust +/// local** (#9417, #9445). It is the caller's receiver, and the call this +/// save/restore brackets is user code, so an evacuating young-gen minor inside +/// it moves that object and rewrites every slot the collector can see — a +/// Rust local is not one. Restoring the stale bits then installs a retired +/// from-space address as the caller's `this`, and the failure is silent: +/// `js_object_get_own_field_or_undef` fails its type check on the recycled cell +/// and answers `undefined`, so the caller's next `this.` throws a +/// TypeError naming a property nowhere near the defect. Every runtime site +/// therefore roots the saved value in a `RuntimeHandleScope` and re-reads it at +/// the restore: +/// +/// ```ignore +/// let this_scope = crate::gc::RuntimeHandleScope::new(); +/// let prev = this_scope.root_nanbox_f64(js_implicit_this_set(receiver)); +/// … user code … +/// js_implicit_this_set(prev.get_nanbox_f64()); +/// ``` +/// +/// This is longjmp-safe: `exception.rs` saves and restores the handle stack at +/// trap boundaries, so a throw through the window truncates the scope exactly +/// as a normal drop would. Inside a loop, open the scope PER ITERATION (or +/// reuse an existing per-iteration scope) so the handle stack does not grow by +/// one slot per callback. +/// /// This is part of every dynamically-dispatched call's save/restore path. /// Keep the shipped path to one TLS replacement; default-off diagnostics here /// still impose their mode checks millions of times on closure-heavy programs. diff --git a/crates/perry-runtime/src/os_process_streams.rs b/crates/perry-runtime/src/os_process_streams.rs index b6271d0391..76056db6c4 100644 --- a/crates/perry-runtime/src/os_process_streams.rs +++ b/crates/perry-runtime/src/os_process_streams.rs @@ -859,7 +859,6 @@ fn pump_stdin_data_chunks() { if bytes.is_empty() { return; } - let this = stdin_this_value(); // #9490: decode ONCE per chunk. The UTF-8 decoder carries state // across chunks, so decoding per listener would push the same bytes // through it N times and give the second listener a continuation of @@ -879,12 +878,15 @@ fn pump_stdin_data_chunks() { for cb in data_listeners { let scope = crate::gc::RuntimeHandleScope::new(); let cb_handle = scope.root_raw_const_ptr(cb as *const crate::closure::ClosureHeader); - // Node calls stream listeners with `this === stream`. - let prev_this = crate::object::js_implicit_this_set(this); + // Node calls stream listeners with `this === stream`. Re-read the + // singleton per listener and root the displaced receiver: the previous + // listener was user code, so either may have moved (#9445). + let this = stdin_this_value(); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); cb_handle.with_const_ptr::(|closure| { crate::closure::js_closure_call1(closure, arg_handle.get_nanbox_f64()); }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } return; } @@ -897,15 +899,16 @@ fn pump_stdin_data_chunks() { .map(|mut l| std::mem::take(&mut *l)) .unwrap_or_default(); readable_listeners.extend(&readable_once); - let this = stdin_this_value(); for cb in readable_listeners { let scope = crate::gc::RuntimeHandleScope::new(); let cb_handle = scope.root_raw_const_ptr(cb as *const crate::closure::ClosureHeader); - let prev_this = crate::object::js_implicit_this_set(this); + // Per-listener re-read + rooted save/restore (#9445), as for `data`. + let this = stdin_this_value(); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); cb_handle.with_const_ptr::(|closure| { crate::closure::js_closure_call0(closure); }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } @@ -941,18 +944,19 @@ fn maybe_fire_stdin_end() { .lock() .map(|l| l.clone()) .unwrap_or_default(); - let this = stdin_this_value(); let flush_scope = crate::gc::RuntimeHandleScope::new(); let flush_handle = flush_scope.root_nanbox_f64(flushed); for cb in data_listeners { let scope = crate::gc::RuntimeHandleScope::new(); let cb_handle = scope.root_raw_const_ptr(cb as *const crate::closure::ClosureHeader); - let prev_this = crate::object::js_implicit_this_set(this); + // Per-listener re-read + rooted save/restore (#9445), as for `data`. + let this = stdin_this_value(); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); cb_handle.with_const_ptr::(|closure| { crate::closure::js_closure_call1(closure, flush_handle.get_nanbox_f64()); }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } } @@ -980,15 +984,16 @@ fn maybe_fire_stdin_end() { .map(|mut l| std::mem::take(&mut *l)) .unwrap_or_default(); end_listeners.extend(&end_once); - let this = stdin_this_value(); for cb in end_listeners { let scope = crate::gc::RuntimeHandleScope::new(); let cb_handle = scope.root_raw_const_ptr(cb as *const crate::closure::ClosureHeader); - let prev_this = crate::object::js_implicit_this_set(this); + // Per-listener re-read + rooted save/restore (#9445), as for `data`. + let this = stdin_this_value(); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); cb_handle.with_const_ptr::(|closure| { crate::closure::js_closure_call0(closure); }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } diff --git a/crates/perry-runtime/src/promise/assimilate.rs b/crates/perry-runtime/src/promise/assimilate.rs index e499d6f52d..577c534edd 100644 --- a/crates/perry-runtime/src/promise/assimilate.rs +++ b/crates/perry-runtime/src/promise/assimilate.rs @@ -382,11 +382,12 @@ extern "C" fn promise_resolve_thenable_job(closure: *const crate::closure::Closu let reject_value = crate::value::js_nanbox_pointer(reject_closure as i64); let args = [resolve_value, reject_value]; - let prev_this = crate::object::js_implicit_this_set(thenable); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(thenable)); let result = combinator_catch_js(|| unsafe { crate::closure::js_native_call_value(then_action, args.as_ptr(), args.len()) }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if let Err(reason) = result { if thenable_job_take_guard(guard_arr) { js_promise_reject(promise, reason); @@ -446,11 +447,12 @@ pub(super) fn assimilate_via_then_property(value: f64) -> f64 { // Bind `this` to the thenable so a non-arrow `then` body reads the right // receiver, then call `Get(value, "then")` as a value (own data property). - let prev = crate::object::js_implicit_this_set(value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(value)); unsafe { crate::closure::js_native_call_value(then_val, args.as_ptr(), args.len()); } - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); crate::value::js_nanbox_pointer(new_promise as i64) } diff --git a/crates/perry-runtime/src/promise/async_step.rs b/crates/perry-runtime/src/promise/async_step.rs index 3b38954990..b8e57e781a 100644 --- a/crates/perry-runtime/src/promise/async_step.rs +++ b/crates/perry-runtime/src/promise/async_step.rs @@ -1501,9 +1501,10 @@ fn array_from_async_map_or_push(closure: *const crate::closure::ClosureHeader, v let this_arg = crate::closure::js_closure_get_capture_f64(closure, AFA_THIS_ARG); let args = [value, index]; let args_arr = crate::array::js_array_from_f64(args.as_ptr(), args.len() as u32); - let prev_this = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let mapped_promise = js_promise_try(map_fn, args_arr); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); let mapped_closure = crate::closure::js_closure_get_capture_ptr(closure, AFA_MAPPED_CLOSURE) as *const crate::closure::ClosureHeader; diff --git a/crates/perry-runtime/src/promise/checked_dispatch.rs b/crates/perry-runtime/src/promise/checked_dispatch.rs index 4070589510..c8bcb5f9e1 100644 --- a/crates/perry-runtime/src/promise/checked_dispatch.rs +++ b/crates/perry-runtime/src/promise/checked_dispatch.rs @@ -50,10 +50,11 @@ pub extern "C" fn js_promise_then_checked( )); } let args = [on_fulfilled, on_rejected]; - let prev = crate::object::js_implicit_this_set(promise_val); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(promise_val)); let result = unsafe { crate::closure::js_native_call_value(own_then, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } if promise_has_own_constructor(promise_addr) @@ -62,9 +63,10 @@ pub extern "C" fn js_promise_then_checked( // Own `constructor` override OR a `class X extends Promise` instance: // route through the SpeciesConstructor-aware thunk, which unwraps the // backing cell and reads `this.constructor` for species chaining. - let prev = crate::object::js_implicit_this_set(promise_val); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(promise_val)); let result = promise_prototype_then_thunk(std::ptr::null(), on_fulfilled, on_rejected); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } let promise = promise_addr as *mut Promise; @@ -82,9 +84,10 @@ pub extern "C" fn js_promise_catch_checked(promise_val: f64, on_rejected: f64) - || promise_has_own_constructor(promise_addr) || subclass::subclass_backing_promise(promise_val).is_some() { - let prev = crate::object::js_implicit_this_set(promise_val); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(promise_val)); let result = promise_prototype_catch_thunk(std::ptr::null(), on_rejected); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } let promise = promise_addr as *mut Promise; @@ -98,9 +101,10 @@ pub extern "C" fn js_promise_finally_checked(promise_val: f64, on_finally: f64) || promise_has_own_constructor(promise_addr) || subclass::subclass_backing_promise(promise_val).is_some() { - let prev = crate::object::js_implicit_this_set(promise_val); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(promise_val)); let result = promise_prototype_finally_thunk(std::ptr::null(), on_finally); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } let promise = promise_addr as *mut Promise; diff --git a/crates/perry-runtime/src/promise/spec_combinators.rs b/crates/perry-runtime/src/promise/spec_combinators.rs index be6ba05c4b..acf6fa2bf0 100644 --- a/crates/perry-runtime/src/promise/spec_combinators.rs +++ b/crates/perry-runtime/src/promise/spec_combinators.rs @@ -297,10 +297,11 @@ fn call_with_this(func: f64, this_arg: f64, args: &[f64]) -> Result { } else { (args.as_ptr(), args.len()) }; - let prev = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let result = combinator_catch_js(|| unsafe { crate::closure::js_native_call_value(func, ptr, len) }); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/promise/then.rs b/crates/perry-runtime/src/promise/then.rs index bd8c993212..c2590921d9 100644 --- a/crates/perry-runtime/src/promise/then.rs +++ b/crates/perry-runtime/src/promise/then.rs @@ -982,10 +982,11 @@ fn call_receiver_then(receiver: f64, args: &[f64]) -> f64 { let err_val = crate::value::JSValue::pointer(err_ptr as *const u8).bits(); crate::exception::js_throw(f64::from_bits(err_val)); } - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = unsafe { crate::closure::js_native_call_value(then_fn, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index de1102ba2d..4f49b6a3ff 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -695,7 +695,8 @@ fn call_trap(handler: f64, trap: f64, args: &[f64]) -> f64 { } let undef = f64::from_bits(TAG_UNDEFINED); let a = |i: usize| -> f64 { args.get(i).copied().unwrap_or(undef) }; - let prev = crate::object::js_implicit_this_set(handler); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(handler)); let result = match args.len() { 0 => js_closure_call0(closure), 1 => js_closure_call1(closure, a(0)), @@ -703,7 +704,7 @@ fn call_trap(handler: f64, trap: f64, args: &[f64]) -> f64 { 3 => js_closure_call3(closure, a(0), a(1), a(2)), _ => crate::closure::js_closure_call4(closure, a(0), a(1), a(2), a(3)), }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } @@ -892,7 +893,8 @@ fn call_with_this_and_args(f: f64, this_arg: f64, args: &[f64]) -> f64 { if closure.is_null() { return throw_type_error("Reflect.apply target is not a function"); } - let prev = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let a = |i: usize| -> f64 { args.get(i) .copied() @@ -905,7 +907,7 @@ fn call_with_this_and_args(f: f64, this_arg: f64, args: &[f64]) -> f64 { 3 => js_closure_call3(closure, a(0), a(1), a(2)), _ => crate::closure::js_closure_call4(closure, a(0), a(1), a(2), a(3)), }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } @@ -1622,9 +1624,10 @@ fn call_setter_with_receiver(setter_bits: u64, receiver: f64, value: f64) -> boo if closure.is_null() { return false; } - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let _ = js_closure_call1(closure, value); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); true } @@ -2252,9 +2255,11 @@ fn class_super_accessor_set( .or_else(|| vtable.setters.get(&setter_alias)) { let f: extern "C" fn(f64, f64) -> f64 = unsafe { std::mem::transmute(setter_ptr) }; - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let _ = f(receiver, value); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return Some(true); } let getter_alias = format!("__get_{}", key_name); diff --git a/crates/perry-runtime/src/proxy/apply_construct.rs b/crates/perry-runtime/src/proxy/apply_construct.rs index a7a37bc35f..72047be4e7 100644 --- a/crates/perry-runtime/src/proxy/apply_construct.rs +++ b/crates/perry-runtime/src/proxy/apply_construct.rs @@ -93,9 +93,10 @@ fn forward_apply(target: f64, this_arg: f64, args_array: f64) -> f64 { } else { (buf.as_ptr(), buf.len()) }; - let prev = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let result = unsafe { crate::closure::js_native_call_value(target, ptr, n) }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } @@ -178,9 +179,10 @@ pub extern "C" fn js_proxy_apply(proxy_boxed: f64, this_arg: f64, args_array: f6 if closure.is_null() { return throw_type_error("proxy apply trap is not a function"); } - let prev = crate::object::js_implicit_this_set(handler); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(handler)); let result = js_closure_call3(closure, target, this_arg, args_array); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } @@ -256,9 +258,10 @@ pub extern "C" fn js_proxy_construct(proxy_boxed: f64, args_array: f64, new_targ if closure.is_null() { return throw_type_error("proxy construct trap is not a function"); } - let prev = crate::object::js_implicit_this_set(handler); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(handler)); let result = js_closure_call3(closure, target, args_array, nt); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); // [[Construct]] must return an Object (spec step 9 of the construct trap). if !reflect_value_is_object(result) { // Node/V8 wording: `'construct' on proxy: trap returned non-object ('1')`. diff --git a/crates/perry-runtime/src/proxy/reflect.rs b/crates/perry-runtime/src/proxy/reflect.rs index 8833a56912..b653b195f4 100644 --- a/crates/perry-runtime/src/proxy/reflect.rs +++ b/crates/perry-runtime/src/proxy/reflect.rs @@ -62,16 +62,18 @@ pub extern "C" fn js_reflect_get(target: f64, key: f64, receiver: f64) -> f64 { if !closure.is_null() { // Also set IMPLICIT_THIS for free-function getters that read // `this` from the implicit-this fallback rather than a slot. - let prev = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let result = js_closure_call0(closure); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } } } - let prev = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let result = target_get_property_key(target, property_key); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } @@ -380,9 +382,10 @@ pub extern "C" fn js_reflect_get_own_property_descriptor(target: f64, key: f64) if closure.is_null() { return throw_type_error("proxy getOwnPropertyDescriptor trap is not a function"); } - let prev = crate::object::js_implicit_this_set(handler); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(handler)); let result = js_closure_call2(closure, inner, property_key); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); let result_handle = scope.root_nanbox_f64(result); let target_desc = crate::object::js_object_get_own_property_descriptor(inner, property_key); diff --git a/crates/perry-runtime/src/pty/mod.rs b/crates/perry-runtime/src/pty/mod.rs index 301b21cc56..c8a30e438d 100644 --- a/crates/perry-runtime/src/pty/mod.rs +++ b/crates/perry-runtime/src/pty/mod.rs @@ -86,11 +86,12 @@ mod unix_impl { break; } let cb = crate::array::js_array_get_f64(arr, i); - let prev = js_implicit_this_set(target); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(target)); unsafe { let _ = js_native_call_value(cb, args.as_ptr(), args.len()); } - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); i += 1; } } diff --git a/crates/perry-runtime/src/regex/replace_fn.rs b/crates/perry-runtime/src/regex/replace_fn.rs index 950428d653..ed6b275a4a 100644 --- a/crates/perry-runtime/src/regex/replace_fn.rs +++ b/crates/perry-runtime/src/regex/replace_fn.rs @@ -5,9 +5,12 @@ use super::*; use crate::value::js_nanbox_string; pub(super) unsafe fn call_replace_callback(callback: f64, args: &[f64]) -> String { - let prev = crate::object::js_implicit_this_set(f64::from_bits(crate::value::TAG_UNDEFINED)); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(f64::from_bits( + crate::value::TAG_UNDEFINED, + ))); let ret = crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); // §22.1.3.19 step "Let replacement be ? ToString(? Call(replaceValue, …))": // the callback result is ToString-coerced — `undefined` renders as // "undefined", a number stringifies, an object runs its `toString`, and a diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 3445ef7e94..7f12f45666 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -1941,9 +1941,10 @@ fn js_set_foreach_impl( let args = [value, value, set_value]; let cb = callback_handle.get_nanbox_f64(); let this_v = this_handle.get_nanbox_f64(); - let prev_this = crate::object::js_implicit_this_set(this_v); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_v)); let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } let set = set_handle.get_raw_const_ptr::(); diff --git a/crates/perry-runtime/src/symbol/accessors.rs b/crates/perry-runtime/src/symbol/accessors.rs index a7db08d7fd..9ceeddbe3c 100644 --- a/crates/perry-runtime/src/symbol/accessors.rs +++ b/crates/perry-runtime/src/symbol/accessors.rs @@ -169,9 +169,10 @@ pub(super) unsafe fn invoke_symbol_accessor_getter(get_bits: u64, receiver: f64) if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/symbol/iterator.rs b/crates/perry-runtime/src/symbol/iterator.rs index 26f85faecb..80de035c62 100644 --- a/crates/perry-runtime/src/symbol/iterator.rs +++ b/crates/perry-runtime/src/symbol/iterator.rs @@ -261,12 +261,14 @@ pub extern "C" fn js_get_iterator(val_f64: f64) -> f64 { if iter_fn.to_bits() == TAG_UNDEFINED || fn_ptr.is_null() { throw_value_not_iterable(val_f64); } - let prev_this = crate::object::js_implicit_this_set(val_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(val_f64)); let rebound = crate::closure::clone_closure_rebind_this(iter_fn.to_bits(), val_f64); let rebound_ptr = crate::value::js_nanbox_get_pointer(f64::from_bits(rebound)) as *const crate::closure::ClosureHeader; let iter = crate::closure::js_closure_call0(rebound_ptr); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if !is_object_value(iter) { throw_iterator_result_not_object(); } @@ -453,9 +455,11 @@ pub extern "C" fn js_get_iterator(val_f64: f64) -> f64 { // `function(){ …this… }` factory reads `this` dynamically off // IMPLICIT_THIS, so set it here too (test262 yield-star-sync-* // asserts the `[Symbol.iterator]` call's thisValue === obj). - let prev_this = crate::object::js_implicit_this_set(val_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(val_f64)); let iter = crate::closure::js_closure_call0(fn_ptr); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // Several Perry host-backed collections expose iterator // helpers as eager arrays for direct `.entries()` parity. When // the same function is reached through `Symbol.iterator`, wrap @@ -650,12 +654,15 @@ pub unsafe extern "C" fn js_to_primitive(value: f64, hint: i32) -> f64 { // `this.celsius` resolved to `undefined`, so `+t` was `NaN` and `` `${t}` `` // was `undefined°C` (test_gap_symbols). The proxy arm above already binds // `this`; mirror it for the closure method. - let prev_this = crate::object::js_implicit_this_set(value_handle.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + value_handle.get_nanbox_f64(), + )); // Spec says the return value must be a primitive; if it's still an // object pointer, that's a TypeError in JS, but we just return it // as-is and let the caller fall back. let result = crate::closure::js_closure_call1(closure_ptr, hint_f64); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/timer.rs b/crates/perry-runtime/src/timer.rs index 4b2a18059b..5f3a27675c 100644 --- a/crates/perry-runtime/src/timer.rs +++ b/crates/perry-runtime/src/timer.rs @@ -516,11 +516,13 @@ fn call_timer_callback( let previous_roots = crate::async_context::root_snapshot(&scope, &previous); let a = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); let cb = callback_handle.get_raw_const_ptr::(); - let prev_this = crate::object::js_implicit_this_set(timer_handle_value(id)); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); with_timer_uncaught_trap(|| unsafe { crate::closure::js_closure_call_array(cb as i64, a.as_ptr(), a.len() as i64); }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); crate::async_context::refresh_snapshot_from_roots(&mut previous, &previous_roots); crate::async_context::restore_context(previous); } @@ -1304,7 +1306,10 @@ pub extern "C" fn js_callback_timer_tick() -> i32 { let mut previous = previous; let previous_roots = crate::async_context::root_snapshot(&batch_scope, &previous); crate::async_hooks::before(timer.async_id, timer.trigger_async_id); - let prev_this = crate::object::js_implicit_this_set(timer_handle_value(timer.id)); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + timer_handle_value(timer.id), + )); enter_timer_callback_dispatch(); with_timer_uncaught_trap(|| { // Installing the timer receiver above is itself a collecting @@ -1359,7 +1364,7 @@ pub extern "C" fn js_callback_timer_tick() -> i32 { // matching Node's `setTimeout1 → micro → setTimeout2` ordering. crate::promise::microtasks::js_promise_run_microtasks_checkpoint(); leave_timer_callback_dispatch(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); crate::async_hooks::after(timer.async_id); crate::async_hooks::destroy(timer.async_id); crate::async_context::refresh_snapshot_from_roots(&mut previous, &previous_roots); @@ -1723,7 +1728,9 @@ pub extern "C" fn js_interval_timer_tick() -> i32 { let previous = crate::async_context::enter_context(&context); let mut previous = previous; let previous_roots = crate::async_context::root_snapshot(&scope, &previous); - let prev_this = crate::object::js_implicit_this_set(timer_handle_value(id)); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); enter_timer_callback_dispatch(); crate::async_hooks::before(async_id, trigger_async_id); with_timer_uncaught_trap(|| { @@ -1744,7 +1751,7 @@ pub extern "C" fn js_interval_timer_tick() -> i32 { }); crate::async_hooks::after(async_id); leave_timer_callback_dispatch(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); crate::async_context::refresh_snapshot_from_roots(&mut previous, &previous_roots); crate::async_context::restore_context(previous); fired += 1; diff --git a/crates/perry-runtime/src/typedarray_props.rs b/crates/perry-runtime/src/typedarray_props.rs index 26d3a44117..ac5e651e61 100644 --- a/crates/perry-runtime/src/typedarray_props.rs +++ b/crates/perry-runtime/src/typedarray_props.rs @@ -238,9 +238,10 @@ fn invoke_typed_array_accessor_getter(get_bits: u64, receiver: f64) -> f64 { if closure.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } @@ -249,9 +250,10 @@ fn invoke_typed_array_accessor_setter(set_bits: u64, receiver: f64, value: f64) if closure.is_null() { return; } - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::closure::js_closure_call1(closure, value); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); } fn barrier_typed_array_own_props(owner: usize, props: &mut [TypedArrayOwnProp]) { diff --git a/crates/perry-runtime/src/url/search_params.rs b/crates/perry-runtime/src/url/search_params.rs index 7627021f5f..6f2afd84b8 100644 --- a/crates/perry-runtime/src/url/search_params.rs +++ b/crates/perry-runtime/src/url/search_params.rs @@ -933,9 +933,11 @@ pub extern "C" fn js_url_search_params_for_each( this_value, ]; unsafe { - let prev_this = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let _ = crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } } diff --git a/crates/perry-runtime/src/util_promisify.rs b/crates/perry-runtime/src/util_promisify.rs index 5fa4c27d98..58210d5695 100644 --- a/crates/perry-runtime/src/util_promisify.rs +++ b/crates/perry-runtime/src/util_promisify.rs @@ -763,7 +763,8 @@ extern "C" fn callbackify_outer_thunk(closure: *const ClosureHeader, rest_value: let on_rejected = nanbox_pointer(rejected_handle.get_raw_const_ptr::() as *const u8); let args = [on_fulfilled, on_rejected]; - let prev_this = crate::object::js_implicit_this_set(returned); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(returned)); unsafe { crate::closure::js_native_call_value( then_handle.get_nanbox_f64(), @@ -771,7 +772,7 @@ extern "C" fn callbackify_outer_thunk(closure: *const ClosureHeader, rest_value: args.len(), ); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return TAG_UNDEFINED_F64; } diff --git a/crates/perry-runtime/src/value/to_string.rs b/crates/perry-runtime/src/value/to_string.rs index 6851ad2086..3f600a3dd3 100644 --- a/crates/perry-runtime/src/value/to_string.rs +++ b/crates/perry-runtime/src/value/to_string.rs @@ -197,13 +197,14 @@ pub(crate) unsafe fn ordinary_to_primitive_for_toprimitive( } let method_handle = scope.root_nanbox_f64(method); let recv = value_handle.get_nanbox_f64(); - let prev_this = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let result = crate::closure::js_native_call_value( method_handle.get_nanbox_f64(), std::ptr::null(), 0, ); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if is_primitive_value(result) { return result; } @@ -456,9 +457,10 @@ pub(crate) unsafe fn call_own_method(method: f64, receiver: f64) -> Option // different value into its reserved `this` slot (an inherited or bound // method), exactly as the method-dispatch tower does (#1982). let bound = crate::closure::clone_closure_rebind_this(bits, receiver); - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); Some(ret) } @@ -833,9 +835,10 @@ unsafe fn call_method_for_primitive( // receiver, so rebinding is a correct no-op. Mirrors #1982. let recv = value_handle.get_nanbox_f64(); let bound = crate::closure::clone_closure_rebind_this(method_bits, recv); - let prev_this = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); let ret_jsv = JSValue::from_bits(ret.to_bits()); let is_primitive = ret_jsv.is_any_string() || ret_jsv.is_number() @@ -888,9 +891,10 @@ unsafe fn call_function_method( let method_handle = scope.root_nanbox_f64(method); let bound = crate::closure::clone_closure_rebind_this(method_handle.get_nanbox_u64(), recv); - let prev_this = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); FunctionMethodOutcome::Value(ret) } diff --git a/crates/perry-runtime/src/value/to_string_class_ref.rs b/crates/perry-runtime/src/value/to_string_class_ref.rs index 19b9700700..83fea2e15a 100644 --- a/crates/perry-runtime/src/value/to_string_class_ref.rs +++ b/crates/perry-runtime/src/value/to_string_class_ref.rs @@ -51,9 +51,10 @@ pub(crate) unsafe fn custom_to_primitive(value: f64, hint: &[u8]) -> CustomToPri if !crate::closure::is_closure_ptr(method_ptr) { return CustomToPrimitiveOutcome::TypeError; } - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(method, &hint, 1); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result }; From 0a512d1c2a92c359e12272d40a112475fef84f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 15:15:58 +0200 Subject: [PATCH 3/6] fix(json): root the replacer closure and root key across the root-level toJSON/replacer calls (#9445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by the #9445 fixture: with the saved implicit-`this` rooted, an allocating replacer still SIGSEGV'd in js_closure_call2. js_json_stringify_full and js_json_stringify_with_replacer run the root toJSON and the root replacer call — both user code — and then handed the walk the raw closure pointer and the "" key. Root both and re-read at each use. --- crates/perry-runtime/src/json/replacer.rs | 40 +++++++++++++++++------ 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/crates/perry-runtime/src/json/replacer.rs b/crates/perry-runtime/src/json/replacer.rs index b98651c75c..48117f34bc 100644 --- a/crates/perry-runtime/src/json/replacer.rs +++ b/crates/perry-runtime/src/json/replacer.rs @@ -685,20 +685,28 @@ pub unsafe extern "C" fn js_json_stringify_with_replacer( // Fall back to normal stringify if replacer is null return js_json_stringify(value, type_hint); } + // #9445: the root `toJSON` and the root replacer call below both run user + // code. The replacer closure and the `""` key are young heap cells held in + // bare locals across them and are consumed AFTER (the key by the replacer + // call, the closure by the walk) — an evacuating minor inside either + // callback left the walk calling a retired closure (SIGSEGV in + // `js_closure_call2`). Root both and re-read at each use. + let root_scope = crate::gc::RuntimeHandleScope::new(); + let replacer_root = root_scope.root_raw_const_ptr(replacer); // Per JSON spec, the initial call to the replacer is with key="" and the // root value — but toJSON runs FIRST (SerializeJSONProperty step 2). let empty_str = js_string_from_bytes(b"".as_ptr(), 0); - let empty_key_f64 = nanbox_string_f64(empty_str); - let value_after_to_json = apply_to_json_keyed(value, empty_key_f64); + let empty_key_root = root_scope.root_nanbox_f64(nanbox_string_f64(empty_str)); + let value_after_to_json = apply_to_json_keyed(value, empty_key_root.get_nanbox_f64()); // Call replacer with ("", root_value), `this` = the `{ "": value }` wrapper. // Per spec the holder wraps the ORIGINAL root value (so a root replacer's // `this[""]` observes the pre-`toJSON` value); only the replacer's value // argument is post-`toJSON`. CodeRabbit (PR #5438). let replaced_root = call_replacer( - replacer, - empty_key_f64, + replacer_root.get_raw_const_ptr::(), + empty_key_root.get_nanbox_f64(), value_after_to_json, root_holder(value), ); @@ -739,7 +747,14 @@ pub unsafe extern "C" fn js_json_stringify_with_replacer( // inline, pointers via the GC-tag dispatch (compact, no indent). if !write_replaced_scalar(&mut buf, replaced_root) { let ptr = extract_pointer(replaced_bits).unwrap(); - dispatch_pointer_with_replacer(ptr, replaced_root, replacer, &mut buf, "", 0); + dispatch_pointer_with_replacer( + ptr, + replaced_root, + replacer_root.get_raw_const_ptr::(), + &mut buf, + "", + 0, + ); } let result = js_string_from_bytes(buf.as_ptr(), buf.len() as u32); @@ -1743,12 +1758,17 @@ pub unsafe extern "C" fn js_json_stringify_full( // Function replacer. Per spec SerializeJSONProperty: toJSON FIRST, then // the replacer, then serialize — threading `indent_str` so the 3-arg // form (replacer + space) pretty-prints, matching Node. + // + // #9445: see `js_json_stringify_with_replacer` — the closure and the + // `""` key live across two user callbacks and are consumed after them. + let root_scope = crate::gc::RuntimeHandleScope::new(); + let replacer_root = root_scope.root_raw_const_ptr(closure_ptr); let empty_str = js_string_from_bytes(b"".as_ptr(), 0); - let empty_key_f64 = nanbox_string_f64(empty_str); - let value_after_to_json = apply_to_json_keyed(value, empty_key_f64); + let empty_key_root = root_scope.root_nanbox_f64(nanbox_string_f64(empty_str)); + let value_after_to_json = apply_to_json_keyed(value, empty_key_root.get_nanbox_f64()); let replaced_root = call_replacer( - closure_ptr, - empty_key_f64, + replacer_root.get_raw_const_ptr::(), + empty_key_root.get_nanbox_f64(), value_after_to_json, root_holder(value_after_to_json), ); @@ -1772,7 +1792,7 @@ pub unsafe extern "C" fn js_json_stringify_full( dispatch_pointer_with_replacer( ptr, replaced_root, - closure_ptr, + replacer_root.get_raw_const_ptr::(), &mut buf, &indent_str, 0, From 412697e57cff6841c7e85188932e154413482b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 15:15:58 +0200 Subject: [PATCH 4/6] fix(runtime): root the displaced receiver once per callback loop; reuse existing handle scopes (#9445) Callback loops (Map/Set/URLSearchParams forEach, EventTarget dispatch, the emitters, fs.watch fan-out, the timer batch, TypedArray.from's map callback) root the caller's receiver once before the loop and restore from that handle each iteration, instead of opening a scope per callback. Single-call sites that already own a RuntimeHandleScope push onto it. Three bare-name callers of js_implicit_this_get are fully qualified. --- crates/perry-runtime/src/array/iterator.rs | 8 +++----- crates/perry-runtime/src/async_hooks.rs | 3 +-- .../perry-runtime/src/child_process/emitter.rs | 6 ++++-- crates/perry-runtime/src/cluster.rs | 12 ++++++++---- crates/perry-runtime/src/event_target.rs | 7 ++++--- .../src/fs/dir_glob_watch/watch.rs | 17 +++++++---------- crates/perry-runtime/src/json/reviver.rs | 3 +-- crates/perry-runtime/src/json/stringify.rs | 3 +-- crates/perry-runtime/src/map.rs | 5 +++-- crates/perry-runtime/src/messaging.rs | 4 +--- .../src/node_submodules/trace_events.rs | 3 +-- crates/perry-runtime/src/node_vm/modules.rs | 3 +-- .../parent_static/private_and_dynamic.rs | 3 +-- .../src/object/global_this/bigint_promise.rs | 6 ++++-- .../src/object/native_call_method.rs | 3 +-- crates/perry-runtime/src/object/property_key.rs | 6 ++---- crates/perry-runtime/src/pty/mod.rs | 6 ++++-- crates/perry-runtime/src/set.rs | 5 +++-- crates/perry-runtime/src/symbol/iterator.rs | 3 +-- crates/perry-runtime/src/timer.rs | 13 +++++-------- crates/perry-runtime/src/url/search_params.rs | 7 ++++--- crates/perry-runtime/src/util_promisify.rs | 3 +-- crates/perry-runtime/src/value/to_string.rs | 11 +++++------ 23 files changed, 66 insertions(+), 74 deletions(-) diff --git a/crates/perry-runtime/src/array/iterator.rs b/crates/perry-runtime/src/array/iterator.rs index 1b3ffffd88..7b1e352868 100644 --- a/crates/perry-runtime/src/array/iterator.rs +++ b/crates/perry-runtime/src/array/iterator.rs @@ -481,9 +481,8 @@ fn async_from_sync_call_raw(iter: f64, method: &[u8], args: &[f64]) -> Result *mut ArrayHeader { // Call(next, iterator): bind `this` for the stored-closure path // exactly like `js_iterator_next_result` — a user-assigned // `it.next = function () { … }` may read `this` (#9019). - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope - .root_nanbox_f64(crate::object::js_implicit_this_set(iter_h.get_nanbox_f64())); + let prev_this = + scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_h.get_nanbox_f64())); let r = closure::js_closure_call1( js_nanbox_get_pointer(next_h.get_nanbox_f64()) as *const closure::ClosureHeader, f64::from_bits(TAG_UNDEFINED), diff --git a/crates/perry-runtime/src/async_hooks.rs b/crates/perry-runtime/src/async_hooks.rs index 28703c7f53..05490c2f1d 100644 --- a/crates/perry-runtime/src/async_hooks.rs +++ b/crates/perry-runtime/src/async_hooks.rs @@ -1811,8 +1811,7 @@ fn call_callback_with_rest(callback_value: f64, this_arg: f64, rest: f64) -> f64 } let args_array = ptr_from_nanboxed(rest) as *const ArrayHeader; let args_array_handle = scope.root_raw_const_ptr(args_array); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( this_arg_handle.get_nanbox_f64(), )); let result = if args_array.is_null() { diff --git a/crates/perry-runtime/src/child_process/emitter.rs b/crates/perry-runtime/src/child_process/emitter.rs index aca68e59b4..d816ea5618 100644 --- a/crates/perry-runtime/src/child_process/emitter.rs +++ b/crates/perry-runtime/src/child_process/emitter.rs @@ -52,6 +52,9 @@ pub(crate) fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool { let key = cp_listener_key(event); let mut i: u32 = 0; let mut fired = false; + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let arr = match cp_array_ptr(cp_get_field(target, &key)) { Some(a) => a, @@ -61,8 +64,7 @@ pub(crate) fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool { break; } let cb = crate::array::js_array_get_f64(arr, i); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(js_implicit_this_set(target)); + js_implicit_this_set(target); unsafe { let _ = js_native_call_value(cb, args.as_ptr(), args.len()); } diff --git a/crates/perry-runtime/src/cluster.rs b/crates/perry-runtime/src/cluster.rs index dee5698e39..4dfcc26d8e 100644 --- a/crates/perry-runtime/src/cluster.rs +++ b/crates/perry-runtime/src/cluster.rs @@ -303,10 +303,12 @@ pub(crate) fn cluster_emit_event(event: &str, args: &[f64]) -> bool { if listeners.is_empty() { return false; } + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for listener in listeners { let cb = f64::from_bits(listener.callback_bits); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(js_implicit_this_set(cluster_default_value())); + js_implicit_this_set(cluster_default_value()); unsafe { let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); } @@ -1416,6 +1418,9 @@ fn emit(target: f64, event: &str, args: &[f64]) -> bool { let key = listener_key(event); let mut i = 0; let mut fired = false; + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let Some(arr) = array_ptr(get_field(target, &key)) else { break; @@ -1424,8 +1429,7 @@ fn emit(target: f64, event: &str, args: &[f64]) -> bool { break; } let cb = crate::array::js_array_get_f64(arr, i); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(js_implicit_this_set(target)); + js_implicit_this_set(target); unsafe { let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); } diff --git a/crates/perry-runtime/src/event_target.rs b/crates/perry-runtime/src/event_target.rs index fabb6e1e25..31d5c4b99d 100644 --- a/crates/perry-runtime/src/event_target.rs +++ b/crates/perry-runtime/src/event_target.rs @@ -979,6 +979,9 @@ pub unsafe extern "C" fn js_event_target_dispatch_event( .unwrap_or_default(); let args = [event]; + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for (callback, capture, once) in callbacks { let Some(callable) = closure_value_from_listener(callback) else { continue; @@ -986,9 +989,7 @@ pub unsafe extern "C" fn js_event_target_dispatch_event( if once { remove_event_listener_value_with_capture(target, event_name_ptr, callback, capture); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(target_value)); + crate::object::js_implicit_this_set(target_value); let _ = crate::closure::js_native_call_value(callable, args.as_ptr(), args.len()); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if event_bool_field(event_ptr, b"_immediateStopped") { diff --git a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs index d833dd306b..3502a4755d 100644 --- a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs +++ b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs @@ -518,8 +518,7 @@ fn emit_listener0(object_value: f64, callback: f64) { if cb.is_null() { return; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( object_handle.get_nanbox_f64(), )); with_watcher_uncaught_trap(|| { @@ -549,15 +548,14 @@ fn emit_fs_watch_event( let refreshed_callbacks = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&callback_handles); let refreshed_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for callback in refreshed_callbacks { let cb = extract_closure_ptr(callback); if cb.is_null() { continue; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( - object_handle.get_nanbox_f64(), - )); + crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); with_watcher_uncaught_trap(|| { crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); }); @@ -587,15 +585,14 @@ fn emit_watch_file_change( let refreshed_callbacks = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&callback_handles); let refreshed_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for callback in refreshed_callbacks { let cb = extract_closure_ptr(callback); if cb.is_null() { continue; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( - object_handle.get_nanbox_f64(), - )); + crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); with_watcher_uncaught_trap(|| { crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); }); diff --git a/crates/perry-runtime/src/json/reviver.rs b/crates/perry-runtime/src/json/reviver.rs index 6e74da888d..8280dfe10b 100644 --- a/crates/perry-runtime/src/json/reviver.rs +++ b/crates/perry-runtime/src/json/reviver.rs @@ -715,8 +715,7 @@ unsafe fn call_reviver( let holder_arg = holder_handle.get_nanbox_f64(); let key_arg = key_handle.get_nanbox_f64(); let value_arg = value_handle.get_nanbox_f64(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(holder_arg)); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(holder_arg)); let result = crate::js_closure_call3(reviver, key_arg, value_arg, context_handle.get_nanbox_f64()); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); diff --git a/crates/perry-runtime/src/json/stringify.rs b/crates/perry-runtime/src/json/stringify.rs index f3db707484..fc54cecbc3 100644 --- a/crates/perry-runtime/src/json/stringify.rs +++ b/crates/perry-runtime/src/json/stringify.rs @@ -347,8 +347,7 @@ pub(crate) unsafe fn array_get_to_json(arr: *const crate::ArrayHeader) -> Option let recv_handle = scope.root_nanbox_f64(recv); // `toJSON(key)` receives the property key of this array value (#5909). let key_f64_arg = current_to_json_key_arg(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( recv_handle.get_nanbox_f64(), )); let result = crate::closure::js_native_call_value(f64::from_bits(method_bits), &key_f64_arg, 1); diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index 3a7b790383..27fc9474c2 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -3128,6 +3128,8 @@ fn js_map_foreach_impl( // appends are visited too. Bounding the walk by `size` exposed holes // and truncated later entries (#9072). let mut i = 0usize; + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let map = map_handle.get_raw_const_ptr::(); if i >= (*map).used as usize { @@ -3154,8 +3156,7 @@ fn js_map_foreach_impl( // Bind `thisArg` for the duration of the call (matches the // URLSearchParams.forEach pattern); `js_native_call_value` // dispatches the NaN-boxed callback with the full arg vector. - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_v)); + crate::object::js_implicit_this_set(this_v); let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } diff --git a/crates/perry-runtime/src/messaging.rs b/crates/perry-runtime/src/messaging.rs index e87b5f3f1a..c16f9bec78 100644 --- a/crates/perry-runtime/src/messaging.rs +++ b/crates/perry-runtime/src/messaging.rs @@ -279,9 +279,7 @@ fn invoke_message_handler(handler: f64, event: f64, port_box: f64) { let handler_h = scope.root_nanbox_f64(handler); let event_h = scope.root_nanbox_f64(event); let port_h = scope.root_nanbox_f64(port_box); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(object::js_implicit_this_set(port_h.get_nanbox_f64())); + let prev_this = scope.root_nanbox_f64(object::js_implicit_this_set(port_h.get_nanbox_f64())); let args = [event_h.get_nanbox_f64()]; unsafe { let _ = crate::closure::js_native_call_value( diff --git a/crates/perry-runtime/src/node_submodules/trace_events.rs b/crates/perry-runtime/src/node_submodules/trace_events.rs index af52b7b484..540ea0f4ac 100644 --- a/crates/perry-runtime/src/node_submodules/trace_events.rs +++ b/crates/perry-runtime/src/node_submodules/trace_events.rs @@ -611,8 +611,7 @@ fn emit_enabled_trace_warning() { let process = scope.root_nanbox_f64(process); let callback = scope.root_nanbox_f64(callback); let warning = scope.root_nanbox_f64(warning); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let previous = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_set( process.get_nanbox_f64(), )); unsafe { diff --git a/crates/perry-runtime/src/node_vm/modules.rs b/crates/perry-runtime/src/node_vm/modules.rs index a0f94f56e4..9d27ad49fd 100644 --- a/crates/perry-runtime/src/node_vm/modules.rs +++ b/crates/perry-runtime/src/node_vm/modules.rs @@ -75,8 +75,7 @@ fn evaluate_synthetic_module(module: *mut ObjectHeader) -> f64 { })); let js = JSValue::from_bits(callback.get_nanbox_f64().to_bits()); if !js.is_undefined() && !js.is_null() { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(with_hmut( + let prev = scope.root_nanbox_f64(crate::object::js_implicit_this_set(with_hmut( &module, object_value, ))); diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs index ae568602a3..eff6bec662 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs @@ -295,8 +295,7 @@ pub(crate) unsafe fn call_private_static_method_for_owner( let scope = crate::gc::RuntimeHandleScope::new(); let this_value = scope.root_nanbox_f64(this_value); let private_brand = scope.root_nanbox_f64(private_brand); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let previous_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( this_value.get_nanbox_f64(), )); crate::object::static_private_owner_push(private_brand.get_nanbox_f64()); diff --git a/crates/perry-runtime/src/object/global_this/bigint_promise.rs b/crates/perry-runtime/src/object/global_this/bigint_promise.rs index c15889a8ab..d03cb60106 100644 --- a/crates/perry-runtime/src/object/global_this/bigint_promise.rs +++ b/crates/perry-runtime/src/object/global_this/bigint_promise.rs @@ -692,12 +692,14 @@ pub(crate) extern "C" fn typed_array_from_thunk( // possibly throwing) element coercion INTERLEAVE per spec, so an abrupt // coercion at element k means the map callback never ran for k+1 // (test262 from/set-value-abrupt-completion). + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); let map_at = |k: usize, v: f64| -> f64 { if map_closure.is_null() { return v; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); + crate::object::js_implicit_this_set(this_arg); let r = crate::closure::js_closure_call2(map_closure, v, k as f64); crate::object::js_implicit_this_set(prev.get_nanbox_f64()); r diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index fe73a62449..6f2301c9d0 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -2510,9 +2510,8 @@ pub unsafe extern "C-unwind" fn js_native_call_method( if crate::promise::subclass_backing_promise(object()).is_some() { if let Some(m) = crate::promise::promise_proto_method(method_name) { let args = refreshed_args(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object())); + root_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object())); let result = crate::closure::js_native_call_value(m, args.as_ptr(), args.len()); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index d1d360a125..116377a03d 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -129,8 +129,7 @@ unsafe fn ordinary_to_primitive_string_key(value: f64) -> Option { continue; } let bound = crate::closure::clone_closure_rebind_this(method_bits, receiver); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); @@ -570,8 +569,7 @@ pub unsafe extern "C" fn js_object_super_call( let bound = crate::closure::clone_closure_rebind_this(callee_handle.get_nanbox_u64(), receiver); let bound_handle = scope.root_nanbox_u64(bound); let receiver = f64::from_bits(receiver_handle.get_heap_word_u64()); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value( f64::from_bits(bound_handle.get_nanbox_u64()), args_ptr, diff --git a/crates/perry-runtime/src/pty/mod.rs b/crates/perry-runtime/src/pty/mod.rs index c8a30e438d..439be74300 100644 --- a/crates/perry-runtime/src/pty/mod.rs +++ b/crates/perry-runtime/src/pty/mod.rs @@ -77,6 +77,9 @@ mod unix_impl { pub(crate) fn pty_emit(target: f64, event: &str, args: &[f64]) { let key = pty_listener_key(event); let mut i: u32 = 0; + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let arr = match cp_array_ptr(cp_get_field(target, &key)) { Some(a) => a, @@ -86,8 +89,7 @@ mod unix_impl { break; } let cb = crate::array::js_array_get_f64(arr, i); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(js_implicit_this_set(target)); + js_implicit_this_set(target); unsafe { let _ = js_native_call_value(cb, args.as_ptr(), args.len()); } diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 7f12f45666..0b4b7a4768 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -1919,6 +1919,8 @@ fn js_set_foreach_impl( // during the callback are visited too. Bounding this by the live count // surfaced holes and stopped before later live values (#9072). let mut i = 0usize; + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let set = set_handle.get_raw_const_ptr::(); if i >= (*set).used as usize { @@ -1941,8 +1943,7 @@ fn js_set_foreach_impl( let args = [value, value, set_value]; let cb = callback_handle.get_nanbox_f64(); let this_v = this_handle.get_nanbox_f64(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_v)); + crate::object::js_implicit_this_set(this_v); let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } diff --git a/crates/perry-runtime/src/symbol/iterator.rs b/crates/perry-runtime/src/symbol/iterator.rs index 80de035c62..2c732a05d3 100644 --- a/crates/perry-runtime/src/symbol/iterator.rs +++ b/crates/perry-runtime/src/symbol/iterator.rs @@ -654,8 +654,7 @@ pub unsafe extern "C" fn js_to_primitive(value: f64, hint: i32) -> f64 { // `this.celsius` resolved to `undefined`, so `+t` was `NaN` and `` `${t}` `` // was `undefined°C` (test_gap_symbols). The proxy arm above already binds // `this`; mirror it for the closure method. - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( value_handle.get_nanbox_f64(), )); // Spec says the return value must be a primitive; if it's still an diff --git a/crates/perry-runtime/src/timer.rs b/crates/perry-runtime/src/timer.rs index 5f3a27675c..c09f2cac66 100644 --- a/crates/perry-runtime/src/timer.rs +++ b/crates/perry-runtime/src/timer.rs @@ -516,9 +516,8 @@ fn call_timer_callback( let previous_roots = crate::async_context::root_snapshot(&scope, &previous); let a = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); let cb = callback_handle.get_raw_const_ptr::(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); + scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); with_timer_uncaught_trap(|| unsafe { crate::closure::js_closure_call_array(cb as i64, a.as_ptr(), a.len() as i64); }); @@ -1296,6 +1295,8 @@ pub extern "C" fn js_callback_timer_tick() -> i32 { let mut fired = 0; // Call the callbacks, forwarding any trailing args captured at // `setTimeout(fn, delay, ...args)` time. Refs #665. + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = batch_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for (index, mut timer) in expired.into_iter().enumerate() { if !timer.cleared { crate::async_context::refresh_snapshot_from_roots( @@ -1306,10 +1307,7 @@ pub extern "C" fn js_callback_timer_tick() -> i32 { let mut previous = previous; let previous_roots = crate::async_context::root_snapshot(&batch_scope, &previous); crate::async_hooks::before(timer.async_id, timer.trigger_async_id); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( - timer_handle_value(timer.id), - )); + crate::object::js_implicit_this_set(timer_handle_value(timer.id)); enter_timer_callback_dispatch(); with_timer_uncaught_trap(|| { // Installing the timer receiver above is itself a collecting @@ -1728,9 +1726,8 @@ pub extern "C" fn js_interval_timer_tick() -> i32 { let previous = crate::async_context::enter_context(&context); let mut previous = previous; let previous_roots = crate::async_context::root_snapshot(&scope, &previous); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); + scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); enter_timer_callback_dispatch(); crate::async_hooks::before(async_id, trigger_async_id); with_timer_uncaught_trap(|| { diff --git a/crates/perry-runtime/src/url/search_params.rs b/crates/perry-runtime/src/url/search_params.rs index 6f2afd84b8..d43d066cad 100644 --- a/crates/perry-runtime/src/url/search_params.rs +++ b/crates/perry-runtime/src/url/search_params.rs @@ -926,6 +926,9 @@ pub extern "C" fn js_url_search_params_for_each( crate::fs::validate::validate_function("callback", callback); let entries = get_url_search_params_entries(params); let this_value = crate::value::js_nanbox_pointer(params as i64); + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for (key, value) in entries { let args = [ create_string_f64(&value), @@ -933,9 +936,7 @@ pub extern "C" fn js_url_search_params_for_each( this_value, ]; unsafe { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); + crate::object::js_implicit_this_set(this_arg); let _ = crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } diff --git a/crates/perry-runtime/src/util_promisify.rs b/crates/perry-runtime/src/util_promisify.rs index 58210d5695..262d6bbcd1 100644 --- a/crates/perry-runtime/src/util_promisify.rs +++ b/crates/perry-runtime/src/util_promisify.rs @@ -763,8 +763,7 @@ extern "C" fn callbackify_outer_thunk(closure: *const ClosureHeader, rest_value: let on_rejected = nanbox_pointer(rejected_handle.get_raw_const_ptr::() as *const u8); let args = [on_fulfilled, on_rejected]; - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(returned)); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(returned)); unsafe { crate::closure::js_native_call_value( then_handle.get_nanbox_f64(), diff --git a/crates/perry-runtime/src/value/to_string.rs b/crates/perry-runtime/src/value/to_string.rs index 3f600a3dd3..12bbd5ec1f 100644 --- a/crates/perry-runtime/src/value/to_string.rs +++ b/crates/perry-runtime/src/value/to_string.rs @@ -183,6 +183,8 @@ pub(crate) unsafe fn ordinary_to_primitive_for_toprimitive( } else { [b"valueOf", b"toString"] }; + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for name in order { let recv = value_handle.get_nanbox_f64(); let key_ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); @@ -197,8 +199,7 @@ pub(crate) unsafe fn ordinary_to_primitive_for_toprimitive( } let method_handle = scope.root_nanbox_f64(method); let recv = value_handle.get_nanbox_f64(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); + crate::object::js_implicit_this_set(recv); let result = crate::closure::js_native_call_value( method_handle.get_nanbox_f64(), std::ptr::null(), @@ -835,8 +836,7 @@ unsafe fn call_method_for_primitive( // receiver, so rebinding is a correct no-op. Mirrors #1982. let recv = value_handle.get_nanbox_f64(); let bound = crate::closure::clone_closure_rebind_this(method_bits, recv); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); let ret_jsv = JSValue::from_bits(ret.to_bits()); @@ -891,8 +891,7 @@ unsafe fn call_function_method( let method_handle = scope.root_nanbox_f64(method); let bound = crate::closure::clone_closure_rebind_this(method_handle.get_nanbox_u64(), recv); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); From 23c845c68ef47d36cd5b2a4b8e0e157ca592fa91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 15:29:08 +0200 Subject: [PATCH 5/6] =?UTF-8?q?test:=20#9445=20fixture=20=E2=80=94=2034=20?= =?UTF-8?q?witnesses=20for=20the=20implicit-this=20restore=20across=20a=20?= =?UTF-8?q?moving=20minor,=20plus=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../9445-implicit-this-restore-sweep.md | 62 ++ ...st_gap_9445_implicit_this_restore_sweep.ts | 569 ++++++++++++++++++ 2 files changed, 631 insertions(+) create mode 100644 changelog.d/9445-implicit-this-restore-sweep.md create mode 100644 test-files/test_gap_9445_implicit_this_restore_sweep.ts diff --git a/changelog.d/9445-implicit-this-restore-sweep.md b/changelog.d/9445-implicit-this-restore-sweep.md new file mode 100644 index 0000000000..d485226def --- /dev/null +++ b/changelog.d/9445-implicit-this-restore-sweep.md @@ -0,0 +1,62 @@ +**Every runtime callback site now restores the caller's `this` through a GC +root** (#9445) — the sweep PR #9444 asked for after fixing four accessor sites +for #9417. + +The runtime binds a callback's receiver by writing the GC-rooted +`IMPLICIT_THIS` cell and keeping the previous occupant in a **bare Rust local** +for the duration of the callback: + +```rust +let prev = js_implicit_this_set(receiver); +… user code, which allocates … +js_implicit_this_set(prev); // pre-collection address +``` + +That local is the caller's receiver, and the collector cannot see or rewrite +it. An evacuating young-gen minor inside the window — perry's default GC since +PR #7019 — relocates the caller's object, and the restore reinstalls a retired +from-space address as the caller's `this`. Nothing faults: the caller's next +`this.` fails the object-type check on the recycled cell and answers +`undefined`, so the member access after it throws a TypeError naming a +property nowhere near the defect (#9417's `Cannot read properties of +undefined (reading 'def')`). + +The issue counted ~20 sites; a grep of the whole runtime finds **122** +unrooted save/restores in 65 files (one of them landed with #9518 while this +sweep was in flight) (timers, node streams, dgram, cluster, +`fs.watch`, `EventTarget`, `Map`/`Set`/`URLSearchParams.forEach`, promisify, +JSON `toJSON`/replacer/reviver, ToPrimitive and ToPropertyKey, the iterator +protocol, Proxy traps and `Reflect`, bound functions, `super.x`, static +dispatch, …). Every one is now the idiom `prototype_chain.rs` and PR #9444 +already use: root the saved value in a `RuntimeHandleScope` and re-read it at +the restore. Nine sites were already rooted; `dyn_eval/bridge.rs` roots +through its own stack. None of the 122 could be left alone — every one calls +user code (a closure, a class accessor or static method, a Proxy trap, a +`then`), which can allocate. Callback loops (`Map`/`Set`/`URLSearchParams.forEach`, +`EventTarget` dispatch, the emitters, watchers and timer batches) root the +displaced receiver **once per loop** rather than once per callback, and sites +that already own a `RuntimeHandleScope` reuse it, so the hot per-callback cost +is one handle read. Three sites also consumed their receiver again *after* the +call (`intl_subclass_super`, `temporal_subclass_super`, and the `process.stdin` +listener loops); those re-read it through a root too. + +**Also fixed, same family, found by the fixture:** `JSON.stringify(value, +replacerFn)` handed the walk a **raw replacer closure pointer** after the +root-level replacer call (and the root `toJSON`) had run user code +(`json/replacer.rs`, both the pretty and the compact entry points). With an +allocating replacer this was a SIGSEGV in `js_closure_call2` — the walk called +a retired closure — and it survived the `prev` rooting alone. The closure and +the `""` key are now rooted across those calls. + +**Test.** `test-files/test_gap_9445_implicit_this_restore_sweep.ts` — 34 +cases, one per synchronously reachable site family, each a `function`-method +on a fresh young object that drives the site with an allocating callback and +then reads `this`. Deterministic, no GC env knobs; the PR description records +which cases print a non-zero `bad=` count on unfixed `main`. Event-loop-driven +sites (timers, `process.stdin`, dgram, cluster, `fs.watch`, pty, child +process) only see a heap `prev` from a nested pump and have no synchronous +reproduction; they carry the same mechanical fix. Two further candidate cases +(a `defineProperty` accessor on a typed array, a `toISOString` override on a +`Date`) diverge from node for a reason unrelated to rooting and are filed as +#9529; a `util.callbackify` case crashed the microtask pump at exit on every +build (a promise-side rooting bug, filed separately) and is not in the file. diff --git a/test-files/test_gap_9445_implicit_this_restore_sweep.ts b/test-files/test_gap_9445_implicit_this_restore_sweep.ts new file mode 100644 index 0000000000..3205cfd48a --- /dev/null +++ b/test-files/test_gap_9445_implicit_this_restore_sweep.ts @@ -0,0 +1,569 @@ +// #9445: every runtime site that binds `this` for a callback must not corrupt +// the CALLER's `this` across an evacuating young-gen minor. +// +// The shape (#9417 / PR #9444 fixed it for the accessor sites; this is the +// sweep of the ~120 others): +// +// let prev = js_implicit_this_set(receiver); +// … user code, which allocates … +// js_implicit_this_set(prev); // pre-collection address +// +// `prev` is the caller's receiver in a bare Rust local. A copying minor inside +// the window relocates that object and rewrites every slot the collector can +// see — a Rust local is not one — so the restore reinstalls a retired +// from-space address as the caller's `this`. Nothing faults: the caller's next +// `this.` reads `undefined` off the recycled cell and the member access +// after it throws `Cannot read properties of undefined (reading 'def')`. +// +// Each case below is a factory returning a fresh (young, escaping) object whose +// `run` is a `function`-expression method — one that reads `this` dynamically +// off the implicit-`this` cell, not from a captured slot. `run` drives ONE +// runtime site with a callback that allocates past the nursery, then reads +// `this.inner.def`. The callback is reached through a syntax or a typed +// builtin that lowers to a direct runtime call, so no compiled-code +// save/restore sits between the runtime site and `run`'s next `this` read. +// +// Pre-fix each case prints a non-zero `bad=` count, deterministically and with +// no GC env knobs. Node prints 0 for every line. + +import { Writable, Readable, Transform } from "node:stream"; + +const N = 1500; + +function churn(): number { + const tmp: any[] = []; + for (let k = 0; k < 480; k++) tmp.push({ k: k, s: "t" + k, pad: [k, k + 1] }); + return tmp.length; +} + +function check(name: string, factory: (i: number) => any): void { + let bad = 0; + const notes: string[] = []; + for (let i = 0; i < N; i++) { + const c: any = factory(i); + const want = "c" + i + ":480"; + let got: any; + try { + got = c.run(); + } catch (e: any) { + got = "THREW:" + (e && e.message); + } + if (got !== want) { + bad++; + if (bad <= 2) notes.push("[" + i + " got=" + String(got) + "]"); + } + } + console.log(name + " bad=" + bad + notes.join("")); +} + +function host(i: number, run: (this: any) => string): any { + return { id: i, inner: { def: "c" + i }, run: run }; +} + +function userIterable(onNext: () => void): any { + return { + [Symbol.iterator]: function () { + let done = false; + return { + next: function () { + if (done) return { done: true, value: undefined }; + done = true; + onNext(); + return { done: false, value: 1 }; + }, + }; + }, + }; +} + +// --- collections --------------------------------------------------------- + +check("map_forEach", function (i) { + const m = new Map([[1, 1]]); + return host(i, function (this: any) { + let n = 0; + m.forEach(function () { + n = churn(); + }); + return this.inner.def + ":" + n; + }); +}); + +check("set_forEach", function (i) { + const s = new Set([1]); + return host(i, function (this: any) { + let n = 0; + s.forEach(function () { + n = churn(); + }); + return this.inner.def + ":" + n; + }); +}); + +check("urlsearchparams_forEach", function (i) { + const p = new URLSearchParams("a=1"); + return host(i, function (this: any) { + let n = 0; + p.forEach(function () { + n = churn(); + }); + return this.inner.def + ":" + n; + }); +}); + +// --- events -------------------------------------------------------------- + +check("event_target_dispatch", function (i) { + const et = new EventTarget(); + let n = 0; + et.addEventListener("x", function () { + n = churn(); + }); + return host(i, function (this: any) { + et.dispatchEvent(new Event("x")); + return this.inner.def + ":" + n; + }); +}); + +// --- property keys, accessors, ToPrimitive -------------------------------- + +check("to_property_key_toString", function (i) { + const target: any = { k: 1 }; + return host(i, function (this: any) { + let n = 0; + const key: any = { + toString: function () { + n = churn(); + return "k"; + }, + }; + const v = target[key]; + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +check("defineProperty_setter", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = {}; + Object.defineProperty(o, "s", { + set: function (_v: any) { + n = churn(); + }, + configurable: true, + }); + o.s = 1; + return this.inner.def + ":" + n; + }); +}); + +check("function_object_getter", function (i) { + return host(i, function (this: any) { + let n = 0; + const f: any = function () {}; + Object.defineProperty(f, "g", { + get: function () { + n = churn(); + return 1; + }, + configurable: true, + }); + const v = f.g; + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +check("valueOf_to_primitive", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = { + valueOf: function () { + n = churn(); + return 1; + }, + }; + const v = o + 1; + return this.inner.def + ":" + (v === 2 ? n : -1); + }); +}); + +check("toString_template", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = { + toString: function () { + n = churn(); + return "s"; + }, + }; + const v = `${o}`; + return this.inner.def + ":" + (v === "s" ? n : -1); + }); +}); + +check("symbol_toPrimitive", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = { + [Symbol.toPrimitive]: function (_hint: string) { + n = churn(); + return 1; + }, + }; + const v = +o; + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +// --- JSON ---------------------------------------------------------------- + +check("json_stringify_getter", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = {}; + Object.defineProperty(o, "g", { + get: function () { + n = churn(); + return 1; + }, + enumerable: true, + configurable: true, + }); + const s = JSON.stringify(o); + return this.inner.def + ":" + (s === '{"g":1}' ? n : -1); + }); +}); + +check("json_stringify_toJSON", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = { + toJSON: function () { + n = churn(); + return 1; + }, + }; + const s = JSON.stringify({ o: o }); + return this.inner.def + ":" + (s === '{"o":1}' ? n : -1); + }); +}); + +check("json_stringify_replacer", function (i) { + return host(i, function (this: any) { + let n = 0; + const s = JSON.stringify({ a: 1 }, function (_k: string, v: any) { + n = churn(); + return v; + }); + return this.inner.def + ":" + (s === '{"a":1}' ? n : -1); + }); +}); + +check("json_parse_reviver", function (i) { + return host(i, function (this: any) { + let n = 0; + const o = JSON.parse('{"a":1}', function (_k: string, v: any) { + n = churn(); + return v; + }); + return this.inner.def + ":" + (o.a === 1 ? n : -1); + }); +}); + +// --- iteration protocol -------------------------------------------------- + +check("for_of_user_iterator", function (i) { + return host(i, function (this: any) { + let n = 0; + let sum = 0; + for (const v of userIterable(function () { + n = churn(); + })) { + sum += v; + } + return this.inner.def + ":" + (sum === 1 ? n : -1); + }); +}); + +check("spread_user_iterator", function (i) { + return host(i, function (this: any) { + let n = 0; + const arr = [ + ...userIterable(function () { + n = churn(); + }), + ]; + return this.inner.def + ":" + (arr.length === 1 ? n : -1); + }); +}); + +check("destructure_user_iterator", function (i) { + return host(i, function (this: any) { + let n = 0; + const [first] = userIterable(function () { + n = churn(); + }); + return this.inner.def + ":" + (first === 1 ? n : -1); + }); +}); + +check("array_from_user_iterator", function (i) { + return host(i, function (this: any) { + let n = 0; + const arr = Array.from( + userIterable(function () { + n = churn(); + }), + ); + return this.inner.def + ":" + (arr.length === 1 ? n : -1); + }); +}); + +// --- Proxy / Reflect ----------------------------------------------------- + +check("proxy_get_trap", function (i) { + return host(i, function (this: any) { + let n = 0; + const p: any = new Proxy( + {}, + { + get: function (_t: any, _k: any) { + n = churn(); + return 1; + }, + }, + ); + const v = p.x; + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +check("proxy_set_trap", function (i) { + return host(i, function (this: any) { + let n = 0; + const p: any = new Proxy( + {}, + { + set: function (_t: any, _k: any, _v: any) { + n = churn(); + return true; + }, + }, + ); + p.x = 1; + return this.inner.def + ":" + n; + }); +}); + +check("proxy_apply_trap", function (i) { + return host(i, function (this: any) { + let n = 0; + const p: any = new Proxy(function () {}, { + apply: function () { + n = churn(); + return 1; + }, + }); + const v = p(); + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +check("proxy_construct_trap", function (i) { + return host(i, function (this: any) { + let n = 0; + const p: any = new Proxy(function () {}, { + construct: function () { + n = churn(); + return { ok: 1 }; + }, + }); + const v = new p(); + return this.inner.def + ":" + (v.ok === 1 ? n : -1); + }); +}); + +check("reflect_apply", function (i) { + return host(i, function (this: any) { + let n = 0; + const v = Reflect.apply( + function () { + n = churn(); + return 1; + }, + null, + [], + ); + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +check("reflect_get_receiver_getter", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = { + get g() { + n = churn(); + return 1; + }, + }; + const v = Reflect.get(o, "g", o); + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +// --- functions ----------------------------------------------------------- + +check("bound_function_call", function (i) { + return host(i, function (this: any) { + let n = 0; + const b = function (this: any) { + n = churn(); + return 1; + }.bind({}); + const v = b(); + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +check("string_replace_callback", function (i) { + return host(i, function (this: any) { + let n = 0; + const s = "abc".replace(/b/, function () { + n = churn(); + return "x"; + }); + return this.inner.def + ":" + (s === "axc" ? n : -1); + }); +}); + +check("using_dispose", function (i) { + return host(i, function (this: any) { + let n = 0; + { + using _r = { + [Symbol.dispose]: function () { + n = churn(); + }, + }; + } + return this.inner.def + ":" + n; + }); +}); + +// --- node:stream user hooks ---------------------------------------------- + +check("writable_write", function (i) { + let n = 0; + const w = new Writable({ + write(_chunk, _enc, cb) { + n = churn(); + cb(); + }, + }); + return host(i, function (this: any) { + w.write("x"); + return this.inner.def + ":" + n; + }); +}); + +check("writable_writev", function (i) { + let n = 0; + const w = new Writable({ + write(_chunk, _enc, cb) { + cb(); + }, + writev(_chunks, cb) { + n = churn(); + cb(); + }, + }); + return host(i, function (this: any) { + w.cork(); + w.write("a"); + w.write("b"); + w.uncork(); + return this.inner.def + ":" + n; + }); +}); + +check("transform_transform", function (i) { + let n = 0; + const t = new Transform({ + transform(chunk, _enc, cb) { + n = churn(); + cb(null, chunk); + }, + }); + t.on("data", function () {}); + return host(i, function (this: any) { + t.write("x"); + return this.inner.def + ":" + n; + }); +}); + +check("writable_construct", function (i) { + return host(i, function (this: any) { + let n = 0; + new Writable({ + construct(cb) { + n = churn(); + cb(); + }, + write(_chunk, _enc, cb) { + cb(); + }, + }); + // node defers `_construct` to nextTick; only the caller's `this` matters. + return this.inner.def + ":" + (n === 0 || n === 480 ? 480 : -1); + }); +}); + +check("readable_read", function (i) { + let n = 0; + const r = new Readable({ + read() { + n = churn(); + this.push(null); + }, + }); + return host(i, function (this: any) { + r.read(); + return this.inner.def + ":" + n; + }); +}); + +check("writable_final", function (i) { + let n = 0; + const w = new Writable({ + write(_chunk, _enc, cb) { + cb(); + }, + final(cb) { + n = churn(); + cb(); + }, + }); + return host(i, function (this: any) { + w.end(); + return this.inner.def + ":" + n; + }); +}); + +check("transform_flush", function (i) { + let n = 0; + const t = new Transform({ + transform(chunk, _enc, cb) { + cb(null, chunk); + }, + flush(cb) { + n = churn(); + cb(); + }, + }); + t.on("data", function () {}); + return host(i, function (this: any) { + t.end(); + return this.inner.def + ":" + n; + }); +}); From f06f2b605dde360017de26064c5997e91cf0bff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 16:34:06 +0200 Subject: [PATCH 6/6] fix(gates): census baseline refresh for the property_set split; #9541's replacer closure reads via with_const_ptr --- crates/perry-runtime/src/json/replacer.rs | 57 ++++++++++--------- scripts/shape_descriptor_census_baseline.json | 4 +- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/crates/perry-runtime/src/json/replacer.rs b/crates/perry-runtime/src/json/replacer.rs index 48117f34bc..9458b81351 100644 --- a/crates/perry-runtime/src/json/replacer.rs +++ b/crates/perry-runtime/src/json/replacer.rs @@ -704,12 +704,14 @@ pub unsafe extern "C" fn js_json_stringify_with_replacer( // Per spec the holder wraps the ORIGINAL root value (so a root replacer's // `this[""]` observes the pre-`toJSON` value); only the replacer's value // argument is post-`toJSON`. CodeRabbit (PR #5438). - let replaced_root = call_replacer( - replacer_root.get_raw_const_ptr::(), - empty_key_root.get_nanbox_f64(), - value_after_to_json, - root_holder(value), - ); + let replaced_root = replacer_root.with_const_ptr::(|replacer| { + call_replacer( + replacer, + empty_key_root.get_nanbox_f64(), + value_after_to_json, + root_holder(value), + ) + }); let replaced_bits = replaced_root.to_bits(); // If replacer returns undefined for root, return undefined. @@ -747,14 +749,9 @@ pub unsafe extern "C" fn js_json_stringify_with_replacer( // inline, pointers via the GC-tag dispatch (compact, no indent). if !write_replaced_scalar(&mut buf, replaced_root) { let ptr = extract_pointer(replaced_bits).unwrap(); - dispatch_pointer_with_replacer( - ptr, - replaced_root, - replacer_root.get_raw_const_ptr::(), - &mut buf, - "", - 0, - ); + replacer_root.with_const_ptr::(|replacer| { + dispatch_pointer_with_replacer(ptr, replaced_root, replacer, &mut buf, "", 0); + }); } let result = js_string_from_bytes(buf.as_ptr(), buf.len() as u32); @@ -1766,12 +1763,14 @@ pub unsafe extern "C" fn js_json_stringify_full( let empty_str = js_string_from_bytes(b"".as_ptr(), 0); let empty_key_root = root_scope.root_nanbox_f64(nanbox_string_f64(empty_str)); let value_after_to_json = apply_to_json_keyed(value, empty_key_root.get_nanbox_f64()); - let replaced_root = call_replacer( - replacer_root.get_raw_const_ptr::(), - empty_key_root.get_nanbox_f64(), - value_after_to_json, - root_holder(value_after_to_json), - ); + let replaced_root = replacer_root.with_const_ptr::(|replacer| { + call_replacer( + replacer, + empty_key_root.get_nanbox_f64(), + value_after_to_json, + root_holder(value_after_to_json), + ) + }); let replaced_bits = replaced_root.to_bits(); if replaced_bits == TAG_UNDEFINED { STRINGIFY_STACK.with(|s| s.borrow_mut().clear()); @@ -1789,14 +1788,16 @@ pub unsafe extern "C" fn js_json_stringify_full( // (object vs array) so the indent threads through nested structures. if !write_replaced_scalar(&mut buf, replaced_root) { let ptr = extract_pointer(replaced_bits).unwrap(); - dispatch_pointer_with_replacer( - ptr, - replaced_root, - replacer_root.get_raw_const_ptr::(), - &mut buf, - &indent_str, - 0, - ); + replacer_root.with_const_ptr::(|replacer| { + dispatch_pointer_with_replacer( + ptr, + replaced_root, + replacer, + &mut buf, + &indent_str, + 0, + ); + }); } } else { // No replacer. Pre-resolve the ROOT value's own `toJSON` here (same diff --git a/scripts/shape_descriptor_census_baseline.json b/scripts/shape_descriptor_census_baseline.json index 47142ca196..680aff574a 100644 --- a/scripts/shape_descriptor_census_baseline.json +++ b/scripts/shape_descriptor_census_baseline.json @@ -10,8 +10,8 @@ "crates/perry-codegen/src/expr/property_get/helpers.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 3, "crates/perry-codegen/src/expr/property_get/helpers.rs|let header_skip = crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, "crates/perry-codegen/src/expr/property_set.rs|crate::target_layout::object_header_size_bytes(": 3, - "crates/perry-codegen/src/expr/property_set.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple)": 1, - "crates/perry-codegen/src/expr/property_set.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 2, + "crates/perry-codegen/src/expr/property_set/sloppy_class_field.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple)": 1, + "crates/perry-codegen/src/expr/property_set/sloppy_class_field.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 2, "crates/perry-codegen/src/expr/proxy_reflect.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 3, "crates/perry-codegen/src/lower_call/new.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, "crates/perry-codegen/src/lower_call/new_alloc.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple);": 1,