From 24e953b01dad15e3a8620c733df5e655917ca60c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 11:01:37 +0000 Subject: [PATCH 1/2] fix(codegen): own method beats a Date/Array builtin on unproven receivers (#11493) `const d: any = new Date(0); d.getTime = () => 42; d.getTime()` printed 0. #10943 added the own-override test for receivers whose kind is proven, but #10476's receiver-kind guard, which serves UNPROVEN receivers, still went straight to the builtin once the runtime check said "this is a Date" (or a plain array). A matching kind says nothing about own properties. - builtin_kind_guard: a heap receiver that passes the Date / plain-array check (including the Date arm of the toLocaleString guard) now takes the #10943 own-override test before the builtin, and falls through to the universal dispatcher when it may own the name. Numbers and Symbols are primitives and skip it. The test may allocate, so the receiver and every argument are rooted across it and re-read in each arm. - own_override_guard: the proven-receiver name list now comes from the kind guard's own Date table, so a proven Date's getUTCHours, setTime, toUTCString, ... get the diamond too, and it covers the array names the kind guard handles (toReversed, toSorted, toSpliced, reduceRight, copyWithin). - folded_builtin_override: add the zero-argument Date folds that were missing from the table (getTimezoneOffset, toJSON, toDateString, toTimeString, toLocaleDateString, toLocaleTimeString, toLocaleString). DateToUTCString stays out: HIR folds toUTCString and toGMTString into it, so the node does not know which name to test. test_gap_11493_own_method_beats_date_builtin.ts: 71 of 89 lines differ from node on main, 0 with this change (TZ=UTC, America/New_York, Asia/Kolkata). --- .../src/expr/folded_builtin_override.rs | 83 ++++++ .../property_get/builtin_kind_guard.rs | 105 +++++++- .../property_get/builtin_kind_guard_tests.rs | 191 +++++++++++++ .../property_get/own_override_guard.rs | 28 +- ...gap_11493_own_method_beats_date_builtin.ts | 252 ++++++++++++++++++ 5 files changed, 635 insertions(+), 24 deletions(-) create mode 100644 test-files/test_gap_11493_own_method_beats_date_builtin.ts diff --git a/crates/perry-codegen/src/expr/folded_builtin_override.rs b/crates/perry-codegen/src/expr/folded_builtin_override.rs index f7192231c0..ffa8177de0 100644 --- a/crates/perry-codegen/src/expr/folded_builtin_override.rs +++ b/crates/perry-codegen/src/expr/folded_builtin_override.rs @@ -267,6 +267,52 @@ fn folded_call(expr: &Expr) -> Option> { method: "getUTCMilliseconds", args: Vec::new(), }, + // The remaining zero-argument Date folds (#11493). They reach their + // formatters exactly as the getters above do, and were simply missing + // from this table: `d.toDateString = () => 1` ran the builtin. + // + // `Expr::DateToUTCString` is deliberately ABSENT. HIR folds both + // `toUTCString` and `toGMTString` into it, so the node does not record + // which name the source used, and the guard needs that name twice: to + // test which property the receiver owns, and to dispatch it. Guessing + // `toUTCString` would make `d.toGMTString()` call an own `toUTCString`, + // which is wrong in the other direction. Guarding it needs the fold to + // keep its spelling. + Expr::DateGetTimezoneOffset(date) => FoldedCall { + receiver: Receiver::Expr(date), + method: "getTimezoneOffset", + args: Vec::new(), + }, + Expr::DateToJSON(date) => FoldedCall { + receiver: Receiver::Expr(date), + method: "toJSON", + args: Vec::new(), + }, + Expr::DateToDateString(date) => FoldedCall { + receiver: Receiver::Expr(date), + method: "toDateString", + args: Vec::new(), + }, + Expr::DateToTimeString(date) => FoldedCall { + receiver: Receiver::Expr(date), + method: "toTimeString", + args: Vec::new(), + }, + Expr::DateToLocaleDateString(date) => FoldedCall { + receiver: Receiver::Expr(date), + method: "toLocaleDateString", + args: Vec::new(), + }, + Expr::DateToLocaleTimeString(date) => FoldedCall { + receiver: Receiver::Expr(date), + method: "toLocaleTimeString", + args: Vec::new(), + }, + Expr::DateToLocaleString(date) => FoldedCall { + receiver: Receiver::Expr(date), + method: "toLocaleString", + args: Vec::new(), + }, Expr::DateSetFullYear { date, args } => FoldedCall { receiver: Receiver::Expr(date), method: "setFullYear", @@ -385,6 +431,14 @@ pub(crate) fn try_lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result 42; d.getTime()` passed the +//! Date check and returned `0`. So a heap receiver of the right kind asks the +//! #10943 own-override test before it takes the builtin, and one that may own +//! the name takes the dispatcher instead. A number or a Symbol is a primitive +//! and owns nothing, so those arms go straight to the builtin as before. use anyhow::Result; use perry_hir::Expr; +use super::own_override_guard::emit_own_override_branch; use crate::expr::{lower_expr, nanbox_string_inline, unbox_to_i64, FnCtx}; use crate::rooting::{any_operand_may_collect, open_rooted_group, Repr}; use crate::type_analysis::{ @@ -53,6 +62,16 @@ enum ReceiverKind { Array, } +impl ReceiverKind { + /// Can a receiver that PASSES this kind's check own a property that + /// shadows the builtin? A Date and an array are objects and can (#11493); + /// a number is a primitive and cannot. `LocaleValue` admits a Date too, so + /// it answers yes, and its primitive and Symbol arms skip the test. + fn can_own_properties(self) -> bool { + !matches!(self, ReceiverKind::Number) + } +} + /// The runtime entry point the HIR `Expr::Date*` arms lower each name to. /// `toLocale*String` only has a fast path without locale/options arguments; /// with arguments they belong to the generic dispatcher's Intl thunks. @@ -116,6 +135,16 @@ fn date_builtin(property: &str, argc: usize) -> Option { }) } +/// Can this lowering call a Date builtin DIRECTLY for `property` on a proven +/// Date? Every such name is one an own property can shadow, so the +/// own-override guard asks this rather than keeping a second list that has to +/// be kept in step with [`date_builtin`] (#11493). Asked with no arguments, +/// which admits the `toLocale*String` spellings too: over-approximating only +/// costs a diamond whose two arms reach the same dispatcher. +pub(super) fn is_direct_date_builtin_name(property: &str) -> bool { + date_builtin(property, 0).is_some() +} + /// `StringHeader* fn(double number, double arg)` for each Number method. fn number_builtin(property: &str) -> Option<&'static str> { match property { @@ -292,6 +321,11 @@ fn emit_date_builtin( /// The receiver is rooted across argument evaluation, and both are re-read /// below it. The group is released in the merge block, below both consuming /// calls (`open_rooted_group`'s diamond case). +/// +/// A kind whose receiver can own properties also asks the own-override +/// predicate below the kind check (#11493). That predicate may allocate, so +/// for such a kind every operand is rooted, not just those with a collecting +/// operand after them, and each arm re-reads the operands below the test. fn guarded_call( ctx: &mut FnCtx<'_>, object: &Expr, @@ -301,12 +335,13 @@ fn guarded_call( guard: Option, builtin: impl FnOnce(&mut FnCtx<'_>, &str, Option<&str>, &[String]) -> String, ) -> Result { + let own_check = guard.is_some_and(ReceiverKind::can_own_properties); let mut group = open_rooted_group(args.len() + 1); let recv_box = lower_expr(ctx, object)?; - let recv_collects = any_operand_may_collect(ctx, args.iter()); + let recv_collects = own_check || any_operand_may_collect(ctx, args.iter()); let rooted_recv = group.adopt_emitted(ctx, Repr::Boxed, &recv_box, recv_collects); for (i, arg) in args.iter().enumerate() { - let collects = any_operand_may_collect(ctx, args[i + 1..].iter()); + let collects = own_check || any_operand_may_collect(ctx, args[i + 1..].iter()); group.lower(ctx, arg, collects)?; } let recv = group.reread_emitted(ctx, rooted_recv); @@ -321,23 +356,58 @@ fn guarded_call( let builtin_idx = ctx.new_block("kindguard.builtin"); let generic_idx = ctx.new_block("kindguard.generic"); let merge_idx = ctx.new_block("kindguard.merge"); + let own_idx = own_check.then(|| ctx.new_block("kindguard.own")); let builtin_label = ctx.block_label(builtin_idx); let generic_label = ctx.block_label(generic_idx); let merge_label = ctx.block_label(merge_idx); - let time = emit_receiver_kind_branch(ctx, kind, &recv, &builtin_label, &generic_label); + let own_label = own_idx.map(|idx| ctx.block_label(idx)); + // A heap receiver of the right kind is still an object that may own the + // method, so it takes the own-override test before the builtin. + let heap_label = own_label.as_deref().unwrap_or(&builtin_label); + let time = + emit_receiver_kind_branch(ctx, kind, &recv, heap_label, &builtin_label, &generic_label); + + if let Some(own_idx) = own_idx { + ctx.current_block = own_idx; + let recv = group.reread_emitted(ctx, rooted_recv); + emit_own_override_branch( + ctx, + property, + &recv, + kind == ReceiverKind::Array, + &generic_label, + &builtin_label, + ); + } + + // Below the own-override test the operands come from their roots again: + // the test may have collected since the reads above it. + let reread = |ctx: &mut FnCtx<'_>| -> Result<(String, Vec)> { + if own_check { + Ok(( + group.reread_emitted(ctx, rooted_recv), + group.reread_all(ctx)?, + )) + } else { + Ok((recv.clone(), arg_vals.clone())) + } + }; ctx.current_block = builtin_idx; - let builtin_value = builtin(ctx, &recv, time.as_deref(), &arg_vals); + let (builtin_recv, builtin_args) = reread(ctx)?; + // `time` is a Number, not a heap reference, so it survives the test. + let builtin_value = builtin(ctx, &builtin_recv, time.as_deref(), &builtin_args); let builtin_end = ctx.block().label.clone(); ctx.block().br(&merge_label); ctx.current_block = generic_idx; + let (generic_recv, generic_args) = reread(ctx)?; let generic_value = super::super::console_promise::emit_native_method_str_dispatch( ctx, property, call_byte_offset, - &recv, - &arg_vals, + &generic_recv, + &generic_args, ); let generic_end = ctx.block().label.clone(); ctx.block().br(&merge_label); @@ -354,13 +424,18 @@ fn guarded_call( Ok(value) } -/// Branch to `builtin_label` when the NaN-boxed `recv` has the kind the -/// builtin requires, else to `generic_label`. No check allocates or runs user -/// code. A Date check returns the Date's time value. +/// Branch on whether the NaN-boxed `recv` has the kind the builtin requires: +/// a PRIMITIVE of that kind goes to `builtin_label`, a HEAP receiver of that +/// kind (a Date, a plain array) to `heap_label` — which is the own-override +/// test when the caller asks one, because an object of the right kind can +/// still own the method (#11493) — and anything else to `generic_label`. No +/// check allocates or runs user code. A Date check returns the Date's time +/// value. fn emit_receiver_kind_branch( ctx: &mut FnCtx<'_>, kind: ReceiverKind, recv: &str, + heap_label: &str, builtin_label: &str, generic_label: &str, ) -> Option { @@ -377,7 +452,7 @@ fn emit_receiver_kind_branch( let time_bits = blk.bitcast_double_to_i64(&time); let recv_bits = blk.bitcast_double_to_i64(recv); let is_date = blk.icmp_ne(I64, &time_bits, &recv_bits); - blk.cond_br(&is_date, builtin_label, generic_label); + blk.cond_br(&is_date, heap_label, generic_label); Some(time) } // `toLocaleString()` without arguments is `Object.prototype`'s on every @@ -391,7 +466,7 @@ fn emit_receiver_kind_branch( let heap_idx = ctx.new_block("kindguard.locale_heap"); let symbol_idx = ctx.new_block("kindguard.locale_symbol"); let heap_or_nullish_label = ctx.block_label(heap_or_nullish_idx); - let heap_label = ctx.block_label(heap_idx); + let locale_heap_label = ctx.block_label(heap_idx); let symbol_label = ctx.block_label(symbol_idx); let blk = ctx.block(); @@ -405,14 +480,16 @@ fn emit_receiver_kind_branch( blk.cond_br(¬_primitive, &heap_or_nullish_label, builtin_label); ctx.current_block = heap_or_nullish_idx; - ctx.block().cond_br(&is_pointer, &heap_label, generic_label); + ctx.block() + .cond_br(&is_pointer, &locale_heap_label, generic_label); ctx.current_block = heap_idx; let blk = ctx.block(); let time = blk.call(DOUBLE, "js_date_get_time", &[(DOUBLE, recv)]); let time_bits = blk.bitcast_double_to_i64(&time); let is_date = blk.icmp_ne(I64, &time_bits, &bits); - blk.cond_br(&is_date, builtin_label, &symbol_label); + // A Date is an object; a Symbol is a primitive and owns nothing. + blk.cond_br(&is_date, heap_label, &symbol_label); ctx.current_block = symbol_idx; let blk = ctx.block(); @@ -473,7 +550,7 @@ fn emit_receiver_kind_branch( let forwarded = blk.and(I8, &flags, GC_FLAG_FORWARDED_I8); let not_forwarded = blk.icmp_eq(I8, &forwarded, "0"); let plain_array = blk.and(I1, &is_array, ¬_forwarded); - blk.cond_br(&plain_array, builtin_label, generic_label); + blk.cond_br(&plain_array, heap_label, generic_label); None } } diff --git a/crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard_tests.rs b/crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard_tests.rs index 61ca6e98e7..d1908f5978 100644 --- a/crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard_tests.rs +++ b/crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard_tests.rs @@ -298,3 +298,194 @@ fn zero_argument_search_methods_compile_on_any_receiver_and_on_a_string() { "an omitted searchString is `undefined`, not a compile error:\n{ir}" ); } + +// --------------------------------------------------------------------------- +// #11493: a receiver that PASSES the kind check can still own the method. +// --------------------------------------------------------------------------- + +const OWN_TEST: &str = "call i32 @js_receiver_may_own_named_method("; +const OWN_FLAG: &str = "@PERRY_OWN_NAMED_PROP_INSTALLED"; +const MAKE: &str = "perry_fn_kind_guard_ts__make"; + +/// Every register a call to `callee` assigns, in program order. +fn call_results(ir: &str, callee: &str) -> Vec { + let marker = format!(" = call double @{callee}("); + ir.lines() + .filter(|line| line.contains(&marker)) + .filter_map(|line| line.trim_start().split(" = ").next()) + .map(str::to_string) + .collect() +} + +#[test] +fn unproven_date_receiver_asks_the_own_override_test_before_the_builtin() { + // `const d: any = new Date(0); d.getTime = () => 42; d.getTime()` passed + // the Date check and returned 0: nothing between that check and the + // builtin asked whether `d` owns `getTime`. + let ir = main_ir( + "kind_guard_own_get_time.ts", + vec![method_call(any_value(), "getTime", Vec::new())], + ); + assert!( + ir.contains(OWN_FLAG) && ir.contains(OWN_TEST), + "a Date receiver must take the own-override test before the builtin:\n{ir}" + ); + assert_eq!( + ir.matches(DISPATCH).count(), + 1, + "an own getTime and a non-Date receiver share the one dispatch arm:\n{ir}" + ); + assert_eq!( + ir.matches(GET_TIME).count(), + 1, + "the kind check still reads the time value the builtin arm returns:\n{ir}" + ); + // The predicate may allocate. The receiver has to be in a rooted slot + // across it, and both the predicate and the dispatcher read it back. + let recv = call_results(&ir, MAKE).remove(0); + crate::testing::temp_slots::assert_rooted_across( + &ir, + &recv, + "js_receiver_may_own_named_method", + "the own-override test", + ); + crate::testing::temp_slots::assert_rooted_across( + &ir, + &recv, + "js_typed_feedback_native_call_method_by_id", + "the dispatch arm below the own-override test", + ); +} + +#[test] +fn unproven_date_setter_roots_its_argument_across_the_own_override_test() { + // A lone argument has no collecting operand after it, so before #11493 it + // stayed an SSA register. The own-override test is a call that may + // collect, so it is rooted now, and the builtin reads it from the root. + let ir = main_ir( + "kind_guard_own_set_utc_hours.ts", + vec![method_call(any_value(), "setUTCHours", vec![any_value()])], + ); + let results = call_results(&ir, MAKE); + assert_eq!(results.len(), 2, "receiver and argument:\n{ir}"); + assert!( + crate::testing::temp_slots::temp_root_slot_holding(&ir, &results[1]).is_some(), + "the argument must be rooted across the own-override test:\n{ir}" + ); + assert!( + ir.contains(OWN_TEST) && ir.contains("call double @js_date_apply_setter("), + "the setter stays behind the Date check and the own-override test:\n{ir}" + ); + crate::testing::temp_slots::assert_rooted_across( + &ir, + &results[0], + "js_date_apply_setter", + "the setter below the own-override test", + ); +} + +#[test] +fn unproven_array_receiver_asks_the_own_override_test_from_its_header() { + let ir = main_ir( + "kind_guard_own_to_sorted.ts", + vec![method_call(any_value(), "toSorted", vec![Expr::Undefined])], + ); + assert!( + ir.contains(OWN_TEST) && ir.contains("load i16"), + "a plain array tests its own named-property header bits before the \ + builtin, and asks the predicate only when they are set:\n{ir}" + ); + assert!( + !ir.contains(OWN_FLAG), + "an array answers from its own header, not the global install flag:\n{ir}" + ); +} + +#[test] +fn unproven_to_locale_string_asks_the_own_override_test_for_a_date_only() { + let ir = main_ir( + "kind_guard_own_to_locale_string.ts", + vec![method_call(any_value(), "toLocaleString", Vec::new())], + ); + assert!( + ir.contains(OWN_TEST), + "a Date receiver of toLocaleString() may own it:\n{ir}" + ); + assert_eq!( + ir.matches(OWN_TEST).count(), + 1, + "only the Date arm asks; primitives and Symbols own nothing:\n{ir}" + ); +} + +#[test] +fn unproven_number_method_skips_the_own_override_test() { + // A number is a primitive: nothing can shadow its builtin on the receiver, + // so its guard stays the inline tag test and nothing is rooted for it. + let ir = main_ir( + "kind_guard_own_to_fixed.ts", + vec![method_call(any_value(), "toFixed", vec![Expr::Number(2.0)])], + ); + assert!( + !ir.contains(OWN_FLAG) && !ir.contains(OWN_TEST), + "a Number receiver needs no own-override test:\n{ir}" + ); +} + +#[test] +fn proven_date_names_outside_the_old_list_are_guarded() { + // A proven Date reaches the chain's direct builtin for every name + // `date_builtin` knows, so every one of them needs the #10943 diamond. + // `setTime` and `getUTCHours` were missing from the hand-kept list. + for name in ["setTime", "getUTCHours", "toUTCString", "toLocaleString"] { + assert!( + super::is_direct_date_builtin_name(name), + "{name} lowers to a direct Date builtin" + ); + } + let ir = main_ir( + "kind_guard_proven_set_time.ts", + vec![ + method_call( + Expr::DateNew(Vec::new()), + "setTime", + vec![Expr::Number(1.0)], + ), + method_call(Expr::DateNew(Vec::new()), "getUTCHours", Vec::new()), + ], + ); + assert_eq!( + ir.matches(OWN_TEST).count(), + 2, + "each proven Date call pays one own-override test:\n{ir}" + ); +} + +#[test] +fn folded_date_formatters_are_guarded_and_a_numeric_to_locale_string_is_not() { + let ir = main_ir( + "kind_guard_folded_formatters.ts", + vec![ + Stmt::Expr(Expr::DateToDateString(Box::new(Expr::DateNew(Vec::new())))), + Stmt::Expr(Expr::DateGetTimezoneOffset(Box::new(Expr::DateNew( + Vec::new(), + )))), + ], + ); + assert_eq!( + ir.matches(OWN_TEST).count(), + 2, + "each folded Date formatter pays one own-override test:\n{ir}" + ); + // `(12345).toLocaleString()` shares the Date node; a number owns nothing. + let ir = main_ir( + "kind_guard_folded_number_locale.ts", + vec![Stmt::Expr(Expr::DateToLocaleString(Box::new( + Expr::Number(12345.0), + )))], + ); + assert!( + !ir.contains(OWN_FLAG) && !ir.contains(OWN_TEST), + "a numeric toLocaleString keeps the plain fold:\n{ir}" + ); +} diff --git a/crates/perry-codegen/src/lower_call/property_get/own_override_guard.rs b/crates/perry-codegen/src/lower_call/property_get/own_override_guard.rs index 4efaa1fad7..3b8c0ba05a 100644 --- a/crates/perry-codegen/src/lower_call/property_get/own_override_guard.rs +++ b/crates/perry-codegen/src/lower_call/property_get/own_override_guard.rs @@ -62,10 +62,16 @@ use crate::types::{DOUBLE, I16, I32, I64, I8, PTR}; /// both arms of the diamond reach the same dispatcher — but staying close /// keeps the emitted diamonds where the bug is. fn shadowable_builtin_name(property: &str) -> bool { - matches!( - property, - // Map / Set - "get" | "set" | "has" | "delete" | "add" | "clear" | "entries" | "keys" | "values" + // Date: every name the chain can lower to a direct Date builtin, from the + // table that lowering itself uses. The hand-kept list below stopped at + // `getTime`/`toISOString` and a few setters, so a proven Date's + // `getUTCHours`, `setTime` or `toUTCString` reached the direct call with no + // guard at all (#11493). + super::builtin_kind_guard::is_direct_date_builtin_name(property) + || matches!( + property, + // Map / Set + "get" | "set" | "has" | "delete" | "add" | "clear" | "entries" | "keys" | "values" | "forEach" // Array. `push` is ABSENT, and needs no diamond: a proven array's // push is folded by HIR into `Expr::ArrayPush`, whose slow arms honour @@ -76,14 +82,16 @@ fn shadowable_builtin_name(property: &str) -> bool { | "lastIndexOf" | "includes" | "join" | "concat" | "reverse" | "sort" | "fill" | "find" | "findIndex" | "filter" | "map" | "some" | "every" | "reduce" | "flat" | "flatMap" | "at" - // Date - | "getTime" | "getHours" | "getMinutes" | "getSeconds" | "getMilliseconds" - | "getDate" | "getDay" | "getMonth" | "getFullYear" | "setHours" | "setMinutes" - | "setSeconds" | "setDate" | "setMonth" | "setFullYear" | "toISOString" - | "toJSON" | "getTimezoneOffset" | "valueOf" + // The rest of the names the receiver-kind guard lowers directly on an + // UNPROVEN array (`is_array_method_on_values`), which now tests an own + // property first (#11493). A proven array has to test it too, or the + // same call answers differently depending on what the compiler proved. + | "toReversed" | "toSorted" | "toSpliced" | "reduceRight" | "copyWithin" + // Date names with no direct builtin in the table above + | "toJSON" | "valueOf" // Number | "toFixed" | "toPrecision" | "toExponential" - ) + ) } /// Is the receiver's KIND proven to be one whose builtins are lowered diff --git a/test-files/test_gap_11493_own_method_beats_date_builtin.ts b/test-files/test_gap_11493_own_method_beats_date_builtin.ts new file mode 100644 index 0000000000..b608a48cc1 --- /dev/null +++ b/test-files/test_gap_11493_own_method_beats_date_builtin.ts @@ -0,0 +1,252 @@ +// #11493: an own method beats a Date (or Array) builtin whatever the compiler +// proved about the receiver. +// +// #10943 made an own property beat the builtin on a receiver whose KIND is +// proven. The other half is #10476's receiver-kind guard, which serves an +// UNPROVEN receiver: it checks at runtime that the value is a Date (or a plain +// array) and then called the builtin directly. A matching kind says nothing +// about own properties, so `const d: any = new Date(0); d.getTime = () => 42; +// d.getTime()` printed 0. The same held for every Date method that guard +// lowers, for its `toLocaleString()` arm, and for its array methods. +// +// Also covered: the proven-Date folds that were missing from the #10943 table +// (`getTimezoneOffset`, `toJSON`, `toDateString`, `toTimeString`, the three +// `toLocale*String`s), proven-Date names the codegen chain lowers directly +// (`getUTCHours`, `setTime`, reached here through a class field), and the +// proven-array names the kind guard handles. +// +// Not covered: `toUTCString`/`toGMTString` on a Date that HIR proves. HIR +// folds both spellings into one node, so the guard cannot tell which name to +// test. (The unproven rows below do cover both.) +// +// Every native row avoids local-time and locale output, so this file is +// byte-identical to node in any TZ. + +function t(label: string, f: () => unknown) { + try { + console.log(label + "=" + String(f())); + } catch (e: any) { + console.log(label + "=throw:" + e.constructor.name); + } +} + +// --- the issue: an unproven Date receiver ------------------------------------ +const d1: any = new Date(0); +d1.getTime = () => 42; +t("any-annotated local", () => d1.getTime()); + +function viaParam(d) { return d.getTime(); } +const d2 = new Date(0); +(d2 as any).getTime = () => 43; +t("untyped parameter", () => viaParam(d2)); + +const holder: any = { d: new Date(0) }; +holder.d.getTime = () => 44; +t("property read", () => holder.d.getTime()); + +const list: any[] = [new Date(0)]; +list[0].getTime = () => 45; +t("array element", () => list[0].getTime()); + +let made = 0; +function make(): any { made++; const d = new Date(0); (d as any).getTime = () => 46; return d; } +t("call result, evaluated once", () => make().getTime() + "/" + made); + +// --- every Date method the kind guard lowers, on an unproven receiver -------- +const u: any = new Date(0); +u.getTimezoneOffset = () => "own-getTimezoneOffset"; +u.getFullYear = () => "own-getFullYear"; +u.getMonth = () => "own-getMonth"; +u.getDate = () => "own-getDate"; +u.getDay = () => "own-getDay"; +u.getHours = () => "own-getHours"; +u.getMinutes = () => "own-getMinutes"; +u.getSeconds = () => "own-getSeconds"; +u.getMilliseconds = () => "own-getMilliseconds"; +u.getUTCFullYear = () => "own-getUTCFullYear"; +u.getUTCMonth = () => "own-getUTCMonth"; +u.getUTCDate = () => "own-getUTCDate"; +u.getUTCDay = () => "own-getUTCDay"; +u.getUTCHours = () => "own-getUTCHours"; +u.getUTCMinutes = () => "own-getUTCMinutes"; +u.getUTCSeconds = () => "own-getUTCSeconds"; +u.getUTCMilliseconds = () => "own-getUTCMilliseconds"; +u.toISOString = () => "own-toISOString"; +u.toDateString = () => "own-toDateString"; +u.toTimeString = () => "own-toTimeString"; +u.toUTCString = () => "own-toUTCString"; +u.toGMTString = () => "own-toGMTString"; +u.toLocaleDateString = () => "own-toLocaleDateString"; +u.toLocaleTimeString = () => "own-toLocaleTimeString"; +u.setFullYear = (x) => "own-setFullYear:" + x; +u.setMonth = (x) => "own-setMonth:" + x; +u.setDate = (x) => "own-setDate:" + x; +u.setHours = (x) => "own-setHours:" + x; +u.setMinutes = (x) => "own-setMinutes:" + x; +u.setSeconds = (x) => "own-setSeconds:" + x; +u.setMilliseconds = (x) => "own-setMilliseconds:" + x; +u.setTime = (x) => "own-setTime:" + x; +u.setUTCFullYear = (x) => "own-setUTCFullYear:" + x; +u.setUTCMonth = (x) => "own-setUTCMonth:" + x; +u.setUTCDate = (x) => "own-setUTCDate:" + x; +u.setUTCHours = (x) => "own-setUTCHours:" + x; +u.setUTCMinutes = (x) => "own-setUTCMinutes:" + x; +u.setUTCSeconds = (x) => "own-setUTCSeconds:" + x; +u.setUTCMilliseconds = (x) => "own-setUTCMilliseconds:" + x; +t("unproven getTimezoneOffset", () => u.getTimezoneOffset()); +t("unproven getFullYear", () => u.getFullYear()); +t("unproven getMonth", () => u.getMonth()); +t("unproven getDate", () => u.getDate()); +t("unproven getDay", () => u.getDay()); +t("unproven getHours", () => u.getHours()); +t("unproven getMinutes", () => u.getMinutes()); +t("unproven getSeconds", () => u.getSeconds()); +t("unproven getMilliseconds", () => u.getMilliseconds()); +t("unproven getUTCFullYear", () => u.getUTCFullYear()); +t("unproven getUTCMonth", () => u.getUTCMonth()); +t("unproven getUTCDate", () => u.getUTCDate()); +t("unproven getUTCDay", () => u.getUTCDay()); +t("unproven getUTCHours", () => u.getUTCHours()); +t("unproven getUTCMinutes", () => u.getUTCMinutes()); +t("unproven getUTCSeconds", () => u.getUTCSeconds()); +t("unproven getUTCMilliseconds", () => u.getUTCMilliseconds()); +t("unproven toISOString", () => u.toISOString()); +t("unproven toDateString", () => u.toDateString()); +t("unproven toTimeString", () => u.toTimeString()); +t("unproven toUTCString", () => u.toUTCString()); +t("unproven toGMTString", () => u.toGMTString()); +t("unproven toLocaleDateString", () => u.toLocaleDateString()); +t("unproven toLocaleTimeString", () => u.toLocaleTimeString()); +t("unproven setFullYear", () => u.setFullYear(1)); +t("unproven setMonth", () => u.setMonth(1)); +t("unproven setDate", () => u.setDate(1)); +t("unproven setHours", () => u.setHours(1, 2)); +t("unproven setMinutes", () => u.setMinutes(1)); +t("unproven setSeconds", () => u.setSeconds(1)); +t("unproven setMilliseconds", () => u.setMilliseconds(1)); +t("unproven setTime", () => u.setTime(1)); +t("unproven setUTCFullYear", () => u.setUTCFullYear(1)); +t("unproven setUTCMonth", () => u.setUTCMonth(1)); +t("unproven setUTCDate", () => u.setUTCDate(1)); +t("unproven setUTCHours", () => u.setUTCHours(1)); +t("unproven setUTCMinutes", () => u.setUTCMinutes(1)); +t("unproven setUTCSeconds", () => u.setUTCSeconds(1)); +t("unproven setUTCMilliseconds", () => u.setUTCMilliseconds(1)); + +// --- `toLocaleString()`: the kind guard's own arm ---------------------------- +function loc(x) { return x.toLocaleString(); } +const dl = new Date(0); +(dl as any).toLocaleString = () => "own-toLocaleString"; +t("toLocaleString own on a Date", () => loc(dl)); +t("toLocaleString on a string", () => loc("abc")); +t("toLocaleString on a Symbol", () => loc(Symbol("s"))); + +// --- what the own method sees, and how it is read ----------------------------- +const th: any = new Date(0); +th.setHours = function (h, m) { return (this === th) + ":" + h + ":" + m; }; +t("own method this and arguments", () => th.setHours(1, 2)); + +const ac: any = new Date(0); +let gets = 0; +Object.defineProperty(ac, "getTime", { get() { gets++; return () => "accessor"; } }); +t("own accessor runs once", () => ac.getTime() + "/" + gets); + +const nc: any = new Date(0); +nc.getTime = 5; +t("own non-callable throws", () => nc.getTime()); + +const del: any = new Date(7); +del.getTime = () => "own"; +t("before delete", () => del.getTime()); +delete del.getTime; +t("after delete", () => del.getTime()); + +const bor: any = new Date(9); +bor.getTime = Date.prototype.getTime; +t("borrowed builtin", () => bor.getTime()); + +// installed partway through a hot call site +function hot(d) { return d.getTime(); } +const hd: any = new Date(5); +let seen = ""; +for (let i = 0; i < 4; i++) { + if (i === 2) hd.getTime = () => 100; + seen += hot(hd) + ","; +} +t("installed mid-loop", () => seen); + +// --- nothing shadowed: the builtin still runs -------------------------------- +const plain: any = new Date(86400000 + 3600000 * 5 + 60000 * 6 + 7008); +t("native getTime", () => plain.getTime()); +t("native getUTCFullYear", () => plain.getUTCFullYear()); +t("native getUTCHours", () => plain.getUTCHours()); +t("native toISOString", () => plain.toISOString()); +t("native setUTCMinutes", () => plain.setUTCMinutes(30)); +t("native setTime", () => plain.setTime(0) + "/" + plain.getTime()); +t("native call result", () => new Date(3).getTime()); + +// --- proven Dates: the folds that were missing from the #10943 table --------- +const p = new Date(0); +(p as any).getTimezoneOffset = () => "own-getTimezoneOffset"; +(p as any).toJSON = () => "own-toJSON"; +(p as any).toDateString = () => "own-toDateString"; +(p as any).toTimeString = () => "own-toTimeString"; +(p as any).toLocaleDateString = () => "own-toLocaleDateString"; +(p as any).toLocaleTimeString = () => "own-toLocaleTimeString"; +(p as any).toLocaleString = () => "own-toLocaleString"; +t("proven getTimezoneOffset", () => p.getTimezoneOffset()); +t("proven toJSON", () => p.toJSON()); +t("proven toDateString", () => p.toDateString()); +t("proven toTimeString", () => p.toTimeString()); +t("proven toLocaleDateString", () => p.toLocaleDateString()); +t("proven toLocaleTimeString", () => p.toLocaleTimeString()); +t("proven toLocaleString", () => p.toLocaleString()); +const pn = new Date(0); +t("proven native toJSON", () => pn.toJSON()); +t("proven native getTimezoneOffset is a number", () => typeof pn.getTimezoneOffset()); +// the own value keeps its own type through the typed fold +const pt = new Date(0); +(pt as any).toDateString = () => 42; +t("own result keeps its type", () => typeof pt.toDateString() + "/" + (pt.toDateString() as any).length); + +// a proven Date the codegen chain lowers directly +class Holder { d: Date = new Date(0); } +const h = new Holder(); +(h.d as any).getUTCHours = () => "own-getUTCHours"; +(h.d as any).setTime = (x) => "own-setTime:" + x; +t("class field getUTCHours", () => h.d.getUTCHours()); +t("class field setTime", () => h.d.setTime(1)); +const h2 = new Holder(); +t("class field native", () => h2.d.getUTCHours() + "/" + h2.d.setTime(7)); + +// --- arrays: the same kind guard's plain-array arm --------------------------- +const ua: any = JSON.parse("[3,1,2]"); +ua.toReversed = () => "own-toReversed"; +ua.toSorted = () => "own-toSorted"; +ua.toSpliced = () => "own-toSpliced"; +ua.reduceRight = () => "own-reduceRight"; +ua.copyWithin = () => "own-copyWithin"; +ua.flat = () => "own-flat"; +t("unproven array toReversed", () => ua.toReversed()); +t("unproven array toSorted", () => ua.toSorted()); +t("unproven array toSpliced", () => ua.toSpliced(0)); +t("unproven array reduceRight", () => ua.reduceRight((x) => x)); +t("unproven array copyWithin", () => ua.copyWithin(0)); +t("unproven array flat", () => ua.flat()); +const ub: any = JSON.parse("[3,[1],2]"); +t("unproven array native toSorted", () => JSON.stringify(ub.toSorted())); +t("unproven array native flat", () => JSON.stringify(ub.flat())); +t("unproven array native reduceRight", () => ub.reduceRight((acc, x) => acc + "," + x)); + +const pa: any = [3, 1, 2]; +pa.toReversed = () => "own-toReversed"; +pa.toSorted = () => "own-toSorted"; +pa.toSpliced = () => "own-toSpliced"; +pa.reduceRight = () => "own-reduceRight"; +pa.copyWithin = () => "own-copyWithin"; +t("proven array toReversed", () => pa.toReversed()); +t("proven array toSorted", () => pa.toSorted()); +t("proven array toSpliced", () => pa.toSpliced(0)); +t("proven array reduceRight", () => pa.reduceRight((x) => x)); +t("proven array copyWithin", () => pa.copyWithin(0)); +t("proven array native", () => JSON.stringify([3, 1, 2].toSorted()) + JSON.stringify([1, 2].toReversed())); From f72ebea645b2bac8c77bfd91d58865b12cfa6528 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 11:08:29 +0000 Subject: [PATCH 2/2] docs: changelog fragment for #11529 --- .../11529-own-method-beats-date-builtin.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 changelog.d/11529-own-method-beats-date-builtin.md diff --git a/changelog.d/11529-own-method-beats-date-builtin.md b/changelog.d/11529-own-method-beats-date-builtin.md new file mode 100644 index 0000000000..39919a9f8e --- /dev/null +++ b/changelog.d/11529-own-method-beats-date-builtin.md @@ -0,0 +1,43 @@ +An own method now beats a Date or Array builtin on a receiver whose kind the +compiler did not prove, closing #11493. `const d: any = new Date(0); +d.getTime = () => 42; d.getTime()` printed `0`, where node prints `42`. + +#10943 added the own-override test for receivers whose KIND is proven. The +other half was #10476's receiver-kind guard, which serves UNPROVEN receivers +(an `any` local, an untyped parameter, a property read, a call result): it +checks at runtime that the value is a Date or a plain array and then called +the builtin directly. A matching kind says nothing about own properties, so +every Date method that guard lowers ignored an own replacement, as did its +`toLocaleString()` arm and its array methods (`toSorted`, `toReversed`, +`toSpliced`, `reduceRight`, `copyWithin`). A non-callable own `getTime` +did not throw either. + +`lower_call/property_get/builtin_kind_guard.rs` now sends a heap receiver that +passes the Date or plain-array check through `emit_own_override_branch` +before the builtin; one that may own the name takes the universal dispatcher. +Numbers and Symbols are primitives and skip it. The predicate may allocate, +so when it is emitted the receiver and every argument are rooted across it and +re-read in each arm. + +Closed on the proven path too: + +- `own_override_guard.rs` takes its Date names from the kind guard's own + `date_builtin` table instead of a hand-kept list that stopped short, so a + proven Date's `getUTCHours`, `setTime`, `toUTCString`, ... get the diamond; + it also covers the five array names above. +- `expr/folded_builtin_override.rs` gains the Date folds that were missing + from the #10943 table: `getTimezoneOffset`, `toJSON`, `toDateString`, + `toTimeString` and the three `toLocale*String`s. A numeric receiver of the + shared `DateToLocaleString` node keeps the plain fold. + +Not covered: `Expr::DateToUTCString`. HIR folds `toUTCString` and +`toGMTString` into that one node, so it does not know which name to test. + +Cost, callgrind, two unproven Date calls per iteration: 259 -> 280 +instructions with the global install flag clear; 259 -> 1891 with it armed, +which matches the 1853 a proven Date already pays under #10943. + +Validation: `test_gap_11493_own_method_beats_date_builtin.ts` differs from +node on 71 of 89 lines on main and on none with this change (three TZs); +`cargo test -p perry-codegen` green; 0 regressions over 133 related existing +tests; `gc_root_dominance_check.py --moving-only` 0 violations.