How to Fix: Cranelift: Interpreter returns the wrong result for `scalar_to_vector.i64x2`
Cranelift Interpreter Bug: Fixing Wrong Results for scalar_to_vector.i64x2
The failure is subtle but dangerous: the Cranelift interpreter can return the wrong value for scalar_to_vector.i64x2, even when the input is as simple as 0. If your .clif test expects an i64x2 SIMD vector whose first lane is initialized from a scalar and the remaining lane behavior matches Cranelift semantics, an incorrect interpreter implementation can silently break validation, fuzzing, and IR-level test confidence.
Reproducing the Bug
The failing test case is small enough to expose the problem immediately:
test interpret
function %a(i64) -> i64x2 system_v {
block0(v0: i64):
v34 = scalar_to_vector.i64x2 v0
return v34
}
The intent of scalar_to_vector.i64x2 is to create a vector where the input scalar is placed into the low lane, while the remaining bits or lanes are initialized according to the instruction semantics used by Cranelift. In the interpreter, the produced runtime value can be assembled incorrectly, causing the returned vector representation to mismatch expected results.
This is especially visible in interpreter-driven tests because the issue is not in parsing or verification; it is in execution semantics.
Understanding the Root Cause
The core issue usually comes from how the interpreter materializes SIMD vector values from scalar inputs.
For scalar_to_vector.i64x2, the interpreter must construct a 128-bit vector with correct lane placement. That means:
- The input
i64must be written into lane 0. - The remaining lane must be initialized consistently with Cranelift’s instruction definition.
- The resulting internal value must preserve the exact bit-level layout expected by vector operations and test output formatting.
What commonly goes wrong is one of these implementation mistakes:
- The scalar is replicated into both lanes instead of inserted into only the first lane.
- The scalar is inserted into the wrong lane because of endianness or lane-index confusion.
- The vector is built through an incorrect byte packing path, causing the upper 64 bits to contain garbage or an unintended copy.
- The interpreter uses a generic helper that works for smaller lane types but fails for i64x2 because 128-bit vector assembly needs special handling.
In Cranelift, interpreter correctness depends on matching the IR instruction semantics exactly. If scalar_to_vector is implemented as a broadcast-like operation instead of a lane insertion operation, then tests for i64x2 will fail even though scalar arithmetic remains correct.
In short, the bug happens because the interpreter’s internal representation of vector lane initialization does not match the intended semantics of scalar_to_vector.i64x2.
Step-by-Step Solution
To fix this cleanly, update the interpreter logic that evaluates scalar_to_vector so it explicitly constructs an i64x2 value with the scalar in lane 0 and the remaining lane zeroed or otherwise initialized according to the instruction contract.
The exact file location may vary slightly by Cranelift revision, but the fix belongs in the interpreter’s instruction execution path for unary or vector-construction opcodes.
1. Locate the interpreter case for scalar_to_vector
match opcode {
Opcode::ScalarToVector => {
// existing implementation
}
_ => { ... }
}
If the implementation currently reuses a splat/broadcast helper, that is likely the bug.
2. Replace broadcast behavior with explicit lane construction
The corrected logic should conceptually look like this:
let scalar = extract_i64(input_value);
let lanes = [scalar, 0i64];
let result = pack_i64x2(lanes);
return result;
If the interpreter stores vectors as bytes instead of typed lanes, then build the 128-bit payload deterministically:
let scalar = extract_i64(input_value);
let mut bytes = [0u8; 16];
bytes[0..8].copy_from_slice(&scalar.to_le_bytes());
bytes[8..16].copy_from_slice(&0i64.to_le_bytes());
let result = Value::V128(bytes);
If the codebase uses an enum-based value representation, the final shape may be closer to this:
match ty {
types::I64X2 => {
let x = arg.as_i64()?;
Value::V128(pack_vector_i64x2([x, 0]))
}
_ => unimplemented!(),
}
3. Verify lane ordering
This is where many interpreter bugs survive the first patch. Ensure that:
- Lane 0 maps to the low 64 bits.
- Test formatting does not accidentally display lanes in reverse order.
- The byte conversion helper matches the interpreter’s internal vector layout.
A useful assertion during development:
let result = interpret("scalar_to_vector.i64x2", 5i64);
assert_eq!(lane_as_i64(result, 0), 5);
assert_eq!(lane_as_i64(result, 1), 0);
4. Add or update regression tests
Add a focused interpreter test to prevent regressions:
test interpret
function %scalar_to_vector_i64x2(i64) -> i64x2 system_v {
block0(v0: i64):
v1 = scalar_to_vector.i64x2 v0
return v1
}
; run: %scalar_to_vector_i64x2(0) == 0x00000000000000000000000000000000
; run: %scalar_to_vector_i64x2(1) == 0x00000000000000000000000000000001
Add at least one non-zero case and one negative case as well:
; run: %scalar_to_vector_i64x2(-1) == 0x0000000000000000ffffffffffffffff
This catches both sign-sensitive packing mistakes and lane-ordering errors.
5. Run the interpreter test suite
cargo test -p cranelift-interpreter
If your workspace uses broader integration coverage, also run the relevant Cranelift tests to confirm no other vector instructions were affected.
Common Edge Cases
After patching scalar_to_vector.i64x2, check these related pitfalls:
- Negative scalar values: make sure
-1i64becomes0xffffffffffffffffin lane 0, not truncated or reinterpreted incorrectly. - Upper lane contamination: if temporary buffers are reused, lane 1 may retain stale bytes instead of zero.
- Endianness assumptions: byte packing helpers can invert expected low/high lane layout if not standardized.
- Other vector widths: if the implementation is generic, verify
scalar_to_vector.i8x16,i16x8,i32x4, andf32x4behavior too. - Confusion with splat semantics:
scalar_to_vectoris not always equivalent to a full broadcast operation.
A robust fix should treat this issue as a semantic correctness problem, not just a one-off failing test.
FAQ
Why does this bug show up in the interpreter but not necessarily in generated machine code?
The interpreter and code generator are separate execution paths. The machine backend may lower scalar_to_vector.i64x2 correctly, while the interpreter has a mismatched manual implementation. That is why IR interpreter regression tests are so valuable.
Should scalar_to_vector.i64x2 duplicate the scalar into both lanes?
No, not if you are implementing Cranelift’s scalar_to_vector semantics correctly for this instruction. A duplicate-into-all-lanes behavior is a splat pattern, which is different from inserting a scalar into the low lane of a vector.
What is the safest way to prevent similar SIMD interpreter bugs?
Use explicit lane-based construction, add regression tests for zero, positive, and negative inputs, and verify byte layout separately from textual output. Avoid overly generic helpers unless they are proven correct for every vector type.
Once patched, the interpreter should return a correctly formed i64x2 value for scalar_to_vector, restoring trust in Cranelift’s execution model for SIMD-related .clif tests.