Skip to content
Merged
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
6 changes: 5 additions & 1 deletion nova_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,11 @@ impl HostHooks for CliHostHooks {
m.into()
})
.map_err(|err| {
agent.throw_exception(ExceptionType::Error, err.first().unwrap().to_string(), gc)
agent.throw_exception(
ExceptionType::SyntaxError,
err.first().unwrap().to_string(),
gc,
)
});
finish_loading_imported_module(agent, referrer, module_request, payload, result, gc);
}
Expand Down
27 changes: 27 additions & 0 deletions nova_vm/src/ecmascript/execution/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,33 @@ pub trait HostHooks: core::fmt::Debug {
unimplemented!();
}

/// ### [16.2.1.12.1 HostGetSupportedImportAttributes ( )](https://tc39.es/ecma262/#sec-hostgetsupportedimportattributes)
///
/// The host-defined abstract operation HostGetSupportedImportAttributes
/// takes no arguments and returns a List of Strings. It allows host
/// environments to specify which import attributes they support. Only
/// attributes with supported keys will be provided to the host.
///
/// An implementation of HostGetSupportedImportAttributes must conform to
/// the following requirements:
///
/// * It must return a List of Strings, each indicating a supported
/// attribute.
/// * Each time this operation is called, it must return the same List with
/// the same contents in the same order.
///
/// The default implementation of HostGetSupportedImportAttributes is to
/// return a new empty List.
///
/// > Note: The purpose of requiring the host to specify its supported
/// > import attributes, rather than passing all attributes to the host and
/// > letting it then choose which ones it wants to handle, is to ensure
/// > that unsupported attributes are handled in a consistent way across
/// > different hosts.
fn get_supported_import_attributes(&self) -> &[&'static str] {
&[]
}

/// ### [13.3.12.1.1 HostGetImportMetaProperties ( moduleRecord )](https://tc39.es/ecma262/#sec-hostgetimportmetaproperties)
///
/// The host-defined abstract operation HostGetImportMetaProperties takes
Expand Down
252 changes: 208 additions & 44 deletions nova_vm/src/ecmascript/scripts_and_modules/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ use module_semantics::{

use crate::{
ecmascript::{
abstract_operations::type_conversion::{to_string, to_string_primitive},
abstract_operations::{
operations_on_objects::{
enumerable_own_properties, enumerable_properties_kind::EnumerateKeysAndValues, get,
},
type_conversion::to_string,
},
builtins::{
Array,
promise::Promise,
promise_objects::{
promise_abstract_operations::{
Expand All @@ -26,13 +32,16 @@ use crate::{
},
execution::{
Agent, JsResult,
agent::{get_active_script_or_module, unwrap_try},
agent::{ExceptionType, get_active_script_or_module, unwrap_try},
},
types::{IntoValue, Primitive, Value},
scripts_and_modules::module::module_semantics::all_import_attributes_supported,
types::{BUILTIN_STRING_MEMORY, IntoValue, Object, String, Value},
},
engine::{
Scoped,
context::{Bindable, GcScope, NoGcScope},
rootable::Scopable,
typeof_operator,
},
};
pub mod module_semantics;
Expand All @@ -54,63 +63,158 @@ pub(crate) fn evaluate_import_call<'gc>(
) -> Promise<'gc> {
let specifier = specifier.bind(gc.nogc());
let mut options = options.bind(gc.nogc());
if options.is_some_and(|opt| opt.is_undefined()) {
options.take();
}
// 7. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(agent, gc.nogc());
let scoped_promise = promise_capability.promise.scope(agent, gc.nogc());
// 8. Let specifierString be Completion(ToString(specifier)).
let specifier = if let Ok(specifier) = Primitive::try_from(specifier) {
to_string_primitive(agent, specifier, gc.nogc())
.unbind()
.bind(gc.nogc())
let specifier = if let Ok(specifier) = String::try_from(specifier) {
specifier
} else {
let scoped_options = options.map(|o| o.scope(agent, gc.nogc()));
let specifier = to_string(agent, specifier.unbind(), gc.reborrow())
.unbind()
.bind(gc.nogc());
// SAFETY: not shared.
options = scoped_options.map(|o| unsafe { o.take(agent) }.bind(gc.nogc()));
specifier
// 9. IfAbruptRejectPromise(specifierString, promiseCapability).
let promise_capability = PromiseCapability {
promise: scoped_promise.get(agent).bind(gc.nogc()),
must_be_unresolved: true,
};
if_abrupt_reject_promise_m!(agent, specifier, promise_capability, gc)
};
// 9. IfAbruptRejectPromise(specifierString, promiseCapability).
let promise_capability = PromiseCapability {
promise: scoped_promise.get(agent).bind(gc.nogc()),
must_be_unresolved: true,
};
let specifier = if_abrupt_reject_promise_m!(agent, specifier, promise_capability, gc);
// 10. Let attributes be a new empty List.
let attributes: Vec<ImportAttributeRecord> = vec![];
// 11. If options is not undefined, then
if let Some(_options) = options {
let (promise, specifier, attributes, gc) = if let Some(options) = options {
// a. If options is not an Object, then
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
// ii. Return promiseCapability.[[Promise]].
let Ok(options) = Object::try_from(options) else {
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
// ii. Return promiseCapability.[[Promise]].
return reject_import_not_object_or_undefined(
agent,
scoped_promise,
options.unbind(),
gc.into_nogc(),
);
};
let specifier = specifier.scope(agent, gc.nogc());
// b. Let attributesObj be Completion(Get(options, "with")).
let attributes_obj = get(
agent,
options.unbind(),
BUILTIN_STRING_MEMORY.with.to_property_key(),
gc.reborrow(),
)
.unbind()
.bind(gc.nogc());

let promise_capability = PromiseCapability {
promise: scoped_promise.get(agent).bind(gc.nogc()),
must_be_unresolved: true,
};
// c. IfAbruptRejectPromise(attributesObj, promiseCapability).
let attributes_obj =
if_abrupt_reject_promise_m!(agent, attributes_obj, promise_capability, gc);
// d. If attributesObj is not undefined, then
// i. If attributesObj is not an Object, then
// 1. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
// 2. Return promiseCapability.[[Promise]].
// ii. Let entries be Completion(EnumerableOwnProperties(attributesObj, key+value)).
// iii. IfAbruptRejectPromise(entries, promiseCapability).
// iv. For each element entry of entries, do
// 1. Let key be ! Get(entry, "0").
// 2. Let value be ! Get(entry, "1").
// 3. If key is a String, then
// a. If value is not a String, then
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
// ii. Return promiseCapability.[[Promise]].
// b. Append the ImportAttribute Record { [[Key]]: key, [[Value]]: value } to attributes.
// e. If AllImportAttributesSupported(attributes) is false, then
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
// ii. Return promiseCapability.[[Promise]].
// f. Sort attributes according to the lexicographic order of their [[Key]] field, treating the value of each such field as a sequence of UTF-16 code unit values. NOTE: This sorting is observable only in that hosts are prohibited from changing behaviour based on the order in which attributes are enumerated.
todo!()
}
let specifier = specifier.unbind();
let attributes = attributes.unbind();
let gc = gc.into_nogc();
let specifier = specifier.bind(gc);
let attributes = attributes.bind(gc);
if !attributes_obj.is_undefined() {
// i. If attributesObj is not an Object, then
let Ok(attributes_obj) = Object::try_from(attributes_obj) else {
// 1. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
// 2. Return promiseCapability.[[Promise]].
return reject_import_not_object_or_undefined(
agent,
scoped_promise,
attributes_obj.unbind(),
gc.into_nogc(),
);
};
// ii. Let entries be Completion(EnumerableOwnProperties(attributesObj, key+value)).
let entries = enumerable_own_properties::<EnumerateKeysAndValues>(
agent,
attributes_obj.unbind(),
gc.reborrow(),
)
.unbind();
let gc = gc.into_nogc();
let entries = entries.bind(gc);

let promise = unsafe { scoped_promise.take(agent) }.bind(gc);
let promise_capability = PromiseCapability {
promise,
must_be_unresolved: true,
};
// iii. IfAbruptRejectPromise(entries, promiseCapability).
// 1. Assert: value is a Completion Record.
let entries = match entries {
// 2. If value is an abrupt completion, then
Err(err) => {
// a. Perform ? Call(capability.[[Reject]], undefined, « value.[[Value]] »).
promise_capability.reject(agent, err.value().unbind(), gc);
// b. Return capability.[[Promise]].
return promise_capability.promise;
}
// 3. Else,
Ok(value) => {
// a. Set value to ! value.
value
}
};
let mut attributes: Vec<ImportAttributeRecord> = Vec::with_capacity(entries.len());
// iv. For each element entry of entries, do
for entry in entries {
let entry = Array::try_from(entry).unwrap();
let entry = entry.get_storage(agent).values;
// 1. Let key be ! Get(entry, "0").
let key = entry[0].unwrap();
// 2. Let value be ! Get(entry, "1").
let value = entry[0].unwrap();
// 3. If key is a String, then
if let Ok(key) = String::try_from(key) {
// a. If value is not a String, then
let Ok(value) = String::try_from(value) else {
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
// ii. Return promiseCapability.[[Promise]].
return reject_unsupported_import_attribute(
agent,
promise_capability,
key.unbind(),
gc.into_nogc(),
);
};
// b. Append the ImportAttribute Record { [[Key]]: key, [[Value]]: value } to attributes.
attributes.push(ImportAttributeRecord { key, value });
}
}
// e. If AllImportAttributesSupported(attributes) is false, then
if !all_import_attributes_supported(agent, &attributes) {
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
// ii. Return promiseCapability.[[Promise]].
return reject_unsupported_import_attributes(agent, promise_capability, gc);
}
// f. Sort attributes according to the lexicographic order of their
// [[Key]] field, treating the value of each such field as a sequence
// of UTF-16 code unit values. NOTE: This sorting is observable only
// in that hosts are prohibited from changing behaviour based on the
// order in which attributes are enumerated.
attributes.sort_by(|a, b| a.key.as_wtf8(agent).cmp(b.key.as_wtf8(agent)));
let specifier = unsafe { specifier.take(agent) }.bind(gc);
(promise, specifier, attributes.into_boxed_slice(), gc)
} else {
let gc = gc.into_nogc();
let specifier = unsafe { specifier.take(agent) }.bind(gc);
let promise = unsafe { scoped_promise.take(agent) }.bind(gc);
(promise, specifier, Default::default(), gc)
}
} else {
let specifier = specifier.unbind();
let gc = gc.into_nogc();
let specifier = specifier.bind(gc);
let promise = unsafe { scoped_promise.take(agent) }.bind(gc);
(promise, specifier, Default::default(), gc)
};
// 12. Let moduleRequest be a new ModuleRequest Record {
let module_request = ModuleRequest::new_dynamic(
agent, // [[Specifier]]: specifierString,
Expand All @@ -125,8 +229,6 @@ pub(crate) fn evaluate_import_call<'gc>(
.unwrap_or_else(|| agent.current_realm(gc).into());
// 13. Perform HostLoadImportedModule(referrer, moduleRequest, empty, promiseCapability).
// Note: this is against the spec. We'll fix it in post.
// SAFETY: scoped_promise is not shared.
let promise = unsafe { scoped_promise.take(agent) }.bind(gc);
let mut payload = GraphLoadingStateRecord::from_promise(promise);
agent
.host_hooks
Expand All @@ -135,6 +237,68 @@ pub(crate) fn evaluate_import_call<'gc>(
promise
}

#[cold]
#[inline(never)]
fn reject_import_not_object_or_undefined<'gc>(
agent: &mut Agent,
scoped_promise: Scoped<Promise>,
value: Value,
gc: NoGcScope<'gc, '_>,
) -> Promise<'gc> {
let value = value.bind(gc);
let promise_capability = PromiseCapability {
// SAFETY: not shared.
promise: unsafe { scoped_promise.take(agent) }.bind(gc),
must_be_unresolved: true,
};
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
let message = format!(
"import: expected object or undefined, got {}",
typeof_operator(agent, value, gc).to_string_lossy(agent)
);
let error = agent.throw_exception(ExceptionType::TypeError, message, gc);
promise_capability.reject(agent, error.value(), gc);
// ii. Return promiseCapability.[[Promise]].
promise_capability.promise
}

#[cold]
#[inline(never)]
fn reject_unsupported_import_attribute<'gc>(
agent: &mut Agent,
promise_capability: PromiseCapability<'gc>,
key: String<'gc>,
gc: NoGcScope<'gc, '_>,
) -> Promise<'gc> {
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
let message = format!(
"Unsupported import attribute: {}",
key.to_string_lossy(agent)
);
let error = agent.throw_exception(ExceptionType::TypeError, message, gc);
promise_capability.reject(agent, error.value(), gc);
// ii. Return promiseCapability.[[Promise]].
promise_capability.promise
}

#[cold]
#[inline(never)]
fn reject_unsupported_import_attributes<'gc>(
agent: &mut Agent,
promise_capability: PromiseCapability<'gc>,
gc: NoGcScope<'gc, '_>,
) -> Promise<'gc> {
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
let error = agent.throw_exception_with_static_message(
ExceptionType::TypeError,
"Unsupported import attributes",
gc,
);
promise_capability.reject(agent, error.value(), gc);
// ii. Return promiseCapability.[[Promise]].
promise_capability.promise
}

/// ### [13.3.10.3 ContinueDynamicImport ( promiseCapability, moduleCompletion )](https://tc39.es/ecma262/#sec-ContinueDynamicImport)
///
/// The abstract operation ContinueDynamicImport takes arguments
Expand Down
Loading