Regression: Wasm32 SIMD128 narrow intrinsics stopped working correctly in Rust 1.95+
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
I found that starting with Rust 1.95, all Wasm32 Simd128 narrowing intrinsics ("narrow" functions) stopped working correctly.
For example, u8x16_narrow_i16x8() takes an array of i16 values and converts it to an array of u8 values using saturating narrowing. In other words, negative values are clamped to 0, while values that are too large are clamped to 255.
However, there is no equivalent function that accepts an array of u16 values. If the source array contains u16s, the caller must manually clamp the upper bound first, for example with u16x8_min(x, 255). Otherwise, all values greater than i16::MAX will be interpreted as negative numbers and will be converted to 0 instead of 255.
This worked correctly before Rust 1.95. Starting with Rust 1.95, the compiler somehow decides that the u16x8_min(x, 255) operation is unnecessary in this case and safely removes it during optimization.
Other functions from the "narrow" group exhibit the same issue.
Here is a Godbolt example demonstrating the problem: https://rust.godbolt.org/z/1KeKzhe74
Code
I tried this code:
use core::arch::wasm32::*;
#[target_feature(enable = "simd128")]
pub unsafe fn narrow_with_min(data: [u16; 8]) -> [u8; 16] {
let mut result = [0u8; 16];
let mut data_u16x8 = v128_load(data.as_ptr() as *const v128);
// This command will be removed from WASM in Rust v1.95+
data_u16x8 = u16x8_min(data_u16x8, u16x8_splat(0xff));
let data_u8x16 = u8x16_narrow_i16x8(data_u16x8, data_u16x8);
v128_store(result.as_mut_ptr() as *mut v128, data_u8x16);
result
}
#[test]
fn test_narrow_with_min() {
let res = unsafe {narrow_with_min([0, 1, 255, 0x7fff, 0x8000, 0xffff, 0, 0])};
// Next line fails in Rust 1.95+ because res[0..8] will be [0, 1, 255, 255, 0, 0, 0, 0]
assert_eq!(res[0..8], [0, 1, 255, 255, 255, 255, 0, 0]);
}
WASM code from Rust 1.94:
narrow_with_min
local.get 0
local.get 1
v128.load 0:p2align=1
v128.const 255, 255, 255, 255, 255, 255, 255, 255
i16x8.min_u
local.tee 2
local.get 2
i8x16.narrow_i16x8_u
v128.store 0:p2align=0
end_function
WASM code from Rust 1.95:
narrow_with_min
local.get 0
local.get 1
v128.load 0:p2align=1
local.tee 2
local.get 2
i8x16.narrow_i16x8_u
v128.store 0:p2align=0
end_function
Version it worked on
It most recently worked on: Rust 1.94.1
Version with regression
It is broken in Rust 1.95 and 1.96
But if I use a debug build without any optimizations, everything works correctly even in these versions of Rust. This is because the compiler doesn't remove u16x8_min() from WASM.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the provided narrow_with_min reproducer and compare its optimized WASM output between Rust 1.94.1 and Rust 1.95+, using the Godbolt example as the entry point. Done means the u16x8_min operation is preserved for the narrowing intrinsics and the test returns the expected saturated values in optimized builds.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, wasm
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 52/100