Miscompilation at opt-level >= 2 on aarch64: loop vectorizer gives a struct field the value of a sibling field
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
Code
Self-contained, no dependencies, no unsafe:
use std::ops::{Add, Index};
#[derive(Clone, Copy, Debug, PartialEq)]
struct Tensor1<const DIM: usize> { entries: [f64; DIM] }
impl<const DIM: usize> Index<usize> for Tensor1<DIM> {
type Output = f64;
fn index(&self, i: usize) -> &f64 { &self.entries[i] }
}
impl<const DIM: usize> Add for Tensor1<DIM> {
type Output = Self;
fn add(mut self, other: Self) -> Self {
for d in 0..DIM { self.entries[d] += other.entries[d]; }
self
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct Point<const DIM: usize> { coords: Tensor1<DIM> }
impl Point<2> {
fn new(x: f64, y: f64) -> Self { Self { coords: Tensor1 { entries: [x, y] } } }
}
impl<const DIM: usize> Index<usize> for Point<DIM> {
type Output = f64;
fn index(&self, i: usize) -> &f64 { &self.coords[i] }
}
impl<const DIM: usize> Add<Tensor1<DIM>> for Point<DIM> {
type Output = Point<DIM>;
fn add(self, offset: Tensor1<DIM>) -> Point<DIM> {
Point { coords: self.coords + offset }
}
}
fn transform<F>(cells: &mut [Vec<Point<2>>], mut f: F)
where F: FnMut(usize, &Point<2>) -> Point<2> {
for (cell, points) in cells.iter_mut().enumerate() {
for point in points.iter_mut() {
let original = *point;
let moved = f(cell, &original);
*point = original + moved.coords;
}
}
}
fn main() {
// Four cells of nine points; four or more points per cell is the threshold.
let mut cells: Vec<Vec<Point<2>>> = (0..4)
.map(|_| (0..9).map(|k| Point::new(0.25 * k as f64, 0.125 * k as f64)).collect())
.collect();
let before = cells.clone();
transform(&mut cells, |_, p| Point::new(3.0, p[1]));
// Adding (3, y) to (x, y) gives (x + 3, 2y).
for (c, points) in cells.iter().enumerate() {
for (i, a) in points.iter().enumerate() {
let b = before[c][i];
assert_eq!(*a, Point::new(b[0] + 3.0, 2.0 * b[1]),
"cell {c} point {i}: from ({}, {})", b[0], b[1]);
}
}
println!("ok");
}
$ rustc -C opt-level=0 repro.rs && ./repro
ok
$ rustc -C opt-level=1 repro.rs && ./repro
ok
$ rustc -C opt-level=2 repro.rs && ./repro
thread 'main' panicked at repro.rs:62:13:
assertion `left == right` failed: cell 0 point 0: from (0, 0)
left: Point { coords: Tensor1 { entries: [3.0, 3.0] } }
right: Point { coords: Tensor1 { entries: [3.0, 0.0] } }
$ rustc -C opt-level=3 repro.rs && ./repro
(same panic)
Expected
ok at every optimization level. The closure returns Point::new(3.0, p[1]), so the
second component of the returned point is a copy of the second component of the input.
Actual
Correct at -C opt-level=0 and -C opt-level=1. At -C opt-level=2 and 3 the second
component of the returned point holds 3.0 — the constant that was written into the
first component — instead of the coordinate that was read. The difference is not a
rounding difference; the value is wrong by the whole constant.
Narrowing
Each of these was tested by changing one thing at a time on the reproduction above.
- It is the loop vectorizer.
-C opt-level=2 -C no-vectorize-loopsprintsok.
-C opt-level=2 -C no-vectorize-slpstill panics. - A nested loop is needed. Flattening the points into a single
&mut [Point<2>]
and using one loop does not reproduce. - The inner loop needs at least four iterations. Two or three points per cell do
not reproduce; four or more do. A NEON vector holds twof64. - Any arithmetic on the copied component hides it.
Point::new(3.0, 2.0 * p[1])
andPoint::new(3.0, p[1] + 1.0)are correct; only the verbatim copyp[1]is wrong. - Only that component order is wrong.
Point::new(p[1], 3.0)and
Point::new(p[0], p[1])are correct, and so isPoint::new(3.0, p[0]). - Capturing makes no difference. The same failure occurs with
move |_, p| Point::new(three, p[1])capturing a local, and with a value that is only
known at run time (taken from the process argument count, so it cannot be
constant-folded). So this is not constant propagation. - Not tied to the constructor or to the
Indeximpl.Point::from_array([3.0, p[1]])
and readingp.coords[1]directly fail the same way.
Where it was found
In a finite element library, in a routine that displaces the cached support points of a
mesh by a per-point offset. The wrong output was a mesh point at (3, 3) where (3, 0)
was meant. It is invisible in a debug build: the regression test written for it passes at
-C opt-level=0 even without the workaround. The workaround in that code is an
#[inline(never)] bridge, which works only because an uninlinable call in the loop body
stops the loop from being vectorized.
Meta
$ rustc --version --verbose
rustc 1.93.1 (01f6ddf75 2026-02-11)
binary: rustc
commit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf
commit-date: 2026-02-11
host: aarch64-apple-darwin
release: 1.93.1
LLVM version: 21.1.8
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 by compiling repro.rs on aarch64 at opt-level 2 and 3, then compare it with no-vectorize-loops to confirm the loop-vectorizer boundary. Trace the compiler or LLVM loop-vectorization path involved in the nested loop and copied component. Done means the reproduction prints ok at every optimization level and a regression test covers the case.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100