1
//! Tier 3 architectural spike (ADR-0026): confirm the wasm
2
//! exception-handling proposal behaves as the `(handler-case)` /
3
//! `(unwind-protect)` design assumes, *before* any codegen lands.
4
//!
5
//! Load-bearing properties verified:
6
//!
7
//! 1. **GC liveness across a catch edge — guest-allocated.** A
8
//!    `(ref $cond)` struct from `struct.new`, held in a local across a
9
//!    `try_table`, is readable after the catch.
10
//! 2. **GC liveness across a catch edge — host-allocated.** A ref
11
//!    produced by the runtime's `alloc_string_ref` host helper (the
12
//!    same path `(handler-case)` will use to build condition objects),
13
//!    held in a wasm local across the catch edge, survives the unwind
14
//!    and downcasts cleanly afterward. Finding from the 3.1 review:
15
//!    guest-local survival alone doesn't prove the host-ref path.
16
//! 3. **`OutOfFuel` bypasses both `catch_all` and a tagged `catch`.**
17
//!    Engine traps are not wasm exceptions, so neither catch shape
18
//!    swallows them — the deadline budget stays non-catchable.
19
//! 4. **`EpochInterrupt` bypasses `try_table`** — the second engine
20
//!    deadline mechanism is non-catchable too, not just fuel.
21

            
22
use scripting::runtime::{
23
    EngineError, EngineOpts, alloc_string_ref, build_engine, classify_runtime_error,
24
};
25
use wasmtime::{Caller, FuncType, HeapType, Linker, Module, RefType, Store, Val, ValType};
26

            
27
/// Guest-allocated `$cond` struct (field 42) stored in a local before a
28
/// `try_table`, read after the `$err` catch. Proves guest-local GC
29
/// rooting across the unwind.
30
const GUEST_LIVENESS_WAT: &str = r#"
31
(module
32
  (type $cond (struct (field i32)))
33
  (tag $err)
34

            
35
  (func (export "probe") (result i32)
36
    (local $c (ref null $cond))
37
    (local.set $c (struct.new $cond (i32.const 42)))
38
    (block $handler
39
      (try_table (catch $err $handler)
40
        (throw $err))
41
    )
42
    (struct.get $cond 0 (local.get $c)))
43
)
44
"#;
45

            
46
/// Host-allocated arrayref (from `make_ref`) stored in an `anyref`
47
/// local before a `try_table`, downcast + measured after the catch.
48
/// Proves the host-produced ref path — the one `(handler-case)` builds
49
/// condition objects on — survives the catch edge.
50
const HOST_LIVENESS_WAT: &str = r#"
51
(module
52
  (import "spike" "make_ref" (func $make_ref (result anyref)))
53
  (tag $err)
54

            
55
  (func (export "probe") (result i32)
56
    (local $r anyref)
57
    (local.set $r (call $make_ref))
58
    (block $handler
59
      (try_table (catch $err $handler)
60
        (throw $err))
61
    )
62
    (array.len (ref.cast (ref array) (local.get $r))))
63
)
64
"#;
65

            
66
/// Infinite loop wrapped in `try_table (catch_all ...)`. The catch
67
/// label targets a no-result block (the proposal requires `catch_all`
68
/// labels to take no values); reaching it would mean the trap was
69
/// wrongly swallowed, so the handler returns sentinel `1`.
70
const BUSY_CATCH_ALL_WAT: &str = r#"
71
(module
72
  (func (export "burn") (result i32)
73
    (block $handler
74
      (try_table (catch_all $handler)
75
        (loop $l (br $l)))
76
      (return (i32.const 0)))
77
    (i32.const 1)))
78
"#;
79

            
80
/// Infinite loop wrapped in a *tagged* `try_table (catch $err ...)`.
81
/// A tagged catch must not swallow engine traps any more than
82
/// `catch_all` does.
83
const BUSY_TAGGED_CATCH_WAT: &str = r#"
84
(module
85
  (tag $err)
86
  (func (export "burn") (result i32)
87
    (block $handler
88
      (try_table (catch $err $handler)
89
        (loop $l (br $l)))
90
      (return (i32.const 0)))
91
    (i32.const 1)))
92
"#;
93

            
94
/// Condition-object lifetime (3.2/3.3 load-bearing). A payload-carrying
95
/// tag `(tag $err (param (ref $cond)))` throws a struct; the catch
96
/// delivers it as a `$handler` block param; we store it to a local, run
97
/// MORE `struct.new` allocations and a nested inner `try_table`/throw,
98
/// THEN read both fields of the original condition. If the caught ref
99
/// were dropped or moved by the unwind or the later allocations, the
100
/// final reads would fault — so a return of `code*1000 + msg` proves the
101
/// condition object survives exactly the way `(handler-case)` needs.
102
const CONDITION_LIFETIME_WAT: &str = r#"
103
(module
104
  (type $cond (struct (field $code i32) (field $msg i32)))
105
  (tag $err (param (ref $cond)))
106
  (tag $inner)
107

            
108
  (func (export "probe") (result i32)
109
    (local $caught (ref null $cond))
110
    (block $outer (result (ref $cond))
111
      (try_table (result (ref $cond)) (catch $err $outer)
112
        (throw $err (struct.new $cond (i32.const 7) (i32.const 9))))
113
      ;; normal exit unreachable — body always throws
114
      (unreachable))
115
    ;; $outer delivers the caught (ref $cond) on the stack
116
    (local.set $caught)
117
    ;; churn the GC: more allocations after the catch
118
    (drop (struct.new $cond (i32.const 1) (i32.const 2)))
119
    (drop (struct.new $cond (i32.const 3) (i32.const 4)))
120
    ;; a fully-nested throw/catch between catch and the final read
121
    (block $inner_h
122
      (try_table (catch $inner $inner_h)
123
        (throw $inner)))
124
    ;; original condition must still be intact
125
    (i32.add
126
      (i32.mul (struct.get $cond $code (local.get $caught)) (i32.const 1000))
127
      (struct.get $cond $msg (local.get $caught))))
128
)
129
"#;
130

            
131
/// The exact 3-frame boundary-wrapper shape the compiler will emit for a
132
/// VALUED body (i32 result, like `should_apply`): `block $exit (result
133
/// i32)` → `block $handler (result (ref $cond))` → `try_table (result
134
/// i32) (catch $err $handler)`. A `br` out of a nested inner block inside
135
/// the body mimics RETURN-FROM / GO: it must resolve through the +3
136
/// wrapper depth to the right target. On the throw path the handler reads
137
/// the condition and returns a sentinel; the normal path returns the
138
/// body's i32. Returns 11 (body via inner br), proving inner labels
139
/// resolve under the wrapper.
140
const BOUNDARY_VALUED_WAT: &str = r#"
141
(module
142
  (type $cond (struct (field $code i32) (field $msg i32)))
143
  (tag $err (param (ref $cond)))
144

            
145
  (func (export "probe") (result i32)
146
    (block $exit (result i32)
147
      (block $handler (result (ref $cond))
148
        (try_table (result i32) (catch $err $handler)
149
          ;; body: an inner block whose `br` jumps to $exit through the
150
          ;; try_table + handler frames (mimics RETURN-FROM target math)
151
          (block $inner (result i32)
152
            (br $exit (i32.const 11))))
153
        ;; normal try_table completion carries the body's i32 out and
154
        ;; skips the handler tail — the boundary wrapper MUST emit this
155
        ;; `br $exit` after the try_table closes.
156
        (br $exit))
157
      ;; $handler: condition ref on stack → sentinel
158
      (drop)
159
      (i32.const 99))))
160
"#;
161

            
162
/// The void variant of the boundary wrapper (no result arity, like
163
/// `process`). `block $exit` and `try_table` carry no result type; the
164
/// body falls through; the catch path drops the condition. Proves the
165
/// void shape validates and the catch→fallthrough typing is well-formed.
166
/// Exported as `() -> ()`; reaching the end without trap is success.
167
const BOUNDARY_VOID_WAT: &str = r#"
168
(module
169
  (type $cond (struct (field $code i32) (field $msg i32)))
170
  (tag $err (param (ref $cond)))
171

            
172
  (func (export "probe")
173
    (block $exit
174
      (block $handler (result (ref $cond))
175
        (try_table (catch $err $handler)
176
          ;; body: no value, just falls through to br $exit
177
          (br $exit))
178
        ;; normal completion skips the handler tail
179
        (br $exit))
180
      ;; $handler: drop the condition, fall through
181
      (drop))))
182
"#;
183

            
184
/// The exact `(handler-case)` lowering shape (Tier 3.3): `Catch::OneRef`
185
/// delivers BOTH the condition ref AND an `exnref` to a TWO-result handler
186
/// block (`(result (ref $cond) exnref)`), so an unmatched clause can
187
/// `throw_ref` the ORIGINAL exception. Dispatch is FLAT — each clause is an
188
/// independent `if` that `br`s to `$outer` on match, so every clause body
189
/// sits at the same depth (no nested-if depth drift). The SECOND clause's
190
/// body performs an outer `br $outer` (mimicking RETURN-FROM from a
191
/// non-first clause), proving relative-br depth is correct through the flat
192
/// dispatch. Body throws code 2; clause 1 matches code 1 (skipped), clause 2
193
/// matches code 2 → returns 22 via `br $outer`. Proves OneRef + functype
194
/// handler block + flat dispatch + outer-br all validate and run.
195
const HANDLER_CASE_MATCH_WAT: &str = r#"
196
(module
197
  (type $cond (struct (field $code i32) (field $msg i32)))
198
  (tag $err (param (ref $cond)))
199

            
200
  (func (export "probe") (result i32)
201
    (local $c (ref null $cond))
202
    (local $e exnref)
203
    (block $outer (result i32)
204
      (block $handler (result (ref $cond) exnref)
205
        (try_table (result i32) (catch_ref $err $handler)
206
          (throw $err (struct.new $cond (i32.const 2) (i32.const 0))))
207
        ;; normal completion carries the body i32 out, skips the handler
208
        (br $outer))
209
      ;; catch edge: stack = [(ref $cond), exnref]
210
      (local.set $e)
211
      (local.set $c)
212
      ;; clause 1: code == 1 ?
213
      (if (i32.eq (struct.get $cond $code (local.get $c)) (i32.const 1))
214
        (then (br $outer (i32.const 11))))
215
      ;; clause 2: code == 2 ?  (non-first clause doing an outer br)
216
      (if (i32.eq (struct.get $cond $code (local.get $c)) (i32.const 2))
217
        (then (br $outer (i32.const 22))))
218
      ;; no match, no `t`: re-raise the ORIGINAL exception
219
      (throw_ref (local.get $e)))))
220
"#;
221

            
222
/// Same shape, but the body throws code 9 which matches NO clause and there
223
/// is no `t` catch-all → the handler `throw_ref`s the original exnref, which
224
/// escapes the function uncaught. Proves the re-raise path: the call must
225
/// return `Err` (an uncaught exception), not a value.
226
const HANDLER_CASE_RERAISE_WAT: &str = r#"
227
(module
228
  (type $cond (struct (field $code i32) (field $msg i32)))
229
  (tag $err (param (ref $cond)))
230

            
231
  (func (export "probe") (result i32)
232
    (local $c (ref null $cond))
233
    (local $e exnref)
234
    (block $outer (result i32)
235
      (block $handler (result (ref $cond) exnref)
236
        (try_table (result i32) (catch_ref $err $handler)
237
          (throw $err (struct.new $cond (i32.const 9) (i32.const 0))))
238
        (br $outer))
239
      (local.set $e)
240
      (local.set $c)
241
      (if (i32.eq (struct.get $cond $code (local.get $c)) (i32.const 1))
242
        (then (br $outer (i32.const 11))))
243
      (throw_ref (local.get $e)))))
244
"#;
245

            
246
/// Simpler `(handler-case)` lowering: re-raise on no-match by re-`throw`ing a
247
/// FRESH `$nomi_error` carrying the SAME condition (nomiscript has no
248
/// exception identity — conditions are value-like code+message), so we avoid
249
/// `Catch::OneRef` / exnref / a 2-value functype handler block entirely. The
250
/// catch is the proven `Catch::One` single-value shape (same as the 3.2
251
/// boundary wrapper). Flat dispatch; second clause matches code 2 via outer
252
/// `br $outer` → returns 22.
253
const HANDLER_CASE_ONE_RETHROW_WAT: &str = r#"
254
(module
255
  (type $cond (struct (field $code i32) (field $msg i32)))
256
  (tag $err (param (ref $cond)))
257

            
258
  (func (export "probe") (result i32)
259
    (local $c (ref null $cond))
260
    (block $outer (result i32)
261
      (block $handler (result (ref $cond))
262
        (try_table (result i32) (catch $err $handler)
263
          (throw $err (struct.new $cond (i32.const 2) (i32.const 0))))
264
        (br $outer))
265
      ;; catch edge: stack = [(ref $cond)]
266
      (local.set $c)
267
      (if (i32.eq (struct.get $cond $code (local.get $c)) (i32.const 1))
268
        (then (br $outer (i32.const 11))))
269
      (if (i32.eq (struct.get $cond $code (local.get $c)) (i32.const 2))
270
        (then (br $outer (i32.const 22))))
271
      ;; no match: re-throw a fresh $err carrying the SAME condition.
272
      ;; `$c` is nullable; the tag param is non-null → ref.as_non_null.
273
      (throw $err (ref.as_non_null (local.get $c))))))
274
"#;
275

            
276
/// Re-throw variant where code 9 matches nothing and there's no `t` → the
277
/// handler re-`throw`s the same condition, which escapes uncaught.
278
const HANDLER_CASE_ONE_RETHROW_ESCAPE_WAT: &str = r#"
279
(module
280
  (type $cond (struct (field $code i32) (field $msg i32)))
281
  (tag $err (param (ref $cond)))
282

            
283
  (func (export "probe") (result i32)
284
    (local $c (ref null $cond))
285
    (block $outer (result i32)
286
      (block $handler (result (ref $cond))
287
        (try_table (result i32) (catch $err $handler)
288
          (throw $err (struct.new $cond (i32.const 9) (i32.const 0))))
289
        (br $outer))
290
      (local.set $c)
291
      (if (i32.eq (struct.get $cond $code (local.get $c)) (i32.const 1))
292
        (then (br $outer (i32.const 11))))
293
      (throw $err (ref.as_non_null (local.get $c))))))
294
"#;
295

            
296
#[test]
297
1
fn handler_case_catch_one_rethrow_matches_non_first_clause() {
298
1
    assert_eq!(
299
1
        run_i32_probe(HANDLER_CASE_ONE_RETHROW_WAT, "handler-case-one-rethrow"),
300
        22,
301
        "Catch::One + flat dispatch must run the matching (non-first) clause; \
302
         re-raise via a fresh throw of the same condition needs no exnref"
303
    );
304
1
}
305

            
306
#[test]
307
1
fn handler_case_catch_one_rethrow_escapes_on_no_match() {
308
1
    let engine = build_engine(EngineOpts::baseline()).expect("build engine");
309
1
    let module = Module::new(&engine, HANDLER_CASE_ONE_RETHROW_ESCAPE_WAT)
310
1
        .expect("compile handler-case-one-rethrow-escape");
311
1
    let mut store: Store<()> = Store::new(&engine, ());
312
1
    store.set_epoch_deadline(1_000_000);
313
1
    let instance = Linker::<()>::new(&engine)
314
1
        .instantiate(&mut store, &module)
315
1
        .expect("instantiate");
316
1
    let probe = instance
317
1
        .get_func(&mut store, "probe")
318
1
        .expect("probe export");
319
1
    let mut results = [Val::I32(0)];
320
1
    probe
321
1
        .call(&mut store, &[], &mut results)
322
1
        .expect_err("unmatched handler-case with no catch-all must re-raise");
323
1
}
324

            
325
#[test]
326
1
fn handler_case_oneref_dispatch_matches_non_first_clause() {
327
1
    assert_eq!(
328
1
        run_i32_probe(HANDLER_CASE_MATCH_WAT, "handler-case-match"),
329
        22,
330
        "OneRef two-value handler + flat dispatch must run the matching \
331
         (non-first) clause and carry its value out via br $outer"
332
    );
333
1
}
334

            
335
#[test]
336
1
fn handler_case_no_match_reraises_original_exception() {
337
1
    let engine = build_engine(EngineOpts::baseline()).expect("build engine");
338
1
    let module =
339
1
        Module::new(&engine, HANDLER_CASE_RERAISE_WAT).expect("compile handler-case-reraise");
340
1
    let mut store: Store<()> = Store::new(&engine, ());
341
1
    store.set_epoch_deadline(1_000_000);
342
1
    let instance = Linker::<()>::new(&engine)
343
1
        .instantiate(&mut store, &module)
344
1
        .expect("instantiate");
345
1
    let probe = instance
346
1
        .get_func(&mut store, "probe")
347
1
        .expect("probe export");
348
1
    let mut results = [Val::I32(0)];
349
1
    let outcome = probe.call(&mut store, &[], &mut results);
350
1
    outcome.expect_err("an unmatched handler-case with no catch-all must re-raise (throw_ref)");
351
1
}
352

            
353
#[test]
354
1
fn gc_struct_local_survives_catch_edge() {
355
1
    let engine = build_engine(EngineOpts::baseline()).expect("build engine");
356
1
    let module = Module::new(&engine, GUEST_LIVENESS_WAT).expect("compile guest-liveness module");
357

            
358
1
    let mut store: Store<()> = Store::new(&engine, ());
359
1
    store.set_epoch_deadline(1_000_000);
360

            
361
1
    let instance = Linker::<()>::new(&engine)
362
1
        .instantiate(&mut store, &module)
363
1
        .expect("instantiate");
364
1
    let probe = instance
365
1
        .get_func(&mut store, "probe")
366
1
        .expect("probe export");
367

            
368
1
    let mut results = [Val::I32(0)];
369
1
    probe
370
1
        .call(&mut store, &[], &mut results)
371
1
        .expect("probe call");
372
1
    assert_eq!(
373
1
        results[0].i32().expect("i32 result"),
374
        42,
375
        "guest GC struct local must survive the try_table catch edge"
376
    );
377
1
}
378

            
379
#[test]
380
1
fn host_allocated_ref_survives_catch_edge() {
381
1
    let engine = build_engine(EngineOpts::baseline()).expect("build engine");
382
1
    let module = Module::new(&engine, HOST_LIVENESS_WAT).expect("compile host-liveness module");
383

            
384
1
    let mut store: Store<()> = Store::new(&engine, ());
385
1
    store.set_epoch_deadline(1_000_000);
386

            
387
1
    let mut linker: Linker<()> = Linker::new(&engine);
388
1
    let make_ref_ty = FuncType::new(
389
1
        &engine,
390
1
        [],
391
1
        [ValType::Ref(RefType::new(true, HeapType::Any))],
392
    );
393
1
    linker
394
1
        .func_new(
395
1
            "spike",
396
1
            "make_ref",
397
1
            make_ref_ty,
398
1
            |mut caller: Caller<'_, ()>, _args, results| {
399
1
                let array = alloc_string_ref(&mut caller, b"hello")?;
400
1
                results[0] = Val::AnyRef(Some(array.to_anyref()));
401
1
                Ok(())
402
1
            },
403
        )
404
1
        .expect("register make_ref");
405

            
406
1
    let instance = linker
407
1
        .instantiate(&mut store, &module)
408
1
        .expect("instantiate");
409
1
    let probe = instance
410
1
        .get_func(&mut store, "probe")
411
1
        .expect("probe export");
412

            
413
1
    let mut results = [Val::I32(0)];
414
1
    probe
415
1
        .call(&mut store, &[], &mut results)
416
1
        .expect("probe call");
417
1
    assert_eq!(
418
1
        results[0].i32().expect("i32 result"),
419
        5,
420
        "host-allocated arrayref must survive the catch edge and downcast (len of \"hello\")"
421
    );
422
1
}
423

            
424
#[test]
425
1
fn out_of_fuel_bypasses_catch_all() {
426
1
    assert_trap_bypasses_catch(BUSY_CATCH_ALL_WAT, TrapKind::Fuel);
427
1
}
428

            
429
#[test]
430
1
fn out_of_fuel_bypasses_tagged_catch() {
431
1
    assert_trap_bypasses_catch(BUSY_TAGGED_CATCH_WAT, TrapKind::Fuel);
432
1
}
433

            
434
#[test]
435
1
fn epoch_interrupt_bypasses_catch_all() {
436
1
    assert_trap_bypasses_catch(BUSY_CATCH_ALL_WAT, TrapKind::Epoch);
437
1
}
438

            
439
/// Runs a no-import `() -> i32` `probe` export and returns its result.
440
/// Shared by the condition-lifetime and valued-boundary spikes.
441
4
fn run_i32_probe(wat: &str, what: &str) -> i32 {
442
4
    let engine = build_engine(EngineOpts::baseline()).expect("build engine");
443
4
    let module = Module::new(&engine, wat).unwrap_or_else(|e| panic!("compile {what}: {e}"));
444
4
    let mut store: Store<()> = Store::new(&engine, ());
445
4
    store.set_epoch_deadline(1_000_000);
446
4
    let instance = Linker::<()>::new(&engine)
447
4
        .instantiate(&mut store, &module)
448
4
        .expect("instantiate");
449
4
    let probe = instance
450
4
        .get_func(&mut store, "probe")
451
4
        .expect("probe export");
452
4
    let mut results = [Val::I32(0)];
453
4
    probe
454
4
        .call(&mut store, &[], &mut results)
455
4
        .unwrap_or_else(|e| panic!("{what} call: {e}"));
456
4
    results[0].i32().expect("i32 result")
457
4
}
458

            
459
#[test]
460
1
fn caught_condition_object_survives_later_allocations() {
461
1
    assert_eq!(
462
1
        run_i32_probe(CONDITION_LIFETIME_WAT, "condition-lifetime"),
463
        7009,
464
        "caught (ref $cond) must survive store-to-local + later allocations + nested catch \
465
         (code 7 * 1000 + msg 9)"
466
    );
467
1
}
468

            
469
#[test]
470
1
fn valued_boundary_wrapper_inner_branch_resolves() {
471
1
    assert_eq!(
472
1
        run_i32_probe(BOUNDARY_VALUED_WAT, "boundary-valued"),
473
        11,
474
        "inner br must resolve to $exit through the 3-frame valued boundary wrapper"
475
    );
476
1
}
477

            
478
#[test]
479
1
fn void_boundary_wrapper_validates_and_runs() {
480
1
    let engine = build_engine(EngineOpts::baseline()).expect("build engine");
481
1
    let module = Module::new(&engine, BOUNDARY_VOID_WAT).expect("compile boundary-void");
482
1
    let mut store: Store<()> = Store::new(&engine, ());
483
1
    store.set_epoch_deadline(1_000_000);
484
1
    let instance = Linker::<()>::new(&engine)
485
1
        .instantiate(&mut store, &module)
486
1
        .expect("instantiate");
487
1
    let probe = instance
488
1
        .get_func(&mut store, "probe")
489
1
        .expect("probe export");
490
1
    probe
491
1
        .call(&mut store, &[], &mut [])
492
1
        .expect("void boundary probe must validate and run to completion");
493
1
}
494

            
495
#[derive(Clone, Copy)]
496
enum TrapKind {
497
    Fuel,
498
    Epoch,
499
}
500

            
501
/// Runs `wat`'s `burn` export under the given deadline mechanism and
502
/// asserts the resulting trap escapes the `try_table` rather than being
503
/// caught. Both fuel and epoch budgets must stay non-catchable.
504
3
fn assert_trap_bypasses_catch(wat: &str, kind: TrapKind) {
505
3
    let engine = build_engine(EngineOpts::baseline().with_fuel()).expect("build engine");
506
3
    let module = Module::new(&engine, wat).expect("compile busy module");
507

            
508
3
    let mut store: Store<()> = Store::new(&engine, ());
509
3
    match kind {
510
2
        TrapKind::Fuel => {
511
2
            store.set_epoch_deadline(1_000_000);
512
2
            store.set_fuel(10_000).expect("set fuel");
513
2
        }
514
1
        TrapKind::Epoch => {
515
1
            store.set_fuel(1_000_000_000).expect("set fuel");
516
1
            store.set_epoch_deadline(1);
517
1
            engine.increment_epoch();
518
1
            engine.increment_epoch();
519
1
        }
520
    }
521

            
522
3
    let instance = Linker::<()>::new(&engine)
523
3
        .instantiate(&mut store, &module)
524
3
        .expect("instantiate");
525
3
    let burn = instance.get_func(&mut store, "burn").expect("burn export");
526

            
527
3
    let mut results = [Val::I32(0)];
528
3
    let outcome = burn.call(&mut store, &[], &mut results);
529
3
    let err = outcome.expect_err("burn must trap, not be caught");
530
3
    assert!(
531
        matches!(
532
3
            classify_runtime_error(&err),
533
            EngineError::OutOfFuel | EngineError::EpochInterrupt
534
        ),
535
        "engine trap must bypass the catch, got {:?}",
536
        classify_runtime_error(&err)
537
    );
538
3
}