rusty_racer 0.2.3 → 0.2.5

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.
@@ -41,6 +41,17 @@ macro_rules! marshalled {
41
41
  }};
42
42
  }
43
43
 
44
+ // What an eval hands back of its completion value: all of it, marshalled; nothing (`Void`: a statement list run for its
45
+ // EFFECT still has one — the last statement's value — and marshalling it can throw, an accessor or a Proxy trap, or walk
46
+ // a huge graph, for a value the caller discards); or a primitive alone (`Primitive`: an object's never walked, undefined
47
+ // in its place — for a caller that wants a string or a number of it, as a javascript: URL's evaluation does).
48
+ #[derive(Clone, Copy)]
49
+ pub(crate) enum Completion {
50
+ Value,
51
+ Void,
52
+ Primitive,
53
+ }
54
+
44
55
  // One VM operation, built by a magnus method and run inline by Core::run ->
45
56
  // service_request -> dispatch_one. |context_id| selects which realm the op runs
46
57
  // in: 0 = the main realm (Context's own globalThis, swappable by reset_realm),
@@ -51,11 +62,7 @@ pub(crate) enum Request {
51
62
  source: String,
52
63
  filename: String,
53
64
  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,
65
+ completion: Completion,
59
66
  },
60
67
  // Resolve a dotted function path on globalThis and invoke it with marshalled
61
68
  // args (v8::Function::call), preserving the holder as `this`. Distinct from
@@ -76,14 +83,14 @@ pub(crate) enum Request {
76
83
  Attach {
77
84
  context_id: i32,
78
85
  name: String,
79
- host_fn_id: usize,
86
+ host_fn: HostFnKey,
80
87
  timeout_ms: u64,
81
88
  },
82
- // Batch attach: install many (name, host_fn_id) host fns in one round-trip
89
+ // Batch attach: install many (name, key) host fns in one round-trip
83
90
  // (a fresh realm needs ~dozens). Same semantics as Attach, applied in order.
84
91
  AttachMany {
85
92
  context_id: i32,
86
- entries: Vec<(String, usize)>,
93
+ entries: Vec<(String, HostFnKey)>,
87
94
  timeout_ms: u64,
88
95
  },
89
96
  // reset: swap globalThis for a fresh v8::Context, reusing the same warm
@@ -123,6 +130,10 @@ pub(crate) enum Request {
123
130
  EvaluateModule {
124
131
  module_id: i32,
125
132
  timeout_ms: u64,
133
+ // The name of a function on the module's context's global that reports its evaluation's rejection — HTML "run
134
+ // a module script": upon rejection, report the exception — now or as a top-level-await module settles; none:
135
+ // the rejection is the op's error, as far as the drain gets.
136
+ report: Option<String>,
126
137
  },
127
138
  ModuleNamespace {
128
139
  module_id: i32,
@@ -184,6 +195,13 @@ pub(crate) enum Request {
184
195
  // near-heap-limit callback, and tells reclaimable garbage apart from a real
185
196
  // leak (memory that survives this is genuinely retained).
186
197
  LowMemoryNotification,
198
+ // Run pending foreground platform tasks (Platform::PumpMessageLoop) without blocking —
199
+ // the deferred work V8 posts to its task runner rather than running inline, most
200
+ // importantly FinalizationRegistry cleanup callbacks (arbitrary JS, so time-capped by
201
+ // timeout_ms). Realm-independent.
202
+ PumpMessageLoop {
203
+ timeout_ms: u64,
204
+ },
187
205
  }
188
206
 
189
207
  // compile_module result: the module's id plus any produced bytecode cache and
@@ -227,41 +245,30 @@ pub(crate) fn run_source(
227
245
  scope: &mut v8::PinScope<'_, '_>,
228
246
  source: &str,
229
247
  filename: &str,
230
- void: bool,
248
+ completion: Completion,
231
249
  ) -> Result<JsVal, VmError> {
232
250
  v8::tc_scope!(let tc, scope);
233
251
  // Compile and run as distinct phases so a compile failure maps to
234
252
  // ParseError and a thrown exception to RuntimeError (csim rescues both).
235
253
  let Some(code) = v8::String::new(tc, source) else {
236
- return Err(VmError::Parse("source too large".into()));
254
+ return Err(VmError::Parse("source too large".into(), None));
237
255
  };
238
256
  let origin = script_origin(tc, filename);
239
257
  let script = match v8::Script::compile(tc, code, Some(&origin)) {
240
258
  Some(script) => script,
241
259
  None if tc.has_terminated() => return Err(VmError::Terminated),
242
260
  None => {
243
- let msg = tc
244
- .exception()
245
- .map(|e| e.to_rust_string_lossy(tc))
246
- .unwrap_or_else(|| "parse error".to_string());
247
- // Append the location V8 recorded; always name the file, add the
248
- // line when V8 reports one.
249
- let message = tc.message();
250
- let res = message
251
- .and_then(|m| m.get_script_resource_name(tc))
252
- .filter(|v| v.is_string())
253
- .map(|v| v.to_rust_string_lossy(tc))
254
- .unwrap_or_else(|| filename.to_string());
255
- let loc = match message.and_then(|m| m.get_line_number(tc)) {
256
- Some(line) => format!(" at {res}:{line}"),
257
- None => format!(" at {res}"),
258
- };
259
- return Err(VmError::Parse(format!("{msg}{loc}")));
261
+ let (exc, message) = (tc.exception(), tc.message());
262
+ return Err(parse_error(tc, exc, message, filename, "parse error"));
260
263
  }
261
264
  };
262
265
  match script.run(tc) {
263
- Some(_) if void => Ok(JsVal::Undefined),
264
- Some(value) => marshalled!(tc, value),
266
+ Some(value) => match completion {
267
+ Completion::Value => marshalled!(tc, value),
268
+ Completion::Primitive if value.is_object() || value.is_symbol() => Ok(JsVal::Undefined),
269
+ Completion::Primitive => marshalled!(tc, value),
270
+ Completion::Void => Ok(JsVal::Undefined),
271
+ },
265
272
  None if tc.has_terminated() => Err(VmError::Terminated),
266
273
  None => {
267
274
  let exc = tc.exception();
@@ -439,7 +446,8 @@ fn request_realm(state: &IsolateState, request: &Request) -> Option<i32> {
439
446
  | Request::ScriptCodeCache { .. }
440
447
  | Request::ModuleCodeCache { .. }
441
448
  | Request::HeapStatistics
442
- | Request::LowMemoryNotification => None,
449
+ | Request::LowMemoryNotification
450
+ | Request::PumpMessageLoop { .. } => None,
443
451
  }
444
452
  }
445
453
 
@@ -457,9 +465,9 @@ fn dispatch_one(
457
465
  source,
458
466
  filename,
459
467
  timeout_ms,
460
- void,
468
+ completion,
461
469
  } => op_eval(
462
- scope, context_id, source, filename, timeout_ms, void, outermost,
470
+ scope, context_id, source, filename, timeout_ms, completion, outermost,
463
471
  ),
464
472
  Request::Call {
465
473
  context_id,
@@ -472,9 +480,9 @@ fn dispatch_one(
472
480
  Request::Attach {
473
481
  context_id,
474
482
  name,
475
- host_fn_id,
483
+ host_fn,
476
484
  timeout_ms,
477
- } => op_attach(scope, context_id, name, host_fn_id, timeout_ms, outermost),
485
+ } => op_attach(scope, context_id, name, host_fn, timeout_ms, outermost),
478
486
  Request::AttachMany {
479
487
  context_id,
480
488
  entries,
@@ -503,7 +511,8 @@ fn dispatch_one(
503
511
  Request::EvaluateModule {
504
512
  module_id,
505
513
  timeout_ms,
506
- } => op_evaluate_module(scope, module_id, timeout_ms, outermost),
514
+ report,
515
+ } => op_evaluate_module(scope, module_id, timeout_ms, report, outermost),
507
516
  Request::ModuleNamespace { module_id } => op_module_namespace(scope, module_id),
508
517
  Request::ModuleStatus { module_id } => op_module_status(scope, module_id),
509
518
  Request::ModuleGraphAsync { module_id } => op_module_graph_async(scope, module_id),
@@ -544,6 +553,7 @@ fn dispatch_one(
544
553
  Request::ModuleCodeCache { module_id } => op_module_code_cache(scope, module_id),
545
554
  Request::HeapStatistics => op_heap_statistics(scope),
546
555
  Request::LowMemoryNotification => op_low_memory_notification(scope),
556
+ Request::PumpMessageLoop { timeout_ms } => op_pump_message_loop(scope, timeout_ms),
547
557
  }
548
558
  }
549
559
 
@@ -571,6 +581,38 @@ fn op_low_memory_notification(scope: &mut v8::PinScope<'_, '_, ()>) -> VmReply {
571
581
  VmReply::Done(Ok(JsVal::Undefined))
572
582
  }
573
583
 
584
+ // Drain the platform's foreground task queue: run every pending task, then stop (never blocks
585
+ // waiting for work). This is what actually fires FinalizationRegistry cleanup callbacks — V8
586
+ // posts them here after a GC finds a registered target dead, and PerformMicrotaskCheckpoint does
587
+ // NOT run them. Runs on the isolate's owner thread, with the isolate entered (the cleanup task
588
+ // enters the target's realm itself). A no-op when the platform is uninitialized or the queue empty.
589
+ //
590
+ // A cleanup callback is ARBITRARY user JS, so it gets the same bracket every other JS-running op has:
591
+ // a watchdog time-caps a runaway (`while(true){}`) callback and makes it terminable, and the
592
+ // `pump_message_loop(…, false)` loop stays bounded because a fired terminate blocks further JS. After
593
+ // the tasks run, a microtask checkpoint drains any microtasks they queued (a promise reaction), so
594
+ // cleanup-triggered continuations settle in this op — matching the binding's auto-microtask model.
595
+ fn op_pump_message_loop(scope: &mut v8::PinScope<'_, '_, ()>, timeout_ms: u64) -> VmReply {
596
+ let watchdog = arm_watchdog(scope, timeout_ms);
597
+ if let Some(platform) = crate::platform() {
598
+ while v8::Platform::pump_message_loop(platform, scope, false) {}
599
+ }
600
+ if let Some(ctx) = context_for(istate!(scope), 0) {
601
+ let context = v8::Local::new(scope, &ctx);
602
+ let scope = &mut v8::ContextScope::new(scope, context);
603
+ checkpoint_draining(scope);
604
+ }
605
+ let fired = disarm_watchdog(scope, watchdog);
606
+ if fired {
607
+ istate!(scope).watchdog_fired = true;
608
+ }
609
+ VmReply::Done(if fired {
610
+ Err(VmError::Terminated)
611
+ } else {
612
+ Ok(JsVal::Undefined)
613
+ })
614
+ }
615
+
574
616
  #[allow(clippy::too_many_arguments)]
575
617
  fn op_eval(
576
618
  scope: &mut v8::PinScope<'_, '_, ()>,
@@ -578,7 +620,7 @@ fn op_eval(
578
620
  source: String,
579
621
  filename: String,
580
622
  timeout_ms: u64,
581
- void: bool,
623
+ completion: Completion,
582
624
  outermost: bool,
583
625
  ) -> VmReply {
584
626
  let outcome = run_js_bracketed(scope, outermost, timeout_ms, "eval", |scope, outermost| {
@@ -587,7 +629,7 @@ fn op_eval(
587
629
  Some(ctx) => {
588
630
  let context = v8::Local::new(scope, &ctx);
589
631
  let scope = &mut v8::ContextScope::new(scope, context);
590
- let out = run_source(scope, &source, &filename, void);
632
+ let out = run_source(scope, &source, &filename, completion);
591
633
  auto_drain(scope, outermost);
592
634
  (true, out)
593
635
  }
@@ -658,7 +700,7 @@ fn op_attach(
658
700
  scope: &mut v8::PinScope<'_, '_, ()>,
659
701
  context_id: i32,
660
702
  name: String,
661
- host_fn_id: usize,
703
+ host_fn: HostFnKey,
662
704
  timeout_ms: u64,
663
705
  outermost: bool,
664
706
  ) -> VmReply {
@@ -678,7 +720,7 @@ fn op_attach(
678
720
  Some(ctx) => {
679
721
  let context = v8::Local::new(scope, &ctx);
680
722
  let scope = &mut v8::ContextScope::new(scope, context);
681
- let external = v8::External::new(scope, host_fn_id as *mut c_void);
723
+ let external = v8::External::new(scope, host_fn.to_external());
682
724
  let out = match v8::Function::builder(host_fn_callback)
683
725
  .data(external.into())
684
726
  .build(scope)
@@ -706,7 +748,7 @@ fn op_attach(
706
748
  fn op_attach_many(
707
749
  scope: &mut v8::PinScope<'_, '_, ()>,
708
750
  context_id: i32,
709
- entries: Vec<(String, usize)>,
751
+ entries: Vec<(String, HostFnKey)>,
710
752
  timeout_ms: u64,
711
753
  outermost: bool,
712
754
  ) -> VmReply {
@@ -728,8 +770,8 @@ fn op_attach_many(
728
770
  let context = v8::Local::new(scope, &ctx);
729
771
  let scope = &mut v8::ContextScope::new(scope, context);
730
772
  let mut out = Ok(JsVal::Undefined);
731
- for (name, host_fn_id) in &entries {
732
- let external = v8::External::new(scope, *host_fn_id as *mut c_void);
773
+ for (name, host_fn) in &entries {
774
+ let external = v8::External::new(scope, host_fn.to_external());
733
775
  out = match v8::Function::builder(host_fn_callback)
734
776
  .data(external.into())
735
777
  .build(scope)
@@ -775,7 +817,7 @@ fn op_reset(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32) -> VmReply {
775
817
  "cannot reset a realm while a request for it is suspended on the V8 stack".into(),
776
818
  )))
777
819
  } else {
778
- let (fresh, fresh_queue) = new_realm(scope);
820
+ let (fresh, fresh_queue) = new_realm(scope, context_id);
779
821
  {
780
822
  let realms = &mut istate!(scope).realms;
781
823
  // Swap in the fresh realm and PARK the old context + queue in
@@ -812,7 +854,7 @@ fn op_create_context(scope: &mut v8::PinScope<'_, '_, ()>) -> VmReply {
812
854
  realms.next_context_id += 1;
813
855
  id
814
856
  };
815
- let (fresh, fresh_queue) = new_realm(scope);
857
+ let (fresh, fresh_queue) = new_realm(scope, id);
816
858
  istate!(scope).realms.contexts.insert(id, fresh);
817
859
  istate!(scope).realms.queues.insert(id, fresh_queue);
818
860
  VmReply::Done(Ok(JsVal::Int(id as i64)))
@@ -916,21 +958,14 @@ fn op_compile_module(
916
958
  // A module compile failure is a parse error
917
959
  // (compile-time), not a thrown exception.
918
960
  None => {
919
- let msg = tc
920
- .exception()
921
- .map(|e| e.to_rust_string_lossy(tc))
922
- .unwrap_or_else(|| "module parse error".to_string());
923
- let message = tc.message();
924
- let res = message
925
- .and_then(|m| m.get_script_resource_name(tc))
926
- .filter(|v| v.is_string())
927
- .map(|v| v.to_rust_string_lossy(tc))
928
- .unwrap_or_else(|| filename.clone());
929
- let loc = match message.and_then(|m| m.get_line_number(tc)) {
930
- Some(line) => format!(" at {res}:{line}"),
931
- None => format!(" at {res}"),
932
- };
933
- Err(VmError::Parse(format!("{msg}{loc}")))
961
+ let (exc, message) = (tc.exception(), tc.message());
962
+ Err(parse_error(
963
+ tc,
964
+ exc,
965
+ message,
966
+ &filename,
967
+ "module parse error",
968
+ ))
934
969
  }
935
970
  }
936
971
  }
@@ -972,10 +1007,9 @@ fn op_instantiate_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -
972
1007
  | v8::ModuleStatus::Evaluated => Ok(JsVal::Undefined),
973
1008
  // V8 CHECK-aborts on instantiating an errored
974
1009
  // module; surface its exception instead.
975
- v8::ModuleStatus::Errored => Err(VmError::JsError {
976
- message: module.get_exception().to_rust_string_lossy(scope),
977
- backtrace: vec![],
978
- }),
1010
+ v8::ModuleStatus::Errored => {
1011
+ Err(rejected_with(scope, module.get_exception()))
1012
+ }
979
1013
  _ => {
980
1014
  v8::tc_scope!(let tc, scope);
981
1015
  match module.instantiate_module(tc, resolve_imported) {
@@ -1006,6 +1040,7 @@ fn op_evaluate_module(
1006
1040
  scope: &mut v8::PinScope<'_, '_, ()>,
1007
1041
  module_id: i32,
1008
1042
  timeout_ms: u64,
1043
+ report: Option<String>,
1009
1044
  outermost: bool,
1010
1045
  ) -> VmReply {
1011
1046
  // Top-level module code (and, under :auto, the microtasks its
@@ -1040,17 +1075,25 @@ fn op_evaluate_module(
1040
1075
  // must not override their real outcome to Terminated.
1041
1076
  let mut did_eval = false;
1042
1077
  // V8 CHECK-aborts the process if evaluate runs on a
1043
- // module that isn't exactly Instantiated, so guard
1044
- // status explicitly rather than crash.
1078
+ // module that is not instantiated yet (Uninstantiated /
1079
+ // Instantiating), so guard status explicitly rather than
1080
+ // crash.
1045
1081
  let out = match module.get_status() {
1046
- v8::ModuleStatus::Errored => Err(VmError::JsError {
1047
- message: module.get_exception().to_rust_string_lossy(scope),
1048
- backtrace: vec![],
1049
- }),
1050
- v8::ModuleStatus::Evaluated => Ok(JsVal::Undefined),
1051
- v8::ModuleStatus::Instantiated => {
1082
+ // (…one evaluated already, with no reporter: nothing more to do, or its error again)
1083
+ v8::ModuleStatus::Errored if report.is_none() => {
1084
+ Err(rejected_with(scope, module.get_exception()))
1085
+ }
1086
+ v8::ModuleStatus::Evaluated if report.is_none() => Ok(JsVal::Undefined),
1087
+ // (…and with one, Evaluate again, as HTML runs every script element's record — V8 hands back
1088
+ // the same evaluation promise, rejected with its error or still pending its top-level
1089
+ // await — so each run reports)
1090
+ v8::ModuleStatus::Instantiated
1091
+ | v8::ModuleStatus::Evaluated
1092
+ | v8::ModuleStatus::Errored => {
1052
1093
  did_eval = true;
1053
1094
  v8::tc_scope!(let tc, scope);
1095
+ let reporter =
1096
+ report.as_deref().and_then(|name| global_function(tc, name));
1054
1097
  match module.evaluate(tc) {
1055
1098
  // A synchronous top-level throw yields a
1056
1099
  // *rejected* promise (not None); a pending
@@ -1058,12 +1101,27 @@ fn op_evaluate_module(
1058
1101
  // re-checked after the drain.
1059
1102
  Some(value) => {
1060
1103
  match v8::Local::<v8::Promise>::try_from(value) {
1104
+ // (…its rejection the reporter's: a synchronous throw's at once, before
1105
+ // whatever its script's runner does next — its element's `load`, Chrome —
1106
+ // the top-level await's as it settles, a microtask)
1107
+ Ok(p) if reporter.is_some() => {
1108
+ if let Some(reporter) = reporter {
1109
+ if p.state() == v8::PromiseState::Rejected {
1110
+ p.mark_as_handled();
1111
+ let (receiver, reason) = (
1112
+ v8::undefined(tc).into(),
1113
+ p.result(tc),
1114
+ );
1115
+ reporter.call(tc, receiver, &[reason]);
1116
+ } else {
1117
+ p.catch(tc, reporter);
1118
+ }
1119
+ }
1120
+ Ok(JsVal::Undefined)
1121
+ }
1061
1122
  Ok(p) if p.state() == v8::PromiseState::Rejected => {
1062
1123
  let reason = p.result(tc);
1063
- Err(VmError::JsError {
1064
- message: reason.to_rust_string_lossy(tc),
1065
- backtrace: vec![],
1066
- })
1124
+ Err(rejected_with(tc, reason))
1067
1125
  }
1068
1126
  Ok(p) => {
1069
1127
  eval_promise = Some(v8::Global::new(tc, p));
@@ -1091,10 +1149,7 @@ fn op_evaluate_module(
1091
1149
  let p = v8::Local::new(scope, &g);
1092
1150
  if p.state() == v8::PromiseState::Rejected {
1093
1151
  let reason = p.result(scope);
1094
- Err(VmError::JsError {
1095
- message: reason.to_rust_string_lossy(scope),
1096
- backtrace: vec![],
1097
- })
1152
+ Err(rejected_with(scope, reason))
1098
1153
  } else {
1099
1154
  out
1100
1155
  }
@@ -1110,6 +1165,16 @@ fn op_evaluate_module(
1110
1165
  VmReply::Done(outcome)
1111
1166
  }
1112
1167
 
1168
+ // The function the current context's global has under `name`, or None.
1169
+ fn global_function<'s>(
1170
+ scope: &mut v8::PinScope<'s, '_>,
1171
+ name: &str,
1172
+ ) -> Option<v8::Local<'s, v8::Function>> {
1173
+ let key = v8::String::new(scope, name)?;
1174
+ let global = scope.get_current_context().global(scope);
1175
+ v8::Local::<v8::Function>::try_from(global.get(scope, key.into())?).ok()
1176
+ }
1177
+
1113
1178
  // get_module_namespace and is_graph_async both CHECK-abort (a release check, so
1114
1179
  // it kills the process) below Instantiated, so both ask this first. Everything
1115
1180
  // from Instantiated up is admissible, Errored included: a module that fails to
@@ -1138,10 +1203,7 @@ fn op_module_namespace(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) ->
1138
1203
  // never initialized — and the useful answer is WHY it errored,
1139
1204
  // the same one instantiate and evaluate give.
1140
1205
  if module.get_status() == v8::ModuleStatus::Errored {
1141
- Err(VmError::JsError {
1142
- message: module.get_exception().to_rust_string_lossy(scope),
1143
- backtrace: vec![],
1144
- })
1206
+ Err(rejected_with(scope, module.get_exception()))
1145
1207
  } else {
1146
1208
  match require_instantiated(module, "namespace") {
1147
1209
  Err(e) => Err(e),
@@ -1265,21 +1327,14 @@ fn op_compile_script(
1265
1327
  None if tc.has_terminated() => Err(VmError::Terminated),
1266
1328
  // Compile failure = a parse error (with location).
1267
1329
  None => {
1268
- let msg = tc
1269
- .exception()
1270
- .map(|e| e.to_rust_string_lossy(tc))
1271
- .unwrap_or_else(|| "script parse error".to_string());
1272
- let message = tc.message();
1273
- let res = message
1274
- .and_then(|m| m.get_script_resource_name(tc))
1275
- .filter(|v| v.is_string())
1276
- .map(|v| v.to_rust_string_lossy(tc))
1277
- .unwrap_or_else(|| filename.clone());
1278
- let loc = match message.and_then(|m| m.get_line_number(tc)) {
1279
- Some(line) => format!(" at {res}:{line}"),
1280
- None => format!(" at {res}"),
1281
- };
1282
- Err(VmError::Parse(format!("{msg}{loc}")))
1330
+ let (exc, message) = (tc.exception(), tc.message());
1331
+ Err(parse_error(
1332
+ tc,
1333
+ exc,
1334
+ message,
1335
+ &filename,
1336
+ "script parse error",
1337
+ ))
1283
1338
  }
1284
1339
  }
1285
1340
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RustyRacer
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.5"
5
5
  end
data/lib/rusty_racer.rb CHANGED
@@ -14,10 +14,18 @@ require_relative "rusty_racer/version"
14
14
  # flat fallback.
15
15
  versioned = "rusty_racer/#{RUBY_VERSION[/\d+\.\d+/]}/rusty_racer"
16
16
 
17
- if File.exist?(File.join(__dir__, "#{versioned}.#{RbConfig::CONFIG['DLEXT']}"))
18
- require_relative versioned
19
- else
20
- require "rusty_racer/rusty_racer"
17
+ # Skip the native load when an embedder has already defined the classes. Linking
18
+ # rusty_racer as a library into another extension's cdylib (e.g.
19
+ # capybara-simulated, which bundles rusty_racer + its native DOM) calls
20
+ # install_classes from that extension's own init; loading this gem's separate .so
21
+ # on top would put a SECOND V8 runtime in the process (unsound). This file is then
22
+ # required only for the pure-Ruby API wrappers below.
23
+ unless defined?(RustyRacer::Isolate)
24
+ if File.exist?(File.join(__dir__, "#{versioned}.#{RbConfig::CONFIG['DLEXT']}"))
25
+ require_relative versioned
26
+ else
27
+ require "rusty_racer/rusty_racer"
28
+ end
21
29
  end
22
30
 
23
31
  module RustyRacer
@@ -99,10 +107,11 @@ module RustyRacer
99
107
  # the binding's job (V8's host contract), and static imports met while linking
100
108
  # resolve through this same block (also with the realm as the 3rd arg).
101
109
  # (Module#instantiate's own resolve block keeps its 2-arg form.)
102
- # Held in an ivar so the proc stays alive for the isolate's lifetime (the
103
- # native side only keeps a weak handle).
110
+ # The native side keeps the proc alive itself (in the isolate's roots array,
111
+ # marked by its wrappers), so it is NOT held in an ivar here: that second
112
+ # reference would outlive `dispose`, which exists to drop whatever the resolver
113
+ # captured the moment the isolate can no longer call it.
104
114
  def dynamic_import_resolver=(resolver)
105
- @dynamic_import_resolver = resolver
106
115
  _set_dynamic_import_resolver(resolver)
107
116
  end
108
117
  end
@@ -112,7 +121,7 @@ module RustyRacer
112
121
  # `timeout_ms` (0 = the isolate default) caps this eval; `filename` names the
113
122
  # script in stack traces and parse-error locations.
114
123
  def eval(source, timeout_ms: 0, filename: '<eval>')
115
- _eval(source, timeout_ms, filename, false)
124
+ _eval(source, timeout_ms, filename, 0)
116
125
  end
117
126
 
118
127
  # Run `source` for its EFFECT and return nil, without marshalling its
@@ -123,7 +132,14 @@ module RustyRacer
123
132
  # whose writes all landed. `globalThis.win = someProxy` is the shape that
124
133
  # bites.
125
134
  def eval_void(source, timeout_ms: 0, filename: '<eval>')
126
- _eval(source, timeout_ms, filename, true)
135
+ _eval(source, timeout_ms, filename, 1)
136
+ end
137
+
138
+ # Run `source` and return its completion value only where it is a primitive — a string, a number, a boolean, a
139
+ # BigInt, null — and nil for an object or a symbol, which is never walked: for a caller that wants a string of it
140
+ # (a javascript: URL's evaluation), whatever object the source happens to end with.
141
+ def eval_primitive(source, timeout_ms: 0, filename: '<eval>')
142
+ _eval(source, timeout_ms, filename, 2)
127
143
  end
128
144
 
129
145
  # Compile a classic <script>; returns a RustyRacer::Script to #run.
@@ -162,6 +178,15 @@ module RustyRacer
162
178
  self
163
179
  end
164
180
 
181
+ # Evaluate the instantiated module. Its evaluation's rejection — a top-level throw, a top-level await's — raises
182
+ # (RustyRacer::RuntimeError, as far as the drain after it settles), or, `report:` naming a function of its context's
183
+ # global, is that function's to report: called with the rejection's reason — a top-level throw's before this
184
+ # returns, a top-level await's as it settles — and nothing raised (HTML "run a module script": upon rejection,
185
+ # report the exception).
186
+ def evaluate(report: nil)
187
+ _evaluate(report&.to_s)
188
+ end
189
+
165
190
  # The V8 module status: :uninstantiated, :instantiating, :instantiated,
166
191
  # :evaluating, :evaluated or :errored.
167
192
  def status
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rusty_racer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keita Urashima
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-26 00:00:00.000000000 Z
11
+ date: 2026-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys