rusty_racer 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1061 @@
1
+ // The request/dispatch layer: the Request and VmReply value types, the
2
+ // service_request -> dispatch_one fan-out, every per-op handler (op_*), and the
3
+ // three op helpers run_source / call_function / compile_source. Extracted from
4
+ // lib.rs verbatim.
5
+ //
6
+ // Request, VmReply and Compiled are pub(crate) (the magnus method impls and
7
+ // Core/Isolate/Context/Module/Script wiring still in lib.rs build and consume
8
+ // them, and read Compiled's fields). service_request is pub(crate) (Core::run
9
+ // calls it) and run_source too (Snapshot warmup calls it). request_realm,
10
+ // dispatch_one, the op_* handlers, call_function and compile_source are used
11
+ // only here and stay private.
12
+ //
13
+ // ops.rs reaches the crate root's (private) helpers, structs and the istate!
14
+ // macro through the imports below; the marshal/watchdog symbols come from their
15
+ // own modules.
16
+
17
+ use crate::istate;
18
+ use crate::marshal::{js_to_jsval, jsval_to_js, JsVal};
19
+ use crate::*;
20
+
21
+ // One VM operation, built by a magnus method and run inline by Core::run ->
22
+ // service_request -> dispatch_one. |context_id| selects which realm the op runs
23
+ // in: 0 = the main realm (Context's own globalThis, swappable by reset_realm),
24
+ // N >= 1 = an extra realm made by create_context.
25
+ pub(crate) enum Request {
26
+ Eval {
27
+ context_id: i32,
28
+ source: String,
29
+ filename: String,
30
+ timeout_ms: u64,
31
+ },
32
+ // Resolve a dotted function path on globalThis and invoke it with marshalled
33
+ // args (v8::Function::call), preserving the holder as `this`. Distinct from
34
+ // Eval so args keep full type/identity fidelity instead of a JSON literal.
35
+ Call {
36
+ context_id: i32,
37
+ name: String,
38
+ args: Vec<JsVal>,
39
+ // void = don't marshal the return (fire-and-forget): the called fn may
40
+ // return a huge/cyclic JS object the caller never reads.
41
+ void: bool,
42
+ timeout_ms: u64,
43
+ },
44
+ // Drain the isolate's microtask queue once (no auto event loop).
45
+ DrainMicrotasks {
46
+ timeout_ms: u64,
47
+ },
48
+ Attach {
49
+ context_id: i32,
50
+ name: String,
51
+ host_fn_id: usize,
52
+ timeout_ms: u64,
53
+ },
54
+ // Batch attach: install many (name, host_fn_id) host fns in one round-trip
55
+ // (a fresh realm needs ~dozens). Same semantics as Attach, applied in order.
56
+ AttachMany {
57
+ context_id: i32,
58
+ entries: Vec<(String, usize)>,
59
+ timeout_ms: u64,
60
+ },
61
+ // reset: swap globalThis for a fresh v8::Context, reusing the same warm
62
+ // isolate — csim's per-visit reset. Applies to the named context.
63
+ Reset {
64
+ context_id: i32,
65
+ },
66
+ // create_context: build a fresh, persistent v8::Context in the isolate and
67
+ // return its id (the multi-realm model). DisposeContext frees one.
68
+ CreateContext,
69
+ DisposeContext {
70
+ context_id: i32,
71
+ },
72
+ // Thin ES-module primitives (V8's raw compile/instantiate/evaluate). The
73
+ // embedder owns the url->Module registry and the resolve policy; the binding
74
+ // just exposes the steps. A compiled module is addressed by an id (like a
75
+ // realm) since a v8::Local handle can't outlive the op's scope.
76
+ CompileModule {
77
+ // The context to compile the module in (modules are realm-bound).
78
+ context_id: i32,
79
+ source: String,
80
+ filename: String,
81
+ // Bytecode cache to consume (skip reparse); None compiles fresh.
82
+ cached_data: Option<Vec<u8>>,
83
+ // Produce a fresh bytecode cache to hand back (Module#cached_data).
84
+ produce_cache: bool,
85
+ // Eager-compile every function up front (CompileOptions::EagerCompile)
86
+ // instead of V8's default lazy top-level-only compile. Ignored when
87
+ // cached_data is set (V8 forbids ConsumeCodeCache + EagerCompile).
88
+ eager: bool,
89
+ },
90
+ // instantiate: V8 walks imports, calling back to the Ruby resolve block
91
+ // (parked in the slot for the op) per edge via resolve_imported.
92
+ InstantiateModule {
93
+ module_id: i32,
94
+ },
95
+ EvaluateModule {
96
+ module_id: i32,
97
+ timeout_ms: u64,
98
+ },
99
+ ModuleNamespace {
100
+ module_id: i32,
101
+ },
102
+ // The module's v8::Module::Status, as a lowercase name ("uninstantiated",
103
+ // "instantiated", ...) the Ruby wrapper symbolizes.
104
+ ModuleStatus {
105
+ module_id: i32,
106
+ },
107
+ DisposeModule {
108
+ module_id: i32,
109
+ },
110
+ // Classic <script> primitives (V8 ScriptCompiler::CompileUnboundScript): an
111
+ // unbound script, compiled in a context, runnable repeatedly, with the same
112
+ // bytecode-cache options as modules. Addressed by id like a module.
113
+ CompileScript {
114
+ context_id: i32,
115
+ source: String,
116
+ filename: String,
117
+ cached_data: Option<Vec<u8>>,
118
+ produce_cache: bool,
119
+ eager: bool,
120
+ },
121
+ // Bind the script to its context and run it; returns the completion value.
122
+ RunScript {
123
+ script_id: i32,
124
+ timeout_ms: u64,
125
+ },
126
+ DisposeScript {
127
+ script_id: i32,
128
+ },
129
+ // Serialize a bytecode cache from a compiled handle's CURRENT compile state
130
+ // (Script#create_code_cache / Module#create_code_cache). Called after run/
131
+ // evaluate, it captures the inner functions V8 lazily compiled while running
132
+ // — the only way (as of V8-150) to get inner-function bytecode into a cache,
133
+ // since create_code_cache at compile time only sees the top level.
134
+ ScriptCodeCache {
135
+ script_id: i32,
136
+ },
137
+ ModuleCodeCache {
138
+ module_id: i32,
139
+ },
140
+ }
141
+
142
+ // compile_module result: the module's id plus any produced bytecode cache and
143
+ // whether a supplied cache was rejected.
144
+ pub(crate) struct Compiled {
145
+ pub(crate) id: i32,
146
+ pub(crate) cached_data: Option<Vec<u8>>,
147
+ pub(crate) cache_rejected: bool,
148
+ }
149
+
150
+ // The terminal reply of an op: service_request returns it straight up to
151
+ // Core::run (no channel). Host callbacks and module resolvers don't round-trip
152
+ // through here — they run inline (with_gvl).
153
+ pub(crate) enum VmReply {
154
+ Done(Result<JsVal, VmError>),
155
+ // compile_module / compile's richer reply (id + produced cache + rejected).
156
+ ModuleCompiled(Result<Compiled, VmError>),
157
+ ScriptCompiled(Result<Compiled, VmError>),
158
+ // Script#/Module#create_code_cache: the serialized bytes, or None when V8
159
+ // can't produce a cache (or the handle's realm is gone).
160
+ CodeCache(Result<Option<Vec<u8>>, VmError>),
161
+ }
162
+
163
+ pub(crate) fn run_source(scope: &mut v8::PinScope<'_, '_>, source: &str, filename: &str) -> Result<JsVal, VmError> {
164
+ v8::tc_scope!(let tc, scope);
165
+ // Compile and run as distinct phases so a compile failure maps to
166
+ // ParseError and a thrown exception to RuntimeError (csim rescues both).
167
+ let Some(code) = v8::String::new(tc, source) else {
168
+ return Err(VmError::Parse("source too large".into()));
169
+ };
170
+ let origin = script_origin(tc, filename);
171
+ let script = match v8::Script::compile(tc, code, Some(&origin)) {
172
+ Some(script) => script,
173
+ None if tc.has_terminated() => return Err(VmError::Terminated),
174
+ None => {
175
+ let msg = tc
176
+ .exception()
177
+ .map(|e| e.to_rust_string_lossy(tc))
178
+ .unwrap_or_else(|| "parse error".to_string());
179
+ // Append the location V8 recorded; always name the file, add the
180
+ // line when V8 reports one.
181
+ let message = tc.message();
182
+ let res = message
183
+ .and_then(|m| m.get_script_resource_name(tc))
184
+ .filter(|v| v.is_string())
185
+ .map(|v| v.to_rust_string_lossy(tc))
186
+ .unwrap_or_else(|| filename.to_string());
187
+ let loc = match message.and_then(|m| m.get_line_number(tc)) {
188
+ Some(line) => format!(" at {res}:{line}"),
189
+ None => format!(" at {res}"),
190
+ };
191
+ return Err(VmError::Parse(format!("{msg}{loc}")));
192
+ }
193
+ };
194
+ match script.run(tc) {
195
+ Some(value) => Ok(js_to_jsval(tc, value)),
196
+ None if tc.has_terminated() => Err(VmError::Terminated),
197
+ None => {
198
+ let exc = tc.exception();
199
+ let stack = tc.stack_trace();
200
+ Err(capture_js_error(tc, exc, stack))
201
+ }
202
+ }
203
+ }
204
+
205
+ // Resolve a dotted property path on globalThis to a function and invoke it via
206
+ // v8::Function::call, with the property's holder as `this` (so `a.b.f` gets the
207
+ // right receiver). Args/result marshal through the ref-preserving paths.
208
+ fn call_function(
209
+ scope: &mut v8::PinScope<'_, '_>,
210
+ name: &str,
211
+ args: Vec<JsVal>,
212
+ void: bool,
213
+ ) -> Result<JsVal, VmError> {
214
+ v8::tc_scope!(let tc, scope);
215
+ let context = tc.get_current_context();
216
+ let global = context.global(tc);
217
+ let mut recv: v8::Local<v8::Value> = global.into();
218
+ let mut target: v8::Local<v8::Value> = global.into();
219
+ for part in name.split('.') {
220
+ let Some(obj) = target.to_object(tc) else {
221
+ // The holder of `part` (a preceding segment) was null/undefined, so
222
+ // there's nothing to read `part` from — name the holder, not `part`.
223
+ return Err(VmError::Runtime(format!(
224
+ "`{name}`: cannot read `{part}` (a preceding path segment is not an object)"
225
+ )));
226
+ };
227
+ let Some(key) = v8::String::new(tc, part) else {
228
+ return Err(VmError::Runtime("property name too large".into()));
229
+ };
230
+ let Some(next) = obj.get(tc, key.into()) else {
231
+ if tc.has_terminated() {
232
+ return Err(VmError::Terminated);
233
+ }
234
+ let msg = tc
235
+ .exception()
236
+ .map(|e| e.to_rust_string_lossy(tc))
237
+ .unwrap_or_else(|| format!("cannot read `{part}` of `{name}`"));
238
+ return Err(VmError::Runtime(msg));
239
+ };
240
+ recv = target;
241
+ target = next;
242
+ }
243
+ let Ok(func) = v8::Local::<v8::Function>::try_from(target) else {
244
+ return Err(VmError::Runtime(format!("`{name}` is not a function")));
245
+ };
246
+ let argv: Vec<v8::Local<v8::Value>> = args.into_iter().map(|a| jsval_to_js(tc, a)).collect();
247
+ match func.call(tc, recv, &argv) {
248
+ // void: skip marshalling the return so a huge/cyclic result is never walked.
249
+ Some(_) if void => Ok(JsVal::Undefined),
250
+ Some(value) => Ok(js_to_jsval(tc, value)),
251
+ None if tc.has_terminated() => Err(VmError::Terminated),
252
+ None => {
253
+ let exc = tc.exception();
254
+ let stack = tc.stack_trace();
255
+ Err(capture_js_error(tc, exc, stack))
256
+ }
257
+ }
258
+ }
259
+
260
+ // The (Source, CompileOptions) pair shared by the module and script compile
261
+ // handlers: consume a supplied bytecode cache (skip reparse), else eager-compile
262
+ // every function up front, else compile lazily (V8's default — only the top
263
+ // level). A supplied cache wins over `eager`: V8's CompileOptionsIsValid forbids
264
+ // ConsumeCodeCache + EagerCompile together, so `eager` is ignored on the consume
265
+ // path. (Source is an owned struct — V8 copies the origin in — so returning it
266
+ // across this fn boundary keeps the same handle-lifetime contract as inlining.)
267
+ fn compile_source<'s>(
268
+ code: v8::Local<'s, v8::String>,
269
+ origin: &v8::ScriptOrigin<'s>,
270
+ cached_data: &Option<Vec<u8>>,
271
+ eager: bool,
272
+ ) -> (v8::script_compiler::Source, v8::script_compiler::CompileOptions) {
273
+ use v8::script_compiler::{CompileOptions, Source};
274
+ match cached_data {
275
+ Some(bytes) => (
276
+ Source::new_with_cached_data(code, Some(origin), v8::script_compiler::CachedData::new(bytes)),
277
+ CompileOptions::ConsumeCodeCache,
278
+ ),
279
+ None if eager => (Source::new(code, Some(origin)), CompileOptions::EagerCompile),
280
+ None => (Source::new(code, Some(origin)), CompileOptions::NoCompileOptions),
281
+ }
282
+ }
283
+
284
+ // Service ONE request inline on the owner thread and RETURN its terminal reply.
285
+ // This is the single dispatcher for BOTH a top-level op and a re-entrant one (a
286
+ // host proc / module resolver that issues another op), so EVERY op — not just
287
+ // eval/call — works re-entrantly. `outermost` (depth == 0, computed by Core::run
288
+ // before it bumped the depth) owns the terminate-flag cleanup; a nested op
289
+ // passes false.
290
+ pub(crate) fn service_request(scope: &mut v8::PinScope<'_, '_, ()>, request: Request, outermost: bool) -> VmReply {
291
+ // Clear any terminate left over from BEFORE this request. An
292
+ // Isolate#terminate fired while no JS was running arms the isolate-global
293
+ // flag but no watchdog_fired, so the end-of-request sweep would miss it and
294
+ // the next eval would abort spuriously — and an idle terminate isn't even
295
+ // observable via is_execution_terminating() yet, so cancel unconditionally.
296
+ // Only at the outermost frame: a terminate aimed at a SUSPENDED outer frame
297
+ // must survive a nested request.
298
+ if outermost {
299
+ scope.cancel_terminate_execution();
300
+ }
301
+ // Mark the realm this request runs in active while it is on the stack, so
302
+ // Reset/DisposeContext can refuse to pull a live realm out from under a
303
+ // suspended frame.
304
+ let realm = request_realm(istate!(scope), &request);
305
+ if let Some(id) = realm {
306
+ istate!(scope).active_realms.push(id);
307
+ }
308
+ let reply = dispatch_one(scope, request, outermost);
309
+ if realm.is_some() {
310
+ istate!(scope).active_realms.pop();
311
+ }
312
+ // Sweep a leftover terminate flag once the whole request stack has
313
+ // unwound (see watchdog_fired for why nested frames must not cancel).
314
+ if outermost && istate!(scope).watchdog_fired {
315
+ istate!(scope).watchdog_fired = false;
316
+ scope.cancel_terminate_execution();
317
+ }
318
+ reply
319
+ }
320
+
321
+ // The realm a request will run in (None for realm-independent ops); feeds
322
+ // ACTIVE_REALMS above.
323
+ fn request_realm(state: &IsolateState, request: &Request) -> Option<i32> {
324
+ match request {
325
+ Request::Eval { context_id, .. }
326
+ | Request::Call { context_id, .. }
327
+ | Request::Attach { context_id, .. }
328
+ | Request::AttachMany { context_id, .. }
329
+ | Request::CompileModule { context_id, .. }
330
+ | Request::CompileScript { context_id, .. } => Some(*context_id),
331
+ Request::DrainMicrotasks { .. } => Some(0),
332
+ Request::InstantiateModule { module_id, .. }
333
+ | Request::EvaluateModule { module_id, .. }
334
+ | Request::ModuleNamespace { module_id, .. } => {
335
+ module_handle(state, *module_id).map(|(_, cid)| cid)
336
+ }
337
+ Request::RunScript { script_id, .. } => script_handle(state, *script_id).map(|(_, cid)| cid),
338
+ Request::Reset { .. }
339
+ | Request::CreateContext
340
+ | Request::DisposeContext { .. }
341
+ | Request::ModuleStatus { .. }
342
+ | Request::DisposeModule { .. }
343
+ | Request::DisposeScript { .. }
344
+ | Request::ScriptCodeCache { .. }
345
+ | Request::ModuleCodeCache { .. } => None,
346
+ }
347
+ }
348
+
349
+ fn dispatch_one(scope: &mut v8::PinScope<'_, '_, ()>, request: Request, outermost: bool) -> VmReply {
350
+ // A request-scoped handle scope, so handles created while servicing a
351
+ // nested request don't pile up in the suspended callback's scope.
352
+ v8::scope!(let scope, &mut *scope);
353
+ match request {
354
+ Request::Eval {
355
+ context_id,
356
+ source,
357
+ filename,
358
+ timeout_ms,
359
+ } => op_eval(scope, context_id, source, filename, timeout_ms, outermost),
360
+ Request::Call {
361
+ context_id,
362
+ name,
363
+ args,
364
+ void,
365
+ timeout_ms,
366
+ } => op_call(scope, context_id, name, args, void, timeout_ms, outermost),
367
+ Request::DrainMicrotasks { timeout_ms } => op_drain_microtasks(scope, timeout_ms),
368
+ Request::Attach {
369
+ context_id,
370
+ name,
371
+ host_fn_id,
372
+ timeout_ms,
373
+ } => op_attach(scope, context_id, name, host_fn_id, timeout_ms, outermost),
374
+ Request::AttachMany {
375
+ context_id,
376
+ entries,
377
+ timeout_ms,
378
+ } => op_attach_many(scope, context_id, entries, timeout_ms, outermost),
379
+ Request::Reset { context_id } => op_reset(scope, context_id),
380
+ Request::CreateContext => op_create_context(scope),
381
+ Request::DisposeContext { context_id } => op_dispose_context(scope, context_id),
382
+ Request::CompileModule {
383
+ context_id,
384
+ source,
385
+ filename,
386
+ cached_data,
387
+ produce_cache,
388
+ eager,
389
+ } => op_compile_module(scope, context_id, source, filename, cached_data, produce_cache, eager),
390
+ Request::InstantiateModule { module_id } => op_instantiate_module(scope, module_id),
391
+ Request::EvaluateModule { module_id, timeout_ms } => op_evaluate_module(scope, module_id, timeout_ms, outermost),
392
+ Request::ModuleNamespace { module_id } => op_module_namespace(scope, module_id),
393
+ Request::ModuleStatus { module_id } => op_module_status(scope, module_id),
394
+ Request::DisposeModule { module_id } => op_dispose_module(scope, module_id),
395
+ Request::CompileScript {
396
+ context_id,
397
+ source,
398
+ filename,
399
+ cached_data,
400
+ produce_cache,
401
+ eager,
402
+ } => op_compile_script(scope, context_id, source, filename, cached_data, produce_cache, eager),
403
+ Request::RunScript {
404
+ script_id,
405
+ timeout_ms,
406
+ } => op_run_script(scope, script_id, timeout_ms, outermost),
407
+ Request::DisposeScript { script_id } => op_dispose_script(scope, script_id),
408
+ // Serialize the script's CURRENT compile state. The stored handle is
409
+ // the UnboundScript, which V8 fills in with inner-function bytecode as
410
+ // run() lazily compiles them — so calling this after run() captures
411
+ // the functions that actually executed (a warm cache). None when V8
412
+ // can't serialize, or when the realm was reset/disposed out from under
413
+ // the script (its handle is gone): produce nil, not an error.
414
+ Request::ScriptCodeCache { script_id } => op_script_code_cache(scope, script_id),
415
+ // Same, for a module: get_unbound_module_script gives the shared
416
+ // compiled script, which evaluate() fills with inner-function bytecode.
417
+ // It needs the module's context entered (unlike UnboundScript), so
418
+ // a gone realm yields nil.
419
+ Request::ModuleCodeCache { module_id } => op_module_code_cache(scope, module_id),
420
+ }
421
+ }
422
+
423
+ fn op_eval(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32, source: String, filename: String, timeout_ms: u64, outermost: bool) -> VmReply {
424
+ let outcome = run_js_bracketed(scope, outermost, timeout_ms, "eval", |scope, outermost| {
425
+ let realm = context_for(istate!(scope), context_id);
426
+ match realm {
427
+ Some(ctx) => {
428
+ let context = v8::Local::new(scope, &ctx);
429
+ let scope = &mut v8::ContextScope::new(scope, context);
430
+ let out = run_source(scope, &source, &filename);
431
+ auto_drain(scope, outermost);
432
+ (true, out)
433
+ }
434
+ None => (false, Err(VmError::Runtime("realm disposed or unknown".into()))),
435
+ }
436
+ });
437
+ VmReply::Done(outcome)
438
+ }
439
+
440
+ #[allow(clippy::too_many_arguments)]
441
+ fn op_call(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32, name: String, args: Vec<JsVal>, void: bool, timeout_ms: u64, outermost: bool) -> VmReply {
442
+ // A host fn invoked by the called function runs inline
443
+ // (host_fn_callback, with_gvl) — no routing setup needed.
444
+ let outcome = run_js_bracketed(scope, outermost, timeout_ms, "call", |scope, outermost| {
445
+ let realm = context_for(istate!(scope), context_id);
446
+ match realm {
447
+ Some(ctx) => {
448
+ let context = v8::Local::new(scope, &ctx);
449
+ let scope = &mut v8::ContextScope::new(scope, context);
450
+ let out = call_function(scope, &name, args, void);
451
+ auto_drain(scope, outermost);
452
+ (true, out)
453
+ }
454
+ None => (false, Err(VmError::Runtime("realm disposed or unknown".into()))),
455
+ }
456
+ });
457
+ VmReply::Done(outcome)
458
+ }
459
+
460
+ fn op_drain_microtasks(scope: &mut v8::PinScope<'_, '_, ()>, timeout_ms: u64) -> VmReply {
461
+ // A microtask may call an attached host fn (a Promise .then ->
462
+ // ruby), which runs inline via host_fn_callback — no routing
463
+ // setup needed any more.
464
+ let watchdog = arm_watchdog(scope, timeout_ms);
465
+ let main = context_for(istate!(scope), 0);
466
+ if let Some(ctx) = main {
467
+ let context = v8::Local::new(scope, &ctx);
468
+ let scope = &mut v8::ContextScope::new(scope, context);
469
+ checkpoint_draining(scope);
470
+ }
471
+ let fired = disarm_watchdog(scope, watchdog);
472
+ if fired {
473
+ istate!(scope).watchdog_fired = true;
474
+ }
475
+ let outcome = if fired {
476
+ Err(VmError::Terminated)
477
+ } else {
478
+ Ok(JsVal::Undefined)
479
+ };
480
+ VmReply::Done(outcome)
481
+ }
482
+
483
+ fn op_attach(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32, name: String, host_fn_id: usize, timeout_ms: u64, outermost: bool) -> VmReply {
484
+ // attach_at_path writes onto globalThis (and walks a dotted
485
+ // path), which can fire a user-defined accessor or Proxy trap —
486
+ // arbitrary JS. So it goes through the same bracket as Eval: a
487
+ // host fn the trap calls routes back, and a looping trap is
488
+ // time-capped.
489
+ let outcome = run_js_bracketed(scope, outermost, timeout_ms, "attach", |scope, outermost| {
490
+ let realm = context_for(istate!(scope), context_id);
491
+ match realm {
492
+ Some(ctx) => {
493
+ let context = v8::Local::new(scope, &ctx);
494
+ let scope = &mut v8::ContextScope::new(scope, context);
495
+ let external = v8::External::new(scope, host_fn_id as *mut c_void);
496
+ let out = match v8::Function::builder(host_fn_callback)
497
+ .data(external.into())
498
+ .build(scope)
499
+ {
500
+ // A dotted name (e.g. "MiniRacer.foo") attaches
501
+ // under a namespace object, creating missing
502
+ // intermediates, so host fns needn't pollute the
503
+ // bare global.
504
+ Some(function) => attach_at_path(scope, context, &name, function),
505
+ None => Err(VmError::Runtime("failed to build function".into())),
506
+ };
507
+ auto_drain(scope, outermost);
508
+ (true, out)
509
+ }
510
+ None => (false, Err(VmError::Runtime("realm disposed or unknown".into()))),
511
+ }
512
+ });
513
+ VmReply::Done(outcome)
514
+ }
515
+
516
+ fn op_attach_many(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32, entries: Vec<(String, usize)>, timeout_ms: u64, outermost: bool) -> VmReply {
517
+ // Same as Attach (arbitrary JS via accessors/Proxy traps), but
518
+ // installs every entry under one bracket/drain. Applied in order;
519
+ // stops at the first failure and reports its (name-tagged) error.
520
+ // NOT transactional: entries before the failure stay attached —
521
+ // the realm is not rolled back (matches single Attach, which also
522
+ // commits its one write or fails it).
523
+ let outcome = run_js_bracketed(scope, outermost, timeout_ms, "attach_many", |scope, outermost| {
524
+ let realm = context_for(istate!(scope), context_id);
525
+ match realm {
526
+ Some(ctx) => {
527
+ let context = v8::Local::new(scope, &ctx);
528
+ let scope = &mut v8::ContextScope::new(scope, context);
529
+ let mut out = Ok(JsVal::Undefined);
530
+ for (name, host_fn_id) in &entries {
531
+ let external = v8::External::new(scope, *host_fn_id as *mut c_void);
532
+ out = match v8::Function::builder(host_fn_callback)
533
+ .data(external.into())
534
+ .build(scope)
535
+ {
536
+ Some(function) => attach_at_path(scope, context, name, function),
537
+ None => Err(VmError::Runtime(format!(
538
+ "failed to build function for `{name}`"
539
+ ))),
540
+ };
541
+ if out.is_err() {
542
+ break;
543
+ }
544
+ }
545
+ auto_drain(scope, outermost);
546
+ (true, out)
547
+ }
548
+ None => (false, Err(VmError::Runtime("realm disposed or unknown".into()))),
549
+ }
550
+ });
551
+ VmReply::Done(outcome)
552
+ }
553
+
554
+ fn op_reset(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32) -> VmReply {
555
+ let known =
556
+ context_id == 0 || istate!(scope).realms.contexts.contains_key(&context_id);
557
+ if istate!(scope).draining {
558
+ // A microtask from ANY realm may be mid-flight on the stack;
559
+ // swapping a v8::Context out from under it corrupts state.
560
+ VmReply::Done(Err(VmError::Runtime(
561
+ "cannot reset a realm during a microtask checkpoint".into(),
562
+ )))
563
+ } else if !known {
564
+ VmReply::Done(Err(VmError::Runtime(
565
+ "context disposed or unknown".into(),
566
+ )))
567
+ } else if istate!(scope).active_realms.contains(&context_id) {
568
+ // Swapping the v8::Context behind a suspended frame would
569
+ // drop its in-flight modules/scripts and let the realm id
570
+ // refer to a different context than the one on the stack
571
+ // (defeating the cross-context import guards).
572
+ VmReply::Done(Err(VmError::Runtime(
573
+ "cannot reset a realm while a request for it is suspended on the V8 stack"
574
+ .into(),
575
+ )))
576
+ } else {
577
+ let fresh = new_realm(scope, context_id);
578
+ {
579
+ let realms = &mut istate!(scope).realms;
580
+ if context_id == 0 {
581
+ realms.main_context = Some(fresh);
582
+ } else {
583
+ realms.contexts.insert(context_id, fresh);
584
+ }
585
+ }
586
+ // Drop modules bound to this context — their realm just changed.
587
+ drop_context_artifacts(istate!(scope), context_id);
588
+ VmReply::Done(Ok(JsVal::Undefined))
589
+ }
590
+ }
591
+
592
+ fn op_create_context(scope: &mut v8::PinScope<'_, '_, ()>) -> VmReply {
593
+ let id = {
594
+ let realms = &mut istate!(scope).realms;
595
+ let id = realms.next_context_id;
596
+ realms.next_context_id += 1;
597
+ id
598
+ };
599
+ let fresh = new_realm(scope, id);
600
+ istate!(scope).realms.contexts.insert(id, fresh);
601
+ VmReply::Done(Ok(JsVal::Int(id as i64)))
602
+ }
603
+
604
+ fn op_dispose_context(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32) -> VmReply {
605
+ if istate!(scope).draining {
606
+ // Same hazard as Reset: a microtask from any realm may be live.
607
+ VmReply::Done(Err(VmError::Runtime(
608
+ "cannot dispose a realm during a microtask checkpoint".into(),
609
+ )))
610
+ } else if istate!(scope).active_realms.contains(&context_id) {
611
+ // Same hazard as Reset: a suspended frame still runs in it.
612
+ VmReply::Done(Err(VmError::Runtime(
613
+ "cannot dispose a realm while a request for it is suspended on the V8 stack"
614
+ .into(),
615
+ )))
616
+ } else {
617
+ // Dropping the Global lets V8 collect the context. id 0 is the
618
+ // default context and never disposed independently.
619
+ istate!(scope).realms.contexts.remove(&context_id);
620
+ // Reclaim the modules compiled in it (else they leak until
621
+ // isolate teardown).
622
+ drop_context_artifacts(istate!(scope), context_id);
623
+ VmReply::Done(Ok(JsVal::Undefined))
624
+ }
625
+ }
626
+
627
+ #[allow(clippy::too_many_arguments)]
628
+ fn op_compile_module(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32, source: String, filename: String, cached_data: Option<Vec<u8>>, produce_cache: bool, eager: bool) -> VmReply {
629
+ let ctx = context_for(istate!(scope), context_id);
630
+ let outcome = match ctx {
631
+ None => Err(VmError::Runtime("context disposed or unknown".into())),
632
+ Some(cx) => {
633
+ let context = v8::Local::new(scope, &cx);
634
+ let scope = &mut v8::ContextScope::new(scope, context);
635
+ v8::tc_scope!(let tc, scope);
636
+ match v8::String::new(tc, &source) {
637
+ None => Err(VmError::Runtime("module source too large".into())),
638
+ Some(code) => {
639
+ let origin = module_origin(tc, &filename);
640
+ // Consume a supplied bytecode cache (skip reparse),
641
+ // eager-compile every function, or compile fresh
642
+ // (lazy). cached_data wins: V8 forbids combining
643
+ // ConsumeCodeCache with EagerCompile.
644
+ let (mut src, opts) = compile_source(code, &origin, &cached_data, eager);
645
+ let compiled = v8::script_compiler::compile_module2(
646
+ tc,
647
+ &mut src,
648
+ opts,
649
+ v8::script_compiler::NoCacheReason::NoReason,
650
+ );
651
+ match compiled {
652
+ Some(module) => {
653
+ // V8 marks a stale/incompatible supplied cache
654
+ // rejected; the embedder recompiles & re-caches.
655
+ let cache_rejected = cached_data.is_some()
656
+ && src.get_cached_data().is_some_and(|c| c.rejected());
657
+ // Produce a fresh cache from the unbound script.
658
+ let produced = if produce_cache {
659
+ module
660
+ .get_unbound_module_script(tc)
661
+ .create_code_cache()
662
+ .map(|c| c.to_vec())
663
+ } else {
664
+ None
665
+ };
666
+ let hash = module.get_identity_hash().get();
667
+ let g = v8::Global::new(tc, module);
668
+ let id = {
669
+ let m = &mut istate!(tc).modules;
670
+ let id = m.next_id;
671
+ m.next_id += 1;
672
+ m.by_id
673
+ .insert(id, (g.clone(), filename.clone(), context_id));
674
+ m.by_hash.entry(hash).or_default().push((g, id));
675
+ id
676
+ };
677
+ Ok(Compiled {
678
+ id,
679
+ cached_data: produced,
680
+ cache_rejected,
681
+ })
682
+ }
683
+ None if tc.has_terminated() => Err(VmError::Terminated),
684
+ // A module compile failure is a parse error
685
+ // (compile-time), not a thrown exception.
686
+ None => {
687
+ let msg = tc
688
+ .exception()
689
+ .map(|e| e.to_rust_string_lossy(tc))
690
+ .unwrap_or_else(|| "module parse error".to_string());
691
+ let message = tc.message();
692
+ let res = message
693
+ .and_then(|m| m.get_script_resource_name(tc))
694
+ .filter(|v| v.is_string())
695
+ .map(|v| v.to_rust_string_lossy(tc))
696
+ .unwrap_or_else(|| filename.clone());
697
+ let loc = match message.and_then(|m| m.get_line_number(tc)) {
698
+ Some(line) => format!(" at {res}:{line}"),
699
+ None => format!(" at {res}"),
700
+ };
701
+ Err(VmError::Parse(format!("{msg}{loc}")))
702
+ }
703
+ }
704
+ }
705
+ }
706
+ }
707
+ };
708
+ VmReply::ModuleCompiled(outcome)
709
+ }
710
+
711
+ fn op_instantiate_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
712
+ // V8's module instantiation is NOT re-entrant: a nested
713
+ // instantiate issued from a resolve block walks the outer,
714
+ // half-built module graph and SEGVs the process. Refuse it
715
+ // cleanly — a resolve block may COMPILE dependencies lazily
716
+ // and return them; the outer instantiate links them.
717
+ if istate!(scope).instantiating {
718
+ VmReply::Done(Err(VmError::Runtime(
719
+ "instantiate is not re-entrant: another module is currently \
720
+ instantiating (compile the dependency and return it; the outer \
721
+ instantiate links it)"
722
+ .into(),
723
+ )))
724
+ } else {
725
+ istate!(scope).instantiating = true;
726
+ let handle = module_handle(istate!(scope), module_id);
727
+ let outcome = match handle {
728
+ None => Err(VmError::Runtime("unknown module".into())),
729
+ Some((g, cid)) => match context_for(istate!(scope), cid) {
730
+ None => Err(VmError::Runtime("module's context is gone".into())),
731
+ Some(cx) => {
732
+ let context = v8::Local::new(scope, &cx);
733
+ let scope = &mut v8::ContextScope::new(scope, context);
734
+ let module = v8::Local::new(scope, &g);
735
+ match module.get_status() {
736
+ // Already linked (or further along): a no-op,
737
+ // not an error — instantiate is idempotent.
738
+ v8::ModuleStatus::Instantiated
739
+ | v8::ModuleStatus::Evaluating
740
+ | v8::ModuleStatus::Evaluated => Ok(JsVal::Undefined),
741
+ // V8 CHECK-aborts on instantiating an errored
742
+ // module; surface its exception instead.
743
+ v8::ModuleStatus::Errored => Err(VmError::JsError {
744
+ message: module
745
+ .get_exception()
746
+ .to_rust_string_lossy(scope),
747
+ backtrace: vec![],
748
+ }),
749
+ _ => {
750
+ v8::tc_scope!(let tc, scope);
751
+ match module.instantiate_module(tc, resolve_imported) {
752
+ Some(true) => Ok(JsVal::Undefined),
753
+ _ if tc.has_terminated() => Err(VmError::Terminated),
754
+ // A resolver that RAISED is re-raised with its
755
+ // real class by instantiate_module (via the
756
+ // stashed exception); this generic link error
757
+ // is only used when no resolver exception was
758
+ // stashed.
759
+ _ => {
760
+ let exc = tc.exception();
761
+ let stack = tc.stack_trace();
762
+ Err(capture_js_error(tc, exc, stack))
763
+ }
764
+ }
765
+ }
766
+ }
767
+ }
768
+ }
769
+ };
770
+ istate!(scope).instantiating = false;
771
+ VmReply::Done(outcome)
772
+ }
773
+ }
774
+
775
+ fn op_evaluate_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32, timeout_ms: u64, outermost: bool) -> VmReply {
776
+ // Top-level module code (and, under :auto, the microtasks its
777
+ // TLA continuation drains) can loop, so it runs in the same
778
+ // watchdog bracket as Eval/Call/RunScript.
779
+ let outcome = run_js_bracketed(scope, outermost, timeout_ms, "evaluate_module", |scope, outermost| {
780
+ let handle = module_handle(istate!(scope), module_id);
781
+ match handle {
782
+ None => (false, Err(VmError::Runtime("unknown module".into()))),
783
+ Some((g, cid)) => match context_for(istate!(scope), cid) {
784
+ None => (false, Err(VmError::Runtime("module's context is gone".into()))),
785
+ Some(cx) => {
786
+ let context = v8::Local::new(scope, &cx);
787
+ let scope = &mut v8::ContextScope::new(scope, context);
788
+ let module = v8::Local::new(scope, &g);
789
+ // A top-level-await module's evaluate() returns a
790
+ // PENDING promise that only settles once the drain
791
+ // runs its continuation — remember it so we can read
792
+ // its post-drain state instead of reporting a stale Ok.
793
+ let mut eval_promise: Option<v8::Global<v8::Promise>> = None;
794
+ // ran_js is true ONLY for the Instantiated arm that
795
+ // actually calls evaluate(); the Errored/Evaluated/
796
+ // non-instantiated arms run no JS, so a raced watchdog
797
+ // must not override their real outcome to Terminated.
798
+ let mut did_eval = false;
799
+ // V8 CHECK-aborts the process if evaluate runs on a
800
+ // module that isn't exactly Instantiated, so guard
801
+ // status explicitly rather than crash.
802
+ let out = match module.get_status() {
803
+ v8::ModuleStatus::Errored => {
804
+ Err(VmError::JsError {
805
+ message: module
806
+ .get_exception()
807
+ .to_rust_string_lossy(scope),
808
+ backtrace: vec![],
809
+ })
810
+ }
811
+ v8::ModuleStatus::Evaluated => Ok(JsVal::Undefined),
812
+ v8::ModuleStatus::Instantiated => {
813
+ did_eval = true;
814
+ v8::tc_scope!(let tc, scope);
815
+ match module.evaluate(tc) {
816
+ // A synchronous top-level throw yields a
817
+ // *rejected* promise (not None); a pending
818
+ // (TLA) or fulfilled one is remembered and
819
+ // re-checked after the drain.
820
+ Some(value) => match v8::Local::<v8::Promise>::try_from(value) {
821
+ Ok(p) if p.state() == v8::PromiseState::Rejected => {
822
+ let reason = p.result(tc);
823
+ Err(VmError::JsError {
824
+ message: reason.to_rust_string_lossy(tc),
825
+ backtrace: vec![],
826
+ })
827
+ }
828
+ Ok(p) => {
829
+ eval_promise = Some(v8::Global::new(tc, p));
830
+ Ok(JsVal::Undefined)
831
+ }
832
+ _ => Ok(JsVal::Undefined),
833
+ },
834
+ None if tc.has_terminated() => Err(VmError::Terminated),
835
+ None => {
836
+ let exc = tc.exception();
837
+ let stack = tc.stack_trace();
838
+ Err(capture_js_error(tc, exc, stack))
839
+ }
840
+ }
841
+ }
842
+ _ => Err(VmError::Runtime(
843
+ "module must be instantiated before evaluate".into(),
844
+ )),
845
+ };
846
+ auto_drain(scope, outermost);
847
+ // The drain may have settled a TLA module's promise to
848
+ // rejected — surface that instead of the provisional Ok.
849
+ let result = if let (true, Some(g)) = (out.is_ok(), eval_promise) {
850
+ let p = v8::Local::new(scope, &g);
851
+ if p.state() == v8::PromiseState::Rejected {
852
+ let reason = p.result(scope);
853
+ Err(VmError::JsError {
854
+ message: reason.to_rust_string_lossy(scope),
855
+ backtrace: vec![],
856
+ })
857
+ } else {
858
+ out
859
+ }
860
+ } else {
861
+ out
862
+ };
863
+ (did_eval, result)
864
+ }
865
+ }
866
+ }
867
+ });
868
+ VmReply::Done(outcome)
869
+ }
870
+
871
+ fn op_module_namespace(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
872
+ let handle = module_handle(istate!(scope), module_id);
873
+ let outcome = match handle {
874
+ None => Err(VmError::Runtime("unknown module".into())),
875
+ Some((g, cid)) => match context_for(istate!(scope), cid) {
876
+ None => Err(VmError::Runtime("module's context is gone".into())),
877
+ Some(cx) => {
878
+ let context = v8::Local::new(scope, &cx);
879
+ let scope = &mut v8::ContextScope::new(scope, context);
880
+ let module = v8::Local::new(scope, &g);
881
+ // get_module_namespace CHECK-aborts unless the module
882
+ // is at least Instantiated.
883
+ match module.get_status() {
884
+ v8::ModuleStatus::Uninstantiated
885
+ | v8::ModuleStatus::Instantiating => Err(VmError::Runtime(
886
+ "module must be instantiated before namespace".into(),
887
+ )),
888
+ _ => {
889
+ let ns = module.get_module_namespace();
890
+ Ok(js_to_jsval(scope, ns))
891
+ }
892
+ }
893
+ }
894
+ }
895
+ };
896
+ VmReply::Done(outcome)
897
+ }
898
+
899
+ fn op_module_status(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
900
+ let handle = module_handle(istate!(scope), module_id);
901
+ let outcome = match handle {
902
+ None => Err(VmError::Runtime("unknown module".into())),
903
+ Some((g, _cid)) => {
904
+ let module = v8::Local::new(scope, &g);
905
+ let name = match module.get_status() {
906
+ v8::ModuleStatus::Uninstantiated => "uninstantiated",
907
+ v8::ModuleStatus::Instantiating => "instantiating",
908
+ v8::ModuleStatus::Instantiated => "instantiated",
909
+ v8::ModuleStatus::Evaluating => "evaluating",
910
+ v8::ModuleStatus::Evaluated => "evaluated",
911
+ v8::ModuleStatus::Errored => "errored",
912
+ };
913
+ Ok(JsVal::Str(name.into()))
914
+ }
915
+ };
916
+ VmReply::Done(outcome)
917
+ }
918
+
919
+ fn op_dispose_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
920
+ let m = &mut istate!(scope).modules;
921
+ m.by_id.remove(&module_id);
922
+ for bucket in m.by_hash.values_mut() {
923
+ bucket.retain(|(_, id)| *id != module_id);
924
+ }
925
+ VmReply::Done(Ok(JsVal::Undefined))
926
+ }
927
+
928
+ #[allow(clippy::too_many_arguments)]
929
+ fn op_compile_script(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32, source: String, filename: String, cached_data: Option<Vec<u8>>, produce_cache: bool, eager: bool) -> VmReply {
930
+ let ctx = context_for(istate!(scope), context_id);
931
+ let outcome = match ctx {
932
+ None => Err(VmError::Runtime("context disposed or unknown".into())),
933
+ Some(cx) => {
934
+ let context = v8::Local::new(scope, &cx);
935
+ let scope = &mut v8::ContextScope::new(scope, context);
936
+ v8::tc_scope!(let tc, scope);
937
+ match v8::String::new(tc, &source) {
938
+ None => Err(VmError::Runtime("script source too large".into())),
939
+ Some(code) => {
940
+ let origin = script_origin(tc, &filename);
941
+ let (mut src, opts) = compile_source(code, &origin, &cached_data, eager);
942
+ match v8::script_compiler::compile_unbound_script(
943
+ tc,
944
+ &mut src,
945
+ opts,
946
+ v8::script_compiler::NoCacheReason::NoReason,
947
+ ) {
948
+ Some(unbound) => {
949
+ let cache_rejected = cached_data.is_some()
950
+ && src.get_cached_data().is_some_and(|c| c.rejected());
951
+ let produced = if produce_cache {
952
+ unbound.create_code_cache().map(|c| c.to_vec())
953
+ } else {
954
+ None
955
+ };
956
+ let g = v8::Global::new(tc, unbound);
957
+ let id = {
958
+ let s = &mut istate!(tc).scripts;
959
+ let id = s.next_id;
960
+ s.next_id += 1;
961
+ s.by_id.insert(id, (g, context_id));
962
+ id
963
+ };
964
+ Ok(Compiled {
965
+ id,
966
+ cached_data: produced,
967
+ cache_rejected,
968
+ })
969
+ }
970
+ None if tc.has_terminated() => Err(VmError::Terminated),
971
+ // Compile failure = a parse error (with location).
972
+ None => {
973
+ let msg = tc
974
+ .exception()
975
+ .map(|e| e.to_rust_string_lossy(tc))
976
+ .unwrap_or_else(|| "script parse error".to_string());
977
+ let message = tc.message();
978
+ let res = message
979
+ .and_then(|m| m.get_script_resource_name(tc))
980
+ .filter(|v| v.is_string())
981
+ .map(|v| v.to_rust_string_lossy(tc))
982
+ .unwrap_or_else(|| filename.clone());
983
+ let loc = match message.and_then(|m| m.get_line_number(tc)) {
984
+ Some(line) => format!(" at {res}:{line}"),
985
+ None => format!(" at {res}"),
986
+ };
987
+ Err(VmError::Parse(format!("{msg}{loc}")))
988
+ }
989
+ }
990
+ }
991
+ }
992
+ }
993
+ };
994
+ VmReply::ScriptCompiled(outcome)
995
+ }
996
+
997
+ fn op_run_script(scope: &mut v8::PinScope<'_, '_, ()>, script_id: i32, timeout_ms: u64, outermost: bool) -> VmReply {
998
+ let outcome = run_js_bracketed(scope, outermost, timeout_ms, "run_script", |scope, outermost| {
999
+ let handle = script_handle(istate!(scope), script_id);
1000
+ match handle {
1001
+ None => (false, Err(VmError::Runtime("unknown script".into()))),
1002
+ Some((g, cid)) => match context_for(istate!(scope), cid) {
1003
+ None => (false, Err(VmError::Runtime("script's context is gone".into()))),
1004
+ Some(cx) => {
1005
+ let context = v8::Local::new(scope, &cx);
1006
+ let scope = &mut v8::ContextScope::new(scope, context);
1007
+ let unbound = v8::Local::new(scope, &g);
1008
+ let script = unbound.bind_to_current_context(scope);
1009
+ let out = {
1010
+ v8::tc_scope!(let tc, scope);
1011
+ match script.run(tc) {
1012
+ Some(value) => Ok(js_to_jsval(tc, value)),
1013
+ None if tc.has_terminated() => Err(VmError::Terminated),
1014
+ None => {
1015
+ let exc = tc.exception();
1016
+ let stack = tc.stack_trace();
1017
+ Err(capture_js_error(tc, exc, stack))
1018
+ }
1019
+ }
1020
+ };
1021
+ auto_drain(scope, outermost);
1022
+ (true, out)
1023
+ }
1024
+ }
1025
+ }
1026
+ });
1027
+ VmReply::Done(outcome)
1028
+ }
1029
+
1030
+ fn op_dispose_script(scope: &mut v8::PinScope<'_, '_, ()>, script_id: i32) -> VmReply {
1031
+ istate!(scope).scripts.by_id.remove(&script_id);
1032
+ VmReply::Done(Ok(JsVal::Undefined))
1033
+ }
1034
+
1035
+ fn op_script_code_cache(scope: &mut v8::PinScope<'_, '_, ()>, script_id: i32) -> VmReply {
1036
+ let handle = script_handle(istate!(scope), script_id);
1037
+ let outcome = match handle {
1038
+ None => Ok(None),
1039
+ Some((g, _cid)) => {
1040
+ let unbound = v8::Local::new(scope, &g);
1041
+ Ok(unbound.create_code_cache().map(|c| c.to_vec()))
1042
+ }
1043
+ };
1044
+ VmReply::CodeCache(outcome)
1045
+ }
1046
+
1047
+ fn op_module_code_cache(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
1048
+ let mh = module_handle(istate!(scope), module_id);
1049
+ let handle = mh.and_then(|(g, cid)| context_for(istate!(scope), cid).map(|cx| (g, cx)));
1050
+ let outcome = match handle {
1051
+ None => Ok(None),
1052
+ Some((g, cx)) => {
1053
+ let context = v8::Local::new(scope, &cx);
1054
+ let scope = &mut v8::ContextScope::new(scope, context);
1055
+ let module = v8::Local::new(scope, &g);
1056
+ let unbound = module.get_unbound_module_script(scope);
1057
+ Ok(unbound.create_code_cache().map(|c| c.to_vec()))
1058
+ }
1059
+ };
1060
+ VmReply::CodeCache(outcome)
1061
+ }