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. 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()));