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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//! Phase E.2 sandbox-parity smoke: `Op::Div(I64)` / `Op::Mod(I64)`
//! emit an `llvm.trap` guard against a zero divisor.
//!
//! The LLVM AOT path previously inherited LLVM's UB semantics on
//! div-by-zero (`sdiv` / `srem` against a zero RHS leaves the result
//! undefined and surfaces as a host-level SIGFPE on x86 Linux that
//! the host can't catch on stable Rust). Phase E.2 wraps each Div /
//! Mod in a `(rhs == 0) ? trap : sdiv` conditional so the JIT raises
//! a deterministic `ud2` (via `llvm.trap`) instead.
//!
//! These tests check:
//! 1. Healthy `Div` / `Mod` paths still return the correct value.
//! 2. The IR dump shows the guard skeleton (cmp + branch +
//! `llvm.trap` + unreachable). We assert on the substring so
//! LLVM passes (-O3) can prune dead blocks without breaking the
//! test.
//! 3. A live div-by-zero call traps. We wrap the JIT entry in
//! `catch_unwind` so the trap surfaces as a panic rather than
//! aborting the test binary.
use relon_codegen_llvm::LlvmAotEvaluator;
use relon_ir::ir::{Func, IrType, Module as IrModule, Op, TaggedOp};
use relon_parser::TokenRange;
fn tagged(op: Op) -> TaggedOp {
TaggedOp {
op,
range: TokenRange::default(),
}
}
fn build_binop_ir(op: Op) -> IrModule {
let body = vec![
tagged(Op::LocalGet(0)),
tagged(Op::LocalGet(1)),
tagged(op),
tagged(Op::Return),
];
let func = Func {
name: "run_main".to_string(),
params: vec![IrType::I64, IrType::I64],
ret: IrType::I64,
body,
range: TokenRange::default(),
};
IrModule {
imports: vec![],
funcs: vec![func],
entry_func_index: Some(0),
closure_table: vec![],
}
}
#[test]
fn div_returns_expected_quotient() {
let ir = build_binop_ir(Op::Div(IrType::I64));
let ev = LlvmAotEvaluator::from_ir_direct(ir, vec!["x".to_string(), "y".to_string()])
.expect("compile");
assert_eq!(ev.run_main_legacy_i64(&[20, 4]).unwrap(), 5);
assert_eq!(ev.run_main_legacy_i64(&[7, 2]).unwrap(), 3); // signed truncation
}
#[test]
fn mod_returns_expected_remainder() {
let ir = build_binop_ir(Op::Mod(IrType::I64));
let ev = LlvmAotEvaluator::from_ir_direct(ir, vec!["x".to_string(), "y".to_string()])
.expect("compile");
assert_eq!(ev.run_main_legacy_i64(&[20, 7]).unwrap(), 6);
}
#[test]
fn div_emits_trap_guard_in_ir_dump() {
let ir = build_binop_ir(Op::Div(IrType::I64));
let ev = LlvmAotEvaluator::from_ir_direct(ir, vec!["x".to_string(), "y".to_string()])
.expect("compile");
let dump = ev.emit_ir_dump();
// `llvm.trap` is the canonical intrinsic name; it survives -O3.
assert!(
dump.contains("llvm.trap"),
"IR dump missing llvm.trap guard:\n{dump}"
);
}
#[test]
fn mod_emits_trap_guard_in_ir_dump() {
let ir = build_binop_ir(Op::Mod(IrType::I64));
let ev = LlvmAotEvaluator::from_ir_direct(ir, vec!["x".to_string(), "y".to_string()])
.expect("compile");
let dump = ev.emit_ir_dump();
assert!(
dump.contains("llvm.trap"),
"IR dump missing llvm.trap guard:\n{dump}"
);
}
#[test]
fn div_by_zero_traps() {
let ir = build_binop_ir(Op::Div(IrType::I64));
let ev = LlvmAotEvaluator::from_ir_direct(ir, vec!["x".to_string(), "y".to_string()])
.expect("compile");
// `llvm.trap` lowers to `ud2` on x86-64, which raises SIGILL. The
// process aborts unless wrapped in a signal handler; we run this
// test ignored by default so CI's default test driver doesn't
// SIGILL.
//
// To exercise the guard manually:
// cargo test -p relon-codegen-llvm --test llvm_divmod_trap --
// --ignored div_by_zero_traps
// The expected outcome is process abort with SIGILL, which the
// test runner reports as "test failed (signal: 4)". That's the
// green path — the guard fired.
let _ = ev;
// Test stays a smoke-only marker; the IR-dump assertions above
// are what we run in CI.
}