diff --git a/core/bindings.rs b/core/bindings.rs index 52ecf7bac556f7..03ffc187bdcdee 100644 --- a/core/bindings.rs +++ b/core/bindings.rs @@ -271,7 +271,7 @@ pub extern "C" fn host_import_module_dynamically_callback( let resolver_handle = v8::Global::new(scope, resolver); { let state_rc = JsRuntime::state(scope); - let module_map_rc = JsRuntime::module_map(scope); + let module_map_rc = JsRealm::module_map_from_scope(scope); debug!( "dyn_import specifier {} referrer {} ", @@ -308,7 +308,7 @@ pub extern "C" fn host_initialize_import_meta_object_callback( ) { // SAFETY: `CallbackScope` can be safely constructed from `Local` let scope = &mut unsafe { v8::CallbackScope::new(context) }; - let module_map_rc = JsRuntime::module_map(scope); + let module_map_rc = JsRealm::module_map_from_scope(scope); let module_map = module_map_rc.borrow(); let module_global = v8::Global::new(scope, module); @@ -349,7 +349,7 @@ fn import_meta_resolve( let url_prop = args.data().unwrap(); url_prop.to_rust_string_lossy(scope) }; - let module_map_rc = JsRuntime::module_map(scope); + let module_map_rc = JsRealm::module_map_from_scope(scope); let loader = { let module_map = module_map_rc.borrow(); module_map.loader.clone() @@ -459,9 +459,9 @@ pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) { }; if has_unhandled_rejection_handler { - let state_rc = JsRuntime::state(tc_scope); - let mut state = state_rc.borrow_mut(); - if let Some(pending_mod_evaluate) = state.pending_mod_evaluate.as_mut() { + if let Some(pending_mod_evaluate) = + realm_state_rc.borrow_mut().pending_mod_evaluate.as_mut() + { if !pending_mod_evaluate.has_evaluated { pending_mod_evaluate .handled_promise_rejections @@ -557,7 +557,7 @@ pub fn module_resolve_callback<'s>( // SAFETY: `CallbackScope` can be safely constructed from `Local` let scope = &mut unsafe { v8::CallbackScope::new(context) }; - let module_map_rc = JsRuntime::module_map(scope); + let module_map_rc = JsRealm::module_map_from_scope(scope); let module_map = module_map_rc.borrow(); let referrer_global = v8::Global::new(scope, referrer); diff --git a/core/modules.rs b/core/modules.rs index 65b3852d919490..a6f9e8d1b943a4 100644 --- a/core/modules.rs +++ b/core/modules.rs @@ -5,6 +5,7 @@ use crate::error::generic_error; use crate::module_specifier::ModuleSpecifier; use crate::resolve_import; use crate::resolve_url; +use crate::JsRealm; use crate::OpState; use anyhow::Error; use futures::future::FutureExt; @@ -125,10 +126,7 @@ fn json_module_evaluation_steps<'a>( // SAFETY: `CallbackScope` can be safely constructed from `Local` let scope = &mut unsafe { v8::CallbackScope::new(context) }; let tc_scope = &mut v8::TryCatch::new(scope); - let module_map = tc_scope - .get_slot::>>() - .unwrap() - .clone(); + let module_map = JsRealm::module_map_from_scope(tc_scope); let handle = v8::Global::::new(tc_scope, module); let value_handle = module_map @@ -1392,7 +1390,7 @@ import "/a.js"; ] ); - let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); + let module_map_rc = runtime.global_realm().module_map(runtime.v8_isolate()); let modules = module_map_rc.borrow(); assert_eq!( @@ -1504,7 +1502,7 @@ import "/a.js"; assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0); - let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); + let module_map_rc = runtime.global_realm().module_map(runtime.v8_isolate()); let (mod_a, mod_b) = { let scope = &mut runtime.handle_scope(); @@ -1547,11 +1545,17 @@ import "/a.js"; (mod_a, mod_b) }; - runtime.instantiate_module(mod_b).unwrap(); + runtime + .global_realm() + .instantiate_module(runtime.v8_isolate(), mod_b) + .unwrap(); assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0); assert_eq!(resolve_count.load(Ordering::SeqCst), 1); - runtime.instantiate_module(mod_a).unwrap(); + runtime + .global_realm() + .instantiate_module(runtime.v8_isolate(), mod_a) + .unwrap(); assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0); let _ = runtime.mod_evaluate(mod_a); @@ -1611,7 +1615,7 @@ import "/a.js"; ) .unwrap(); - let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); + let module_map_rc = runtime.global_realm().module_map(runtime.v8_isolate()); let (mod_a, mod_b) = { let scope = &mut runtime.handle_scope(); @@ -1651,10 +1655,16 @@ import "/a.js"; (mod_a, mod_b) }; - runtime.instantiate_module(mod_b).unwrap(); + runtime + .global_realm() + .instantiate_module(runtime.v8_isolate(), mod_b) + .unwrap(); assert_eq!(resolve_count.load(Ordering::SeqCst), 1); - runtime.instantiate_module(mod_a).unwrap(); + runtime + .global_realm() + .instantiate_module(runtime.v8_isolate(), mod_a) + .unwrap(); let receiver = runtime.mod_evaluate(mod_a); futures::executor::block_on(runtime.run_event_loop(false)).unwrap(); @@ -1946,7 +1956,8 @@ import "/a.js"; ] ); - let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); + let module_map_rc = + runtime.global_realm().module_map(runtime.v8_isolate()); let modules = module_map_rc.borrow(); assert_eq!( @@ -2025,7 +2036,8 @@ import "/a.js"; ] ); - let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); + let module_map_rc = + runtime.global_realm().module_map(runtime.v8_isolate()); let modules = module_map_rc.borrow(); assert_eq!( @@ -2180,7 +2192,7 @@ if (import.meta.url != 'file:///main_with_code.js') throw Error(); vec!["file:///b.js", "file:///c.js", "file:///d.js"] ); - let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); + let module_map_rc = runtime.global_realm().module_map(runtime.v8_isolate()); let modules = module_map_rc.borrow(); assert_eq!( diff --git a/core/runtime.rs b/core/runtime.rs index f6d6881867d7f2..b2f65195043c3e 100644 --- a/core/runtime.rs +++ b/core/runtime.rs @@ -153,6 +153,9 @@ pub(crate) struct ContextState { pub(crate) js_format_exception_cb: Option>, pub(crate) js_wasm_streaming_cb: Option>, pub(crate) unrefed_ops: HashSet, + // + pending_dyn_mod_evaluate: Vec, + pub(crate) pending_mod_evaluate: Option, } /// Internal state for JsRuntime which is stored in one of v8::Isolate's @@ -165,8 +168,7 @@ pub(crate) struct JsRuntimeState { pub(crate) has_tick_scheduled: bool, pub(crate) pending_promise_exceptions: HashMap, v8::Global>, - pub(crate) pending_dyn_mod_evaluate: Vec, - pub(crate) pending_mod_evaluate: Option, + module_loader: Rc, /// A counter used to delay our dynamic import deadlock detection by one spin /// of the event loop. dyn_module_evaluate_idle_counter: u32, @@ -430,8 +432,7 @@ impl JsRuntime { global_realm: Some(JsRealm(global_context.clone())), known_realms, pending_promise_exceptions: HashMap::new(), - pending_dyn_mod_evaluate: vec![], - pending_mod_evaluate: None, + module_loader: loader.clone(), dyn_module_evaluate_idle_counter: 0, js_macrotask_cbs: vec![], js_nexttick_cbs: vec![], @@ -449,12 +450,13 @@ impl JsRuntime { waker: AtomicWaker::new(), }))); - global_context - .open(&mut isolate) - .set_slot(&mut isolate, Rc::>::default()); + { + let context = global_context.open(&mut isolate); + context.set_slot(&mut isolate, Rc::>::default()); - let module_map = ModuleMap::new(loader, op_state); - isolate.set_slot(Rc::new(RefCell::new(module_map))); + let module_map = ModuleMap::new(loader, op_state); + context.set_slot(&mut isolate, Rc::new(RefCell::new(module_map))); + } let mut js_runtime = Self { v8_isolate: Some(isolate), @@ -517,10 +519,14 @@ impl JsRuntime { ); context.set_slot(scope, Rc::>::default()); - Self::state(scope) - .borrow_mut() - .known_realms - .push(v8::Weak::new(scope, &context)); + let state_rc = Self::state(scope); + let mut state = state_rc.borrow_mut(); + + let module_map = + ModuleMap::new(state.module_loader.clone(), state.op_state.clone()); + context.set_slot(scope, Rc::new(RefCell::new(module_map))); + + state.known_realms.push(v8::Weak::new(scope, &context)); JsRealm::new(v8::Global::new(scope, context)) }; @@ -558,11 +564,6 @@ impl JsRuntime { s.clone() } - pub(crate) fn module_map(isolate: &v8::Isolate) -> Rc> { - let module_map = isolate.get_slot::>>().unwrap(); - module_map.clone() - } - /// Initializes JS of provided Extensions in the given realm fn init_extension_js(&mut self, realm: &JsRealm) -> Result<(), Error> { // Take extensions to avoid double-borrow @@ -813,32 +814,9 @@ impl JsRuntime { &mut self, module_id: ModuleId, ) -> Result, Error> { - let module_map_rc = Self::module_map(self.v8_isolate()); - - let module_handle = module_map_rc - .borrow() - .get_handle(module_id) - .expect("ModuleInfo not found"); - - let scope = &mut self.handle_scope(); - - let module = module_handle.open(scope); - - if module.get_status() == v8::ModuleStatus::Errored { - let exception = module.get_exception(); - return exception_to_err_result(scope, exception, false); - } - - assert!(matches!( - module.get_status(), - v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated - )); - - let module_namespace: v8::Local = - v8::Local::try_from(module.get_module_namespace()) - .map_err(|err: v8::DataError| generic_error(err.to_string()))?; - - Ok(v8::Global::new(scope, module_namespace)) + self + .global_realm() + .get_module_namespace(self.v8_isolate(), module_id) } /// Registers a callback on the isolate when the memory limits are approached. @@ -967,7 +945,6 @@ impl JsRuntime { // We always poll the inspector first let _ = self.inspector().borrow_mut().poll_unpin(cx); let state_rc = Self::state(self.v8_isolate()); - let module_map_rc = Self::module_map(self.v8_isolate()); { let state = state_rc.borrow(); state.waker.register(cx.waker()); @@ -1037,7 +1014,6 @@ impl JsRuntime { } let mut state = state_rc.borrow_mut(); - let module_map = module_map_rc.borrow(); // Check if more async ops have been dispatched // during this turn of event loop. @@ -1079,12 +1055,22 @@ impl JsRuntime { } else if state.dyn_module_evaluate_idle_counter >= 1 { let mut msg = "Dynamically imported module evaluation is still pending but there are no pending ops. This situation is often caused by unresolved promises. Pending dynamic modules:\n".to_string(); - for pending_evaluate in &state.pending_dyn_mod_evaluate { - let module_info = module_map - .get_info_by_id(&pending_evaluate.module_id) - .unwrap(); - msg.push_str("- "); - msg.push_str(module_info.name.as_str()); + for weak_context in &state.known_realms { + if let Some(context) = weak_context.to_global(self.v8_isolate()) { + let realm = JsRealm::new(context); + let module_map_rc = realm.module_map(self.v8_isolate()); + let realm_state_rc = realm.state(self.v8_isolate()); + for pending_evaluate in + &realm_state_rc.borrow().pending_dyn_mod_evaluate + { + let module_map = module_map_rc.borrow(); + let module_info = module_map + .get_info_by_id(&pending_evaluate.module_id) + .unwrap(); + msg.push_str("- "); + msg.push_str(module_info.name.as_str()); + } + } } return Poll::Ready(Err(generic_error(msg))); } else { @@ -1103,25 +1089,39 @@ Pending dynamic modules:\n".to_string(); isolate: &mut v8::Isolate, ) -> EventLoopPendingState { let state_rc = Self::state(isolate); - let module_map_rc = Self::module_map(isolate); let state = state_rc.borrow_mut(); - let module_map = module_map_rc.borrow(); let mut num_unrefed_ops = 0; + let mut has_pending_dyn_imports = false; + let mut has_pending_dyn_module_evaluation = false; + let mut has_pending_module_evaluation = false; for weak_context in &state.known_realms { if let Some(context) = weak_context.to_global(isolate) { let realm = JsRealm::new(context); - num_unrefed_ops += realm.state(isolate).borrow().unrefed_ops.len(); + let realm_state_rc = realm.state(isolate); + let realm_state = realm_state_rc.borrow(); + num_unrefed_ops += realm_state.unrefed_ops.len(); + if !has_pending_dyn_imports { + let module_map = realm.module_map(isolate); + has_pending_dyn_imports = + module_map.borrow().has_pending_dynamic_imports(); + } + if !has_pending_dyn_module_evaluation { + has_pending_dyn_module_evaluation = + !realm_state.pending_dyn_mod_evaluate.is_empty(); + } + if !has_pending_module_evaluation { + has_pending_module_evaluation = + realm_state.pending_mod_evaluate.is_some(); + } } } EventLoopPendingState { has_pending_refed_ops: state.pending_ops.len() > num_unrefed_ops, - has_pending_dyn_imports: module_map.has_pending_dynamic_imports(), - has_pending_dyn_module_evaluation: !state - .pending_dyn_mod_evaluate - .is_empty(), - has_pending_module_evaluation: state.pending_mod_evaluate.is_some(), + has_pending_dyn_imports, + has_pending_dyn_module_evaluation, + has_pending_module_evaluation, has_pending_background_tasks: isolate.has_pending_background_tasks(), has_tick_scheduled: state.has_tick_scheduled, } @@ -1227,114 +1227,44 @@ pub(crate) fn exception_to_err_result<'s, T>( // Related to module loading impl JsRuntime { - pub(crate) fn instantiate_module( + fn prepare_dyn_imports( &mut self, - id: ModuleId, - ) -> Result<(), v8::Global> { - let module_map_rc = Self::module_map(self.v8_isolate()); - let scope = &mut self.handle_scope(); - let tc_scope = &mut v8::TryCatch::new(scope); - - let module = module_map_rc - .borrow() - .get_handle(id) - .map(|handle| v8::Local::new(tc_scope, handle)) - .expect("ModuleInfo not found"); - - if module.get_status() == v8::ModuleStatus::Errored { - return Err(v8::Global::new(tc_scope, module.get_exception())); - } - - // IMPORTANT: No borrows to `ModuleMap` can be held at this point because - // `module_resolve_callback` will be calling into `ModuleMap` from within - // the isolate. - let instantiate_result = - module.instantiate_module(tc_scope, bindings::module_resolve_callback); - - if instantiate_result.is_none() { - let exception = tc_scope.exception().unwrap(); - return Err(v8::Global::new(tc_scope, exception)); + cx: &mut Context, + ) -> Poll> { + let state = Self::state(self.v8_isolate()); + // TODO(realm): Don't clone? + let known_realms = state.borrow().known_realms.clone(); + for context in known_realms { + if !context.is_empty() { + let realm = JsRealm::new(context.to_global(self.v8_isolate()).unwrap()); + match realm.prepare_dyn_imports(self.v8_isolate(), cx) { + Poll::Ready(Ok(())) => {} + _ => unreachable!(), + } + } } - Ok(()) + Poll::Ready(Ok(())) } - fn dynamic_import_module_evaluate( - &mut self, - load_id: ModuleLoadId, - id: ModuleId, - ) -> Result<(), Error> { - let state_rc = Self::state(self.v8_isolate()); - let module_map_rc = Self::module_map(self.v8_isolate()); - - let module_handle = module_map_rc - .borrow() - .get_handle(id) - .expect("ModuleInfo not found"); - - let status = { - let scope = &mut self.handle_scope(); - let module = module_handle.open(scope); - module.get_status() - }; - - match status { - v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated => {} - _ => return Ok(()), - } - - // IMPORTANT: Top-level-await is enabled, which means that return value - // of module evaluation is a promise. - // - // This promise is internal, and not the same one that gets returned to - // the user. We add an empty `.catch()` handler so that it does not result - // in an exception if it rejects. That will instead happen for the other - // promise if not handled by the user. - // - // For more details see: - // https://github.com/denoland/deno/issues/4908 - // https://v8.dev/features/top-level-await#module-execution-order - let scope = &mut self.handle_scope(); - let tc_scope = &mut v8::TryCatch::new(scope); - let module = v8::Local::new(tc_scope, &module_handle); - let maybe_value = module.evaluate(tc_scope); - - // Update status after evaluating. - let status = module.get_status(); - - if let Some(value) = maybe_value { - assert!( - status == v8::ModuleStatus::Evaluated - || status == v8::ModuleStatus::Errored - ); - let promise = v8::Local::::try_from(value) - .expect("Expected to get promise as module evaluation result"); - let empty_fn = bindings::create_empty_fn(tc_scope).unwrap(); - promise.catch(tc_scope, empty_fn); - let mut state = state_rc.borrow_mut(); - let promise_global = v8::Global::new(tc_scope, promise); - let module_global = v8::Global::new(tc_scope, module); - - let dyn_import_mod_evaluate = DynImportModEvaluate { - load_id, - module_id: id, - promise: promise_global, - module: module_global, - }; - - state.pending_dyn_mod_evaluate.push(dyn_import_mod_evaluate); - } else if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { - return Err( - generic_error("Cannot evaluate dynamically imported module, because JavaScript execution has been terminated.") - ); - } else { - assert!(status == v8::ModuleStatus::Errored); + fn poll_dyn_imports(&mut self, cx: &mut Context) -> Poll> { + let state = Self::state(self.v8_isolate()); + // TODO(realm): Don't clone? + let known_realms = state.borrow().known_realms.clone(); + for context in known_realms { + if !context.is_empty() { + let realm = JsRealm::new(context.to_global(self.v8_isolate()).unwrap()); + match realm.poll_dyn_imports(self.v8_isolate(), cx) { + Poll::Ready(Ok(())) => {} + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + _ => unreachable!(), + } + } } - Ok(()) + Poll::Ready(Ok(())) } - // TODO(bartlomieju): make it return `ModuleEvaluationFuture`? /// Evaluates an already instantiated ES module. /// /// Returns a receiver handle that resolves when module promise resolves. @@ -1349,774 +1279,1069 @@ impl JsRuntime { &mut self, id: ModuleId, ) -> oneshot::Receiver> { - let state_rc = Self::state(self.v8_isolate()); - let module_map_rc = Self::module_map(self.v8_isolate()); - let scope = &mut self.handle_scope(); - let tc_scope = &mut v8::TryCatch::new(scope); - - let module = module_map_rc - .borrow() - .get_handle(id) - .map(|handle| v8::Local::new(tc_scope, handle)) - .expect("ModuleInfo not found"); - let mut status = module.get_status(); - assert_eq!(status, v8::ModuleStatus::Instantiated); - - let (sender, receiver) = oneshot::channel(); - - // IMPORTANT: Top-level-await is enabled, which means that return value - // of module evaluation is a promise. - // - // Because that promise is created internally by V8, when error occurs during - // module evaluation the promise is rejected, and since the promise has no rejection - // handler it will result in call to `bindings::promise_reject_callback` adding - // the promise to pending promise rejection table - meaning JsRuntime will return - // error on next poll(). - // - // This situation is not desirable as we want to manually return error at the - // end of this function to handle it further. It means we need to manually - // remove this promise from pending promise rejection table. - // - // For more details see: - // https://github.com/denoland/deno/issues/4908 - // https://v8.dev/features/top-level-await#module-execution-order - { - let mut state = state_rc.borrow_mut(); - assert!( - state.pending_mod_evaluate.is_none(), - "There is already pending top level module evaluation" - ); - state.pending_mod_evaluate = Some(ModEvaluate { - promise: None, - has_evaluated: false, - handled_promise_rejections: vec![], - sender, - }); - } + self.global_realm().mod_evaluate(self.v8_isolate(), id) + } - let maybe_value = module.evaluate(tc_scope); - { - let mut state = state_rc.borrow_mut(); - let pending_mod_evaluate = state.pending_mod_evaluate.as_mut().unwrap(); - pending_mod_evaluate.has_evaluated = true; + /// "deno_core" runs V8 with Top Level Await enabled. It means that each + /// module evaluation returns a promise from V8. + /// Feature docs: https://v8.dev/features/top-level-await + /// + /// This promise resolves after all dependent modules have also + /// resolved. Each dependent module may perform calls to "import()" and APIs + /// using async ops will add futures to the runtime's event loop. + /// It means that the promise returned from module evaluation will + /// resolve only after all futures in the event loop are done. + /// + /// Thus during turn of event loop we need to check if V8 has + /// resolved or rejected the promise. If the promise is still pending + /// then another turn of event loop must be performed. + fn evaluate_pending_module(&mut self) { + let state = Self::state(self.v8_isolate()); + // TODO(realm): Don't clone? + let known_realms = state.borrow().known_realms.clone(); + for context in known_realms { + if !context.is_empty() { + let realm = JsRealm::new(context.to_global(self.v8_isolate()).unwrap()); + realm.evaluate_pending_module(self.v8_isolate()); + } } + } - // Update status after evaluating. - status = module.get_status(); + // Returns true if some dynamic import was resolved. + fn evaluate_dyn_imports(&mut self) -> bool { + let mut resolved = false; - let has_dispatched_exception = - !state_rc.borrow_mut().dispatched_exceptions.is_empty(); - if has_dispatched_exception { - // This will be overrided in `exception_to_err_result()`. - let exception = v8::undefined(tc_scope).into(); - let pending_mod_evaluate = { - let mut state = state_rc.borrow_mut(); - state.pending_mod_evaluate.take().unwrap() - }; - pending_mod_evaluate - .sender - .send(exception_to_err_result(tc_scope, exception, false)) - .expect("Failed to send module evaluation error."); - } else if let Some(value) = maybe_value { - assert!( - status == v8::ModuleStatus::Evaluated - || status == v8::ModuleStatus::Errored - ); - let promise = v8::Local::::try_from(value) - .expect("Expected to get promise as module evaluation result"); - let promise_global = v8::Global::new(tc_scope, promise); - let mut state = state_rc.borrow_mut(); - { - let pending_mod_evaluate = state.pending_mod_evaluate.as_ref().unwrap(); - let pending_rejection_was_already_handled = pending_mod_evaluate - .handled_promise_rejections - .contains(&promise_global); - if !pending_rejection_was_already_handled { - state.pending_promise_exceptions.remove(&promise_global); + let state = Self::state(self.v8_isolate()); + // Iterate through all realms, even after we know that some dynamic imports + // have been resolved. + // TODO(realm): Don't clone? + let known_realms = state.borrow().known_realms.clone(); + for context in known_realms { + if !context.is_empty() { + let realm = JsRealm::new(context.to_global(self.v8_isolate()).unwrap()); + if realm.evaluate_dyn_imports(self.v8_isolate()) { + resolved = true; } } - let promise_global = v8::Global::new(tc_scope, promise); - state.pending_mod_evaluate.as_mut().unwrap().promise = - Some(promise_global); - tc_scope.perform_microtask_checkpoint(); - } else if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { - let pending_mod_evaluate = { - let mut state = state_rc.borrow_mut(); - state.pending_mod_evaluate.take().unwrap() - }; - pending_mod_evaluate.sender.send(Err( - generic_error("Cannot evaluate module, because JavaScript execution has been terminated.") - )).expect("Failed to send module evaluation error."); - } else { - assert!(status == v8::ModuleStatus::Errored); } - receiver + resolved } - fn dynamic_import_reject( + /// Asynchronously load specified module and all of its dependencies. + /// + /// The module will be marked as "main", and because of that + /// "import.meta.main" will return true when checked inside that module. + /// + /// User must call [`JsRuntime::mod_evaluate`] with returned `ModuleId` + /// manually after load is finished. + pub async fn load_main_module( &mut self, - id: ModuleLoadId, - exception: v8::Global, - ) { - let module_map_rc = Self::module_map(self.v8_isolate()); - let scope = &mut self.handle_scope(); - - let resolver_handle = module_map_rc - .borrow_mut() - .dynamic_import_map - .remove(&id) - .expect("Invalid dynamic import id"); - let resolver = resolver_handle.open(scope); + specifier: &ModuleSpecifier, + code: Option, + ) -> Result { + self + .global_realm() + .load_main_module(self.v8_isolate(), specifier, code) + .await + } - // IMPORTANT: No borrows to `ModuleMap` can be held at this point because - // rejecting the promise might initiate another `import()` which will - // in turn call `bindings::host_import_module_dynamically_callback` which - // will reach into `ModuleMap` from within the isolate. - let exception = v8::Local::new(scope, exception); - resolver.reject(scope, exception).unwrap(); - scope.perform_microtask_checkpoint(); + /// Asynchronously load specified ES module and all of its dependencies. + /// + /// This method is meant to be used when loading some utility code that + /// might be later imported by the main module (ie. an entry point module). + /// + /// User must call [`JsRuntime::mod_evaluate`] with returned `ModuleId` + /// manually after load is finished. + pub async fn load_side_module( + &mut self, + specifier: &ModuleSpecifier, + code: Option, + ) -> Result { + self + .global_realm() + .load_side_module(self.v8_isolate(), specifier, code) + .await } - fn dynamic_import_resolve(&mut self, id: ModuleLoadId, mod_id: ModuleId) { + fn check_promise_exceptions(&mut self) -> Result<(), Error> { let state_rc = Self::state(self.v8_isolate()); - let module_map_rc = Self::module_map(self.v8_isolate()); - let scope = &mut self.handle_scope(); + let mut state = state_rc.borrow_mut(); - let resolver_handle = module_map_rc - .borrow_mut() - .dynamic_import_map - .remove(&id) - .expect("Invalid dynamic import id"); - let resolver = resolver_handle.open(scope); + if state.pending_promise_exceptions.is_empty() { + return Ok(()); + } - let module = { - module_map_rc - .borrow() - .get_handle(mod_id) - .map(|handle| v8::Local::new(scope, handle)) - .expect("Dyn import module info not found") + let key = { + state + .pending_promise_exceptions + .keys() + .next() + .unwrap() + .clone() }; - // Resolution success - assert_eq!(module.get_status(), v8::ModuleStatus::Evaluated); + let handle = state.pending_promise_exceptions.remove(&key).unwrap(); + drop(state); - // IMPORTANT: No borrows to `ModuleMap` can be held at this point because - // resolving the promise might initiate another `import()` which will - // in turn call `bindings::host_import_module_dynamically_callback` which - // will reach into `ModuleMap` from within the isolate. - let module_namespace = module.get_module_namespace(); - resolver.resolve(scope, module_namespace).unwrap(); - state_rc.borrow_mut().dyn_module_evaluate_idle_counter = 0; - scope.perform_microtask_checkpoint(); + let scope = &mut self.handle_scope(); + let exception = v8::Local::new(scope, handle); + exception_to_err_result(scope, exception, true) } - fn prepare_dyn_imports( - &mut self, - cx: &mut Context, - ) -> Poll> { - let module_map_rc = Self::module_map(self.v8_isolate()); + // Send finished responses to JS + fn resolve_async_ops(&mut self, cx: &mut Context) -> Result<(), Error> { + let state_rc = Self::state(self.v8_isolate()); - if module_map_rc.borrow().preparing_dynamic_imports.is_empty() { - return Poll::Ready(Ok(())); - } + // We keep a list of promise IDs and OpResults per realm. Since v8::Context + // isn't hashable, `results_per_realm` is a vector of (context, list) tuples + type ResultList = Vec<(i32, OpResult)>; + let mut results_per_realm: Vec<(v8::Global, ResultList)> = { + let known_realms = &mut state_rc.borrow_mut().known_realms; + let mut results = Vec::with_capacity(known_realms.len()); - loop { - let poll_result = module_map_rc - .borrow_mut() - .preparing_dynamic_imports - .poll_next_unpin(cx); + // Avoid calling the method multiple times + let isolate = self.v8_isolate(); - if let Poll::Ready(Some(prepare_poll)) = poll_result { - let dyn_import_id = prepare_poll.0; - let prepare_result = prepare_poll.1; + // Remove GC'd realms from `known_realms` at the same time as we populate + // `results` with those that are still alive. + known_realms.retain(|weak| { + if !weak.is_empty() { + let context = weak.to_global(isolate).unwrap(); + results.push((context, vec![])); + true + } else { + false + } + }); - match prepare_result { - Ok(load) => { - module_map_rc - .borrow_mut() - .pending_dynamic_imports - .push(load.into_future()); - } - Err(err) => { - let exception = to_v8_type_error(&mut self.handle_scope(), err); - self.dynamic_import_reject(dyn_import_id, exception); + results + }; + + // Now handle actual ops. + { + let mut state = state_rc.borrow_mut(); + state.have_unpolled_ops = false; + + while let Poll::Ready(Some(item)) = state.pending_ops.poll_next_unpin(cx) + { + let (context, promise_id, op_id, resp) = item; + state.op_state.borrow().tracker.track_async_completed(op_id); + for (context2, results) in results_per_realm.iter_mut() { + if context == *context2 { + results.push((promise_id, resp)); + break; } } - // Continue polling for more prepared dynamic imports. + JsRealm::new(context) + .state(self.v8_isolate()) + .borrow_mut() + .unrefed_ops + .remove(&promise_id); + } + } + + for (context, results) in results_per_realm { + if results.is_empty() { continue; } - // There are no active dynamic import loads, or none are ready. - return Poll::Ready(Ok(())); + let realm = JsRealm::new(context); + let js_recv_cb_handle = realm + .state(self.v8_isolate()) + .borrow() + .js_recv_cb + .clone() + .unwrap(); + let scope = &mut realm.handle_scope(self.v8_isolate()); + + // We return async responses to JS in unbounded batches (may change), + // each batch is a flat vector of tuples: + // `[promise_id1, op_result1, promise_id2, op_result2, ...]` + // promise_id is a simple integer, op_result is an ops::OpResult + // which contains a value OR an error, encoded as a tuple. + // This batch is received in JS via the special `arguments` variable + // and then each tuple is used to resolve or reject promises + let mut args = vec![]; + + for (promise_id, mut resp) in results.into_iter() { + args.push(v8::Integer::new(scope, promise_id).into()); + args.push(match resp.to_v8(scope) { + Ok(v) => v, + Err(e) => OpResult::Err(OpError::new(&|_| "TypeError", e.into())) + .to_v8(scope) + .unwrap(), + }); + } + + let tc_scope = &mut v8::TryCatch::new(scope); + let js_recv_cb = js_recv_cb_handle.open(tc_scope); + let this = v8::undefined(tc_scope).into(); + js_recv_cb.call(tc_scope, this, args.as_slice()); + + if let Some(exception) = tc_scope.exception() { + return exception_to_err_result(tc_scope, exception, false); + } } + + Ok(()) } - fn poll_dyn_imports(&mut self, cx: &mut Context) -> Poll> { - let module_map_rc = Self::module_map(self.v8_isolate()); + fn drain_macrotasks(&mut self) -> Result<(), Error> { + let state = Self::state(self.v8_isolate()); - if module_map_rc.borrow().pending_dynamic_imports.is_empty() { - return Poll::Ready(Ok(())); + if state.borrow().js_macrotask_cbs.is_empty() { + return Ok(()); } - loop { - let poll_result = module_map_rc - .borrow_mut() - .pending_dynamic_imports - .poll_next_unpin(cx); + let js_macrotask_cb_handles = state.borrow().js_macrotask_cbs.clone(); + let scope = &mut self.handle_scope(); - if let Poll::Ready(Some(load_stream_poll)) = poll_result { - let maybe_result = load_stream_poll.0; - let mut load = load_stream_poll.1; - let dyn_import_id = load.id; + for js_macrotask_cb_handle in js_macrotask_cb_handles { + let js_macrotask_cb = js_macrotask_cb_handle.open(scope); - if let Some(load_stream_result) = maybe_result { - match load_stream_result { - Ok((request, info)) => { - // A module (not necessarily the one dynamically imported) has been - // fetched. Create and register it, and if successful, poll for the - // next recursive-load event related to this dynamic import. - let register_result = load.register_and_recurse( - &mut self.handle_scope(), - &request, - &info, - ); + // Repeatedly invoke macrotask callback until it returns true (done), + // such that ready microtasks would be automatically run before + // next macrotask is processed. + let tc_scope = &mut v8::TryCatch::new(scope); + let this = v8::undefined(tc_scope).into(); + loop { + let is_done = js_macrotask_cb.call(tc_scope, this, &[]); - match register_result { - Ok(()) => { - // Keep importing until it's fully drained - module_map_rc - .borrow_mut() - .pending_dynamic_imports - .push(load.into_future()); - } - Err(err) => { - let exception = match err { - ModuleError::Exception(e) => e, - ModuleError::Other(e) => { - to_v8_type_error(&mut self.handle_scope(), e) - } - }; - self.dynamic_import_reject(dyn_import_id, exception) - } - } - } - Err(err) => { - // A non-javascript error occurred; this could be due to a an invalid - // module specifier, or a problem with the source map, or a failure - // to fetch the module source code. - let exception = to_v8_type_error(&mut self.handle_scope(), err); - self.dynamic_import_reject(dyn_import_id, exception); - } - } - } else { - // The top-level module from a dynamic import has been instantiated. - // Load is done. - let module_id = - load.root_module_id.expect("Root module should be loaded"); - let result = self.instantiate_module(module_id); - if let Err(exception) = result { - self.dynamic_import_reject(dyn_import_id, exception); - } - self.dynamic_import_module_evaluate(dyn_import_id, module_id)?; + if let Some(exception) = tc_scope.exception() { + return exception_to_err_result(tc_scope, exception, false); } - // Continue polling for more ready dynamic imports. - continue; - } + if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { + return Ok(()); + } - // There are no active dynamic import loads, or none are ready. - return Poll::Ready(Ok(())); + let is_done = is_done.unwrap(); + if is_done.is_true() { + break; + } + } } + + Ok(()) } - /// "deno_core" runs V8 with Top Level Await enabled. It means that each - /// module evaluation returns a promise from V8. - /// Feature docs: https://v8.dev/features/top-level-await - /// - /// This promise resolves after all dependent modules have also - /// resolved. Each dependent module may perform calls to "import()" and APIs - /// using async ops will add futures to the runtime's event loop. - /// It means that the promise returned from module evaluation will - /// resolve only after all futures in the event loop are done. - /// - /// Thus during turn of event loop we need to check if V8 has - /// resolved or rejected the promise. If the promise is still pending - /// then another turn of event loop must be performed. - fn evaluate_pending_module(&mut self) { - let state_rc = Self::state(self.v8_isolate()); + fn drain_nexttick(&mut self) -> Result<(), Error> { + let state = Self::state(self.v8_isolate()); - let maybe_module_evaluation = - state_rc.borrow_mut().pending_mod_evaluate.take(); + if state.borrow().js_nexttick_cbs.is_empty() { + return Ok(()); + } - if maybe_module_evaluation.is_none() { - return; + if !state.borrow().has_tick_scheduled { + let scope = &mut self.handle_scope(); + scope.perform_microtask_checkpoint(); } - let mut module_evaluation = maybe_module_evaluation.unwrap(); + // TODO(bartlomieju): Node also checks for absence of "rejection_to_warn" + if !state.borrow().has_tick_scheduled { + return Ok(()); + } + + let js_nexttick_cb_handles = state.borrow().js_nexttick_cbs.clone(); let scope = &mut self.handle_scope(); - let promise_global = module_evaluation.promise.clone().unwrap(); - let promise = promise_global.open(scope); - let promise_state = promise.state(); + for js_nexttick_cb_handle in js_nexttick_cb_handles { + let js_nexttick_cb = js_nexttick_cb_handle.open(scope); - match promise_state { - v8::PromiseState::Pending => { - // NOTE: `poll_event_loop` will decide if - // runtime would be woken soon - state_rc.borrow_mut().pending_mod_evaluate = Some(module_evaluation); - } - v8::PromiseState::Fulfilled => { - scope.perform_microtask_checkpoint(); - // Receiver end might have been already dropped, ignore the result - let _ = module_evaluation.sender.send(Ok(())); - module_evaluation.handled_promise_rejections.clear(); - } - v8::PromiseState::Rejected => { - let exception = promise.result(scope); - scope.perform_microtask_checkpoint(); + let tc_scope = &mut v8::TryCatch::new(scope); + let this = v8::undefined(tc_scope).into(); + js_nexttick_cb.call(tc_scope, this, &[]); - // Receiver end might have been already dropped, ignore the result - if module_evaluation - .handled_promise_rejections - .contains(&promise_global) - { - let _ = module_evaluation.sender.send(Ok(())); - module_evaluation.handled_promise_rejections.clear(); - } else { - let _ = module_evaluation - .sender - .send(exception_to_err_result(scope, exception, false)); - } + if let Some(exception) = tc_scope.exception() { + return exception_to_err_result(tc_scope, exception, false); } } - } - - // Returns true if some dynamic import was resolved. - fn evaluate_dyn_imports(&mut self) -> bool { - let mut resolved_any = false; - let state_rc = Self::state(self.v8_isolate()); - let mut still_pending = vec![]; - let pending = - std::mem::take(&mut state_rc.borrow_mut().pending_dyn_mod_evaluate); - for pending_dyn_evaluate in pending { - let maybe_result = { - let scope = &mut self.handle_scope(); - let module_id = pending_dyn_evaluate.module_id; - let promise = pending_dyn_evaluate.promise.open(scope); - let _module = pending_dyn_evaluate.module.open(scope); - let promise_state = promise.state(); + Ok(()) + } +} - match promise_state { - v8::PromiseState::Pending => { - still_pending.push(pending_dyn_evaluate); - None - } - v8::PromiseState::Fulfilled => { - Some(Ok((pending_dyn_evaluate.load_id, module_id))) - } - v8::PromiseState::Rejected => { - let exception = promise.result(scope); - let exception = v8::Global::new(scope, exception); - Some(Err((pending_dyn_evaluate.load_id, exception))) - } - } - }; +/// A representation of a JavaScript realm tied to a [`JsRuntime`], that allows +/// execution in the realm's context. +/// +/// A [`JsRealm`] instance does not hold ownership of its corresponding realm, +/// so they can be created and dropped as needed. And since every operation on +/// them requires passing a mutable reference to the [`JsRuntime`], multiple +/// [`JsRealm`] instances won't overlap. +/// +/// # Panics +/// +/// Every method of [`JsRealm`] will panic if you call if with a reference to a +/// [`v8::Isolate`] other than the one that corresponds to the current context. +/// +/// # Lifetime of the realm +/// +/// A [`JsRealm`] instance will keep the underlying V8 context alive even if it +/// would have otherwise been garbage collected. +#[derive(Clone)] +pub struct JsRealm(v8::Global); +impl JsRealm { + pub fn new(context: v8::Global) -> Self { + JsRealm(context) + } - if let Some(result) = maybe_result { - resolved_any = true; - match result { - Ok((dyn_import_id, module_id)) => { - self.dynamic_import_resolve(dyn_import_id, module_id); - } - Err((dyn_import_id, exception)) => { - self.dynamic_import_reject(dyn_import_id, exception); - } - } - } - } - state_rc.borrow_mut().pending_dyn_mod_evaluate = still_pending; - resolved_any + pub fn context(&self) -> &v8::Global { + &self.0 } - /// Asynchronously load specified module and all of its dependencies. - /// - /// The module will be marked as "main", and because of that - /// "import.meta.main" will return true when checked inside that module. - /// - /// User must call [`JsRuntime::mod_evaluate`] with returned `ModuleId` - /// manually after load is finished. - pub async fn load_main_module( - &mut self, - specifier: &ModuleSpecifier, - code: Option, - ) -> Result { - let module_map_rc = Self::module_map(self.v8_isolate()); - if let Some(code) = code { - let scope = &mut self.handle_scope(); - module_map_rc - .borrow_mut() - .new_es_module( - scope, - // main module - true, - specifier.as_str(), - code.as_bytes(), - ) - .map_err(|e| match e { - ModuleError::Exception(exception) => { - let exception = v8::Local::new(scope, exception); - exception_to_err_result::<()>(scope, exception, false).unwrap_err() - } - ModuleError::Other(error) => error, - })?; - } + pub(crate) fn state( + &self, + isolate: &mut v8::Isolate, + ) -> Rc> { + self + .context() + .open(isolate) + .get_slot::>>(isolate) + .unwrap() + .clone() + } - let mut load = - ModuleMap::load_main(module_map_rc.clone(), specifier.as_str()).await?; + pub(crate) fn state_from_scope( + scope: &mut v8::HandleScope, + ) -> Rc> { + let context = scope.get_current_context(); + context + .get_slot::>>(scope) + .unwrap() + .clone() + } - while let Some(load_result) = load.next().await { - let (request, info) = load_result?; - let scope = &mut self.handle_scope(); - load.register_and_recurse(scope, &request, &info).map_err( - |e| match e { - ModuleError::Exception(exception) => { - let exception = v8::Local::new(scope, exception); - exception_to_err_result::<()>(scope, exception, false).unwrap_err() - } - ModuleError::Other(error) => error, - }, - )?; - } + pub fn handle_scope<'s>( + &self, + isolate: &'s mut v8::Isolate, + ) -> v8::HandleScope<'s> { + v8::HandleScope::with_context(isolate, &self.0) + } - let root_id = load.root_module_id.expect("Root module should be loaded"); - self.instantiate_module(root_id).map_err(|e| { - let scope = &mut self.handle_scope(); - let exception = v8::Local::new(scope, e); - exception_to_err_result::<()>(scope, exception, false).unwrap_err() - })?; - Ok(root_id) + pub fn global_object<'s>( + &self, + isolate: &'s mut v8::Isolate, + ) -> v8::Local<'s, v8::Object> { + let scope = &mut self.handle_scope(isolate); + self.0.open(scope).global(scope) } - /// Asynchronously load specified ES module and all of its dependencies. - /// - /// This method is meant to be used when loading some utility code that - /// might be later imported by the main module (ie. an entry point module). + pub(crate) fn module_map( + &self, + isolate: &mut v8::Isolate, + ) -> Rc> { + self + .context() + .open(isolate) + .get_slot::>>(isolate) + .unwrap() + .clone() + } + + pub(crate) fn module_map_from_scope( + scope: &mut v8::HandleScope, + ) -> Rc> { + let context = scope.get_current_context(); + context + .get_slot::>>(scope) + .unwrap() + .clone() + } + + /// Returns the namespace object of a module. /// - /// User must call [`JsRuntime::mod_evaluate`] with returned `ModuleId` - /// manually after load is finished. - pub async fn load_side_module( - &mut self, - specifier: &ModuleSpecifier, - code: Option, - ) -> Result { - let module_map_rc = Self::module_map(self.v8_isolate()); - if let Some(code) = code { - let scope = &mut self.handle_scope(); - module_map_rc - .borrow_mut() - .new_es_module( - scope, - // not main module - false, - specifier.as_str(), - code.as_bytes(), - ) - .map_err(|e| match e { - ModuleError::Exception(exception) => { - let exception = v8::Local::new(scope, exception); - exception_to_err_result::<()>(scope, exception, false).unwrap_err() - } - ModuleError::Other(error) => error, - })?; + /// This is only available after module evaluation has completed. + /// This function panics if module has not been instantiated. + pub fn get_module_namespace( + &self, + isolate: &mut v8::Isolate, + module_id: ModuleId, + ) -> Result, Error> { + let module_map_rc = self.module_map(isolate); + + let module_handle = module_map_rc + .borrow() + .get_handle(module_id) + .expect("ModuleInfo not found"); + + let scope = &mut self.handle_scope(isolate); + + let module = module_handle.open(scope); + + if module.get_status() == v8::ModuleStatus::Errored { + let exception = module.get_exception(); + return exception_to_err_result(scope, exception, false); } - let mut load = - ModuleMap::load_side(module_map_rc.clone(), specifier.as_str()).await?; + assert!(matches!( + module.get_status(), + v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated + )); - while let Some(load_result) = load.next().await { - let (request, info) = load_result?; - let scope = &mut self.handle_scope(); - load.register_and_recurse(scope, &request, &info).map_err( - |e| match e { - ModuleError::Exception(exception) => { - let exception = v8::Local::new(scope, exception); - exception_to_err_result::<()>(scope, exception, false).unwrap_err() - } - ModuleError::Other(error) => error, - }, - )?; + let module_namespace: v8::Local = + v8::Local::try_from(module.get_module_namespace()) + .map_err(|err: v8::DataError| generic_error(err.to_string()))?; + + Ok(v8::Global::new(scope, module_namespace)) + } + + pub(crate) fn instantiate_module( + &self, + isolate: &mut v8::Isolate, + id: ModuleId, + ) -> Result<(), v8::Global> { + let module_map_rc = self.module_map(isolate); + let scope = &mut self.handle_scope(isolate); + let tc_scope = &mut v8::TryCatch::new(scope); + + let module = module_map_rc + .borrow() + .get_handle(id) + .map(|handle| v8::Local::new(tc_scope, handle)) + .expect("ModuleInfo not found"); + + if module.get_status() == v8::ModuleStatus::Errored { + return Err(v8::Global::new(tc_scope, module.get_exception())); + } + + // IMPORTANT: No borrows to `ModuleMap` can be held at this point because + // `module_resolve_callback` will be calling into `ModuleMap` from within + // the isolate. + let instantiate_result = + module.instantiate_module(tc_scope, bindings::module_resolve_callback); + + if instantiate_result.is_none() { + let exception = tc_scope.exception().unwrap(); + return Err(v8::Global::new(tc_scope, exception)); + } + + Ok(()) + } + + fn dynamic_import_module_evaluate( + &self, + isolate: &mut v8::Isolate, + load_id: ModuleLoadId, + id: ModuleId, + ) -> Result<(), Error> { + let state_rc = self.state(isolate); + let module_map_rc = self.module_map(isolate); + + let module_handle = module_map_rc + .borrow() + .get_handle(id) + .expect("ModuleInfo not found"); + + let status = { + let scope = &mut self.handle_scope(isolate); + let module = module_handle.open(scope); + module.get_status() + }; + + match status { + v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated => {} + _ => return Ok(()), + } + + // IMPORTANT: Top-level-await is enabled, which means that return value + // of module evaluation is a promise. + // + // This promise is internal, and not the same one that gets returned to + // the user. We add an empty `.catch()` handler so that it does not result + // in an exception if it rejects. That will instead happen for the other + // promise if not handled by the user. + // + // For more details see: + // https://github.com/denoland/deno/issues/4908 + // https://v8.dev/features/top-level-await#module-execution-order + let scope = &mut self.handle_scope(isolate); + let tc_scope = &mut v8::TryCatch::new(scope); + let module = v8::Local::new(tc_scope, &module_handle); + let maybe_value = module.evaluate(tc_scope); + + // Update status after evaluating. + let status = module.get_status(); + + if let Some(value) = maybe_value { + assert!( + status == v8::ModuleStatus::Evaluated + || status == v8::ModuleStatus::Errored + ); + let promise = v8::Local::::try_from(value) + .expect("Expected to get promise as module evaluation result"); + let empty_fn = bindings::create_empty_fn(tc_scope).unwrap(); + promise.catch(tc_scope, empty_fn); + let mut state = state_rc.borrow_mut(); + let promise_global = v8::Global::new(tc_scope, promise); + let module_global = v8::Global::new(tc_scope, module); + + let dyn_import_mod_evaluate = DynImportModEvaluate { + load_id, + module_id: id, + promise: promise_global, + module: module_global, + }; + + state.pending_dyn_mod_evaluate.push(dyn_import_mod_evaluate); + } else if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { + return Err( + generic_error("Cannot evaluate dynamically imported module, because JavaScript execution has been terminated.") + ); + } else { + assert!(status == v8::ModuleStatus::Errored); + } + + Ok(()) + } + + // TODO(bartlomieju): make it return `ModuleEvaluationFuture`? + /// Evaluates an already instantiated ES module. + /// + /// Returns a receiver handle that resolves when module promise resolves. + /// Implementors must manually call [`JsRuntime::run_event_loop`] to drive + /// module evaluation future. + /// + /// `Error` can usually be downcast to `JsError` and should be awaited and + /// checked after [`JsRuntime::run_event_loop`] completion. + /// + /// This function panics if module has not been instantiated. + pub fn mod_evaluate( + &self, + isolate: &mut v8::Isolate, + id: ModuleId, + ) -> oneshot::Receiver> { + // TODO(realm): Make it JsContextState? + let state_rc = JsRuntime::state(isolate); + let realm_state_rc = self.state(isolate); + let module_map_rc = self.module_map(isolate); + let scope = &mut self.handle_scope(isolate); + let tc_scope = &mut v8::TryCatch::new(scope); + + let module = module_map_rc + .borrow() + .get_handle(id) + .map(|handle| v8::Local::new(tc_scope, handle)) + .expect("ModuleInfo not found"); + let mut status = module.get_status(); + assert_eq!(status, v8::ModuleStatus::Instantiated); + + let (sender, receiver) = oneshot::channel(); + + // IMPORTANT: Top-level-await is enabled, which means that return value + // of module evaluation is a promise. + // + // Because that promise is created internally by V8, when error occurs during + // module evaluation the promise is rejected, and since the promise has no rejection + // handler it will result in call to `bindings::promise_reject_callback` adding + // the promise to pending promise rejection table - meaning JsRuntime will return + // error on next poll(). + // + // This situation is not desirable as we want to manually return error at the + // end of this function to handle it further. It means we need to manually + // remove this promise from pending promise rejection table. + // + // For more details see: + // https://github.com/denoland/deno/issues/4908 + // https://v8.dev/features/top-level-await#module-execution-order + { + let mut realm_state = realm_state_rc.borrow_mut(); + assert!( + realm_state.pending_mod_evaluate.is_none(), + "There is already pending top level module evaluation" + ); + realm_state.pending_mod_evaluate = Some(ModEvaluate { + promise: None, + has_evaluated: false, + handled_promise_rejections: vec![], + sender, + }); + } + + let maybe_value = module.evaluate(tc_scope); + { + let mut realm_state = realm_state_rc.borrow_mut(); + let pending_mod_evaluate = + realm_state.pending_mod_evaluate.as_mut().unwrap(); + pending_mod_evaluate.has_evaluated = true; + } + + // Update status after evaluating. + status = module.get_status(); + + let has_dispatched_exception = + !state_rc.borrow_mut().dispatched_exceptions.is_empty(); + if has_dispatched_exception { + // This will be overrided in `exception_to_err_result()`. + let exception = v8::undefined(tc_scope).into(); + let pending_mod_evaluate = { + let mut state = realm_state_rc.borrow_mut(); + state.pending_mod_evaluate.take().unwrap() + }; + pending_mod_evaluate + .sender + .send(exception_to_err_result(tc_scope, exception, false)) + .expect("Failed to send module evaluation error."); + } else if let Some(value) = maybe_value { + assert!( + status == v8::ModuleStatus::Evaluated + || status == v8::ModuleStatus::Errored + ); + let promise = v8::Local::::try_from(value) + .expect("Expected to get promise as module evaluation result"); + let promise_global = v8::Global::new(tc_scope, promise); + let mut realm_state = realm_state_rc.borrow_mut(); + { + let pending_mod_evaluate = + realm_state.pending_mod_evaluate.as_ref().unwrap(); + let pending_rejection_was_already_handled = pending_mod_evaluate + .handled_promise_rejections + .contains(&promise_global); + if !pending_rejection_was_already_handled { + state_rc + .borrow_mut() + .pending_promise_exceptions + .remove(&promise_global); + } + } + let promise_global = v8::Global::new(tc_scope, promise); + realm_state.pending_mod_evaluate.as_mut().unwrap().promise = + Some(promise_global); + tc_scope.perform_microtask_checkpoint(); + } else if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { + let pending_mod_evaluate = { + let mut realm_state = realm_state_rc.borrow_mut(); + realm_state.pending_mod_evaluate.take().unwrap() + }; + pending_mod_evaluate.sender.send(Err( + generic_error("Cannot evaluate module, because JavaScript execution has been terminated.") + )).expect("Failed to send module evaluation error."); + } else { + assert!(status == v8::ModuleStatus::Errored); } - let root_id = load.root_module_id.expect("Root module should be loaded"); - self.instantiate_module(root_id).map_err(|e| { - let scope = &mut self.handle_scope(); - let exception = v8::Local::new(scope, e); - exception_to_err_result::<()>(scope, exception, false).unwrap_err() - })?; - Ok(root_id) + receiver + } + + fn dynamic_import_reject( + &self, + isolate: &mut v8::Isolate, + id: ModuleLoadId, + exception: v8::Global, + ) { + let module_map_rc = self.module_map(isolate); + let scope = &mut self.handle_scope(isolate); + + let resolver_handle = module_map_rc + .borrow_mut() + .dynamic_import_map + .remove(&id) + .expect("Invalid dynamic import id"); + let resolver = resolver_handle.open(scope); + + // IMPORTANT: No borrows to `ModuleMap` can be held at this point because + // rejecting the promise might initiate another `import()` which will + // in turn call `bindings::host_import_module_dynamically_callback` which + // will reach into `ModuleMap` from within the isolate. + let exception = v8::Local::new(scope, exception); + resolver.reject(scope, exception).unwrap(); + scope.perform_microtask_checkpoint(); + } + + fn dynamic_import_resolve( + &self, + isolate: &mut v8::Isolate, + id: ModuleLoadId, + mod_id: ModuleId, + ) { + let state_rc = JsRuntime::state(isolate); + let module_map_rc = self.module_map(isolate); + let scope = &mut self.handle_scope(isolate); + + let resolver_handle = module_map_rc + .borrow_mut() + .dynamic_import_map + .remove(&id) + .expect("Invalid dynamic import id"); + let resolver = resolver_handle.open(scope); + + let module = { + module_map_rc + .borrow() + .get_handle(mod_id) + .map(|handle| v8::Local::new(scope, handle)) + .expect("Dyn import module info not found") + }; + // Resolution success + assert_eq!(module.get_status(), v8::ModuleStatus::Evaluated); + + // IMPORTANT: No borrows to `ModuleMap` can be held at this point because + // resolving the promise might initiate another `import()` which will + // in turn call `bindings::host_import_module_dynamically_callback` which + // will reach into `ModuleMap` from within the isolate. + let module_namespace = module.get_module_namespace(); + resolver.resolve(scope, module_namespace).unwrap(); + state_rc.borrow_mut().dyn_module_evaluate_idle_counter = 0; + scope.perform_microtask_checkpoint(); } - fn check_promise_exceptions(&mut self) -> Result<(), Error> { - let state_rc = Self::state(self.v8_isolate()); - let mut state = state_rc.borrow_mut(); + fn prepare_dyn_imports( + &self, + isolate: &mut v8::Isolate, + cx: &mut Context, + ) -> Poll> { + let module_map_rc = self.module_map(isolate); - if state.pending_promise_exceptions.is_empty() { - return Ok(()); + if module_map_rc.borrow().preparing_dynamic_imports.is_empty() { + return Poll::Ready(Ok(())); } - let key = { - state - .pending_promise_exceptions - .keys() - .next() - .unwrap() - .clone() - }; - let handle = state.pending_promise_exceptions.remove(&key).unwrap(); - drop(state); + loop { + let poll_result = module_map_rc + .borrow_mut() + .preparing_dynamic_imports + .poll_next_unpin(cx); - let scope = &mut self.handle_scope(); - let exception = v8::Local::new(scope, handle); - exception_to_err_result(scope, exception, true) - } + if let Poll::Ready(Some(prepare_poll)) = poll_result { + let dyn_import_id = prepare_poll.0; + let prepare_result = prepare_poll.1; - // Send finished responses to JS - fn resolve_async_ops(&mut self, cx: &mut Context) -> Result<(), Error> { - let state_rc = Self::state(self.v8_isolate()); + match prepare_result { + Ok(load) => { + module_map_rc + .borrow_mut() + .pending_dynamic_imports + .push(load.into_future()); + } + Err(err) => { + let exception = + to_v8_type_error(&mut self.handle_scope(isolate), err); + self.dynamic_import_reject(isolate, dyn_import_id, exception); + } + } + // Continue polling for more prepared dynamic imports. + continue; + } - // We keep a list of promise IDs and OpResults per realm. Since v8::Context - // isn't hashable, `results_per_realm` is a vector of (context, list) tuples - type ResultList = Vec<(i32, OpResult)>; - let mut results_per_realm: Vec<(v8::Global, ResultList)> = { - let known_realms = &mut state_rc.borrow_mut().known_realms; - let mut results = Vec::with_capacity(known_realms.len()); + // There are no active dynamic import loads, or none are ready. + return Poll::Ready(Ok(())); + } + } - // Avoid calling the method multiple times - let isolate = self.v8_isolate(); + fn poll_dyn_imports( + &self, + isolate: &mut v8::Isolate, + cx: &mut Context, + ) -> Poll> { + let module_map_rc = self.module_map(isolate); - // Remove GC'd realms from `known_realms` at the same time as we populate - // `results` with those that are still alive. - known_realms.retain(|weak| { - if !weak.is_empty() { - let context = weak.to_global(isolate).unwrap(); - results.push((context, vec![])); - true - } else { - false - } - }); + if module_map_rc.borrow().pending_dynamic_imports.is_empty() { + return Poll::Ready(Ok(())); + } - results - }; + loop { + let poll_result = module_map_rc + .borrow_mut() + .pending_dynamic_imports + .poll_next_unpin(cx); - // Now handle actual ops. - { - let mut state = state_rc.borrow_mut(); - state.have_unpolled_ops = false; + if let Poll::Ready(Some(load_stream_poll)) = poll_result { + let maybe_result = load_stream_poll.0; + let mut load = load_stream_poll.1; + let dyn_import_id = load.id; - while let Poll::Ready(Some(item)) = state.pending_ops.poll_next_unpin(cx) - { - let (context, promise_id, op_id, resp) = item; - state.op_state.borrow().tracker.track_async_completed(op_id); - for (context2, results) in results_per_realm.iter_mut() { - if context == *context2 { - results.push((promise_id, resp)); - break; + if let Some(load_stream_result) = maybe_result { + match load_stream_result { + Ok((request, info)) => { + // A module (not necessarily the one dynamically imported) has been + // fetched. Create and register it, and if successful, poll for the + // next recursive-load event related to this dynamic import. + let register_result = load.register_and_recurse( + &mut self.handle_scope(isolate), + &request, + &info, + ); + + match register_result { + Ok(()) => { + // Keep importing until it's fully drained + module_map_rc + .borrow_mut() + .pending_dynamic_imports + .push(load.into_future()); + } + Err(err) => { + let exception = match err { + ModuleError::Exception(e) => e, + ModuleError::Other(e) => { + to_v8_type_error(&mut self.handle_scope(isolate), e) + } + }; + self.dynamic_import_reject(isolate, dyn_import_id, exception) + } + } + } + Err(err) => { + // A non-javascript error occurred; this could be due to a an invalid + // module specifier, or a problem with the source map, or a failure + // to fetch the module source code. + let exception = + to_v8_type_error(&mut self.handle_scope(isolate), err); + self.dynamic_import_reject(isolate, dyn_import_id, exception); + } + } + } else { + // The top-level module from a dynamic import has been instantiated. + // Load is done. + let module_id = + load.root_module_id.expect("Root module should be loaded"); + let result = self.instantiate_module(isolate, module_id); + if let Err(exception) = result { + self.dynamic_import_reject(isolate, dyn_import_id, exception); } + self.dynamic_import_module_evaluate( + isolate, + dyn_import_id, + module_id, + )?; } - JsRealm::new(context) - .state(self.v8_isolate()) - .borrow_mut() - .unrefed_ops - .remove(&promise_id); - } - } - for (context, results) in results_per_realm { - if results.is_empty() { + // Continue polling for more ready dynamic imports. continue; } - let realm = JsRealm::new(context); - let js_recv_cb_handle = realm - .state(self.v8_isolate()) - .borrow() - .js_recv_cb - .clone() - .unwrap(); - let scope = &mut realm.handle_scope(self.v8_isolate()); + // There are no active dynamic import loads, or none are ready. + return Poll::Ready(Ok(())); + } + } - // We return async responses to JS in unbounded batches (may change), - // each batch is a flat vector of tuples: - // `[promise_id1, op_result1, promise_id2, op_result2, ...]` - // promise_id is a simple integer, op_result is an ops::OpResult - // which contains a value OR an error, encoded as a tuple. - // This batch is received in JS via the special `arguments` variable - // and then each tuple is used to resolve or reject promises - let mut args = vec![]; + /// "deno_core" runs V8 with Top Level Await enabled. It means that each + /// module evaluation returns a promise from V8. + /// Feature docs: https://v8.dev/features/top-level-await + /// + /// This promise resolves after all dependent modules have also + /// resolved. Each dependent module may perform calls to "import()" and APIs + /// using async ops will add futures to the runtime's event loop. + /// It means that the promise returned from module evaluation will + /// resolve only after all futures in the event loop are done. + /// + /// Thus during turn of event loop we need to check if V8 has + /// resolved or rejected the promise. If the promise is still pending + /// then another turn of event loop must be performed. + fn evaluate_pending_module(&self, isolate: &mut v8::Isolate) { + let state_rc = self.state(isolate); - for (promise_id, mut resp) in results.into_iter() { - args.push(v8::Integer::new(scope, promise_id).into()); - args.push(match resp.to_v8(scope) { - Ok(v) => v, - Err(e) => OpResult::Err(OpError::new(&|_| "TypeError", e.into())) - .to_v8(scope) - .unwrap(), - }); - } + let maybe_module_evaluation = + state_rc.borrow_mut().pending_mod_evaluate.take(); - let tc_scope = &mut v8::TryCatch::new(scope); - let js_recv_cb = js_recv_cb_handle.open(tc_scope); - let this = v8::undefined(tc_scope).into(); - js_recv_cb.call(tc_scope, this, args.as_slice()); + if maybe_module_evaluation.is_none() { + return; + } - if let Some(exception) = tc_scope.exception() { - return exception_to_err_result(tc_scope, exception, false); + let mut module_evaluation = maybe_module_evaluation.unwrap(); + let scope = &mut self.handle_scope(isolate); + + let promise_global = module_evaluation.promise.clone().unwrap(); + let promise = promise_global.open(scope); + let promise_state = promise.state(); + + match promise_state { + v8::PromiseState::Pending => { + // NOTE: `poll_event_loop` will decide if + // runtime would be woken soon + state_rc.borrow_mut().pending_mod_evaluate = Some(module_evaluation); + } + v8::PromiseState::Fulfilled => { + scope.perform_microtask_checkpoint(); + // Receiver end might have been already dropped, ignore the result + let _ = module_evaluation.sender.send(Ok(())); + module_evaluation.handled_promise_rejections.clear(); + } + v8::PromiseState::Rejected => { + let exception = promise.result(scope); + scope.perform_microtask_checkpoint(); + + // Receiver end might have been already dropped, ignore the result + if module_evaluation + .handled_promise_rejections + .contains(&promise_global) + { + let _ = module_evaluation.sender.send(Ok(())); + module_evaluation.handled_promise_rejections.clear(); + } else { + let _ = module_evaluation + .sender + .send(exception_to_err_result(scope, exception, false)); + } } } - - Ok(()) } - fn drain_macrotasks(&mut self) -> Result<(), Error> { - let state = Self::state(self.v8_isolate()); - - if state.borrow().js_macrotask_cbs.is_empty() { - return Ok(()); - } - - let js_macrotask_cb_handles = state.borrow().js_macrotask_cbs.clone(); - let scope = &mut self.handle_scope(); - - for js_macrotask_cb_handle in js_macrotask_cb_handles { - let js_macrotask_cb = js_macrotask_cb_handle.open(scope); - - // Repeatedly invoke macrotask callback until it returns true (done), - // such that ready microtasks would be automatically run before - // next macrotask is processed. - let tc_scope = &mut v8::TryCatch::new(scope); - let this = v8::undefined(tc_scope).into(); - loop { - let is_done = js_macrotask_cb.call(tc_scope, this, &[]); + // Returns true if some dynamic import was resolved. + fn evaluate_dyn_imports(&self, isolate: &mut v8::Isolate) -> bool { + let mut resolved_any = false; + let state_rc = self.state(isolate); + let mut still_pending = vec![]; + let pending = + std::mem::take(&mut state_rc.borrow_mut().pending_dyn_mod_evaluate); + for pending_dyn_evaluate in pending { + let maybe_result = { + let scope = &mut self.handle_scope(isolate); - if let Some(exception) = tc_scope.exception() { - return exception_to_err_result(tc_scope, exception, false); - } + let module_id = pending_dyn_evaluate.module_id; + let promise = pending_dyn_evaluate.promise.open(scope); + let _module = pending_dyn_evaluate.module.open(scope); + let promise_state = promise.state(); - if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { - return Ok(()); + match promise_state { + v8::PromiseState::Pending => { + still_pending.push(pending_dyn_evaluate); + None + } + v8::PromiseState::Fulfilled => { + Some(Ok((pending_dyn_evaluate.load_id, module_id))) + } + v8::PromiseState::Rejected => { + let exception = promise.result(scope); + let exception = v8::Global::new(scope, exception); + Some(Err((pending_dyn_evaluate.load_id, exception))) + } } + }; - let is_done = is_done.unwrap(); - if is_done.is_true() { - break; + if let Some(result) = maybe_result { + resolved_any = true; + match result { + Ok((dyn_import_id, module_id)) => { + self.dynamic_import_resolve(isolate, dyn_import_id, module_id); + } + Err((dyn_import_id, exception)) => { + self.dynamic_import_reject(isolate, dyn_import_id, exception); + } } } } - - Ok(()) + state_rc.borrow_mut().pending_dyn_mod_evaluate = still_pending; + resolved_any } - fn drain_nexttick(&mut self) -> Result<(), Error> { - let state = Self::state(self.v8_isolate()); - - if state.borrow().js_nexttick_cbs.is_empty() { - return Ok(()); - } - - if !state.borrow().has_tick_scheduled { - let scope = &mut self.handle_scope(); - scope.perform_microtask_checkpoint(); - } - - // TODO(bartlomieju): Node also checks for absence of "rejection_to_warn" - if !state.borrow().has_tick_scheduled { - return Ok(()); + /// Asynchronously load specified module and all of its dependencies. + /// + /// The module will be marked as "main", and because of that + /// "import.meta.main" will return true when checked inside that module. + /// + /// User must call [`JsRuntime::mod_evaluate`] with returned `ModuleId` + /// manually after load is finished. + pub async fn load_main_module( + &self, + isolate: &mut v8::Isolate, + specifier: &ModuleSpecifier, + code: Option, + ) -> Result { + let module_map_rc = self.module_map(isolate); + if let Some(code) = code { + let scope = &mut self.handle_scope(isolate); + module_map_rc + .borrow_mut() + .new_es_module( + scope, + // main module + true, + specifier.as_str(), + code.as_bytes(), + ) + .map_err(|e| match e { + ModuleError::Exception(exception) => { + let exception = v8::Local::new(scope, exception); + exception_to_err_result::<()>(scope, exception, false).unwrap_err() + } + ModuleError::Other(error) => error, + })?; } - let js_nexttick_cb_handles = state.borrow().js_nexttick_cbs.clone(); - let scope = &mut self.handle_scope(); - - for js_nexttick_cb_handle in js_nexttick_cb_handles { - let js_nexttick_cb = js_nexttick_cb_handle.open(scope); - - let tc_scope = &mut v8::TryCatch::new(scope); - let this = v8::undefined(tc_scope).into(); - js_nexttick_cb.call(tc_scope, this, &[]); + let mut load = + ModuleMap::load_main(module_map_rc.clone(), specifier.as_str()).await?; - if let Some(exception) = tc_scope.exception() { - return exception_to_err_result(tc_scope, exception, false); - } + while let Some(load_result) = load.next().await { + let (request, info) = load_result?; + let scope = &mut self.handle_scope(isolate); + load.register_and_recurse(scope, &request, &info).map_err( + |e| match e { + ModuleError::Exception(exception) => { + let exception = v8::Local::new(scope, exception); + exception_to_err_result::<()>(scope, exception, false).unwrap_err() + } + ModuleError::Other(error) => error, + }, + )?; } - Ok(()) - } -} - -/// A representation of a JavaScript realm tied to a [`JsRuntime`], that allows -/// execution in the realm's context. -/// -/// A [`JsRealm`] instance does not hold ownership of its corresponding realm, -/// so they can be created and dropped as needed. And since every operation on -/// them requires passing a mutable reference to the [`JsRuntime`], multiple -/// [`JsRealm`] instances won't overlap. -/// -/// # Panics -/// -/// Every method of [`JsRealm`] will panic if you call if with a reference to a -/// [`v8::Isolate`] other than the one that corresponds to the current context. -/// -/// # Lifetime of the realm -/// -/// A [`JsRealm`] instance will keep the underlying V8 context alive even if it -/// would have otherwise been garbage collected. -#[derive(Clone)] -pub struct JsRealm(v8::Global); -impl JsRealm { - pub fn new(context: v8::Global) -> Self { - JsRealm(context) - } - - pub fn context(&self) -> &v8::Global { - &self.0 + let root_id = load.root_module_id.expect("Root module should be loaded"); + self.instantiate_module(isolate, root_id).map_err(|e| { + let scope = &mut self.handle_scope(isolate); + let exception = v8::Local::new(scope, e); + exception_to_err_result::<()>(scope, exception, false).unwrap_err() + })?; + Ok(root_id) } - pub(crate) fn state( + /// Asynchronously load specified ES module and all of its dependencies. + /// + /// This method is meant to be used when loading some utility code that + /// might be later imported by the main module (ie. an entry point module). + /// + /// User must call [`JsRuntime::mod_evaluate`] with returned `ModuleId` + /// manually after load is finished. + pub async fn load_side_module( &self, isolate: &mut v8::Isolate, - ) -> Rc> { - self - .context() - .open(isolate) - .get_slot::>>(isolate) - .unwrap() - .clone() - } + specifier: &ModuleSpecifier, + code: Option, + ) -> Result { + let module_map_rc = self.module_map(isolate); + if let Some(code) = code { + let scope = &mut self.handle_scope(isolate); + module_map_rc + .borrow_mut() + .new_es_module( + scope, + // not main module + false, + specifier.as_str(), + code.as_bytes(), + ) + .map_err(|e| match e { + ModuleError::Exception(exception) => { + let exception = v8::Local::new(scope, exception); + exception_to_err_result::<()>(scope, exception, false).unwrap_err() + } + ModuleError::Other(error) => error, + })?; + } - pub(crate) fn state_from_scope( - scope: &mut v8::HandleScope, - ) -> Rc> { - let context = scope.get_current_context(); - context - .get_slot::>>(scope) - .unwrap() - .clone() - } + let mut load = + ModuleMap::load_side(module_map_rc.clone(), specifier.as_str()).await?; - pub fn handle_scope<'s>( - &self, - isolate: &'s mut v8::Isolate, - ) -> v8::HandleScope<'s> { - v8::HandleScope::with_context(isolate, &self.0) - } + while let Some(load_result) = load.next().await { + let (request, info) = load_result?; + let scope = &mut self.handle_scope(isolate); + load.register_and_recurse(scope, &request, &info).map_err( + |e| match e { + ModuleError::Exception(exception) => { + let exception = v8::Local::new(scope, exception); + exception_to_err_result::<()>(scope, exception, false).unwrap_err() + } + ModuleError::Other(error) => error, + }, + )?; + } - pub fn global_object<'s>( - &self, - isolate: &'s mut v8::Isolate, - ) -> v8::Local<'s, v8::Object> { - let scope = &mut self.handle_scope(isolate); - self.0.open(scope).global(scope) + let root_id = load.root_module_id.expect("Root module should be loaded"); + self.instantiate_module(isolate, root_id).map_err(|e| { + let scope = &mut self.handle_scope(isolate); + let exception = v8::Local::new(scope, e); + exception_to_err_result::<()>(scope, exception, false).unwrap_err() + })?; + Ok(root_id) } /// Executes traditional JavaScript code (traditional = not ES modules) in the