use std::{cell::RefCell, mem::MaybeUninit};
use boa_string::JsString;
use dynify::Dynify;
use super::{IndexOperand, RegisterOperand};
use crate::{
Context, JsError, JsExpect, JsObject, JsResult, JsValue, NativeFunction,
builtins::{Promise, promise::PromiseCapability},
error::JsNativeError,
job::NativeAsyncJob,
module::{ImportAttribute, ModuleKind, ModuleRequest, Referrer},
object::FunctionObjectBuilder,
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;
}