char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somext_build: support generating typedefs from typescript implf90c0ce

main
27.8 KiB725 linesraw
1mod extensions;
2pub mod message;
3mod microtasks;
4mod modules;
5mod op;
6mod repl;
7mod reporting;
8mod snapshot;
9mod stack_trace;
10mod structured_clone;
11mod tasks;
12mod wasm_streaming;
13
14use std::cell::RefCell;
15use std::collections::HashMap;
16use std::rc::Rc;
17use std::sync::OnceLock;
18use std::task::Context;
19
20use serde_json::json;
21use v8::inspector::StringView;
22
23use serde::de::DeserializeOwned;
24
25pub use modules::{ModuleFormat, ModuleLoader, ModuleSource};
26pub use op::Op;
27pub use serde_v8;
28pub use snapshot::Snapshot;
29pub use v8;
30pub use wasm_streaming::{WasmStreamingRegistry, WasmStreamingStart, WasmStreamingStartError};
31
32/// Bootstrap modules with a private graph, separate from guest modules and public builtins.
33pub struct Extension {
34    pub name: &'static str,
35    /// Extensions that must be present and whose entry points run first.
36    pub dependencies: &'static [&'static str],
37    pub ops: Vec<Op>,
38    /// Canonical specifiers and sources, registered before any entry point executes.
39    pub modules: &'static [(&'static str, &'static str)],
40    /// An embedded module to evaluate before guest execution.
41    /// Live setup registered through `ext:core/bootstrap` runs after all entries.
42    pub entry_point: Option<&'static str>,
43}
44
45/// A guest module and the embedded source/declaration pair that defines it.
46pub struct PublicModule {
47    pub name: &'static str,
48    pub module: (&'static str, &'static str),
49    pub declaration: &'static str,
50}
51
52impl PublicModule {
53    pub fn register(&self, runtime: &mut Runtime) -> Result<(), Error> {
54        runtime.register_extension_module(self.name, self.module.0)
55    }
56}
57
58/// Embed matching JavaScript and declarations from one build output stem.
59/// Use the row's `module` in `Extension::modules` as well as registering the row.
60#[macro_export]
61macro_rules! public_module {
62    ($name:literal, $extension:literal, $output:literal $(,)?) => {
63        $crate::PublicModule {
64            name: $name,
65            module: (
66                concat!($extension, "/", $output, ".js"),
67                include_str!(concat!(env!("OUT_DIR"), "/", $output, ".js")),
68            ),
69            declaration: include_str!(concat!(env!("OUT_DIR"), "/", $output, ".d.ts")),
70        }
71    };
72}
73
74#[derive(Default)]
75pub struct RuntimeOptions {
76    pub extensions: Vec<Extension>,
77    pub module_loader: Option<Rc<dyn ModuleLoader>>,
78}
79
80#[derive(Debug, thiserror::Error)]
81pub enum Error {
82    #[error("{0}")]
83    JavaScript(String),
84    #[error("Duplicate op: {0}")]
85    DuplicateOp(&'static str),
86    #[error("{0}")]
87    Module(String),
88    #[error("SyntaxError: {0}")]
89    Syntax(String),
90    #[error(transparent)]
91    Serde(#[from] serde_v8::Error),
92    #[error(transparent)]
93    Snapshot(#[from] SnapshotError),
94}
95
96#[derive(Debug, thiserror::Error)]
97pub enum SnapshotError {
98    #[error("Snapshot has no layout matching these extensions")]
99    IncompatibleLayout,
100    #[error("Snapshot layouts must be distinct")]
101    DuplicateLayout,
102    #[error("Cannot snapshot active runtime state: {0}")]
103    ActiveState(&'static str),
104    #[error("V8 could not create a snapshot")]
105    CreationFailed,
106    #[error("Invalid snapshot artifact: {0}")]
107    InvalidArtifact(&'static str),
108    #[error("Invalid snapshot metadata: {0}")]
109    Metadata(#[from] serde_json::Error),
110    #[error("Invalid snapshot data: {0}")]
111    Data(#[from] v8::DataError),
112    #[error("Live runtimes in this process use a different V8 read-only heap")]
113    ReadOnlyHeapInUse,
114    #[error("Runtimes cannot start in a snapshot generator process")]
115    GeneratorProcess,
116    #[error("Snapshot generation must initialize V8 before any runtime in its process")]
117    RuntimeProcess,
118}
119
120pub struct Runtime {
121    // Global handles must be dropped before their isolate.
122    repl: Option<repl::Inspector>,
123    module_evaluation: Option<v8::Global<v8::Promise>>,
124    context: v8::Global<v8::Context>,
125    isolate: snapshot::IsolateOwner,
126}
127
128pub(crate) fn source<'s>(
129    scope: &mut v8::PinScope<'s, '_>,
130    name: &str,
131    code: &str,
132    is_module: bool,
133    host_options: Option<v8::Local<'s, v8::Data>>,
134) -> v8::script_compiler::Source {
135    let resource = v8::String::new(scope, name).unwrap();
136    let origin = v8::ScriptOrigin::new(
137        scope,
138        resource.into(),
139        0,
140        0,
141        false,
142        -1,
143        None,
144        false,
145        false,
146        is_module,
147        host_options,
148    );
149    let code = v8::String::new(scope, code).unwrap();
150    v8::script_compiler::Source::new(code, Some(&origin))
151}
152
153#[derive(Clone, Copy, PartialEq, Eq)]
154pub(crate) enum Process {
155    Runtime,
156    Generator,
157}
158
159/// The first caller decides how this process initializes V8.
160pub(crate) fn initialize_v8(process: Process) -> Process {
161    static PROCESS: OnceLock<Process> = OnceLock::new();
162    *PROCESS.get_or_init(|| {
163        // Snapshot creators must reuse V8's read-only heap rather than extend it.
164        let mut flags = String::from("--no-extensible-ro-snapshot");
165        if process == Process::Generator {
166            // Seeds identity hashes and V8's mmap hints, which shape artifact bytes.
167            flags.push_str(" --random-seed=1534");
168        }
169        v8::V8::set_flags_from_string(&flags);
170        let platform = v8::new_default_platform(0, false).make_shared();
171        v8::V8::initialize_platform(platform);
172        v8::V8::initialize();
173        process
174    })
175}
176
177/// Initialize V8 for reproducible snapshot generation.
178///
179/// This must run before any other avril-core use, in a dedicated process such as a
180/// build script: it seeds V8's process-wide random number generators, so runtimes
181/// are refused afterwards with [`SnapshotError::GeneratorProcess`].
182pub fn initialize_snapshot_generator() -> Result<(), SnapshotError> {
183    match initialize_v8(Process::Generator) {
184        Process::Generator => Ok(()),
185        Process::Runtime => Err(SnapshotError::RuntimeProcess),
186    }
187}
188
189fn runtime_process() -> Result<(), SnapshotError> {
190    match initialize_v8(Process::Runtime) {
191        Process::Runtime => Ok(()),
192        Process::Generator => Err(SnapshotError::GeneratorProcess),
193    }
194}
195
196pub(crate) enum ContextSource<'a> {
197    /// A new context for cold startup or snapshot generation.
198    Fresh,
199    Layout(&'a snapshot::Layout),
200}
201
202impl Runtime {
203    /// Bootstrap on V8's built-in heap.
204    pub fn new(options: RuntimeOptions) -> Result<Self, Error> {
205        runtime_process()?;
206        let isolate = snapshot::IsolateOwner::new(snapshot::Source::BuiltIn)?;
207        Self::bootstrap(isolate, options, ContextSource::Fresh)?.start()
208    }
209
210    /// Restore the snapshot layout matching these extensions, attach fresh native
211    /// state, then run live initializers.
212    pub fn from_snapshot(options: RuntimeOptions, snapshot: &Snapshot) -> Result<Self, Error> {
213        runtime_process()?;
214        let layout = snapshot.layout(&snapshot::fingerprint(&options.extensions)?)?;
215        let isolate = snapshot::IsolateOwner::new(snapshot::Source::Snapshot(snapshot))?;
216        Self::bootstrap(isolate, options, ContextSource::Layout(layout))?.start()
217    }
218
219    /// Evaluate each layout's static extension graph, without live initializers,
220    /// into one snapshot whose contexts share a read-only heap.
221    ///
222    /// Static entries must not retain native resources, pending work, or global
223    /// handles outside core-owned state: V8 aborts the process rather than
224    /// serialize a heap still referenced by foreign handles.
225    ///
226    /// Run generation in a short-lived build process, after
227    /// [`initialize_snapshot_generator`] for reproducible bytes. rusty_v8 cannot
228    /// dispose an unfinished snapshot creator: on failure, core-owned native state
229    /// is released, but the creator and V8's built-in read-only heap are leaked
230    /// (with a warning) until process exit, so later snapshot restores in this
231    /// process fail with [`SnapshotError::ReadOnlyHeapInUse`].
232    pub fn create_snapshot(layouts: Vec<RuntimeOptions>) -> Result<Snapshot, Error> {
233        initialize_v8(Process::Runtime);
234        snapshot::create(layouts)
235    }
236
237    pub(crate) fn bootstrap(
238        mut isolate: snapshot::IsolateOwner,
239        options: RuntimeOptions,
240        context: ContextSource,
241    ) -> Result<Self, Error> {
242        let order = extensions::validate(&options.extensions)?;
243        let mut ops = Vec::new();
244        let mut sources = HashMap::new();
245        for extension in &options.extensions {
246            sources.extend(extension.modules.iter().copied());
247        }
248        sources.extend(extensions::SOURCES.iter().copied());
249        let entries = order
250            .iter()
251            .filter_map(|index| options.extensions[*index].entry_point)
252            .collect::<Vec<_>>();
253        for extension in options.extensions {
254            ops.extend(extension.ops);
255        }
256        let ops = op::RegisteredOps::new(ops)?;
257
258        let wasm_streaming = Rc::new(RefCell::new(WasmStreamingRegistry::default()));
259        isolate.set_slot(wasm_streaming.clone());
260        isolate.set_wasm_streaming_callback(|scope, source, stream| {
261            let wasm_streaming = scope
262                .get_slot::<Rc<RefCell<WasmStreamingRegistry>>>()
263                .unwrap()
264                .clone();
265            let started = wasm_streaming.borrow_mut().start(stream);
266            match started {
267                Ok(start) => {
268                    if let Some(callback) = start.callback {
269                        let callback = v8::Local::new(scope, callback);
270                        let id = v8::Number::new(scope, start.id as f64);
271                        let undefined = v8::undefined(scope);
272                        v8::tc_scope!(let try_catch, scope);
273                        let result =
274                            callback.call(try_catch, undefined.into(), &[source, id.into()]);
275                        if result.is_none() {
276                            let exception = try_catch
277                                .exception()
278                                .unwrap_or_else(|| v8::undefined(try_catch).into());
279                            try_catch.reset();
280                            let _ = wasm_streaming.borrow_mut().abort(start.id, exception);
281                        }
282                    } else {
283                        let message =
284                            v8::String::new(scope, "Wasm streaming callback is unavailable")
285                                .unwrap();
286                        let exception = v8::Exception::type_error(scope, message);
287                        let _ = wasm_streaming.borrow_mut().abort(start.id, exception);
288                    }
289                }
290                Err(start) => {
291                    let message = v8::String::new(scope, &start.message).unwrap();
292                    let exception = v8::Exception::type_error(scope, message);
293                    start.stream.abort(Some(exception));
294                }
295            }
296        });
297        isolate.set_microtasks_policy(v8::MicrotasksPolicy::Explicit);
298        isolate.set_slot(ops.clone());
299        op::init(&mut isolate);
300        reporting::init(&mut isolate);
301        microtasks::init(&mut isolate);
302        tasks::init(&mut isolate);
303        modules::init(&mut isolate, options.module_loader);
304        let capture = isolate.is_creator();
305        let restored = matches!(context, ContextSource::Layout(_));
306        let context = {
307            v8::scope!(let scope, &mut *isolate);
308            match context {
309                ContextSource::Layout(layout) => {
310                    let context =
311                        v8::Context::from_snapshot(scope, layout.context, Default::default())
312                            .ok_or(SnapshotError::InvalidArtifact("missing layout context"))?;
313                    let scope = &mut v8::ContextScope::new(scope, context);
314                    extensions::restore(scope, sources, &layout.state)?;
315                    v8::Global::new(scope, context)
316                }
317                ContextSource::Fresh => {
318                    let context = v8::Context::new(scope, Default::default());
319                    let scope = &mut v8::ContextScope::new(scope, context);
320                    if capture {
321                        snapshot::define_stack_trace_limit(scope).unwrap();
322                    }
323                    let registry = ops.object(scope).unwrap();
324
325                    let mut source = source(
326                        scope,
327                        "<primordials>",
328                        include_str!("primordials.js"),
329                        false,
330                        None,
331                    );
332                    let primordials = v8::script_compiler::compile(
333                        scope,
334                        &mut source,
335                        v8::script_compiler::CompileOptions::NoCompileOptions,
336                        v8::script_compiler::NoCacheReason::NoReason,
337                    )
338                    .unwrap()
339                    .run(scope)
340                    .unwrap();
341                    let primordial_object = v8::Local::<v8::Object>::try_from(primordials).unwrap();
342                    extensions::init(scope, sources, primordial_object, registry).unwrap();
343                    v8::Global::new(scope, context)
344                }
345            }
346        };
347
348        let mut runtime = Self {
349            repl: None,
350            module_evaluation: None,
351            context,
352            isolate,
353        };
354        runtime.with_context(|scope| {
355            let module = extensions::execute(scope, "ext:core/stack_trace.js")?;
356            let namespace = v8::Local::<v8::Object>::try_from(module.get_module_namespace())
357                .expect("Core stack formatter must have a module namespace");
358            let key = v8::String::new(scope, "formatStackTrace")?;
359            let formatter = v8::Local::<v8::Function>::try_from(namespace.get(scope, key.into())?)
360                .expect("Core stack formatter must export formatStackTrace");
361            stack_trace::init(scope, formatter);
362            extensions::execute(scope, "ext:core/shared_array_buffer")?;
363            Some(())
364        })?;
365        if !restored {
366            for entry in entries {
367                runtime.with_context(|scope| extensions::execute(scope, entry).map(|_| ()))?;
368            }
369        }
370        runtime
371            .with_context(|scope| extensions::execute(scope, "ext:core/bootstrap").map(|_| ()))?;
372        Ok(runtime)
373    }
374
375    /// Run the live initializers registered through `ext:core/bootstrap`, exactly once.
376    fn start(mut self) -> Result<Self, Error> {
377        self.with_context(|scope| {
378            let module = extensions::execute(scope, "ext:core/bootstrap")?;
379            let namespace =
380                v8::Local::<v8::Object>::try_from(module.get_module_namespace()).ok()?;
381            let key = v8::String::new(scope, "startRuntime")?;
382            let start =
383                v8::Local::<v8::Function>::try_from(namespace.get(scope, key.into())?).ok()?;
384            start.call(scope, v8::undefined(scope).into(), &[])?;
385            Some(())
386        })?;
387        Ok(self)
388    }
389
390    fn with_context<T>(
391        &mut self,
392        execute: impl for<'s, 'i> FnOnce(&mut v8::PinScope<'s, 'i>) -> Option<T>,
393    ) -> Result<T, Error> {
394        v8::scope!(let scope, &mut *self.isolate);
395        let context = v8::Local::new(scope, &self.context);
396        let scope = &mut v8::ContextScope::new(scope, context);
397        v8::tc_scope!(let scope, scope);
398        reporting::clear_processed(scope);
399        let result = execute(scope);
400        let execution_exception = if result.is_none() {
401            scope.exception().map(|error| v8::Global::new(scope, error))
402        } else {
403            None
404        };
405        let execution_exception_reported = if let Some(error) = &execution_exception {
406            let error = v8::Local::new(scope, error);
407            reporting::was_processed(scope, error)
408        } else {
409            false
410        };
411        op::checkpoint(scope);
412        let exception = microtasks::process_exceptions(scope);
413        let rejection = op::process_rejections(scope);
414        let result = result.and_then(|value| {
415            if let Some(exception) = exception {
416                let error = v8::Local::new(scope, exception);
417                reporting::clear_processed(scope);
418                reporting::mark_processed_exception(scope, error);
419                scope.throw_exception(error);
420                None
421            } else if let Some(error) = rejection {
422                let error = v8::Local::new(scope, error);
423                if let Some(execution_exception) = &execution_exception {
424                    reporting::clear_processed(scope);
425                    scope.throw_exception(v8::Local::new(scope, execution_exception));
426                } else {
427                    scope.throw_exception(error);
428                }
429                None
430            } else {
431                Some(value)
432            }
433        });
434        if result.is_none()
435            && let Some(error) = scope.exception()
436            && !(execution_exception_reported
437                && execution_exception
438                    .as_ref()
439                    .is_some_and(|reported| v8::Local::new(scope, reported) == error))
440            && (execution_exception.is_some() || !reporting::was_processed(scope, error))
441        {
442            reporting::report_exception_for_value(scope, error);
443        }
444        let result = result.ok_or_else(|| {
445            let exception = scope
446                .stack_trace()
447                .filter(|value| !value.is_undefined())
448                .or_else(|| scope.exception());
449            let message = exception
450                .map(|value| value.to_rust_string_lossy(scope))
451                .unwrap_or_else(|| "JavaScript execution terminated".into());
452            Error::JavaScript(message)
453        });
454        reporting::clear_processed(scope);
455        result
456    }
457
458    /// Register a built-in module, capturing its export values at registration.
459    pub fn register_module(
460        &mut self,
461        name: &str,
462        exports: impl for<'s, 'i> FnOnce(&mut v8::PinScope<'s, 'i>) -> Option<v8::Local<'s, v8::Object>>,
463    ) -> Result<(), Error> {
464        self.with_context(|scope| {
465            let exports = exports(scope)?;
466            modules::register(scope, name, exports)
467        })
468    }
469
470    /// Register an extension module's exports as a built-in module, capturing
471    /// its export values at registration. The extension module is evaluated if
472    /// it has not been already.
473    pub fn register_extension_module(
474        &mut self,
475        name: &str,
476        extension_module: &str,
477    ) -> Result<(), Error> {
478        self.with_context(|scope| {
479            let module = extensions::execute(scope, extension_module)?;
480            let namespace =
481                v8::Local::<v8::Object>::try_from(module.get_module_namespace()).ok()?;
482            modules::register(scope, name, namespace)
483        })
484    }
485
486    /// Execute a module graph; the first entry is the runtime's main module.
487    pub fn execute_module(&mut self, name: &str) -> Result<(), Error> {
488        let name = modules::prepare_entry(&mut self.isolate, name)?;
489        self.with_context(|scope| {
490            let promise = modules::execute(scope, &name)?;
491            op::run_event_loop(scope, Some(promise), true)
492        })
493    }
494
495    /// Evaluate an entry, then pass its namespace to a trusted extension bootstrap.
496    pub fn execute_module_with_bootstrap(
497        &mut self,
498        name: &str,
499        bootstrap: &str,
500        export: &str,
501    ) -> Result<(), Error> {
502        let name = modules::prepare_entry(&mut self.isolate, name)?;
503        self.with_context(|scope| {
504            let promise = modules::execute(scope, &name)?;
505            op::run_event_loop(scope, Some(promise), false)?;
506            let namespace = modules::namespace(scope, &name)?;
507            let module = extensions::execute(scope, bootstrap)?;
508            let exports = v8::Local::<v8::Object>::try_from(module.get_module_namespace()).ok()?;
509            let key = v8::String::new(scope, export)?;
510            let function =
511                v8::Local::<v8::Function>::try_from(exports.get(scope, key.into())?).ok()?;
512            let undefined = v8::undefined(scope);
513            let value = function.call(scope, undefined.into(), &[namespace])?;
514            let completion = v8::Local::<v8::Promise>::try_from(value).ok();
515            op::run_event_loop(scope, completion, false)
516        })
517    }
518
519    /// Start a prepared entry without blocking on top-level await. Polling the event
520    /// loop also monitors its evaluation promise.
521    pub fn start_module(&mut self, name: &str) -> Result<(), Error> {
522        if self.module_evaluation.is_some() {
523            return Err(Error::Module("Module evaluation is already pending".into()));
524        }
525        let name = modules::prepare_entry(&mut self.isolate, name)?;
526        let promise = self.with_context(|scope| {
527            modules::execute(scope, &name).map(|promise| v8::Global::new(scope, promise))
528        })?;
529        self.module_evaluation = Some(promise);
530        Ok(())
531    }
532
533    /// Drive microtasks and native ops until idle, reporting unhandled rejections.
534    pub fn run_event_loop(&mut self) -> Result<(), Error> {
535        let evaluation = self.module_evaluation.take();
536        self.with_context(|scope| {
537            let promise = evaluation.map(|promise| v8::Local::new(scope, promise));
538            op::run_event_loop(scope, promise, true)
539        })
540    }
541
542    /// Drive one task or native completion and register the caller's wakeup.
543    /// A false result does not imply idle: native work may still be pending.
544    pub fn poll_event_loop(&mut self, context: &mut Context<'_>) -> Result<bool, Error> {
545        self.isolate.set_slot(context.waker().clone());
546        let evaluation = self.module_evaluation.take();
547        let (progressed, evaluation) = self.with_context(|scope| {
548            let progressed = op::poll_event_loop(scope, context)?;
549            let evaluation = if let Some(promise) = evaluation {
550                let local = v8::Local::new(scope, &promise);
551                match local.state() {
552                    v8::PromiseState::Pending => Some(promise),
553                    v8::PromiseState::Fulfilled => None,
554                    v8::PromiseState::Rejected => {
555                        let error = local.result(scope);
556                        scope.throw_exception(error);
557                        return None;
558                    }
559                }
560            } else {
561                None
562            };
563            Some((progressed, evaluation))
564        })?;
565        self.module_evaluation = evaluation;
566        if progressed {
567            context.waker().wake_by_ref();
568        }
569        Ok(progressed)
570    }
571
572    pub fn isolate_handle(&self) -> v8::IsolateHandle {
573        self.isolate.thread_safe_handle()
574    }
575
576    pub fn has_pending_background_tasks(&self) -> bool {
577        self.isolate.has_pending_background_tasks()
578    }
579
580    pub fn has_pending_work(&self) -> bool {
581        self.module_evaluation.is_some()
582            || op::has_pending_work(&self.isolate)
583            || tasks::has_pending(&self.isolate)
584            || self.has_pending_background_tasks()
585    }
586
587    pub fn enable_repl(&mut self) -> Result<(), Error> {
588        if self.repl.is_some() {
589            return Err(Error::JavaScript(
590                "REPL inspector is already enabled".into(),
591            ));
592        }
593
594        self.repl = Some(repl::Inspector::new(&mut self.isolate, &self.context));
595
596        let id = self.dispatch_repl_message(json!({"method": "Runtime.enable", "params": {}}))?;
597        let _ = self.take_repl_response(id);
598        let inspector = self.repl.as_mut().unwrap();
599        let Some(context_id) = inspector.context_id() else {
600            return Err(Error::JavaScript(
601                "REPL execution context was not created".into(),
602            ));
603        };
604        inspector.context_id = context_id;
605        Ok(())
606    }
607
608    pub fn evaluate_repl(&mut self, expression: &str, source_url: &str) -> Result<i32, Error> {
609        self.set_repl_referrer(Some(source_url))?;
610        let expression = format!("{expression}\n//# sourceURL={source_url}");
611        self.dispatch_repl_message(json!({
612            "method": "Runtime.evaluate",
613            "params": {
614                "expression": expression,
615                "contextId": self.repl.as_ref().map(|inspector| inspector.context_id),
616                "awaitPromise": true,
617                "replMode": true,
618                "includeCommandLineAPI": false,
619                "objectGroup": "avril-repl"
620            }
621        }))
622    }
623
624    /// Call a function in the REPL context with inspector `CallArgument`s, such as
625    /// `{"objectId": ...}` from an earlier result, and resolve to its JSON value.
626    pub fn call_repl_function(
627        &mut self,
628        declaration: &str,
629        arguments: &[serde_json::Value],
630    ) -> Result<i32, Error> {
631        self.dispatch_repl_message(json!({
632            "method": "Runtime.callFunctionOn",
633            "params": {
634                "functionDeclaration": declaration,
635                "executionContextId": self.repl.as_ref().map(|inspector| inspector.context_id),
636                "arguments": arguments,
637                "awaitPromise": true,
638                "returnByValue": true,
639                "objectGroup": "avril-repl"
640            }
641        }))
642    }
643
644    pub fn has_repl_response(&self, id: i32) -> bool {
645        self.repl
646            .as_ref()
647            .is_some_and(|inspector| inspector.has_response(id))
648    }
649
650    pub fn take_repl_response(&mut self, id: i32) -> Option<String> {
651        self.repl.as_mut()?.take_response(id)
652    }
653
654    fn set_repl_referrer(&mut self, referrer: Option<&str>) -> Result<(), Error> {
655        v8::scope!(let scope, &mut *self.isolate);
656        let context = v8::Local::new(scope, &self.context);
657        let scope = &mut v8::ContextScope::new(scope, context);
658        modules::set_repl_referrer(scope, referrer)
659            .ok_or_else(|| Error::JavaScript("REPL module map is unavailable".into()))
660    }
661
662    pub fn terminate_repl_execution(&mut self) -> Result<(), Error> {
663        let id = self.dispatch_repl_message(json!({
664            "method": "Runtime.terminateExecution",
665            "params": {}
666        }))?;
667        let _ = self.take_repl_response(id);
668        Ok(())
669    }
670
671    pub fn release_repl_objects(&mut self) -> Result<(), Error> {
672        let id = self.dispatch_repl_message(json!({
673            "method": "Runtime.releaseObjectGroup",
674            "params": {"objectGroup": "avril-repl"}
675        }))?;
676        let _ = self.take_repl_response(id);
677        Ok(())
678    }
679
680    fn dispatch_repl_message(&mut self, mut command: serde_json::Value) -> Result<i32, Error> {
681        let inspector = self
682            .repl
683            .as_mut()
684            .ok_or_else(|| Error::JavaScript("REPL inspector is not enabled".into()))?;
685        let id = inspector.next_id;
686        inspector.next_id = inspector
687            .next_id
688            .checked_add(1)
689            .ok_or_else(|| Error::JavaScript("REPL inspector command ID space exhausted".into()))?;
690        command["id"] = json!(id);
691        let message = serde_json::to_string(&command)
692            .map_err(|error| Error::JavaScript(error.to_string()))?;
693        inspector
694            .session
695            .dispatch_protocol_message(StringView::from(message.as_bytes()));
696        Ok(id)
697    }
698
699    pub fn disable_repl(&mut self) -> Result<(), Error> {
700        self.isolate.remove_slot::<std::task::Waker>();
701        let result = self.set_repl_referrer(None);
702        self.repl.take();
703        result
704    }
705
706    /// Evaluate a script and drain microtasks without waiting for native ops.
707    pub fn execute_script<T: DeserializeOwned>(
708        &mut self,
709        name: &str,
710        code: &str,
711    ) -> Result<T, Error> {
712        self.with_context(|scope| {
713            let mut source = source(scope, name, code, false, None);
714            let value = v8::script_compiler::compile(
715                scope,
716                &mut source,
717                v8::script_compiler::CompileOptions::NoCompileOptions,
718                v8::script_compiler::NoCacheReason::NoReason,
719            )?
720            .run(scope)?;
721            op::checkpoint(scope);
722            Some(serde_v8::from_v8(scope, value).map_err(Error::from))
723        })?
724    }
725}