use crate::{
Context, JsArgs, JsResult, JsStr, JsString, JsValue, SpannedSourceText,
builtins::{
BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, OrdinaryObject,
},
bytecompiler::FunctionCompiler,
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
environments::{EnvironmentStack, FunctionSlots, PrivateEnvironment, ThisBindingStatus},
error::JsNativeError,
js_error, js_string,
native_function::NativeFunctionObject,
object::{
JsData, JsFunction, JsObject, PrivateElement, PrivateName,
internal_methods::{
CallValue, InternalMethodCallContext, InternalObjectMethods, ORDINARY_INTERNAL_METHODS,
get_prototype_from_constructor,
},
},
property::{Attribute, PropertyDescriptor, PropertyKey},
realm::Realm,
string::StaticJsStrings,
symbol::JsSymbol,
value::IntegerOrInfinity,
vm::{ActiveRunnable, CallFrame, CallFrameFlags, CodeBlock},
};
use boa_ast::{
Position, Span, Spanned, StatementList,
function::{FormalParameterList, FunctionBody},
operations::{
ContainsSymbol, all_private_identifiers_valid, bound_names, contains,
lexically_declared_names,
},
scope::BindingLocatorScope,
};
use boa_gc::{self, Finalize, Gc, Trace, custom_trace};
use boa_interner::Sym;
use boa_macros::js_str;
use boa_parser::{Parser, Source};
use thin_vec::ThinVec;
use super::Proxy;
pub(crate) mod arguments;
mod bound;
pub use bound::BoundFunction;
#[cfg(test)]
mod tests;
#[derive(Debug, Trace, Finalize, PartialEq, Eq, Clone)]
pub enum ThisMode {
Lexical,
Strict,
Global,
}
impl ThisMode {
#[must_use]
pub const fn is_lexical(&self) -> bool {
matches!(self, Self::Lexical)
}
#[must_use]
pub const fn is_strict(&self) -> bool {
matches!(self, Self::Strict)
}
#[must_use]
pub const fn is_global(&self) -> bool {
matches!(self, Self::Global)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConstructorKind {
Base,
Derived,
}
impl ConstructorKind {
#[must_use]
pub const fn is_base(&self) -> bool {
matches!(self, Self::Base)
}
#[must_use]
pub const fn is_derived(&self) -> bool {
matches!(self, Self::Derived)
}
}
#[derive(Clone, Debug, Finalize)]
pub enum ClassFieldDefinition {
Public(PropertyKey, JsFunction, Option<PropertyKey>),
Private(PrivateName, JsFunction),
}
unsafe impl Trace for ClassFieldDefinition {
custom_trace! {this, mark, {
match this {
Self::Public(_key, func, _) => {
mark(func);
}
Self::Private(_, func) => {
mark(func);
}
}
}}
}
#[derive(Debug, Trace, Finalize)]
pub struct OrdinaryFunction {
pub(crate) code: Gc<CodeBlock>,
pub(crate) environments: EnvironmentStack,
pub(crate) home_object: Option<JsObject>,
pub(crate) script_or_module: Option<ActiveRunnable>,
pub(crate) realm: Realm,
fields: ThinVec<ClassFieldDefinition>,
private_methods: ThinVec<(PrivateName, PrivateElement)>,
}
impl JsData for OrdinaryFunction {
fn internal_methods(&self) -> &'static InternalObjectMethods {
static FUNCTION_METHODS: InternalObjectMethods = InternalObjectMethods {
__call__: function_call,
..ORDINARY_INTERNAL_METHODS
};
static CONSTRUCTOR_METHODS: InternalObjectMethods = InternalObjectMethods {
__call__: function_call,
__construct__: function_construct,
..ORDINARY_INTERNAL_METHODS
};
if self.code.has_prototype_property() {
&CONSTRUCTOR_METHODS
} else {
&FUNCTION_METHODS
}
}
}
impl OrdinaryFunction {
pub(crate) fn new(
code: Gc<CodeBlock>,
environments: EnvironmentStack,
script_or_module: Option<ActiveRunnable>,
realm: Realm,
) -> Self {
Self {
code,
environments,
home_object: None,
script_or_module,
realm,
fields: ThinVec::default(),
private_methods: ThinVec::default(),
}
}
#[must_use]
pub fn codeblock(&self) -> &CodeBlock {
&self.code
}
pub(crate) fn push_private_environment(&mut self, environment: Gc<PrivateEnvironment>) {
self.environments.push_private(environment);
}
pub(crate) fn is_derived_constructor(&self) -> bool {
self.code.is_derived_constructor()
}
pub(crate) fn in_class_field_initializer(&self) -> bool {
self.code.in_class_field_initializer()
}
pub(crate) const fn get_home_object(&self) -> Option<&JsObject> {
self.home_object.as_ref()
}
pub(crate) fn set_home_object(&mut self, object: JsObject) {
self.home_object = Some(object);
}
pub(crate) fn get_fields(&self) -> &[ClassFieldDefinition] {
&self.fields
}
pub(crate) fn push_field(
&mut self,
key: PropertyKey,
value: JsFunction,
function_name: Option<PropertyKey>,
) {
self.fields
.push(ClassFieldDefinition::Public(key, value, function_name));
}
pub(crate) fn push_field_private(&mut self, name: PrivateName, value: JsFunction) {
self.fields.push(ClassFieldDefinition::Private(name, value));
}
pub(crate) fn get_private_methods(&self) -> &[(PrivateName, PrivateElement)] {
&self.private_methods
}
pub(crate) fn push_private_method(&mut self, name: PrivateName, method: PrivateElement) {
self.private_methods.push((name, method));
}
#[must_use]
pub const fn realm(&self) -> &Realm {
&self.realm
}
pub(crate) fn is_ordinary(&self) -> bool {
self.code.is_ordinary()
}
}
#[derive(Debug, Clone, Copy)]
pub struct BuiltInFunctionObject;
impl IntrinsicObject for BuiltInFunctionObject {
fn init(realm: &Realm) {
let has_instance = BuiltInBuilder::callable(realm, Self::has_instance)
.name(js_string!("[Symbol.hasInstance]"))
.length(1)
.build();
let throw_type_error = realm.intrinsics().objects().throw_type_error();
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.method(Self::apply, js_string!("apply"), 2)
.method(Self::bind, js_string!("bind"), 1)
.method(Self::call, js_string!("call"), 1)
.method(Self::to_string, js_string!("toString"), 0)
.property(JsSymbol::has_instance(), has_instance, Attribute::default())
.accessor(
js_string!("caller"),
Some(throw_type_error.clone()),
Some(throw_type_error.clone()),
Attribute::CONFIGURABLE,
)
.accessor(
js_string!("arguments"),
Some(throw_type_error.clone()),
Some(throw_type_error),
Attribute::CONFIGURABLE,
)
.build();
let prototype = realm.intrinsics().constructors().function().prototype();
BuiltInBuilder::callable_with_object(realm, prototype.clone(), Self::prototype)
.name(js_string!())
.length(0)
.build();
prototype.set_prototype(Some(realm.intrinsics().constructors().object().prototype()));
}
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
}
impl BuiltInObject for BuiltInFunctionObject {
const NAME: JsString = StaticJsStrings::FUNCTION;
}
impl BuiltInConstructor for BuiltInFunctionObject {
const CONSTRUCTOR_ARGUMENTS: usize = 1;
const PROTOTYPE_STORAGE_SLOTS: usize = 10;
const CONSTRUCTOR_STORAGE_SLOTS: usize = 0;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::function;
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let active_function = context
.active_function_object()
.unwrap_or_else(|| context.intrinsics().constructors().function().constructor());
Self::create_dynamic_function(active_function, new_target, args, false, false, context)
.map(Into::into)
}
}
impl BuiltInFunctionObject {
pub(crate) fn create_dynamic_function(
constructor: JsObject,
new_target: &JsValue,
args: &[JsValue],
r#async: bool,
generator: bool,
context: &mut Context,
) -> JsResult<JsObject> {
let new_target = if new_target.is_undefined() {
constructor.into()
} else {
new_target.clone()
};
let strict = context.is_strict();
let default = if r#async && generator {
StandardConstructors::async_generator_function
} else if r#async {
StandardConstructors::async_function
} else if generator {
StandardConstructors::generator_function
} else {
StandardConstructors::function
};
let prototype = get_prototype_from_constructor(&new_target, default, context)?;
let (body, param_list) = if let Some((body, params)) = args.split_last() {
let mut parameters = Vec::with_capacity(args.len());
for param in params {
parameters.push(param.to_string(context)?);
}
let body = body.to_string(context)?;
(body, parameters)
} else {
(js_string!(), Vec::new())
};
let current_realm = context.realm().clone();
context.host_hooks().ensure_can_compile_strings(
current_realm,
¶m_list,
&body,
false,
context,
)?;
let parameters = if param_list.is_empty() {
FormalParameterList::default()
} else {
let parameters = itertools::Itertools::intersperse(
param_list.iter().map(JsString::iter),
js_str!(",").iter(),
)
.flatten()
.collect::<Vec<_>>();
let mut parser = Parser::new(Source::from_utf16(¶meters));
parser.set_identifier(context.next_parser_identifier());
let parameters = parser
.parse_formal_parameters(context.interner_mut(), generator, r#async)
.map_err(|e| {
JsNativeError::syntax()
.with_message(format!("failed to parse function parameters: {e}"))
})?;
if generator && contains(¶meters, ContainsSymbol::YieldExpression) {
return Err(JsNativeError::syntax().with_message(
if r#async {
"yield expression is not allowed in formal parameter list of async generator"
} else {
"yield expression is not allowed in formal parameter list of generator"
}
).into());
}
if r#async && contains(¶meters, ContainsSymbol::AwaitExpression) {
return Err(JsNativeError::syntax()
.with_message(
if generator {
"await expression is not allowed in formal parameter list of async function"
} else {
"await expression is not allowed in formal parameter list of async generator"
})
.into());
}
parameters
};
let body = if body.is_empty() {
FunctionBody::new(StatementList::default(), Span::new((1, 1), (1, 1)))
} else {
let mut body_parse = Vec::with_capacity(body.len());
body_parse.push(u16::from(b'\n'));
body_parse.extend(body.iter());
body_parse.push(u16::from(b'\n'));
let mut parser = Parser::new(Source::from_utf16(&body_parse));
parser.set_identifier(context.next_parser_identifier());
let body = parser
.parse_function_body(context.interner_mut(), generator, r#async)
.map_err(|e| {
JsNativeError::syntax()
.with_message(format!("failed to parse function body: {e}"))
})?;
if !all_private_identifiers_valid(&body, Vec::new()) {
return Err(JsNativeError::syntax()
.with_message("invalid private identifier usage")
.into());
}
if body.strict() {
for name in bound_names(¶meters) {
if name == Sym::ARGUMENTS || name == Sym::EVAL {
return Err(JsNativeError::syntax()
.with_message("Unexpected 'eval' or 'arguments' in strict mode")
.into());
}
}
}
if (body.strict()) && parameters.has_duplicates() {
return Err(JsNativeError::syntax()
.with_message("Duplicate parameter name not allowed in this context")
.into());
}
if body.strict() && !parameters.is_simple() {
return Err(JsNativeError::syntax()
.with_message(
"Illegal 'use strict' directive in function with non-simple parameter list",
)
.into());
}
if contains(&body, ContainsSymbol::SuperProperty) {
return Err(JsNativeError::syntax()
.with_message("invalid `super` reference")
.into());
}
if contains(&body, ContainsSymbol::SuperCall) {
return Err(JsNativeError::syntax()
.with_message("invalid `super` call")
.into());
}
{
let lexically_declared_names = lexically_declared_names(&body);
for name in bound_names(¶meters) {
if lexically_declared_names.contains(&name) {
return Err(JsNativeError::syntax()
.with_message(format!(
"Redeclaration of formal parameter `{}`",
context.interner().resolve_expect(name)
))
.into());
}
}
}
body
};
let function_span_start = Position::new(1, 1);
let function_span_end = body.span().end();
let mut function = boa_ast::function::FunctionExpression::new(
None,
parameters,
body,
None,
false,
Span::new(function_span_start, function_span_end),
);
if let Err(reason) =
function.analyze_scope(strict, context.realm().scope(), context.interner())
{
return Err(js_error!(SyntaxError: "failed to analyze function scope: {}", reason));
}
let in_with = context.vm.environments.has_object_environment();
let spanned_source_text = SpannedSourceText::new_empty();
let code = FunctionCompiler::new(spanned_source_text)
.name(js_string!("anonymous"))
.generator(generator)
.r#async(r#async)
.in_with(in_with)
.force_function_scope(true)
.compile(
function.parameters(),
function.body(),
context.realm().scope().clone(),
context.realm().scope().clone(),
function.scopes(),
function.contains_direct_eval(),
context.interner_mut(),
);
let environments = context.vm.environments.pop_to_global();
let function_object = crate::vm::create_function_object(code, prototype, context);
context.vm.environments.extend(environments);
Ok(function_object)
}
fn apply(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let func = this.as_callable().ok_or_else(|| {
JsNativeError::typ().with_message(format!("{} is not a function", this.display()))
})?;
let this_arg = args.get_or_undefined(0);
let arg_array = args.get_or_undefined(1);
if arg_array.is_null_or_undefined() {
return func.call(this_arg, &[], context);
}
let arg_list = arg_array.create_list_from_array_like(&[], context)?;
func.call(this_arg, &arg_list, context)
}
fn bind(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let target = this.as_callable().ok_or_else(|| {
JsNativeError::typ()
.with_message("cannot bind `this` without a `[[Call]]` internal method")
})?;
let this_arg = args.get_or_undefined(0).clone();
let bound_args = args.get(1..).unwrap_or(&[]).to_vec();
let arg_count = bound_args.len() as i64;
let f = BoundFunction::create(target.clone(), this_arg, bound_args, context)?;
let mut l = JsValue::new(0);
if target.has_own_property(StaticJsStrings::LENGTH, context)? {
let target_len = target.get(StaticJsStrings::LENGTH, context)?;
if target_len.is_number() {
match target_len
.to_integer_or_infinity(context)
.expect("to_integer_or_infinity cannot fail for a number")
{
IntegerOrInfinity::PositiveInfinity => l = f64::INFINITY.into(),
IntegerOrInfinity::NegativeInfinity => {}
IntegerOrInfinity::Integer(target_len) => {
l = (target_len - arg_count).max(0).into();
}
}
}
}
f.define_property_or_throw(
StaticJsStrings::LENGTH,
PropertyDescriptor::builder()
.value(l)
.writable(false)
.enumerable(false)
.configurable(true),
context,
)
.expect("defining the `length` property for a new object should not fail");
let target_name = target.get(js_string!("name"), context)?;
let target_name = target_name.as_string().unwrap_or_default();
set_function_name(&f, &target_name.into(), Some(js_str!("bound")), context);
Ok(f.into())
}
fn call(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let func = this.as_callable().ok_or_else(|| {
JsNativeError::typ().with_message(format!("{} is not a function", this.display()))
})?;
let this_arg = args.get_or_undefined(0);
func.call(this_arg, args.get(1..).unwrap_or(&[]), context)
}
#[allow(clippy::wrong_self_convention)]
fn to_string(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let func = this;
let Some(object) = func.as_callable() else {
return Err(JsNativeError::typ().with_message("not a function").into());
};
if object.is::<NativeFunctionObject>() {
let name = {
let value = object.get(js_string!("name"), &mut *context)?;
if value.is_null_or_undefined() {
js_string!()
} else {
value.to_string(context)?
}
};
return Ok(
js_string!(js_str!("function "), &name, js_str!("() { [native code] }")).into(),
);
} else if object.is::<Proxy>() || object.is::<BoundFunction>() {
return Ok(js_string!("function () { [native code] }").into());
}
let function = object
.downcast_ref::<OrdinaryFunction>()
.ok_or_else(|| JsNativeError::typ().with_message("not a function"))?;
let code = function.codeblock();
if let Some(code_points) = code.source_info().text_spanned().to_code_points() {
return Ok(JsString::from(code_points).into());
}
Ok(js_string!(
js_str!("function "),
code.name(),
js_str!("() { [native code] }")
)
.into())
}
fn has_instance(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::ordinary_has_instance(this, args.get_or_undefined(0), context)?.into())
}
#[allow(clippy::unnecessary_wraps)]
fn prototype(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
}
pub(crate) fn set_function_name(
function: &JsObject,
name: &PropertyKey,
prefix: Option<JsStr<'_>>,
context: &mut Context,
) {
let mut name = match name {
PropertyKey::Symbol(sym) => {
sym.description().map_or_else(
|| js_string!(),
|desc| js_string!(js_str!("["), &desc, js_str!("]")),
)
}
PropertyKey::String(string) => string.clone(),
PropertyKey::Index(index) => js_string!(format!("{}", index.get())),
};
if let Some(prefix) = prefix {
name = js_string!(prefix, js_str!(" "), &name);
}
function
.define_property_or_throw(
js_string!("name"),
PropertyDescriptor::builder()
.value(name)
.writable(false)
.enumerable(false)
.configurable(true),
context,
)
.expect("defining the `name` property must not fail per the spec");
}
pub(crate) fn function_call(
function_object: &JsObject,
argument_count: usize,
#[allow(
unused_variables,
reason = "Only used if native-backtrace feature is enabled"
)]
context: &mut InternalMethodCallContext<'_>,
) -> JsResult<CallValue> {
context.check_runtime_limits()?;
let function = function_object
.downcast_ref::<OrdinaryFunction>()
.expect("not a function");
let realm = function.realm().clone();
if function.code.is_class_constructor() {
debug_assert!(
function.is_ordinary(),
"only ordinary functions can be classes"
);
return Err(JsNativeError::typ()
.with_message("class constructor cannot be invoked without 'new'")
.with_realm(realm)
.into());
}
let code = function.code.clone();
let environments = function.environments.clone();
let script_or_module = function.script_or_module.clone();
drop(function);
let env_fp = environments.len() as u32;
let frame = CallFrame::new(code.clone(), script_or_module, environments, realm)
.with_argument_count(argument_count as u32)
.with_env_fp(env_fp);
#[cfg(feature = "native-backtrace")]
{
let native_source_info = context.native_source_info();
context
.vm
.shadow_stack
.patch_last_native(native_source_info);
}
context.vm.push_frame(frame);
let this = context.vm.stack.get_this(context.vm.frame());
let context = context.context();
let lexical_this_mode = code.this_mode == ThisMode::Lexical;
let this = if lexical_this_mode {
ThisBindingStatus::Lexical
} else if code.strict() {
context.vm.frame_mut().flags |= CallFrameFlags::THIS_VALUE_CACHED;
ThisBindingStatus::Initialized(this)
} else if this.is_null_or_undefined() {
context.vm.frame_mut().flags |= CallFrameFlags::THIS_VALUE_CACHED;
let this: JsValue = context.realm().global_this().clone().into();
context.vm.stack.set_this(&context.vm.frame, this.clone());
ThisBindingStatus::Initialized(this)
} else {
let this: JsValue = this
.to_object(context)
.expect("conversion cannot fail")
.into();
context.vm.frame_mut().flags |= CallFrameFlags::THIS_VALUE_CACHED;
context.vm.stack.set_this(&context.vm.frame, this.clone());
ThisBindingStatus::Initialized(this)
};
let mut last_env = 0;
if code.has_binding_identifier() {
let index = context.vm.environments.push_lexical(1);
context.vm.environments.put_lexical_value(
BindingLocatorScope::Stack(index),
0,
function_object.clone().into(),
);
last_env += 1;
}
if code.has_function_scope() {
context.vm.environments.push_function(
code.constant_scope(last_env),
FunctionSlots::new(this, function_object.clone(), None),
);
}
Ok(CallValue::Ready)
}
fn function_construct(
this_function_object: &JsObject,
argument_count: usize,
context: &mut InternalMethodCallContext<'_>,
) -> JsResult<CallValue> {
context.check_runtime_limits()?;
let function = this_function_object
.downcast_ref::<OrdinaryFunction>()
.expect("not a function");
let realm = function.realm().clone();
debug_assert!(
function.is_ordinary(),
"only ordinary functions can be constructed"
);
let code = function.code.clone();
let environments = function.environments.clone();
let script_or_module = function.script_or_module.clone();
drop(function);
let env_fp = environments.len() as u32;
let new_target = context.vm.stack.pop();
let this = if code.is_derived_constructor() {
None
} else {
let prototype =
get_prototype_from_constructor(&new_target, StandardConstructors::object, context)?;
let this = JsObject::from_proto_and_data_with_shared_shape(
context.root_shape(),
prototype,
OrdinaryObject,
);
this.initialize_instance_elements(this_function_object, context)?;
Some(this)
};
let mut frame = CallFrame::new(code.clone(), script_or_module, environments, realm)
.with_argument_count(argument_count as u32)
.with_env_fp(env_fp)
.with_flags(CallFrameFlags::CONSTRUCT);
frame
.flags
.set(CallFrameFlags::THIS_VALUE_CACHED, this.is_some());
#[cfg(feature = "native-backtrace")]
{
let native_source_info = context.native_source_info();
context
.vm
.shadow_stack
.patch_last_native(native_source_info);
}
context.vm.push_frame(frame);
let mut last_env = 0;
if code.has_binding_identifier() {
let index = context.vm.environments.push_lexical(1);
context.vm.environments.put_lexical_value(
BindingLocatorScope::Stack(index),
0,
this_function_object.clone().into(),
);
last_env += 1;
}
if code.has_function_scope() {
context.vm.environments.push_function(
code.constant_scope(last_env),
FunctionSlots::new(
this.clone().map_or(ThisBindingStatus::Uninitialized, |o| {
ThisBindingStatus::Initialized(o.into())
}),
this_function_object.clone(),
Some(
new_target
.as_object()
.expect("new.target should be an object")
.clone(),
),
),
);
}
let context = context.context();
context.vm.stack.set_this(
&context.vm.frame,
this.map(JsValue::new).unwrap_or_default(),
);
Ok(CallValue::Ready)
}