Skip to content

Commit 0913629

Browse files
authored
Implement Array.fromAsync (#4115)
* Implement `Array.fromAsync` * Add state machine methods * Polish documentation * cargo clippy fix * fix hyperlinks * Fix typo
1 parent b2ba5f6 commit 0913629

File tree

9 files changed

+883
-13
lines changed

9 files changed

+883
-13
lines changed

core/engine/src/builtins/array/from_async.rs

Lines changed: 622 additions & 0 deletions
Large diffs are not rendered by default.

core/engine/src/builtins/array/mod.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ mod array_iterator;
4141
use crate::value::JsVariant;
4242
pub(crate) use array_iterator::ArrayIterator;
4343

44+
#[cfg(feature = "experimental")]
45+
mod from_async;
46+
4447
#[cfg(test)]
4548
mod tests;
4649

@@ -106,7 +109,7 @@ impl IntrinsicObject for Array {
106109

107110
let unscopables_object = Self::unscopables_object();
108111

109-
BuiltInBuilder::from_standard_constructor::<Self>(realm)
112+
let builder = BuiltInBuilder::from_standard_constructor::<Self>(realm)
110113
// Static Methods
111114
.static_method(Self::from, js_string!("from"), 1)
112115
.static_method(Self::is_array, js_string!("isArray"), 1)
@@ -177,8 +180,12 @@ impl IntrinsicObject for Array {
177180
symbol_unscopables,
178181
unscopables_object,
179182
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
180-
)
181-
.build();
183+
);
184+
185+
#[cfg(feature = "experimental")]
186+
let builder = builder.static_method(Self::from_async, js_string!("fromAsync"), 1);
187+
188+
builder.build();
182189
}
183190

184191
fn get(intrinsics: &Intrinsics) -> JsObject {

core/engine/src/module/synthetic.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ pub struct SyntheticModuleInitializer {
5858

5959
impl std::fmt::Debug for SyntheticModuleInitializer {
6060
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61-
f.debug_struct("ModuleInitializer").finish_non_exhaustive()
61+
f.debug_struct("SyntheticModuleInitializer")
62+
.finish_non_exhaustive()
6263
}
6364
}
6465

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use boa_gc::{Finalize, Gc, Trace};
2+
3+
use crate::{Context, JsResult, JsValue};
4+
5+
#[derive(Trace, Finalize)]
6+
#[boa_gc(unsafe_no_drop)]
7+
pub(crate) enum CoroutineState {
8+
Yielded(JsValue),
9+
Done,
10+
}
11+
12+
trait TraceableCoroutine: Trace {
13+
fn call(&self, value: JsResult<JsValue>, context: &mut Context) -> CoroutineState;
14+
}
15+
16+
#[derive(Trace, Finalize)]
17+
struct Coroutine<F, T>
18+
where
19+
F: Fn(JsResult<JsValue>, &T, &mut Context) -> CoroutineState,
20+
T: Trace,
21+
{
22+
// SAFETY: `NativeCoroutine`'s safe API ensures only `Copy` closures are stored; its unsafe API,
23+
// on the other hand, explains the invariants to hold in order for this to be safe, shifting
24+
// the responsibility to the caller.
25+
#[unsafe_ignore_trace]
26+
f: F,
27+
captures: T,
28+
}
29+
30+
impl<F, T> TraceableCoroutine for Coroutine<F, T>
31+
where
32+
F: Fn(JsResult<JsValue>, &T, &mut Context) -> CoroutineState,
33+
T: Trace,
34+
{
35+
fn call(&self, result: JsResult<JsValue>, context: &mut Context) -> CoroutineState {
36+
(self.f)(result, &self.captures, context)
37+
}
38+
}
39+
40+
/// A callable Rust coroutine that can be used to await promises.
41+
///
42+
/// # Caveats
43+
///
44+
/// By limitations of the Rust language, the garbage collector currently cannot inspect closures
45+
/// in order to trace their captured variables. This means that only [`Copy`] closures are 100% safe
46+
/// to use. All other closures can also be stored in a `NativeCoroutine`, albeit by using an `unsafe`
47+
/// API, but note that passing closures implicitly capturing traceable types could cause
48+
/// **Undefined Behaviour**.
49+
#[derive(Clone, Trace, Finalize)]
50+
pub(crate) struct NativeCoroutine {
51+
inner: Gc<dyn TraceableCoroutine>,
52+
}
53+
54+
impl std::fmt::Debug for NativeCoroutine {
55+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56+
f.debug_struct("NativeCoroutine").finish_non_exhaustive()
57+
}
58+
}
59+
60+
impl NativeCoroutine {
61+
/// Creates a `NativeCoroutine` from a `Copy` closure and a list of traceable captures.
62+
pub(crate) fn from_copy_closure_with_captures<F, T>(closure: F, captures: T) -> Self
63+
where
64+
F: Fn(JsResult<JsValue>, &T, &mut Context) -> CoroutineState + Copy + 'static,
65+
T: Trace + 'static,
66+
{
67+
// SAFETY: The `Copy` bound ensures there are no traceable types inside the closure.
68+
unsafe { Self::from_closure_with_captures(closure, captures) }
69+
}
70+
71+
/// Create a new `NativeCoroutine` from a closure and a list of traceable captures.
72+
///
73+
/// # Safety
74+
///
75+
/// Passing a closure that contains a captured variable that needs to be traced by the garbage
76+
/// collector could cause an use after free, memory corruption or other kinds of **Undefined
77+
/// Behaviour**. See <https://github.com/Manishearth/rust-gc/issues/50> for a technical explanation
78+
/// on why that is the case.
79+
pub(crate) unsafe fn from_closure_with_captures<F, T>(closure: F, captures: T) -> Self
80+
where
81+
F: Fn(JsResult<JsValue>, &T, &mut Context) -> CoroutineState + 'static,
82+
T: Trace + 'static,
83+
{
84+
// Hopefully, this unsafe operation will be replaced by the `CoerceUnsized` API in the
85+
// future: https://github.com/rust-lang/rust/issues/18598
86+
let ptr = Gc::into_raw(Gc::new(Coroutine {
87+
f: closure,
88+
captures,
89+
}));
90+
// SAFETY: The pointer returned by `into_raw` is only used to coerce to a trait object,
91+
// meaning this is safe.
92+
unsafe {
93+
Self {
94+
inner: Gc::from_raw(ptr),
95+
}
96+
}
97+
}
98+
99+
/// Calls this `NativeCoroutine`, forwarding the arguments to the corresponding function.
100+
#[inline]
101+
pub(crate) fn call(&self, result: JsResult<JsValue>, context: &mut Context) -> CoroutineState {
102+
self.inner.call(result, context)
103+
}
104+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ use crate::{
2020
Context, JsNativeError, JsObject, JsResult, JsValue,
2121
};
2222

23+
#[cfg(feature = "experimental")]
24+
mod continuation;
25+
26+
#[cfg(feature = "experimental")]
27+
pub(crate) use continuation::{CoroutineState, NativeCoroutine};
28+
2329
/// The required signature for all native built-in function pointers.
2430
///
2531
/// # Arguments

core/engine/src/object/builtins/jspromise.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,139 @@ impl JsPromise {
11541154
}
11551155
}
11561156
}
1157+
1158+
#[cfg(feature = "experimental")]
1159+
pub(crate) fn await_native(
1160+
&self,
1161+
continuation: crate::native_function::NativeCoroutine,
1162+
context: &mut Context,
1163+
) {
1164+
use crate::{
1165+
builtins::{async_generator::AsyncGenerator, generator::GeneratorContext},
1166+
js_string,
1167+
object::FunctionObjectBuilder,
1168+
};
1169+
use std::cell::Cell;
1170+
1171+
let mut frame = context.vm.frame().clone();
1172+
frame.environments = context.vm.environments.clone();
1173+
frame.realm = context.realm().clone();
1174+
1175+
let gen_ctx = GeneratorContext {
1176+
call_frame: Some(frame),
1177+
stack: context.vm.stack.clone(),
1178+
};
1179+
1180+
// 3. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures asyncContext and performs the following steps when called:
1181+
// 4. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »).
1182+
let on_fulfilled = FunctionObjectBuilder::new(
1183+
context.realm(),
1184+
NativeFunction::from_copy_closure_with_captures(
1185+
|_this, args, captures, context| {
1186+
// a. Let prevContext be the running execution context.
1187+
// b. Suspend prevContext.
1188+
// c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context.
1189+
// d. Resume the suspended evaluation of asyncContext using NormalCompletion(value) as the result of the operation that suspended it.
1190+
let continuation = &captures.0;
1191+
let mut gen = captures.1.take().expect("should only run once");
1192+
1193+
// NOTE: We need to get the object before resuming, since it could clear the stack.
1194+
let async_generator = gen.async_generator_object();
1195+
1196+
std::mem::swap(&mut context.vm.stack, &mut gen.stack);
1197+
let frame = gen.call_frame.take().expect("should have a call frame");
1198+
context.vm.push_frame(frame);
1199+
1200+
if let crate::native_function::CoroutineState::Yielded(value) =
1201+
continuation.call(Ok(args.get_or_undefined(0).clone()), context)
1202+
{
1203+
JsPromise::resolve(value, context)
1204+
.await_native(continuation.clone(), context);
1205+
}
1206+
1207+
std::mem::swap(&mut context.vm.stack, &mut gen.stack);
1208+
gen.call_frame = context.vm.pop_frame();
1209+
assert!(gen.call_frame.is_some());
1210+
1211+
if let Some(async_generator) = async_generator {
1212+
async_generator
1213+
.downcast_mut::<AsyncGenerator>()
1214+
.expect("must be async generator")
1215+
.context = Some(gen);
1216+
}
1217+
1218+
// e. Assert: When we reach this step, asyncContext has already been removed from the execution context stack and prevContext is the currently running execution context.
1219+
// f. Return undefined.
1220+
Ok(JsValue::undefined())
1221+
},
1222+
(continuation.clone(), Cell::new(Some(gen_ctx.clone()))),
1223+
),
1224+
)
1225+
.name(js_string!())
1226+
.length(1)
1227+
.build();
1228+
1229+
// 5. Let rejectedClosure be a new Abstract Closure with parameters (reason) that captures asyncContext and performs the following steps when called:
1230+
// 6. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »).
1231+
let on_rejected = FunctionObjectBuilder::new(
1232+
context.realm(),
1233+
NativeFunction::from_copy_closure_with_captures(
1234+
|_this, args, captures, context| {
1235+
// a. Let prevContext be the running execution context.
1236+
// b. Suspend prevContext.
1237+
// c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context.
1238+
// d. Resume the suspended evaluation of asyncContext using ThrowCompletion(reason) as the result of the operation that suspended it.
1239+
// e. Assert: When we reach this step, asyncContext has already been removed from the execution context stack and prevContext is the currently running execution context.
1240+
// f. Return undefined.
1241+
let continuation = &captures.0;
1242+
let mut gen = captures.1.take().expect("should only run once");
1243+
1244+
// NOTE: We need to get the object before resuming, since it could clear the stack.
1245+
let async_generator = gen.async_generator_object();
1246+
1247+
std::mem::swap(&mut context.vm.stack, &mut gen.stack);
1248+
let frame = gen.call_frame.take().expect("should have a call frame");
1249+
context.vm.push_frame(frame);
1250+
1251+
if let crate::native_function::CoroutineState::Yielded(value) = continuation
1252+
.call(
1253+
Err(JsError::from_opaque(args.get_or_undefined(0).clone())),
1254+
context,
1255+
)
1256+
{
1257+
JsPromise::resolve(value, context)
1258+
.await_native(continuation.clone(), context);
1259+
}
1260+
1261+
std::mem::swap(&mut context.vm.stack, &mut gen.stack);
1262+
gen.call_frame = context.vm.pop_frame();
1263+
assert!(gen.call_frame.is_some());
1264+
1265+
if let Some(async_generator) = async_generator {
1266+
async_generator
1267+
.downcast_mut::<AsyncGenerator>()
1268+
.expect("must be async generator")
1269+
.context = Some(gen);
1270+
}
1271+
1272+
Ok(JsValue::undefined())
1273+
},
1274+
(continuation, Cell::new(Some(gen_ctx))),
1275+
),
1276+
)
1277+
.name(js_string!())
1278+
.length(1)
1279+
.build();
1280+
1281+
// 7. Perform PerformPromiseThen(promise, onFulfilled, onRejected).
1282+
Promise::perform_promise_then(
1283+
&self.inner,
1284+
Some(on_fulfilled),
1285+
Some(on_rejected),
1286+
None,
1287+
context,
1288+
);
1289+
}
11571290
}
11581291

11591292
impl From<JsPromise> for JsObject {

core/engine/src/vm/call_frame/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ pub struct CallFrame {
6262

6363
// SAFETY: Nothing requires tracing, so this is safe.
6464
#[unsafe_ignore_trace]
65-
pub(crate) local_binings_initialized: Box<[bool]>,
65+
pub(crate) local_bindings_initialized: Box<[bool]>,
6666

6767
/// How many iterations a loop has done.
6868
pub(crate) loop_iteration_count: u64,
@@ -154,16 +154,15 @@ impl CallFrame {
154154
environments: EnvironmentStack,
155155
realm: Realm,
156156
) -> Self {
157-
let local_binings_initialized = code_block.local_bindings_initialized.clone();
158157
Self {
159-
code_block,
160158
pc: 0,
161159
rp: 0,
162160
env_fp: 0,
163161
argument_count: 0,
164162
iterators: ThinVec::new(),
165163
binding_stack: Vec::new(),
166-
local_binings_initialized,
164+
local_bindings_initialized: code_block.local_bindings_initialized.clone(),
165+
code_block,
167166
loop_iteration_count: 0,
168167
active_runnable,
169168
environments,
@@ -235,6 +234,7 @@ impl CallFrame {
235234
.cloned()
236235
}
237236

237+
#[track_caller]
238238
pub(crate) fn promise_capability(&self, stack: &[JsValue]) -> Option<PromiseCapability> {
239239
if !self.code_block().is_async() {
240240
return None;

core/engine/src/vm/opcode/locals/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ impl PopIntoLocal {
1414
#[allow(clippy::unnecessary_wraps)]
1515
#[allow(clippy::needless_pass_by_value)]
1616
fn operation(dst: u32, context: &mut Context) -> JsResult<CompletionType> {
17-
context.vm.frame_mut().local_binings_initialized[dst as usize] = true;
17+
context.vm.frame_mut().local_bindings_initialized[dst as usize] = true;
1818
let value = context.vm.pop();
1919

2020
let rp = context.vm.frame().rp;
@@ -55,7 +55,7 @@ impl PushFromLocal {
5555
#[allow(clippy::unnecessary_wraps)]
5656
#[allow(clippy::needless_pass_by_value)]
5757
fn operation(dst: u32, context: &mut Context) -> JsResult<CompletionType> {
58-
if !context.vm.frame().local_binings_initialized[dst as usize] {
58+
if !context.vm.frame().local_bindings_initialized[dst as usize] {
5959
return Err(JsNativeError::reference()
6060
.with_message("access to uninitialized binding")
6161
.into());

test262_config.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ features = [
5050
# https://github.com/tc39/proposal-duplicate-named-capturing-groups
5151
"regexp-duplicate-named-groups",
5252

53-
# https://github.com/tc39/proposal-array-from-async
54-
"Array.fromAsync",
55-
5653
# https://github.com/tc39/proposal-json-parse-with-source
5754
"json-parse-with-source",
5855

0 commit comments

Comments
 (0)