Summary
A typed-array element store used as an expression evaluates to 0 instead of the assigned value, whenever it appears anywhere inside an expression statement. The store itself is correct — only the expression's value is wrong. Silent: no crash, no diagnostic, just a wrong number.
const buf = new Uint8Array(4);
function sink(label: string, got: number, want: number): void {
if (got !== want) console.log("WRONG", label, "got", got, "want", want);
}
sink("store-arg", (buf[0] = 5), 5); // consumed as a call argument
let n = 0;
n = buf[1] = 7; // consumed by an assignment
sink("store-assign", n, 7);
sink("store-binary", (buf[2] = 3) + 100, 103); // consumed by arithmetic
console.log("buf", buf[0], buf[1], buf[2]);
|
output |
| node |
buf 5 7 3 |
perry (d2dca5823) |
WRONG store-arg got 0 want 5
WRONG store-assign got 0 want 7
WRONG store-binary got 100 want 103
buf 5 7 3 |
buf 5 7 3 on both, so the stores land correctly. An assignment expression must evaluate to the assigned value (ES2024 §13.15.2); here it evaluates to 0.
Cause
ctx.discard_expr_value means "this STATEMENT's value is discarded", and lower_expr does not clear it when recursing into subexpressions — the only reset in the tree is lower_call/new_ctor_args.rs:252, for constructor arguments. So while lowering the operands of sink(buf[0] = 5); the flag is still set from the enclosing Stmt::Expr.
Four sites read it as though it meant "this EXPRESSION's value is discarded" and return double_literal(0.0):
expr/index_set.rs:754 — lower_typed_array_store
expr/index_set.rs:769 — proven-view checked store
expr/index_set.rs:848
expr/arrays_finds.rs:788, :824, :895, :912 — Uint8ArraySet / buffer stores
expr/dispatch.rs:25 also reads the flag, but only to pick a materialization path, so it is not affected.
Scope
Needs a typed array or buffer view — plain arr[i] = v on an ordinary array does not take these paths, so this is narrower than "all index assignment". Every affected site is a store whose value would normally be materialized for the caller.
Suggested fix
The flag cannot answer the question these sites are asking. Either clear discard_expr_value in lower_expr when recursing into operand position — which makes it mean what the sites assume, and is the smaller change but touches every reader — or give the sites a non-leaking signal set only for an expression that is the direct child of Stmt::Expr.
I hit this from the other direction: I tried gating arr.push(x)'s length computation on the same flag as a perf change and it produced exactly this bug for sink(a.push(10)), n = a.push(20), a.push(1) + 100 and a.push(1) > 0 ? 7 : 9. So whichever fix is chosen should land with the array-push case in the same test, since that path is about to want the same optimization (js_array_length on a discarded push is 8–13% of push_cls — see #7511).
Summary
A typed-array element store used as an expression evaluates to
0instead of the assigned value, whenever it appears anywhere inside an expression statement. The store itself is correct — only the expression's value is wrong. Silent: no crash, no diagnostic, just a wrong number.buf 5 7 3d2dca5823)WRONG store-arg got 0 want 5WRONG store-assign got 0 want 7WRONG store-binary got 100 want 103buf 5 7 3buf 5 7 3on both, so the stores land correctly. An assignment expression must evaluate to the assigned value (ES2024 §13.15.2); here it evaluates to0.Cause
ctx.discard_expr_valuemeans "this STATEMENT's value is discarded", andlower_exprdoes not clear it when recursing into subexpressions — the only reset in the tree islower_call/new_ctor_args.rs:252, for constructor arguments. So while lowering the operands ofsink(buf[0] = 5);the flag is still set from the enclosingStmt::Expr.Four sites read it as though it meant "this EXPRESSION's value is discarded" and return
double_literal(0.0):expr/index_set.rs:754—lower_typed_array_storeexpr/index_set.rs:769— proven-view checked storeexpr/index_set.rs:848expr/arrays_finds.rs:788,:824,:895,:912—Uint8ArraySet/ buffer storesexpr/dispatch.rs:25also reads the flag, but only to pick a materialization path, so it is not affected.Scope
Needs a typed array or buffer view — plain
arr[i] = von an ordinary array does not take these paths, so this is narrower than "all index assignment". Every affected site is a store whose value would normally be materialized for the caller.Suggested fix
The flag cannot answer the question these sites are asking. Either clear
discard_expr_valueinlower_exprwhen recursing into operand position — which makes it mean what the sites assume, and is the smaller change but touches every reader — or give the sites a non-leaking signal set only for an expression that is the direct child ofStmt::Expr.I hit this from the other direction: I tried gating
arr.push(x)'s length computation on the same flag as a perf change and it produced exactly this bug forsink(a.push(10)),n = a.push(20),a.push(1) + 100anda.push(1) > 0 ? 7 : 9. So whichever fix is chosen should land with the array-push case in the same test, since that path is about to want the same optimization (js_array_lengthon a discarded push is 8–13% ofpush_cls— see #7511).