Skip to content

Commit ba9ed50

Browse files
authored
feat(ecmascript): ImportCall attributes (#924)
1 parent 72a1d45 commit ba9ed50

File tree

8 files changed

+273
-98
lines changed

8 files changed

+273
-98
lines changed

nova_cli/src/main.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,11 @@ impl HostHooks for CliHostHooks {
282282
m.into()
283283
})
284284
.map_err(|err| {
285-
agent.throw_exception(ExceptionType::Error, err.first().unwrap().to_string(), gc)
285+
agent.throw_exception(
286+
ExceptionType::SyntaxError,
287+
err.first().unwrap().to_string(),
288+
gc,
289+
)
286290
});
287291
finish_loading_imported_module(agent, referrer, module_request, payload, result, gc);
288292
}

nova_vm/src/ecmascript/execution/agent.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,33 @@ pub trait HostHooks: core::fmt::Debug {
506506
unimplemented!();
507507
}
508508

509+
/// ### [16.2.1.12.1 HostGetSupportedImportAttributes ( )](https://tc39.es/ecma262/#sec-hostgetsupportedimportattributes)
510+
///
511+
/// The host-defined abstract operation HostGetSupportedImportAttributes
512+
/// takes no arguments and returns a List of Strings. It allows host
513+
/// environments to specify which import attributes they support. Only
514+
/// attributes with supported keys will be provided to the host.
515+
///
516+
/// An implementation of HostGetSupportedImportAttributes must conform to
517+
/// the following requirements:
518+
///
519+
/// * It must return a List of Strings, each indicating a supported
520+
/// attribute.
521+
/// * Each time this operation is called, it must return the same List with
522+
/// the same contents in the same order.
523+
///
524+
/// The default implementation of HostGetSupportedImportAttributes is to
525+
/// return a new empty List.
526+
///
527+
/// > Note: The purpose of requiring the host to specify its supported
528+
/// > import attributes, rather than passing all attributes to the host and
529+
/// > letting it then choose which ones it wants to handle, is to ensure
530+
/// > that unsupported attributes are handled in a consistent way across
531+
/// > different hosts.
532+
fn get_supported_import_attributes(&self) -> &[&'static str] {
533+
&[]
534+
}
535+
509536
/// ### [13.3.12.1.1 HostGetImportMetaProperties ( moduleRecord )](https://tc39.es/ecma262/#sec-hostgetimportmetaproperties)
510537
///
511538
/// The host-defined abstract operation HostGetImportMetaProperties takes

nova_vm/src/ecmascript/scripts_and_modules/module.rs

Lines changed: 208 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@ use module_semantics::{
1313

1414
use crate::{
1515
ecmascript::{
16-
abstract_operations::type_conversion::{to_string, to_string_primitive},
16+
abstract_operations::{
17+
operations_on_objects::{
18+
enumerable_own_properties, enumerable_properties_kind::EnumerateKeysAndValues, get,
19+
},
20+
type_conversion::to_string,
21+
},
1722
builtins::{
23+
Array,
1824
promise::Promise,
1925
promise_objects::{
2026
promise_abstract_operations::{
@@ -26,13 +32,16 @@ use crate::{
2632
},
2733
execution::{
2834
Agent, JsResult,
29-
agent::{get_active_script_or_module, unwrap_try},
35+
agent::{ExceptionType, get_active_script_or_module, unwrap_try},
3036
},
31-
types::{IntoValue, Primitive, Value},
37+
scripts_and_modules::module::module_semantics::all_import_attributes_supported,
38+
types::{BUILTIN_STRING_MEMORY, IntoValue, Object, String, Value},
3239
},
3340
engine::{
41+
Scoped,
3442
context::{Bindable, GcScope, NoGcScope},
3543
rootable::Scopable,
44+
typeof_operator,
3645
},
3746
};
3847
pub mod module_semantics;
@@ -54,63 +63,158 @@ pub(crate) fn evaluate_import_call<'gc>(
5463
) -> Promise<'gc> {
5564
let specifier = specifier.bind(gc.nogc());
5665
let mut options = options.bind(gc.nogc());
66+
if options.is_some_and(|opt| opt.is_undefined()) {
67+
options.take();
68+
}
5769
// 7. Let promiseCapability be ! NewPromiseCapability(%Promise%).
5870
let promise_capability = PromiseCapability::new(agent, gc.nogc());
5971
let scoped_promise = promise_capability.promise.scope(agent, gc.nogc());
6072
// 8. Let specifierString be Completion(ToString(specifier)).
61-
let specifier = if let Ok(specifier) = Primitive::try_from(specifier) {
62-
to_string_primitive(agent, specifier, gc.nogc())
63-
.unbind()
64-
.bind(gc.nogc())
73+
let specifier = if let Ok(specifier) = String::try_from(specifier) {
74+
specifier
6575
} else {
6676
let scoped_options = options.map(|o| o.scope(agent, gc.nogc()));
6777
let specifier = to_string(agent, specifier.unbind(), gc.reborrow())
6878
.unbind()
6979
.bind(gc.nogc());
7080
// SAFETY: not shared.
7181
options = scoped_options.map(|o| unsafe { o.take(agent) }.bind(gc.nogc()));
72-
specifier
82+
// 9. IfAbruptRejectPromise(specifierString, promiseCapability).
83+
let promise_capability = PromiseCapability {
84+
promise: scoped_promise.get(agent).bind(gc.nogc()),
85+
must_be_unresolved: true,
86+
};
87+
if_abrupt_reject_promise_m!(agent, specifier, promise_capability, gc)
7388
};
74-
// 9. IfAbruptRejectPromise(specifierString, promiseCapability).
75-
let promise_capability = PromiseCapability {
76-
promise: scoped_promise.get(agent).bind(gc.nogc()),
77-
must_be_unresolved: true,
78-
};
79-
let specifier = if_abrupt_reject_promise_m!(agent, specifier, promise_capability, gc);
8089
// 10. Let attributes be a new empty List.
81-
let attributes: Vec<ImportAttributeRecord> = vec![];
8290
// 11. If options is not undefined, then
83-
if let Some(_options) = options {
91+
let (promise, specifier, attributes, gc) = if let Some(options) = options {
8492
// a. If options is not an Object, then
85-
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
86-
// ii. Return promiseCapability.[[Promise]].
93+
let Ok(options) = Object::try_from(options) else {
94+
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
95+
// ii. Return promiseCapability.[[Promise]].
96+
return reject_import_not_object_or_undefined(
97+
agent,
98+
scoped_promise,
99+
options.unbind(),
100+
gc.into_nogc(),
101+
);
102+
};
103+
let specifier = specifier.scope(agent, gc.nogc());
87104
// b. Let attributesObj be Completion(Get(options, "with")).
105+
let attributes_obj = get(
106+
agent,
107+
options.unbind(),
108+
BUILTIN_STRING_MEMORY.with.to_property_key(),
109+
gc.reborrow(),
110+
)
111+
.unbind()
112+
.bind(gc.nogc());
113+
114+
let promise_capability = PromiseCapability {
115+
promise: scoped_promise.get(agent).bind(gc.nogc()),
116+
must_be_unresolved: true,
117+
};
88118
// c. IfAbruptRejectPromise(attributesObj, promiseCapability).
119+
let attributes_obj =
120+
if_abrupt_reject_promise_m!(agent, attributes_obj, promise_capability, gc);
89121
// d. If attributesObj is not undefined, then
90-
// i. If attributesObj is not an Object, then
91-
// 1. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
92-
// 2. Return promiseCapability.[[Promise]].
93-
// ii. Let entries be Completion(EnumerableOwnProperties(attributesObj, key+value)).
94-
// iii. IfAbruptRejectPromise(entries, promiseCapability).
95-
// iv. For each element entry of entries, do
96-
// 1. Let key be ! Get(entry, "0").
97-
// 2. Let value be ! Get(entry, "1").
98-
// 3. If key is a String, then
99-
// a. If value is not a String, then
100-
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
101-
// ii. Return promiseCapability.[[Promise]].
102-
// b. Append the ImportAttribute Record { [[Key]]: key, [[Value]]: value } to attributes.
103-
// e. If AllImportAttributesSupported(attributes) is false, then
104-
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
105-
// ii. Return promiseCapability.[[Promise]].
106-
// 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.
107-
todo!()
108-
}
109-
let specifier = specifier.unbind();
110-
let attributes = attributes.unbind();
111-
let gc = gc.into_nogc();
112-
let specifier = specifier.bind(gc);
113-
let attributes = attributes.bind(gc);
122+
if !attributes_obj.is_undefined() {
123+
// i. If attributesObj is not an Object, then
124+
let Ok(attributes_obj) = Object::try_from(attributes_obj) else {
125+
// 1. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
126+
// 2. Return promiseCapability.[[Promise]].
127+
return reject_import_not_object_or_undefined(
128+
agent,
129+
scoped_promise,
130+
attributes_obj.unbind(),
131+
gc.into_nogc(),
132+
);
133+
};
134+
// ii. Let entries be Completion(EnumerableOwnProperties(attributesObj, key+value)).
135+
let entries = enumerable_own_properties::<EnumerateKeysAndValues>(
136+
agent,
137+
attributes_obj.unbind(),
138+
gc.reborrow(),
139+
)
140+
.unbind();
141+
let gc = gc.into_nogc();
142+
let entries = entries.bind(gc);
143+
144+
let promise = unsafe { scoped_promise.take(agent) }.bind(gc);
145+
let promise_capability = PromiseCapability {
146+
promise,
147+
must_be_unresolved: true,
148+
};
149+
// iii. IfAbruptRejectPromise(entries, promiseCapability).
150+
// 1. Assert: value is a Completion Record.
151+
let entries = match entries {
152+
// 2. If value is an abrupt completion, then
153+
Err(err) => {
154+
// a. Perform ? Call(capability.[[Reject]], undefined, « value.[[Value]] »).
155+
promise_capability.reject(agent, err.value().unbind(), gc);
156+
// b. Return capability.[[Promise]].
157+
return promise_capability.promise;
158+
}
159+
// 3. Else,
160+
Ok(value) => {
161+
// a. Set value to ! value.
162+
value
163+
}
164+
};
165+
let mut attributes: Vec<ImportAttributeRecord> = Vec::with_capacity(entries.len());
166+
// iv. For each element entry of entries, do
167+
for entry in entries {
168+
let entry = Array::try_from(entry).unwrap();
169+
let entry = entry.get_storage(agent).values;
170+
// 1. Let key be ! Get(entry, "0").
171+
let key = entry[0].unwrap();
172+
// 2. Let value be ! Get(entry, "1").
173+
let value = entry[0].unwrap();
174+
// 3. If key is a String, then
175+
if let Ok(key) = String::try_from(key) {
176+
// a. If value is not a String, then
177+
let Ok(value) = String::try_from(value) else {
178+
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
179+
// ii. Return promiseCapability.[[Promise]].
180+
return reject_unsupported_import_attribute(
181+
agent,
182+
promise_capability,
183+
key.unbind(),
184+
gc.into_nogc(),
185+
);
186+
};
187+
// b. Append the ImportAttribute Record { [[Key]]: key, [[Value]]: value } to attributes.
188+
attributes.push(ImportAttributeRecord { key, value });
189+
}
190+
}
191+
// e. If AllImportAttributesSupported(attributes) is false, then
192+
if !all_import_attributes_supported(agent, &attributes) {
193+
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
194+
// ii. Return promiseCapability.[[Promise]].
195+
return reject_unsupported_import_attributes(agent, promise_capability, gc);
196+
}
197+
// f. Sort attributes according to the lexicographic order of their
198+
// [[Key]] field, treating the value of each such field as a sequence
199+
// of UTF-16 code unit values. NOTE: This sorting is observable only
200+
// in that hosts are prohibited from changing behaviour based on the
201+
// order in which attributes are enumerated.
202+
attributes.sort_by(|a, b| a.key.as_wtf8(agent).cmp(b.key.as_wtf8(agent)));
203+
let specifier = unsafe { specifier.take(agent) }.bind(gc);
204+
(promise, specifier, attributes.into_boxed_slice(), gc)
205+
} else {
206+
let gc = gc.into_nogc();
207+
let specifier = unsafe { specifier.take(agent) }.bind(gc);
208+
let promise = unsafe { scoped_promise.take(agent) }.bind(gc);
209+
(promise, specifier, Default::default(), gc)
210+
}
211+
} else {
212+
let specifier = specifier.unbind();
213+
let gc = gc.into_nogc();
214+
let specifier = specifier.bind(gc);
215+
let promise = unsafe { scoped_promise.take(agent) }.bind(gc);
216+
(promise, specifier, Default::default(), gc)
217+
};
114218
// 12. Let moduleRequest be a new ModuleRequest Record {
115219
let module_request = ModuleRequest::new_dynamic(
116220
agent, // [[Specifier]]: specifierString,
@@ -125,8 +229,6 @@ pub(crate) fn evaluate_import_call<'gc>(
125229
.unwrap_or_else(|| agent.current_realm(gc).into());
126230
// 13. Perform HostLoadImportedModule(referrer, moduleRequest, empty, promiseCapability).
127231
// Note: this is against the spec. We'll fix it in post.
128-
// SAFETY: scoped_promise is not shared.
129-
let promise = unsafe { scoped_promise.take(agent) }.bind(gc);
130232
let mut payload = GraphLoadingStateRecord::from_promise(promise);
131233
agent
132234
.host_hooks
@@ -135,6 +237,68 @@ pub(crate) fn evaluate_import_call<'gc>(
135237
promise
136238
}
137239

240+
#[cold]
241+
#[inline(never)]
242+
fn reject_import_not_object_or_undefined<'gc>(
243+
agent: &mut Agent,
244+
scoped_promise: Scoped<Promise>,
245+
value: Value,
246+
gc: NoGcScope<'gc, '_>,
247+
) -> Promise<'gc> {
248+
let value = value.bind(gc);
249+
let promise_capability = PromiseCapability {
250+
// SAFETY: not shared.
251+
promise: unsafe { scoped_promise.take(agent) }.bind(gc),
252+
must_be_unresolved: true,
253+
};
254+
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
255+
let message = format!(
256+
"import: expected object or undefined, got {}",
257+
typeof_operator(agent, value, gc).to_string_lossy(agent)
258+
);
259+
let error = agent.throw_exception(ExceptionType::TypeError, message, gc);
260+
promise_capability.reject(agent, error.value(), gc);
261+
// ii. Return promiseCapability.[[Promise]].
262+
promise_capability.promise
263+
}
264+
265+
#[cold]
266+
#[inline(never)]
267+
fn reject_unsupported_import_attribute<'gc>(
268+
agent: &mut Agent,
269+
promise_capability: PromiseCapability<'gc>,
270+
key: String<'gc>,
271+
gc: NoGcScope<'gc, '_>,
272+
) -> Promise<'gc> {
273+
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
274+
let message = format!(
275+
"Unsupported import attribute: {}",
276+
key.to_string_lossy(agent)
277+
);
278+
let error = agent.throw_exception(ExceptionType::TypeError, message, gc);
279+
promise_capability.reject(agent, error.value(), gc);
280+
// ii. Return promiseCapability.[[Promise]].
281+
promise_capability.promise
282+
}
283+
284+
#[cold]
285+
#[inline(never)]
286+
fn reject_unsupported_import_attributes<'gc>(
287+
agent: &mut Agent,
288+
promise_capability: PromiseCapability<'gc>,
289+
gc: NoGcScope<'gc, '_>,
290+
) -> Promise<'gc> {
291+
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
292+
let error = agent.throw_exception_with_static_message(
293+
ExceptionType::TypeError,
294+
"Unsupported import attributes",
295+
gc,
296+
);
297+
promise_capability.reject(agent, error.value(), gc);
298+
// ii. Return promiseCapability.[[Promise]].
299+
promise_capability.promise
300+
}
301+
138302
/// ### [13.3.10.3 ContinueDynamicImport ( promiseCapability, moduleCompletion )](https://tc39.es/ecma262/#sec-ContinueDynamicImport)
139303
///
140304
/// The abstract operation ContinueDynamicImport takes arguments

0 commit comments

Comments
 (0)