Skip to content

Silent miscompilation: store to a module-global Buffer dropped when the read index comes from a conditionally-assigned local #9278

Description

@parv0888

What happened

A store into a module-global Buffer is silently dropped when the stored value's
source index comes from a conditionally-assigned local, inside a function. The
destination buffer keeps its initial contents. No error, no warning, no crash —
the program runs to completion and prints a wrong answer.

Minimal reproduction

const W = 64, H = 64, N = W * H;
const src = Buffer.alloc(N), dst = Buffer.alloc(N);
for (let i = 0; i < N; i++) src[i] = (i * 7 + 13) & 0xff;

function wb(y: number, x: number): void {
  const yy = y < 0 ? 0 : y > H - 1 ? H - 1 : y;   // conditionally-assigned local
  dst[y * W + x] = src[yy * W + x] & 0xff;        // <-- this store never lands
}
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) wb(y, x);

let s = 0;
for (let i = 0; i < N; i++) s += dst[i];
console.log("sum=" + s);
node  : sum=522240
perry : sum=0          <-- dst is entirely untouched

What you expected

perry to print sum=522240, matching Node.

Environment

  • Perry: 0.5.1220 (latest release), perry-linux-x86_64.tar.gz, sha256 verified
  • Host: x86_64, Linux 6.17, Ubuntu; Node v22.23.1 as the oracle
  • Command: perry compile repro.ts -o repro && ./repro
  • Not yet checked against main (~0.5.1519) — I don't have a source build.

Bisect

Same program shape, varying only the line that computes yy:

yy computed as result
const yy = y; correct
const yy = y < 0 ? 0 : y > H-1 ? H-1 : y; dst all zeros
let yy = y; if (yy<0) yy=0; if (yy>H-1) yy=H-1; dst all zeros

So it is not ternary-specific — an if-statement form fails identically. The
trigger is a conditionally-assigned local flowing into the index of a read from
one module-global buffer, where the function also stores into a different one.

These all stay correct, so the individual ingredients are fine:

  • the same function with dst[i] = v and no conditional (m1m5)
  • the clamp expression evaluated on its own, outside a buffer index
  • Buffer reads/writes at top level rather than inside a function
  • the same loop with number[] instead of Buffer
  • Math.imul and the FNV hash used to check the result

Wider blast radius

The bug is not limited to the all-zeros case. In a 5×5 convolution I was writing
against benchmarks/honest_bench/workloads/3_image_convolution, the same shape
produced a partially wrong image rather than an empty one — checksum 2d3a7c7f
where Node and Rust both produce 2ba2e053. That form is harder to notice,
because the output looks plausible.

Why this may be a regression

benchmarks/honest_bench/REPORT.md records a closely related bug as fixed in
v0.5.30:

buf[i] = v on Buffer / Uint8Array was a silent no-op. The lowering for
Expr::Uint8ArraySet in crates/perry-codegen/src/expr.rs was
lower_expr(value) — it evaluated the RHS and threw it away.

The trigger here is narrower — the conditional on the read index seems to be
what matters — but the symptom is the same: the store is evaluated and discarded.
It may be worth checking whether the v0.5.30 fix covers this path.

Impact

Silent wrong answers are the worst failure mode for a compiler: the program exits
0 and the output looks reasonable. Anything doing image, audio, or buffer
processing with clamped or wrapped indices — an extremely common shape — can hit
this. A compile-time error or even a crash would be far preferable.


🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions