use std::{cell::RefCell, mem::MaybeUninit};
use boa_string::JsString;
use dynify::Dynify;
use super::args::Argument;
use super::{IndexOperand, RegisterOperand};
use crate::{
Context, JsError, JsExpect, JsObject, JsResult, JsValue, NativeFunction,
builtins::{Promise, math::Math, promise::PromiseCapability},
error::JsNativeError,
job::NativeAsyncJob,
module::{ImportAttribute, ModuleKind, ModuleRequest, Referrer},
object::{FunctionObjectBuilder, internal_methods::InternalMethodPropertyContext},
property::PropertyKey,
string::StaticJsStrings,
vm::opcode::Operation,
};
#[derive(Debug, Clone, Copy)]
pub(crate) struct CallEval;
impl CallEval {
#[inline(always)]
pub(super) fn operation(
(argument_count, scope_index): (IndexOperand, IndexOperand),
context: &mut Context,
) -> JsResult<()> {
let func = context
.vm
.stack
.calling_convention_get_function(argument_count.into());
let Some(object) = func.as_object() else {
return Err(JsNativeError::typ()
.with_message("not a callable function")
.into());
};
let eval = context.intrinsics().objects().eval();
if JsObject::equals(&object, &eval) {
let arguments = context
.vm
.stack
.calling_convention_pop_arguments(argument_count.into());
let _func = context.vm.stack.pop();
let _this = context.vm.stack.pop();
if let Some(x) = arguments.first() {
let strict = context.vm.frame().code_block.strict();
let scope = context
.vm
.frame()
.code_block()
.constant_scope(scope_index.into());
let result = crate::builtins::eval::Eval::perform_eval(
x,
true,
Some(scope),
strict,
context,
)?;
context.vm.stack.push(result);
} else {
context.vm.stack.push(JsValue::undefined());
}
return Ok(());
}
object.__call__(argument_count.into()).resolve(context)?;
Ok(())
}
}
impl Operation for CallEval {
const NAME: &'static str = "CallEval";
const INSTRUCTION: &'static str = "INST - CallEval";
const COST: u8 = 5;
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct CallEvalSpread;
impl CallEvalSpread {
#[inline(always)]
pub(super) fn operation(index: IndexOperand, context: &mut Context) -> JsResult<()> {
let arguments_array = context.vm.stack.pop();
let arguments_array_object = arguments_array
.as_object()
.js_expect("arguments array in call spread function must be an object")?;
let arguments = arguments_array_object
.borrow()
.properties()
.to_dense_indexed_properties()
.js_expect("arguments array in call spread function must be dense")?;
let func = context.vm.stack.calling_convention_get_function(0);
let Some(object) = func.as_object() else {
return Err(JsNativeError::typ()
.with_message("not a callable function")
.into());
};
let eval = context.intrinsics().objects().eval();
if JsObject::equals(&object, &eval) {
let _func = context.vm.stack.pop();
let _this = context.vm.stack.pop();
if let Some(x) = arguments.first() {
let strict = context.vm.frame().code_block.strict();
let scope = context.vm.frame().code_block().constant_scope(index.into());
let result = crate::builtins::eval::Eval::perform_eval(
x,
true,
Some(scope),
strict,
context,
)?;
context.vm.stack.push(result);
} else {
context.vm.stack.push(JsValue::undefined());
}
return Ok(());
}
let argument_count = arguments.len();
context
.vm
.stack
.calling_convention_push_arguments(&arguments);
object.__call__(argument_count).resolve(context)?;
Ok(())
}
}
impl Operation for CallEvalSpread {
const NAME: &'static str = "CallEvalSpread";
const INSTRUCTION: &'static str = "INST - CallEvalSpread";
const COST: u8 = 5;
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Call;
impl Call {
#[inline(always)]
pub(super) fn operation(argument_count: IndexOperand, context: &mut Context) -> JsResult<()> {
let func = context
.vm
.stack
.calling_convention_get_function(argument_count.into());
let Some(object) = func.as_object() else {
return Err(Self::handle_not_callable());
};
object.__call__(argument_count.into()).resolve(context)?;
Ok(())
}
#[cold]
#[inline(never)]
fn handle_not_callable() -> JsError {
JsNativeError::typ()
.with_message("not a callable function")
.into()
}
}
impl Operation for Call {
const NAME: &'static str = "Call";
const INSTRUCTION: &'static str = "INST - Call";
const COST: u8 = 3;
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct CallSpread;
impl CallSpread {
#[inline(always)]
pub(super) fn operation((): (), context: &mut Context) -> JsResult<()> {
let arguments_array = context.vm.stack.pop();
let arguments_array_object = arguments_array
.as_object()
.js_expect("arguments array in call spread function must be an object")?;
let arguments = arguments_array_object
.borrow()
.properties()
.to_dense_indexed_properties()
.js_expect("arguments array in call spread function must be dense")?;
let argument_count = arguments.len();
context
.vm
.stack
.calling_convention_push_arguments(&arguments);
let func = context
.vm
.stack
.calling_convention_get_function(argument_count);
let Some(object) = func.as_object() else {
return Err(JsNativeError::typ()
.with_message("not a callable function")
.into());
};
object.__call__(argument_count).resolve(context)?;
Ok(())
}
}
impl Operation for CallSpread {
const NAME: &'static str = "CallSpread";
const INSTRUCTION: &'static str = "INST - CallSpread";
const COST: u8 = 3;
}
fn parse_import_attributes(
specifier: JsString,
options: &JsValue,
context: &mut Context,
) -> JsResult<ModuleRequest> {
let mut attributes = Vec::new();
if !options.is_undefined() {
let Some(options_obj) = options.as_object() else {
return Err(JsNativeError::typ()
.with_message("import options must be an object or undefined")
.into());
};
let attributes_obj = options_obj.get(crate::js_str!("with"), context)?;
if !attributes_obj.is_undefined() {
let Some(attributes_obj) = attributes_obj.as_object() else {
return Err(JsNativeError::typ()
.with_message("the 'with' option must be an object")
.into());
};
let entries = attributes_obj.enumerable_own_property_names(
crate::property::PropertyNameKind::KeyAndValue,
context,
)?;
attributes.reserve(entries.len());
for entry in entries {
let entry = entry
.as_object()
.js_expect("entry from EnumerableOwnProperties must be an object")?;
let key = entry.get(0, context)?;
let key_str = key
.as_string()
.js_expect("key from EnumerableOwnProperties must be a string")?
.clone();
let value = entry.get(1, context)?;
let Some(value_str) = value.as_string() else {
return Err(JsNativeError::typ()
.with_message("import attribute value must be a string")
.into());
};
let value_str = value_str.clone();
attributes.push(ImportAttribute::new(key_str, value_str));
}
}
}
Ok(ModuleRequest::new(specifier, attributes.into_boxed_slice()))
}
async fn load_dyn_import(
referrer: Referrer,
request: ModuleRequest,
cap: PromiseCapability,
phase: u32,
context: &RefCell<&mut Context>,
) -> JsResult<()> {
let loader = context.borrow().module_loader();
let fut = loader.load_imported_module(referrer.clone(), request.clone(), context);
let mut stack = [MaybeUninit::<u8>::uninit(); 16];
let mut heap = Vec::<MaybeUninit<u8>>::new();
let completion = fut.init2(&mut stack, &mut heap).await;
let module = match completion {
Err(err) => {
let err = err.into_opaque(&mut context.borrow_mut())?;
cap.reject()
.call(&JsValue::undefined(), &[err], &mut context.borrow_mut())
.expect("default `reject` function cannot throw");
return Ok(());
}
Ok(m) => m,
};
match referrer {
Referrer::Module(mod_ref) => {
let ModuleKind::SourceText(src) = mod_ref.kind() else {
panic!("referrer cannot be a synthetic module");
};
let mut loaded_modules = src.loaded_modules().borrow_mut();
let entry = loaded_modules
.entry(request)
.or_insert_with(|| module.clone());
debug_assert_eq!(&module, entry);
}
Referrer::Realm(realm) => {
let mut loaded_modules = realm.loaded_modules().borrow_mut();
let entry = loaded_modules
.entry(request.specifier().clone())
.or_insert_with(|| module.clone());
debug_assert_eq!(&module, entry);
}
Referrer::Script(script) => {
let mut loaded_modules = script.loaded_modules().borrow_mut();
let entry = loaded_modules
.entry(request.specifier().clone())
.or_insert_with(|| module.clone());
debug_assert_eq!(&module, entry);
}
}
#[cfg(not(feature = "experimental"))]
if phase != 0 {
let err = JsNativeError::syntax()
.with_message("import.defer() and import.source() require the 'experimental' feature")
.into();
let err = JsError::into_opaque(err, &mut context.borrow_mut())?;
cap.reject()
.call(&JsValue::undefined(), &[err], &mut context.borrow_mut())
.expect("default `reject` function cannot throw");
return Ok(());
}
#[cfg(feature = "experimental")]
if phase == 2 {
let err = JsNativeError::syntax()
.with_message("source phase import is not available for this module")
.into();
let err = JsError::into_opaque(err, &mut context.borrow_mut())?;
cap.reject()
.call(&JsValue::undefined(), &[err], &mut context.borrow_mut())
.expect("default `reject` function cannot throw");
return Ok(());
}
let load = module.load(&mut context.borrow_mut());
let on_rejected = FunctionObjectBuilder::new(
context.borrow().realm(),
NativeFunction::from_copy_closure_with_captures(
|_, args, cap, context| {
cap.reject()
.call(&JsValue::undefined(), args, context)
.expect("default `reject` function cannot throw");
Ok(JsValue::undefined())
},
cap.clone(),
),
)
.build();
let link_evaluate = FunctionObjectBuilder::new(
context.borrow().realm(),
NativeFunction::from_copy_closure_with_captures(
|_, _, (module, cap, on_rejected), context| {
if let Err(e) = module.link(context) {
let e = e.into_opaque(context)?;
cap.reject()
.call(&JsValue::undefined(), &[e], context)
.expect("default `reject` function cannot throw");
return Ok(JsValue::undefined());
}
let evaluate = module.evaluate(context)?;
let fulfill = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_, _, (module, cap), context| {
let namespace = module.namespace(context);
cap.resolve()
.call(&JsValue::undefined(), &[namespace.into()], context)
.expect("default `resolve` function cannot throw");
Ok(JsValue::undefined())
},
(module.clone(), cap.clone()),
),
)
.build();
Promise::perform_promise_then(
&evaluate,
Some(fulfill),
Some(on_rejected.clone()),
None,
context,
);
Ok(JsValue::undefined())
},
(module.clone(), cap.clone(), on_rejected.clone()),
),
)
.build();
Promise::perform_promise_then(
&load,
Some(link_evaluate),
Some(on_rejected),
None,
&mut context.borrow_mut(),
);
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ImportCall;
impl ImportCall {
#[inline(always)]
pub(super) fn operation(
(specifier_op, options_op, phase_op): (RegisterOperand, RegisterOperand, IndexOperand),
context: &mut Context,
) -> JsResult<()> {
let phase: u32 = phase_op.into();
let referrer = context
.get_active_script_or_module()
.map_or_else(|| Referrer::Realm(context.realm().clone()), Into::into);
let specifier = context.vm.get_register(specifier_op.into()).clone();
let options = context.vm.get_register(options_op.into()).clone();
let cap = PromiseCapability::new(
&context.intrinsics().constructors().promise().constructor(),
context,
)
.expect("operation cannot fail for the %Promise% intrinsic");
let promise = cap.promise().clone();
let specifier_str = match specifier.to_string(context) {
Ok(s) => s,
Err(err) => {
let err = err.into_opaque(context)?;
cap.reject().call(&JsValue::undefined(), &[err], context)?;
context.vm.set_register(specifier_op.into(), promise.into());
return Ok(());
}
};
let request = match parse_import_attributes(specifier_str, &options, context) {
Ok(req) => req,
Err(err) => {
let err = err.into_opaque(context)?;
cap.reject().call(&JsValue::undefined(), &[err], context)?;
context.vm.set_register(specifier_op.into(), promise.into());
return Ok(());
}
};
let job = NativeAsyncJob::with_realm(
async move |context| {
load_dyn_import(referrer, request, cap, phase, context).await?;
Ok(JsValue::undefined())
},
context.realm().clone(),
);
context.enqueue_job(job.into());
context.vm.set_register(specifier_op.into(), promise.into());
Ok(())
}
}
impl Operation for ImportCall {
const NAME: &'static str = "ImportCall";
const INSTRUCTION: &'static str = "INST - ImportCall";
const COST: u8 = 15;
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct PrepareMathOperands {
pub(crate) callee: RegisterOperand,
pub(crate) this: RegisterOperand,
pub(crate) function: u8,
pub(crate) binding_index: IndexOperand,
}
impl std::fmt::Display for PrepareMathOperands {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"callee:{}, this:{}, function:{}, binding_index:{}",
self.callee, self.this, self.function, self.binding_index
)
}
}
impl Argument for PrepareMathOperands {
fn encode(self, bytes: &mut Vec<u8>) {
self.callee.encode(bytes);
self.this.encode(bytes);
self.function.encode(bytes);
self.binding_index.encode(bytes);
}
fn decode(bytes: &[u8], pos: usize) -> (Self, usize) {
let (callee, pos) = RegisterOperand::decode(bytes, pos);
let (this, pos) = RegisterOperand::decode(bytes, pos);
let (function, pos) = u8::decode(bytes, pos);
let (binding_index, pos) = IndexOperand::decode(bytes, pos);
(
Self {
callee,
this,
function,
binding_index,
},
pos,
)
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct CallMathOperands {
pub(crate) dst: RegisterOperand,
pub(crate) callee: RegisterOperand,
pub(crate) this: RegisterOperand,
pub(crate) function: u8,
pub(crate) argc: u8,
pub(crate) args: [RegisterOperand; 3],
}
impl std::fmt::Display for CallMathOperands {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"dst:{}, callee:{}, this:{}, function:{}, argc:{}",
self.dst, self.callee, self.this, self.function, self.argc
)?;
for arg in self.args.iter().take(usize::from(self.argc)) {
write!(f, ", arg:{arg}")?;
}
Ok(())
}
}
impl Argument for CallMathOperands {
fn encode(self, bytes: &mut Vec<u8>) {
self.dst.encode(bytes);
self.callee.encode(bytes);
self.this.encode(bytes);
self.function.encode(bytes);
self.argc.encode(bytes);
self.args[0].encode(bytes);
self.args[1].encode(bytes);
self.args[2].encode(bytes);
}
fn decode(bytes: &[u8], pos: usize) -> (Self, usize) {
let (dst, pos) = RegisterOperand::decode(bytes, pos);
let (callee, pos) = RegisterOperand::decode(bytes, pos);
let (this, pos) = RegisterOperand::decode(bytes, pos);
let (function, pos) = u8::decode(bytes, pos);
let (count, pos) = u8::decode(bytes, pos);
let (arg0, pos) = RegisterOperand::decode(bytes, pos);
let (arg1, pos) = RegisterOperand::decode(bytes, pos);
let (arg2, pos) = RegisterOperand::decode(bytes, pos);
(
Self {
dst,
callee,
this,
function,
argc: count,
args: [arg0, arg1, arg2],
},
pos,
)
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct PrepareMath;
impl Operation for PrepareMath {
const NAME: &'static str = "PrepareMath";
const INSTRUCTION: &'static str = "INST - PrepareMath";
const COST: u8 = 2;
}
impl PrepareMath {
#[inline(always)]
pub(super) fn operation(operands: PrepareMathOperands, context: &mut Context) -> JsResult<()> {
if context.realm().math_pristine() && context.global_binding_is_static() {
context
.vm
.set_register(operands.this.into(), JsValue::undefined());
return Ok(());
}
prepare_math_slow(operands, context)
}
}
#[cold]
#[inline(never)]
fn prepare_math_slow(operands: PrepareMathOperands, context: &mut Context) -> JsResult<()> {
let base = load_math_binding(operands.binding_index, context)?;
let object = base.base_class(context)?;
let key = PropertyKey::from(Math::pure_name(operands.function));
let callee = object.__get__(
&key,
base.clone(),
&mut InternalMethodPropertyContext::new(context),
)?;
context.vm.set_register(operands.this.into(), base);
context.vm.set_register(operands.callee.into(), callee);
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct CallMath;
impl Operation for CallMath {
const NAME: &'static str = "CallMath";
const INSTRUCTION: &'static str = "INST - CallMath";
const COST: u8 = 15;
}
impl CallMath {
#[inline(always)]
pub(super) fn operation(operands: CallMathOperands, context: &mut Context) -> JsResult<()> {
let live = usize::from(operands.argc);
assert!(live <= 3, "CallMath argument count");
if context.vm.get_register(operands.this.into()).is_undefined() {
let mut numbers = [0f64; 3];
let all_numbers =
numbers
.iter_mut()
.zip(operands.args)
.take(live)
.all(|(slot, reg)| {
context
.vm
.get_register(reg.into())
.as_number()
.map(|number| *slot = number)
.is_some()
});
if all_numbers
&& let Some(value) = Math::call_pure_numbers(operands.function, &numbers[..live])
{
context
.vm
.set_register(operands.dst.into(), JsValue::from(value));
return Ok(());
}
}
let mut stored = [
JsValue::undefined(),
JsValue::undefined(),
JsValue::undefined(),
];
for (slot, reg) in stored.iter_mut().zip(operands.args).take(live) {
*slot = context.vm.get_register(reg.into()).clone();
}
let args = &stored[..live];
let value = if context.vm.get_register(operands.this.into()).is_undefined() {
Math::call_pure(operands.function, args, context)?
} else {
call_math_slow(operands, args, context)?
};
context.vm.set_register(operands.dst.into(), value);
Ok(())
}
}
#[cold]
#[inline(never)]
fn call_math_slow(
operands: CallMathOperands,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let this = context.vm.get_register(operands.this.into()).clone();
let callee = context.vm.get_register(operands.callee.into()).clone();
let Some(function) = callee.as_callable() else {
return Err(JsNativeError::typ()
.with_message("not a callable function")
.into());
};
function.call(&this, args, context)
}
fn load_math_binding(index: IndexOperand, context: &mut Context) -> JsResult<JsValue> {
let slot = usize::from(index);
let is_global = context.vm.frame().code_block.bindings[slot].is_global();
if is_global && context.global_binding_is_static() {
return get_global_math(context);
}
let mut locator = context.vm.frame().code_block.bindings[slot].clone();
context.find_runtime_binding(&mut locator)?;
if locator.is_global() {
return get_global_math(context);
}
context.get_binding(&locator)?.ok_or_else(|| {
let name = locator.name().to_std_string_escaped();
JsNativeError::reference()
.with_message(format!("{name} is not defined"))
.into()
})
}
fn get_global_math(context: &mut Context) -> JsResult<JsValue> {
let object = context.global_object();
let key = PropertyKey::from(StaticJsStrings::MATH);
let property_context = &mut InternalMethodPropertyContext::new(context);
match object.__try_get__(&key, object.clone().into(), property_context)? {
Some(result) => Ok(result),
None => Err(JsNativeError::reference()
.with_message(format!("{key} is not defined"))
.into()),
}
}