Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Array.fromAsync #4115

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
622 changes: 622 additions & 0 deletions core/engine/src/builtins/array/from_async.rs

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions core/engine/src/builtins/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ mod array_iterator;
use crate::value::JsVariant;
pub(crate) use array_iterator::ArrayIterator;

#[cfg(feature = "experimental")]
mod from_async;

#[cfg(test)]
mod tests;

Expand Down Expand Up @@ -106,7 +109,7 @@ impl IntrinsicObject for Array {

let unscopables_object = Self::unscopables_object();

BuiltInBuilder::from_standard_constructor::<Self>(realm)
let builder = BuiltInBuilder::from_standard_constructor::<Self>(realm)
// Static Methods
.static_method(Self::from, js_string!("from"), 1)
.static_method(Self::is_array, js_string!("isArray"), 1)
Expand Down Expand Up @@ -177,8 +180,12 @@ impl IntrinsicObject for Array {
symbol_unscopables,
unscopables_object,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build();
);

#[cfg(feature = "experimental")]
let builder = builder.static_method(Self::from_async, js_string!("fromAsync"), 1);

builder.build();
}

fn get(intrinsics: &Intrinsics) -> JsObject {
Expand Down
3 changes: 2 additions & 1 deletion core/engine/src/module/synthetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ pub struct SyntheticModuleInitializer {

impl std::fmt::Debug for SyntheticModuleInitializer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModuleInitializer").finish_non_exhaustive()
f.debug_struct("SyntheticModuleInitializer")
.finish_non_exhaustive()
}
}

Expand Down
104 changes: 104 additions & 0 deletions core/engine/src/native_function/continuation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use boa_gc::{Finalize, Gc, Trace};

use crate::{Context, JsResult, JsValue};

#[derive(Trace, Finalize)]
#[boa_gc(unsafe_no_drop)]
pub(crate) enum CoroutineState {
Yielded(JsValue),
Done,
}

trait TraceableCoroutine: Trace {
fn call(&self, value: JsResult<JsValue>, context: &mut Context) -> CoroutineState;
}

#[derive(Trace, Finalize)]
struct Coroutine<F, T>
where
F: Fn(JsResult<JsValue>, &T, &mut Context) -> CoroutineState,
T: Trace,
{
// SAFETY: `NativeCoroutine`'s safe API ensures only `Copy` closures are stored; its unsafe API,
// on the other hand, explains the invariants to hold in order for this to be safe, shifting
// the responsibility to the caller.
#[unsafe_ignore_trace]
f: F,
captures: T,
}

impl<F, T> TraceableCoroutine for Coroutine<F, T>
where
F: Fn(JsResult<JsValue>, &T, &mut Context) -> CoroutineState,
T: Trace,
{
fn call(&self, result: JsResult<JsValue>, context: &mut Context) -> CoroutineState {
(self.f)(result, &self.captures, context)
}
}

/// A callable Rust coroutine that can be used to await promises.
///
/// # Caveats
///
/// By limitations of the Rust language, the garbage collector currently cannot inspect closures
/// in order to trace their captured variables. This means that only [`Copy`] closures are 100% safe
/// to use. All other closures can also be stored in a `NativeCoroutine`, albeit by using an `unsafe`
/// API, but note that passing closures implicitly capturing traceable types could cause
/// **Undefined Behaviour**.
#[derive(Clone, Trace, Finalize)]
pub(crate) struct NativeCoroutine {
inner: Gc<dyn TraceableCoroutine>,
}

impl std::fmt::Debug for NativeCoroutine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NativeCoroutine").finish_non_exhaustive()
}
}

impl NativeCoroutine {
/// Creates a `NativeCoroutine` from a `Copy` closure and a list of traceable captures.
pub(crate) fn from_copy_closure_with_captures<F, T>(closure: F, captures: T) -> Self
where
F: Fn(JsResult<JsValue>, &T, &mut Context) -> CoroutineState + Copy + 'static,
T: Trace + 'static,
{
// SAFETY: The `Copy` bound ensures there are no traceable types inside the closure.
unsafe { Self::from_closure_with_captures(closure, captures) }
}

/// Create a new `NativeCoroutine` from a closure and a list of traceable captures.
///
/// # Safety
///
/// Passing a closure that contains a captured variable that needs to be traced by the garbage
/// collector could cause an use after free, memory corruption or other kinds of **Undefined
/// Behaviour**. See <https://github.com/Manishearth/rust-gc/issues/50> for a technical explanation
/// on why that is the case.
pub(crate) unsafe fn from_closure_with_captures<F, T>(closure: F, captures: T) -> Self
where
F: Fn(JsResult<JsValue>, &T, &mut Context) -> CoroutineState + 'static,
T: Trace + 'static,
{
// Hopefully, this unsafe operation will be replaced by the `CoerceUnsized` API in the
// future: https://github.com/rust-lang/rust/issues/18598
let ptr = Gc::into_raw(Gc::new(Coroutine {
f: closure,
captures,
}));
// SAFETY: The pointer returned by `into_raw` is only used to coerce to a trait object,
// meaning this is safe.
unsafe {
Self {
inner: Gc::from_raw(ptr),
}
}
}

/// Calls this `NativeCoroutine`, forwarding the arguments to the corresponding function.
#[inline]
pub(crate) fn call(&self, result: JsResult<JsValue>, context: &mut Context) -> CoroutineState {
self.inner.call(result, context)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ use crate::{
Context, JsNativeError, JsObject, JsResult, JsValue,
};

#[cfg(feature = "experimental")]
mod continuation;

#[cfg(feature = "experimental")]
pub(crate) use continuation::{CoroutineState, NativeCoroutine};

/// The required signature for all native built-in function pointers.
///
/// # Arguments
Expand Down
133 changes: 133 additions & 0 deletions core/engine/src/object/builtins/jspromise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,139 @@ impl JsPromise {
}
}
}

#[cfg(feature = "experimental")]
pub(crate) fn await_native(
&self,
continuation: crate::native_function::NativeCoroutine,
context: &mut Context,
) {
use crate::{
builtins::{async_generator::AsyncGenerator, generator::GeneratorContext},
js_string,
object::FunctionObjectBuilder,
};
use std::cell::Cell;

let mut frame = context.vm.frame().clone();
frame.environments = context.vm.environments.clone();
frame.realm = context.realm().clone();

let gen_ctx = GeneratorContext {
call_frame: Some(frame),
stack: context.vm.stack.clone(),
};

// 3. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures asyncContext and performs the following steps when called:
// 4. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »).
let on_fulfilled = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_this, args, captures, context| {
// a. Let prevContext be the running execution context.
// b. Suspend prevContext.
// c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context.
// d. Resume the suspended evaluation of asyncContext using NormalCompletion(value) as the result of the operation that suspended it.
let continuation = &captures.0;
let mut gen = captures.1.take().expect("should only run once");

// NOTE: We need to get the object before resuming, since it could clear the stack.
let async_generator = gen.async_generator_object();

std::mem::swap(&mut context.vm.stack, &mut gen.stack);
let frame = gen.call_frame.take().expect("should have a call frame");
context.vm.push_frame(frame);

if let crate::native_function::CoroutineState::Yielded(value) =
continuation.call(Ok(args.get_or_undefined(0).clone()), context)
{
JsPromise::resolve(value, context)
.await_native(continuation.clone(), context);
}

std::mem::swap(&mut context.vm.stack, &mut gen.stack);
gen.call_frame = context.vm.pop_frame();
assert!(gen.call_frame.is_some());

if let Some(async_generator) = async_generator {
async_generator
.downcast_mut::<AsyncGenerator>()
.expect("must be async generator")
.context = Some(gen);
}

// 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.
// f. Return undefined.
Ok(JsValue::undefined())
},
(continuation.clone(), Cell::new(Some(gen_ctx.clone()))),
),
)
.name(js_string!())
.length(1)
.build();

// 5. Let rejectedClosure be a new Abstract Closure with parameters (reason) that captures asyncContext and performs the following steps when called:
// 6. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »).
let on_rejected = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_this, args, captures, context| {
// a. Let prevContext be the running execution context.
// b. Suspend prevContext.
// c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context.
// d. Resume the suspended evaluation of asyncContext using ThrowCompletion(reason) as the result of the operation that suspended it.
// 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.
// f. Return undefined.
let continuation = &captures.0;
let mut gen = captures.1.take().expect("should only run once");

// NOTE: We need to get the object before resuming, since it could clear the stack.
let async_generator = gen.async_generator_object();

std::mem::swap(&mut context.vm.stack, &mut gen.stack);
let frame = gen.call_frame.take().expect("should have a call frame");
context.vm.push_frame(frame);

if let crate::native_function::CoroutineState::Yielded(value) = continuation
.call(
Err(JsError::from_opaque(args.get_or_undefined(0).clone())),
context,
)
{
JsPromise::resolve(value, context)
.await_native(continuation.clone(), context);
}

std::mem::swap(&mut context.vm.stack, &mut gen.stack);
gen.call_frame = context.vm.pop_frame();
assert!(gen.call_frame.is_some());

if let Some(async_generator) = async_generator {
async_generator
.downcast_mut::<AsyncGenerator>()
.expect("must be async generator")
.context = Some(gen);
}

Ok(JsValue::undefined())
},
(continuation, Cell::new(Some(gen_ctx))),
),
)
.name(js_string!())
.length(1)
.build();

// 7. Perform PerformPromiseThen(promise, onFulfilled, onRejected).
Promise::perform_promise_then(
&self.inner,
Some(on_fulfilled),
Some(on_rejected),
None,
context,
);
}
}

impl From<JsPromise> for JsObject {
Expand Down
8 changes: 4 additions & 4 deletions core/engine/src/vm/call_frame/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub struct CallFrame {

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

/// How many iterations a loop has done.
pub(crate) loop_iteration_count: u64,
Expand Down Expand Up @@ -154,16 +154,15 @@ impl CallFrame {
environments: EnvironmentStack,
realm: Realm,
) -> Self {
let local_binings_initialized = code_block.local_bindings_initialized.clone();
Self {
code_block,
pc: 0,
rp: 0,
env_fp: 0,
argument_count: 0,
iterators: ThinVec::new(),
binding_stack: Vec::new(),
local_binings_initialized,
local_bindings_initialized: code_block.local_bindings_initialized.clone(),
code_block,
loop_iteration_count: 0,
active_runnable,
environments,
Expand Down Expand Up @@ -235,6 +234,7 @@ impl CallFrame {
.cloned()
}

#[track_caller]
pub(crate) fn promise_capability(&self, stack: &[JsValue]) -> Option<PromiseCapability> {
if !self.code_block().is_async() {
return None;
Expand Down
4 changes: 2 additions & 2 deletions core/engine/src/vm/opcode/locals/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ impl PopIntoLocal {
#[allow(clippy::unnecessary_wraps)]
#[allow(clippy::needless_pass_by_value)]
fn operation(dst: u32, context: &mut Context) -> JsResult<CompletionType> {
context.vm.frame_mut().local_binings_initialized[dst as usize] = true;
context.vm.frame_mut().local_bindings_initialized[dst as usize] = true;
let value = context.vm.pop();

let rp = context.vm.frame().rp;
Expand Down Expand Up @@ -55,7 +55,7 @@ impl PushFromLocal {
#[allow(clippy::unnecessary_wraps)]
#[allow(clippy::needless_pass_by_value)]
fn operation(dst: u32, context: &mut Context) -> JsResult<CompletionType> {
if !context.vm.frame().local_binings_initialized[dst as usize] {
if !context.vm.frame().local_bindings_initialized[dst as usize] {
return Err(JsNativeError::reference()
.with_message("access to uninitialized binding")
.into());
Expand Down
3 changes: 0 additions & 3 deletions test262_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ features = [
# https://github.com/tc39/proposal-duplicate-named-capturing-groups
"regexp-duplicate-named-groups",

# https://github.com/tc39/proposal-array-from-async
"Array.fromAsync",

# https://github.com/tc39/proposal-json-parse-with-source
"json-parse-with-source",

Expand Down
Loading