use boa_gc::Gc;
use boa_parser::Source;
use crate::{
Context, JsObject, JsResult, JsValue,
builtins::{OrdinaryObject, function::OrdinaryFunction},
js_string,
object::{
ObjectInitializer, internal_methods::InternalMethodPropertyContext,
shape::slot::SlotAttributes,
},
property::{Attribute, PropertyDescriptor, PropertyKey},
vm::CodeBlock,
};
#[test]
fn get_own_property_internal_method() {
let context = &mut Context::default();
let o = context
.intrinsics()
.templates()
.ordinary_object()
.create(OrdinaryObject, Vec::default());
let property: PropertyKey = js_string!("prop").into();
let value = 100;
o.set(property.clone(), value, true, context)
.expect("should not fail");
let context = &mut InternalMethodPropertyContext::new(context);
assert_eq!(context.slot().index, 0);
assert_eq!(context.slot().attributes, SlotAttributes::empty());
o.__get_own_property__(&property, context)
.expect("should not fail");
assert!(
!context.slot().in_prototype(),
"Since it's an owned property, the prototype bit should not be set"
);
assert!(
context.slot().is_cacheable(),
"Since it's an owned property, this should be cacheable"
);
let shape = o.borrow().shape().clone();
let slot = shape.lookup(&property);
assert!(slot.is_some(), "the property should be found in the object");
let slot = slot.expect("the property should be found in the object");
assert_eq!(context.slot().index, slot.index);
}
#[test]
fn get_internal_method() {
let context = &mut Context::default();
let o = context
.intrinsics()
.templates()
.ordinary_object()
.create(OrdinaryObject, Vec::default());
let property: PropertyKey = js_string!("prop").into();
let value = 100;
o.set(property.clone(), value, true, context)
.expect("should not fail");
let context = &mut InternalMethodPropertyContext::new(context);
assert_eq!(context.slot().index, 0);
assert_eq!(context.slot().attributes, SlotAttributes::empty());
o.__get__(&property, o.clone().into(), context)
.expect("should not fail");
assert!(
!context.slot().in_prototype(),
"Since it's an owned property, the prototype bit should not be set"
);
assert!(
context.slot().is_cacheable(),
"Since it's an owned property, this should be cacheable"
);
let shape = o.borrow().shape().clone();
let slot = shape.lookup(&property);
assert!(slot.is_some(), "the property should be found in the object");
let slot = slot.expect("the property should be found in the object");
assert_eq!(context.slot().index, slot.index);
}
#[test]
fn get_internal_method_in_prototype() {
let context = &mut Context::default();
let o = context
.intrinsics()
.templates()
.ordinary_object()
.create(OrdinaryObject, Vec::default());
let property: PropertyKey = js_string!("prop").into();
let value = 100;
let prototype = context.intrinsics().constructors().object().prototype();
prototype
.set(property.clone(), value, true, context)
.expect("should not fail");
let context = &mut InternalMethodPropertyContext::new(context);
assert_eq!(context.slot().index, 0);
assert_eq!(context.slot().attributes, SlotAttributes::empty());
o.__get__(&property, o.clone().into(), context)
.expect("should not fail");
assert!(
context.slot().in_prototype(),
"Since it's an prototype property, the prototype bit should not be set"
);
assert!(
context.slot().is_cacheable(),
"Since it's an prototype property, this should be cacheable"
);
let shape = prototype.borrow().shape().clone();
let slot = shape.lookup(&property);
assert!(slot.is_some(), "the property should be found in the object");
let slot = slot.expect("the property should be found in the object");
assert_eq!(context.slot().index, slot.index);
}
#[test]
fn define_own_property_internal_method_non_existent_property() {
let context = &mut Context::default();
let o = context
.intrinsics()
.templates()
.ordinary_object()
.create(OrdinaryObject, Vec::default());
let property: PropertyKey = js_string!("prop").into();
let value = 100;
o.set(property.clone(), value, true, context)
.expect("should not fail");
let context = &mut InternalMethodPropertyContext::new(context);
assert_eq!(context.slot().index, 0);
assert_eq!(context.slot().attributes, SlotAttributes::empty());
o.__define_own_property__(
&property,
PropertyDescriptor::builder()
.value(value)
.writable(true)
.configurable(true)
.enumerable(true)
.build(),
context,
)
.expect("should not fail");
assert!(
!context.slot().in_prototype(),
"Since it's an owned property, the prototype bit should not be set"
);
assert!(
context.slot().is_cacheable(),
"Since it's an owned property, this should be cacheable"
);
let shape = o.borrow().shape().clone();
let slot = shape.lookup(&property);
assert!(slot.is_some(), "the property should be found in the object");
let slot = slot.expect("the property should be found in the object");
assert_eq!(context.slot().index, slot.index);
}
#[test]
fn define_own_property_internal_method_existing_property_property() {
let context = &mut Context::default();
let o = context
.intrinsics()
.templates()
.ordinary_object()
.create(OrdinaryObject, Vec::default());
let property: PropertyKey = js_string!("prop").into();
let value = 100;
o.set(property.clone(), value, true, context)
.expect("should not fail");
o.__define_own_property__(
&property,
PropertyDescriptor::builder()
.value(value)
.writable(true)
.configurable(true)
.enumerable(true)
.build(),
&mut context.into(),
)
.expect("should not fail");
let context = &mut InternalMethodPropertyContext::new(context);
assert_eq!(context.slot().index, 0);
assert_eq!(context.slot().attributes, SlotAttributes::empty());
o.__define_own_property__(
&property,
PropertyDescriptor::builder()
.value(value + 100)
.writable(true)
.configurable(true)
.enumerable(true)
.build(),
context,
)
.expect("should not fail");
assert!(
!context.slot().in_prototype(),
"Since it's an owned property, the prototype bit should not be set"
);
assert!(
context.slot().is_cacheable(),
"Since it's an owned property, this should be cacheable"
);
let shape = o.borrow().shape().clone();
let slot = shape.lookup(&property);
assert!(slot.is_some(), "the property should be found in the object");
let slot = slot.expect("the property should be found in the object");
assert_eq!(context.slot().index, slot.index);
}
#[test]
fn set_internal_method() {
let context = &mut Context::default();
let o = context
.intrinsics()
.templates()
.ordinary_object()
.create(OrdinaryObject, Vec::default());
let property: PropertyKey = js_string!("prop").into();
let value = 100;
o.set(property.clone(), value, true, context)
.expect("should not fail");
let context = &mut InternalMethodPropertyContext::new(context);
assert_eq!(context.slot().index, 0);
assert_eq!(context.slot().attributes, SlotAttributes::empty());
o.__set__(property.clone(), value.into(), o.clone().into(), context)
.expect("should not fail");
assert!(
!context.slot().in_prototype(),
"Since it's an owned property, the prototype bit should not be set"
);
assert!(
context.slot().is_cacheable(),
"Since it's an owned property, this should be cacheable"
);
let shape = o.borrow().shape().clone();
let slot = shape.lookup(&property);
assert!(slot.is_some(), "the property should be found in the object");
let slot = slot.expect("the property should be found in the object");
assert_eq!(context.slot().index, slot.index);
}
fn get_codeblock(value: &JsValue) -> Option<(JsObject, Gc<CodeBlock>)> {
let object = value.as_object()?.clone();
let code = object.downcast_ref::<OrdinaryFunction>()?.code.clone();
Some((object, code))
}
#[test]
fn set_property_by_name_set_inline_cache_on_property_load() -> JsResult<()> {
let context = &mut Context::default();
let function = context.eval(Source::from_bytes("(function (o) { return o.test; })"))?;
let (function, code) = get_codeblock(&function).unwrap();
assert_eq!(code.ic.len(), 1);
assert_eq!(code.ic[0].entries.borrow().len(), 0);
let o = ObjectInitializer::new(context)
.property(js_string!("test"), 0, Attribute::all())
.build();
let o_shape = o.borrow().shape().clone();
function.call(&JsValue::undefined(), &[o.clone().into()], context)?;
assert_eq!(code.ic[0].entries.borrow().len(), 1);
assert_eq!(
code.ic[0].entries.borrow()[0]
.shape
.upgrade()
.unwrap()
.to_addr_usize(),
o_shape.to_addr_usize()
);
Ok(())
}
#[test]
fn get_property_by_name_set_inline_cache_on_property_load() -> JsResult<()> {
let context = &mut Context::default();
let function = context.eval(Source::from_bytes("(function (o) { o.test = 30; })"))?;
let (function, code) = get_codeblock(&function).unwrap();
assert_eq!(code.ic.len(), 1);
assert_eq!(code.ic[0].entries.borrow().len(), 0);
let o = ObjectInitializer::new(context)
.property(js_string!("test"), 0, Attribute::all())
.build();
let o_shape = o.borrow().shape().clone();
function.call(&JsValue::undefined(), &[o.clone().into()], context)?;
assert_eq!(code.ic[0].entries.borrow().len(), 1);
assert_eq!(
code.ic[0].entries.borrow()[0]
.shape
.upgrade()
.unwrap()
.to_addr_usize(),
o_shape.to_addr_usize()
);
Ok(())
}
#[test]
fn unique_shape_insert_invalidates_a_cached_prototype_getter() -> JsResult<()> {
let context = &mut Context::default();
let prototype = context
.eval(Source::from_bytes(
"({ get memo() { \
Object.defineProperty(this, 'memo', { value: 42 }); \
return this.memo; \
} })",
))?
.as_object()
.expect("the object literal produces an object")
.clone();
let object = JsObject::from_proto_and_data(Some(prototype), OrdinaryObject);
let reader = context.eval(Source::from_bytes("(object) => object.memo"))?;
let reader = reader
.as_object()
.expect("the arrow function is callable")
.clone();
assert_eq!(
reader.call(&JsValue::undefined(), &[object.clone().into()], context)?,
JsValue::from(42)
);
assert_eq!(
reader.call(&JsValue::undefined(), &[object.into()], context)?,
JsValue::from(42)
);
Ok(())
}
#[test]
fn getter_mutation_does_not_poison_the_inline_cache() -> JsResult<()> {
let context = &mut Context::default();
let callback = context.eval(Source::from_bytes(
"(() => { \
let getterCalls = 0; \
class Fixture { \
get value() { \
getterCalls++; \
Object.defineProperty(this, 'value', { value: 42, configurable: true }); \
return 42; \
} \
} \
const fixture = new Fixture(); \
return () => { fixture.value; return getterCalls; }; \
})()",
))?;
let callback = callback
.as_object()
.expect("the outer function returns a callback")
.clone();
for _ in 0..2 {
assert_eq!(
callback.call(&JsValue::undefined(), &[], context)?,
JsValue::from(1)
);
}
Ok(())
}
#[test]
fn getter_mutation_does_not_poison_a_reentrant_inline_cache() -> JsResult<()> {
let context = &mut Context::default();
context.eval(Source::from_bytes(
"let getterCalls = 0; \
let listener; \
class Fixture { \
get value() { \
getterCalls++; \
Object.defineProperty(this, 'value', { value: 42 }); \
return this.value; \
} \
attach() { \
listener = () => this.value; \
} \
} \
const field = { \
addEventListener(_, callback) { listener = callback; }, \
dispatchEvent() { listener(); }, \
}; \
const fixture = new Fixture(); \
fixture.attach(field); \
field.addEventListener('input', listener);",
))?;
assert_eq!(
context.eval(Source::from_bytes("field.dispatchEvent(); getterCalls"))?,
JsValue::from(1)
);
assert_eq!(
context.eval(Source::from_bytes("field.dispatchEvent(); getterCalls"))?,
JsValue::from(1)
);
Ok(())
}
#[test]
fn global_getter_mutation_does_not_poison_the_inline_cache() -> JsResult<()> {
let context = &mut Context::default();
context.eval(Source::from_bytes(
"var getterCalls = 0; \
Object.defineProperty(globalThis, 'memo', { \
configurable: true, \
get() { \
getterCalls++; \
Object.defineProperty(globalThis, 'memo', { value: 42, configurable: true }); \
return 42; \
} \
});",
))?;
let callback = context.eval(Source::from_bytes("() => { memo; return getterCalls; }"))?;
let callback = callback
.as_object()
.expect("the outer function returns a callback")
.clone();
for _ in 0..2 {
assert_eq!(
callback.call(&JsValue::undefined(), &[], context)?,
JsValue::from(1)
);
}
Ok(())
}
#[test]
fn test_polymorphic_inline_cache() -> JsResult<()> {
let context = &mut Context::default();
let function = context.eval(Source::from_bytes("(function (o) { return o.test; })"))?;
let (function, code) = get_codeblock(&function).unwrap();
assert_eq!(code.ic.len(), 1);
assert_eq!(code.ic[0].entries.borrow().len(), 0);
assert!(!code.ic[0].megamorphic.get());
let shapes = vec![
ObjectInitializer::new(context)
.property(js_string!("test"), 1, Attribute::all())
.build(),
ObjectInitializer::new(context)
.property(js_string!("a"), 2, Attribute::all())
.property(js_string!("test"), 3, Attribute::all())
.build(),
ObjectInitializer::new(context)
.property(js_string!("b"), 4, Attribute::all())
.property(js_string!("test"), 5, Attribute::all())
.build(),
ObjectInitializer::new(context)
.property(js_string!("c"), 6, Attribute::all())
.property(js_string!("test"), 7, Attribute::all())
.build(),
];
for o in &shapes {
function.call(&JsValue::undefined(), &[o.clone().into()], context)?;
}
assert_eq!(code.ic[0].entries.borrow().len(), 4);
assert!(!code.ic[0].megamorphic.get());
Ok(())
}
#[test]
fn test_megamorphic_inline_cache() -> JsResult<()> {
let context = &mut Context::default();
let function = context.eval(Source::from_bytes("(function (o) { return o.test; })"))?;
let (function, code) = get_codeblock(&function).unwrap();
let shapes = vec![
ObjectInitializer::new(context)
.property(js_string!("test"), 1, Attribute::all())
.build(),
ObjectInitializer::new(context)
.property(js_string!("a"), 1, Attribute::all())
.property(js_string!("test"), 1, Attribute::all())
.build(),
ObjectInitializer::new(context)
.property(js_string!("b"), 1, Attribute::all())
.property(js_string!("test"), 1, Attribute::all())
.build(),
ObjectInitializer::new(context)
.property(js_string!("c"), 1, Attribute::all())
.property(js_string!("test"), 1, Attribute::all())
.build(),
ObjectInitializer::new(context)
.property(js_string!("d"), 1, Attribute::all())
.property(js_string!("test"), 1, Attribute::all())
.build(),
];
for o in &shapes {
function.call(&JsValue::undefined(), &[o.clone().into()], context)?;
}
assert_eq!(code.ic[0].entries.borrow().len(), 0);
assert!(code.ic[0].megamorphic.get());
let o6 = ObjectInitializer::new(context)
.property(js_string!("e"), 1, Attribute::all())
.property(js_string!("test"), 1, Attribute::all())
.build();
function.call(&JsValue::undefined(), &[o6.clone().into()], context)?;
assert_eq!(code.ic[0].entries.borrow().len(), 0);
assert!(code.ic[0].megamorphic.get());
Ok(())
}