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,1234 @@
1
+ use crate::lock::lock;
2
+ use crate::primitives::Mutex;
3
+
4
+ /// Why V8 stopped running a dispatch's JavaScript.
5
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
6
+ pub enum TerminationCause {
7
+ Stop,
8
+ Timeout,
9
+ Cancelled,
10
+ MaxMemory,
11
+ HeapExhausted,
12
+ }
13
+
14
+ impl TerminationCause {
15
+ // Most important first: the memory causes raise a different Ruby error
16
+ // than the rest, so they win when several land on one dispatch.
17
+ const BY_PRIORITY: [Self; 5] = [
18
+ Self::HeapExhausted,
19
+ Self::MaxMemory,
20
+ Self::Cancelled,
21
+ Self::Timeout,
22
+ Self::Stop,
23
+ ];
24
+
25
+ const fn bit(self) -> u8 {
26
+ match self {
27
+ Self::Stop => 1,
28
+ Self::Timeout => 1 << 1,
29
+ Self::Cancelled => 1 << 2,
30
+ Self::MaxMemory => 1 << 3,
31
+ Self::HeapExhausted => 1 << 4,
32
+ }
33
+ }
34
+ }
35
+
36
+ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
37
+ struct CauseSet(u8);
38
+
39
+ impl CauseSet {
40
+ // The causes that outlive the dispatch they arrived in, as upstream's
41
+ // non-watchdog termination bit does.
42
+ const PENDING: Self = Self(
43
+ TerminationCause::Stop.bit()
44
+ | TerminationCause::MaxMemory.bit()
45
+ | TerminationCause::HeapExhausted.bit(),
46
+ );
47
+
48
+ // The causes upstream records in its request flag beside the one-shot
49
+ // interrupt, which V8 can use up while the flag stays.
50
+ const SPENDABLE: Self = Self(TerminationCause::Stop.bit() | TerminationCause::MaxMemory.bit());
51
+
52
+ const fn with(self, cause: TerminationCause) -> Self {
53
+ Self(self.0 | cause.bit())
54
+ }
55
+
56
+ const fn without(self, cause: TerminationCause) -> Self {
57
+ Self(self.0 & !cause.bit())
58
+ }
59
+
60
+ const fn contains(self, cause: TerminationCause) -> bool {
61
+ self.0 & cause.bit() != 0
62
+ }
63
+
64
+ const fn is_empty(self) -> bool {
65
+ self.0 == 0
66
+ }
67
+
68
+ const fn intersect(self, other: Self) -> Self {
69
+ Self(self.0 & other.0)
70
+ }
71
+
72
+ const fn minus(self, other: Self) -> Self {
73
+ Self(self.0 & !other.0)
74
+ }
75
+
76
+ // What outlives a dispatch that does not report it: a stop or memory
77
+ // cause, unless a cancel cleared everything, as upstream's cancel path
78
+ // stores zero.
79
+ const fn carried(self) -> Self {
80
+ if self.contains(TerminationCause::Cancelled) {
81
+ return Self(0);
82
+ }
83
+ self.intersect(Self::PENDING)
84
+ }
85
+
86
+ fn first(self) -> Option<TerminationCause> {
87
+ TerminationCause::BY_PRIORITY
88
+ .into_iter()
89
+ .find(|cause| self.contains(*cause))
90
+ }
91
+ }
92
+
93
+ /// Identifies one outermost dispatch, so a late timeout cannot hit the next one.
94
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95
+ pub struct DispatchId(u64);
96
+
97
+ /// How a dispatch treats pending causes, following `mini_racer` 0.22.1.
98
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
99
+ pub enum DispatchKind {
100
+ /// `eval`, `call` and their await forms, snapshot creation and warmup:
101
+ /// report the cause; outermost, consume it.
102
+ Script,
103
+ /// `pump_message_loop`: never report; outermost, consume silently.
104
+ Pump,
105
+ /// Isolate setup, `attach`, heap statistics, heap snapshots, the low
106
+ /// memory notification and the microtask checkpoint: never report; leave
107
+ /// a stop or memory cause for the next script.
108
+ Maintenance,
109
+ }
110
+
111
+ impl DispatchKind {
112
+ fn report(self, causes: CauseSet) -> Option<TerminationCause> {
113
+ match self {
114
+ Self::Script => causes.first(),
115
+ Self::Pump | Self::Maintenance => None,
116
+ }
117
+ }
118
+
119
+ // A cancelled dispatch clears everything, so an abandoned wait leaves no
120
+ // stale termination behind.
121
+ const fn remaining(self, causes: CauseSet) -> CauseSet {
122
+ match self {
123
+ Self::Script | Self::Pump => CauseSet(0),
124
+ Self::Maintenance => causes.carried(),
125
+ }
126
+ }
127
+ }
128
+
129
+ /// A request to terminate. Timeouts and cancellations name the dispatch they
130
+ /// belong to and are dropped if it has already ended.
131
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
132
+ pub enum Interrupt {
133
+ Stop,
134
+ Timeout(DispatchId),
135
+ Cancel(DispatchId),
136
+ MaxMemory,
137
+ HeapExhausted,
138
+ }
139
+
140
+ impl Interrupt {
141
+ const fn cause(self) -> TerminationCause {
142
+ match self {
143
+ Self::Stop => TerminationCause::Stop,
144
+ Self::Timeout(_) => TerminationCause::Timeout,
145
+ Self::Cancel(_) => TerminationCause::Cancelled,
146
+ Self::MaxMemory => TerminationCause::MaxMemory,
147
+ Self::HeapExhausted => TerminationCause::HeapExhausted,
148
+ }
149
+ }
150
+
151
+ const fn dispatch(self) -> Option<DispatchId> {
152
+ match self {
153
+ Self::Timeout(id) | Self::Cancel(id) => Some(id),
154
+ Self::Stop | Self::MaxMemory | Self::HeapExhausted => None,
155
+ }
156
+ }
157
+ }
158
+
159
+ /// What a forked child takes over with a context's termination state.
160
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
161
+ pub struct Inherited {
162
+ pending: CauseSet,
163
+ pub disposing: bool,
164
+ }
165
+
166
+ /// How many requests a cell had seen, taken as a dispatch's own work ends, so
167
+ /// `spend` leaves alone any that arrived after.
168
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
169
+ pub struct RequestMark(u64);
170
+
171
+ /// Why a dispatch must not report its own result.
172
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
173
+ pub enum Halt {
174
+ Terminated(TerminationCause),
175
+ Disposed,
176
+ }
177
+
178
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
179
+ pub enum TermError {
180
+ #[error("the context is being disposed")]
181
+ Disposing,
182
+ #[error("a dispatch is already armed")]
183
+ AlreadyArmed,
184
+ }
185
+
186
+ /// Stops and resumes the isolate's JavaScript from any thread. `terminate`
187
+ /// must also wake the runner if it sleeps waiting for tasks, or a stop during
188
+ /// an await would wait for the next task to be noticed.
189
+ pub trait Terminator {
190
+ fn terminate(&self);
191
+ fn cancel(&self);
192
+ }
193
+
194
+ #[derive(Clone, Copy)]
195
+ struct Armed {
196
+ id: DispatchId,
197
+ kind: DispatchKind,
198
+ terminated: bool,
199
+ // `settle` has given the verdict, and the result is being serialized.
200
+ settled: bool,
201
+ }
202
+
203
+ struct State<T> {
204
+ handle: Option<T>,
205
+ armed: Option<Armed>,
206
+ causes: CauseSet,
207
+ // A carried stop or memory cause is still to be reported, but V8 no
208
+ // longer holds its termination: mini_racer's request flag stays set
209
+ // after its one-shot interrupt is used up or dropped, so the next script
210
+ // runs and reports the cause when it ends.
211
+ spent: bool,
212
+ requests: u64,
213
+ disposing: bool,
214
+ suspended: bool,
215
+ // A script nested in an untimed dispatch, timed on its own.
216
+ nested_timer: Option<DispatchId>,
217
+ next: u64,
218
+ }
219
+
220
+ impl<T: Terminator> State<T> {
221
+ fn finish(&mut self, id: DispatchId) -> Option<Halt> {
222
+ match self.armed {
223
+ Some(armed) if armed.id == id && armed.settled => {}
224
+ _ => return self.end(id),
225
+ }
226
+ self.cancel_armed();
227
+ self.suspended = false;
228
+ self.nested_timer = None;
229
+ self.armed = None;
230
+ if self.disposing {
231
+ return Some(Halt::Disposed);
232
+ }
233
+ let causes = self.causes.carried();
234
+ self.set_causes(causes);
235
+ None
236
+ }
237
+
238
+ // The causes V8 still has a termination for.
239
+ const fn live(&self) -> CauseSet {
240
+ if self.spent {
241
+ self.causes.minus(CauseSet::SPENDABLE)
242
+ } else {
243
+ self.causes
244
+ }
245
+ }
246
+
247
+ const fn set_causes(&mut self, causes: CauseSet) {
248
+ self.causes = causes;
249
+ if causes.carried().is_empty() {
250
+ self.spent = false;
251
+ }
252
+ }
253
+
254
+ fn terminate_armed(&mut self) {
255
+ if self.suspended {
256
+ return;
257
+ }
258
+ if let (Some(armed), Some(handle)) = (&mut self.armed, &self.handle) {
259
+ handle.terminate();
260
+ armed.terminated = true;
261
+ }
262
+ }
263
+
264
+ fn cancel_armed(&mut self) {
265
+ if let (Some(armed), Some(handle)) = (&mut self.armed, &self.handle)
266
+ && armed.terminated
267
+ {
268
+ handle.cancel();
269
+ armed.terminated = false;
270
+ }
271
+ }
272
+
273
+ fn end(&mut self, id: DispatchId) -> Option<Halt> {
274
+ if self.armed.map(|armed| armed.id) != Some(id) {
275
+ return None;
276
+ }
277
+ self.cancel_armed();
278
+ self.suspended = false;
279
+ self.nested_timer = None;
280
+ let armed = self.armed.take()?;
281
+ if self.disposing {
282
+ return Some(Halt::Disposed);
283
+ }
284
+ let causes = self.causes;
285
+ self.set_causes(armed.kind.remaining(causes));
286
+ armed.kind.report(causes).map(Halt::Terminated)
287
+ }
288
+ }
289
+
290
+ /// A context's termination state.
291
+ ///
292
+ /// V8 is told to terminate only while an outermost dispatch is armed, and
293
+ /// `end` cancels that under the same lock, so a cause that arrives late is
294
+ /// either recorded for later or dropped, never left behind to kill the next
295
+ /// dispatch.
296
+ pub struct TermCell<T> {
297
+ state: Mutex<State<T>>,
298
+ }
299
+
300
+ impl<T> Default for TermCell<T> {
301
+ fn default() -> Self {
302
+ Self {
303
+ state: Mutex::new(State {
304
+ handle: None,
305
+ armed: None,
306
+ causes: CauseSet::default(),
307
+ spent: false,
308
+ requests: 0,
309
+ disposing: false,
310
+ suspended: false,
311
+ nested_timer: None,
312
+ next: 0,
313
+ }),
314
+ }
315
+ }
316
+ }
317
+
318
+ impl<T: Terminator> TermCell<T> {
319
+ pub fn install(&self, handle: T) {
320
+ lock(&self.state).handle = Some(handle);
321
+ }
322
+
323
+ /// Taken back before the isolate is dropped, so no later request reaches
324
+ /// a dead isolate.
325
+ pub fn uninstall(&self) -> Option<T> {
326
+ lock(&self.state).handle.take()
327
+ }
328
+
329
+ /// Arms an outermost dispatch; nested dispatches run inside it. A cause
330
+ /// still pending terminates it at once, as upstream's queued termination
331
+ /// kills the next entry into JavaScript of any kind, unless that
332
+ /// termination is spent.
333
+ pub fn begin(&self, kind: DispatchKind) -> Result<DispatchId, TermError> {
334
+ let mut state = lock(&self.state);
335
+ if state.disposing {
336
+ return Err(TermError::Disposing);
337
+ }
338
+ if state.armed.is_some() {
339
+ return Err(TermError::AlreadyArmed);
340
+ }
341
+ let id = DispatchId(state.next);
342
+ state.next = state.next.wrapping_add(1);
343
+ state.armed = Some(Armed {
344
+ id,
345
+ kind,
346
+ terminated: false,
347
+ settled: false,
348
+ });
349
+ if !state.live().is_empty() {
350
+ state.terminate_armed();
351
+ }
352
+ drop(state);
353
+ Ok(id)
354
+ }
355
+
356
+ /// Records `interrupt` and terminates the armed dispatch. Returns false
357
+ /// when the interrupt names a dispatch that is no longer armed.
358
+ pub fn request(&self, interrupt: Interrupt) -> bool {
359
+ let mut state = lock(&self.state);
360
+ if let Some(id) = interrupt.dispatch()
361
+ && state.armed.map(|armed| armed.id) != Some(id)
362
+ && !(matches!(interrupt, Interrupt::Timeout(_)) && state.nested_timer == Some(id))
363
+ {
364
+ return false;
365
+ }
366
+ state.causes = state.causes.with(interrupt.cause());
367
+ state.spent = false;
368
+ state.requests = state.requests.wrapping_add(1);
369
+ state.terminate_armed();
370
+ true
371
+ }
372
+
373
+ #[must_use]
374
+ pub fn mark(&self) -> RequestMark {
375
+ RequestMark(lock(&self.state).requests)
376
+ }
377
+
378
+ /// V8 used up a termination during work that ended at `since`, as
379
+ /// `mini_racer`'s attach or a microtask does with its one-shot interrupt:
380
+ /// a carried stop or memory cause is left for the next script to report,
381
+ /// and that script runs. A request after `since` keeps its own
382
+ /// termination. For a dispatch nested in a callback, the termination is
383
+ /// lifted so the script around it runs on, as in `mini_racer`, unless a
384
+ /// request arrived after `since`, a dispose began, the heap is exhausted,
385
+ /// which `mini_racer` has no counterpart for, or a timeout ended a script
386
+ /// that has its verdict: its leftover microtasks would then run with no
387
+ /// limit. Returns whether it lifted.
388
+ pub fn spend(&self, since: RequestMark, nested: bool) -> bool {
389
+ let mut state = lock(&self.state);
390
+ if state.requests != since.0 {
391
+ return false;
392
+ }
393
+ if !state.causes.carried().is_empty() {
394
+ state.spent = true;
395
+ }
396
+ let lift = nested
397
+ && !state.disposing
398
+ && !state.causes.contains(TerminationCause::HeapExhausted)
399
+ && (!state.causes.contains(TerminationCause::Timeout)
400
+ || state.armed.is_none_or(|armed| !armed.settled));
401
+ if lift {
402
+ state.cancel_armed();
403
+ }
404
+ lift
405
+ }
406
+
407
+ /// A single-threaded runner has locked its isolate again for a request,
408
+ /// and V8 drops every termination it had not acted on: the carried
409
+ /// causes are spent, as they are when `mini_racer` enters its locker.
410
+ pub fn relocked(&self) {
411
+ let mut state = lock(&self.state);
412
+ if !state.causes.carried().is_empty() {
413
+ state.spent = true;
414
+ }
415
+ }
416
+
417
+ /// A nested script that reports a cause hands the termination back to the
418
+ /// dispatch around it, as `mini_racer`'s `restore_termination` does, even
419
+ /// one an earlier dispatch had spent.
420
+ pub fn renew(&self) {
421
+ let mut state = lock(&self.state);
422
+ if !state.causes.is_empty() {
423
+ state.spent = false;
424
+ state.terminate_armed();
425
+ }
426
+ }
427
+
428
+ /// Whether a dispatch of `kind` must not start: V8 would end it at its
429
+ /// first step, or a dispose has begun. A spent cause lets it run.
430
+ #[must_use]
431
+ pub fn refuses(&self, kind: DispatchKind) -> bool {
432
+ let state = lock(&self.state);
433
+ state.disposing || kind.report(state.live()).is_some()
434
+ }
435
+
436
+ /// Takes over what a context's cell held in the parent of a fork.
437
+ pub fn adopt(&self, inherited: Inherited) {
438
+ lock(&self.state).causes = inherited.pending;
439
+ }
440
+
441
+ /// What a forked child takes over with the context: the causes pending
442
+ /// for the next script, and whether a dispose had begun. `None` while the
443
+ /// lock is held, which in a child means by a thread that is gone.
444
+ #[must_use]
445
+ pub fn inherited(&self) -> Option<Inherited> {
446
+ let state = self.state.try_lock().ok()?;
447
+ Some(Inherited {
448
+ pending: state.causes.intersect(CauseSet::PENDING),
449
+ disposing: state.disposing,
450
+ })
451
+ }
452
+
453
+ /// Marks the context as disposing: the armed dispatch, if any, is
454
+ /// terminated and halts with `Disposed`, and no new one can begin.
455
+ pub fn dispose(&self) -> bool {
456
+ let mut state = lock(&self.state);
457
+ state.disposing = true;
458
+ state.terminate_armed();
459
+ state.armed.is_some()
460
+ }
461
+
462
+ /// What a dispatch of `kind` must report instead of its result, without
463
+ /// consuming anything: runners ask before running script, and a nested
464
+ /// dispatch asks before replying, leaving the cause to its enclosing one.
465
+ #[must_use]
466
+ pub fn check(&self, kind: DispatchKind) -> Option<Halt> {
467
+ let state = lock(&self.state);
468
+ if state.disposing {
469
+ return Some(Halt::Disposed);
470
+ }
471
+ kind.report(state.causes).map(Halt::Terminated)
472
+ }
473
+
474
+ /// Lifts the armed dispatch's termination so a nested dispatch can encode
475
+ /// its reply. Causes that arrive meanwhile are recorded, and `resume`
476
+ /// terminates again if any are pending.
477
+ pub fn suspend(&self) {
478
+ let mut state = lock(&self.state);
479
+ state.suspended = true;
480
+ state.cancel_armed();
481
+ }
482
+
483
+ pub fn resume(&self) {
484
+ let mut state = lock(&self.state);
485
+ state.suspended = false;
486
+ if !state.causes.is_empty() || state.disposing {
487
+ state.terminate_armed();
488
+ }
489
+ }
490
+
491
+ /// Gives a script nested in an untimed dispatch (a pump or a microtask
492
+ /// checkpoint, through a callback) a timeout of its own, as `mini_racer`
493
+ /// arms its watchdog for any script when none is armed: an id the timeout
494
+ /// names. `None` when the armed dispatch is a script, whose own timeout
495
+ /// covers what it runs, or when an enclosing nested script has one.
496
+ pub fn time_nested(&self) -> Option<DispatchId> {
497
+ let mut state = lock(&self.state);
498
+ let armed = state.armed?;
499
+ if armed.kind == DispatchKind::Script || state.nested_timer.is_some() {
500
+ return None;
501
+ }
502
+ let id = DispatchId(state.next);
503
+ state.next = state.next.wrapping_add(1);
504
+ state.nested_timer = Some(id);
505
+ drop(state);
506
+ Some(id)
507
+ }
508
+
509
+ /// Ends a nested script's own timeout. One that fired is dropped, and the
510
+ /// dispatch around the script carries on unless another cause needs the
511
+ /// termination, as `mini_racer` lifts its watchdog's.
512
+ pub fn untime_nested(&self, id: DispatchId) {
513
+ let mut state = lock(&self.state);
514
+ if state.nested_timer != Some(id) {
515
+ return;
516
+ }
517
+ state.nested_timer = None;
518
+ if state.causes.contains(TerminationCause::Timeout) {
519
+ let causes = state.causes.without(TerminationCause::Timeout);
520
+ state.set_causes(causes);
521
+ if state.causes.is_empty() && !state.disposing {
522
+ state.cancel_armed();
523
+ }
524
+ }
525
+ }
526
+
527
+ /// Terminates the armed dispatch's script for an abandoned nested dispatch
528
+ /// without recording a cause, so the dispatch it runs in carries on once
529
+ /// `release` lifts it.
530
+ pub fn interrupt(&self) {
531
+ lock(&self.state).terminate_armed();
532
+ }
533
+
534
+ /// Lifts an `interrupt` once its nested dispatch has responded, unless a
535
+ /// recorded cause or a dispose still needs the termination.
536
+ pub fn release(&self) {
537
+ let mut state = lock(&self.state);
538
+ if state.causes.is_empty() && !state.disposing {
539
+ state.cancel_armed();
540
+ }
541
+ }
542
+
543
+ #[must_use]
544
+ pub fn is_disposing(&self) -> bool {
545
+ lock(&self.state).disposing
546
+ }
547
+
548
+ /// Whether the armed dispatch has its verdict (`settle`) and is finishing
549
+ /// its reply.
550
+ #[must_use]
551
+ pub fn is_settled(&self) -> bool {
552
+ lock(&self.state).armed.is_some_and(|armed| armed.settled)
553
+ }
554
+
555
+ /// The armed dispatch, for a cancellation from a nested wait.
556
+ #[must_use]
557
+ pub fn armed(&self) -> Option<DispatchId> {
558
+ lock(&self.state).armed.map(|armed| armed.id)
559
+ }
560
+
561
+ /// Disarms `id` and settles its outcome. The runner calls it after the
562
+ /// dispatch's last V8 work, and a halt it returns replaces that work's
563
+ /// result; a cause arriving later waits for the next dispatch. A no-op
564
+ /// returning `None` when `id` is not armed.
565
+ pub fn end(&self, id: DispatchId) -> Option<Halt> {
566
+ lock(&self.state).end(id)
567
+ }
568
+
569
+ /// `mini_racer`'s last check before an outermost dispatch serializes its
570
+ /// result: reports and consumes the causes so far, as `end` would, and
571
+ /// lifts the termination so the result can be serialized. The dispatch
572
+ /// stays armed, so a cause that lands meanwhile still stops a getter.
573
+ pub fn settle(&self, id: DispatchId) -> Option<Halt> {
574
+ let mut state = lock(&self.state);
575
+ let armed = state.armed.filter(|armed| armed.id == id)?;
576
+ if state.disposing {
577
+ return Some(Halt::Disposed);
578
+ }
579
+ state.cancel_armed();
580
+ let causes = state.causes;
581
+ state.set_causes(armed.kind.remaining(causes));
582
+ if let Some(armed) = &mut state.armed {
583
+ armed.settled = true;
584
+ }
585
+ drop(state);
586
+ armed.kind.report(causes).map(Halt::Terminated)
587
+ }
588
+
589
+ /// Disarms `id` once its reply is complete. After `settle`, a cause that
590
+ /// landed while the result serialized, without stopping it, leaves the
591
+ /// result alone: a timeout or cancel is dropped, and a stop or memory
592
+ /// cause waits for the next script, as `mini_racer` leaves them. A dispatch
593
+ /// that was never settled ends as `end` ends it.
594
+ pub fn finish(&self, id: DispatchId) -> Option<Halt> {
595
+ lock(&self.state).finish(id)
596
+ }
597
+
598
+ /// Disarms `id` once the leftover microtasks of its terminated script ran
599
+ /// without V8 acting on a cause. One still live fails `mini_racer`'s reply,
600
+ /// whose retry reports it with this script, as `end` does; with none, a
601
+ /// spent stop or memory cause is carried, as `finish` leaves it.
602
+ pub fn end_drained(&self, id: DispatchId) -> Option<Halt> {
603
+ let mut state = lock(&self.state);
604
+ if state.live().is_empty() {
605
+ state.finish(id)
606
+ } else {
607
+ state.end(id)
608
+ }
609
+ }
610
+ }
611
+
612
+ #[cfg(test)]
613
+ #[cfg(not(loom))]
614
+ mod tests {
615
+ use std::cell::Cell;
616
+ use std::rc::Rc;
617
+
618
+ use super::*;
619
+
620
+ #[derive(Default)]
621
+ struct Calls {
622
+ terminate: Cell<u32>,
623
+ cancel: Cell<u32>,
624
+ }
625
+
626
+ struct Probe(Rc<Calls>);
627
+
628
+ impl Terminator for Probe {
629
+ fn terminate(&self) {
630
+ self.0.terminate.set(self.0.terminate.get() + 1);
631
+ }
632
+
633
+ fn cancel(&self) {
634
+ self.0.cancel.set(self.0.cancel.get() + 1);
635
+ }
636
+ }
637
+
638
+ fn cell() -> (TermCell<Probe>, Rc<Calls>) {
639
+ let calls = Rc::new(Calls::default());
640
+ let cell = TermCell::default();
641
+ cell.install(Probe(Rc::clone(&calls)));
642
+ (cell, calls)
643
+ }
644
+
645
+ fn run(cell: &TermCell<Probe>, kind: DispatchKind) -> Option<Halt> {
646
+ let id = cell.begin(kind).expect("nothing armed");
647
+ cell.end(id)
648
+ }
649
+
650
+ const STOPPED: Option<Halt> = Some(Halt::Terminated(TerminationCause::Stop));
651
+
652
+ #[test]
653
+ fn a_stop_while_idle_is_reported_by_the_next_script_only() {
654
+ let (cell, calls) = cell();
655
+ assert!(cell.request(Interrupt::Stop));
656
+ assert_eq!(calls.terminate.get(), 0);
657
+
658
+ assert_eq!(run(&cell, DispatchKind::Script), STOPPED);
659
+ assert_eq!(run(&cell, DispatchKind::Script), None);
660
+ }
661
+
662
+ #[test]
663
+ fn a_pending_stop_terminates_the_next_dispatch_of_any_kind() {
664
+ let (cell, calls) = cell();
665
+ cell.request(Interrupt::Stop);
666
+ let id = cell
667
+ .begin(DispatchKind::Maintenance)
668
+ .expect("nothing armed");
669
+ assert_eq!(calls.terminate.get(), 1);
670
+
671
+ assert_eq!(cell.end(id), None);
672
+ assert_eq!(calls.cancel.get(), 1);
673
+ assert_eq!(run(&cell, DispatchKind::Script), STOPPED);
674
+ }
675
+
676
+ #[test]
677
+ fn a_stop_during_a_script_terminates_it_and_is_cancelled_at_end() {
678
+ let (cell, calls) = cell();
679
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
680
+ cell.request(Interrupt::Stop);
681
+ assert_eq!(calls.terminate.get(), 1);
682
+
683
+ assert_eq!(cell.end(id), STOPPED);
684
+ assert_eq!(calls.cancel.get(), 1);
685
+ assert_eq!(cell.check(DispatchKind::Script), None);
686
+ }
687
+
688
+ #[test]
689
+ fn a_nested_script_reports_a_stop_and_leaves_it_pending() {
690
+ let (cell, _) = cell();
691
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
692
+ cell.request(Interrupt::Stop);
693
+
694
+ assert_eq!(cell.check(DispatchKind::Script), STOPPED);
695
+ assert_eq!(cell.check(DispatchKind::Pump), None);
696
+ assert_eq!(cell.check(DispatchKind::Maintenance), None);
697
+ assert_eq!(cell.end(id), STOPPED);
698
+ }
699
+
700
+ #[test]
701
+ fn an_interrupt_for_a_nested_dispatch_is_lifted_and_reports_nothing() {
702
+ let (cell, calls) = cell();
703
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
704
+ cell.interrupt();
705
+ assert_eq!(calls.terminate.get(), 1);
706
+ assert_eq!(cell.check(DispatchKind::Script), None);
707
+ cell.release();
708
+ assert_eq!(calls.cancel.get(), 1);
709
+ assert_eq!(cell.end(id), None);
710
+ }
711
+
712
+ #[test]
713
+ fn a_cause_recorded_meanwhile_keeps_the_interrupt() {
714
+ let (cell, calls) = cell();
715
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
716
+ cell.interrupt();
717
+ cell.request(Interrupt::Stop);
718
+ cell.release();
719
+ assert_eq!(calls.cancel.get(), 0);
720
+ assert_eq!(cell.end(id), STOPPED);
721
+ }
722
+
723
+ #[test]
724
+ fn a_pump_consumes_a_stop_silently() {
725
+ let (cell, _) = cell();
726
+ cell.request(Interrupt::Stop);
727
+
728
+ assert_eq!(run(&cell, DispatchKind::Pump), None);
729
+ assert_eq!(run(&cell, DispatchKind::Script), None);
730
+ }
731
+
732
+ #[test]
733
+ fn maintenance_leaves_a_stop_pending() {
734
+ let (cell, _) = cell();
735
+ let id = cell
736
+ .begin(DispatchKind::Maintenance)
737
+ .expect("nothing armed");
738
+ cell.request(Interrupt::Stop);
739
+ assert_eq!(cell.check(DispatchKind::Maintenance), None);
740
+ assert_eq!(cell.end(id), None);
741
+
742
+ assert_eq!(run(&cell, DispatchKind::Script), STOPPED);
743
+ }
744
+
745
+ #[test]
746
+ fn a_timeout_after_its_dispatch_ended_is_dropped() {
747
+ let (cell, calls) = cell();
748
+ let first = cell.begin(DispatchKind::Script).expect("nothing armed");
749
+ cell.end(first);
750
+ let second = cell.begin(DispatchKind::Script).expect("nothing armed");
751
+
752
+ assert!(!cell.request(Interrupt::Timeout(first)));
753
+ assert_eq!(calls.terminate.get(), 0);
754
+ assert_eq!(cell.end(second), None);
755
+ }
756
+
757
+ #[test]
758
+ fn a_timeout_does_not_outlive_its_dispatch() {
759
+ let (cell, _) = cell();
760
+ let id = cell
761
+ .begin(DispatchKind::Maintenance)
762
+ .expect("nothing armed");
763
+ assert!(cell.request(Interrupt::Timeout(id)));
764
+ assert_eq!(cell.end(id), None);
765
+
766
+ assert_eq!(run(&cell, DispatchKind::Script), None);
767
+ }
768
+
769
+ #[test]
770
+ fn a_cancel_clears_every_cause() {
771
+ let (cell, _) = cell();
772
+ cell.request(Interrupt::Stop);
773
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
774
+ cell.request(Interrupt::Cancel(id));
775
+
776
+ assert_eq!(
777
+ cell.end(id),
778
+ Some(Halt::Terminated(TerminationCause::Cancelled))
779
+ );
780
+ assert_eq!(run(&cell, DispatchKind::Script), None);
781
+ }
782
+
783
+ #[test]
784
+ fn check_and_end_agree_on_the_cause() {
785
+ let (cell, _) = cell();
786
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
787
+ cell.request(Interrupt::Cancel(id));
788
+ cell.request(Interrupt::MaxMemory);
789
+ let memory = Some(Halt::Terminated(TerminationCause::MaxMemory));
790
+
791
+ assert_eq!(cell.check(DispatchKind::Script), memory);
792
+ assert_eq!(cell.end(id), memory);
793
+ assert_eq!(run(&cell, DispatchKind::Script), None);
794
+ }
795
+
796
+ #[test]
797
+ fn memory_causes_outrank_a_stop() {
798
+ let (cell, _) = cell();
799
+ cell.request(Interrupt::Stop);
800
+ cell.request(Interrupt::MaxMemory);
801
+
802
+ assert_eq!(
803
+ run(&cell, DispatchKind::Script),
804
+ Some(Halt::Terminated(TerminationCause::MaxMemory))
805
+ );
806
+ }
807
+
808
+ #[test]
809
+ fn a_suspended_dispatch_is_terminated_again_on_resume() {
810
+ let (cell, calls) = cell();
811
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
812
+ cell.request(Interrupt::Stop);
813
+ cell.suspend();
814
+ assert_eq!(calls.cancel.get(), 1);
815
+
816
+ cell.request(Interrupt::Stop);
817
+ assert_eq!(calls.terminate.get(), 1);
818
+ cell.resume();
819
+ assert_eq!(calls.terminate.get(), 2);
820
+ assert_eq!(cell.end(id), STOPPED);
821
+ assert_eq!(calls.cancel.get(), 2);
822
+ }
823
+
824
+ #[test]
825
+ fn resume_without_causes_leaves_the_dispatch_running() {
826
+ let (cell, calls) = cell();
827
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
828
+ cell.suspend();
829
+ cell.resume();
830
+
831
+ assert_eq!(calls.terminate.get(), 0);
832
+ assert_eq!(cell.end(id), None);
833
+ }
834
+
835
+ #[test]
836
+ fn disposing_halts_the_armed_dispatch_and_refuses_new_ones() {
837
+ let (cell, calls) = cell();
838
+ let id = cell
839
+ .begin(DispatchKind::Maintenance)
840
+ .expect("nothing armed");
841
+ assert!(cell.dispose());
842
+ assert_eq!(calls.terminate.get(), 1);
843
+ assert_eq!(cell.check(DispatchKind::Maintenance), Some(Halt::Disposed));
844
+
845
+ assert_eq!(cell.end(id), Some(Halt::Disposed));
846
+ assert_eq!(calls.cancel.get(), 1);
847
+ assert_eq!(cell.begin(DispatchKind::Script), Err(TermError::Disposing));
848
+ }
849
+
850
+ #[test]
851
+ fn one_outermost_dispatch_at_a_time() {
852
+ let (cell, _) = cell();
853
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
854
+
855
+ assert_eq!(
856
+ cell.begin(DispatchKind::Script),
857
+ Err(TermError::AlreadyArmed)
858
+ );
859
+ assert_eq!(cell.armed(), Some(id));
860
+ cell.end(id);
861
+ assert_eq!(cell.end(id), None);
862
+ assert_eq!(cell.armed(), None);
863
+ }
864
+
865
+ #[test]
866
+ fn nothing_is_cancelled_when_nothing_was_terminated() {
867
+ let (cell, calls) = cell();
868
+ run(&cell, DispatchKind::Script);
869
+
870
+ assert_eq!(calls.cancel.get(), 0);
871
+ }
872
+
873
+ #[test]
874
+ fn settling_reports_and_consumes_what_came_before_serializing() {
875
+ let (cell, calls) = cell();
876
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
877
+ cell.request(Interrupt::Stop);
878
+
879
+ assert_eq!(cell.settle(id), STOPPED);
880
+ assert_eq!(calls.cancel.get(), 1);
881
+ assert_eq!(cell.armed(), Some(id));
882
+ assert_eq!(cell.end(id), None);
883
+ assert_eq!(run(&cell, DispatchKind::Script), None);
884
+ }
885
+
886
+ #[test]
887
+ fn a_timeout_while_serializing_leaves_the_result_and_the_next_dispatch() {
888
+ let (cell, calls) = cell();
889
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
890
+ assert_eq!(cell.settle(id), None);
891
+ assert!(cell.request(Interrupt::Timeout(id)));
892
+ assert_eq!(calls.terminate.get(), 1);
893
+
894
+ assert_eq!(cell.finish(id), None);
895
+ assert_eq!(calls.cancel.get(), 1);
896
+ assert_eq!(run(&cell, DispatchKind::Script), None);
897
+ }
898
+
899
+ #[test]
900
+ fn a_stop_while_serializing_is_reported_by_the_next_script() {
901
+ let (cell, _) = cell();
902
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
903
+ assert_eq!(cell.settle(id), None);
904
+ cell.request(Interrupt::Stop);
905
+
906
+ assert_eq!(cell.finish(id), None);
907
+ assert_eq!(run(&cell, DispatchKind::Script), STOPPED);
908
+ }
909
+
910
+ #[test]
911
+ fn a_cause_that_stops_the_serializing_is_reported_by_end() {
912
+ let (cell, _) = cell();
913
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
914
+ assert_eq!(cell.settle(id), None);
915
+ cell.request(Interrupt::Timeout(id));
916
+
917
+ assert_eq!(
918
+ cell.end(id),
919
+ Some(Halt::Terminated(TerminationCause::Timeout))
920
+ );
921
+ assert_eq!(run(&cell, DispatchKind::Script), None);
922
+ }
923
+
924
+ #[test]
925
+ fn a_dispose_while_serializing_still_halts_the_dispatch() {
926
+ let (cell, _) = cell();
927
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
928
+ assert_eq!(cell.settle(id), None);
929
+ cell.dispose();
930
+
931
+ assert_eq!(cell.finish(id), Some(Halt::Disposed));
932
+ }
933
+
934
+ #[test]
935
+ fn finishing_an_unsettled_dispatch_ends_it() {
936
+ let (cell, _) = cell();
937
+ cell.request(Interrupt::Stop);
938
+ let id = cell.begin(DispatchKind::Pump).expect("nothing armed");
939
+
940
+ assert_eq!(cell.finish(id), None);
941
+ assert_eq!(run(&cell, DispatchKind::Script), None);
942
+ }
943
+
944
+ #[test]
945
+ fn a_script_nested_in_an_untimed_dispatch_is_timed_on_its_own() {
946
+ let (cell, calls) = cell();
947
+ let pump = cell.begin(DispatchKind::Pump).expect("nothing armed");
948
+ let nested = cell.time_nested().expect("an untimed dispatch");
949
+ assert_eq!(cell.time_nested(), None);
950
+
951
+ assert!(cell.request(Interrupt::Timeout(nested)));
952
+ assert_eq!(calls.terminate.get(), 1);
953
+ assert_eq!(
954
+ cell.check(DispatchKind::Script),
955
+ Some(Halt::Terminated(TerminationCause::Timeout))
956
+ );
957
+ cell.untime_nested(nested);
958
+ assert_eq!(calls.cancel.get(), 1);
959
+ assert_eq!(cell.check(DispatchKind::Script), None);
960
+ assert!(!cell.request(Interrupt::Timeout(nested)));
961
+ assert_eq!(cell.end(pump), None);
962
+ }
963
+
964
+ #[test]
965
+ fn a_nested_timeout_keeps_a_stop_that_came_with_it() {
966
+ let (cell, calls) = cell();
967
+ let checkpoint = cell
968
+ .begin(DispatchKind::Maintenance)
969
+ .expect("nothing armed");
970
+ let nested = cell.time_nested().expect("an untimed dispatch");
971
+ cell.request(Interrupt::Timeout(nested));
972
+ cell.request(Interrupt::Stop);
973
+
974
+ cell.untime_nested(nested);
975
+ assert_eq!(calls.cancel.get(), 0);
976
+ assert_eq!(cell.check(DispatchKind::Script), STOPPED);
977
+ assert_eq!(cell.end(checkpoint), None);
978
+ assert_eq!(run(&cell, DispatchKind::Script), STOPPED);
979
+ }
980
+
981
+ #[test]
982
+ fn a_script_inside_a_script_is_covered_by_the_outer_timeout() {
983
+ let (cell, _) = cell();
984
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
985
+ assert_eq!(cell.time_nested(), None);
986
+ assert_eq!(cell.end(id), None);
987
+ assert_eq!(cell.time_nested(), None);
988
+ }
989
+
990
+ #[test]
991
+ fn a_child_takes_over_a_stop_and_a_memory_cause_but_no_timeout() {
992
+ let (idle, _) = cell();
993
+ idle.request(Interrupt::Stop);
994
+ idle.request(Interrupt::MaxMemory);
995
+ let inherited = idle.inherited().expect("unlocked");
996
+ assert!(!inherited.disposing);
997
+ let (child, calls) = cell();
998
+ child.adopt(inherited);
999
+ assert!(child.refuses(DispatchKind::Script));
1000
+ assert_eq!(
1001
+ run(&child, DispatchKind::Script),
1002
+ Some(Halt::Terminated(TerminationCause::MaxMemory))
1003
+ );
1004
+ assert_eq!(calls.terminate.get(), 1);
1005
+ assert_eq!(run(&child, DispatchKind::Script), None);
1006
+
1007
+ let (timed, _) = cell();
1008
+ let id = timed.begin(DispatchKind::Script).expect("nothing armed");
1009
+ timed.request(Interrupt::Timeout(id));
1010
+ let (child, _) = cell();
1011
+ child.adopt(timed.inherited().expect("unlocked"));
1012
+ assert_eq!(run(&child, DispatchKind::Script), None);
1013
+ }
1014
+
1015
+ #[test]
1016
+ fn a_child_spends_an_inherited_stop_when_it_locks_its_isolate() {
1017
+ let (parent, _) = cell();
1018
+ parent.request(Interrupt::Stop);
1019
+ let (child, calls) = cell();
1020
+ child.adopt(parent.inherited().expect("unlocked"));
1021
+ child.relocked();
1022
+ assert!(!child.refuses(DispatchKind::Script));
1023
+ assert_eq!(run(&child, DispatchKind::Script), STOPPED);
1024
+ assert_eq!(calls.terminate.get(), 0);
1025
+ }
1026
+
1027
+ #[test]
1028
+ fn a_dispose_or_an_exhausted_heap_is_never_lifted() {
1029
+ let (exhausted, calls) = cell();
1030
+ exhausted.request(Interrupt::Stop);
1031
+ exhausted.request(Interrupt::HeapExhausted);
1032
+ let outer = exhausted
1033
+ .begin(DispatchKind::Script)
1034
+ .expect("nothing armed");
1035
+ assert!(!exhausted.spend(exhausted.mark(), true));
1036
+ assert!(exhausted.refuses(DispatchKind::Script));
1037
+ assert_eq!(calls.cancel.get(), 0);
1038
+ assert!(exhausted.end(outer).is_some());
1039
+
1040
+ let (disposed, calls) = cell();
1041
+ let outer = disposed.begin(DispatchKind::Script).expect("nothing armed");
1042
+ assert!(disposed.dispose());
1043
+ assert!(!disposed.spend(disposed.mark(), true));
1044
+ assert_eq!(calls.cancel.get(), 0);
1045
+ assert_eq!(disposed.end(outer), Some(Halt::Disposed));
1046
+ }
1047
+
1048
+ // mini_racer's attach uses up the one-shot interrupt a stop left, and its
1049
+ // request flag makes the next script report the stop when it ends.
1050
+ #[test]
1051
+ fn a_spent_stop_lets_the_next_script_run_and_report_it() {
1052
+ let (cell, calls) = cell();
1053
+ cell.request(Interrupt::Stop);
1054
+ let since = cell.mark();
1055
+ let attach = cell
1056
+ .begin(DispatchKind::Maintenance)
1057
+ .expect("nothing armed");
1058
+ assert_eq!(calls.terminate.get(), 1);
1059
+ assert!(!cell.spend(since, false));
1060
+ assert_eq!(cell.end(attach), None);
1061
+
1062
+ assert!(!cell.refuses(DispatchKind::Maintenance));
1063
+ assert_eq!(run(&cell, DispatchKind::Maintenance), None);
1064
+ assert!(!cell.refuses(DispatchKind::Script));
1065
+ let script = cell.begin(DispatchKind::Script).expect("nothing armed");
1066
+ assert_eq!(calls.terminate.get(), 1);
1067
+ assert_eq!(cell.settle(script), STOPPED);
1068
+ assert_eq!(cell.finish(script), None);
1069
+ assert_eq!(run(&cell, DispatchKind::Script), None);
1070
+ }
1071
+
1072
+ #[test]
1073
+ fn a_stop_after_a_spent_one_terminates_the_next_dispatch() {
1074
+ let (cell, calls) = cell();
1075
+ cell.request(Interrupt::Stop);
1076
+ cell.spend(cell.mark(), false);
1077
+ cell.request(Interrupt::Stop);
1078
+ assert!(cell.refuses(DispatchKind::Script));
1079
+ assert_eq!(run(&cell, DispatchKind::Script), STOPPED);
1080
+ assert_eq!(calls.terminate.get(), 1);
1081
+ }
1082
+
1083
+ // A request that lands while the dispatch runs keeps its own termination,
1084
+ // as upstream's later TerminateExecution stays pending.
1085
+ #[test]
1086
+ fn a_request_since_the_mark_is_neither_spent_nor_lifted() {
1087
+ let (cell, calls) = cell();
1088
+ let outer = cell.begin(DispatchKind::Script).expect("nothing armed");
1089
+ let since = cell.mark();
1090
+ cell.request(Interrupt::Stop);
1091
+ assert!(!cell.spend(since, true));
1092
+ assert!(cell.refuses(DispatchKind::Script));
1093
+ assert_eq!(calls.cancel.get(), 0);
1094
+ assert_eq!(cell.end(outer), STOPPED);
1095
+ }
1096
+
1097
+ // Inside a callback the termination a nested dispatch used up is lifted,
1098
+ // so the script around it runs on and reports the cause when it ends.
1099
+ #[test]
1100
+ fn a_nested_spend_lifts_the_termination_for_the_script_around() {
1101
+ let (cell, calls) = cell();
1102
+ let outer = cell.begin(DispatchKind::Script).expect("nothing armed");
1103
+ cell.request(Interrupt::Stop);
1104
+ assert!(cell.spend(cell.mark(), true));
1105
+ assert_eq!(calls.cancel.get(), 1);
1106
+ assert!(!cell.refuses(DispatchKind::Script));
1107
+ assert_eq!(cell.check(DispatchKind::Script), STOPPED);
1108
+ assert_eq!(cell.end(outer), STOPPED);
1109
+ assert_eq!(calls.cancel.get(), 1);
1110
+ }
1111
+
1112
+ #[test]
1113
+ fn a_settled_dispatch_with_a_timeout_is_never_lifted() {
1114
+ let (cell, calls) = cell();
1115
+ let outer = cell.begin(DispatchKind::Script).expect("nothing armed");
1116
+ cell.request(Interrupt::Timeout(outer));
1117
+ assert_eq!(
1118
+ cell.settle(outer),
1119
+ Some(Halt::Terminated(TerminationCause::Timeout))
1120
+ );
1121
+ assert!(cell.is_settled());
1122
+ cell.request(Interrupt::Timeout(outer));
1123
+ let cancels = calls.cancel.get();
1124
+ assert!(!cell.spend(cell.mark(), true));
1125
+ assert_eq!(calls.cancel.get(), cancels);
1126
+ assert_eq!(cell.finish(outer), None);
1127
+ assert!(!cell.is_settled());
1128
+ }
1129
+
1130
+ // A getter running while a finished script's result serializes, or a
1131
+ // leftover microtask, runs on in mini_racer once a nested attach uses up
1132
+ // a stop, and the next script reports it.
1133
+ #[test]
1134
+ fn a_settled_dispatch_lifts_a_spent_stop() {
1135
+ let (cell, calls) = cell();
1136
+ let outer = cell.begin(DispatchKind::Script).expect("nothing armed");
1137
+ assert_eq!(cell.settle(outer), None);
1138
+ cell.request(Interrupt::Stop);
1139
+ assert!(cell.spend(cell.mark(), true));
1140
+ assert_eq!(calls.cancel.get(), 1);
1141
+ assert_eq!(cell.finish(outer), None);
1142
+ assert!(!cell.refuses(DispatchKind::Script));
1143
+ assert_eq!(run(&cell, DispatchKind::Script), STOPPED);
1144
+ }
1145
+
1146
+ #[test]
1147
+ fn a_drained_script_carries_a_spent_stop_and_reports_a_live_one() {
1148
+ let (cell, _) = cell();
1149
+ let outer = cell.begin(DispatchKind::Script).expect("nothing armed");
1150
+ cell.request(Interrupt::Stop);
1151
+ assert_eq!(cell.settle(outer), STOPPED);
1152
+ cell.request(Interrupt::Stop);
1153
+ cell.spend(cell.mark(), true);
1154
+ assert_eq!(cell.end_drained(outer), None);
1155
+ assert_eq!(run(&cell, DispatchKind::Script), STOPPED);
1156
+
1157
+ let outer = cell.begin(DispatchKind::Script).expect("nothing armed");
1158
+ cell.request(Interrupt::Stop);
1159
+ assert_eq!(cell.settle(outer), STOPPED);
1160
+ cell.request(Interrupt::Stop);
1161
+ assert_eq!(cell.end_drained(outer), STOPPED);
1162
+ assert_eq!(run(&cell, DispatchKind::Script), None);
1163
+ }
1164
+
1165
+ #[test]
1166
+ fn a_nested_spend_lifts_a_timeout_and_keeps_reporting_it() {
1167
+ let (cell, calls) = cell();
1168
+ let outer = cell.begin(DispatchKind::Script).expect("nothing armed");
1169
+ cell.request(Interrupt::Timeout(outer));
1170
+ assert!(cell.spend(cell.mark(), true));
1171
+ assert_eq!(calls.cancel.get(), 1);
1172
+ assert_eq!(
1173
+ cell.end(outer),
1174
+ Some(Halt::Terminated(TerminationCause::Timeout))
1175
+ );
1176
+ }
1177
+
1178
+ // V8 drops every termination it had not acted on when a single-threaded
1179
+ // runner locks its isolate again, however the cause arrived.
1180
+ #[test]
1181
+ fn relocking_spends_every_carried_stop_or_memory_cause() {
1182
+ let (cell, calls) = cell();
1183
+ cell.request(Interrupt::Stop);
1184
+ cell.relocked();
1185
+ assert!(!cell.refuses(DispatchKind::Script));
1186
+ assert_eq!(run(&cell, DispatchKind::Script), STOPPED);
1187
+ assert_eq!(calls.terminate.get(), 0);
1188
+
1189
+ let heap = cell
1190
+ .begin(DispatchKind::Maintenance)
1191
+ .expect("nothing armed");
1192
+ cell.request(Interrupt::MaxMemory);
1193
+ assert_eq!(calls.terminate.get(), 1);
1194
+ assert_eq!(cell.end(heap), None);
1195
+ cell.relocked();
1196
+ assert!(!cell.refuses(DispatchKind::Script));
1197
+ assert_eq!(
1198
+ run(&cell, DispatchKind::Script),
1199
+ Some(Halt::Terminated(TerminationCause::MaxMemory))
1200
+ );
1201
+
1202
+ cell.request(Interrupt::HeapExhausted);
1203
+ cell.relocked();
1204
+ assert!(cell.refuses(DispatchKind::Script));
1205
+ }
1206
+
1207
+ // mini_racer's restore_termination: a nested script that reports a stop
1208
+ // terminates the script around it again.
1209
+ #[test]
1210
+ fn renew_hands_a_spent_stop_back_to_the_dispatch_around() {
1211
+ let (cell, calls) = cell();
1212
+ cell.request(Interrupt::Stop);
1213
+ cell.spend(cell.mark(), false);
1214
+ let outer = cell.begin(DispatchKind::Script).expect("nothing armed");
1215
+ assert_eq!(calls.terminate.get(), 0);
1216
+ assert_eq!(cell.check(DispatchKind::Script), STOPPED);
1217
+ cell.renew();
1218
+ assert_eq!(calls.terminate.get(), 1);
1219
+ assert!(cell.refuses(DispatchKind::Script));
1220
+ assert_eq!(cell.end(outer), STOPPED);
1221
+ assert_eq!(calls.cancel.get(), 1);
1222
+ }
1223
+
1224
+ #[test]
1225
+ fn without_a_handle_causes_are_still_recorded() {
1226
+ let (cell, calls) = cell();
1227
+ cell.uninstall();
1228
+ let id = cell.begin(DispatchKind::Script).expect("nothing armed");
1229
+ cell.request(Interrupt::Stop);
1230
+
1231
+ assert_eq!(calls.terminate.get(), 0);
1232
+ assert_eq!(cell.end(id), STOPPED);
1233
+ }
1234
+ }