How to Fix: Cranelift: Wrong result for `fcvt_from_sint.f32` on RISCV

5 min read

The bug is a classic RISC-V code generation mismatch: Cranelift emits the wrong sequence for fcvt_from_sint.f32, so converting certain signed integers to f32 on riscv64gc produces an incorrect runtime value even though interpretation succeeds.

Problem Overview

This issue appears when a .clif test uses fcvt_from_sint.f32 and targets RISCV. The interpreter computes the expected result, but the generated machine code returns a different value. That usually means the bug is not in the IR semantics but in the backend lowering, instruction selection, or the handling of operand width before the final floating-point conversion instruction is emitted.

On RISC-V, integer-to-float conversion is sensitive to the exact source width and signedness. If Cranelift lowers the operation as if the source were a different bit width, uses the wrong temporary register class, or applies an unintended extension before conversion, the final f32 result will be wrong.

Understanding the Root Cause

The root cause is typically a mismatch between the Cranelift IR opcode semantics and the backend instruction sequence selected for RISC-V. The IR operation fcvt_from_sint.f32 means: take a signed integer value, interpret it with the correct source bit width, and convert it to a 32-bit IEEE floating-point value.

On riscv64, this gets tricky because many integer values live in 64-bit general-purpose registers even when the logical IR value is narrower. If the backend:

  • forgets to sign-extend a smaller signed integer before conversion,
  • sign-extends when it should have preserved an already-correct 64-bit value,
  • uses the wrong fcvt variant,
  • or lowers through an intermediate node with the wrong type,

then the hardware performs a valid conversion on the wrong integer input.

For example, a 32-bit signed integer stored in a 64-bit register must be interpreted consistently. If the backend accidentally treats it as zero-extended or as a full-width value with stale upper bits, fcvt.s.l or an equivalent sequence will produce an incorrect f32.

In short, this is a type/legalization and lowering correctness bug, not a floating-point rounding bug in the hardware.

Step-by-Step Solution

The fix is to audit and correct the RISC-V lowering path for signed integer to f32 conversion so the source width and sign are preserved exactly.

  1. Locate the lowering rule for fcvt_from_sint in the RISC-V backend.
  2. Check how the input IR type is mapped when targeting f32.
  3. Ensure the backend emits the correct conversion instruction for the legalized source type.
  4. Insert explicit sign extension when the logical source type is narrower than the physical GPR width and the backend does not already guarantee proper upper bits.
  5. Add regression tests using the failing .clif case and nearby boundary values.

A practical debugging workflow looks like this:

# 1. Reproduce the failure with the original CLIF test case
cargo test -p cranelift-codegen -- riscv fcvt_from_sint

# 2. Narrow the failure to codegen vs interpreter
cargo test -p cranelift-filetests -- test_run

# 3. Emit or inspect generated machine code for the RISC-V backend
# Use the project's existing flags/utilities for backend dumps if enabled

# 4. Patch the lowering for signed int -> f32 conversion
# Pseudocode:
# if src_ty is i8/i16/i32:
#   src = sign_extend_to_xlen(src)
# emit integer_to_f32_signed_convert(src)

# if src_ty is i64 on riscv64:
#   emit direct signed i64_to_f32 conversion

# 5. Re-run targeted tests and the full backend suite
cargo test -p cranelift-codegen riscv
cargo test -p cranelift-filetests

A representative backend fix often resembles this logic:

// Pseudocode only
match op {
    FcvtFromSintToF32(src, src_ty) => {
        let legal_src = match src_ty {
            I8 | I16 | I32 => sign_extend_to_xlen(src),
            I64 => src,
            _ => unreachable!(),
        };
        emit_riscv_signed_int_to_f32(legal_src);
    }
}

If the existing lowering already extends values, verify that it does so with the correct signedness and at the correct stage. A common mistake is relying on earlier legalization passes even though a later combine or register move drops the intended semantics.

After patching, add a regression test based on the reported failure:

test interpret
test run
target riscv64gc

function %test(i32) -> f32 {
block0(v0: i32):
    v1 = fcvt_from_sint.f32 v0
    return v1
}

Then expand coverage with negative values and boundary inputs such as -1, i32::MIN, i32::MAX, and values near exact f32 precision limits.

Common Edge Cases

  • Narrow integer sources: i8, i16, and i32 are the most likely to fail on a 64-bit backend if upper bits are mishandled.
  • Negative values: wrong sign extension often only becomes obvious for negative inputs.
  • Large magnitude integers: even with correct lowering, some integers cannot be represented exactly in f32. Do not confuse expected rounding with backend corruption.
  • Different float targets: fcvt_from_sint.f64 may follow a separate lowering path and should be tested independently.
  • Cross-backend assumptions: x86-64 or AArch64 behavior does not prove the RISC-V path is correct because legalization and instruction selection differ.
  • ABI or register-class bugs: if the conversion result is correct internally but wrong at function return boundaries, also inspect FP register return handling.

FAQ

1. Why does the interpreter pass while native RISC-V execution fails?

The interpreter executes Cranelift IR semantics directly, so it validates the intended operation. Native execution depends on backend lowering and emitted machine instructions. If those are wrong, only the generated code fails.

2. Is this a floating-point precision problem?

Usually no. The issue is generally that the backend converts the wrong signed integer value because of bad sign extension, type legalization, or instruction selection. Normal f32 rounding is expected and separate.

3. What tests should be added to prevent regressions?

Add filetests covering i8, i16, i32, and i64 inputs to fcvt_from_sint.f32 on riscv64gc, especially negative numbers, boundary values, and inputs that stress exact-vs-rounded f32 behavior.

For maintainers, the safest long-term fix is to make the RISC-V lowering rule explicit about source width and signedness instead of depending on implicit register state. That keeps fcvt_from_sint.f32 correct across legalization, optimization, and future backend refactors.

Leave a Reply

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