How to Fix: Cranelift: `opt_level=speed` breaks `sdiv i64::MIN, i64::MIN`
Cranelift opt_level=speed miscompiles sdiv i64::MIN, i64::MIN: why it returns -1 instead of 1 and how to fix it
This bug is a classic signed-division optimization regression: under opt_level=speed and opt_level=speed_and_size, Cranelift can transform sdiv i64::MIN, i64::MIN into code that produces -1 even though the mathematically and semantically correct result is 1. The problem is not the interpreter or the input itself; it is an incorrect optimization or lowering path that mishandles a critical signed integer edge case.
Reproducing the failure
The issue appears when the optimizer runs in speed-focused modes. A minimal CLIF test should validate both the interpreter and compiled execution path so the regression is visible during optimization.
test interpret
test run
set opt_level=speed
function %main() -> i64 {
block0:
v0 = iconst.i64 0x8000000000000000
v1 = sdiv v0, v0
return v1
}
; expected: 1
You should also test the size-focused variant:
test interpret
test run
set opt_level=speed_and_size
function %main() -> i64 {
block0:
v0 = iconst.i64 0x8000000000000000
v1 = sdiv v0, v0
return v1
}
; expected: 1
If the compiled result is -1 while the interpreter or unoptimized path returns 1, you are looking at a backend optimization bug rather than invalid IR.
Understanding the Root Cause
Signed division has several dangerous corner cases, and i64::MIN is the most notorious one. In two’s-complement arithmetic, i64::MIN is -9223372036854775808, represented as 0x8000000000000000. Unlike most negative numbers, it cannot be negated into a positive value within the same signed 64-bit range.
That matters because many optimized division strategies rely on algebraic rewrites, sign extraction, absolute-value transforms, or target-specific instruction selection. If an optimization assumes identities like “divide magnitudes, then restore the sign” without preserving the special behavior of MIN, it can derive the wrong sign bit or fold the result incorrectly.
For sdiv i64::MIN, i64::MIN, the correct answer is straightforward:
(-2^63) / (-2^63) = 1
So how does -1 appear? Typically one of these things is happening:
- A sign computation rewrite incorrectly concludes the result must be negative.
- An absolute-value based transform overflows or becomes invalid for
i64::MIN. - A pattern-matching optimization for division or comparison folds this case using assumptions that are true for most values but false for
MIN. - A backend lowering rule emits target instructions whose sign behavior is correct in general but not after an earlier illegal canonicalization.
In short, the bug happens because the optimizer treats a rare but legal signed division input as if it followed ordinary sign-normalization rules. i64::MIN is not an ordinary signed value; any pass that rewrites division around negation, sign masks, or absolute values must handle it explicitly.
Step-by-Step Solution
The fix is to prevent the optimizer or lowering stage from applying an invalid transform when either operand may be i64::MIN, especially when both operands are equal. In practice, the safest repair is to preserve exact signed-division semantics for this pattern instead of relying on a shortcut.
1. Add a focused regression test
Before touching optimization code, lock the behavior down with a dedicated test.
test interpret
test run
set opt_level=speed
function %main() -> i64 {
block0:
v0 = iconst.i64 0x8000000000000000
v1 = sdiv v0, v0
return v1
}
; expect: 1
Also add the same test for speed_and_size so both optimization modes are covered.
test interpret
test run
set opt_level=speed_and_size
function %main() -> i64 {
block0:
v0 = iconst.i64 0x8000000000000000
v1 = sdiv v0, v0
return v1
}
; expect: 1
2. Inspect the optimization pipeline for sdiv rewrites
Look for transformations in Cranelift that:
- replace
sdiv x, xwith a constant, - compute the sign of a division using XOR or sign bits,
- convert signed division into unsigned division over normalized magnitudes,
- fold or simplify division when operands are known equal.
A suspicious rewrite might look conceptually like this:
// Pseudocode: unsafe unless special cases are preserved
if lhs == rhs {
return -1 // incorrect for signed division equality case
}
The only valid simplification for sdiv x, x is 1 when x != 0. If the optimizer tries to infer the sign from operand sign bits and accidentally treats equal negative operands as producing a negative result, it will return -1, which matches the bug report.
3. Guard all sign-normalization logic for MIN
If the implementation uses absolute values, negation, or sign restoration, add an explicit guard.
// Pseudocode
fn safe_sdiv_i64(lhs: i64, rhs: i64) -> i64 {
assert!(rhs != 0);
if lhs == i64::MIN && rhs == i64::MIN {
return 1;
}
// Existing optimized path only if proven safe.
// Avoid abs(lhs) or abs(rhs) when value may be MIN.
perform_regular_signed_division(lhs, rhs)
}
If the bug lives in a rewrite pass rather than execution logic, disable the rewrite unless it is semantically airtight:
// Pseudocode for an optimizer rule
match inst {
sdiv(lhs, rhs) if lhs == rhs => {
// valid only when rhs != 0 and semantics are preserved
replace_with(1)
}
_ => {}
}
Even then, the rewrite must not bypass traps or UB rules if the IR semantics require division-by-zero handling first. In Cranelift IR, ensure the fold is legal in the exact context where it is applied.
4. Prefer semantic correctness over aggressive folding
If you cannot prove the transform is valid for all signed 64-bit inputs, do not apply it in speed modes. A tiny missed optimization is far cheaper than a silent miscompile.
// Better: keep the original sdiv until a fully correct lowering path handles it
v1 = sdiv v0, v0
This is especially important in compiler backends, where a single wrong fold can invalidate generated machine code across all consumers.
5. Verify across all optimization modes
Run the full matrix after the patch:
opt_level=none
opt_level=speed
opt_level=speed_and_size
And validate both interpreter and compiled execution where applicable.
# Example workflow
cargo test
# run Cranelift filetests that include the new .clif regression
# verify no backend-specific regressions appear
6. Add nearby tests for related signed-division boundaries
Do not stop at one input. Add tests for values that stress the same optimization family.
; i64::MIN / 1 == i64::MIN
; i64::MIN / -1 == overflow-sensitive special case
; i64::MIN / i64::MIN == 1
; -5 / -5 == 1
; 5 / 5 == 1
; 0 / 1 == 0
This helps ensure the final patch fixes the real class of bug instead of papering over one constant pattern.
Common Edge Cases
- Division by zero: Any fold like
sdiv x, x -> 1is invalid ifxcould be zero and the IR requires preserving trap behavior or explicit failure semantics. i64::MIN / -1overflow: This is another famous signed-division edge case. A patch that handlesMIN / MINbut breaksMIN / -1is incomplete.- Cross-target lowering differences: The bug may reproduce only on certain ISAs if the bad assumption appears in target-specific instruction selection rather than generic optimization.
- Constant folding vs runtime lowering: The interpreter may be correct while generated machine code is wrong, which means you need coverage for both compile-time simplification and backend emission.
- Width-specific regressions: If one optimization is shared across integer widths, inspect
i8,i16,i32, andi128behavior too, especially their correspondingMINvalues.
FAQ
Why is i64::MIN such a special value in compiler bugs?
Because in two’s-complement representation, i64::MIN has no positive counterpart within the same type. Operations like negation or absolute value can overflow or become non-representable, so optimizations that are safe for other negatives can fail here.
Why does this happen only with opt_level=speed or speed_and_size?
Those modes enable more aggressive optimization passes and lowering shortcuts. The unoptimized path often preserves the original division semantics, while optimized modes try to simplify or rewrite the operation and expose the bug.
What is the safest long-term fix?
The safest fix is to correct or remove the invalid signed-division transform, then add regression tests for MIN-related cases across all optimization levels and execution modes. In compiler code, proven correctness must come before micro-optimizations.
The key takeaway is simple: sdiv i64::MIN, i64::MIN must evaluate to 1. If Cranelift returns -1 under optimized builds, a signed-division rewrite is violating integer semantics and should be guarded, corrected, or eliminated with targeted regression coverage.