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 1 year 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
}
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 {
ObjectInitializer::new(context)
.function(NativeFunction::from_fn_ptr(flowgraph), "flowgraph", 1)
.function(NativeFunction::from_fn_ptr(bytecode), "bytecode", 1)
.function(NativeFunction::from_fn_ptr(trace), "trace", 1)
.function(NativeFunction::from_fn_ptr(traceable), "traceable", 2)
.build()
}

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

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

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

@ -490,49 +490,22 @@ impl AsyncGenerator {
.expect("already checked before")
.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.
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.
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
.borrow_mut()
@ -543,7 +516,8 @@ impl AsyncGenerator {
// 8. Assert: result is never an abrupt 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.
}

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.
pub(crate) fn set_class_object(&mut self, object: JsObject) {
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)]
pub(crate) struct GeneratorContext {
pub(crate) environments: DeclarativeEnvironmentStack,
pub(crate) call_frame: CallFrame,
pub(crate) stack: Vec<JsValue>,
pub(crate) active_function: Option<JsObject>,
pub(crate) call_frame: Option<CallFrame>,
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.
#[derive(Debug, Finalize, Trace)]
pub struct Generator {
@ -117,15 +177,19 @@ impl Generator {
args: &[JsValue],
context: &mut Context<'_>,
) -> 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).
this.as_object().map_or_else(
|| {
Err(JsNativeError::typ()
.with_message("Generator.prototype.next called on non generator")
.into())
},
|obj| Self::generator_resume(obj, args.get_or_undefined(0), context),
)
let completion =
Self::generator_resume(generator, 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.return ( value )`
@ -144,9 +208,17 @@ impl Generator {
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let g be the this value.
let generator = Self::generator_validate(this)?;
// 2. Let C be Completion { [[Type]]: return, [[Value]]: value, [[Target]]: 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 )`
@ -166,46 +238,67 @@ impl Generator {
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let g be the this value.
let generator = Self::generator_validate(this)?;
// 2. Let C be ThrowCompletion(exception).
// 3. Return ? GeneratorResumeAbrupt(g, C, empty).
Self::generator_resume_abrupt(
this,
let completion = Self::generator_resume_abrupt(
generator,
Err(JsError::from_opaque(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),
}
}
/// `27.5.3.3 GeneratorResume ( generator, value, generatorBrand )`
/// `27.5.3.3 GeneratorValidate ( generator, generatorBrand )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-generatorresume
pub(crate) fn generator_resume(
generator_obj: &JsObject,
value: &JsValue,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let state be ? GeneratorValidate(generator, generatorBrand).
/// [spec]:https://tc39.es/ecma262/#sec-generatorvalidate
fn generator_validate(gen: &JsValue) -> JsResult<&JsObject> {
let generator_obj = gen.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("Generator method called on non generator")
})?;
let mut generator_obj_mut = generator_obj.borrow_mut();
let generator = generator_obj_mut.as_generator_mut().ok_or_else(|| {
JsNativeError::typ().with_message("generator resumed on non generator object")
})?;
let state = generator.state;
if state == GeneratorState::Executing {
return Err(JsNativeError::typ()
if generator.state == GeneratorState::Executing {
Err(JsNativeError::typ()
.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).
if state == GeneratorState::Completed {
return Ok(create_iter_result_object(
JsValue::undefined(),
true,
context,
));
return CompletionRecord::Normal(JsValue::undefined());
}
// 3. Assert: state is either suspendedStart or suspendedYield.
@ -227,64 +320,30 @@ impl Generator {
.expect("generator context cannot be empty here");
drop(generator_obj_mut);
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());
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,
let record = generator_context.resume(
(!first_execution).then_some(value),
GeneratorResumeKind::Normal,
context,
);
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
.as_generator_mut()
.expect("already checked this object type");
match record {
CompletionRecord::Return(value) => {
generator.state = GeneratorState::SuspendedYield;
generator.state = match record {
CompletionRecord::Return(_) => {
generator.context = Some(generator_context);
Ok(create_iter_result_object(value, false, context))
}
CompletionRecord::Normal(value) => {
generator.state = GeneratorState::Completed;
Ok(create_iter_result_object(value, true, context))
GeneratorState::SuspendedYield
}
CompletionRecord::Throw(err) => {
generator.state = GeneratorState::Completed;
Err(err)
}
}
CompletionRecord::Normal(_) | CompletionRecord::Throw(_) => GeneratorState::Completed,
};
// 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.
// 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).
record
}
/// `27.5.3.4 GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand )`
@ -294,26 +353,17 @@ impl Generator {
///
/// [spec]: https://tc39.es/ecma262/#sec-generatorresumeabrupt
pub(crate) fn generator_resume_abrupt(
this: &JsValue,
gen: &JsObject,
abrupt_completion: JsResult<JsValue>,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
) -> CompletionRecord {
// 1. Let state be ? GeneratorValidate(generator, generatorBrand).
let generator_obj = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("generator resumed on non generator object")
})?;
let mut generator_obj_mut = generator_obj.borrow_mut();
let generator = generator_obj_mut.as_generator_mut().ok_or_else(|| {
JsNativeError::typ().with_message("generator resumed on non generator object")
})?;
let mut generator_obj_mut = gen.borrow_mut();
let generator = generator_obj_mut
.as_generator_mut()
.expect("already validated the generator object");
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
if state == GeneratorState::SuspendedStart {
// a. Set generator.[[GeneratorState]] to completed.
@ -327,12 +377,10 @@ impl Generator {
// 3. If state is completed, then
if state == GeneratorState::Completed {
// a. If abruptCompletion.[[Type]] is return, then
if let Ok(value) = abrupt_completion {
// i. Return CreateIterResultObject(abruptCompletion.[[Value]], true).
return Ok(create_iter_result_object(value, true, context));
}
// i. Return CreateIterResultObject(abruptCompletion.[[Value]], true).
// b. Return Completion(abruptCompletion).
return abrupt_completion;
return abrupt_completion
.map_or_else(CompletionRecord::Throw, CompletionRecord::Normal);
}
// 4. Assert: state is suspendedYield.
@ -352,65 +400,26 @@ impl Generator {
generator.state = GeneratorState::Executing;
drop(generator_obj_mut);
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 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()
}
let (value, resume_kind) = match abrupt_completion {
Ok(value) => (value, GeneratorResumeKind::Return),
Err(err) => (err.to_opaque(context), GeneratorResumeKind::Throw),
};
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
.as_generator_mut()
.expect("already checked this object type");
match completion_record {
CompletionRecord::Return(value) => {
generator.state = GeneratorState::SuspendedYield;
generator.state = match record {
CompletionRecord::Return(_) => {
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) => {
generator.state = GeneratorState::Completed;
Err(err)
}
}
CompletionRecord::Normal(_) | CompletionRecord::Throw(_) => GeneratorState::Completed,
};
record
}
}

7
boa_engine/src/context/mod.rs

@ -536,6 +536,13 @@ impl<'host> Context<'host> {
// ==== 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")]
impl<'host> Context<'host> {
/// 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")
}
/// 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.
///
/// # Panics

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

@ -5,7 +5,7 @@
mod abrupt_record;
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 thin_vec::ThinVec;
@ -21,7 +21,7 @@ pub struct CallFrame {
#[unsafe_ignore_trace]
pub(crate) abrupt_completion: Option<AbruptCompletionRecord>,
#[unsafe_ignore_trace]
pub(crate) early_return: Option<EarlyReturnType>,
pub(crate) r#yield: bool,
pub(crate) pop_on_return: usize,
// Tracks the number of environments in environment entry.
// 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,
#[unsafe_ignore_trace]
pub(crate) generator_resume_kind: GeneratorResumeKind,
pub(crate) promise_capability: Option<PromiseCapability>,
// 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).
@ -52,10 +53,11 @@ impl CallFrame {
pop_on_return: 0,
env_stack: Vec::from([EnvStackEntry::new(0, max_length)]),
abrupt_completion: None,
early_return: None,
r#yield: false,
param_count: 0,
arg_count: 0,
generator_resume_kind: GeneratorResumeKind::Normal,
promise_capability: None,
async_generator: None,
iterators: ThinVec::new(),
}
@ -107,10 +109,3 @@ pub(crate) enum GeneratorResumeKind {
Throw,
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 (code, mut environments, class_object, promise, async_, gen) =
let (code, mut environments, class_object, promise_cap, async_, gen) =
match function_object.kind() {
FunctionKind::Native {
function,
@ -874,7 +874,7 @@ impl JsObject {
code.clone(),
environments.clone(),
class_object.clone(),
Some(promise_capability.promise().clone()),
Some(promise_capability.clone()),
true,
false,
),
@ -1009,11 +1009,12 @@ impl JsObject {
let param_count = code.params.as_ref().len();
context.vm.push_frame(
CallFrame::new(code)
.with_param_count(param_count)
.with_arg_count(arg_count),
);
let mut frame = CallFrame::new(code)
.with_param_count(param_count)
.with_arg_count(arg_count);
frame.promise_capability = promise_cap.clone();
context.vm.push_frame(frame);
let result = context
.run()
@ -1024,13 +1025,10 @@ impl JsObject {
std::mem::swap(&mut environments, &mut context.vm.environments);
std::mem::swap(&mut context.vm.stack, &mut stack);
if let Some(promise) = promise {
return Ok(promise.into());
}
let value = result?;
let value = if gen {
if let Some(promise_cap) = promise_cap {
Ok(promise_cap.promise().clone().into())
} else if gen {
result?;
let proto = this_function_object
.get(PROTOTYPE, context)
.expect("generator must have a prototype property")
@ -1045,34 +1043,32 @@ impl JsObject {
},
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(
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(),
}),
})
},
);
let generator = Self::from_proto_and_data(proto, data);
if async_ {
let gen_clone = generator.clone();
@ -1081,15 +1077,18 @@ impl JsObject {
.as_async_generator_mut()
.expect("must be object here");
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 {
value
};
Ok(value)
result
}
}
pub(crate) fn construct_internal(

38
boa_engine/src/vm/mod.rs

@ -7,7 +7,7 @@
use crate::{
builtins::async_generator::{AsyncGenerator, AsyncGeneratorState},
environments::{DeclarativeEnvironment, DeclarativeEnvironmentStack},
vm::{call_frame::EarlyReturnType, code_block::Readable},
vm::code_block::Readable,
Context, JsError, JsObject, JsResult, JsValue,
};
#[cfg(feature = "fuzz")]
@ -98,6 +98,7 @@ impl Vm {
/// # Panics
///
/// If there is no frame, then this will panic.
#[track_caller]
pub(crate) fn frame(&self) -> &CallFrame {
self.frames.last().expect("no frame found")
}
@ -107,6 +108,7 @@ impl Vm {
/// # Panics
///
/// If there is no frame, then this will panic.
#[track_caller]
pub(crate) fn frame_mut(&mut self) -> &mut CallFrame {
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.
// The relevant spec section is 3. in [AsyncBlockStart](https://tc39.es/ecma262/#sec-asyncblockstart).
let promise_capability = self
.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 promise_capability = self.vm.frame().promise_capability.clone();
let execution_completion = loop {
// 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.
if let Some(early_return) = self.vm.frame().early_return {
match early_return {
EarlyReturnType::Await => {
let result = self.vm.pop();
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);
}
}
if self.vm.frame().r#yield {
let result = self.vm.pop();
self.vm.frame_mut().r#yield = false;
return CompletionRecord::Return(result);
}
#[cfg(feature = "trace")]
@ -359,7 +339,6 @@ impl Context<'_> {
};
if let Some(promise) = promise_capability {
// Step 3.e-g in [AsyncGeneratorStart](https://tc39.es/ecma262/#sec-asyncgeneratorstart)
match execution_completion {
CompletionType::Normal => {
promise
@ -383,6 +362,7 @@ impl Context<'_> {
}
}
} 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 generator = generator_object_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::{
builtins::{generator::GeneratorContext, Promise},
native_function::NativeFunction,
object::FunctionObjectBuilder,
vm::{
call_frame::{EarlyReturnType, GeneratorResumeKind},
opcode::Operation,
CompletionType,
},
vm::{opcode::Operation, CompletionType, GeneratorResumeKind},
Context, JsArgs, JsResult, JsValue,
};
@ -33,13 +29,9 @@ impl Operation for Await {
context,
)?;
let generator_context = GeneratorContext {
environments: context.vm.environments.clone(),
call_frame: context.vm.frame().clone(),
stack: context.vm.stack.clone(),
active_function: context.vm.active_function.clone(),
realm: context.realm().clone(),
};
let gen = GeneratorContext::from_current(context);
let captures = Gc::new(GcRefCell::new(Some(gen)));
// 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, "", « »).
@ -47,49 +39,22 @@ impl Operation for Await {
context,
NativeFunction::from_copy_closure_with_captures(
|_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.
// 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.
// 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.
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 mut gen = captures.borrow_mut().take().expect("should only run once");
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Normal;
context.vm.push(args.get_or_undefined(0).clone());
context.run();
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,
gen.resume(
Some(args.get_or_undefined(0).clone()),
GeneratorResumeKind::Normal,
context,
);
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())
},
GcRefCell::new(Some(generator_context.clone())),
captures.clone(),
),
)
.name("")
@ -102,8 +67,6 @@ impl Operation for Await {
context,
NativeFunction::from_copy_closure_with_captures(
|_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.
// b. Suspend prevContext.
// 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.
// 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::Throw;
context.vm.push(args.get_or_undefined(0).clone());
context.run();
let mut gen = captures.borrow_mut().take().expect("should only run once");
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,
gen.resume(
Some(args.get_or_undefined(0).clone()),
GeneratorResumeKind::Throw,
context,
);
context.enter_realm(old_realm);
Ok(JsValue::undefined())
},
GcRefCell::new(Some(generator_context)),
captures,
),
)
.name("")
@ -161,7 +101,7 @@ impl Operation for Await {
);
context.vm.push(JsValue::undefined());
context.vm.frame_mut().early_return = Some(EarlyReturnType::Await);
context.vm.frame_mut().r#yield = true;
Ok(CompletionType::Return)
}
}

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

@ -6,7 +6,7 @@ use crate::{
error::JsNativeError,
string::utf16,
vm::{
call_frame::{AbruptCompletionRecord, EarlyReturnType, GeneratorResumeKind},
call_frame::{AbruptCompletionRecord, GeneratorResumeKind},
opcode::Operation,
CompletionType,
},
@ -155,7 +155,7 @@ impl Operation for GeneratorNextDelegate {
context.vm.push(iterator.clone());
context.vm.push(next_method.clone());
context.vm.push(value);
context.vm.frame_mut().early_return = Some(EarlyReturnType::Yield);
context.vm.frame_mut().r#yield = true;
Ok(CompletionType::Return)
}
GeneratorResumeKind::Throw => {
@ -177,7 +177,7 @@ impl Operation for GeneratorNextDelegate {
context.vm.push(iterator.clone());
context.vm.push(next_method.clone());
context.vm.push(value);
context.vm.frame_mut().early_return = Some(EarlyReturnType::Yield);
context.vm.frame_mut().r#yield = true;
return Ok(CompletionType::Return);
}
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(next_method.clone());
context.vm.push(value);
context.vm.frame_mut().early_return = Some(EarlyReturnType::Yield);
context.vm.frame_mut().r#yield = true;
return Ok(CompletionType::Return);
}
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::{
vm::{call_frame::EarlyReturnType, opcode::Operation, CompletionType},
vm::{opcode::Operation, CompletionType},
Context, JsResult,
};
@ -15,7 +15,7 @@ impl Operation for Yield {
const INSTRUCTION: &'static str = "INST - Yield";
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)
}
}

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.
### 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)`
It can be used to get the instruction flowgraph, like the command-line flag.

Loading…
Cancel
Save