rusty_racer 0.2.4 → 0.2.6

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.
@@ -29,18 +29,40 @@ macro_rules! marshalled {
29
29
  ($tc:expr, $value:expr) => {{
30
30
  let tc = $tc;
31
31
  let out = js_to_jsval(tc, $value);
32
- let exc = tc.exception();
33
- if exc.is_none() {
32
+ if tc.has_caught() {
33
+ Err(caught!(tc))
34
+ } else {
34
35
  Ok(out)
35
- } else if tc.has_terminated() {
36
- Err(VmError::Terminated)
36
+ }
37
+ }};
38
+ }
39
+
40
+ // The failure a TryCatch holds once a JS step came back empty: a termination, or
41
+ // the exception with its stack (and the thrown value kept — `keep_thrown`).
42
+ macro_rules! caught {
43
+ ($tc:expr) => {{
44
+ let tc = $tc;
45
+ if tc.has_terminated() {
46
+ VmError::Terminated
37
47
  } else {
48
+ let exc = tc.exception();
38
49
  let stack = tc.stack_trace();
39
- Err(capture_js_error(tc, exc, stack))
50
+ capture_js_error(tc, exc, stack)
40
51
  }
41
52
  }};
42
53
  }
43
54
 
55
+ // What an eval hands back of its completion value: all of it, marshalled; nothing (`Void`: a statement list run for its
56
+ // EFFECT still has one — the last statement's value — and marshalling it can throw, an accessor or a Proxy trap, or walk
57
+ // a huge graph, for a value the caller discards); or a primitive alone (`Primitive`: an object's never walked, undefined
58
+ // in its place — for a caller that wants a string or a number of it, as a javascript: URL's evaluation does).
59
+ #[derive(Clone, Copy)]
60
+ pub(crate) enum Completion {
61
+ Value,
62
+ Void,
63
+ Primitive,
64
+ }
65
+
44
66
  // One VM operation, built by a magnus method and run inline by Core::run ->
45
67
  // service_request -> dispatch_one. |context_id| selects which realm the op runs
46
68
  // in: 0 = the main realm (Context's own globalThis, swappable by reset_realm),
@@ -51,11 +73,7 @@ pub(crate) enum Request {
51
73
  source: String,
52
74
  filename: String,
53
75
  timeout_ms: u64,
54
- // void = don't marshal the completion value (see Call's |void|). A
55
- // statement-list eval run for its EFFECT still has one — the last
56
- // statement's value — and marshalling it can throw (an accessor, a
57
- // Proxy trap) or walk a huge graph, for a value the caller discards.
58
- void: bool,
76
+ completion: Completion,
59
77
  },
60
78
  // Resolve a dotted function path on globalThis and invoke it with marshalled
61
79
  // args (v8::Function::call), preserving the holder as `this`. Distinct from
@@ -69,6 +87,22 @@ pub(crate) enum Request {
69
87
  void: bool,
70
88
  timeout_ms: u64,
71
89
  },
90
+ // Context#function: resolve the path as Call does, once, and keep the
91
+ // function and its holder (FunctionReg); replies with the handle's id.
92
+ ResolveFunction {
93
+ context_id: i32,
94
+ path: String,
95
+ timeout_ms: u64,
96
+ },
97
+ // Function#call: invoke a resolved handle as Call invokes what it resolves,
98
+ // with no path walk and no property name made.
99
+ CallFunction {
100
+ context_id: i32,
101
+ function_id: u64,
102
+ args: Vec<JsVal>,
103
+ void: bool,
104
+ timeout_ms: u64,
105
+ },
72
106
  // Drain the isolate's microtask queue once (no auto event loop).
73
107
  DrainMicrotasks {
74
108
  timeout_ms: u64,
@@ -76,14 +110,14 @@ pub(crate) enum Request {
76
110
  Attach {
77
111
  context_id: i32,
78
112
  name: String,
79
- host_fn_id: usize,
113
+ host_fn: HostFnKey,
80
114
  timeout_ms: u64,
81
115
  },
82
- // Batch attach: install many (name, host_fn_id) host fns in one round-trip
116
+ // Batch attach: install many (name, key) host fns in one round-trip
83
117
  // (a fresh realm needs ~dozens). Same semantics as Attach, applied in order.
84
118
  AttachMany {
85
119
  context_id: i32,
86
- entries: Vec<(String, usize)>,
120
+ entries: Vec<(String, HostFnKey)>,
87
121
  timeout_ms: u64,
88
122
  },
89
123
  // reset: swap globalThis for a fresh v8::Context, reusing the same warm
@@ -123,6 +157,10 @@ pub(crate) enum Request {
123
157
  EvaluateModule {
124
158
  module_id: i32,
125
159
  timeout_ms: u64,
160
+ // The name of a function on the module's context's global that reports its evaluation's rejection — HTML "run
161
+ // a module script": upon rejection, report the exception — now or as a top-level-await module settles; none:
162
+ // the rejection is the op's error, as far as the drain gets.
163
+ report: Option<String>,
126
164
  },
127
165
  ModuleNamespace {
128
166
  module_id: i32,
@@ -137,9 +175,6 @@ pub(crate) enum Request {
137
175
  ModuleGraphAsync {
138
176
  module_id: i32,
139
177
  },
140
- DisposeModule {
141
- module_id: i32,
142
- },
143
178
  // Classic <script> primitives (V8 ScriptCompiler::CompileUnboundScript): an
144
179
  // unbound script, compiled in a context, runnable repeatedly, with the same
145
180
  // bytecode-cache options as modules. Addressed by id like a module.
@@ -158,9 +193,6 @@ pub(crate) enum Request {
158
193
  timeout_ms: u64,
159
194
  void: bool,
160
195
  },
161
- DisposeScript {
162
- script_id: i32,
163
- },
164
196
  // Serialize a bytecode cache from a compiled handle's CURRENT compile state
165
197
  // (Script#create_code_cache / Module#create_code_cache). Called after run/
166
198
  // evaluate, it captures the inner functions V8 lazily compiled while running
@@ -234,102 +266,104 @@ pub(crate) fn run_source(
234
266
  scope: &mut v8::PinScope<'_, '_>,
235
267
  source: &str,
236
268
  filename: &str,
237
- void: bool,
269
+ completion: Completion,
238
270
  ) -> Result<JsVal, VmError> {
239
271
  v8::tc_scope!(let tc, scope);
240
272
  // Compile and run as distinct phases so a compile failure maps to
241
273
  // ParseError and a thrown exception to RuntimeError (csim rescues both).
242
274
  let Some(code) = v8::String::new(tc, source) else {
243
- return Err(VmError::Parse("source too large".into()));
275
+ return Err(VmError::Parse("source too large".into(), None));
244
276
  };
245
277
  let origin = script_origin(tc, filename);
246
278
  let script = match v8::Script::compile(tc, code, Some(&origin)) {
247
279
  Some(script) => script,
248
280
  None if tc.has_terminated() => return Err(VmError::Terminated),
249
281
  None => {
250
- let msg = tc
251
- .exception()
252
- .map(|e| e.to_rust_string_lossy(tc))
253
- .unwrap_or_else(|| "parse error".to_string());
254
- // Append the location V8 recorded; always name the file, add the
255
- // line when V8 reports one.
256
- let message = tc.message();
257
- let res = message
258
- .and_then(|m| m.get_script_resource_name(tc))
259
- .filter(|v| v.is_string())
260
- .map(|v| v.to_rust_string_lossy(tc))
261
- .unwrap_or_else(|| filename.to_string());
262
- let loc = match message.and_then(|m| m.get_line_number(tc)) {
263
- Some(line) => format!(" at {res}:{line}"),
264
- None => format!(" at {res}"),
265
- };
266
- return Err(VmError::Parse(format!("{msg}{loc}")));
282
+ let (exc, message) = (tc.exception(), tc.message());
283
+ return Err(parse_error(tc, exc, message, filename, "parse error"));
267
284
  }
268
285
  };
269
286
  match script.run(tc) {
270
- Some(_) if void => Ok(JsVal::Undefined),
271
- Some(value) => marshalled!(tc, value),
272
- None if tc.has_terminated() => Err(VmError::Terminated),
273
- None => {
274
- let exc = tc.exception();
275
- let stack = tc.stack_trace();
276
- Err(capture_js_error(tc, exc, stack))
277
- }
287
+ Some(value) => match completion {
288
+ Completion::Value => marshalled!(tc, value),
289
+ Completion::Primitive if value.is_object() || value.is_symbol() => Ok(JsVal::Undefined),
290
+ Completion::Primitive => marshalled!(tc, value),
291
+ Completion::Void => Ok(JsVal::Undefined),
292
+ },
293
+ None => Err(caught!(tc)),
278
294
  }
279
295
  }
280
296
 
281
- // Resolve a dotted property path on globalThis to a function and invoke it via
282
- // v8::Function::call, with the property's holder as `this` (so `a.b.f` gets the
283
- // right receiver). Args/result marshal through the ref-preserving paths.
284
- fn call_function(
285
- scope: &mut v8::PinScope<'_, '_>,
286
- name: &str,
287
- args: Vec<JsVal>,
288
- void: bool,
289
- ) -> Result<JsVal, VmError> {
290
- v8::tc_scope!(let tc, scope);
291
- let context = tc.get_current_context();
292
- let global = context.global(tc);
293
- let mut recv: v8::Local<v8::Value> = global.into();
297
+ // Resolve a dotted property path on the current realm's globalThis to a
298
+ // function and the holder it was read off — its `this`, so `a.b.f` gets the right
299
+ // receiver. Each segment is an ordinary [[Get]], so a getter or Proxy trap on the
300
+ // path runs; when one throws, the error is Err(None): the caller's TryCatch holds
301
+ // it (`caught!`).
302
+ fn resolve_function<'s>(
303
+ scope: &mut v8::PinScope<'s, '_>,
304
+ path: &str,
305
+ ) -> Result<(v8::Local<'s, v8::Function>, v8::Local<'s, v8::Value>), Option<VmError>> {
306
+ let global = scope.get_current_context().global(scope);
307
+ let mut holder: v8::Local<v8::Value> = global.into();
294
308
  let mut target: v8::Local<v8::Value> = global.into();
295
- for part in name.split('.') {
296
- let Some(obj) = target.to_object(tc) else {
309
+ for part in path.split('.') {
310
+ let Some(obj) = target.to_object(scope) else {
297
311
  // The holder of `part` (a preceding segment) was null/undefined, so
298
312
  // there's nothing to read `part` from — name the holder, not `part`.
299
- return Err(VmError::Runtime(format!(
300
- "`{name}`: cannot read `{part}` (a preceding path segment is not an object)"
301
- )));
313
+ return Err(Some(VmError::Runtime(format!(
314
+ "`{path}`: cannot read `{part}` (a preceding path segment is not an object)"
315
+ ))));
302
316
  };
303
- let Some(key) = v8::String::new(tc, part) else {
304
- return Err(VmError::Runtime("property name too large".into()));
317
+ let Some(key) = v8::String::new(scope, part) else {
318
+ return Err(Some(VmError::Runtime("property name too large".into())));
305
319
  };
306
- let Some(next) = obj.get(tc, key.into()) else {
307
- if tc.has_terminated() {
308
- return Err(VmError::Terminated);
309
- }
310
- let msg = tc
311
- .exception()
312
- .map(|e| e.to_rust_string_lossy(tc))
313
- .unwrap_or_else(|| format!("cannot read `{part}` of `{name}`"));
314
- return Err(VmError::Runtime(msg));
320
+ let Some(next) = obj.get(scope, key.into()) else {
321
+ return Err(None);
315
322
  };
316
- recv = target;
323
+ holder = target;
317
324
  target = next;
318
325
  }
319
- let Ok(func) = v8::Local::<v8::Function>::try_from(target) else {
320
- return Err(VmError::Runtime(format!("`{name}` is not a function")));
326
+ match v8::Local::<v8::Function>::try_from(target) {
327
+ Ok(function) => Ok((function, holder)),
328
+ Err(_) => Err(Some(VmError::Runtime(format!(
329
+ "`{path}` is not a function"
330
+ )))),
331
+ }
332
+ }
333
+
334
+ // Context#call: resolve |name| (above), then call what it names.
335
+ fn call_function(
336
+ scope: &mut v8::PinScope<'_, '_>,
337
+ name: &str,
338
+ args: Vec<JsVal>,
339
+ void: bool,
340
+ ) -> Result<JsVal, VmError> {
341
+ let (function, receiver) = {
342
+ v8::tc_scope!(let tc, scope);
343
+ match resolve_function(tc, name) {
344
+ Ok(resolved) => resolved,
345
+ Err(failure) => return Err(failure.unwrap_or_else(|| caught!(tc))),
346
+ }
321
347
  };
348
+ call_resolved(scope, function, receiver, args, void)
349
+ }
350
+
351
+ // Invoke |function| via v8::Function::call with |receiver| as `this`.
352
+ // Args/result marshal through the ref-preserving paths.
353
+ fn call_resolved(
354
+ scope: &mut v8::PinScope<'_, '_>,
355
+ function: v8::Local<v8::Function>,
356
+ receiver: v8::Local<v8::Value>,
357
+ args: Vec<JsVal>,
358
+ void: bool,
359
+ ) -> Result<JsVal, VmError> {
360
+ v8::tc_scope!(let tc, scope);
322
361
  let argv: Vec<v8::Local<v8::Value>> = args.into_iter().map(|a| jsval_to_js(tc, a)).collect();
323
- match func.call(tc, recv, &argv) {
362
+ match function.call(tc, receiver, &argv) {
324
363
  // void: skip marshalling the return so a huge/cyclic result is never walked.
325
364
  Some(_) if void => Ok(JsVal::Undefined),
326
365
  Some(value) => marshalled!(tc, value),
327
- None if tc.has_terminated() => Err(VmError::Terminated),
328
- None => {
329
- let exc = tc.exception();
330
- let stack = tc.stack_trace();
331
- Err(capture_js_error(tc, exc, stack))
332
- }
366
+ None => Err(caught!(tc)),
333
367
  }
334
368
  }
335
369
 
@@ -413,6 +447,7 @@ pub(crate) fn service_request(
413
447
  // (the graveyard repoint inside) requires that.
414
448
  if outermost {
415
449
  flush_retiring(scope);
450
+ forget_released_handles(scope);
416
451
  }
417
452
  reply
418
453
  }
@@ -423,6 +458,8 @@ fn request_realm(state: &IsolateState, request: &Request) -> Option<i32> {
423
458
  match request {
424
459
  Request::Eval { context_id, .. }
425
460
  | Request::Call { context_id, .. }
461
+ | Request::ResolveFunction { context_id, .. }
462
+ | Request::CallFunction { context_id, .. }
426
463
  | Request::Attach { context_id, .. }
427
464
  | Request::AttachMany { context_id, .. }
428
465
  | Request::CompileModule { context_id, .. }
@@ -441,8 +478,6 @@ fn request_realm(state: &IsolateState, request: &Request) -> Option<i32> {
441
478
  | Request::DisposeContext { .. }
442
479
  | Request::ModuleStatus { .. }
443
480
  | Request::ModuleGraphAsync { .. }
444
- | Request::DisposeModule { .. }
445
- | Request::DisposeScript { .. }
446
481
  | Request::ScriptCodeCache { .. }
447
482
  | Request::ModuleCodeCache { .. }
448
483
  | Request::HeapStatistics
@@ -465,9 +500,9 @@ fn dispatch_one(
465
500
  source,
466
501
  filename,
467
502
  timeout_ms,
468
- void,
503
+ completion,
469
504
  } => op_eval(
470
- scope, context_id, source, filename, timeout_ms, void, outermost,
505
+ scope, context_id, source, filename, timeout_ms, completion, outermost,
471
506
  ),
472
507
  Request::Call {
473
508
  context_id,
@@ -476,13 +511,33 @@ fn dispatch_one(
476
511
  void,
477
512
  timeout_ms,
478
513
  } => op_call(scope, context_id, name, args, void, timeout_ms, outermost),
514
+ Request::ResolveFunction {
515
+ context_id,
516
+ path,
517
+ timeout_ms,
518
+ } => op_resolve_function(scope, context_id, path, timeout_ms, outermost),
519
+ Request::CallFunction {
520
+ context_id,
521
+ function_id,
522
+ args,
523
+ void,
524
+ timeout_ms,
525
+ } => op_call_function(
526
+ scope,
527
+ context_id,
528
+ function_id,
529
+ args,
530
+ void,
531
+ timeout_ms,
532
+ outermost,
533
+ ),
479
534
  Request::DrainMicrotasks { timeout_ms } => op_drain_microtasks(scope, timeout_ms),
480
535
  Request::Attach {
481
536
  context_id,
482
537
  name,
483
- host_fn_id,
538
+ host_fn,
484
539
  timeout_ms,
485
- } => op_attach(scope, context_id, name, host_fn_id, timeout_ms, outermost),
540
+ } => op_attach(scope, context_id, name, host_fn, timeout_ms, outermost),
486
541
  Request::AttachMany {
487
542
  context_id,
488
543
  entries,
@@ -511,11 +566,11 @@ fn dispatch_one(
511
566
  Request::EvaluateModule {
512
567
  module_id,
513
568
  timeout_ms,
514
- } => op_evaluate_module(scope, module_id, timeout_ms, outermost),
569
+ report,
570
+ } => op_evaluate_module(scope, module_id, timeout_ms, report, outermost),
515
571
  Request::ModuleNamespace { module_id } => op_module_namespace(scope, module_id),
516
572
  Request::ModuleStatus { module_id } => op_module_status(scope, module_id),
517
573
  Request::ModuleGraphAsync { module_id } => op_module_graph_async(scope, module_id),
518
- Request::DisposeModule { module_id } => op_dispose_module(scope, module_id),
519
574
  Request::CompileScript {
520
575
  context_id,
521
576
  source,
@@ -537,7 +592,6 @@ fn dispatch_one(
537
592
  timeout_ms,
538
593
  void,
539
594
  } => op_run_script(scope, script_id, timeout_ms, void, outermost),
540
- Request::DisposeScript { script_id } => op_dispose_script(scope, script_id),
541
595
  // Serialize the script's CURRENT compile state. The stored handle is
542
596
  // the UnboundScript, which V8 fills in with inner-function bytecode as
543
597
  // run() lazily compiles them — so calling this after run() captures
@@ -619,7 +673,7 @@ fn op_eval(
619
673
  source: String,
620
674
  filename: String,
621
675
  timeout_ms: u64,
622
- void: bool,
676
+ completion: Completion,
623
677
  outermost: bool,
624
678
  ) -> VmReply {
625
679
  let outcome = run_js_bracketed(scope, outermost, timeout_ms, "eval", |scope, outermost| {
@@ -628,7 +682,7 @@ fn op_eval(
628
682
  Some(ctx) => {
629
683
  let context = v8::Local::new(scope, &ctx);
630
684
  let scope = &mut v8::ContextScope::new(scope, context);
631
- let out = run_source(scope, &source, &filename, void);
685
+ let out = run_source(scope, &source, &filename, completion);
632
686
  auto_drain(scope, outermost);
633
687
  (true, out)
634
688
  }
@@ -672,6 +726,132 @@ fn op_call(
672
726
  VmReply::Done(outcome)
673
727
  }
674
728
 
729
+ fn op_resolve_function(
730
+ scope: &mut v8::PinScope<'_, '_, ()>,
731
+ context_id: i32,
732
+ path: String,
733
+ timeout_ms: u64,
734
+ outermost: bool,
735
+ ) -> VmReply {
736
+ // The id this resolve registered, for a terminate after it (in the drain, or
737
+ // the watchdog's verdict over the bracket) to take back: a failed resolve hands
738
+ // Ruby no wrapper, so nothing else would ever release it.
739
+ let mut registered = None;
740
+ let outcome = run_js_bracketed(
741
+ scope,
742
+ outermost,
743
+ timeout_ms,
744
+ "function",
745
+ |scope, outermost| {
746
+ let Some(ctx) = context_for(istate!(scope), context_id) else {
747
+ return (false, Err(VmError::Disposed(REALM_GONE.into())));
748
+ };
749
+ let context = v8::Local::new(scope, &ctx);
750
+ let scope = &mut v8::ContextScope::new(scope, context);
751
+ let resolved = {
752
+ v8::tc_scope!(let tc, scope);
753
+ match resolve_function(tc, &path) {
754
+ Ok((function, receiver)) => {
755
+ Ok((v8::Global::new(tc, function), v8::Global::new(tc, receiver)))
756
+ }
757
+ Err(failure) => Err(failure.unwrap_or_else(|| caught!(tc))),
758
+ }
759
+ };
760
+ let out = resolved.map(|(function, receiver)| {
761
+ let functions = &mut istate!(scope).functions;
762
+ let id = functions.next_id;
763
+ functions.next_id += 1;
764
+ functions.by_id.insert(
765
+ id,
766
+ FunctionEntry {
767
+ function,
768
+ receiver,
769
+ context_id,
770
+ },
771
+ );
772
+ registered = Some(id);
773
+ JsVal::Int(id as i64)
774
+ });
775
+ auto_drain(scope, outermost);
776
+ (true, out)
777
+ },
778
+ );
779
+ if let (Err(_), Some(id)) = (&outcome, registered) {
780
+ istate!(scope).functions.by_id.remove(&id);
781
+ }
782
+ VmReply::Done(outcome)
783
+ }
784
+
785
+ // What a call or a resolve of a handle whose realm is gone raises.
786
+ const REALM_GONE: &str = "function's realm was reset or disposed";
787
+
788
+ // Remove the handles whose Ruby wrappers were disposed or collected
789
+ // (Core.released) from their registries: a wrapper could not drop its Globals
790
+ // itself. At the outermost request boundary, and only when one was released — a
791
+ // flag read, not a lock, on every other op.
792
+ fn forget_released_handles(scope: &mut v8::PinScope<'_, '_, ()>) {
793
+ let core = istate!(scope).core_ptr;
794
+ if core.is_null() {
795
+ return;
796
+ }
797
+ let core = unsafe { &*core };
798
+ if !core.handles_released.swap(false, Ordering::Relaxed) {
799
+ return;
800
+ }
801
+ let released = std::mem::take(&mut *core.released.lock().unwrap());
802
+ let state = istate!(scope);
803
+ for handle in released {
804
+ match handle {
805
+ Released::Function(id) => {
806
+ state.functions.by_id.remove(&id);
807
+ }
808
+ Released::Module(id) => {
809
+ state.modules.by_id.remove(&id);
810
+ }
811
+ Released::Script(id) => {
812
+ state.scripts.by_id.remove(&id);
813
+ }
814
+ }
815
+ }
816
+ }
817
+
818
+ #[allow(clippy::too_many_arguments)]
819
+ fn op_call_function(
820
+ scope: &mut v8::PinScope<'_, '_, ()>,
821
+ context_id: i32,
822
+ function_id: u64,
823
+ args: Vec<JsVal>,
824
+ void: bool,
825
+ timeout_ms: u64,
826
+ outermost: bool,
827
+ ) -> VmReply {
828
+ let outcome = run_js_bracketed(scope, outermost, timeout_ms, "call", |scope, outermost| {
829
+ let Some(ctx) = context_for(istate!(scope), context_id) else {
830
+ return (false, Err(VmError::Disposed(REALM_GONE.into())));
831
+ };
832
+ let context = v8::Local::new(scope, &ctx);
833
+ let scope = &mut v8::ContextScope::new(scope, context);
834
+ // Locals straight off the registry's Globals: a shared borrow of the slot
835
+ // beside the shared one Local::new takes, so nothing is cloned per call.
836
+ let target = scope
837
+ .get_slot::<IsolateState>()
838
+ .and_then(|state| state.functions.by_id.get(&function_id))
839
+ .map(|entry| {
840
+ (
841
+ v8::Local::new(scope, &entry.function),
842
+ v8::Local::new(scope, &entry.receiver),
843
+ )
844
+ });
845
+ let Some((function, receiver)) = target else {
846
+ return (false, Err(VmError::Disposed(REALM_GONE.into())));
847
+ };
848
+ let out = call_resolved(scope, function, receiver, args, void);
849
+ auto_drain(scope, outermost);
850
+ (true, out)
851
+ });
852
+ VmReply::Done(outcome)
853
+ }
854
+
675
855
  fn op_drain_microtasks(scope: &mut v8::PinScope<'_, '_, ()>, timeout_ms: u64) -> VmReply {
676
856
  // A microtask may call an attached host fn (a Promise .then ->
677
857
  // ruby), which runs inline via host_fn_callback — no routing
@@ -699,7 +879,7 @@ fn op_attach(
699
879
  scope: &mut v8::PinScope<'_, '_, ()>,
700
880
  context_id: i32,
701
881
  name: String,
702
- host_fn_id: usize,
882
+ host_fn: HostFnKey,
703
883
  timeout_ms: u64,
704
884
  outermost: bool,
705
885
  ) -> VmReply {
@@ -719,7 +899,7 @@ fn op_attach(
719
899
  Some(ctx) => {
720
900
  let context = v8::Local::new(scope, &ctx);
721
901
  let scope = &mut v8::ContextScope::new(scope, context);
722
- let external = v8::External::new(scope, host_fn_id as *mut c_void);
902
+ let external = v8::External::new(scope, host_fn.to_external());
723
903
  let out = match v8::Function::builder(host_fn_callback)
724
904
  .data(external.into())
725
905
  .build(scope)
@@ -747,7 +927,7 @@ fn op_attach(
747
927
  fn op_attach_many(
748
928
  scope: &mut v8::PinScope<'_, '_, ()>,
749
929
  context_id: i32,
750
- entries: Vec<(String, usize)>,
930
+ entries: Vec<(String, HostFnKey)>,
751
931
  timeout_ms: u64,
752
932
  outermost: bool,
753
933
  ) -> VmReply {
@@ -769,8 +949,8 @@ fn op_attach_many(
769
949
  let context = v8::Local::new(scope, &ctx);
770
950
  let scope = &mut v8::ContextScope::new(scope, context);
771
951
  let mut out = Ok(JsVal::Undefined);
772
- for (name, host_fn_id) in &entries {
773
- let external = v8::External::new(scope, *host_fn_id as *mut c_void);
952
+ for (name, host_fn) in &entries {
953
+ let external = v8::External::new(scope, host_fn.to_external());
774
954
  out = match v8::Function::builder(host_fn_callback)
775
955
  .data(external.into())
776
956
  .build(scope)
@@ -938,13 +1118,13 @@ fn op_compile_module(
938
1118
  };
939
1119
  let hash = module.get_identity_hash().get();
940
1120
  let g = v8::Global::new(tc, module);
1121
+ let weak = v8::Weak::new(tc, module);
941
1122
  let id = {
942
1123
  let m = &mut istate!(tc).modules;
943
1124
  let id = m.next_id;
944
1125
  m.next_id += 1;
945
- m.by_id
946
- .insert(id, (g.clone(), filename.clone(), context_id));
947
- m.by_hash.entry(hash).or_default().push((g, id));
1126
+ m.by_id.insert(id, (g, context_id));
1127
+ m.add_url(hash, weak, filename.clone());
948
1128
  id
949
1129
  };
950
1130
  Ok(Compiled {
@@ -957,21 +1137,14 @@ fn op_compile_module(
957
1137
  // A module compile failure is a parse error
958
1138
  // (compile-time), not a thrown exception.
959
1139
  None => {
960
- let msg = tc
961
- .exception()
962
- .map(|e| e.to_rust_string_lossy(tc))
963
- .unwrap_or_else(|| "module parse error".to_string());
964
- let message = tc.message();
965
- let res = message
966
- .and_then(|m| m.get_script_resource_name(tc))
967
- .filter(|v| v.is_string())
968
- .map(|v| v.to_rust_string_lossy(tc))
969
- .unwrap_or_else(|| filename.clone());
970
- let loc = match message.and_then(|m| m.get_line_number(tc)) {
971
- Some(line) => format!(" at {res}:{line}"),
972
- None => format!(" at {res}"),
973
- };
974
- Err(VmError::Parse(format!("{msg}{loc}")))
1140
+ let (exc, message) = (tc.exception(), tc.message());
1141
+ Err(parse_error(
1142
+ tc,
1143
+ exc,
1144
+ message,
1145
+ &filename,
1146
+ "module parse error",
1147
+ ))
975
1148
  }
976
1149
  }
977
1150
  }
@@ -1013,10 +1186,9 @@ fn op_instantiate_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -
1013
1186
  | v8::ModuleStatus::Evaluated => Ok(JsVal::Undefined),
1014
1187
  // V8 CHECK-aborts on instantiating an errored
1015
1188
  // module; surface its exception instead.
1016
- v8::ModuleStatus::Errored => Err(VmError::JsError {
1017
- message: module.get_exception().to_rust_string_lossy(scope),
1018
- backtrace: vec![],
1019
- }),
1189
+ v8::ModuleStatus::Errored => {
1190
+ Err(rejected_with(scope, module.get_exception()))
1191
+ }
1020
1192
  _ => {
1021
1193
  v8::tc_scope!(let tc, scope);
1022
1194
  match module.instantiate_module(tc, resolve_imported) {
@@ -1047,6 +1219,7 @@ fn op_evaluate_module(
1047
1219
  scope: &mut v8::PinScope<'_, '_, ()>,
1048
1220
  module_id: i32,
1049
1221
  timeout_ms: u64,
1222
+ report: Option<String>,
1050
1223
  outermost: bool,
1051
1224
  ) -> VmReply {
1052
1225
  // Top-level module code (and, under :auto, the microtasks its
@@ -1081,17 +1254,25 @@ fn op_evaluate_module(
1081
1254
  // must not override their real outcome to Terminated.
1082
1255
  let mut did_eval = false;
1083
1256
  // V8 CHECK-aborts the process if evaluate runs on a
1084
- // module that isn't exactly Instantiated, so guard
1085
- // status explicitly rather than crash.
1257
+ // module that is not instantiated yet (Uninstantiated /
1258
+ // Instantiating), so guard status explicitly rather than
1259
+ // crash.
1086
1260
  let out = match module.get_status() {
1087
- v8::ModuleStatus::Errored => Err(VmError::JsError {
1088
- message: module.get_exception().to_rust_string_lossy(scope),
1089
- backtrace: vec![],
1090
- }),
1091
- v8::ModuleStatus::Evaluated => Ok(JsVal::Undefined),
1092
- v8::ModuleStatus::Instantiated => {
1261
+ // (…one evaluated already, with no reporter: nothing more to do, or its error again)
1262
+ v8::ModuleStatus::Errored if report.is_none() => {
1263
+ Err(rejected_with(scope, module.get_exception()))
1264
+ }
1265
+ v8::ModuleStatus::Evaluated if report.is_none() => Ok(JsVal::Undefined),
1266
+ // (…and with one, Evaluate again, as HTML runs every script element's record — V8 hands back
1267
+ // the same evaluation promise, rejected with its error or still pending its top-level
1268
+ // await — so each run reports)
1269
+ v8::ModuleStatus::Instantiated
1270
+ | v8::ModuleStatus::Evaluated
1271
+ | v8::ModuleStatus::Errored => {
1093
1272
  did_eval = true;
1094
1273
  v8::tc_scope!(let tc, scope);
1274
+ let reporter =
1275
+ report.as_deref().and_then(|name| global_function(tc, name));
1095
1276
  match module.evaluate(tc) {
1096
1277
  // A synchronous top-level throw yields a
1097
1278
  // *rejected* promise (not None); a pending
@@ -1099,12 +1280,27 @@ fn op_evaluate_module(
1099
1280
  // re-checked after the drain.
1100
1281
  Some(value) => {
1101
1282
  match v8::Local::<v8::Promise>::try_from(value) {
1283
+ // (…its rejection the reporter's: a synchronous throw's at once, before
1284
+ // whatever its script's runner does next — its element's `load`, Chrome —
1285
+ // the top-level await's as it settles, a microtask)
1286
+ Ok(p) if reporter.is_some() => {
1287
+ if let Some(reporter) = reporter {
1288
+ if p.state() == v8::PromiseState::Rejected {
1289
+ p.mark_as_handled();
1290
+ let (receiver, reason) = (
1291
+ v8::undefined(tc).into(),
1292
+ p.result(tc),
1293
+ );
1294
+ reporter.call(tc, receiver, &[reason]);
1295
+ } else {
1296
+ p.catch(tc, reporter);
1297
+ }
1298
+ }
1299
+ Ok(JsVal::Undefined)
1300
+ }
1102
1301
  Ok(p) if p.state() == v8::PromiseState::Rejected => {
1103
1302
  let reason = p.result(tc);
1104
- Err(VmError::JsError {
1105
- message: reason.to_rust_string_lossy(tc),
1106
- backtrace: vec![],
1107
- })
1303
+ Err(rejected_with(tc, reason))
1108
1304
  }
1109
1305
  Ok(p) => {
1110
1306
  eval_promise = Some(v8::Global::new(tc, p));
@@ -1113,12 +1309,7 @@ fn op_evaluate_module(
1113
1309
  _ => Ok(JsVal::Undefined),
1114
1310
  }
1115
1311
  }
1116
- None if tc.has_terminated() => Err(VmError::Terminated),
1117
- None => {
1118
- let exc = tc.exception();
1119
- let stack = tc.stack_trace();
1120
- Err(capture_js_error(tc, exc, stack))
1121
- }
1312
+ None => Err(caught!(tc)),
1122
1313
  }
1123
1314
  }
1124
1315
  _ => Err(VmError::Runtime(
@@ -1132,10 +1323,7 @@ fn op_evaluate_module(
1132
1323
  let p = v8::Local::new(scope, &g);
1133
1324
  if p.state() == v8::PromiseState::Rejected {
1134
1325
  let reason = p.result(scope);
1135
- Err(VmError::JsError {
1136
- message: reason.to_rust_string_lossy(scope),
1137
- backtrace: vec![],
1138
- })
1326
+ Err(rejected_with(scope, reason))
1139
1327
  } else {
1140
1328
  out
1141
1329
  }
@@ -1151,6 +1339,16 @@ fn op_evaluate_module(
1151
1339
  VmReply::Done(outcome)
1152
1340
  }
1153
1341
 
1342
+ // The function the current context's global has under `name`, or None.
1343
+ fn global_function<'s>(
1344
+ scope: &mut v8::PinScope<'s, '_>,
1345
+ name: &str,
1346
+ ) -> Option<v8::Local<'s, v8::Function>> {
1347
+ let key = v8::String::new(scope, name)?;
1348
+ let global = scope.get_current_context().global(scope);
1349
+ v8::Local::<v8::Function>::try_from(global.get(scope, key.into())?).ok()
1350
+ }
1351
+
1154
1352
  // get_module_namespace and is_graph_async both CHECK-abort (a release check, so
1155
1353
  // it kills the process) below Instantiated, so both ask this first. Everything
1156
1354
  // from Instantiated up is admissible, Errored included: a module that fails to
@@ -1179,10 +1377,7 @@ fn op_module_namespace(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) ->
1179
1377
  // never initialized — and the useful answer is WHY it errored,
1180
1378
  // the same one instantiate and evaluate give.
1181
1379
  if module.get_status() == v8::ModuleStatus::Errored {
1182
- Err(VmError::JsError {
1183
- message: module.get_exception().to_rust_string_lossy(scope),
1184
- backtrace: vec![],
1185
- })
1380
+ Err(rejected_with(scope, module.get_exception()))
1186
1381
  } else {
1187
1382
  match require_instantiated(module, "namespace") {
1188
1383
  Err(e) => Err(e),
@@ -1244,15 +1439,6 @@ fn op_module_graph_async(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -
1244
1439
  VmReply::Done(outcome)
1245
1440
  }
1246
1441
 
1247
- fn op_dispose_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
1248
- let m = &mut istate!(scope).modules;
1249
- m.by_id.remove(&module_id);
1250
- for bucket in m.by_hash.values_mut() {
1251
- bucket.retain(|(_, id)| *id != module_id);
1252
- }
1253
- VmReply::Done(Ok(JsVal::Undefined))
1254
- }
1255
-
1256
1442
  #[allow(clippy::too_many_arguments)]
1257
1443
  fn op_compile_script(
1258
1444
  scope: &mut v8::PinScope<'_, '_, ()>,
@@ -1306,21 +1492,14 @@ fn op_compile_script(
1306
1492
  None if tc.has_terminated() => Err(VmError::Terminated),
1307
1493
  // Compile failure = a parse error (with location).
1308
1494
  None => {
1309
- let msg = tc
1310
- .exception()
1311
- .map(|e| e.to_rust_string_lossy(tc))
1312
- .unwrap_or_else(|| "script parse error".to_string());
1313
- let message = tc.message();
1314
- let res = message
1315
- .and_then(|m| m.get_script_resource_name(tc))
1316
- .filter(|v| v.is_string())
1317
- .map(|v| v.to_rust_string_lossy(tc))
1318
- .unwrap_or_else(|| filename.clone());
1319
- let loc = match message.and_then(|m| m.get_line_number(tc)) {
1320
- Some(line) => format!(" at {res}:{line}"),
1321
- None => format!(" at {res}"),
1322
- };
1323
- Err(VmError::Parse(format!("{msg}{loc}")))
1495
+ let (exc, message) = (tc.exception(), tc.message());
1496
+ Err(parse_error(
1497
+ tc,
1498
+ exc,
1499
+ message,
1500
+ &filename,
1501
+ "script parse error",
1502
+ ))
1324
1503
  }
1325
1504
  }
1326
1505
  }
@@ -1361,12 +1540,7 @@ fn op_run_script(
1361
1540
  match script.run(tc) {
1362
1541
  Some(_) if void => Ok(JsVal::Undefined),
1363
1542
  Some(value) => marshalled!(tc, value),
1364
- None if tc.has_terminated() => Err(VmError::Terminated),
1365
- None => {
1366
- let exc = tc.exception();
1367
- let stack = tc.stack_trace();
1368
- Err(capture_js_error(tc, exc, stack))
1369
- }
1543
+ None => Err(caught!(tc)),
1370
1544
  }
1371
1545
  };
1372
1546
  auto_drain(scope, outermost);
@@ -1379,11 +1553,6 @@ fn op_run_script(
1379
1553
  VmReply::Done(outcome)
1380
1554
  }
1381
1555
 
1382
- fn op_dispose_script(scope: &mut v8::PinScope<'_, '_, ()>, script_id: i32) -> VmReply {
1383
- istate!(scope).scripts.by_id.remove(&script_id);
1384
- VmReply::Done(Ok(JsVal::Undefined))
1385
- }
1386
-
1387
1556
  fn op_script_code_cache(scope: &mut v8::PinScope<'_, '_, ()>, script_id: i32) -> VmReply {
1388
1557
  let handle = script_handle(istate!(scope), script_id);
1389
1558
  let outcome = match handle {