Browse Source

Implement async functions using generators (#2821)

This should hopefully fix more async/futures issues related to resuming execution in the future, since we can leverage generator logic to handle this for us.

It changes the following:

- Refactors `GeneratorContext` to handle context preparation.
- Reuses the functionality of `GeneratorContext` in `Await`.
- Removes `EarlyReturnType` in favour of a single `r#await` bool flag in `CallFrame`.
pull/2825/head
José Julián Espina 2 years ago
parent
commit
6d3550d0cb
  1. 16
      boa_cli/src/debug/function.rs
  2. 2
      boa_engine/src/builtins/async_function/mod.rs
  3. 54
      boa_engine/src/builtins/async_generator/mod.rs
  4. 12
      boa_engine/src/builtins/function/mod.rs
  5. 305
      boa_engine/src/builtins/generator/mod.rs
  6. 7
      boa_engine/src/context/mod.rs
  7. 20
      boa_engine/src/environments/runtime.rs
  8. 15
      boa_engine/src/vm/call_frame/mod.rs
  9. 93
      boa_engine/src/vm/code_block.rs
  10. 38
      boa_engine/src/vm/mod.rs
  11. 100
      boa_engine/src/vm/opcode/await_stm/mod.rs
  12. 8
      boa_engine/src/vm/opcode/generator/mod.rs
  13. 4
      boa_engine/src/vm/opcode/generator/yield_stm.rs
  14. 39
      docs/boa_object.md

16
boa_cli/src/debug/function.rs

@ -159,10 +159,26 @@ fn trace(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<J
result result
} }
fn traceable(_: &JsValue, args: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
let value = args.get_or_undefined(0);
let traceable = args.get_or_undefined(1).to_boolean();
let Some(callable) = value.as_callable() else {
return Err(JsNativeError::typ()
.with_message("expected callable object")
.into());
};
set_trace_flag_in_function_object(callable, traceable)?;
Ok(value.clone())
}
pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { pub(super) fn create_object(context: &mut Context<'_>) -> JsObject {
ObjectInitializer::new(context) ObjectInitializer::new(context)
.function(NativeFunction::from_fn_ptr(flowgraph), "flowgraph", 1) .function(NativeFunction::from_fn_ptr(flowgraph), "flowgraph", 1)
.function(NativeFunction::from_fn_ptr(bytecode), "bytecode", 1) .function(NativeFunction::from_fn_ptr(bytecode), "bytecode", 1)
.function(NativeFunction::from_fn_ptr(trace), "trace", 1) .function(NativeFunction::from_fn_ptr(trace), "trace", 1)
.function(NativeFunction::from_fn_ptr(traceable), "traceable", 2)
.build() .build()
} }

2
boa_engine/src/builtins/async_function/mod.rs

@ -70,7 +70,7 @@ impl BuiltInConstructor for AsyncFunction {
context context
.intrinsics() .intrinsics()
.constructors() .constructors()
.generator_function() .async_function()
.constructor() .constructor()
}); });
BuiltInFunctionObject::create_dynamic_function( BuiltInFunctionObject::create_dynamic_function(

54
boa_engine/src/builtins/async_generator/mod.rs

@ -490,49 +490,22 @@ impl AsyncGenerator {
.expect("already checked before") .expect("already checked before")
.state = AsyncGeneratorState::Executing; .state = AsyncGeneratorState::Executing;
let (value, resume_kind) = match completion {
(Ok(value), r#return) => (
value,
if r#return {
GeneratorResumeKind::Return
} else {
GeneratorResumeKind::Normal
},
),
(Err(value), _) => (value.to_opaque(context), GeneratorResumeKind::Throw),
};
// 6. Push genContext onto the execution context stack; genContext is now the running execution context. // 6. Push genContext onto the execution context stack; genContext is now the running execution context.
std::mem::swap(
&mut context.vm.environments,
&mut generator_context.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
std::mem::swap(
&mut context.vm.active_function,
&mut generator_context.active_function,
);
let old_realm = context.enter_realm(generator_context.realm.clone());
context.vm.push_frame(generator_context.call_frame.clone()); let result = generator_context.resume(Some(value), resume_kind, context);
// 7. Resume the suspended evaluation of genContext using completion as the result of the operation that suspended it. Let result be the Completion Record returned by the resumed computation. // 7. Resume the suspended evaluation of genContext using completion as the result of the operation that suspended it. Let result be the Completion Record returned by the resumed computation.
match completion {
(Ok(value), r#return) => {
context.vm.push(value);
if r#return {
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Return;
} else {
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Normal;
}
}
(Err(value), _) => {
let value = value.to_opaque(context);
context.vm.push(value);
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Throw;
}
}
let result = context.run();
std::mem::swap(
&mut context.vm.environments,
&mut generator_context.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
generator_context.call_frame = context.vm.pop_frame().expect("generator frame must exist");
std::mem::swap(
&mut context.vm.active_function,
&mut generator_context.active_function,
);
context.enter_realm(old_realm);
generator generator
.borrow_mut() .borrow_mut()
@ -543,7 +516,8 @@ impl AsyncGenerator {
// 8. Assert: result is never an abrupt completion. // 8. Assert: result is never an abrupt completion.
assert!(!result.is_throw_completion()); assert!(!result.is_throw_completion());
// 9. Assert: When we return here, genContext has already been removed from the execution context stack and callerContext is the currently running execution context. // 9. Assert: When we return here, genContext has already been removed from the execution context stack and
// callerContext is the currently running execution context.
// 10. Return unused. // 10. Return unused.
} }

12
boa_engine/src/builtins/function/mod.rs

@ -416,18 +416,6 @@ impl Function {
} }
} }
/// Returns the promise capability if the function is an async function.
pub(crate) const fn get_promise_capability(&self) -> Option<&PromiseCapability> {
if let FunctionKind::Async {
promise_capability, ..
} = &self.kind
{
Some(promise_capability)
} else {
None
}
}
/// Sets the class object. /// Sets the class object.
pub(crate) fn set_class_object(&mut self, object: JsObject) { pub(crate) fn set_class_object(&mut self, object: JsObject) {
match &mut self.kind { match &mut self.kind {

305
boa_engine/src/builtins/generator/mod.rs

@ -44,12 +44,72 @@ pub(crate) enum GeneratorState {
#[derive(Debug, Clone, Finalize, Trace)] #[derive(Debug, Clone, Finalize, Trace)]
pub(crate) struct GeneratorContext { pub(crate) struct GeneratorContext {
pub(crate) environments: DeclarativeEnvironmentStack, pub(crate) environments: DeclarativeEnvironmentStack,
pub(crate) call_frame: CallFrame,
pub(crate) stack: Vec<JsValue>, pub(crate) stack: Vec<JsValue>,
pub(crate) active_function: Option<JsObject>, pub(crate) active_function: Option<JsObject>,
pub(crate) call_frame: Option<CallFrame>,
pub(crate) realm: Realm, pub(crate) realm: Realm,
} }
impl GeneratorContext {
/// Creates a new `GeneratorContext` from the raw `Context` state components.
pub(crate) fn new(
environments: DeclarativeEnvironmentStack,
stack: Vec<JsValue>,
active_function: Option<JsObject>,
call_frame: CallFrame,
realm: Realm,
) -> Self {
Self {
environments,
stack,
active_function,
call_frame: Some(call_frame),
realm,
}
}
/// Creates a new `GeneratorContext` from the current `Context` state.
pub(crate) fn from_current(context: &mut Context<'_>) -> Self {
Self {
environments: context.vm.environments.clone(),
call_frame: Some(context.vm.frame().clone()),
stack: context.vm.stack.clone(),
active_function: context.vm.active_function.clone(),
realm: context.realm().clone(),
}
}
/// Resumes execution with `GeneratorContext` as the current execution context.
pub(crate) fn resume(
&mut self,
value: Option<JsValue>,
resume_kind: GeneratorResumeKind,
context: &mut Context<'_>,
) -> CompletionRecord {
std::mem::swap(&mut context.vm.environments, &mut self.environments);
std::mem::swap(&mut context.vm.stack, &mut self.stack);
std::mem::swap(&mut context.vm.active_function, &mut self.active_function);
context.swap_realm(&mut self.realm);
context
.vm
.push_frame(self.call_frame.take().expect("should have a call frame"));
context.vm.frame_mut().generator_resume_kind = resume_kind;
if let Some(value) = value {
context.vm.push(value);
}
let result = context.run();
std::mem::swap(&mut context.vm.environments, &mut self.environments);
std::mem::swap(&mut context.vm.stack, &mut self.stack);
std::mem::swap(&mut context.vm.active_function, &mut self.active_function);
context.swap_realm(&mut self.realm);
self.call_frame = context.vm.pop_frame();
assert!(self.call_frame.is_some());
result
}
}
/// The internal representation of a `Generator` object. /// The internal representation of a `Generator` object.
#[derive(Debug, Finalize, Trace)] #[derive(Debug, Finalize, Trace)]
pub struct Generator { pub struct Generator {
@ -117,15 +177,19 @@ impl Generator {
args: &[JsValue], args: &[JsValue],
context: &mut Context<'_>, context: &mut Context<'_>,
) -> JsResult<JsValue> { ) -> JsResult<JsValue> {
// Extracted `GeneratorValidate` since we don't need to worry about validation
// of async functions.
let generator = Self::generator_validate(this)?;
// 1. Return ? GeneratorResume(this value, value, empty). // 1. Return ? GeneratorResume(this value, value, empty).
this.as_object().map_or_else( let completion =
|| { Self::generator_resume(generator, args.get_or_undefined(0).clone(), context);
Err(JsNativeError::typ()
.with_message("Generator.prototype.next called on non generator") match completion {
.into()) CompletionRecord::Return(value) => Ok(create_iter_result_object(value, false, context)),
}, CompletionRecord::Normal(value) => Ok(create_iter_result_object(value, true, context)),
|obj| Self::generator_resume(obj, args.get_or_undefined(0), context), CompletionRecord::Throw(err) => Err(err),
) }
} }
/// `Generator.prototype.return ( value )` /// `Generator.prototype.return ( value )`
@ -144,9 +208,17 @@ impl Generator {
context: &mut Context<'_>, context: &mut Context<'_>,
) -> JsResult<JsValue> { ) -> JsResult<JsValue> {
// 1. Let g be the this value. // 1. Let g be the this value.
let generator = Self::generator_validate(this)?;
// 2. Let C be Completion { [[Type]]: return, [[Value]]: value, [[Target]]: empty }. // 2. Let C be Completion { [[Type]]: return, [[Value]]: value, [[Target]]: empty }.
// 3. Return ? GeneratorResumeAbrupt(g, C, empty). // 3. Return ? GeneratorResumeAbrupt(g, C, empty).
Self::generator_resume_abrupt(this, Ok(args.get_or_undefined(0).clone()), context) let completion =
Self::generator_resume_abrupt(generator, Ok(args.get_or_undefined(0).clone()), context);
match completion {
CompletionRecord::Return(value) => Ok(create_iter_result_object(value, false, context)),
CompletionRecord::Normal(value) => Ok(create_iter_result_object(value, true, context)),
CompletionRecord::Throw(err) => Err(err),
}
} }
/// `Generator.prototype.throw ( exception )` /// `Generator.prototype.throw ( exception )`
@ -166,46 +238,67 @@ impl Generator {
context: &mut Context<'_>, context: &mut Context<'_>,
) -> JsResult<JsValue> { ) -> JsResult<JsValue> {
// 1. Let g be the this value. // 1. Let g be the this value.
let generator = Self::generator_validate(this)?;
// 2. Let C be ThrowCompletion(exception). // 2. Let C be ThrowCompletion(exception).
// 3. Return ? GeneratorResumeAbrupt(g, C, empty). // 3. Return ? GeneratorResumeAbrupt(g, C, empty).
Self::generator_resume_abrupt( let completion = Self::generator_resume_abrupt(
this, generator,
Err(JsError::from_opaque(args.get_or_undefined(0).clone())), Err(JsError::from_opaque(args.get_or_undefined(0).clone())),
context, context,
) );
match completion {
CompletionRecord::Return(value) => Ok(create_iter_result_object(value, false, context)),
CompletionRecord::Normal(value) => Ok(create_iter_result_object(value, true, context)),
CompletionRecord::Throw(err) => Err(err),
}
} }
/// `27.5.3.3 GeneratorResume ( generator, value, generatorBrand )` /// `27.5.3.3 GeneratorValidate ( generator, generatorBrand )`
/// ///
/// More information: /// More information:
/// - [ECMAScript reference][spec] /// - [ECMAScript reference][spec]
/// ///
/// [spec]: https://tc39.es/ecma262/#sec-generatorresume /// [spec]:https://tc39.es/ecma262/#sec-generatorvalidate
pub(crate) fn generator_resume( fn generator_validate(gen: &JsValue) -> JsResult<&JsObject> {
generator_obj: &JsObject, let generator_obj = gen.as_object().ok_or_else(|| {
value: &JsValue, JsNativeError::typ().with_message("Generator method called on non generator")
context: &mut Context<'_>, })?;
) -> JsResult<JsValue> {
// 1. Let state be ? GeneratorValidate(generator, generatorBrand).
let mut generator_obj_mut = generator_obj.borrow_mut(); let mut generator_obj_mut = generator_obj.borrow_mut();
let generator = generator_obj_mut.as_generator_mut().ok_or_else(|| { let generator = generator_obj_mut.as_generator_mut().ok_or_else(|| {
JsNativeError::typ().with_message("generator resumed on non generator object") JsNativeError::typ().with_message("generator resumed on non generator object")
})?; })?;
let state = generator.state;
if state == GeneratorState::Executing { if generator.state == GeneratorState::Executing {
return Err(JsNativeError::typ() Err(JsNativeError::typ()
.with_message("Generator should not be executing") .with_message("Generator should not be executing")
.into()); .into())
} else {
Ok(generator_obj)
} }
}
/// `27.5.3.3 GeneratorResume ( generator, value, generatorBrand )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-generatorresume
pub(crate) fn generator_resume(
gen: &JsObject,
value: JsValue,
context: &mut Context<'_>,
) -> CompletionRecord {
// 1. Let state be ? GeneratorValidate(generator, generatorBrand).
let mut generator_obj_mut = gen.borrow_mut();
let generator = generator_obj_mut
.as_generator_mut()
.expect("already validated that this is a generator");
let state = generator.state;
// 2. If state is completed, return CreateIterResultObject(undefined, true). // 2. If state is completed, return CreateIterResultObject(undefined, true).
if state == GeneratorState::Completed { if state == GeneratorState::Completed {
return Ok(create_iter_result_object( return CompletionRecord::Normal(JsValue::undefined());
JsValue::undefined(),
true,
context,
));
} }
// 3. Assert: state is either suspendedStart or suspendedYield. // 3. Assert: state is either suspendedStart or suspendedYield.
@ -227,64 +320,30 @@ impl Generator {
.expect("generator context cannot be empty here"); .expect("generator context cannot be empty here");
drop(generator_obj_mut); drop(generator_obj_mut);
std::mem::swap( let record = generator_context.resume(
&mut context.vm.environments, (!first_execution).then_some(value),
&mut generator_context.environments, GeneratorResumeKind::Normal,
); context,
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
std::mem::swap(
&mut context.vm.active_function,
&mut generator_context.active_function,
);
let old_realm = context.enter_realm(generator_context.realm.clone());
context.vm.push_frame(generator_context.call_frame.clone());
if !first_execution {
context.vm.push(value.clone());
}
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Normal;
let record = context.run();
generator_context.call_frame = context
.vm
.pop_frame()
.expect("generator call frame must exist");
std::mem::swap(
&mut context.vm.environments,
&mut generator_context.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
std::mem::swap(
&mut context.vm.active_function,
&mut generator_context.active_function,
); );
context.enter_realm(old_realm);
let mut generator_obj_mut = generator_obj.borrow_mut(); let mut generator_obj_mut = gen.borrow_mut();
let generator = generator_obj_mut let generator = generator_obj_mut
.as_generator_mut() .as_generator_mut()
.expect("already checked this object type"); .expect("already checked this object type");
match record { generator.state = match record {
CompletionRecord::Return(value) => { CompletionRecord::Return(_) => {
generator.state = GeneratorState::SuspendedYield;
generator.context = Some(generator_context); generator.context = Some(generator_context);
Ok(create_iter_result_object(value, false, context)) GeneratorState::SuspendedYield
}
CompletionRecord::Normal(value) => {
generator.state = GeneratorState::Completed;
Ok(create_iter_result_object(value, true, context))
} }
CompletionRecord::Throw(err) => { CompletionRecord::Normal(_) | CompletionRecord::Throw(_) => GeneratorState::Completed,
generator.state = GeneratorState::Completed; };
Err(err)
}
}
// 8. Push genContext onto the execution context stack; genContext is now the running execution context. // 8. Push genContext onto the execution context stack; genContext is now the running execution context.
// 9. Resume the suspended evaluation of genContext using NormalCompletion(value) as the result of the operation that suspended it. Let result be the value returned by the resumed computation. // 9. Resume the suspended evaluation of genContext using NormalCompletion(value) as the result of the operation that suspended it. Let result be the value returned by the resumed computation.
// 10. Assert: When we return here, genContext has already been removed from the execution context stack and methodContext is the currently running execution context. // 10. Assert: When we return here, genContext has already been removed from the execution context stack and methodContext is the currently running execution context.
// 11. Return Completion(result). // 11. Return Completion(result).
record
} }
/// `27.5.3.4 GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand )` /// `27.5.3.4 GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand )`
@ -294,26 +353,17 @@ impl Generator {
/// ///
/// [spec]: https://tc39.es/ecma262/#sec-generatorresumeabrupt /// [spec]: https://tc39.es/ecma262/#sec-generatorresumeabrupt
pub(crate) fn generator_resume_abrupt( pub(crate) fn generator_resume_abrupt(
this: &JsValue, gen: &JsObject,
abrupt_completion: JsResult<JsValue>, abrupt_completion: JsResult<JsValue>,
context: &mut Context<'_>, context: &mut Context<'_>,
) -> JsResult<JsValue> { ) -> CompletionRecord {
// 1. Let state be ? GeneratorValidate(generator, generatorBrand). // 1. Let state be ? GeneratorValidate(generator, generatorBrand).
let generator_obj = this.as_object().ok_or_else(|| { let mut generator_obj_mut = gen.borrow_mut();
JsNativeError::typ().with_message("generator resumed on non generator object") let generator = generator_obj_mut
})?; .as_generator_mut()
let mut generator_obj_mut = generator_obj.borrow_mut(); .expect("already validated the generator object");
let generator = generator_obj_mut.as_generator_mut().ok_or_else(|| {
JsNativeError::typ().with_message("generator resumed on non generator object")
})?;
let mut state = generator.state; let mut state = generator.state;
if state == GeneratorState::Executing {
return Err(JsNativeError::typ()
.with_message("Generator should not be executing")
.into());
}
// 2. If state is suspendedStart, then // 2. If state is suspendedStart, then
if state == GeneratorState::SuspendedStart { if state == GeneratorState::SuspendedStart {
// a. Set generator.[[GeneratorState]] to completed. // a. Set generator.[[GeneratorState]] to completed.
@ -327,12 +377,10 @@ impl Generator {
// 3. If state is completed, then // 3. If state is completed, then
if state == GeneratorState::Completed { if state == GeneratorState::Completed {
// a. If abruptCompletion.[[Type]] is return, then // a. If abruptCompletion.[[Type]] is return, then
if let Ok(value) = abrupt_completion { // i. Return CreateIterResultObject(abruptCompletion.[[Value]], true).
// i. Return CreateIterResultObject(abruptCompletion.[[Value]], true).
return Ok(create_iter_result_object(value, true, context));
}
// b. Return Completion(abruptCompletion). // b. Return Completion(abruptCompletion).
return abrupt_completion; return abrupt_completion
.map_or_else(CompletionRecord::Throw, CompletionRecord::Normal);
} }
// 4. Assert: state is suspendedYield. // 4. Assert: state is suspendedYield.
@ -352,65 +400,26 @@ impl Generator {
generator.state = GeneratorState::Executing; generator.state = GeneratorState::Executing;
drop(generator_obj_mut); drop(generator_obj_mut);
std::mem::swap( let (value, resume_kind) = match abrupt_completion {
&mut context.vm.environments, Ok(value) => (value, GeneratorResumeKind::Return),
&mut generator_context.environments, Err(err) => (err.to_opaque(context), GeneratorResumeKind::Throw),
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
std::mem::swap(
&mut context.vm.active_function,
&mut generator_context.active_function,
);
let old_realm = context.enter_realm(generator_context.realm.clone());
context.vm.push_frame(generator_context.call_frame.clone());
let completion_record = match abrupt_completion {
Ok(value) => {
context.vm.push(value);
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Return;
context.run()
}
Err(value) => {
let value = value.to_opaque(context);
context.vm.push(value);
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Throw;
context.run()
}
}; };
generator_context.call_frame = context
.vm
.pop_frame()
.expect("generator call frame must exist");
std::mem::swap(
&mut context.vm.environments,
&mut generator_context.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
std::mem::swap(
&mut context.vm.active_function,
&mut generator_context.active_function,
);
context.enter_realm(old_realm);
let mut generator_obj_mut = generator_obj.borrow_mut(); let record = generator_context.resume(Some(value), resume_kind, context);
let mut generator_obj_mut = gen.borrow_mut();
let generator = generator_obj_mut let generator = generator_obj_mut
.as_generator_mut() .as_generator_mut()
.expect("already checked this object type"); .expect("already checked this object type");
match completion_record { generator.state = match record {
CompletionRecord::Return(value) => { CompletionRecord::Return(_) => {
generator.state = GeneratorState::SuspendedYield;
generator.context = Some(generator_context); generator.context = Some(generator_context);
Ok(create_iter_result_object(value, false, context)) GeneratorState::SuspendedYield
} }
CompletionRecord::Normal(value) => { CompletionRecord::Normal(_) | CompletionRecord::Throw(_) => GeneratorState::Completed,
generator.state = GeneratorState::Completed; };
Ok(create_iter_result_object(value, true, context))
} record
CompletionRecord::Throw(err) => {
generator.state = GeneratorState::Completed;
Err(err)
}
}
} }
} }

7
boa_engine/src/context/mod.rs

@ -536,6 +536,13 @@ impl<'host> Context<'host> {
// ==== Private API ==== // ==== Private API ====
impl Context<'_> {
/// Swaps the currently active realm with `realm`.
pub(crate) fn swap_realm(&mut self, realm: &mut Realm) {
std::mem::swap(&mut self.realm, realm);
}
}
#[cfg(feature = "intl")] #[cfg(feature = "intl")]
impl<'host> Context<'host> { impl<'host> Context<'host> {
/// Get the ICU related utilities /// Get the ICU related utilities

20
boa_engine/src/environments/runtime.rs

@ -552,26 +552,6 @@ impl DeclarativeEnvironmentStack {
.expect("environment stack is cannot be empty") .expect("environment stack is cannot be empty")
} }
/// Get the most outer function environment slots.
///
/// # Panics
///
/// Panics if no environment exists on the stack.
pub(crate) fn current_function_slots(&self) -> &EnvironmentSlots {
for env in self
.stack
.iter()
.filter_map(Environment::as_declarative)
.rev()
{
if let Some(slots) = &env.slots {
return slots;
}
}
panic!("global environment must exist")
}
/// Get the most outer environment. /// Get the most outer environment.
/// ///
/// # Panics /// # Panics

15
boa_engine/src/vm/call_frame/mod.rs

@ -5,7 +5,7 @@
mod abrupt_record; mod abrupt_record;
mod env_stack; mod env_stack;
use crate::{object::JsObject, vm::CodeBlock}; use crate::{builtins::promise::PromiseCapability, object::JsObject, vm::CodeBlock};
use boa_gc::{Finalize, Gc, Trace}; use boa_gc::{Finalize, Gc, Trace};
use thin_vec::ThinVec; use thin_vec::ThinVec;
@ -21,7 +21,7 @@ pub struct CallFrame {
#[unsafe_ignore_trace] #[unsafe_ignore_trace]
pub(crate) abrupt_completion: Option<AbruptCompletionRecord>, pub(crate) abrupt_completion: Option<AbruptCompletionRecord>,
#[unsafe_ignore_trace] #[unsafe_ignore_trace]
pub(crate) early_return: Option<EarlyReturnType>, pub(crate) r#yield: bool,
pub(crate) pop_on_return: usize, pub(crate) pop_on_return: usize,
// Tracks the number of environments in environment entry. // Tracks the number of environments in environment entry.
// On abrupt returns this is used to decide how many environments need to be pop'ed. // On abrupt returns this is used to decide how many environments need to be pop'ed.
@ -31,6 +31,7 @@ pub struct CallFrame {
pub(crate) arg_count: usize, pub(crate) arg_count: usize,
#[unsafe_ignore_trace] #[unsafe_ignore_trace]
pub(crate) generator_resume_kind: GeneratorResumeKind, pub(crate) generator_resume_kind: GeneratorResumeKind,
pub(crate) promise_capability: Option<PromiseCapability>,
// When an async generator is resumed, the generator object is needed // When an async generator is resumed, the generator object is needed
// to fulfill the steps 4.e-j in [AsyncGeneratorStart](https://tc39.es/ecma262/#sec-asyncgeneratorstart). // to fulfill the steps 4.e-j in [AsyncGeneratorStart](https://tc39.es/ecma262/#sec-asyncgeneratorstart).
@ -52,10 +53,11 @@ impl CallFrame {
pop_on_return: 0, pop_on_return: 0,
env_stack: Vec::from([EnvStackEntry::new(0, max_length)]), env_stack: Vec::from([EnvStackEntry::new(0, max_length)]),
abrupt_completion: None, abrupt_completion: None,
early_return: None, r#yield: false,
param_count: 0, param_count: 0,
arg_count: 0, arg_count: 0,
generator_resume_kind: GeneratorResumeKind::Normal, generator_resume_kind: GeneratorResumeKind::Normal,
promise_capability: None,
async_generator: None, async_generator: None,
iterators: ThinVec::new(), iterators: ThinVec::new(),
} }
@ -107,10 +109,3 @@ pub(crate) enum GeneratorResumeKind {
Throw, Throw,
Return, Return,
} }
// An enum to mark whether a return is early due to Async or Yield
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum EarlyReturnType {
Await,
Yield,
}

93
boa_engine/src/vm/code_block.rs

@ -824,7 +824,7 @@ impl JsObject {
let context = &mut ContextCleanupGuard::new(context, realm, active_function); let context = &mut ContextCleanupGuard::new(context, realm, active_function);
let (code, mut environments, class_object, promise, async_, gen) = let (code, mut environments, class_object, promise_cap, async_, gen) =
match function_object.kind() { match function_object.kind() {
FunctionKind::Native { FunctionKind::Native {
function, function,
@ -874,7 +874,7 @@ impl JsObject {
code.clone(), code.clone(),
environments.clone(), environments.clone(),
class_object.clone(), class_object.clone(),
Some(promise_capability.promise().clone()), Some(promise_capability.clone()),
true, true,
false, false,
), ),
@ -1009,11 +1009,12 @@ impl JsObject {
let param_count = code.params.as_ref().len(); let param_count = code.params.as_ref().len();
context.vm.push_frame( let mut frame = CallFrame::new(code)
CallFrame::new(code) .with_param_count(param_count)
.with_param_count(param_count) .with_arg_count(arg_count);
.with_arg_count(arg_count), frame.promise_capability = promise_cap.clone();
);
context.vm.push_frame(frame);
let result = context let result = context
.run() .run()
@ -1024,13 +1025,10 @@ impl JsObject {
std::mem::swap(&mut environments, &mut context.vm.environments); std::mem::swap(&mut environments, &mut context.vm.environments);
std::mem::swap(&mut context.vm.stack, &mut stack); std::mem::swap(&mut context.vm.stack, &mut stack);
if let Some(promise) = promise { if let Some(promise_cap) = promise_cap {
return Ok(promise.into()); Ok(promise_cap.promise().clone().into())
} } else if gen {
result?;
let value = result?;
let value = if gen {
let proto = this_function_object let proto = this_function_object
.get(PROTOTYPE, context) .get(PROTOTYPE, context)
.expect("generator must have a prototype property") .expect("generator must have a prototype property")
@ -1045,34 +1043,32 @@ impl JsObject {
}, },
Clone::clone, Clone::clone,
); );
let data = if async_ {
ObjectData::async_generator(AsyncGenerator {
state: AsyncGeneratorState::SuspendedStart,
context: Some(GeneratorContext::new(
environments,
stack,
context.vm.active_function.clone(),
call_frame,
context.realm().clone(),
)),
queue: VecDeque::new(),
})
} else {
ObjectData::generator(Generator {
state: GeneratorState::SuspendedStart,
context: Some(GeneratorContext::new(
environments,
stack,
context.vm.active_function.clone(),
call_frame,
context.realm().clone(),
)),
})
};
let generator = Self::from_proto_and_data( let generator = Self::from_proto_and_data(proto, data);
proto,
if async_ {
ObjectData::async_generator(AsyncGenerator {
state: AsyncGeneratorState::SuspendedStart,
context: Some(GeneratorContext {
environments,
call_frame,
stack,
active_function: context.vm.active_function.clone(),
realm: context.realm().clone(),
}),
queue: VecDeque::new(),
})
} else {
ObjectData::generator(Generator {
state: GeneratorState::SuspendedStart,
context: Some(GeneratorContext {
environments,
call_frame,
stack,
active_function: context.vm.active_function.clone(),
realm: context.realm().clone(),
}),
})
},
);
if async_ { if async_ {
let gen_clone = generator.clone(); let gen_clone = generator.clone();
@ -1081,15 +1077,18 @@ impl JsObject {
.as_async_generator_mut() .as_async_generator_mut()
.expect("must be object here"); .expect("must be object here");
let gen_context = gen.context.as_mut().expect("must exist"); let gen_context = gen.context.as_mut().expect("must exist");
gen_context.call_frame.async_generator = Some(gen_clone); // TODO: try to move this to the context itself.
gen_context
.call_frame
.as_mut()
.expect("should have a call frame initialized")
.async_generator = Some(gen_clone);
} }
generator.into() Ok(generator.into())
} else { } else {
value result
}; }
Ok(value)
} }
pub(crate) fn construct_internal( pub(crate) fn construct_internal(

38
boa_engine/src/vm/mod.rs

@ -7,7 +7,7 @@
use crate::{ use crate::{
builtins::async_generator::{AsyncGenerator, AsyncGeneratorState}, builtins::async_generator::{AsyncGenerator, AsyncGeneratorState},
environments::{DeclarativeEnvironment, DeclarativeEnvironmentStack}, environments::{DeclarativeEnvironment, DeclarativeEnvironmentStack},
vm::{call_frame::EarlyReturnType, code_block::Readable}, vm::code_block::Readable,
Context, JsError, JsObject, JsResult, JsValue, Context, JsError, JsObject, JsResult, JsValue,
}; };
#[cfg(feature = "fuzz")] #[cfg(feature = "fuzz")]
@ -98,6 +98,7 @@ impl Vm {
/// # Panics /// # Panics
/// ///
/// If there is no frame, then this will panic. /// If there is no frame, then this will panic.
#[track_caller]
pub(crate) fn frame(&self) -> &CallFrame { pub(crate) fn frame(&self) -> &CallFrame {
self.frames.last().expect("no frame found") self.frames.last().expect("no frame found")
} }
@ -107,6 +108,7 @@ impl Vm {
/// # Panics /// # Panics
/// ///
/// If there is no frame, then this will panic. /// If there is no frame, then this will panic.
#[track_caller]
pub(crate) fn frame_mut(&mut self) -> &mut CallFrame { pub(crate) fn frame_mut(&mut self) -> &mut CallFrame {
self.frames.last_mut().expect("no frame found") self.frames.last_mut().expect("no frame found")
} }
@ -189,19 +191,7 @@ impl Context<'_> {
// If the current executing function is an async function we have to resolve/reject it's promise at the end. // 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). // The relevant spec section is 3. in [AsyncBlockStart](https://tc39.es/ecma262/#sec-asyncblockstart).
let promise_capability = self let promise_capability = self.vm.frame().promise_capability.clone();
.vm
.environments
.current_function_slots()
.as_function_slots()
.and_then(|slots| {
let slots_borrow = slots.borrow();
let function_object = slots_borrow.function_object();
let function = function_object.borrow();
function
.as_function()
.and_then(|f| f.get_promise_capability().cloned())
});
let execution_completion = loop { let execution_completion = loop {
// 1. Exit the execution loop if program counter ever is equal to or exceeds the amount of instructions // 1. Exit the execution loop if program counter ever is equal to or exceeds the amount of instructions
@ -300,20 +290,10 @@ impl Context<'_> {
}; };
// Early return immediately after loop. // Early return immediately after loop.
if let Some(early_return) = self.vm.frame().early_return { if self.vm.frame().r#yield {
match early_return { let result = self.vm.pop();
EarlyReturnType::Await => { self.vm.frame_mut().r#yield = false;
let result = self.vm.pop(); return CompletionRecord::Return(result);
self.vm.stack.truncate(self.vm.frame().fp);
self.vm.frame_mut().early_return = None;
return CompletionRecord::Normal(result);
}
EarlyReturnType::Yield => {
let result = self.vm.pop();
self.vm.frame_mut().early_return = None;
return CompletionRecord::Return(result);
}
}
} }
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
@ -359,7 +339,6 @@ impl Context<'_> {
}; };
if let Some(promise) = promise_capability { if let Some(promise) = promise_capability {
// Step 3.e-g in [AsyncGeneratorStart](https://tc39.es/ecma262/#sec-asyncgeneratorstart)
match execution_completion { match execution_completion {
CompletionType::Normal => { CompletionType::Normal => {
promise promise
@ -383,6 +362,7 @@ impl Context<'_> {
} }
} }
} else if let Some(generator_object) = self.vm.frame().async_generator.clone() { } else if let Some(generator_object) = self.vm.frame().async_generator.clone() {
// Step 3.e-g in [AsyncGeneratorStart](https://tc39.es/ecma262/#sec-asyncgeneratorstart)
let mut generator_object_mut = generator_object.borrow_mut(); let mut generator_object_mut = generator_object.borrow_mut();
let generator = generator_object_mut let generator = generator_object_mut
.as_async_generator_mut() .as_async_generator_mut()

100
boa_engine/src/vm/opcode/await_stm/mod.rs

@ -1,14 +1,10 @@
use boa_gc::GcRefCell; use boa_gc::{Gc, GcRefCell};
use crate::{ use crate::{
builtins::{generator::GeneratorContext, Promise}, builtins::{generator::GeneratorContext, Promise},
native_function::NativeFunction, native_function::NativeFunction,
object::FunctionObjectBuilder, object::FunctionObjectBuilder,
vm::{ vm::{opcode::Operation, CompletionType, GeneratorResumeKind},
call_frame::{EarlyReturnType, GeneratorResumeKind},
opcode::Operation,
CompletionType,
},
Context, JsArgs, JsResult, JsValue, Context, JsArgs, JsResult, JsValue,
}; };
@ -33,13 +29,9 @@ impl Operation for Await {
context, context,
)?; )?;
let generator_context = GeneratorContext { let gen = GeneratorContext::from_current(context);
environments: context.vm.environments.clone(),
call_frame: context.vm.frame().clone(), let captures = Gc::new(GcRefCell::new(Some(gen)));
stack: context.vm.stack.clone(),
active_function: context.vm.active_function.clone(),
realm: context.realm().clone(),
};
// 3. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures asyncContext and performs the following steps when called: // 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, "", « »). // 4. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »).
@ -47,49 +39,22 @@ impl Operation for Await {
context, context,
NativeFunction::from_copy_closure_with_captures( NativeFunction::from_copy_closure_with_captures(
|_this, args, captures, context| { |_this, args, captures, context| {
let mut generator_context = std::mem::take(&mut *captures.borrow_mut())
.expect("function should only be called once");
// a. Let prevContext be the running execution context. // a. Let prevContext be the running execution context.
// b. Suspend prevContext. // b. Suspend prevContext.
// c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context. // 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. // d. Resume the suspended evaluation of asyncContext using NormalCompletion(value) 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. let mut gen = captures.borrow_mut().take().expect("should only run once");
// f. Return undefined.
std::mem::swap(
&mut context.vm.environments,
&mut generator_context.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
std::mem::swap(
&mut context.vm.active_function,
&mut generator_context.active_function,
);
let old_realm = context.enter_realm(generator_context.realm.clone());
context.vm.push_frame(generator_context.call_frame.clone());
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Normal; gen.resume(
context.vm.push(args.get_or_undefined(0).clone()); Some(args.get_or_undefined(0).clone()),
context.run(); GeneratorResumeKind::Normal,
context,
context
.vm
.pop_frame()
.expect("generator call frame must exist");
std::mem::swap(
&mut context.vm.environments,
&mut generator_context.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
std::mem::swap(
&mut context.vm.active_function,
&mut generator_context.active_function,
); );
context.enter_realm(old_realm); // 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()) Ok(JsValue::undefined())
}, },
GcRefCell::new(Some(generator_context.clone())), captures.clone(),
), ),
) )
.name("") .name("")
@ -102,8 +67,6 @@ impl Operation for Await {
context, context,
NativeFunction::from_copy_closure_with_captures( NativeFunction::from_copy_closure_with_captures(
|_this, args, captures, context| { |_this, args, captures, context| {
let mut generator_context = std::mem::take(&mut *captures.borrow_mut())
.expect("function should only be called once");
// a. Let prevContext be the running execution context. // a. Let prevContext be the running execution context.
// b. Suspend prevContext. // b. Suspend prevContext.
// c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context. // c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context.
@ -111,40 +74,17 @@ impl Operation for Await {
// 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. // 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. // f. Return undefined.
std::mem::swap( let mut gen = captures.borrow_mut().take().expect("should only run once");
&mut context.vm.environments,
&mut generator_context.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
std::mem::swap(
&mut context.vm.active_function,
&mut generator_context.active_function,
);
let old_realm = context.enter_realm(generator_context.realm.clone());
context.vm.push_frame(generator_context.call_frame.clone());
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Throw;
context.vm.push(args.get_or_undefined(0).clone());
context.run();
context gen.resume(
.vm Some(args.get_or_undefined(0).clone()),
.pop_frame() GeneratorResumeKind::Throw,
.expect("generator call frame must exist"); context,
std::mem::swap(
&mut context.vm.environments,
&mut generator_context.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
std::mem::swap(
&mut context.vm.active_function,
&mut generator_context.active_function,
); );
context.enter_realm(old_realm);
Ok(JsValue::undefined()) Ok(JsValue::undefined())
}, },
GcRefCell::new(Some(generator_context)), captures,
), ),
) )
.name("") .name("")
@ -161,7 +101,7 @@ impl Operation for Await {
); );
context.vm.push(JsValue::undefined()); context.vm.push(JsValue::undefined());
context.vm.frame_mut().early_return = Some(EarlyReturnType::Await); context.vm.frame_mut().r#yield = true;
Ok(CompletionType::Return) Ok(CompletionType::Return)
} }
} }

8
boa_engine/src/vm/opcode/generator/mod.rs

@ -6,7 +6,7 @@ use crate::{
error::JsNativeError, error::JsNativeError,
string::utf16, string::utf16,
vm::{ vm::{
call_frame::{AbruptCompletionRecord, EarlyReturnType, GeneratorResumeKind}, call_frame::{AbruptCompletionRecord, GeneratorResumeKind},
opcode::Operation, opcode::Operation,
CompletionType, CompletionType,
}, },
@ -155,7 +155,7 @@ impl Operation for GeneratorNextDelegate {
context.vm.push(iterator.clone()); context.vm.push(iterator.clone());
context.vm.push(next_method.clone()); context.vm.push(next_method.clone());
context.vm.push(value); context.vm.push(value);
context.vm.frame_mut().early_return = Some(EarlyReturnType::Yield); context.vm.frame_mut().r#yield = true;
Ok(CompletionType::Return) Ok(CompletionType::Return)
} }
GeneratorResumeKind::Throw => { GeneratorResumeKind::Throw => {
@ -177,7 +177,7 @@ impl Operation for GeneratorNextDelegate {
context.vm.push(iterator.clone()); context.vm.push(iterator.clone());
context.vm.push(next_method.clone()); context.vm.push(next_method.clone());
context.vm.push(value); context.vm.push(value);
context.vm.frame_mut().early_return = Some(EarlyReturnType::Yield); context.vm.frame_mut().r#yield = true;
return Ok(CompletionType::Return); return Ok(CompletionType::Return);
} }
context.vm.frame_mut().pc = done_address as usize; context.vm.frame_mut().pc = done_address as usize;
@ -207,7 +207,7 @@ impl Operation for GeneratorNextDelegate {
context.vm.push(iterator.clone()); context.vm.push(iterator.clone());
context.vm.push(next_method.clone()); context.vm.push(next_method.clone());
context.vm.push(value); context.vm.push(value);
context.vm.frame_mut().early_return = Some(EarlyReturnType::Yield); context.vm.frame_mut().r#yield = true;
return Ok(CompletionType::Return); return Ok(CompletionType::Return);
} }
context.vm.frame_mut().pc = done_address as usize; context.vm.frame_mut().pc = done_address as usize;

4
boa_engine/src/vm/opcode/generator/yield_stm.rs

@ -1,5 +1,5 @@
use crate::{ use crate::{
vm::{call_frame::EarlyReturnType, opcode::Operation, CompletionType}, vm::{opcode::Operation, CompletionType},
Context, JsResult, Context, JsResult,
}; };
@ -15,7 +15,7 @@ impl Operation for Yield {
const INSTRUCTION: &'static str = "INST - Yield"; const INSTRUCTION: &'static str = "INST - Yield";
fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> { fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
context.vm.frame_mut().early_return = Some(EarlyReturnType::Yield); context.vm.frame_mut().r#yield = true;
Ok(CompletionType::Return) Ok(CompletionType::Return)
} }
} }

39
docs/boa_object.md

@ -76,6 +76,45 @@ their instructions aren't traced.
The `this` value can be changed as well as the arguments that are passed to the function. The `this` value can be changed as well as the arguments that are passed to the function.
### Function `$boa.function.traceable(func, mode)`
Marks a single function as traceable on all future executions of the function. Both useful to mark
several functions as traceable and to trace functions that suspend their execution (async functions,
generators, async generators).
#### Input
```Javascript
function* g() {
yield 1;
yield 2;
yield 3;
}
$boa.function.traceable(g, true);
var iter = g();
iter.next();
iter.next();
iter.next();
```
#### Output
```bash
1μs RestParameterPop <empty>
1μs PushUndefined undefined
2μs Yield undefined
4μs GetName 0000: 'a' 1
0μs Yield 1
1μs GeneratorNext undefined
1μs Pop <empty>
15μs GetName 0001: 'b' 2
1μs Yield 2
1μs GeneratorNext undefined
1μs Pop <empty>
4μs GetName 0002: 'c' 3
1μs Yield 3
```
## Function `$boa.function.flowgraph(func, options)` ## Function `$boa.function.flowgraph(func, options)`
It can be used to get the instruction flowgraph, like the command-line flag. It can be used to get the instruction flowgraph, like the command-line flag.

Loading…
Cancel
Save