mini_racer_rs 1.0.0

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.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/AI-DISCLOSURE.md +13 -0
  3. data/CHANGELOG.md +45 -0
  4. data/COMPATIBILITY.md +235 -0
  5. data/Cargo.lock +1263 -0
  6. data/Cargo.toml +62 -0
  7. data/LICENSE +21 -0
  8. data/README.md +254 -0
  9. data/THIRD_PARTY_NOTICES.md +4501 -0
  10. data/crates/mini_racer_engine/Cargo.toml +25 -0
  11. data/crates/mini_racer_engine/build.rs +13 -0
  12. data/crates/mini_racer_engine/src/context.rs +802 -0
  13. data/crates/mini_racer_engine/src/control.rs +239 -0
  14. data/crates/mini_racer_engine/src/error.rs +88 -0
  15. data/crates/mini_racer_engine/src/fast_calls.rs +681 -0
  16. data/crates/mini_racer_engine/src/lib.rs +20 -0
  17. data/crates/mini_racer_engine/src/notify.rs +107 -0
  18. data/crates/mini_racer_engine/src/process/code_cache.rs +412 -0
  19. data/crates/mini_racer_engine/src/process/fork.rs +261 -0
  20. data/crates/mini_racer_engine/src/process/platform.rs +194 -0
  21. data/crates/mini_racer_engine/src/process/tasks.rs +96 -0
  22. data/crates/mini_racer_engine/src/process/watchdog.rs +185 -0
  23. data/crates/mini_racer_engine/src/process/workers.rs +165 -0
  24. data/crates/mini_racer_engine/src/process.rs +69 -0
  25. data/crates/mini_racer_engine/src/runner.rs +1560 -0
  26. data/crates/mini_racer_engine/src/serialize.rs +253 -0
  27. data/crates/mini_racer_engine/src/snapshot.rs +221 -0
  28. data/crates/mini_racer_engine/src/spin.rs +23 -0
  29. data/crates/mini_racer_engine/src/sys.rs +566 -0
  30. data/crates/mini_racer_sync/Cargo.toml +18 -0
  31. data/crates/mini_racer_sync/src/channel.rs +621 -0
  32. data/crates/mini_racer_sync/src/fork.rs +116 -0
  33. data/crates/mini_racer_sync/src/lib.rs +20 -0
  34. data/crates/mini_racer_sync/src/lock.rs +49 -0
  35. data/crates/mini_racer_sync/src/parker.rs +44 -0
  36. data/crates/mini_racer_sync/src/primitives.rs +10 -0
  37. data/crates/mini_racer_sync/src/tasks.rs +345 -0
  38. data/crates/mini_racer_sync/src/termination.rs +1234 -0
  39. data/crates/mini_racer_sync/src/workers.rs +283 -0
  40. data/crates/mini_racer_wire/Cargo.toml +18 -0
  41. data/crates/mini_racer_wire/src/decode.rs +926 -0
  42. data/crates/mini_racer_wire/src/lib.rs +67 -0
  43. data/crates/mini_racer_wire/src/marker.rs +43 -0
  44. data/crates/mini_racer_wire/src/text.rs +106 -0
  45. data/crates/mini_racer_wire/src/write.rs +187 -0
  46. data/ext/mini_racer_rs/Cargo.toml +26 -0
  47. data/ext/mini_racer_rs/build.rs +16 -0
  48. data/ext/mini_racer_rs/extconf.rb +78 -0
  49. data/ext/mini_racer_rs/src/context.rs +485 -0
  50. data/ext/mini_racer_rs/src/decode.rs +450 -0
  51. data/ext/mini_racer_rs/src/encode.rs +333 -0
  52. data/ext/mini_racer_rs/src/errors.rs +156 -0
  53. data/ext/mini_racer_rs/src/fork.rs +31 -0
  54. data/ext/mini_racer_rs/src/gc_root.rs +117 -0
  55. data/ext/mini_racer_rs/src/guard.rs +7 -0
  56. data/ext/mini_racer_rs/src/host.rs +339 -0
  57. data/ext/mini_racer_rs/src/lib.rs +44 -0
  58. data/ext/mini_racer_rs/src/options.rs +269 -0
  59. data/ext/mini_racer_rs/src/platform.rs +42 -0
  60. data/ext/mini_racer_rs/src/rb_raw.rs +368 -0
  61. data/ext/mini_racer_rs/src/snapshot.rs +149 -0
  62. data/ext/mini_racer_rs/v8-archives.sha256 +8 -0
  63. data/lib/mini_racer/shared.rb +420 -0
  64. data/lib/mini_racer/truffleruby.rb +439 -0
  65. data/lib/mini_racer/version.rb +6 -0
  66. data/lib/mini_racer.rb +3 -0
  67. data/lib/mini_racer_rs/internal_error.rb +7 -0
  68. data/lib/mini_racer_rs/version.rb +5 -0
  69. data/lib/mini_racer_rs/vocabulary.rb +105 -0
  70. data/lib/mini_racer_rs.rb +33 -0
  71. data/licenses/mini_racer.txt +21 -0
  72. data/licenses/rust/SOURCES.tsv +6 -0
  73. data/licenses/rust/std/COPYRIGHT +34 -0
  74. data/licenses/rust/std/COPYRIGHT-library.html +29357 -0
  75. data/licenses/rust/std/LICENSE-APACHE +176 -0
  76. data/licenses/rust/std/LICENSE-MIT +25 -0
  77. data/licenses/v8/SOURCES.tsv +20 -0
  78. data/licenses/v8/abseil-cpp/LICENSE +203 -0
  79. data/licenses/v8/chromium-build/LICENSE +27 -0
  80. data/licenses/v8/components.tsv +29 -0
  81. data/licenses/v8/dragonbox/LICENSE-Boost +23 -0
  82. data/licenses/v8/fast_float/LICENSE-MIT +27 -0
  83. data/licenses/v8/highway/LICENSE +371 -0
  84. data/licenses/v8/icu/LICENSE +568 -0
  85. data/licenses/v8/inspector_protocol/LICENSE +27 -0
  86. data/licenses/v8/libc++/LICENSE.TXT +311 -0
  87. data/licenses/v8/libc++abi/LICENSE.TXT +311 -0
  88. data/licenses/v8/llvm-libc/LICENSE.TXT +278 -0
  89. data/licenses/v8/rapidhash/LICENSE +34 -0
  90. data/licenses/v8/simdutf/LICENSE +18 -0
  91. data/licenses/v8/siphash/LICENSE +116 -0
  92. data/licenses/v8/utf8-decoder/LICENSE +19 -0
  93. data/licenses/v8/v8/LICENSE +61 -0
  94. data/licenses/v8/v8/LICENSE.fdlibm +6 -0
  95. data/licenses/v8/v8/LICENSE.strongtalk +29 -0
  96. data/licenses/v8/v8/LICENSE.v8 +26 -0
  97. metadata +156 -0
@@ -0,0 +1,1560 @@
1
+ use std::collections::hash_map::RandomState;
2
+ use std::hash::BuildHasher;
3
+ use std::panic::{AssertUnwindSafe, catch_unwind};
4
+ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
5
+ use std::sync::{Arc, Mutex};
6
+ use std::time::Duration;
7
+
8
+ use mini_racer_sync::{
9
+ DispatchId, DispatchKind, Halt, Interrupt, Phase, Side, TerminationCause, ToV8, lock,
10
+ };
11
+ use mini_racer_wire::Writer;
12
+ use v8::ValueDeserializerHelper;
13
+
14
+ use crate::control::{CallbackReply, ControlBlock, Depth, IsolateTerminator, JsText, Request};
15
+ use crate::fast_calls::{self, Hoisted, Shape};
16
+ use crate::process::code_cache::{Building, Plan};
17
+ use crate::process::fork::Generation;
18
+ use crate::process::platform::{self, Mode};
19
+ use crate::process::watchdog;
20
+ use crate::serialize::{self, Filter, Tc};
21
+ use crate::spin::spin_until;
22
+ use crate::{EngineError, JsError, JsErrorKind};
23
+ use crate::{snapshot, sys};
24
+
25
+ // Spinning after each response catches a busy caller's next request without a
26
+ // futex wake on either side; an idle context then parks.
27
+ const IDLE_SPIN: Duration = Duration::from_micros(100);
28
+
29
+ // Closes the channel however the runner ends, so a waiting Ruby thread never
30
+ // parks on a runner that is gone.
31
+ struct Closing<'a>(&'a ControlBlock);
32
+
33
+ impl Drop for Closing<'_> {
34
+ fn drop(&mut self) {
35
+ self.0.term.uninstall();
36
+ self.0.channel.close();
37
+ }
38
+ }
39
+
40
+ // Kept in an isolate slot: the callback trampoline gets only the isolate. A
41
+ // shared isolate's slots must be Send, as any thread may lock it.
42
+ struct Runtime {
43
+ block: Arc<ControlBlock>,
44
+ generation: &'static Generation,
45
+ // Whether the runner holds its isolate only while serving a request.
46
+ released: bool,
47
+ context: v8::Global<v8::Context>,
48
+ filter: Filter,
49
+ // The exception last thrown for a raising callback; mini_racer keeps one
50
+ // per isolate and compares what escapes a dispatch against it.
51
+ raised: Mutex<Option<v8::Global<v8::Value>>>,
52
+ // The address V8 posts this isolate's tasks under.
53
+ key: usize,
54
+ // Tasks running, one inside another when a task's script pumps.
55
+ running: AtomicU32,
56
+ // Set by a request, at any depth, that left memory worth handing back
57
+ // once the outermost response is out.
58
+ freed: AtomicBool,
59
+ // The startup snapshot this isolate began from, for the code cache's key.
60
+ snapshot: Option<u64>,
61
+ // Large scripts to cache once their dispatch is over, so making a cache
62
+ // never counts against a timeout.
63
+ uncached: Mutex<Vec<Uncached>>,
64
+ hoisting: Mutex<Hoisting>,
65
+ // The values of each fast call running, innermost last, for `hoisted`.
66
+ frames: Mutex<Vec<v8::Global<v8::Array>>>,
67
+ }
68
+
69
+ struct Uncached {
70
+ script: v8::Global<v8::UnboundScript>,
71
+ building: Building,
72
+ }
73
+
74
+ // A context's fast calls: the binding their templates read values through,
75
+ // the templates compiled so far, most recently used last, and the last source
76
+ // a fast call took, up to `LARGE_SOURCE`.
77
+ #[derive(Default)]
78
+ struct Hoisting {
79
+ binding: Binding,
80
+ templates: Vec<Template>,
81
+ last: Vec<u8>,
82
+ }
83
+
84
+ struct Template {
85
+ text: Box<[u8]>,
86
+ // `None` for one refused: its code does not leave the call alone, or it
87
+ // does not compile.
88
+ script: Option<v8::Global<v8::Script>>,
89
+ // How much of its text comes before its first value and after its last.
90
+ ends: (usize, usize),
91
+ }
92
+
93
+ impl Template {
94
+ // A source that starts and ends as this refused template does would make
95
+ // it again, so it takes the normal path without being read.
96
+ fn refuses(&self, source: &[u8]) -> bool {
97
+ let (head, tail) = self.ends;
98
+ self.script.is_none()
99
+ && source.len() >= head + tail
100
+ && self
101
+ .text
102
+ .get(..head)
103
+ .is_some_and(|head| source.starts_with(head))
104
+ && self
105
+ .text
106
+ .len()
107
+ .checked_sub(tail)
108
+ .and_then(|at| self.text.get(at..))
109
+ .is_some_and(|tail| source.ends_with(tail))
110
+ }
111
+ }
112
+
113
+ #[derive(Default)]
114
+ enum Binding {
115
+ #[default]
116
+ Unmade,
117
+ Made(String),
118
+ Failed,
119
+ }
120
+
121
+ const TEMPLATES: usize = 32;
122
+
123
+ impl Runtime {
124
+ fn new(
125
+ block: &Arc<ControlBlock>,
126
+ generation: &'static Generation,
127
+ (context, filter): (v8::Global<v8::Context>, Filter),
128
+ (key, snapshot): (usize, Option<u64>),
129
+ ) -> Self {
130
+ Self {
131
+ block: Arc::clone(block),
132
+ generation,
133
+ released: platform::mode() == Mode::SingleThreaded,
134
+ context,
135
+ filter,
136
+ raised: Mutex::default(),
137
+ key,
138
+ running: AtomicU32::new(0),
139
+ freed: AtomicBool::new(false),
140
+ snapshot,
141
+ uncached: Mutex::default(),
142
+ hoisting: Mutex::default(),
143
+ frames: Mutex::default(),
144
+ }
145
+ }
146
+ }
147
+
148
+ struct Running<'a>(&'a AtomicU32);
149
+
150
+ impl Drop for Running<'_> {
151
+ fn drop(&mut self) {
152
+ self.0.fetch_sub(1, Ordering::Relaxed);
153
+ }
154
+ }
155
+
156
+ /// How a runner gets its isolate: made fresh, from the control block's
157
+ /// snapshot if it has one, or taken over in a forked child from an idle runner
158
+ /// of the parent.
159
+ #[derive(Clone, Copy)]
160
+ pub enum Start {
161
+ Fresh,
162
+ /// The isolate waits in the new control block's custody.
163
+ Adopted,
164
+ }
165
+
166
+ pub fn run(block: &Arc<ControlBlock>, generation: &'static Generation, start: Start) {
167
+ let closing = Closing(block);
168
+ let disposed = catch_unwind(AssertUnwindSafe(|| match start {
169
+ Start::Fresh => serve(block, generation),
170
+ Start::Adopted => {
171
+ let isolate = lock(&block.custody).take();
172
+ isolate.map_or_else(
173
+ || refuse(block, "no isolate to take over"),
174
+ |isolate| adopt(block, generation, isolate),
175
+ )
176
+ }
177
+ }))
178
+ .unwrap_or_else(|_| {
179
+ if block.channel.ready(Side::V8) {
180
+ let _ = block
181
+ .channel
182
+ .respond(Err(EngineError::Internal("the runner panicked".to_owned())));
183
+ }
184
+ false
185
+ });
186
+ // Answering only after the isolate is gone makes an explicit dispose return
187
+ // once V8 has released everything.
188
+ if disposed {
189
+ let _ = block.channel.respond(Ok(Vec::new()));
190
+ }
191
+ drop(closing);
192
+ block.phase.set(Phase::Exited);
193
+ }
194
+
195
+ // mini_racer's max_memory, checked after every garbage collection: a heap
196
+ // past it terminates the running script, or the next one.
197
+ pub fn check_heap(isolate: &mut v8::Isolate) {
198
+ let Some(runtime) = isolate.get_slot::<Arc<Runtime>>().cloned() else {
199
+ return;
200
+ };
201
+ let Some(max) = runtime.block.config.max_memory else {
202
+ return;
203
+ };
204
+ let used = isolate.get_heap_statistics().used_heap_size();
205
+ if u64::try_from(used).unwrap_or(u64::MAX) > max {
206
+ runtime.block.term.request(Interrupt::MaxMemory);
207
+ }
208
+ }
209
+
210
+ pub fn park_for_fork(isolate: &v8::Isolate) {
211
+ if let Some(runtime) = isolate.get_slot::<Arc<Runtime>>() {
212
+ runtime.generation.pause(&runtime.block);
213
+ }
214
+ }
215
+
216
+ fn refuse(block: &ControlBlock, message: &str) -> bool {
217
+ // Holding no isolate, it never keeps a fork waiting.
218
+ block.phase.set(Phase::Deposited);
219
+ if receive(block).is_some() {
220
+ let _ = block
221
+ .channel
222
+ .respond(Err(EngineError::Internal(message.to_owned())));
223
+ }
224
+ false
225
+ }
226
+
227
+ // Returns whether it stopped for an explicit dispose.
228
+ fn serve(block: &Arc<ControlBlock>, generation: &'static Generation) -> bool {
229
+ // No isolate is made while a fork waits for every runner to be idle.
230
+ generation.resume(block);
231
+ let snapshot = block.config.snapshot.as_ref();
232
+ let from_snapshot = snapshot.is_some();
233
+ let snapshot_id = snapshot.map(|blob| generation.code_cache().snapshot_id(blob.as_bytes()));
234
+ let params = v8::CreateParams::default();
235
+ let params = match snapshot {
236
+ Some(blob) => params.snapshot_blob(sys::startup_data(blob)),
237
+ None => params,
238
+ };
239
+ let mut isolate = v8::Isolate::new(params);
240
+ if block.config.max_memory.is_some() {
241
+ sys::watch_heap(&mut isolate);
242
+ }
243
+ let key = sys::isolate_key(&isolate);
244
+ generation.tasks().register(key, &block.tasks);
245
+ let contexts = {
246
+ v8::scope!(let scope, &mut isolate);
247
+ serialize::contexts(scope, from_snapshot)
248
+ };
249
+ let Some(contexts) = contexts else {
250
+ retire(block, generation, key, isolate);
251
+ return refuse(block, "context setup failed");
252
+ };
253
+ let runtime = Arc::new(Runtime::new(
254
+ block,
255
+ generation,
256
+ contexts,
257
+ (key, snapshot_id),
258
+ ));
259
+ isolate.set_slot(Arc::clone(&runtime));
260
+ // Installed only once the context exists, so nothing terminates its setup.
261
+ block.term.install(IsolateTerminator::new(
262
+ isolate.thread_safe_handle(),
263
+ &block.tasks,
264
+ ));
265
+ let shared = match sys::share(isolate) {
266
+ Ok(shared) => shared,
267
+ Err(refused) => {
268
+ retire(block, generation, key, refused);
269
+ return refuse(block, "isolate could not be shared");
270
+ }
271
+ };
272
+ let _ = block.isolate.set(shared.thread_safe_handle());
273
+ serve_isolate(block, shared, runtime)
274
+ }
275
+
276
+ // The parent's runtime still names the parent's control block, so the child
277
+ // builds its own around the same contexts.
278
+ fn adopt(
279
+ block: &Arc<ControlBlock>,
280
+ generation: &'static Generation,
281
+ shared: v8::SharedIsolate,
282
+ ) -> bool {
283
+ generation.resume(block);
284
+ let inherited = shared.lock().get_slot::<Arc<Runtime>>().cloned();
285
+ let Some(inherited) = inherited else {
286
+ // Disposed here, before the runner counts as idle to a fork.
287
+ drop(block.tasks.close());
288
+ drop(shared);
289
+ return refuse(block, "inherited isolate has no context");
290
+ };
291
+ let runtime = {
292
+ let mut locker = shared.lock();
293
+ let key = sys::isolate_key(&locker);
294
+ generation.tasks().register(key, &block.tasks);
295
+ let runtime = Arc::new(Runtime::new(
296
+ block,
297
+ generation,
298
+ (inherited.context.clone(), inherited.filter.clone()),
299
+ (key, inherited.snapshot),
300
+ ));
301
+ locker.set_slot(Arc::clone(&runtime));
302
+ runtime
303
+ };
304
+ block.term.install(IsolateTerminator::new(
305
+ shared.thread_safe_handle(),
306
+ &block.tasks,
307
+ ));
308
+ let _ = block.isolate.set(shared.thread_safe_handle());
309
+ serve_isolate(block, shared, runtime)
310
+ }
311
+
312
+ fn serve_isolate(block: &ControlBlock, shared: v8::SharedIsolate, runtime: Arc<Runtime>) -> bool {
313
+ let (generation, key) = (runtime.generation, runtime.key);
314
+ let (disposed, shared) = if runtime.released {
315
+ serve_released(block, shared, &runtime)
316
+ } else {
317
+ (serve_held(block, &shared, &runtime), Some(shared))
318
+ };
319
+ block.term.uninstall();
320
+ // Its handles are released under the lock, before the isolate goes.
321
+ match shared {
322
+ Some(shared) => {
323
+ let mut isolate = shared.lock();
324
+ drop(isolate.remove_slot::<Arc<Runtime>>());
325
+ drop(runtime);
326
+ drop(isolate);
327
+ retire(block, generation, key, shared);
328
+ }
329
+ None => retire(block, generation, key, ()),
330
+ }
331
+ disposed
332
+ }
333
+
334
+ // Queued tasks are dropped while their isolate exists. Its entry stays until
335
+ // the isolate is gone, so a task V8 posts during the teardown meets the closed
336
+ // queue and is dropped too, never kept for a later isolate at that address.
337
+ fn retire<I>(block: &ControlBlock, generation: &Generation, key: usize, isolate: I) {
338
+ drop(block.tasks.close());
339
+ drop(isolate);
340
+ generation.tasks().unregister(key, &block.tasks);
341
+ }
342
+
343
+ enum Served {
344
+ Next,
345
+ Stop { disposed: bool },
346
+ }
347
+
348
+ // With `gc_after_idle`, mini_racer's runner asks V8 for a full collection
349
+ // once it has waited that long after a request, and not again until the next.
350
+ fn serve_held(block: &ControlBlock, shared: &v8::SharedIsolate, runtime: &Arc<Runtime>) -> bool {
351
+ let mut isolate = shared.lock();
352
+ let mut collected = true;
353
+ loop {
354
+ // Between requests it holds its isolate but runs nothing in it, so a
355
+ // fork need not wait for it.
356
+ block.phase.set(Phase::Parked);
357
+ let received = match block.config.gc_after_idle {
358
+ Some(idle) if !collected => receive_within(block, idle),
359
+ _ => Received::Message(receive(block)),
360
+ };
361
+ runtime.generation.resume(block);
362
+ let message = match received {
363
+ Received::Message(message) => message,
364
+ // A collection for a context about to be torn down is wasted.
365
+ Received::Idle => {
366
+ if !block.term.is_disposing() {
367
+ isolate.low_memory_notification();
368
+ sys::release_free_memory();
369
+ }
370
+ collected = true;
371
+ continue;
372
+ }
373
+ };
374
+ // mini_racer's context has sent nothing before its first request.
375
+ if !matches!(message, Some(ToV8::Request(Request::Init { .. }))) {
376
+ collected = false;
377
+ }
378
+ if let Served::Stop { disposed } = serve_one(&mut isolate, block, runtime, message) {
379
+ return disposed;
380
+ }
381
+ }
382
+ }
383
+
384
+ // Between requests a single-threaded runner leaves its isolate unlocked in the
385
+ // control block, as mini_racer's holds its isolate only for a request.
386
+ fn serve_released(
387
+ block: &ControlBlock,
388
+ shared: v8::SharedIsolate,
389
+ runtime: &Arc<Runtime>,
390
+ ) -> (bool, Option<v8::SharedIsolate>) {
391
+ let mut held = Some(shared);
392
+ loop {
393
+ *lock(&block.custody) = held.take();
394
+ block.phase.set(Phase::Deposited);
395
+ let message = receive(block);
396
+ runtime.generation.resume(block);
397
+ let Some(shared) = lock(&block.custody).take() else {
398
+ return (false, None);
399
+ };
400
+ let mut locked = shared.lock();
401
+ block.term.relocked();
402
+ let served = serve_one(&mut locked, block, runtime, message);
403
+ drop(locked);
404
+ held = Some(shared);
405
+ if let Served::Stop { disposed } = served {
406
+ return (disposed, held);
407
+ }
408
+ }
409
+ }
410
+
411
+ fn serve_one(
412
+ isolate: &mut v8::Isolate,
413
+ block: &ControlBlock,
414
+ runtime: &Arc<Runtime>,
415
+ message: Option<ToV8<crate::control::Wire>>,
416
+ ) -> Served {
417
+ let request = match message {
418
+ Some(ToV8::Request(Request::Dispose)) => return Served::Stop { disposed: true },
419
+ // The snapshot's own isolate is entered with none of this
420
+ // runner's scopes open.
421
+ // A build the caller abandoned before it began is not started; once
422
+ // begun it runs to the end, as nothing can terminate its isolate.
423
+ Some(ToV8::Request(Request::Snapshot { code, base, depth })) => {
424
+ let response = match depth {
425
+ Depth::Outermost(dispatch) => {
426
+ let built = if block.term.check(DispatchKind::Script).is_some() {
427
+ Err(EngineError::Internal("halted without a cause".to_owned()))
428
+ } else {
429
+ let built = snapshot::create(&code, base.as_ref(), |isolate| {
430
+ isolate.set_slot(Arc::clone(runtime));
431
+ *lock(&block.building) = Some(isolate.thread_safe_handle());
432
+ });
433
+ lock(&block.building).take();
434
+ built
435
+ };
436
+ block
437
+ .term
438
+ .end(dispatch)
439
+ .map_or(built, |halt| Err(halted(halt)))
440
+ }
441
+ Depth::Nested => Err(EngineError::Internal(
442
+ "snapshot inside a callback".to_owned(),
443
+ )),
444
+ };
445
+ let served = respond(block, response);
446
+ if matches!(served, Served::Next) {
447
+ sys::release_free_memory();
448
+ }
449
+ return served;
450
+ }
451
+ Some(ToV8::Request(request)) => request,
452
+ // No callback is outstanding here, so there is nothing to reply to.
453
+ Some(ToV8::Reply(_)) => return Served::Next,
454
+ None => return Served::Stop { disposed: false },
455
+ };
456
+ let response = {
457
+ v8::scope_with_context!(let scope, isolate, &runtime.context);
458
+ handle(scope, runtime, request)
459
+ };
460
+ // Before the response, so a fork that follows finds this runner idle.
461
+ cache_uncached(isolate, runtime);
462
+ let served = respond(block, response);
463
+ // After the response, so the caller does not wait for it.
464
+ if matches!(served, Served::Next) && runtime.freed.swap(false, Ordering::Relaxed) {
465
+ sys::release_free_memory();
466
+ }
467
+ served
468
+ }
469
+
470
+ fn cache_uncached(isolate: &mut v8::Isolate, runtime: &Runtime) {
471
+ let uncached = std::mem::take(&mut *lock(&runtime.uncached));
472
+ if uncached.is_empty() || runtime.block.term.is_disposing() {
473
+ return;
474
+ }
475
+ let cache = runtime.generation.code_cache();
476
+ v8::scope!(let scope, isolate);
477
+ for Uncached { script, building } in uncached {
478
+ let script = v8::Local::new(scope, &script);
479
+ if let Some(data) = script.create_code_cache() {
480
+ cache.store(building, Box::from(&**data));
481
+ }
482
+ }
483
+ }
484
+
485
+ // A bundle, not a render: what parsing it frees is worth handing back.
486
+ const LARGE_SOURCE: usize = 1 << 20;
487
+
488
+ const fn large(source: &JsText) -> bool {
489
+ match source {
490
+ JsText::Utf8(bytes) | JsText::Latin1(bytes) => bytes.len() >= LARGE_SOURCE,
491
+ JsText::Utf16(units) => units.len() >= LARGE_SOURCE / 2,
492
+ }
493
+ }
494
+
495
+ fn respond(block: &ControlBlock, response: Result<Vec<u8>, EngineError>) -> Served {
496
+ if block.channel.respond(response).is_ok() {
497
+ Served::Next
498
+ } else {
499
+ Served::Stop { disposed: false }
500
+ }
501
+ }
502
+
503
+ enum Received {
504
+ Message(Option<ToV8<crate::control::Wire>>),
505
+ Idle,
506
+ }
507
+
508
+ fn receive_within(block: &ControlBlock, idle: Duration) -> Received {
509
+ let deadline = std::time::Instant::now().checked_add(idle);
510
+ spin_until(IDLE_SPIN.min(idle), || block.channel.ready(Side::V8));
511
+ loop {
512
+ // As mini_racer's runner: once the deadline has passed it collects
513
+ // before it looks for a request, which then waits in the channel.
514
+ let left = deadline.map(|at| at.saturating_duration_since(std::time::Instant::now()));
515
+ if left.is_some_and(|left| left.is_zero()) {
516
+ return Received::Idle;
517
+ }
518
+ match block.channel.recv_v8() {
519
+ Ok(Some(message)) => return Received::Message(Some(message)),
520
+ Ok(None) => block.channel.park(Side::V8, left),
521
+ Err(_) => return Received::Message(None),
522
+ }
523
+ }
524
+ }
525
+
526
+ fn receive(block: &ControlBlock) -> Option<ToV8<crate::control::Wire>> {
527
+ spin_until(IDLE_SPIN, || block.channel.ready(Side::V8));
528
+ loop {
529
+ match block.channel.recv_v8() {
530
+ Ok(Some(message)) => return Some(message),
531
+ Ok(None) => block.channel.park(Side::V8, None),
532
+ Err(_) => return None,
533
+ }
534
+ }
535
+ }
536
+
537
+ fn handle(
538
+ scope: &mut v8::PinScope<'_, '_>,
539
+ runtime: &Runtime,
540
+ request: Request,
541
+ ) -> Result<Vec<u8>, EngineError> {
542
+ match request {
543
+ Request::Init { depth } => match depth {
544
+ Depth::Outermost(dispatch) => ended(runtime, dispatch),
545
+ Depth::Nested => Err(EngineError::Internal("setup inside a callback".to_owned())),
546
+ },
547
+ Request::Eval {
548
+ depth: Depth::Nested,
549
+ awaited: true,
550
+ ..
551
+ }
552
+ | Request::Call {
553
+ depth: Depth::Nested,
554
+ awaited: true,
555
+ ..
556
+ } => refuse_nested_await(scope, runtime),
557
+ Request::Eval {
558
+ filename,
559
+ source,
560
+ depth,
561
+ awaited,
562
+ } => script(scope, runtime, depth, DispatchKind::Script, |tc| {
563
+ evaluate(tc, runtime, (&filename, &source), (depth, awaited))
564
+ }),
565
+ Request::Call {
566
+ request,
567
+ depth,
568
+ awaited,
569
+ } => script(scope, runtime, depth, DispatchKind::Script, |tc| {
570
+ invoke(tc, runtime, &request, (depth, awaited))
571
+ }),
572
+ Request::Attach { request, depth } => {
573
+ script(scope, runtime, depth, DispatchKind::Maintenance, |tc| {
574
+ attach(tc, &request)
575
+ })
576
+ }
577
+ // A collection for a context about to be torn down is wasted.
578
+ Request::LowMemoryNotification { depth } => {
579
+ if !runtime.block.term.is_disposing() {
580
+ scope.low_memory_notification();
581
+ runtime.freed.store(true, Ordering::Relaxed);
582
+ }
583
+ match depth {
584
+ Depth::Outermost(dispatch) => ended(runtime, dispatch),
585
+ Depth::Nested => Ok(Vec::new()),
586
+ }
587
+ }
588
+ // mini_racer leaves a termination this meets for the dispatch around
589
+ // it, or for the next script.
590
+ Request::MicrotaskCheckpoint { depth } => {
591
+ script(scope, runtime, depth, DispatchKind::Maintenance, |tc| {
592
+ tc.perform_microtask_checkpoint();
593
+ Ok(Vec::new())
594
+ })
595
+ }
596
+ Request::HeapStats { depth } => {
597
+ script(scope, runtime, depth, DispatchKind::Maintenance, |tc| {
598
+ Ok(heap_stats(tc))
599
+ })
600
+ }
601
+ Request::HeapSnapshot { depth } => {
602
+ script(scope, runtime, depth, DispatchKind::Maintenance, |tc| {
603
+ Ok(heap_snapshot(tc))
604
+ })
605
+ }
606
+ Request::PumpMessageLoop { depth } => {
607
+ script(scope, runtime, depth, DispatchKind::Pump, |tc| {
608
+ Ok(vec![u8::from(pump(tc, runtime))])
609
+ })
610
+ }
611
+ // The extension refuses a dispose from inside a callback before it
612
+ // gets here.
613
+ Request::Dispose => Err(EngineError::DisposeBusy),
614
+ // Snapshots are built on a context of their own, never nested.
615
+ Request::Snapshot { .. } => Err(EngineError::Internal(
616
+ "snapshot inside a callback".to_owned(),
617
+ )),
618
+ }
619
+ }
620
+
621
+ // A maintenance dispatch reports only whether a dispose caught it.
622
+ fn ended(runtime: &Runtime, dispatch: DispatchId) -> Result<Vec<u8>, EngineError> {
623
+ match runtime.block.term.end(dispatch) {
624
+ Some(_) => Err(EngineError::Disposed),
625
+ None => Ok(Vec::new()),
626
+ }
627
+ }
628
+
629
+ fn halted(halt: Halt) -> EngineError {
630
+ match halt {
631
+ Halt::Terminated(cause) => EngineError::from(JsError::terminated(cause)),
632
+ // mini_racer cancels the wait of a dispatch a dispose catches in
633
+ // flight, which raises its disposed error.
634
+ Halt::Disposed => EngineError::Disposed,
635
+ }
636
+ }
637
+
638
+ // Runs a dispatch: nothing starts once it is halted. A script takes its
639
+ // verdict before serializing its result (`verdict`) and stays armed through
640
+ // it, as serializing runs getters: a cause that stops the serializing is
641
+ // reported, and one that lands without stopping it leaves the reply. A nested
642
+ // dispatch reports a halt but leaves its cause to the dispatch it runs in. An
643
+ // exception that escapes as the one a raising callback threw takes
644
+ // precedence, as in mini_racer.
645
+ fn script(
646
+ scope: &mut v8::PinScope<'_, '_>,
647
+ runtime: &Runtime,
648
+ depth: Depth,
649
+ kind: DispatchKind,
650
+ run: impl FnOnce(&mut Tc<'_, '_, '_, '_>) -> Result<Vec<u8>, EngineError>,
651
+ ) -> Result<Vec<u8>, EngineError> {
652
+ let term = &runtime.block.term;
653
+ v8::tc_scope!(let tc, scope);
654
+ tc.set_verbose(runtime.block.config.verbose_exceptions);
655
+ let result = if let Err(error) = arm_timeout(runtime, depth, kind) {
656
+ Err(error)
657
+ } else if term.refuses(kind) {
658
+ Err(EngineError::Internal("halted without a cause".to_owned()))
659
+ } else {
660
+ run(tc)
661
+ };
662
+ let since = term.mark();
663
+ // mini_racer's maintenance never cancels a termination it meets, so the
664
+ // interrupt is used up while the cause waits for the next script. Inside
665
+ // a callback, the termination caught here would otherwise unwind the
666
+ // script around it, which in mini_racer runs on and reports the cause
667
+ // when it ends.
668
+ if kind == DispatchKind::Maintenance
669
+ && tc.has_terminated()
670
+ && term.spend(since, matches!(depth, Depth::Nested))
671
+ {
672
+ // Lifted, the termination this caught must not be rethrown as the
673
+ // scope closes.
674
+ tc.reset();
675
+ }
676
+ let raised = result.is_err() && kind == DispatchKind::Script && escaped(tc, runtime);
677
+ let halt = match depth {
678
+ Depth::Outermost(dispatch) if result.is_ok() => term.finish(dispatch),
679
+ // mini_racer replies to a terminated script with its termination
680
+ // cancelled, and V8 runs the microtasks the script left then, still
681
+ // within the dispatch. A cause V8 acts on there is carried, spent, to
682
+ // the next script; one still pending when they finish fails the
683
+ // reply, and mini_racer's retry reports it with this script. When the
684
+ // script's own timeout ended it, mini_racer runs them with no limit;
685
+ // the timeout bounds them again here.
686
+ Depth::Outermost(dispatch) if kind == DispatchKind::Script && tc.has_terminated() => {
687
+ let verdict = term.settle(dispatch);
688
+ let drains = verdict.is_some_and(|halt| halt != Halt::Disposed)
689
+ && rearm_timeout(runtime, dispatch).is_ok();
690
+ if drains {
691
+ tc.reset();
692
+ tc.perform_microtask_checkpoint();
693
+ }
694
+ if drains && (tc.has_terminated() || tc.is_execution_terminating()) {
695
+ term.spend(term.mark(), false);
696
+ match term.finish(dispatch) {
697
+ Some(Halt::Disposed) => Some(Halt::Disposed),
698
+ _ => verdict,
699
+ }
700
+ } else if drains {
701
+ term.end_drained(dispatch).or(verdict)
702
+ } else {
703
+ match term.end(dispatch) {
704
+ Some(Halt::Disposed) => Some(Halt::Disposed),
705
+ _ => verdict,
706
+ }
707
+ }
708
+ }
709
+ Depth::Outermost(dispatch) => term.end(dispatch),
710
+ // Its verdict came before it serialized, but a dispose still cancels
711
+ // the wait for it, as in mini_racer.
712
+ Depth::Nested if result.is_ok() && kind == DispatchKind::Script => {
713
+ term.is_disposing().then_some(Halt::Disposed)
714
+ }
715
+ Depth::Nested => {
716
+ let halt = term.check(kind);
717
+ if kind == DispatchKind::Script && matches!(halt, Some(Halt::Terminated(_))) {
718
+ term.renew();
719
+ }
720
+ halt
721
+ }
722
+ };
723
+ if raised {
724
+ return Err(EngineError::from(JsError {
725
+ kind: JsErrorKind::HostException,
726
+ message: String::new(),
727
+ }));
728
+ }
729
+ halt.map_or(result, |halt| Err(halted(halt)))
730
+ }
731
+
732
+ // mini_racer starts a script's watchdog on its V8 thread as it takes the
733
+ // request, so time spent waiting for the runner, such as behind a collection
734
+ // after `ensure_gc_after_idle`, never counts against the timeout. Setup and
735
+ // maintenance are never timed.
736
+ fn arm_timeout(runtime: &Runtime, depth: Depth, kind: DispatchKind) -> Result<(), EngineError> {
737
+ match (depth, kind, runtime.block.config.timeout) {
738
+ (Depth::Outermost(dispatch), DispatchKind::Script, Some(timeout)) => {
739
+ watchdog::schedule(timeout, &runtime.block, dispatch)
740
+ }
741
+ _ => Ok(()),
742
+ }
743
+ }
744
+
745
+ fn rearm_timeout(runtime: &Runtime, dispatch: DispatchId) -> Result<(), EngineError> {
746
+ runtime.block.config.timeout.map_or(Ok(()), |timeout| {
747
+ watchdog::rearm_if_fired(timeout, &runtime.block, dispatch)
748
+ })
749
+ }
750
+
751
+ fn escaped(tc: &Tc<'_, '_, '_, '_>, runtime: &Runtime) -> bool {
752
+ let Some(exception) = tc.exception() else {
753
+ return false;
754
+ };
755
+ lock(&runtime.raised)
756
+ .as_ref()
757
+ .is_some_and(|raised| v8::Local::new(tc, raised).same_value(exception))
758
+ }
759
+
760
+ fn evaluate(
761
+ tc: &mut Tc<'_, '_, '_, '_>,
762
+ runtime: &Runtime,
763
+ (filename, source): (&JsText, &JsText),
764
+ (depth, awaited): (Depth, bool),
765
+ ) -> Result<Vec<u8>, EngineError> {
766
+ if let Some(value) = fast_call(tc, runtime, filename, source)? {
767
+ return settle(tc, runtime, value, (depth, awaited));
768
+ }
769
+ let plan = if large(source) {
770
+ runtime.freed.store(true, Ordering::Relaxed);
771
+ runtime
772
+ .generation
773
+ .code_cache()
774
+ .plan(filename, source, runtime.snapshot)
775
+ } else {
776
+ Plan::Compile
777
+ };
778
+ let too_large = || EngineError::Internal("source too large".to_owned());
779
+ let source = string(tc, source).ok_or_else(too_large)?;
780
+ let name = string(tc, filename).ok_or_else(too_large)?;
781
+ let origin = serialize::origin(tc, name.into());
782
+ let (script, make) = compile(tc, runtime, source, &origin, plan);
783
+ let script = script.ok_or_else(|| failure(tc, JsErrorKind::Parse))?;
784
+ let value = script
785
+ .run(tc)
786
+ .ok_or_else(|| failure(tc, JsErrorKind::Runtime))?;
787
+ let reply = settle(tc, runtime, value, (depth, awaited))?;
788
+ if let Some(building) = make {
789
+ let script = v8::Global::new(tc, script.get_unbound_script(tc));
790
+ lock(&runtime.uncached).push(Uncached { script, building });
791
+ }
792
+ Ok(reply)
793
+ }
794
+
795
+ fn settle<'s>(
796
+ tc: &mut Tc<'s, '_, '_, '_>,
797
+ runtime: &Runtime,
798
+ value: v8::Local<'s, v8::Value>,
799
+ (depth, awaited): (Depth, bool),
800
+ ) -> Result<Vec<u8>, EngineError> {
801
+ let value = if awaited {
802
+ await_promise(tc, runtime, value)?
803
+ } else {
804
+ value
805
+ };
806
+ let value = serialize::sanitize(tc, value);
807
+ verdict(runtime, depth)?;
808
+ serialize::reply(tc, &runtime.filter, value).ok_or_else(|| failure(tc, JsErrorKind::Runtime))
809
+ }
810
+
811
+ // The evals ExecJS and react-rails send once per call run a template compiled
812
+ // once per context, a script like the source, so its frames and `this` are the
813
+ // source's. It reads the values the source carries, parsed as JSON instead of
814
+ // compiled, through a binding to `hoisted`. `None` leaves the eval to the
815
+ // normal path: another shape, values that are not JSON, or a template that is
816
+ // refused (`template`).
817
+ fn fast_call<'s>(
818
+ tc: &mut Tc<'s, '_, '_, '_>,
819
+ runtime: &Runtime,
820
+ filename: &JsText,
821
+ source: &JsText,
822
+ ) -> Result<Option<v8::Local<'s, v8::Value>>, EngineError> {
823
+ // mini_racer's verbose `TryCatch` prints where an exception happened as
824
+ // its offset in the script, which a template moves.
825
+ if !crate::process::fast_calls() || runtime.block.config.verbose_exceptions {
826
+ return Ok(None);
827
+ }
828
+ let (JsText::Utf8(name), JsText::Utf8(source)) = (filename, source) else {
829
+ return Ok(None);
830
+ };
831
+ // A source sent again goes the normal way: from its second run there,
832
+ // V8's compilation cache runs it faster than a fast call parses its values
833
+ // again, and keeps no script per run.
834
+ if name != b"<eval>" || {
835
+ let hoisting = lock(&runtime.hoisting);
836
+ hoisting.last == *source
837
+ || hoisting
838
+ .templates
839
+ .iter()
840
+ .any(|template| template.refuses(source))
841
+ } {
842
+ return Ok(None);
843
+ }
844
+ let Some(shape) = fast_calls::recognize(source) else {
845
+ return Ok(None);
846
+ };
847
+ remember(&mut lock(&runtime.hoisting).last, source);
848
+ if matches!(lock(&runtime.hoisting).binding, Binding::Unmade) {
849
+ bind_reader(tc, runtime);
850
+ }
851
+ let text = match &lock(&runtime.hoisting).binding {
852
+ Binding::Made(reader) => shape.template(reader.as_bytes()),
853
+ Binding::Unmade | Binding::Failed => None,
854
+ };
855
+ // Kept per context, as `last` is, so held to the same size.
856
+ let Some(text) = text.filter(|text| text.len() <= LARGE_SOURCE) else {
857
+ return declined(tc);
858
+ };
859
+ // Before parsing anything, so a render whose template is refused is never
860
+ // JSON-parsed.
861
+ let Some(script) = template(tc, runtime, text, &shape, filename) else {
862
+ return declined(tc);
863
+ };
864
+ let mut values = Vec::with_capacity(shape.values.len());
865
+ for value in &shape.values {
866
+ let (Hoisted::Json(literal) | Hoisted::Text(literal)) = *value;
867
+ let string = v8::String::new_from_utf8(tc, literal, v8::NewStringType::Normal);
868
+ let parsed = match value {
869
+ Hoisted::Json(_) => string.and_then(|string| v8::json::parse(tc, string)),
870
+ Hoisted::Text(_) => string.map(Into::into),
871
+ };
872
+ let Some(parsed) = parsed else {
873
+ return declined(tc);
874
+ };
875
+ values.push(parsed);
876
+ }
877
+ let frame = v8::Array::new_with_elements(tc, &values);
878
+ lock(&runtime.frames).push(v8::Global::new(tc, frame));
879
+ let value = script.run(tc);
880
+ lock(&runtime.frames).pop();
881
+ value
882
+ .map(Some)
883
+ .ok_or_else(|| failure(tc, JsErrorKind::Runtime))
884
+ }
885
+
886
+ fn remember(last: &mut Vec<u8>, source: &[u8]) {
887
+ if source.len() > LARGE_SOURCE {
888
+ *last = Vec::new();
889
+ } else {
890
+ last.clear();
891
+ last.extend_from_slice(source);
892
+ }
893
+ }
894
+
895
+ // Declining leaves nothing it threw for the normal path, though a termination
896
+ // that arrived meanwhile still ends the dispatch.
897
+ fn declined<'s>(
898
+ tc: &mut Tc<'s, '_, '_, '_>,
899
+ ) -> Result<Option<v8::Local<'s, v8::Value>>, EngineError> {
900
+ if tc.has_terminated() {
901
+ return Err(failure(tc, JsErrorKind::Runtime));
902
+ }
903
+ tc.reset();
904
+ Ok(None)
905
+ }
906
+
907
+ // A global `const` holding the function a template calls for its values: no
908
+ // property lists it, user code cannot reassign it, and its random name keeps
909
+ // it from meeting a global of the user's. The setup script takes the function
910
+ // from a global property of another random name, deleted once it has run.
911
+ // Made under the explicit microtask policy, so microtasks already queued run
912
+ // after the template's script, as they would after the source's.
913
+ fn bind_reader(tc: &mut Tc<'_, '_, '_, '_>, runtime: &Runtime) {
914
+ let [name, key] = [0, 1].map(|salt| {
915
+ let random = RandomState::new().hash_one((runtime.key, salt));
916
+ format!("__mini_racer_rs_{random:016x}")
917
+ });
918
+ let policy = tc.get_microtasks_policy();
919
+ tc.set_microtasks_policy(v8::MicrotasksPolicy::Explicit);
920
+ let global = tc.get_current_context().global(tc);
921
+ let made = v8::String::new(tc, &key).is_some_and(|property| {
922
+ let made = (|| {
923
+ let read = v8::Function::builder(hoisted).build(tc)?;
924
+ global
925
+ .create_data_property(tc, property.into(), read.into())
926
+ .filter(|&defined| defined)?;
927
+ let setup = v8::String::new(tc, &format!("const {name} = this[\"{key}\"];"))?;
928
+ v8::Script::compile(tc, setup, None)?.run(tc)
929
+ })()
930
+ .is_some();
931
+ global.delete(tc, property.into());
932
+ made
933
+ });
934
+ tc.set_microtasks_policy(policy);
935
+ // A termination may have cut the setup short; the next call makes another.
936
+ if made || !tc.has_terminated() {
937
+ lock(&runtime.hoisting).binding = if made {
938
+ Binding::Made(name)
939
+ } else {
940
+ Binding::Failed
941
+ };
942
+ }
943
+ }
944
+
945
+ fn template<'s>(
946
+ scope: &v8::PinScope<'s, '_>,
947
+ runtime: &Runtime,
948
+ text: Vec<u8>,
949
+ shape: &Shape<'_>,
950
+ filename: &JsText,
951
+ ) -> Option<v8::Local<'s, v8::Script>> {
952
+ let mut hoisting = lock(&runtime.hoisting);
953
+ let kept = hoisting
954
+ .templates
955
+ .iter()
956
+ .position(|template| *template.text == *text);
957
+ if let Some(at) = kept {
958
+ let template = hoisting.templates.remove(at);
959
+ let script = template
960
+ .script
961
+ .as_ref()
962
+ .map(|script| v8::Local::new(scope, script));
963
+ hoisting.templates.push(template);
964
+ return script;
965
+ }
966
+ drop(hoisting);
967
+ let source = v8::String::new_from_utf8(scope, &text, v8::NewStringType::Normal)?;
968
+ let name = string(scope, filename)?;
969
+ let origin = serialize::origin(scope, name.into());
970
+ let script = shape
971
+ .code
972
+ .iter()
973
+ .all(|code| fast_calls::leaves_the_call_alone(code) && compiles_alone(scope, code))
974
+ .then(|| v8::Script::compile(scope, source, Some(&origin)))
975
+ .flatten();
976
+ let mut hoisting = lock(&runtime.hoisting);
977
+ if hoisting.templates.len() >= TEMPLATES {
978
+ hoisting.templates.remove(0);
979
+ }
980
+ let ends = (
981
+ shape.around.first().map_or(0, |head| head.len()),
982
+ shape.around.last().map_or(0, |tail| tail.len()),
983
+ );
984
+ hoisting.templates.push(Template {
985
+ text: text.into_boxed_slice(),
986
+ script: script.map(|script| v8::Global::new(scope, script)),
987
+ ends,
988
+ });
989
+ script
990
+ }
991
+
992
+ // As a function body of its own, so its statements close before the call.
993
+ fn compiles_alone(scope: &v8::PinScope<'_, '_>, code: &[u8]) -> bool {
994
+ v8::String::new_from_utf8(scope, code, v8::NewStringType::Normal).is_some_and(|code| {
995
+ let mut source = v8::script_compiler::Source::new(code, None);
996
+ v8::script_compiler::compile_function(
997
+ scope,
998
+ &mut source,
999
+ &[],
1000
+ &[],
1001
+ v8::script_compiler::CompileOptions::NoCompileOptions,
1002
+ v8::script_compiler::NoCacheReason::NoReason,
1003
+ )
1004
+ .is_some()
1005
+ })
1006
+ }
1007
+
1008
+ // What a fast call's template reads in place of the literal at that index.
1009
+ #[expect(
1010
+ clippy::needless_pass_by_value,
1011
+ reason = "the signature V8 calls a function callback with"
1012
+ )]
1013
+ fn hoisted<'s>(
1014
+ scope: &mut v8::PinScope<'s, '_>,
1015
+ args: v8::FunctionCallbackArguments<'s>,
1016
+ mut rv: v8::ReturnValue<'s, v8::Value>,
1017
+ ) {
1018
+ let _ = catch_unwind(AssertUnwindSafe(|| {
1019
+ let Some(runtime) = scope.get_slot::<Arc<Runtime>>().cloned() else {
1020
+ return;
1021
+ };
1022
+ let frame = lock(&runtime.frames)
1023
+ .last()
1024
+ .map(|frame| v8::Local::new(scope, frame));
1025
+ let index = args.get(0).uint32_value(scope);
1026
+ if let (Some(frame), Some(index)) = (frame, index)
1027
+ && let Some(value) = frame.get_index(scope, index)
1028
+ {
1029
+ rv.set(value);
1030
+ }
1031
+ }));
1032
+ }
1033
+
1034
+ // From this process's code cache when the plan has an entry. V8 refuses one
1035
+ // made under other flags, another V8 or another snapshot, and compiles from
1036
+ // source; the entry is then forgotten. Also returns the claim to cache the
1037
+ // script under once it has run.
1038
+ fn compile<'s>(
1039
+ scope: &v8::PinScope<'s, '_>,
1040
+ runtime: &Runtime,
1041
+ source: v8::Local<'s, v8::String>,
1042
+ origin: &v8::ScriptOrigin<'s>,
1043
+ plan: Plan,
1044
+ ) -> (Option<v8::Local<'s, v8::Script>>, Option<Building>) {
1045
+ let (entry, key) = match plan {
1046
+ Plan::Compile => return (v8::Script::compile(scope, source, Some(origin)), None),
1047
+ Plan::Make(building) => {
1048
+ return (
1049
+ v8::Script::compile(scope, source, Some(origin)),
1050
+ Some(building),
1051
+ );
1052
+ }
1053
+ Plan::Consume(entry, key) => (entry, key),
1054
+ };
1055
+ // `Source` keeps a pointer into the entry's bytes that the borrow checker
1056
+ // does not see, so it lives only inside this block, while `entry` does.
1057
+ let (script, rejected) = {
1058
+ let data = v8::script_compiler::CachedData::new(entry.data());
1059
+ let mut cached =
1060
+ v8::script_compiler::Source::new_with_cached_data(source, Some(origin), data);
1061
+ let script = v8::script_compiler::compile(
1062
+ scope,
1063
+ &mut cached,
1064
+ v8::script_compiler::CompileOptions::ConsumeCodeCache,
1065
+ v8::script_compiler::NoCacheReason::NoReason,
1066
+ );
1067
+ let rejected = cached
1068
+ .get_cached_data()
1069
+ .is_some_and(v8::CachedData::rejected);
1070
+ (script, rejected)
1071
+ };
1072
+ let cache = runtime.generation.code_cache();
1073
+ if rejected {
1074
+ cache.forget(&entry);
1075
+ return (script, cache.begin(key));
1076
+ }
1077
+ cache.hit();
1078
+ (script, None)
1079
+ }
1080
+
1081
+ // mini_racer's last termination check, before a script's result is
1082
+ // serialized. A nested script reads the causes and leaves them to the
1083
+ // dispatch around it.
1084
+ fn verdict(runtime: &Runtime, depth: Depth) -> Result<(), EngineError> {
1085
+ let term = &runtime.block.term;
1086
+ let halt = match depth {
1087
+ Depth::Outermost(dispatch) => term.settle(dispatch),
1088
+ Depth::Nested => term.check(DispatchKind::Script),
1089
+ };
1090
+ halt.map_or(Ok(()), |halt| Err(halted(halt)))
1091
+ }
1092
+
1093
+ // V8 runs no microtask checkpoint inside another, so an await nested in a
1094
+ // callback could never settle; mini_racer refuses one before running anything.
1095
+ // With a stop or timeout pending, it reports the refusal as terminated, and the
1096
+ // refusal's text stays the message.
1097
+ fn refuse_nested_await(
1098
+ scope: &mut v8::PinScope<'_, '_>,
1099
+ runtime: &Runtime,
1100
+ ) -> Result<Vec<u8>, EngineError> {
1101
+ v8::tc_scope!(let tc, scope);
1102
+ tc.set_verbose(runtime.block.config.verbose_exceptions);
1103
+ let failed = |tc: &Tc<'_, '_, '_, '_>| failure(tc, JsErrorKind::Runtime);
1104
+ let message = v8::String::new(tc, "nested call_await/eval_await is not supported")
1105
+ .ok_or_else(|| failed(tc))?;
1106
+ let exception = v8::Exception::error(tc, message);
1107
+ tc.throw_exception(exception);
1108
+ let refused = failed(tc);
1109
+ let halt = runtime.block.term.check(DispatchKind::Script);
1110
+ if matches!(halt, Some(Halt::Terminated(_))) {
1111
+ runtime.block.term.renew();
1112
+ }
1113
+ let halt = halt.map(halted);
1114
+ Err(match (halt, refused) {
1115
+ (Some(EngineError::Js(halt)), EngineError::Js(refused)) => EngineError::from(JsError {
1116
+ kind: halt.kind,
1117
+ message: refused.message,
1118
+ }),
1119
+ (Some(disposed @ EngineError::Disposed), _) => disposed,
1120
+ (_, refused) => refused,
1121
+ })
1122
+ }
1123
+
1124
+ // mini_racer's await: run the microtasks, then look at the promise. While it
1125
+ // is pending, run due tasks one at a time, sleeping until one is due, until
1126
+ // the promise settles or the dispatch is halted.
1127
+ fn await_promise<'s>(
1128
+ tc: &mut Tc<'s, '_, '_, '_>,
1129
+ runtime: &Runtime,
1130
+ value: v8::Local<'s, v8::Value>,
1131
+ ) -> Result<v8::Local<'s, v8::Value>, EngineError> {
1132
+ let Ok(promise) = v8::Local::<v8::Promise>::try_from(value) else {
1133
+ return Ok(value);
1134
+ };
1135
+ loop {
1136
+ tc.perform_microtask_checkpoint();
1137
+ match promise.state() {
1138
+ v8::PromiseState::Fulfilled => return Ok(promise.result(tc)),
1139
+ v8::PromiseState::Rejected => {
1140
+ let reason = promise.result(tc);
1141
+ tc.throw_exception(reason);
1142
+ return Err(failure(tc, JsErrorKind::Runtime));
1143
+ }
1144
+ v8::PromiseState::Pending => {}
1145
+ }
1146
+ if tc.is_execution_terminating() || runtime.block.term.check(DispatchKind::Script).is_some()
1147
+ {
1148
+ return Err(EngineError::from(JsError::terminated(
1149
+ TerminationCause::Stop,
1150
+ )));
1151
+ }
1152
+ match runtime.block.tasks.pop(false) {
1153
+ Some(task) => run_task(runtime, task),
1154
+ None => wait_for_task(runtime),
1155
+ }
1156
+ }
1157
+ }
1158
+
1159
+ // Waiting, the runner holds its isolate but runs nothing in it, so a fork need
1160
+ // not wait for it.
1161
+ fn wait_for_task(runtime: &Runtime) {
1162
+ runtime.block.phase.set(Phase::Parked);
1163
+ runtime.block.tasks.wait();
1164
+ runtime.generation.resume(&runtime.block);
1165
+ }
1166
+
1167
+ fn run_task(runtime: &Runtime, task: v8::Task) {
1168
+ runtime.running.fetch_add(1, Ordering::Relaxed);
1169
+ let _running = Running(&runtime.running);
1170
+ task.run();
1171
+ }
1172
+
1173
+ // mini_racer's heap statistics, in its order: the numbers V8 reports, which
1174
+ // it hands JavaScript as doubles.
1175
+ #[expect(
1176
+ clippy::cast_precision_loss,
1177
+ reason = "mini_racer passes each statistic to Ruby as a V8 number"
1178
+ )]
1179
+ fn heap_stats(isolate: &mut v8::Isolate) -> Vec<u8> {
1180
+ let stats = isolate.get_heap_statistics();
1181
+ let named = [
1182
+ ("total_heap_size", stats.total_heap_size()),
1183
+ (
1184
+ "total_heap_size_executable",
1185
+ stats.total_heap_size_executable(),
1186
+ ),
1187
+ ("total_physical_size", stats.total_physical_size()),
1188
+ ("total_available_size", stats.total_available_size()),
1189
+ (
1190
+ "total_global_handles_size",
1191
+ stats.total_global_handles_size(),
1192
+ ),
1193
+ ("used_global_handles_size", stats.used_global_handles_size()),
1194
+ ("used_heap_size", stats.used_heap_size()),
1195
+ ("heap_size_limit", stats.heap_size_limit()),
1196
+ ("malloced_memory", stats.malloced_memory()),
1197
+ ("external_memory", stats.external_memory()),
1198
+ ("peak_malloced_memory", stats.peak_malloced_memory()),
1199
+ (
1200
+ "number_of_native_contexts",
1201
+ stats.number_of_native_contexts(),
1202
+ ),
1203
+ (
1204
+ "number_of_detached_contexts",
1205
+ stats.number_of_detached_contexts(),
1206
+ ),
1207
+ ];
1208
+ let mut out = Vec::new();
1209
+ let mut writer = Writer::new(&mut out);
1210
+ writer.begin_object();
1211
+ for (name, value) in named {
1212
+ writer.utf8(name.as_bytes());
1213
+ writer.number(value as f64);
1214
+ }
1215
+ writer.end_object(u32::try_from(named.len()).unwrap_or(u32::MAX));
1216
+ out
1217
+ }
1218
+
1219
+ fn heap_snapshot(isolate: &mut v8::Isolate) -> Vec<u8> {
1220
+ let mut json = Vec::new();
1221
+ isolate.take_heap_snapshot(|chunk| {
1222
+ json.extend_from_slice(chunk);
1223
+ true
1224
+ });
1225
+ json
1226
+ }
1227
+
1228
+ // mini_racer's pump: one due task, then a microtask checkpoint if it ran and
1229
+ // left nothing thrown or terminating. A pump from a running task's script
1230
+ // passes over non-nestable tasks, as V8's own runner does.
1231
+ fn pump(tc: &mut Tc<'_, '_, '_, '_>, runtime: &Runtime) -> bool {
1232
+ let nested = runtime.running.load(Ordering::Relaxed) > 0;
1233
+ let Some(task) = runtime.block.tasks.pop(nested) else {
1234
+ return false;
1235
+ };
1236
+ run_task(runtime, task);
1237
+ if !tc.is_execution_terminating() && !tc.has_caught() {
1238
+ tc.perform_microtask_checkpoint();
1239
+ }
1240
+ true
1241
+ }
1242
+
1243
+ struct Reader;
1244
+
1245
+ impl v8::ValueDeserializerImpl for Reader {}
1246
+
1247
+ // `call` and `attach` requests are an array whose first element is a dotted
1248
+ // path; failing to read one is an internal error in mini_racer.
1249
+ fn read_request<'s>(
1250
+ tc: &Tc<'s, '_, '_, '_>,
1251
+ request: &[u8],
1252
+ ) -> Option<(v8::Local<'s, v8::Object>, String)> {
1253
+ let context = tc.get_current_context();
1254
+ let deserializer = v8::ValueDeserializer::new(tc, Box::new(Reader), request);
1255
+ if deserializer.read_header(context) != Some(true) {
1256
+ return None;
1257
+ }
1258
+ let request = deserializer
1259
+ .read_value(context)
1260
+ .and_then(|request| request.to_object(tc))?;
1261
+ let path = request
1262
+ .get_index(tc, 0)
1263
+ .and_then(|name| name.to_string(tc))?
1264
+ .to_rust_string_lossy(tc);
1265
+ Some((request, path))
1266
+ }
1267
+
1268
+ // mini_racer's order: everything from the path walk on fails as a runtime
1269
+ // error. Each dotted segment but the last becomes an object, and the function
1270
+ // is called on its parent.
1271
+ fn invoke(
1272
+ tc: &mut Tc<'_, '_, '_, '_>,
1273
+ runtime: &Runtime,
1274
+ request: &[u8],
1275
+ (depth, awaited): (Depth, bool),
1276
+ ) -> Result<Vec<u8>, EngineError> {
1277
+ let context = tc.get_current_context();
1278
+ let failed = |tc: &Tc<'_, '_, '_, '_>| failure(tc, JsErrorKind::Runtime);
1279
+ let (request, path) =
1280
+ read_request(tc, request).ok_or_else(|| failure(tc, JsErrorKind::Internal))?;
1281
+ let mut receiver = context.global(tc);
1282
+ let mut keys = path.split('.').peekable();
1283
+ let mut function = None;
1284
+ while let Some(key) = keys.next() {
1285
+ let key = v8::String::new(tc, key).ok_or_else(|| failed(tc))?;
1286
+ let value = receiver.get(tc, key.into()).ok_or_else(|| failed(tc))?;
1287
+ if keys.peek().is_none() {
1288
+ function = Some(value);
1289
+ } else {
1290
+ receiver = value.to_object(tc).ok_or_else(|| failed(tc))?;
1291
+ }
1292
+ }
1293
+ let Some(Ok(function)) = function.map(v8::Local::<v8::Function>::try_from) else {
1294
+ let message = v8::String::new(tc, "not a function").ok_or_else(|| failed(tc))?;
1295
+ let exception = v8::Exception::type_error(tc, message);
1296
+ tc.throw_exception(exception);
1297
+ return Err(failed(tc));
1298
+ };
1299
+ let count = v8::Local::<v8::Array>::try_from(request).map_or(0, |array| array.length());
1300
+ let mut args = Vec::new();
1301
+ for index in 1..count {
1302
+ args.push(request.get_index(tc, index).ok_or_else(|| failed(tc))?);
1303
+ }
1304
+ let value = function
1305
+ .call(tc, receiver.into(), &args)
1306
+ .ok_or_else(|| failed(tc))?;
1307
+ let value = if awaited {
1308
+ await_promise(tc, runtime, value)?
1309
+ } else {
1310
+ value
1311
+ };
1312
+ let value = serialize::sanitize(tc, value);
1313
+ verdict(runtime, depth)?;
1314
+ serialize::reply(tc, &runtime.filter, value).ok_or_else(|| failed(tc))
1315
+ }
1316
+
1317
+ // mini_racer's attach: any failure is an internal error, even under a pending
1318
+ // termination, which attach never checks for; the message is then V8's
1319
+ // termination exception, "null". Each dotted segment but the last that is not
1320
+ // an object or function becomes a new object, and the callback is set on the
1321
+ // last.
1322
+ fn attach(tc: &Tc<'_, '_, '_, '_>, request: &[u8]) -> Result<Vec<u8>, EngineError> {
1323
+ let context = tc.get_current_context();
1324
+ let internal = |tc: &Tc<'_, '_, '_, '_>| described(tc, JsErrorKind::Internal);
1325
+ let (request, path) = read_request(tc, request).ok_or_else(|| internal(tc))?;
1326
+ let id = request
1327
+ .get_index(tc, 1)
1328
+ .filter(|id| id.is_int32())
1329
+ .ok_or_else(|| internal(tc))?;
1330
+ let mut object = context.global(tc);
1331
+ let mut keys = path.split('.').peekable();
1332
+ while let Some(key) = keys.next() {
1333
+ let key = v8::String::new(tc, key).ok_or_else(|| internal(tc))?;
1334
+ if keys.peek().is_none() {
1335
+ let function = v8::Function::builder(trampoline)
1336
+ .data(id)
1337
+ .build(tc)
1338
+ .ok_or_else(|| internal(tc))?;
1339
+ if object.set(tc, key.into(), function.into()) != Some(true) {
1340
+ return Err(internal(tc));
1341
+ }
1342
+ break;
1343
+ }
1344
+ let value = object.get(tc, key.into()).ok_or_else(|| internal(tc))?;
1345
+ object = if let Ok(inner) = v8::Local::<v8::Object>::try_from(value) {
1346
+ inner
1347
+ } else {
1348
+ let inner = v8::Object::new(tc);
1349
+ if object.set(tc, key.into(), inner.into()) != Some(true) {
1350
+ return Err(internal(tc));
1351
+ }
1352
+ inner
1353
+ };
1354
+ }
1355
+ Ok(Vec::new())
1356
+ }
1357
+
1358
+ // An attached function: its arguments go to Ruby with its id, and requests
1359
+ // Ruby makes meanwhile are served here until the reply comes. A panic closes
1360
+ // the channel, as one in the runner loop does.
1361
+ #[expect(
1362
+ clippy::needless_pass_by_value,
1363
+ reason = "the signature V8 calls a function callback with"
1364
+ )]
1365
+ fn trampoline<'s>(
1366
+ scope: &mut v8::PinScope<'s, '_>,
1367
+ args: v8::FunctionCallbackArguments<'s>,
1368
+ rv: v8::ReturnValue<'s, v8::Value>,
1369
+ ) {
1370
+ let Some(runtime) = scope.get_slot::<Arc<Runtime>>().cloned() else {
1371
+ return;
1372
+ };
1373
+ let outcome = catch_unwind(AssertUnwindSafe(|| callback(scope, &runtime, &args, rv)));
1374
+ if outcome.is_err() {
1375
+ scope.terminate_execution();
1376
+ runtime.block.channel.close();
1377
+ }
1378
+ }
1379
+
1380
+ fn callback<'s>(
1381
+ scope: &mut v8::PinScope<'s, '_>,
1382
+ runtime: &Runtime,
1383
+ args: &v8::FunctionCallbackArguments<'s>,
1384
+ mut rv: v8::ReturnValue<'s, v8::Value>,
1385
+ ) {
1386
+ let count = args.length();
1387
+ // mini_racer builds the request in the safe context, out of reach of a
1388
+ // tampered Array.prototype.
1389
+ let request = {
1390
+ let safe = v8::Local::new(scope, &runtime.filter.context);
1391
+ let scope = &mut v8::ContextScope::new(scope, safe);
1392
+ v8::Array::new(scope, count.saturating_add(1))
1393
+ };
1394
+ let mut index = 0;
1395
+ for position in 0..count {
1396
+ let value = serialize::sanitize(scope, args.get(position));
1397
+ if request.set_index(scope, index, value).is_none() {
1398
+ return;
1399
+ }
1400
+ index = index.saturating_add(1);
1401
+ }
1402
+ if request.set_index(scope, index, args.data()).is_none() {
1403
+ return;
1404
+ }
1405
+ let Some(bytes) = serialize::write(scope, request.into()) else {
1406
+ return;
1407
+ };
1408
+ if runtime.block.channel.callback(bytes).is_err() {
1409
+ scope.terminate_execution();
1410
+ return;
1411
+ }
1412
+ loop {
1413
+ // Waiting for Ruby, V8 is still: a fork need not wait for this.
1414
+ runtime.block.phase.set(Phase::Parked);
1415
+ let message = receive(&runtime.block);
1416
+ runtime.generation.resume(&runtime.block);
1417
+ let reply = match message {
1418
+ Some(ToV8::Reply(reply)) => reply,
1419
+ Some(ToV8::Request(request)) => {
1420
+ // Each nested request gets its own handle scope, so its values
1421
+ // are released when it responds, not when the callback returns.
1422
+ let response = {
1423
+ v8::scope_with_context!(let scope, scope, &runtime.context);
1424
+ handle(scope, runtime, request)
1425
+ };
1426
+ if runtime.block.channel.respond(response).is_err() {
1427
+ scope.terminate_execution();
1428
+ return;
1429
+ }
1430
+ continue;
1431
+ }
1432
+ None => {
1433
+ scope.terminate_execution();
1434
+ return;
1435
+ }
1436
+ };
1437
+ // A nested dispatch that was halted leaves its termination to unwind
1438
+ // this script too, as mini_racer restores it.
1439
+ let terminating = scope.is_execution_terminating();
1440
+ match reply {
1441
+ // As mini_racer answers a cancelled callback: the throw stops the
1442
+ // script at once, and the termination takes over at V8's next
1443
+ // interrupt check. Once the script's verdict is in, mini_racer
1444
+ // only throws to the microtasks it left.
1445
+ CallbackReply::Abort => {
1446
+ if !terminating {
1447
+ let exception = error(scope, &JsText::from("terminated"));
1448
+ scope.throw_exception(exception);
1449
+ }
1450
+ if !runtime.block.term.is_settled() {
1451
+ scope.terminate_execution();
1452
+ }
1453
+ }
1454
+ // mini_racer throws a raising callback's exception even with a
1455
+ // cause pending, or a nested script's termination restored:
1456
+ // escaping the script, it takes precedence, and the termination
1457
+ // takes over if the script catches it.
1458
+ CallbackReply::Raised(message) => {
1459
+ if terminating {
1460
+ scope.cancel_terminate_execution();
1461
+ }
1462
+ let exception = error(scope, &message);
1463
+ *lock(&runtime.raised) = Some(v8::Global::new(scope, exception));
1464
+ scope.throw_exception(exception);
1465
+ if terminating {
1466
+ scope.terminate_execution();
1467
+ }
1468
+ }
1469
+ _ if terminating => {
1470
+ scope.terminate_execution();
1471
+ }
1472
+ // As mini_racer reads the reply: a termination pending for this
1473
+ // script makes the read throw it, and the script ends here.
1474
+ CallbackReply::Value(bytes) => {
1475
+ let context = scope.get_current_context();
1476
+ let deserializer = v8::ValueDeserializer::new(scope, Box::new(Reader), &bytes);
1477
+ if deserializer.read_header(context) == Some(true)
1478
+ && let Some(value) = deserializer.read_value(context)
1479
+ {
1480
+ rv.set(value);
1481
+ }
1482
+ }
1483
+ }
1484
+ return;
1485
+ }
1486
+ }
1487
+
1488
+ fn error<'s>(scope: &v8::PinScope<'s, '_>, message: &JsText) -> v8::Local<'s, v8::Value> {
1489
+ let message = string(scope, message)
1490
+ .or_else(|| v8::String::new(scope, "Ruby exception"))
1491
+ .unwrap_or_else(|| v8::String::empty(scope));
1492
+ v8::Exception::error(scope, message)
1493
+ }
1494
+
1495
+ // mini_racer's suspend_termination: a failure under a pending termination is
1496
+ // reported as the termination.
1497
+ fn failure(tc: &Tc<'_, '_, '_, '_>, kind: JsErrorKind) -> EngineError {
1498
+ if tc.has_terminated() {
1499
+ return EngineError::from(JsError::terminated(TerminationCause::Stop));
1500
+ }
1501
+ described(tc, kind)
1502
+ }
1503
+
1504
+ // The message mini_racer builds for each kind: a parse error with where it
1505
+ // happened, a runtime error with its stack when the exception has one.
1506
+ fn described(tc: &Tc<'_, '_, '_, '_>, kind: JsErrorKind) -> EngineError {
1507
+ let message = match kind {
1508
+ JsErrorKind::Parse => parse_message(tc),
1509
+ JsErrorKind::Runtime
1510
+ | JsErrorKind::Internal
1511
+ | JsErrorKind::Terminated(_)
1512
+ | JsErrorKind::HostException => tc
1513
+ .stack_trace()
1514
+ .and_then(|trace| trace.to_string(tc))
1515
+ .map(|trace| trace.to_rust_string_lossy(tc)),
1516
+ };
1517
+ let message = message.unwrap_or_else(|| {
1518
+ tc.exception()
1519
+ .and_then(|exception| exception.to_string(tc))
1520
+ .map_or_else(
1521
+ || "unexpected failure".to_owned(),
1522
+ |text| text.to_rust_string_lossy(tc),
1523
+ )
1524
+ });
1525
+ EngineError::from(JsError { kind, message })
1526
+ }
1527
+
1528
+ pub fn string<'s>(
1529
+ scope: &v8::PinScope<'s, '_>,
1530
+ text: &JsText,
1531
+ ) -> Option<v8::Local<'s, v8::String>> {
1532
+ match text {
1533
+ JsText::Utf8(bytes) => v8::String::new_from_utf8(scope, bytes, v8::NewStringType::Normal),
1534
+ JsText::Latin1(bytes) => {
1535
+ v8::String::new_from_one_byte(scope, bytes, v8::NewStringType::Normal)
1536
+ }
1537
+ JsText::Utf16(units) => {
1538
+ v8::String::new_from_two_byte(scope, units, v8::NewStringType::Normal)
1539
+ }
1540
+ }
1541
+ }
1542
+
1543
+ fn parse_message(tc: &Tc<'_, '_, '_, '_>) -> Option<String> {
1544
+ located(tc, " at ")
1545
+ }
1546
+
1547
+ /// The pending exception's message and where it happened, as `mini_racer`
1548
+ /// formats both, joined by `separator`.
1549
+ pub fn located(tc: &Tc<'_, '_, '_, '_>, separator: &str) -> Option<String> {
1550
+ let message = tc.message()?;
1551
+ let text = message.get(tc).to_rust_string_lossy(tc);
1552
+ let name = message
1553
+ .get_script_resource_name(tc)?
1554
+ .to_string(tc)?
1555
+ .to_rust_string_lossy(tc);
1556
+ let line = message.get_line_number(tc).unwrap_or(0);
1557
+ // V8's -1 for no column comes back as usize::MAX; mini_racer prints it signed.
1558
+ let column = message.get_start_column().cast_signed();
1559
+ Some(format!("{text}{separator}{name}:{line}:{column}"))
1560
+ }