Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lazy builtins #3973

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"type": "process",
"label": "Run JS file",
"command": "cargo",
"args": ["run", "--bin", "boa", "${file}"],
"args": ["run", "--bin", "boa", "${workspaceFolder}/debug/script.js"],
"group": {
"kind": "build",
"isDefault": true
Expand Down
27 changes: 20 additions & 7 deletions core/engine/src/builtins/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
native_function::{NativeFunctionObject, NativeFunctionPointer},
object::{
shape::{property_table::PropertyTableInner, slot::SlotAttributes},
FunctionBinding, JsFunction, JsPrototype, CONSTRUCTOR, PROTOTYPE,
FunctionBinding, JsFunction, JsPrototype, LazyBuiltIn, CONSTRUCTOR, PROTOTYPE,
},
property::{Attribute, PropertyDescriptor, PropertyKey},
realm::Realm,
Expand Down Expand Up @@ -394,12 +394,25 @@ impl BuiltInConstructorWithPrototype<'_> {
}

let mut object = self.object.borrow_mut();
let function = object
.downcast_mut::<NativeFunctionObject>()
.expect("Builtin must be a function object");
function.f = NativeFunction::from_fn_ptr(self.function);
function.constructor = Some(ConstructorKind::Base);
function.realm = Some(self.realm.clone());
jasonwilliams marked this conversation as resolved.
Show resolved Hide resolved
if object.is::<NativeFunctionObject>() {
let function = object
.downcast_mut::<NativeFunctionObject>()
.expect("Builtin must be a function object");
function.f = NativeFunction::from_fn_ptr(self.function);
function.constructor = Some(ConstructorKind::Base);
function.realm = Some(self.realm.clone());
} else if object.is::<LazyBuiltIn>() {
let lazy = object
.downcast_mut::<LazyBuiltIn>()
.expect("Builtin must be a lazy object");
lazy.set_constructor(
NativeFunction::from_fn_ptr(self.function),
self.realm.clone(),
);
} else {
unreachable!("The object must be a function or a lazy object");
}

object
.properties_mut()
.shape
Expand Down
8 changes: 4 additions & 4 deletions core/engine/src/builtins/function/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::{
get_prototype_from_constructor, CallValue, InternalObjectMethods,
ORDINARY_INTERNAL_METHODS,
},
JsData, JsFunction, JsObject, PrivateElement, PrivateName,
JsData, JsFunction, JsObject, LazyBuiltIn, PrivateElement, PrivateName,
},
property::{Attribute, PropertyDescriptor, PropertyKey},
realm::Realm,
Expand Down Expand Up @@ -844,8 +844,7 @@ impl BuiltInFunctionObject {
return Err(JsNativeError::typ().with_message("not a function").into());
};

let object_borrow = object.borrow();
if object_borrow.is::<NativeFunctionObject>() {
if object.is::<NativeFunctionObject>() || object.is::<LazyBuiltIn>() {
let name = {
// Is there a case here where if there is no name field on a value
// name should default to None? Do all functions have names set?
Expand All @@ -859,10 +858,11 @@ impl BuiltInFunctionObject {
return Ok(
js_string!(js_str!("function "), &name, js_str!("() { [native code] }")).into(),
);
} else if object_borrow.is::<Proxy>() || object_borrow.is::<BoundFunction>() {
} else if object.is::<Proxy>() || object.is::<BoundFunction>() {
return Ok(js_string!("function () { [native code] }").into());
}

let object_borrow = object.borrow();
let function = object_borrow
.downcast_ref::<OrdinaryFunction>()
.ok_or_else(|| JsNativeError::typ().with_message("not a function"))?;
Expand Down
1 change: 0 additions & 1 deletion core/engine/src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ impl Realm {
ForInIterator::init(self);
Math::init(self);
Json::init(self);
Array::init(self);
ArrayIterator::init(self);
Proxy::init(self);
ArrayBuffer::init(self);
Expand Down
63 changes: 54 additions & 9 deletions core/engine/src/context/intrinsics.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
//! Data structures that contain intrinsic objects and constructors.

use boa_gc::{Finalize, Trace};
use boa_gc::{Finalize, Trace, WeakGc};
use boa_macros::js_str;

use crate::{
builtins::{iterable::IteratorPrototypes, uri::UriFunctions, Array, OrdinaryObject},
builtins::{
function::ConstructorKind, iterable::IteratorPrototypes, uri::UriFunctions, Array,
IntrinsicObject, OrdinaryObject,
},
js_string,
native_function::NativeFunctionObject,
object::{
internal_methods::immutable_prototype::IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS,
shape::{shared_shape::template::ObjectTemplate, RootShape},
JsFunction, JsObject, Object, CONSTRUCTOR, PROTOTYPE,
BuiltinKind, JsFunction, JsObject, LazyBuiltIn, Object, CONSTRUCTOR, PROTOTYPE,
},
property::{Attribute, PropertyKey},
JsSymbol,
realm::{Realm, RealmInner},
JsSymbol, JsValue, NativeFunction,
};

#[cfg(feature = "intl")]
Expand Down Expand Up @@ -40,8 +45,8 @@ impl Intrinsics {
/// To initialize all the intrinsics with their spec properties, see [`Realm::initialize`].
///
/// [`Realm::initialize`]: crate::realm::Realm::initialize
pub(crate) fn uninit(root_shape: &RootShape) -> Option<Self> {
let constructors = StandardConstructors::default();
pub(crate) fn uninit(root_shape: &RootShape, realm_inner: &WeakGc<RealmInner>) -> Option<Self> {
let constructors = StandardConstructors::new(realm_inner);
let templates = ObjectTemplates::new(root_shape, &constructors);

Some(Self {
Expand Down Expand Up @@ -95,6 +100,46 @@ impl StandardConstructor {
}
}

/// Build a constructor that is lazily initialized.
/// Both the constructor and the prototype are lazily initialized.
///
/// For example: Both the initiation of the `Array` and accessing its prototype will fire `init` once.
/// This means that the actual creation of the array and the retrieval of its
/// prototype are deferred until they are actually needed.
///
/// ## Example
///
/// ```javascript
/// // Lazy initiation of the Array
/// let array = new Array(10); // Creates an array with 10 empty slots
///
/// // Lazy access to the prototype
/// let prototype = Object.getPrototypeOf(array);
///
/// // Accessing an index in the array
/// array[0] = 42; // Sets the first element to 42
/// console.log(array[0]); // Logs 42
/// ```
fn lazy_array(init: fn(&Realm) -> (), realm_inner: &WeakGc<RealmInner>) -> Self {
let obj: JsObject<LazyBuiltIn> = JsObject::new_unique(
None,
LazyBuiltIn {
init_and_realm: Some((init, realm_inner.clone())),
kind: BuiltinKind::Function(NativeFunctionObject {
f: NativeFunction::from_fn_ptr(|_, _, _| Ok(JsValue::undefined())),
constructor: Some(ConstructorKind::Base),
realm: None,
}),
},
);
let constructor = JsFunction::from_object_unchecked(obj.clone().upcast());

Self {
constructor: constructor.clone(),
prototype: JsObject::lazy_array_prototype(obj),
}
}

/// Build a constructor with a defined prototype.
fn with_prototype(prototype: JsObject) -> Self {
Self {
Expand Down Expand Up @@ -203,8 +248,8 @@ pub struct StandardConstructors {
calendar: StandardConstructor,
}

impl Default for StandardConstructors {
fn default() -> Self {
impl StandardConstructors {
fn new(realm_inner: &WeakGc<RealmInner>) -> Self {
Self {
object: StandardConstructor::with_prototype(JsObject::from_object_and_vtable(
Object::<OrdinaryObject>::default(),
Expand All @@ -219,7 +264,7 @@ impl Default for StandardConstructors {
},
async_function: StandardConstructor::default(),
generator_function: StandardConstructor::default(),
array: StandardConstructor::with_prototype(JsObject::from_proto_and_data(None, Array)),
array: StandardConstructor::lazy_array(Array::init, realm_inner),
bigint: StandardConstructor::default(),
number: StandardConstructor::with_prototype(JsObject::from_proto_and_data(None, 0.0)),
boolean: StandardConstructor::with_prototype(JsObject::from_proto_and_data(
Expand Down
50 changes: 40 additions & 10 deletions core/engine/src/native_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,28 @@ pub(crate) fn native_function_call(
obj: &JsObject,
argument_count: usize,
context: &mut Context,
) -> JsResult<CallValue> {
let native_function = &obj
.clone()
.downcast::<NativeFunctionObject>()
.expect("the object should be a native function object")
.borrow_mut()
.data
.clone();
native_function_call_inner(obj, native_function, argument_count, context)
}

/// Call this object.
///
/// # Panics
///
/// Panics if the object is currently mutably borrowed.
// <https://tc39.es/ecma262/#sec-built-in-function-objects-call-thisargument-argumentslist>
pub(crate) fn native_function_call_inner(
obj: &JsObject,
native_function: &NativeFunctionObject,
argument_count: usize,
context: &mut Context,
) -> JsResult<CallValue> {
let args = context.vm.pop_n_values(argument_count);
let _func = context.vm.pop();
Expand All @@ -356,10 +378,7 @@ pub(crate) fn native_function_call(
f: function,
constructor,
realm,
} = obj
.downcast_ref::<NativeFunctionObject>()
.expect("the object should be a native function object")
.clone();
} = native_function.clone();

let mut realm = realm.unwrap_or_else(|| context.realm().clone());

Expand All @@ -380,7 +399,6 @@ pub(crate) fn native_function_call(

Ok(CallValue::Complete)
}

/// Construct an instance of this object with the specified arguments.
///
/// # Panics
Expand All @@ -391,20 +409,32 @@ fn native_function_construct(
obj: &JsObject,
argument_count: usize,
context: &mut Context,
) -> JsResult<CallValue> {
native_function_construct_inner(
&obj.downcast_ref::<NativeFunctionObject>()
.expect("the object should be a native function object")
.clone(),
obj.clone(),
argument_count,
context,
)
}

pub(crate) fn native_function_construct_inner(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there's been some (minor) performance regressions with this PR and I think its related to this, I'm going to assume because its not [inline]'d this is now calling 2 functions instead of 1, and because its on a hotpath its actually slowed it down. Ill try adding the inline attribute and see if thats what it is, but leaving this here as a reminder.

native_function: &NativeFunctionObject,
this_function_object: JsObject,
argument_count: usize,
context: &mut Context,
) -> JsResult<CallValue> {
// We technically don't need this since native functions don't push any new frames to the
// vm, but we'll eventually have to combine the native stack with the vm stack.
context.check_runtime_limits()?;
let this_function_object = obj.clone();

let NativeFunctionObject {
f: function,
constructor,
realm,
} = obj
.downcast_ref::<NativeFunctionObject>()
.expect("the object should be a native function object")
.clone();
} = native_function.clone();

let mut realm = realm.unwrap_or_else(|| context.realm().clone());

Expand Down
Loading
Loading