How to Fix: Cranelift: `iabs.i128` not implemented on AArch64
Cranelift AArch64 Fix: Implementing iabs.i128 Without Backend Support
The failure is not in your .clif test syntax; it comes from a real backend gap: AArch64 in Cranelift does not implement iabs.i128, so legalization or instruction selection reaches an operation the target cannot lower directly. The correct fix is to expand the absolute-value operation into primitive integer instructions that already exist for 128-bit values on AArch64.
Understanding the Root Cause
In Cranelift, IR instructions such as iabs may be handled in one of three ways depending on the target:
- Matched directly to a machine instruction.
- Lowered through backend-specific code.
- Legalized into simpler operations before lowering.
For smaller integer widths, an absolute value can often be lowered efficiently. But i128 on AArch64 is special: standard AArch64 does not provide a single native scalar instruction for 128-bit signed absolute value. Cranelift therefore needs either:
- a custom lowering sequence for 128-bit absolute value, or
- a legalization rule that rewrites
iabs.i128into supported primitive operations.
The bug appears because that path is missing. When the backend sees this test case:
function %iabs_i128(i128) -> i128 {
block0(v0: i128):
v1 = iabs.i128 v0
return v1
}
the compiler reaches an operation that has no AArch64 implementation. The robust solution is to rewrite:
abs(x) = x >= 0 ? x : -x
For signed integers, this can also be expressed branchlessly as:
mask = x >> 127
result = (x ^ mask) - mask
That form is typically ideal for legalization because it depends only on operations Cranelift already knows how to represent for 128-bit integers, such as arithmetic shift, xor, and sub.
Step-by-Step Solution
The cleanest fix is to add a legalization or lowering rule so that iabs.i128 never reaches AArch64 as an unsupported node.
1. Reproduce the failure
Use the failing .clif test and run Cranelift tests for the AArch64 target.
test interpret
test run
target aarch64
function %iabs_i128(i128) -> i128 {
block0(v0: i128):
v1 = iabs.i128 v0
return v1
}
If your local checkout includes filetests, run the relevant suite and confirm the backend reports that iabs.i128 is not implemented or cannot be lowered.
2. Decide where to fix it
In Cranelift, this kind of issue is usually best fixed in one of these layers:
- Legalization: transform unsupported IR into supported IR before backend lowering.
- ISLE/lowering: teach the AArch64 backend how to lower the operation.
For portability and maintainability, legalization is usually the better choice here because the transformation is target-independent and valid for any backend lacking direct support.
3. Expand iabs.i128 into primitive operations
Implement the following logic:
mask = sshr_imm x, 127
xorv = bxor x, mask
res = isub xorv, mask
This works because:
- if
x >= 0, thenmask = 0, so result is(x ^ 0) - 0 = x - if
x < 0, thenmask = -1, so result is(x ^ -1) - (-1) = ~x + 1 = -x
A conceptual legalization helper might look like this:
fn expand_iabs_i128(pos: &mut FuncCursor, val: Value) -> Value {
let mask = pos.ins().sshr_imm(val, 127);
let flipped = pos.ins().bxor(val, mask);
pos.ins().isub(flipped, mask)
}
If your Cranelift tree uses a legalization pattern table rather than a Rust helper in that exact form, apply the same transformation in the appropriate legalization pass.
4. Wire the expansion into the legalization path
Locate the place where iabs is legalized or lowered. Add a special case for i128 when the target does not provide direct support.
match opcode {
Opcode::Iabs if ty == types::I128 => {
let x = inst_args[0];
let mask = pos.ins().sshr_imm(x, 127);
let flipped = pos.ins().bxor(x, mask);
let result = pos.ins().isub(flipped, mask);
replace_inst_with(result);
}
_ => { /* existing behavior */ }
}
The exact function names vary across Cranelift versions, but the rewrite strategy stays the same: replace unsupported iabs.i128 with a branchless integer sequence.
5. Add or update tests
Keep the original regression test and expand it with meaningful cases:
test interpret
test run
target aarch64
function %iabs_i128(i128) -> i128 {
block0(v0: i128):
v1 = iabs.i128 v0
return v1
}
; run: %iabs_i128(0) == 0
; run: %iabs_i128(1) == 1
; run: %iabs_i128(-1) == 1
; run: %iabs_i128(170141183460469231731687303715884105727) == 170141183460469231731687303715884105727
; run: %iabs_i128(-170141183460469231731687303715884105727) == 170141183460469231731687303715884105727
If your test harness supports overflow-sensitive cases, also consider the minimum signed 128-bit integer separately.
6. Validate generated code paths
After the legalization change, verify that AArch64 no longer sees raw iabs.i128. Instead, it should receive operations it already knows how to lower. Depending on the implementation, 128-bit values may be represented internally as pairs of 64-bit registers or pseudo-values, but the important result is that the backend no longer fails on instruction selection.
cargo test -p cranelift-codegen
cargo test -p cranelift-filetests
If you use targeted test execution in your environment, run the specific AArch64 filetest subset to speed up iteration.
7. Optional: add backend-specific lowering only if needed
If the project prefers an AArch64-specific implementation instead of generic legalization, you can lower iabs.i128 with equivalent operations during backend lowering. However, this often duplicates logic and is less reusable than a generic legalization pass.
Common Edge Cases
i128::MIN overflow behavior
The most important edge case is the minimum signed 128-bit value:
-170141183460469231731687303715884105728
Its mathematical absolute value cannot be represented in signed i128. In two’s-complement arithmetic, negating it wraps to itself. Your implementation must match Cranelift’s intended integer semantics for iabs. Before merging, confirm whether the IR expects wrapping behavior, trapping behavior, or leaves this case architecture-defined. Most low-level compiler IRs use wrapping machine semantics unless documented otherwise.
Incorrect shift kind
Use arithmetic right shift, not logical shift. A logical shift would produce 0 or 1 instead of 0 or -1, which breaks the branchless absolute-value formula.
Wrong immediate width
The sign bit for i128 is bit 127. Shifting by 63 would accidentally compute a 64-bit sign mask and produce invalid results for many 128-bit inputs.
Backend legality of expanded ops
Make sure the replacement operations themselves are legal or further legalizable on AArch64. Usually sshr, bxor, and isub are much easier for Cranelift to decompose than iabs.i128.
Interpreter versus backend mismatch
If the interpreter already supports iabs.i128 directly, but AArch64 does not, a test might pass under test interpret and fail under test run. That mismatch is a strong sign that the bug is in lowering/legalization rather than IR semantics.
FAQ
Why not just add a native AArch64 instruction mapping?
Because AArch64 does not have a single scalar instruction for signed 128-bit absolute value. Cranelift must synthesize it from smaller primitive operations or legalize it before instruction selection.
Is branchless lowering better than a compare-and-branch sequence?
Usually yes for compiler backends. The branchless form (x ^ mask) - mask avoids control-flow expansion, simplifies optimization, and is easier to legalize consistently across targets.
Should this fix live in AArch64 lowering or generic legalization?
Generic legalization is typically the best long-term fix if multiple backends can benefit from the same expansion. Use backend-specific lowering only if the project architecture strongly prefers target-owned implementations.
Recommended Patch Strategy
If you want the shortest path to resolving the issue, use this checklist:
- Add a legalization rule for
iabs.i128. - Rewrite it as
sshr 127,bxor,isub. - Run the existing AArch64 filetest.
- Add regression cases for
0, positive, negative, and near-boundary values. - Confirm expected behavior for
i128::MIN.
That approach fixes the immediate AArch64 failure, keeps the implementation simple, and improves Cranelift’s handling of unsupported wide-integer operations in a backend-friendly way.