1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//! Regression: a panic inside the Cranelift JIT (most notably the AArch64
//! near-call relocation assertion in cranelift-jit 0.116,
//! `compiled_blob.rs:90` — `(diff >> 26 == -1) || (diff >> 26 == 0)`) must
//! be caught and surfaced as a stderr breadcrumb + engine fallback, not a
//! process crash.
//!
//! Hard repro of the AArch64 bug is non-deterministic and platform-specific
//! (depends on JIT code-cache vs runtime memory layout), so these tests use
//! the debug-build env-var hook `ILO_FORCE_JIT_PANIC=1` which raises a
//! synthetic panic at the same call site. The release binary does not have
//! the hook — the `cfg(debug_assertions)` guard trips it out — so this
//! cannot affect production users.
//!
//! Coverage: explicit `--jit` falls back to the bytecode VM, since the
//! user opted into a JIT engine and VM is the closest non-JIT tier.
//! (Post-#390 the default engine is the bytecode VM, so the default path
//! never invokes the JIT and the synthetic-panic hook cannot fire on it.)
//!
//! Gated on `cfg(debug_assertions)`: the env-var hook in
//! `vm::jit_cranelift::check_force_panic_env` is only compiled in debug
//! builds. In a release-mode test the hook is absent, so the synthetic
//! panic never fires and the assertions would misfire.
#![cfg(debug_assertions)]
use std::process::Command;
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
// Note: pre-#390 there was a `cranelift_panic_default_falls_back_to_interpreter`
// test here. Post-#390 the default engine is the bytecode VM and never invokes
// the JIT, so the synthetic-panic hook cannot fire on the default path. The
// `--jit` opt-in path is covered by
// `cranelift_panic_explicit_engine_falls_back_to_vm` below.
#[test]
fn cranelift_panic_explicit_engine_falls_back_to_vm() {
let out = ilo()
.args(["--jit", "f x:n>n;*x 2", "f", "5"])
.env("ILO_FORCE_JIT_PANIC", "1")
.output()
.expect("failed to run ilo");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"--jit should fall back to VM after JIT panic. \
stdout={stdout:?} stderr={stderr:?} status={:?}",
out.status.code()
);
assert!(
stdout.trim() == "10",
"VM fallback should produce f(5)=10, got stdout={stdout:?}"
);
assert!(
stderr.contains("Cranelift JIT panicked"),
"stderr breadcrumb missing, got {stderr:?}"
);
assert!(
stderr.contains("falling back to bytecode VM"),
"explicit-cranelift breadcrumb should mention VM fallback, got {stderr:?}"
);
}
/// The breadcrumb must include the panic payload so the upstream issue
/// (AArch64 relocation assertion, etc.) is searchable in production logs
/// rather than being collapsed into a generic message. Exercised via the
/// `--jit` opt-in path since the default engine no longer invokes the JIT.
#[test]
fn cranelift_panic_breadcrumb_includes_payload() {
let out = ilo()
.args(["--jit", "f x:n>n;*x 2", "f", "5"])
.env("ILO_FORCE_JIT_PANIC", "1")
.output()
.expect("failed to run ilo");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(out.status.success(), "fallback should succeed");
assert!(
stderr.contains("ILO_FORCE_JIT_PANIC") || stderr.contains("synthetic cranelift panic"),
"breadcrumb should include the panic payload, got {stderr:?}"
);
}