Skip to content

Commit 5df641c

Browse files
proggeramlugRalph
andauthored
fix(intl): #5835 — new.target requirement + Reflect.construct prototype for Intl service ctors (#5850)
Intl.ListFormat/RelativeTimeFormat/Segmenter/PluralRules/Locale must throw a TypeError when NewTarget is undefined (ECMA-402 step 1) instead of silently constructing like the legacy factory-pattern NumberFormat/DateTimeFormat/ Collator. And OrdinaryCreateFromConstructor means the shared make_instance() tail (plus Locale's own constructor) must install new.target's own prototype when it's an object, not always the invoked closure's default prototype — otherwise Reflect.construct(Intl.X, args, Custom) silently drops the custom prototype. Fixes 7 of the 108 test262 intl402 cases from #5835 (newtarget-undefined x5, ctor-custom-prototype x2); zero regressions across the scoped Collator/ DisplayNames/ListFormat/PluralRules/RelativeTimeFormat/Segmenter/Locale subset. `class extends Intl.X { super(...) }` subclassing remains open — a separate, deeper fix in the native-parent super() dispatch path (discards make_instance's returned object), out of scope here per the issue's "pick a coherent subcluster" guidance. Co-authored-by: Ralph <ralph@skelpo.com>
1 parent a046048 commit 5df641c

4 files changed

Lines changed: 100 additions & 2 deletions

File tree

crates/perry-runtime/src/intl.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use crate::StringHeader;
1717
#[cfg(feature = "intl-segmenter")]
1818
use unicode_segmentation::UnicodeSegmentation;
1919

20+
mod ctor_guard;
21+
use ctor_guard::{constructor_target_prototype, require_new_target};
2022
mod display_names;
2123
mod duration_format;
2224
mod locale;
@@ -1600,7 +1602,7 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option
16001602
_ => {}
16011603
}
16021604

1603-
let proto = crate::closure::closure_get_dynamic_prop(closure as usize, "prototype");
1605+
let proto = constructor_target_prototype(closure);
16041606
if JSValue::from_bits(proto.to_bits()).is_pointer() {
16051607
crate::object::prototype_chain::object_set_static_prototype(obj as usize, proto.to_bits());
16061608
}
@@ -1654,6 +1656,7 @@ extern "C" fn collator_constructor_thunk(closure: *const ClosureHeader, rest: f6
16541656
}
16551657

16561658
extern "C" fn segmenter_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 {
1659+
require_new_target("Segmenter");
16571660
make_instance(
16581661
closure,
16591662
KIND_SEGMENTER,
@@ -1663,6 +1666,7 @@ extern "C" fn segmenter_constructor_thunk(closure: *const ClosureHeader, rest: f
16631666
}
16641667

16651668
extern "C" fn list_format_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 {
1669+
require_new_target("ListFormat");
16661670
make_instance(
16671671
closure,
16681672
KIND_LIST_FORMAT,
@@ -1675,6 +1679,7 @@ extern "C" fn relative_time_format_constructor_thunk(
16751679
closure: *const ClosureHeader,
16761680
rest: f64,
16771681
) -> f64 {
1682+
require_new_target("RelativeTimeFormat");
16781683
make_instance(
16791684
closure,
16801685
KIND_RELATIVE_TIME,
@@ -1684,6 +1689,7 @@ extern "C" fn relative_time_format_constructor_thunk(
16841689
}
16851690

16861691
extern "C" fn plural_rules_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 {
1692+
require_new_target("PluralRules");
16871693
make_instance(
16881694
closure,
16891695
KIND_PLURAL_RULES,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//! Shared `[[Construct]]`-only checks and `new.target`-aware prototype
2+
//! resolution used by the Intl service constructors (#5835). Split out of
3+
//! `intl.rs` to keep that file under the repository's 2,000-line gate.
4+
5+
use crate::closure::ClosureHeader;
6+
use crate::object::{js_object_get_field_by_name_f64, ObjectHeader};
7+
use crate::string::js_string_from_bytes;
8+
use crate::value::JSValue;
9+
10+
/// `GetPrototypeFromConstructor(new.target, "%<Ctor>Prototype%")`: a
11+
/// `Reflect.construct(Intl.X, args, CustomCtor)` call should install
12+
/// `CustomCtor.prototype` on the result (test262 `ctor-custom-prototype.js`),
13+
/// falling back to the invoked closure's own `"prototype"` when `new.target`
14+
/// is absent (a bare `new Intl.X()`) or its `prototype` isn't an object.
15+
pub(super) fn constructor_target_prototype(closure: *const ClosureHeader) -> f64 {
16+
const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000;
17+
const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF;
18+
let new_target = crate::object::js_new_target_get();
19+
let bits = new_target.to_bits();
20+
if (bits & !POINTER_MASK) == POINTER_TAG {
21+
let raw = (bits & POINTER_MASK) as usize;
22+
if raw != 0 {
23+
let key = js_string_from_bytes(b"prototype".as_ptr(), b"prototype".len() as u32);
24+
let proto = js_object_get_field_by_name_f64(raw as *const ObjectHeader, key);
25+
if JSValue::from_bits(proto.to_bits()).is_pointer() {
26+
return proto;
27+
}
28+
}
29+
}
30+
crate::closure::closure_get_dynamic_prop(closure as usize, "prototype")
31+
}
32+
33+
/// `Intl.<X>` is `[[Construct]]`-only per ECMA-402 (unlike the legacy
34+
/// factory-pattern `NumberFormat`/`DateTimeFormat`/`Collator`): a bare call
35+
/// or `.call(obj)` must throw a `TypeError` rather than silently `new`-ing.
36+
pub(super) fn require_new_target(name: &str) {
37+
if crate::object::js_new_target_get().to_bits() == crate::value::TAG_UNDEFINED {
38+
super::throw_type_error(&format!("Constructor Intl.{name} requires 'new'"));
39+
}
40+
}

crates/perry-runtime/src/intl/locale.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,7 @@ fn transform_instance(obj: *const ObjectHeader, transform: fn(&mut ParsedLocale)
650650
}
651651

652652
extern "C" fn locale_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 {
653+
super::require_new_target("Locale");
653654
let tag_value = super::rest_arg(rest, 0);
654655
let options_value = super::rest_arg(rest, 1);
655656
let tag_js = JSValue::from_bits(tag_value.to_bits());
@@ -682,7 +683,7 @@ extern "C" fn locale_constructor_thunk(closure: *const ClosureHeader, rest: f64)
682683
}
683684
apply_options(&mut parsed, options);
684685

685-
let proto = crate::closure::closure_get_dynamic_prop(closure as usize, "prototype");
686+
let proto = super::constructor_target_prototype(closure);
686687
make_locale_instance(proto.to_bits(), &parsed)
687688
}
688689

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// #5835 — Intl service constructor mechanics: `new.target` requirement and
2+
// `Reflect.construct`-driven prototype selection.
3+
//
4+
// ECMA-402 step 1 for ListFormat/RelativeTimeFormat/Segmenter/PluralRules/
5+
// Locale is "If NewTarget is undefined, throw a TypeError exception" — unlike
6+
// the legacy factory-pattern NumberFormat/DateTimeFormat/Collator, which
7+
// silently `new`-ed on a bare call. And OrdinaryCreateFromConstructor means
8+
// `Reflect.construct(Intl.X, args, Custom)` must install `Custom.prototype`
9+
// on the result, not `Intl.X.prototype`. Both must match
10+
// `node --experimental-strip-types` byte-for-byte.
11+
function probe(name: string, fn: () => unknown): string {
12+
try {
13+
fn();
14+
return "no throw";
15+
} catch (e) {
16+
return (e as Error).constructor.name;
17+
}
18+
}
19+
20+
const I: any = (globalThis as any).Intl;
21+
22+
console.log("ListFormat()", probe("ListFormat", () => I.ListFormat()));
23+
console.log("RelativeTimeFormat()", probe("RelativeTimeFormat", () => I.RelativeTimeFormat()));
24+
console.log("Segmenter()", probe("Segmenter", () => I.Segmenter()));
25+
console.log("PluralRules()", probe("PluralRules", () => I.PluralRules()));
26+
console.log("PluralRules.call(undefined)", probe("PluralRules.call", () => I.PluralRules.call(undefined)));
27+
console.log("Locale()", probe("Locale", () => I.Locale()));
28+
console.log("Locale('en')", probe("Locale('en')", () => I.Locale("en")));
29+
30+
// Legacy factory-pattern constructors still allow a bare call (no `new`).
31+
console.log("NumberFormat bare call", probe("NumberFormat", () => I.NumberFormat("en")));
32+
console.log("DateTimeFormat bare call", probe("DateTimeFormat", () => I.DateTimeFormat("en")));
33+
console.log("Collator bare call", probe("Collator", () => I.Collator("en")));
34+
35+
// `Reflect.construct(Intl.X, args, Custom)` installs `Custom.prototype`.
36+
{
37+
const custom: any = new Function();
38+
custom.prototype = {};
39+
const obj = Reflect.construct(I.DisplayNames, [undefined, { type: "language" }], custom);
40+
console.log("DisplayNames custom prototype", Object.getPrototypeOf(obj) === custom.prototype);
41+
}
42+
{
43+
const custom: any = new Function();
44+
custom.prototype = {};
45+
const obj = Reflect.construct(I.Segmenter, [], custom);
46+
console.log("Segmenter custom prototype", Object.getPrototypeOf(obj) === custom.prototype);
47+
}
48+
49+
// Ordinary (non-Reflect.construct) instances still resolve the default prototype.
50+
console.log("plain ListFormat prototype", Object.getPrototypeOf(new I.ListFormat("en")) === I.ListFormat.prototype);
51+
console.log("plain Locale prototype", Object.getPrototypeOf(new I.Locale("en")) === I.Locale.prototype);

0 commit comments

Comments
 (0)