Skip to content

Prototype support for async native functions #4237

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
397 changes: 352 additions & 45 deletions core/engine/src/native_function/mod.rs

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions core/engine/src/object/internal_methods/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,10 +406,23 @@ pub(crate) enum CallValue {
argument_count: usize,
},

/// Further processing is needed.
///
/// Unlike for `Pending`, the further processing should not block the VM and
/// be completed synchronously, it should integrate with VM cycle budgeting
/// and yielding.
AsyncPending,

/// The value has been computed and is the first element on the stack.
Complete,
}

pub(crate) enum ResolvedCallValue {
Ready { register_count: usize },
Pending,
Complete,
}

impl CallValue {
/// Resolves the [`CallValue`], and return if the value is complete.
pub(crate) fn resolve(mut self, context: &mut Context) -> JsResult<Option<usize>> {
Expand All @@ -425,7 +438,25 @@ impl CallValue {
match self {
Self::Ready { register_count } => Ok(Some(register_count)),
Self::Complete => Ok(None),
Self::Pending { .. } | Self::AsyncPending { .. } => unreachable!(),
}
}

pub(crate) fn async_resolve(mut self, context: &mut Context) -> JsResult<ResolvedCallValue> {
while let Self::Pending {
func,
object,
argument_count,
} = self
{
self = func(&object, argument_count, context)?;
}

match self {
Self::Ready { register_count } => Ok(ResolvedCallValue::Ready { register_count }),
Self::Complete => Ok(ResolvedCallValue::Complete),
Self::Pending { .. } => unreachable!(),
Self::AsyncPending { .. } => Ok(ResolvedCallValue::Pending),
}
}
}
Expand Down
64 changes: 55 additions & 9 deletions core/engine/src/vm/completion_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

#![allow(clippy::inline_always)]

use super::Registers;
use super::{OpStatus, Registers};
use crate::{Context, JsError, JsResult, JsValue};
use boa_gc::{custom_trace, Finalize, Trace};
use std::ops::ControlFlow;
Expand Down Expand Up @@ -56,7 +56,8 @@ pub(crate) trait IntoCompletionRecord {
self,
context: &mut Context,
registers: &mut Registers,
) -> ControlFlow<CompletionRecord>;
saved_pc: u32,
) -> ControlFlow<CompletionRecord, OpStatus>;
}

impl IntoCompletionRecord for () {
Expand All @@ -65,8 +66,9 @@ impl IntoCompletionRecord for () {
self,
_: &mut Context,
_: &mut Registers,
) -> ControlFlow<CompletionRecord> {
ControlFlow::Continue(())
_: u32,
) -> ControlFlow<CompletionRecord, OpStatus> {
ControlFlow::Continue(OpStatus::Finished)
}
}

Expand All @@ -76,7 +78,8 @@ impl IntoCompletionRecord for JsError {
self,
context: &mut Context,
registers: &mut Registers,
) -> ControlFlow<CompletionRecord> {
_: u32,
) -> ControlFlow<CompletionRecord, OpStatus> {
context.handle_error(registers, self)
}
}
Expand All @@ -87,9 +90,29 @@ impl IntoCompletionRecord for JsResult<()> {
self,
context: &mut Context,
registers: &mut Registers,
) -> ControlFlow<CompletionRecord> {
_: u32,
) -> ControlFlow<CompletionRecord, OpStatus> {
match self {
Ok(()) => ControlFlow::Continue(()),
Ok(()) => ControlFlow::Continue(OpStatus::Finished),
Err(err) => context.handle_error(registers, err),
}
}
}

impl IntoCompletionRecord for JsResult<OpStatus> {
#[inline(always)]
fn into_completion_record(
self,
context: &mut Context,
registers: &mut Registers,
saved_pc: u32,
) -> ControlFlow<CompletionRecord, OpStatus> {
match self {
Ok(OpStatus::Finished) => ControlFlow::Continue(OpStatus::Finished),
Ok(OpStatus::Pending) => {
context.vm.frame_mut().pc = saved_pc;
ControlFlow::Continue(OpStatus::Pending)
}
Err(err) => context.handle_error(registers, err),
}
}
Expand All @@ -101,7 +124,30 @@ impl IntoCompletionRecord for ControlFlow<CompletionRecord> {
self,
_: &mut Context,
_: &mut Registers,
) -> ControlFlow<CompletionRecord> {
self
_: u32,
) -> ControlFlow<CompletionRecord, OpStatus> {
match self {
ControlFlow::Continue(()) => ControlFlow::Continue(OpStatus::Finished),
ControlFlow::Break(completion_record) => ControlFlow::Break(completion_record),
}
}
}

impl IntoCompletionRecord for ControlFlow<CompletionRecord, OpStatus> {
#[inline(always)]
fn into_completion_record(
self,
context: &mut Context,
_: &mut Registers,
saved_pc: u32,
) -> ControlFlow<CompletionRecord, OpStatus> {
match self {
ControlFlow::Continue(OpStatus::Finished) => ControlFlow::Continue(OpStatus::Finished),
ControlFlow::Continue(OpStatus::Pending) => {
context.vm.frame_mut().pc = saved_pc;
ControlFlow::Continue(OpStatus::Pending)
}
ControlFlow::Break(completion_record) => ControlFlow::Break(completion_record),
}
}
}
52 changes: 35 additions & 17 deletions core/engine/src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,9 @@ impl Context {
f: F,
registers: &mut Registers,
opcode: Opcode,
) -> ControlFlow<CompletionRecord>
) -> ControlFlow<CompletionRecord, OpStatus>
where
F: FnOnce(&mut Context, &mut Registers, Opcode) -> ControlFlow<CompletionRecord>,
F: FnOnce(&mut Context, &mut Registers, Opcode) -> ControlFlow<CompletionRecord, OpStatus>,
{
let frame = self.vm.frame();
let (instruction, _) = frame
Expand Down Expand Up @@ -375,15 +375,21 @@ impl Context {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OpStatus {
Finished,
Pending,
}

impl Context {
fn execute_instruction<F>(
&mut self,
f: F,
registers: &mut Registers,
opcode: Opcode,
) -> ControlFlow<CompletionRecord>
) -> ControlFlow<CompletionRecord, OpStatus>
where
F: FnOnce(&mut Context, &mut Registers, Opcode) -> ControlFlow<CompletionRecord>,
F: FnOnce(&mut Context, &mut Registers, Opcode) -> ControlFlow<CompletionRecord, OpStatus>,
{
f(self, registers, opcode)
}
Expand All @@ -393,9 +399,9 @@ impl Context {
f: F,
registers: &mut Registers,
opcode: Opcode,
) -> ControlFlow<CompletionRecord>
) -> ControlFlow<CompletionRecord, OpStatus>
where
F: FnOnce(&mut Context, &mut Registers, Opcode) -> ControlFlow<CompletionRecord>,
F: FnOnce(&mut Context, &mut Registers, Opcode) -> ControlFlow<CompletionRecord, OpStatus>,
{
#[cfg(feature = "fuzz")]
{
Expand All @@ -422,7 +428,7 @@ impl Context {
&mut self,
registers: &mut Registers,
err: JsError,
) -> ControlFlow<CompletionRecord> {
) -> ControlFlow<CompletionRecord, OpStatus> {
// If we hit the execution step limit, bubble up the error to the
// (Rust) caller instead of trying to handle as an exception.
if !err.is_catchable() {
Expand Down Expand Up @@ -451,7 +457,7 @@ impl Context {
let pc = self.vm.frame().pc.saturating_sub(1);
if self.vm.handle_exception_at(pc) {
self.vm.pending_exception = Some(err);
return ControlFlow::Continue(());
return ControlFlow::Continue(OpStatus::Finished);
}

// Inject realm before crossing the function boundry
Expand All @@ -461,7 +467,10 @@ impl Context {
self.handle_thow(registers)
}

fn handle_return(&mut self, registers: &mut Registers) -> ControlFlow<CompletionRecord> {
fn handle_return(
&mut self,
registers: &mut Registers,
) -> ControlFlow<CompletionRecord, OpStatus> {
let frame = self.vm.frame();
let fp = frame.fp() as usize;
let exit_early = frame.exit_early();
Expand All @@ -475,10 +484,13 @@ impl Context {
self.vm.push(result);
self.vm.pop_frame().expect("frame must exist");
registers.pop_function(self.vm.frame().code_block().register_count as usize);
ControlFlow::Continue(())
ControlFlow::Continue(OpStatus::Finished)
}

fn handle_yield(&mut self, registers: &mut Registers) -> ControlFlow<CompletionRecord> {
fn handle_yield(
&mut self,
registers: &mut Registers,
) -> ControlFlow<CompletionRecord, OpStatus> {
let result = self.vm.take_return_value();
if self.vm.frame().exit_early() {
return ControlFlow::Break(CompletionRecord::Return(result));
Expand All @@ -487,10 +499,13 @@ impl Context {
self.vm.push(result);
self.vm.pop_frame().expect("frame must exist");
registers.pop_function(self.vm.frame().code_block().register_count as usize);
ControlFlow::Continue(())
ControlFlow::Continue(OpStatus::Finished)
}

fn handle_thow(&mut self, registers: &mut Registers) -> ControlFlow<CompletionRecord> {
fn handle_thow(
&mut self,
registers: &mut Registers,
) -> ControlFlow<CompletionRecord, OpStatus> {
let frame = self.vm.frame();
let mut fp = frame.fp();
let mut env_fp = frame.env_fp;
Expand All @@ -515,7 +530,7 @@ impl Context {
let exit_early = self.vm.frame.exit_early();

if self.vm.handle_exception_at(pc) {
return ControlFlow::Continue(());
return ControlFlow::Continue(OpStatus::Finished);
}

if exit_early {
Expand All @@ -535,7 +550,7 @@ impl Context {
}
self.vm.environments.truncate(env_fp as usize);
self.vm.stack.truncate(fp as usize);
ControlFlow::Continue(())
ControlFlow::Continue(OpStatus::Finished)
}

/// Runs the current frame to completion, yielding to the caller each time `budget`
Expand Down Expand Up @@ -576,7 +591,10 @@ impl Context {
registers,
opcode,
) {
ControlFlow::Continue(()) => {}
ControlFlow::Continue(OpStatus::Finished) => {}
ControlFlow::Continue(OpStatus::Pending) => {
runtime_budget = 0;
}
ControlFlow::Break(value) => return value,
}

Expand Down Expand Up @@ -608,7 +626,7 @@ impl Context {
let opcode = Opcode::decode(*byte);

match self.execute_one(Self::execute_bytecode_instruction, registers, opcode) {
ControlFlow::Continue(()) => {}
ControlFlow::Continue(_) => {}
ControlFlow::Break(value) => return value,
}
}
Expand Down
10 changes: 5 additions & 5 deletions core/engine/src/vm/opcode/await/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
js_string,
native_function::NativeFunction,
object::FunctionObjectBuilder,
vm::{opcode::Operation, CompletionRecord, GeneratorResumeKind, Registers},
vm::{opcode::Operation, CompletionRecord, GeneratorResumeKind, OpStatus, Registers},
Context, JsArgs, JsValue,
};
use boa_gc::Gc;
Expand All @@ -26,7 +26,7 @@ impl Await {
value: VaryingOperand,
registers: &mut Registers,
context: &mut Context,
) -> ControlFlow<CompletionRecord> {
) -> ControlFlow<CompletionRecord, OpStatus> {
let value = registers.get(value.into());

// 2. Let promise be ? PromiseResolve(%Promise%, value).
Expand Down Expand Up @@ -197,14 +197,14 @@ impl CompletePromiseCapability {
(): (),
registers: &mut Registers,
context: &mut Context,
) -> ControlFlow<CompletionRecord> {
) -> ControlFlow<CompletionRecord, OpStatus> {
// If the current executing function is an async function we have to resolve/reject it's promise at the end.
// The relevant spec section is 3. in [AsyncBlockStart](https://tc39.es/ecma262/#sec-asyncblockstart).
let Some(promise_capability) = context.vm.frame().promise_capability(registers) else {
return if context.vm.pending_exception.is_some() {
context.handle_thow(registers)
} else {
ControlFlow::Continue(())
ControlFlow::Continue(OpStatus::Finished)
};
};

Expand All @@ -225,7 +225,7 @@ impl CompletePromiseCapability {
.vm
.set_return_value(promise_capability.promise().clone().into());

ControlFlow::Continue(())
ControlFlow::Continue(OpStatus::Finished)
}
}

Expand Down
21 changes: 14 additions & 7 deletions core/engine/src/vm/opcode/call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use crate::{
builtins::{promise::PromiseCapability, Promise},
error::JsNativeError,
module::{ModuleKind, Referrer},
object::FunctionObjectBuilder,
vm::{opcode::Operation, Registers},
object::{internal_methods::ResolvedCallValue, FunctionObjectBuilder},
vm::{opcode::Operation, OpStatus, Registers},
Context, JsObject, JsResult, JsValue, NativeFunction,
};

Expand Down Expand Up @@ -185,22 +185,29 @@ impl Call {
argument_count: VaryingOperand,
registers: &mut Registers,
context: &mut Context,
) -> JsResult<()> {
) -> JsResult<OpStatus> {
let argument_count = usize::from(argument_count);
let at = context.vm.stack.len() - argument_count;
let func = &context.vm.stack[at - 1];

//println!("Call function: {:?}", func);
let Some(object) = func.as_object() else {
return Err(JsNativeError::typ()
.with_message("not a callable function")
.into());
};

if let Some(register_count) = object.__call__(argument_count).resolve(context)? {
registers.push_function(register_count);
match object.__call__(argument_count).async_resolve(context)? {
ResolvedCallValue::Ready { register_count } => {
registers.push_function(register_count);
Ok(OpStatus::Finished)
}
ResolvedCallValue::Complete => Ok(OpStatus::Finished),
ResolvedCallValue::Pending => {
//println!("Pending call");
Ok(OpStatus::Pending)
}
}

Ok(())
}
}

Expand Down
Loading