How to Fix: Cranelift: `iabs.i128` not implemented on AArch64

6 min read

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.i128 into 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, then mask = 0, so result is (x ^ 0) - 0 = x
  • if x < 0, then mask = -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.

If you want the shortest path to resolving the issue, use this checklist:

  1. Add a legalization rule for iabs.i128.
  2. Rewrite it as sshr 127, bxor, isub.
  3. Run the existing AArch64 filetest.
  4. Add regression cases for 0, positive, negative, and near-boundary values.
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *