Skip to content

Commit b78e36f

Browse files
committed
typedarray: preserve observable toLocaleString semantics while keeping shared NumberFormat optimization
Signed-off-by: mrhapile <allinonegaming3456@gmail.com>
1 parent 5e5b7d7 commit b78e36f

File tree

3 files changed

+80
-19
lines changed

3 files changed

+80
-19
lines changed

core/engine/src/builtins/number/mod.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ pub(crate) struct Number;
4848

4949
impl IntrinsicObject for Number {
5050
fn init(realm: &Realm) {
51+
let to_locale_string_function = BuiltInBuilder::callable_with_object(
52+
realm,
53+
realm
54+
.intrinsics()
55+
.objects()
56+
.number_prototype_to_locale_string()
57+
.into(),
58+
Self::to_locale_string,
59+
)
60+
.name(js_string!("toLocaleString"))
61+
.length(0)
62+
.build();
63+
5164
let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT;
5265

5366
BuiltInBuilder::from_standard_constructor::<Self>(realm)
@@ -87,7 +100,11 @@ impl IntrinsicObject for Number {
87100
.static_method(Self::number_is_integer, js_string!("isInteger"), 1)
88101
.method(Self::to_exponential, js_string!("toExponential"), 1)
89102
.method(Self::to_fixed, js_string!("toFixed"), 1)
90-
.method(Self::to_locale_string, js_string!("toLocaleString"), 0)
103+
.property(
104+
js_string!("toLocaleString"),
105+
to_locale_string_function,
106+
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
107+
)
91108
.method(Self::to_precision, js_string!("toPrecision"), 1)
92109
.method(Self::to_string, js_string!("toString"), 1)
93110
.method(Self::value_of, js_string!("valueOf"), 0)

core/engine/src/builtins/typed_array/builtin.rs

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2470,6 +2470,7 @@ impl BuiltinTypedArray {
24702470
///
24712471
/// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.tolocalestring
24722472
/// [spec-402]: https://402.ecma-international.org/10.0/#sup-array.prototype.tolocalestring
2473+
#[allow(clippy::used_underscore_binding)]
24732474
pub(crate) fn to_locale_string(
24742475
this: &JsValue,
24752476
args: &[JsValue],
@@ -2482,7 +2483,7 @@ impl BuiltinTypedArray {
24822483
JsNativeError::typ().with_message("Value is not a typed array object")
24832484
})?;
24842485

2485-
let (len, is_bigint) = {
2486+
let (len, _is_bigint) = {
24862487
let o = array.downcast_ref::<TypedArray>().ok_or_else(|| {
24872488
JsNativeError::typ().with_message("Value is not a typed array object")
24882489
})?;
@@ -2511,7 +2512,7 @@ impl BuiltinTypedArray {
25112512
let options = args.get_or_undefined(1).clone();
25122513

25132514
#[cfg(feature = "intl")]
2514-
let number_format = if is_bigint {
2515+
let number_format = if _is_bigint {
25152516
None
25162517
} else {
25172518
use crate::builtins::intl::number_format::NumberFormat;
@@ -2523,6 +2524,22 @@ impl BuiltinTypedArray {
25232524

25242525
let call_args = [locales, options];
25252526

2527+
#[cfg(feature = "intl")]
2528+
let is_unmodified = {
2529+
let number_proto = context.intrinsics().constructors().number().prototype();
2530+
2531+
let current_tls = number_proto.get(js_string!("toLocaleString"), context)?;
2532+
2533+
let builtin_tls = context
2534+
.intrinsics()
2535+
.objects()
2536+
.number_prototype_to_locale_string();
2537+
2538+
current_tls
2539+
.as_object()
2540+
.is_some_and(|o| JsObject::equals(&o, &builtin_tls.into()))
2541+
};
2542+
25262543
// 5. Let k be 0.
25272544
// 6. Repeat, while k < len,
25282545
for k in 0..len {
@@ -2537,21 +2554,35 @@ impl BuiltinTypedArray {
25372554
}
25382555

25392556
#[cfg(feature = "intl")]
2540-
if is_bigint {
2541-
let s = next_element
2542-
.invoke(js_string!("toLocaleString"), &call_args, context)?
2543-
.to_string(context)?;
2544-
r.push(s);
2545-
} else {
2546-
use crate::builtins::intl::number_format::to_intl_mathematical_value;
2547-
let mut x = to_intl_mathematical_value(&next_element, context)?;
2548-
r.push(js_string!(
2549-
number_format
2550-
.as_ref()
2551-
.expect("number_format should be initialized for numeric typed arrays")
2552-
.format(&mut x)
2553-
.to_string()
2554-
));
2557+
{
2558+
use crate::{
2559+
builtins::intl::number_format::to_intl_mathematical_value, value::JsVariant,
2560+
};
2561+
2562+
// Fast path: finite primitive numbers only
2563+
let use_fast_path = is_unmodified
2564+
&& match next_element.variant() {
2565+
JsVariant::Integer32(_) => !_is_bigint,
2566+
JsVariant::Float64(f) => !_is_bigint && f.is_finite(),
2567+
_ => false,
2568+
};
2569+
2570+
if use_fast_path {
2571+
let mut x = to_intl_mathematical_value(&next_element, context)?;
2572+
r.push(js_string!(
2573+
number_format
2574+
.as_ref()
2575+
.expect("number_format is Some for numeric typed arrays")
2576+
.format(&mut x)
2577+
.to_string()
2578+
));
2579+
} else {
2580+
// Slow path: delegate to element.toLocaleString()
2581+
let s = next_element
2582+
.invoke(js_string!("toLocaleString"), &call_args, context)?
2583+
.to_string(context)?;
2584+
r.push(s);
2585+
}
25552586
}
25562587

25572588
#[cfg(not(feature = "intl"))]
@@ -2566,7 +2597,7 @@ impl BuiltinTypedArray {
25662597
// 7. Return R.
25672598
let separator = js_string!(", ");
25682599
let mut result = Vec::with_capacity(
2569-
r.iter().map(boa_string::JsString::len).sum::<usize>()
2600+
r.iter().map(JsString::len).sum::<usize>()
25702601
+ separator.len() * (len.saturating_sub(1) as usize),
25712602
);
25722603
for (i, s) in r.into_iter().enumerate() {

core/engine/src/context/intrinsics.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,9 @@ pub struct IntrinsicObjects {
10861086
/// [`%Array.prototype.toString%`](https://tc39.es/ecma262/#sec-array.prototype.tostring)
10871087
array_prototype_to_string: JsFunction,
10881088

1089+
/// [`%Number.prototype.toLocaleString%`](https://tc39.es/ecma262/#sec-number.prototype.tolocalestring)
1090+
number_prototype_to_locale_string: JsFunction,
1091+
10891092
/// Cached iterator prototypes.
10901093
iterator_prototypes: IteratorPrototypes,
10911094

@@ -1158,6 +1161,7 @@ impl IntrinsicObjects {
11581161
throw_type_error: JsFunction::empty_intrinsic_function(false),
11591162
array_prototype_values: JsFunction::empty_intrinsic_function(false),
11601163
array_prototype_to_string: JsFunction::empty_intrinsic_function(false),
1164+
number_prototype_to_locale_string: JsFunction::empty_intrinsic_function(false),
11611165
iterator_prototypes: IteratorPrototypes::default(),
11621166
generator: JsObject::with_null_proto(),
11631167
async_generator: JsObject::with_null_proto(),
@@ -1210,6 +1214,15 @@ impl IntrinsicObjects {
12101214
self.array_prototype_to_string.clone()
12111215
}
12121216

1217+
/// Gets the [`%Number.prototype.toLocaleString%`][spec] intrinsic function.
1218+
///
1219+
/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tolocalestring
1220+
#[inline]
1221+
#[must_use]
1222+
pub fn number_prototype_to_locale_string(&self) -> JsFunction {
1223+
self.number_prototype_to_locale_string.clone()
1224+
}
1225+
12131226
/// Gets the cached iterator prototypes.
12141227
#[inline]
12151228
#[must_use]

0 commit comments

Comments
 (0)