diff --git a/boa_engine/src/builtins/function/mod.rs b/boa_engine/src/builtins/function/mod.rs index 68bb0761a2e..76d7473496c 100644 --- a/boa_engine/src/builtins/function/mod.rs +++ b/boa_engine/src/builtins/function/mod.rs @@ -775,6 +775,7 @@ impl BuiltInFunctionObject { let code = FunctionCompiler::new() .name(Sym::ANONYMOUS) .generator(true) + .r#async(r#async) .compile( &FormalParameterList::default(), &FunctionBody::default(), @@ -793,12 +794,15 @@ impl BuiltInFunctionObject { Ok(function_object) } else { - let code = FunctionCompiler::new().name(Sym::ANONYMOUS).compile( - &FormalParameterList::default(), - &FunctionBody::default(), - context.realm().environment().compile_env(), - context, - ); + let code = FunctionCompiler::new() + .r#async(r#async) + .name(Sym::ANONYMOUS) + .compile( + &FormalParameterList::default(), + &FunctionBody::default(), + context.realm().environment().compile_env(), + context, + ); let environments = context.vm.environments.pop_to_global(); let function_object = diff --git a/boa_engine/src/bytecompiler/declarations.rs b/boa_engine/src/bytecompiler/declarations.rs index 67dc3180582..dc4ae66241f 100644 --- a/boa_engine/src/bytecompiler/declarations.rs +++ b/boa_engine/src/bytecompiler/declarations.rs @@ -1010,11 +1010,10 @@ impl ByteCompiler<'_, '_> { if !formals.has_rest_parameter() { self.emit_opcode(Opcode::RestParameterPop); } + if generator { - self.emit_opcode(Opcode::PushUndefined); - // Don't need to use `AsyncGeneratorYield` since - // we just want to stop the execution of the generator. - self.emit_opcode(Opcode::GeneratorYield); + self.emit_opcode(Opcode::Generator); + self.emit_u8(self.in_async().into()); self.emit_opcode(Opcode::Pop); } diff --git a/boa_engine/src/bytecompiler/function.rs b/boa_engine/src/bytecompiler/function.rs index 9ceffde0075..5418488d3f3 100644 --- a/boa_engine/src/bytecompiler/function.rs +++ b/boa_engine/src/bytecompiler/function.rs @@ -4,7 +4,7 @@ use crate::{ builtins::function::ThisMode, bytecompiler::ByteCompiler, environments::CompileTimeEnvironment, - vm::{CodeBlock, CodeBlockFlags}, + vm::{CodeBlock, CodeBlockFlags, Opcode}, Context, }; use boa_ast::function::{FormalParameterList, FunctionBody}; @@ -120,6 +120,31 @@ impl FunctionCompiler { // Function environment compiler.push_compile_environment(true); + // Taken from: + // - 15.9.3 Runtime Semantics: EvaluateAsyncConciseBody: + // - 15.8.4 Runtime Semantics: EvaluateAsyncFunctionBody: + // + // Note: In `EvaluateAsyncGeneratorBody` unlike the async non-generator functions we don't handle exceptions thrown by + // `FunctionDeclarationInstantiation` (so they are propagated). + // + // See: 15.6.2 Runtime Semantics: EvaluateAsyncGeneratorBody: https://tc39.es/ecma262/#sec-runtime-semantics-evaluateasyncgeneratorbody + if compiler.in_async() && !compiler.in_generator() { + // 1. Let promiseCapability be ! NewPromiseCapability(%Promise%). + // + // Note: If the promise capability is already set, then we do nothing. + // This is a deviation from the spec, but it allows to set the promise capability by + // ExecuteAsyncModule ( module ): + compiler.emit_opcode(Opcode::CreatePromiseCapability); + + // 2. Let declResult be Completion(FunctionDeclarationInstantiation(functionObject, argumentsList)). + // + // Note: We push an exception handler so we catch exceptions that are thrown by the + // `FunctionDeclarationInstantiation` abstract function. + // + // Patched in `ByteCompiler::finish()`. + compiler.async_handler = Some(compiler.push_handler()); + } + let (env_label, additional_env) = compiler.function_declaration_instantiation( body, parameters, @@ -128,6 +153,18 @@ impl FunctionCompiler { self.generator, ); + // Taken from: + // - 27.6.3.2 AsyncGeneratorStart ( generator, generatorBody ): + // + // Note: We do handle exceptions thrown by generator body in `AsyncGeneratorStart`. + if compiler.in_generator() { + assert!(compiler.async_handler.is_none()); + if compiler.in_async() { + // Patched in `ByteCompiler::finish()`. + compiler.async_handler = Some(compiler.push_handler()); + } + } + compiler.compile_statement_list(body.statements(), false, false); if let Some(env_labels) = env_label { diff --git a/boa_engine/src/bytecompiler/jump_control.rs b/boa_engine/src/bytecompiler/jump_control.rs index d531eab6ec6..f98191386af 100644 --- a/boa_engine/src/bytecompiler/jump_control.rs +++ b/boa_engine/src/bytecompiler/jump_control.rs @@ -122,7 +122,23 @@ impl JumpRecord { match self.kind { JumpRecordKind::Break => compiler.patch_jump(self.label), JumpRecordKind::Continue => compiler.patch_jump_with_target(self.label, start_address), - JumpRecordKind::Return => compiler.emit_opcode(Opcode::Return), + JumpRecordKind::Return => { + match (compiler.in_async(), compiler.in_generator()) { + // Taken from: + // - 27.6.3.2 AsyncGeneratorStart ( generator, generatorBody ): https://tc39.es/ecma262/#sec-asyncgeneratorstart + // + // Note: If we are returning we have to close the async generator function. + (true, true) => compiler.emit_opcode(Opcode::AsyncGeneratorClose), + + // Taken from: + // - 27.7.5.2 AsyncBlockStart ( promiseCapability, asyncBody, asyncContext ): + // + // Note: If there is promise capability resolve or reject it based on pending exception. + (true, false) => compiler.emit_opcode(Opcode::CompletePromiseCapability), + (_, _) => {} + } + compiler.emit_opcode(Opcode::Return); + } } } } diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index af9dc5c7298..f10e050082f 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/boa_engine/src/bytecompiler/mod.rs @@ -255,8 +255,13 @@ pub struct ByteCompiler<'ctx, 'host> { names_map: FxHashMap, bindings_map: FxHashMap, jump_info: Vec, - in_async: bool, + pub(crate) in_async: bool, in_generator: bool, + + /// Used to handle exception throws that escape the async function types. + /// + /// Async functions and async generator functions, need to be closed and resolved. + pub(crate) async_handler: Option, json_parse: bool, // TODO: remove when we separate scripts from the context @@ -305,6 +310,7 @@ impl<'ctx, 'host> ByteCompiler<'ctx, 'host> { jump_info: Vec::new(), in_async: false, in_generator: false, + async_handler: None, json_parse, current_environment, context, @@ -1410,7 +1416,10 @@ impl<'ctx, 'host> ByteCompiler<'ctx, 'host> { #[allow(clippy::missing_const_for_fn)] pub fn finish(mut self) -> CodeBlock { // Push return at the end of the function compilation. - self.emit_opcode(Opcode::Return); + if let Some(async_handler) = self.async_handler { + self.patch_handler(async_handler); + } + self.r#return(); let name = self .context diff --git a/boa_engine/src/environments/runtime/mod.rs b/boa_engine/src/environments/runtime/mod.rs index 262af76675b..dc5fc8a01e6 100644 --- a/boa_engine/src/environments/runtime/mod.rs +++ b/boa_engine/src/environments/runtime/mod.rs @@ -78,6 +78,18 @@ impl EnvironmentStack { self.stack[0] = Environment::Declarative(global); } + /// Gets the current global environment. + pub(crate) fn global(&self) -> Gc { + let env = self.stack[0].clone(); + + match env { + Environment::Declarative(ref env) => env.clone(), + Environment::Object(_) => { + unreachable!("first environment should be the global environment") + } + } + } + /// Extends the length of the next outer function environment to the number of compiled bindings. /// /// This is only useful when compiled bindings are added after the initial compilation (eval). diff --git a/boa_engine/src/module/source.rs b/boa_engine/src/module/source.rs index f079b15ded1..21e74a9e100 100644 --- a/boa_engine/src/module/source.rs +++ b/boa_engine/src/module/source.rs @@ -1414,6 +1414,10 @@ impl SourceTextModule { let mut compiler = ByteCompiler::new(Sym::MAIN, true, false, module_compile_env.clone(), context); + + compiler.in_async = true; + compiler.async_handler = Some(compiler.push_handler()); + let mut imports = Vec::new(); let (codeblock, functions) = { diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index bce2c9ece8d..5c2a235919a 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -3,12 +3,7 @@ //! This module is for the `CodeBlock` which implements a function representation in the VM use crate::{ - builtins::{ - async_generator::{AsyncGenerator, AsyncGeneratorState}, - function::{arguments::Arguments, ConstructorKind, Function, FunctionKind, ThisMode}, - generator::{Generator, GeneratorContext, GeneratorState}, - promise::PromiseCapability, - }, + builtins::function::{arguments::Arguments, ConstructorKind, Function, FunctionKind, ThisMode}, context::intrinsics::StandardConstructors, environments::{BindingLocator, CompileTimeEnvironment, FunctionSlots, ThisBindingStatus}, error::JsNativeError, @@ -22,7 +17,7 @@ use bitflags::bitflags; use boa_ast::function::FormalParameterList; use boa_gc::{empty_trace, Finalize, Gc, Trace}; use boa_profiler::Profiler; -use std::{cell::Cell, collections::VecDeque, mem::size_of, rc::Rc}; +use std::{cell::Cell, mem::size_of, rc::Rc}; use thin_vec::ThinVec; #[cfg(any(feature = "trace", feature = "flowgraph"))] @@ -325,6 +320,11 @@ impl CodeBlock { *pc += size_of::(); result } + Opcode::Generator => { + let result = self.read::(*pc); + *pc += size_of::(); + format!("async: {}", result != 0) + } Opcode::PushInt8 => { let result = self.read::(*pc).to_string(); *pc += size_of::(); @@ -570,6 +570,9 @@ impl CodeBlock { | Opcode::This | Opcode::Super | Opcode::Return + | Opcode::AsyncGeneratorClose + | Opcode::CreatePromiseCapability + | Opcode::CompletePromiseCapability | Opcode::PopEnvironment | Opcode::IncrementLoopIteration | Opcode::CreateForInIterator @@ -674,11 +677,7 @@ impl CodeBlock { | Opcode::Reserved56 | Opcode::Reserved57 | Opcode::Reserved58 - | Opcode::Reserved59 - | Opcode::Reserved60 - | Opcode::Reserved61 - | Opcode::Reserved62 - | Opcode::Reserved63 => unreachable!("Reserved opcodes are unrechable"), + | Opcode::Reserved59 => unreachable!("Reserved opcodes are unrechable"), } } } @@ -1072,7 +1071,7 @@ impl JsObject { context.enter_realm(realm); context.vm.active_function = Some(active_function); - let (code, mut environments, class_object, mut script_or_module, async_, gen) = + let (code, mut environments, class_object, mut script_or_module) = match function_object.kind() { FunctionKind::Native { function, @@ -1108,44 +1107,26 @@ impl JsObject { environments.clone(), class_object.clone(), script_or_module.clone(), - false, - false, ) } FunctionKind::Async { code, environments, class_object, - script_or_module, .. - } => ( - code.clone(), - environments.clone(), - class_object.clone(), - script_or_module.clone(), - true, - false, - ), - FunctionKind::Generator { + } + | FunctionKind::Generator { code, environments, class_object, script_or_module, .. - } => ( - code.clone(), - environments.clone(), - class_object.clone(), - script_or_module.clone(), - false, - true, - ), - FunctionKind::AsyncGenerator { + } + | FunctionKind::AsyncGenerator { code, environments, class_object, - script_or_module, .. } => ( @@ -1153,21 +1134,11 @@ impl JsObject { environments.clone(), class_object.clone(), script_or_module.clone(), - true, - true, ), }; drop(object); - let promise_capability = (async_ && !gen).then(|| { - PromiseCapability::new( - &context.intrinsics().constructors().promise().constructor(), - context, - ) - .expect("cannot fail per spec") - }); - std::mem::swap(&mut environments, &mut context.vm.environments); let lexical_this_mode = code.this_mode == ThisMode::Lexical; @@ -1277,10 +1248,9 @@ impl JsObject { std::mem::swap(&mut context.vm.stack, &mut stack); - let mut frame = CallFrame::new(code) + let frame = CallFrame::new(code) .with_argument_count(argument_count as u32) .with_env_fp(env_fp); - frame.promise_capability = promise_capability.clone(); std::mem::swap(&mut context.vm.active_runnable, &mut script_or_module); @@ -1291,78 +1261,12 @@ impl JsObject { .consume() .map_err(|err| err.inject_realm(context.realm().clone())); - let call_frame = context.vm.pop_frame().expect("frame must exist"); + context.vm.pop_frame().expect("frame must exist"); std::mem::swap(&mut environments, &mut context.vm.environments); std::mem::swap(&mut context.vm.stack, &mut stack); std::mem::swap(&mut context.vm.active_runnable, &mut script_or_module); - if let Some(promise_capability) = promise_capability { - Ok(promise_capability.promise().clone().into()) - } else if gen { - result?; - let proto = this_function_object - .get(PROTOTYPE, context) - .expect("generator must have a prototype property") - .as_object() - .map_or_else( - || { - if async_ { - context.intrinsics().objects().async_generator() - } else { - context.intrinsics().objects().generator() - } - }, - 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: GeneratorContext::new( - environments, - stack, - context.vm.active_function.clone(), - call_frame, - context.realm().clone(), - ), - }, - }) - }; - - let generator = - Self::from_proto_and_data_with_shared_shape(context.root_shape(), proto, data); - - if async_ { - let gen_clone = generator.clone(); - let mut generator_mut = generator.borrow_mut(); - let gen = generator_mut - .as_async_generator_mut() - .expect("must be object here"); - let gen_context = gen.context.as_mut().expect("must exist"); - // 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); - } - - Ok(generator.into()) - } else { - result - } + result } pub(crate) fn construct_internal( diff --git a/boa_engine/src/vm/flowgraph/mod.rs b/boa_engine/src/vm/flowgraph/mod.rs index ff210e418c6..5dcd45fdd49 100644 --- a/boa_engine/src/vm/flowgraph/mod.rs +++ b/boa_engine/src/vm/flowgraph/mod.rs @@ -62,6 +62,12 @@ impl CodeBlock { graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); } + Opcode::Generator => { + pc += size_of::(); + + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } Opcode::PushInt8 => { pc += size_of::(); @@ -517,6 +523,9 @@ impl CodeBlock { | Opcode::PushNewArray | Opcode::GeneratorYield | Opcode::AsyncGeneratorYield + | Opcode::AsyncGeneratorClose + | Opcode::CreatePromiseCapability + | Opcode::CompletePromiseCapability | Opcode::GeneratorNext | Opcode::PushClassField | Opcode::SuperCallDerived @@ -603,11 +612,7 @@ impl CodeBlock { | Opcode::Reserved56 | Opcode::Reserved57 | Opcode::Reserved58 - | Opcode::Reserved59 - | Opcode::Reserved60 - | Opcode::Reserved61 - | Opcode::Reserved62 - | Opcode::Reserved63 => unreachable!("Reserved opcodes are unrechable"), + | Opcode::Reserved59 => unreachable!("Reserved opcodes are unrechable"), } } diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index 754294930ae..8ddef1b24de 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -7,11 +7,10 @@ #[cfg(feature = "fuzz")] use crate::JsNativeError; use crate::{ - builtins::async_generator::{AsyncGenerator, AsyncGeneratorState}, environments::{DeclarativeEnvironment, EnvironmentStack}, script::Script, vm::code_block::Readable, - Context, JsError, JsObject, JsResult, JsValue, Module, + Context, JsError, JsNativeErrorKind, JsObject, JsResult, JsValue, Module, }; use boa_gc::{custom_trace, Finalize, Gc, Trace}; @@ -150,7 +149,9 @@ impl Vm { self.frames.last_mut().expect("no frame found") } - pub(crate) fn push_frame(&mut self, frame: CallFrame) { + pub(crate) fn push_frame(&mut self, mut frame: CallFrame) { + let current_stack_length = self.stack.len(); + frame.set_frame_pointer(current_stack_length as u32); self.frames.push(frame); } @@ -190,6 +191,93 @@ pub(crate) enum CompletionType { Yield, } +#[cfg(feature = "trace")] +impl Context<'_> { + const COLUMN_WIDTH: usize = 26; + const TIME_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH / 2; + const OPCODE_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH; + const OPERAND_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH; + const NUMBER_OF_COLUMNS: usize = 4; + + fn trace_call_frame(&self) { + let msg = if self.vm.frames.last().is_some() { + format!( + " Call Frame -- {} ", + self.vm.frame().code_block().name().to_std_string_escaped() + ) + } else { + " VM Start ".to_string() + }; + + println!( + "{}", + self.vm + .frame() + .code_block + .to_interned_string(self.interner()) + ); + println!( + "{msg:-^width$}", + width = Self::COLUMN_WIDTH * Self::NUMBER_OF_COLUMNS - 10 + ); + println!( + "{: JsResult { + let mut pc = self.vm.frame().pc as usize; + let opcode: Opcode = self.vm.frame().code_block.read::(pc).into(); + let operands = self + .vm + .frame() + .code_block + .instruction_operands(&mut pc, self.interner()); + + let instant = Instant::now(); + let result = self.execute_instruction(); + + let duration = instant.elapsed(); + + let stack = { + let mut stack = String::from("[ "); + for (i, value) in self.vm.stack.iter().rev().enumerate() { + match value { + value if value.is_callable() => stack.push_str("[function]"), + value if value.is_object() => stack.push_str("[object]"), + value => stack.push_str(&value.display().to_string()), + } + + if i + 1 != self.vm.stack.len() { + stack.push(','); + } + + stack.push(' '); + } + + stack.push(']'); + stack + }; + + println!( + "{: { fn execute_instruction(&mut self) -> JsResult { let opcode: Opcode = { @@ -198,7 +286,7 @@ impl Context<'_> { let frame = self.vm.frame_mut(); let pc = frame.pc; - let opcode = Opcode::from(frame.code_block.bytecode[pc as usize]); + let opcode = frame.code_block.bytecode[pc as usize].into(); frame.pc += 1; opcode }; @@ -209,100 +297,27 @@ impl Context<'_> { } pub(crate) fn run(&mut self) -> CompletionRecord { - #[cfg(feature = "trace")] - const COLUMN_WIDTH: usize = 26; - #[cfg(feature = "trace")] - const TIME_COLUMN_WIDTH: usize = COLUMN_WIDTH / 2; - #[cfg(feature = "trace")] - const OPCODE_COLUMN_WIDTH: usize = COLUMN_WIDTH; - #[cfg(feature = "trace")] - const OPERAND_COLUMN_WIDTH: usize = COLUMN_WIDTH; - #[cfg(feature = "trace")] - const NUMBER_OF_COLUMNS: usize = 4; - let _timer = Profiler::global().start_event("run", "vm"); #[cfg(feature = "trace")] if self.vm.trace { - let msg = if self.vm.frames.last().is_some() { - " Call Frame " - } else { - " VM Start " - }; - - println!( - "{}\n", - self.vm - .frame() - .code_block - .to_interned_string(self.interner()) - ); - println!( - "{msg:-^width$}", - width = COLUMN_WIDTH * NUMBER_OF_COLUMNS - 10 - ); - println!( - "{:(pc) - .try_into() - .expect("invalid opcode"); - let operands = self - .vm - .frame() - .code_block - .instruction_operands(&mut pc, self.interner()); - - let instant = Instant::now(); - let result = self.execute_instruction(); - - let duration = instant.elapsed(); - println!( - "{: "[function]".to_string(), - Some(value) if value.is_object() => "[object]".to_string(), - Some(value) => value.display().to_string(), - None => "".to_string(), - }, - ); - - result + self.trace_execute_instruction() } else { self.execute_instruction() }; @@ -310,14 +325,21 @@ impl Context<'_> { #[cfg(not(feature = "trace"))] let result = self.execute_instruction(); - // 2. Evaluate the result of executing the instruction. match result { Ok(CompletionType::Normal) => {} Ok(CompletionType::Return) => { - break CompletionType::Return; + self.vm.stack.truncate(self.vm.frame().fp as usize); + let execution_result = self.vm.frame_mut().return_value.clone(); + return CompletionRecord::Normal(execution_result); } Ok(CompletionType::Throw) => { - break CompletionType::Throw; + self.vm.stack.truncate(self.vm.frame().fp as usize); + return CompletionRecord::Throw( + self.vm + .pending_exception + .take() + .expect("Err must exist for a CompletionType::Throw"), + ); } // Early return immediately. Ok(CompletionType::Yield) => { @@ -325,144 +347,33 @@ impl Context<'_> { return CompletionRecord::Return(result); } Err(err) => { - #[cfg(feature = "fuzz")] - { - if let Some(native_error) = err.as_native() { - // If we hit the execution step limit, bubble up the error to the - // (Rust) caller instead of trying to handle as an exception. - if native_error.is_no_instructions_remain() { - self.vm.pending_exception = Some(err); - break CompletionType::Throw; - } - } - } - if let Some(native_error) = err.as_native() { // If we hit the execution step limit, bubble up the error to the // (Rust) caller instead of trying to handle as an exception. - if native_error.is_runtime_limit() { - self.vm.pending_exception = Some(err); - break CompletionType::Throw; + match native_error.kind { + #[cfg(feature = "fuzz")] + JsNativeErrorKind::NoInstructionsRemain => { + return CompletionRecord::Throw(err); + } + JsNativeErrorKind::RuntimeLimit => { + self.vm.stack.truncate(self.vm.frame().fp as usize); + return CompletionRecord::Throw(err); + } + _ => {} } } - self.vm.pending_exception = Some(err); - - let evaluation = Opcode::ReThrow - .execute(self) - .expect("Opcode::Throw cannot return Err"); - - if evaluation == CompletionType::Normal { + // Note: -1 because we increment after fetching the opcode. + let pc = self.vm.frame().pc.saturating_sub(1); + if self.vm.handle_exception_at(pc) { + self.vm.pending_exception = Some(err); continue; } - break CompletionType::Throw; + self.vm.stack.truncate(self.vm.frame().fp as usize); + return CompletionRecord::Throw(err); } } - }; - - #[cfg(feature = "trace")] - if self.vm.trace { - println!("\nStack:"); - if self.vm.stack.is_empty() { - println!(" "); - } else { - for (i, value) in self.vm.stack.iter().enumerate() { - println!( - "{i:04}{: { - promise - .resolve() - .call(&JsValue::undefined(), &[], self) - .expect("cannot fail per spec"); - } - CompletionType::Return => { - promise - .resolve() - .call(&JsValue::undefined(), &[execution_result.clone()], self) - .expect("cannot fail per spec"); - } - CompletionType::Throw => { - let err = self - .vm - .pending_exception - .take() - .expect("Take must exist on a Throw"); - promise - .reject() - .call(&JsValue::undefined(), &[err.to_opaque(self)], self) - .expect("cannot fail per spec"); - self.vm.pending_exception = Some(err); - } - CompletionType::Yield => unreachable!("this is handled before"), - } - } 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() - .expect("must be async generator"); - - generator.state = AsyncGeneratorState::Completed; - generator.context = None; - - let next = generator - .queue - .pop_front() - .expect("must have item in queue"); - drop(generator_object_mut); - - if execution_completion == CompletionType::Throw { - AsyncGenerator::complete_step( - &next, - Err(self - .vm - .pending_exception - .take() - .expect("err must exist on a Completion::Throw")), - true, - None, - self, - ); - } else { - AsyncGenerator::complete_step(&next, Ok(execution_result), true, None, self); - } - AsyncGenerator::drain_queue(&generator_object, self); - - return CompletionRecord::Normal(JsValue::undefined()); - } - - // Any valid return statement is re-evaluated as a normal completion vs. return (yield). - if execution_completion == CompletionType::Throw { - return CompletionRecord::Throw( - self.vm - .pending_exception - .take() - .expect("Err must exist for a CompletionType::Throw"), - ); } - CompletionRecord::Normal(execution_result) } } diff --git a/boa_engine/src/vm/opcode/await_stm/mod.rs b/boa_engine/src/vm/opcode/await/mod.rs similarity index 64% rename from boa_engine/src/vm/opcode/await_stm/mod.rs rename to boa_engine/src/vm/opcode/await/mod.rs index 25c92b767de..f952580c39b 100644 --- a/boa_engine/src/vm/opcode/await_stm/mod.rs +++ b/boa_engine/src/vm/opcode/await/mod.rs @@ -125,7 +125,79 @@ impl Operation for Await { context, ); - context.vm.push(JsValue::undefined()); + if let Some(promise_capabality) = context.vm.frame().promise_capability.clone() { + context.vm.push(promise_capabality.promise().clone()); + } else { + context.vm.push(JsValue::undefined()); + } Ok(CompletionType::Yield) } } + +/// `CreatePromiseCapability` implements the Opcode Operation for `Opcode::CreatePromiseCapability` +/// +/// Operation: +/// - Create a promise capacity for an async function, if not already set. +#[derive(Debug, Clone, Copy)] +pub(crate) struct CreatePromiseCapability; + +impl Operation for CreatePromiseCapability { + const NAME: &'static str = "CreatePromiseCapability"; + const INSTRUCTION: &'static str = "INST - CreatePromiseCapability"; + + fn execute(context: &mut Context<'_>) -> JsResult { + if context.vm.frame().promise_capability.is_some() { + return Ok(CompletionType::Normal); + } + + let promise_capability = crate::builtins::promise::PromiseCapability::new( + &context.intrinsics().constructors().promise().constructor(), + context, + ) + .expect("cannot fail per spec"); + + context.vm.frame_mut().promise_capability = Some(promise_capability); + Ok(CompletionType::Normal) + } +} + +/// `CompletePromiseCapability` implements the Opcode Operation for `Opcode::CompletePromiseCapability` +/// +/// Operation: +/// - Resolves or rejects the promise capability, depending if the pending exception is set. +#[derive(Debug, Clone, Copy)] +pub(crate) struct CompletePromiseCapability; + +impl Operation for CompletePromiseCapability { + const NAME: &'static str = "CompletePromiseCapability"; + const INSTRUCTION: &'static str = "INST - CompletePromiseCapability"; + + fn execute(context: &mut Context<'_>) -> JsResult { + // 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_mut().promise_capability.take() else { + return if context.vm.pending_exception.is_some() { + Ok(CompletionType::Throw) + } else { + Ok(CompletionType::Normal) + }; + }; + + if let Some(error) = context.vm.pending_exception.take() { + promise_capability + .reject() + .call(&JsValue::undefined(), &[error.to_opaque(context)], context) + .expect("cannot fail per spec"); + } else { + let return_value = context.vm.frame().return_value.clone(); + promise_capability + .resolve() + .call(&JsValue::undefined(), &[return_value], context) + .expect("cannot fail per spec"); + }; + + context.vm.frame_mut().return_value = promise_capability.promise().clone().into(); + + Ok(CompletionType::Normal) + } +} diff --git a/boa_engine/src/vm/opcode/jump/mod.rs b/boa_engine/src/vm/opcode/control_flow/jump.rs similarity index 100% rename from boa_engine/src/vm/opcode/jump/mod.rs rename to boa_engine/src/vm/opcode/control_flow/jump.rs diff --git a/boa_engine/src/vm/opcode/control_flow/mod.rs b/boa_engine/src/vm/opcode/control_flow/mod.rs index a3ac8d1488f..7f8ddc3340a 100644 --- a/boa_engine/src/vm/opcode/control_flow/mod.rs +++ b/boa_engine/src/vm/opcode/control_flow/mod.rs @@ -1,5 +1,7 @@ +pub(crate) mod jump; pub(crate) mod r#return; pub(crate) mod throw; +pub(crate) use jump::*; pub(crate) use r#return::*; pub(crate) use throw::*; diff --git a/boa_engine/src/vm/opcode/generator/mod.rs b/boa_engine/src/vm/opcode/generator/mod.rs index 6b060c415d1..34d4242a7a9 100644 --- a/boa_engine/src/vm/opcode/generator/mod.rs +++ b/boa_engine/src/vm/opcode/generator/mod.rs @@ -1,20 +1,172 @@ pub(crate) mod yield_stm; +use std::collections::VecDeque; + use crate::{ + builtins::{ + async_generator::{AsyncGenerator, AsyncGeneratorState}, + generator::{GeneratorContext, GeneratorState}, + }, + environments::EnvironmentStack, error::JsNativeError, + object::{ObjectData, PROTOTYPE}, string::utf16, vm::{ call_frame::GeneratorResumeKind, opcode::{Operation, ReThrow}, - CompletionType, + CallFrame, CompletionType, }, - Context, JsError, JsResult, + Context, JsError, JsObject, JsResult, }; pub(crate) use yield_stm::*; use super::SetReturnValue; +/// `Generator` implements the Opcode Operation for `Opcode::Generator` +/// +/// Operation: +/// - Creates the generator object and yields. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Generator; + +impl Operation for Generator { + const NAME: &'static str = "Generator"; + const INSTRUCTION: &'static str = "INST - Generator"; + + fn execute(context: &mut Context<'_>) -> JsResult { + let r#async = context.vm.read::() != 0; + + let code_block = context.vm.frame().code_block().clone(); + let pc = context.vm.frame().pc; + let mut dummy_call_frame = CallFrame::new(code_block); + dummy_call_frame.pc = pc; + let call_frame = std::mem::replace(context.vm.frame_mut(), dummy_call_frame); + + let this_function_object = context + .vm + .active_function + .clone() + .expect("active function should be set to the generator"); + + let proto = this_function_object + .get(PROTOTYPE, context) + .expect("generator must have a prototype property") + .as_object() + .map_or_else( + || { + if r#async { + context.intrinsics().objects().async_generator() + } else { + context.intrinsics().objects().generator() + } + }, + Clone::clone, + ); + + let global_environement = context.vm.environments.global(); + let environments = std::mem::replace( + &mut context.vm.environments, + EnvironmentStack::new(global_environement), + ); + let stack = std::mem::take(&mut context.vm.stack); + + let data = if r#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(crate::builtins::generator::Generator { + state: GeneratorState::SuspendedStart { + context: GeneratorContext::new( + environments, + stack, + context.vm.active_function.clone(), + call_frame, + context.realm().clone(), + ), + }, + }) + }; + + let generator = + JsObject::from_proto_and_data_with_shared_shape(context.root_shape(), proto, data); + + if r#async { + let gen_clone = generator.clone(); + let mut generator_mut = generator.borrow_mut(); + let gen = generator_mut + .as_async_generator_mut() + .expect("must be object here"); + let gen_context = gen.context.as_mut().expect("must exist"); + // 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); + } + + context.vm.push(generator); + Ok(CompletionType::Yield) + } +} + +/// `AsyncGeneratorClose` implements the Opcode Operation for `Opcode::AsyncGeneratorClose` +/// +/// Operation: +/// - Close an async generator function. +#[derive(Debug, Clone, Copy)] +pub(crate) struct AsyncGeneratorClose; + +impl Operation for AsyncGeneratorClose { + const NAME: &'static str = "AsyncGeneratorClose"; + const INSTRUCTION: &'static str = "INST - AsyncGeneratorClose"; + + fn execute(context: &mut Context<'_>) -> JsResult { + // Step 3.e-g in [AsyncGeneratorStart](https://tc39.es/ecma262/#sec-asyncgeneratorstart) + let generator_object = context + .vm + .frame() + .async_generator + .clone() + .expect("There should be a object"); + + let mut generator_object_mut = generator_object.borrow_mut(); + let generator = generator_object_mut + .as_async_generator_mut() + .expect("must be async generator"); + + generator.state = AsyncGeneratorState::Completed; + generator.context = None; + + let next = generator + .queue + .pop_front() + .expect("must have item in queue"); + drop(generator_object_mut); + + let return_value = std::mem::take(&mut context.vm.frame_mut().return_value); + + if let Some(error) = context.vm.pending_exception.take() { + AsyncGenerator::complete_step(&next, Err(error), true, None, context); + } else { + AsyncGenerator::complete_step(&next, Ok(return_value), true, None, context); + } + AsyncGenerator::drain_queue(&generator_object, context); + + Ok(CompletionType::Normal) + } +} + /// `GeneratorNext` implements the Opcode Operation for `Opcode::GeneratorNext` /// /// Operation: diff --git a/boa_engine/src/vm/opcode/mod.rs b/boa_engine/src/vm/opcode/mod.rs index f67794efa84..eb253d5c2fb 100644 --- a/boa_engine/src/vm/opcode/mod.rs +++ b/boa_engine/src/vm/opcode/mod.rs @@ -2,7 +2,7 @@ use crate::{vm::CompletionType, Context, JsResult}; // Operation modules -mod await_stm; +mod r#await; mod binary_ops; mod call; mod concat; @@ -15,7 +15,6 @@ mod environment; mod generator; mod get; mod iteration; -mod jump; mod meta; mod new; mod nop; @@ -33,8 +32,6 @@ mod value; // Operation structs #[doc(inline)] -pub(crate) use await_stm::*; -#[doc(inline)] pub(crate) use binary_ops::*; #[doc(inline)] pub(crate) use call::*; @@ -59,8 +56,6 @@ pub(crate) use get::*; #[doc(inline)] pub(crate) use iteration::*; #[doc(inline)] -pub(crate) use jump::*; -#[doc(inline)] pub(crate) use meta::*; #[doc(inline)] pub(crate) use new::*; @@ -71,6 +66,8 @@ pub(crate) use pop::*; #[doc(inline)] pub(crate) use push::*; #[doc(inline)] +pub(crate) use r#await::*; +#[doc(inline)] pub(crate) use require::*; #[doc(inline)] pub(crate) use rest_parameter::*; @@ -1333,6 +1330,20 @@ generate_impl! { /// Stack: **=>** Return, + /// Close an async generator function. + /// + /// Operands: + /// + /// Stack: **=>** + AsyncGeneratorClose, + + /// Creates the generator object and yields. + /// + /// Operands: async: `u8` + /// + /// Stack: **=>** resume_kind + Generator, + /// Get return value of a function. /// /// Operands: @@ -1561,6 +1572,22 @@ generate_impl! { /// Stack: value **=>** received AsyncGeneratorYield, + /// Create a promise capacity for an async function, if not already set. + /// + /// Operands: + /// + /// Stack: **=>** + CreatePromiseCapability, + + /// Resolves or rejects the promise capability of an async function. + /// + /// If the pending exception is set, reject and rethrow the exception, otherwise resolve. + /// + /// Operands: + /// + /// Stack: **=>** + CompletePromiseCapability, + /// Jumps to the specified address if the resume kind is not equal. /// /// Operands: `exit`: `u32`, `resume_kind`: `u8`. @@ -1763,14 +1790,6 @@ generate_impl! { Reserved58 => Reserved, /// Reserved [`Opcode`]. Reserved59 => Reserved, - /// Reserved [`Opcode`]. - Reserved60 => Reserved, - /// Reserved [`Opcode`]. - Reserved61 => Reserved, - /// Reserved [`Opcode`]. - Reserved62 => Reserved, - /// Reserved [`Opcode`]. - Reserved63 => Reserved, } }