rust-lang / rust · Issue No. 160646
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)
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.
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.
Each of these was tested by changing one thing at a time on the reproduction above.
-C opt-level=2 -C no-vectorize-loops prints ok.-C opt-level=2 -C no-vectorize-slp still panics.&mut [Point<2>]f64.Point::new(3.0, 2.0 * p[1])Point::new(3.0, p[1] + 1.0) are correct; only the verbatim copy p[1] is wrong.Point::new(p[1], 3.0) andPoint::new(p[0], p[1]) are correct, and so is Point::new(3.0, p[0]).move |_, p| Point::new(three, p[1]) capturing a local, and with a value that is onlyIndex impl. Point::from_array([3.0, p[1]])p.coords[1] directly fail the same way.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.
$ 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
Relay reads this issue against the repository's contribution signals: the files it is likely to touch, how the maintainers triage work this size, and what the first contribution would exercise.
The full analysis for this issue is still being assembled. Until then, the description above and the thread on GitHub are the most reliable context.