Skip to content

Commit 2c1eb63

Browse files
authored
Optimize JsValue::to_property_key operation (#4505)
Just some small optimizations around `JsValue::to_property_key`, which is one of our hot spots on the benchmarks. ### Main results RESULT Richards 237 RESULT DeltaBlue 243 RESULT Crypto 192 RESULT RayTrace 524 RESULT EarleyBoyer 618 RESULT RegExp 87.6 RESULT Splay 877 RESULT NavierStokes 427 SCORE 323 ### PR results RESULT Richards 232 RESULT DeltaBlue 239 RESULT Crypto 200 RESULT RayTrace 524 RESULT EarleyBoyer 623 RESULT RegExp 91.8 RESULT Splay 862 RESULT NavierStokes 467 SCORE 328
1 parent c41b031 commit 2c1eb63

File tree

3 files changed

+106
-87
lines changed

3 files changed

+106
-87
lines changed

core/engine/src/object/jsobject.rs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ use super::{
1010
shape::RootShape,
1111
};
1212
use crate::{
13-
Context, JsData, JsResult, JsString, JsValue,
13+
Context, JsData, JsResult, JsString, JsSymbol, JsValue,
1414
builtins::{
1515
array::ARRAY_EXOTIC_INTERNAL_METHODS,
1616
array_buffer::{ArrayBuffer, BufferObject, SharedArrayBuffer},
1717
object::OrdinaryObject,
1818
},
1919
context::intrinsics::Intrinsics,
2020
error::JsNativeError,
21-
js_string,
21+
js_error, js_string,
2222
property::{PropertyDescriptor, PropertyKey},
2323
value::PreferredType,
2424
};
@@ -388,6 +388,52 @@ impl JsObject {
388388
Self::deep_strict_equals_inner(lhs, rhs, &mut HashSet::new(), context)
389389
}
390390

391+
/// The abstract operation `ToPrimitive` takes an input argument and an optional argument
392+
/// `PreferredType`.
393+
///
394+
/// <https://tc39.es/ecma262/#sec-toprimitive>
395+
pub fn to_primitive(
396+
&self,
397+
context: &mut Context,
398+
preferred_type: PreferredType,
399+
) -> JsResult<JsValue> {
400+
// a. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
401+
let Some(exotic_to_prim) = self.get_method(JsSymbol::to_primitive(), context)? else {
402+
// c. If preferredType is not present, let preferredType be number.
403+
let preferred_type = match preferred_type {
404+
PreferredType::Default | PreferredType::Number => PreferredType::Number,
405+
PreferredType::String => PreferredType::String,
406+
};
407+
return self.ordinary_to_primitive(context, preferred_type);
408+
};
409+
410+
// b. If exoticToPrim is not undefined, then
411+
// i. If preferredType is not present, let hint be "default".
412+
// ii. Else if preferredType is string, let hint be "string".
413+
// iii. Else,
414+
// 1. Assert: preferredType is number.
415+
// 2. Let hint be "number".
416+
let hint = match preferred_type {
417+
PreferredType::Default => js_string!("default"),
418+
PreferredType::String => js_string!("string"),
419+
PreferredType::Number => js_string!("number"),
420+
}
421+
.into();
422+
423+
// iv. Let result be ? Call(exoticToPrim, input, « hint »).
424+
let result = exotic_to_prim.call(&self.clone().into(), &[hint], context)?;
425+
426+
// v. If Type(result) is not Object, return result.
427+
// vi. Throw a TypeError exception.
428+
if result.is_object() {
429+
Err(js_error!(
430+
TypeError: "method `[Symbol.toPrimitive]` cannot return an object"
431+
))
432+
} else {
433+
Ok(result)
434+
}
435+
}
436+
391437
/// Converts an object to a primitive.
392438
///
393439
/// Diverges from the spec to prevent a stack overflow when the object is recursive.

core/engine/src/string.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ macro_rules! js_string {
5454
() => {
5555
$crate::string::JsString::default()
5656
};
57-
($s:literal) => {{
57+
($s:literal) => {const {
5858
const LITERAL: &$crate::string::JsStr<'static> = &$crate::js_str!($s);
5959

6060
$crate::string::JsString::from_static_js_str(LITERAL)

core/engine/src/value/mod.rs

Lines changed: 57 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -336,16 +336,15 @@ impl JsValue {
336336
#[allow(clippy::float_cmp)]
337337
pub fn as_i32(&self) -> Option<i32> {
338338
if let Some(integer) = self.0.as_integer32() {
339-
Some(integer)
340-
} else if let Some(rational) = self.0.as_float64() {
341-
if rational == f64::from(rational as i32) {
342-
Some(rational as i32)
343-
} else {
344-
None
345-
}
346-
} else {
347-
None
339+
return Some(integer);
340+
}
341+
342+
if let Some(rational) = self.0.as_float64()
343+
&& rational == f64::from(rational as i32)
344+
{
345+
return Some(rational as i32);
348346
}
347+
None
349348
}
350349

351350
/// Returns true if the value is a number.
@@ -431,52 +430,16 @@ impl JsValue {
431430
/// `PreferredType`.
432431
///
433432
/// <https://tc39.es/ecma262/#sec-toprimitive>
433+
#[inline]
434434
pub fn to_primitive(
435435
&self,
436436
context: &mut Context,
437437
preferred_type: PreferredType,
438438
) -> JsResult<Self> {
439439
// 1. Assert: input is an ECMAScript language value. (always a value not need to check)
440440
// 2. If Type(input) is Object, then
441-
if let Some(input) = self.as_object() {
442-
// a. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
443-
let exotic_to_prim = input.get_method(JsSymbol::to_primitive(), context)?;
444-
445-
// b. If exoticToPrim is not undefined, then
446-
if let Some(exotic_to_prim) = exotic_to_prim {
447-
// i. If preferredType is not present, let hint be "default".
448-
// ii. Else if preferredType is string, let hint be "string".
449-
// iii. Else,
450-
// 1. Assert: preferredType is number.
451-
// 2. Let hint be "number".
452-
let hint = match preferred_type {
453-
PreferredType::Default => js_string!("default"),
454-
PreferredType::String => js_string!("string"),
455-
PreferredType::Number => js_string!("number"),
456-
}
457-
.into();
458-
459-
// iv. Let result be ? Call(exoticToPrim, input, « hint »).
460-
let result = exotic_to_prim.call(self, &[hint], context)?;
461-
// v. If Type(result) is not Object, return result.
462-
// vi. Throw a TypeError exception.
463-
return if result.is_object() {
464-
Err(JsNativeError::typ()
465-
.with_message("Symbol.toPrimitive cannot return an object")
466-
.into())
467-
} else {
468-
Ok(result)
469-
};
470-
}
471-
472-
// c. If preferredType is not present, let preferredType be number.
473-
let preferred_type = match preferred_type {
474-
PreferredType::Default | PreferredType::Number => PreferredType::Number,
475-
PreferredType::String => PreferredType::String,
476-
};
477-
478-
// d. Return ? OrdinaryToPrimitive(input, preferredType).
479-
return input.ordinary_to_primitive(context, preferred_type);
441+
if let Some(o) = self.as_object() {
442+
return o.to_primitive(context, preferred_type);
480443
}
481444

482445
// 3. Return input.
@@ -513,11 +476,10 @@ impl JsValue {
513476
JsVariant::Integer32(_) | JsVariant::Float64(_) => Err(JsNativeError::typ()
514477
.with_message("cannot convert Number to a BigInt")
515478
.into()),
516-
JsVariant::BigInt(b) => Ok(b.clone()),
517-
JsVariant::Object(_) => {
518-
let primitive = self.to_primitive(context, PreferredType::Number)?;
519-
primitive.to_bigint(context)
520-
}
479+
JsVariant::BigInt(b) => Ok(b),
480+
JsVariant::Object(o) => o
481+
.to_primitive(context, PreferredType::Number)?
482+
.to_bigint(context),
521483
JsVariant::Symbol(_) => Err(JsNativeError::typ()
522484
.with_message("cannot convert Symbol to a BigInt")
523485
.into()),
@@ -558,15 +520,14 @@ impl JsValue {
558520
JsVariant::Boolean(false) => Ok(js_string!("false")),
559521
JsVariant::Float64(rational) => Ok(JsString::from(rational)),
560522
JsVariant::Integer32(integer) => Ok(JsString::from(integer)),
561-
JsVariant::String(string) => Ok(string.clone()),
523+
JsVariant::String(string) => Ok(string),
562524
JsVariant::Symbol(_) => Err(JsNativeError::typ()
563525
.with_message("can't convert symbol to string")
564526
.into()),
565527
JsVariant::BigInt(bigint) => Ok(bigint.to_string().into()),
566-
JsVariant::Object(_) => {
567-
let primitive = self.to_primitive(context, PreferredType::String)?;
568-
primitive.to_string(context)
569-
}
528+
JsVariant::Object(o) => o
529+
.to_primitive(context, PreferredType::String)?
530+
.to_string(context),
570531
}
571532
}
572533

@@ -595,22 +556,25 @@ impl JsValue {
595556
.templates()
596557
.number()
597558
.create(rational, Vec::default())),
598-
JsVariant::String(string) => Ok(context
599-
.intrinsics()
600-
.templates()
601-
.string()
602-
.create(string.clone(), vec![string.len().into()])),
559+
JsVariant::String(string) => {
560+
let len = string.len();
561+
Ok(context
562+
.intrinsics()
563+
.templates()
564+
.string()
565+
.create(string, vec![len.into()]))
566+
}
603567
JsVariant::Symbol(symbol) => Ok(context
604568
.intrinsics()
605569
.templates()
606570
.symbol()
607-
.create(symbol.clone(), Vec::default())),
571+
.create(symbol, Vec::default())),
608572
JsVariant::BigInt(bigint) => Ok(context
609573
.intrinsics()
610574
.templates()
611575
.bigint()
612-
.create(bigint.clone(), Vec::default())),
613-
JsVariant::Object(jsobject) => Ok(jsobject.clone()),
576+
.create(bigint, Vec::default())),
577+
JsVariant::Object(jsobject) => Ok(jsobject),
614578
}
615579
}
616580

@@ -635,23 +599,32 @@ impl JsValue {
635599
///
636600
/// See <https://tc39.es/ecma262/#sec-topropertykey>
637601
pub fn to_property_key(&self, context: &mut Context) -> JsResult<PropertyKey> {
638-
Ok(match self.variant() {
639-
// Fast path:
640-
JsVariant::String(string) => string.clone().into(),
641-
JsVariant::Symbol(symbol) => symbol.clone().into(),
642-
JsVariant::Integer32(integer) => integer.into(),
643-
// Slow path:
644-
JsVariant::Object(_) => {
645-
let primitive = self.to_primitive(context, PreferredType::String)?;
646-
match primitive.variant() {
647-
JsVariant::String(string) => string.clone().into(),
648-
JsVariant::Symbol(symbol) => symbol.clone().into(),
649-
JsVariant::Integer32(integer) => integer.into(),
650-
_ => primitive.to_string(context)?.into(),
651-
}
652-
}
653-
_ => self.to_string(context)?.into(),
654-
})
602+
match self.variant() {
603+
// fast path
604+
//
605+
// The compiler will surely make this a jump table, but in case it
606+
// doesn't, we put the "expected" property key types first
607+
// (integer, string, symbol), then the rest of the variants.
608+
JsVariant::Integer32(integer) => Ok(integer.into()),
609+
JsVariant::String(string) => Ok(string.into()),
610+
JsVariant::Symbol(symbol) => Ok(symbol.into()),
611+
612+
// We also inline the call to `to_string`, removing the
613+
// double match against `self.variant()`.
614+
JsVariant::Float64(float) => Ok(JsString::from(float).into()),
615+
JsVariant::Undefined => Ok(js_string!("undefined").into()),
616+
JsVariant::Null => Ok(js_string!("null").into()),
617+
JsVariant::Boolean(true) => Ok(js_string!("true").into()),
618+
JsVariant::Boolean(false) => Ok(js_string!("false").into()),
619+
JsVariant::BigInt(bigint) => Ok(JsString::from(bigint.to_string()).into()),
620+
621+
// slow path
622+
// Cannot infinitely recurse since it is guaranteed that `to_primitive` returns a non-object
623+
// value or errors.
624+
JsVariant::Object(o) => o
625+
.to_primitive(context, PreferredType::String)?
626+
.to_property_key(context),
627+
}
655628
}
656629

657630
/// It returns value converted to a numeric value of type `Number` or `BigInt`.
@@ -663,7 +636,7 @@ impl JsValue {
663636

664637
// 2. If primValue is a BigInt, return primValue.
665638
if let Some(bigint) = primitive.as_bigint() {
666-
return Ok(bigint.clone().into());
639+
return Ok(bigint.into());
667640
}
668641

669642
// 3. Return ? ToNumber(primValue).

0 commit comments

Comments
 (0)