How to Fix: Cranelift: hit unreachable case while lowering `fcvt_to_uint`

6 min read

Cranelift crash on fcvt_to_uint: why the lowering hits an unreachable case and how to fix it

This bug shows up when Cranelift tries to lower a floating-point-to-unsigned-integer conversion for a SIMD vector shape it does not actually support in that backend path. Instead of rejecting the IR cleanly or expanding it to a legal sequence, the lowering code falls through to an unreachable arm and panics.

If your .clif test includes something like converting f32x4 lanes into i32x4 with unsigned semantics, the failure usually means one thing: the legalization or ISA lowering rules do not cover that exact opcode/type combination.

Understanding the Root Cause

The issue centers on fcvt_to_uint, which represents a conversion from floating-point values to unsigned integer values. In scalar form, many backends can lower this directly or emulate it. In vector form such as f32x4 -> i32x4, support depends on:

  • the target ISA,
  • the available SIMD instructions,
  • Cranelift’s legalization rules, and
  • whether the backend implements a fallback expansion for unsupported lane-wise unsigned conversion.

The panic happens because the lowering path assumes the type reaching it has already been legalized into a supported form. When that assumption is false, a match arm or backend-specific lowering helper receives an unhandled vector conversion and triggers an unreachable case.

In practical terms, the failure usually comes from one of these conditions:

  • Missing legalization: fcvt_to_uint on f32x4 is allowed into lowering without being transformed into a legal sequence.
  • Backend mismatch: the selected backend supports scalar conversion but not the vector variant.
  • Incomplete pattern coverage: integer lane width or vector width combinations like x4 are not covered in the lowering implementation.
  • Unsigned conversion complexity: converting float to unsigned int is trickier than signed conversion because values above INT_MAX need special handling, saturation, masking, or bias/subtract tricks depending on the backend.

That last point matters. A signed conversion path may exist for fcvt_to_sint, while fcvt_to_uint still needs custom expansion. If the backend authors only implemented one path, the other can reach a panic when vector IR is presented.

Step-by-Step Solution

The correct fix is to ensure unsupported vector unsigned float conversions are legalized before final lowering, or to add explicit backend lowering support for the missing type combination.

Use the following workflow to diagnose and fix the issue.

1. Reproduce with a minimal .clif test

Reduce the test case so the failure is isolated to the conversion.

function %f13(v0: f32x4) -> i32x4 system_v {
block0(v0: f32x4):
    v1 = fcvt_to_uint.i32x4 v0
    return v1
}

Run the Cranelift test command used in your workspace so you can verify the panic consistently.

Inspect the backend lowering and legalization code for handling of:

  • fcvt_to_uint
  • f32x4
  • i32x4
  • the active target such as x86-64, AArch64, or another ISA

You are looking for one of two outcomes:

  • a direct lowering rule exists for this vector conversion, or
  • the instruction should have been expanded before reaching lowering.

3. Add a legalization rule if lowering support is missing

If the backend cannot lower fcvt_to_uint for vectors directly, add a legalization step that rewrites it into a supported sequence. The exact implementation depends on Cranelift internals, but the logic typically follows one of these patterns:

  • Split the vector into scalar lanes, convert lane-by-lane, and rebuild the vector.
  • Lower to a backend-supported signed conversion trick if the ISA has a known unsigned emulation sequence.
  • Reject invalid type combinations earlier with a verifier or legalization error instead of panicking.

Pseudocode for a legalization-oriented fix:

match opcode {
    Opcode::FcvtToUint => {
        match (input_type, output_type) {
            (F32X4, I32X4) => {
                // Option A: expand into per-lane scalar conversions
                // Option B: lower via backend-specific unsigned conversion sequence
                return expand_fcvt_to_uint_vector(inst);
            }
            _ => {
                // existing legal cases
            }
        }
    }
    _ => {}
}

4. If the backend should support it, implement the missing lowering arm

When the target ISA has a valid instruction sequence, add it explicitly instead of relying on a fallback that does not exist. For example, update the lowering code so it handles the vector type rather than hitting unreachable!().

fn lower_fcvt_to_uint(ctx: &mut LowerCtx, ty_in: Type, ty_out: Type, val: Value) -> Value {
    match (ty_in, ty_out) {
        (types::F32X4, types::I32X4) => {
            return lower_fcvt_to_uint_f32x4_i32x4(ctx, val);
        }
        _ => {
            panic!("unexpected type combination should have been legalized earlier");
        }
    }
}

In a mature fix, replace panic-based assumptions with either:

  • a proper legalization fallback, or
  • a compile-time error path with a precise message.

5. Add regression tests

This issue needs a test that proves Cranelift no longer reaches the unreachable case.

function %fcvt_to_uint_vec(v0: f32x4) -> i32x4 {
block0(v0: f32x4):
    v1 = fcvt_to_uint.i32x4 v0
    return v1
}

Add neighboring tests too:

  • f32x2 -> i32x2
  • f64x2 -> i64x2 if relevant
  • scalar f32 -> i32 to verify the existing path still works
  • negative, NaN, and out-of-range inputs where semantics matter

6. Validate semantics, not just crash behavior

Unsigned conversion has subtle behavior around NaN, negative values, and overflow. After the panic is fixed, verify the result matches Cranelift’s intended semantics for trapping, saturating, or implementation-defined lowering behavior.

// Example test ideas
// 0.0     -> 0
// 1.0     -> 1
// -1.0    -> behavior per IR semantics
// 4294967295.0 -> max u32 if representable by the chosen semantics
// NaN     -> behavior per IR semantics

7. Replace unreachable assumptions with defensive handling

Even if you implement support, this class of bug is easier to maintain when impossible states are verified earlier. A good long-term patch often includes:

  • type assertions in legalization,
  • clear verifier diagnostics, and
  • graceful fallback expansion instead of backend panic paths.

That prevents future SIMD opcode additions from silently reaching unsupported lowering code.

Common Edge Cases

  • NaN inputs: some conversion paths treat NaN specially; make sure your legalized sequence preserves the intended behavior.
  • Negative floating-point lanes: unsigned conversion from negative values can expose differences between scalar and vector fallback logic.
  • Large positive values: values larger than the destination unsigned integer range may overflow differently depending on the ISA or legalization strategy.
  • Different SIMD widths: fixing f32x4 -> i32x4 does not automatically fix f32x8, f64x2, or narrower vector forms.
  • Backend-specific support gaps: x86 and AArch64 may require different lowering sequences even for the same Cranelift IR opcode.
  • Signed vs unsigned confusion: a backend may support fcvt_to_sint but not fcvt_to_uint. Do not assume one implies the other.
  • Verifier blind spots: if verification allows the IR but legalization cannot handle it, the crash may simply move later in the pipeline.

FAQ

Why does fcvt_to_sint work while fcvt_to_uint crashes?

Because unsigned float-to-int conversion usually needs extra lowering logic. Many backends have a native or simpler path for signed conversion, while the unsigned vector form requires a custom expansion that may be missing.

Is this a verifier bug or a lowering bug?

Most often it is primarily a lowering/legalization bug. If the IR opcode and types are accepted, the pipeline should either legalize them or reject them cleanly. Hitting unreachable means an internal assumption was violated.

What is the safest fix for maintainability?

The safest fix is to add a legalization rule for unsupported vector fcvt_to_uint forms and keep backend lowering limited to known-legal cases. That makes unsupported combinations explicit and reduces the chance of future panic paths.

The key takeaway is simple: do not let vector fcvt_to_uint reach backend lowering unless that exact type combination is known to be legal. Either lower it properly, expand it during legalization, or reject it with a precise diagnostic. That change fixes the immediate crash and hardens Cranelift against similar SIMD lowering bugs.

Leave a Reply

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