diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 9bb74e72b..fafbbf695 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1154,7 +1154,12 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr } if (recv?.type.kind === "array" || recv?.type.kind === "bytes") { const receiver = lowerer.declareHiddenLocal("%setArray", recv.type); - const index = lowerer.lowerExpr(expr.left.argumentExpression); + const index = lowerOptionalNumber( + lowerer, + lowerer.lowerExpr(expr.left.argumentExpression), + locOf(expr.left.argumentExpression), + expr.left.argumentExpression, + ); if (index.type.kind !== "f64") lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 0c4e89f87..74f0be955 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -1493,7 +1493,12 @@ export function lowerElementCompoundParts( "compound assignment through a non-native array or byte view", ); } - const index = lowerer.lowerExpr(target.argumentExpression); + const index = lowerOptionalNumber( + lowerer, + lowerer.lowerExpr(target.argumentExpression), + locOf(target.argumentExpression), + target.argumentExpression, + ); if (index.type.kind !== "f64") lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys"); const receiverLocal = lowerer.declareHiddenLocal("%compoundArray", receiver.type); @@ -1836,7 +1841,14 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): const recv = lowerer.lowerExpr(target.expression); // The checker's flow type is a typed array, but the storage is wider. if (recv.type.kind !== "bytes") return lowerWiderBytesWrite(lowerer, expr, target, recv); - const index = lowerer.lowerExpr(target.argumentExpression); + // An index the checker types as a number but whose storage may hold + // undefined (a hole read) converts with ToNumber, as the reads do. + const index = lowerOptionalNumber( + lowerer, + lowerer.lowerExpr(target.argumentExpression), + locOf(target.argumentExpression), + target.argumentExpression, + ); if (index.type.kind === "dyn" && recv.type.kind === "bytes") { return { kind: "exprStmt", diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 104057906..7269f6b52 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -261,7 +261,12 @@ export function lowerIncDec( (raw.type.kind === "array" || raw.type.kind === "bytes") ) { const receiver = save(raw, "%incrementArray"); - const rawIndex = lowerer.lowerExpr(access.argumentExpression); + const rawIndex = lowerOptionalNumber( + lowerer, + lowerer.lowerExpr(access.argumentExpression), + locOf(access.argumentExpression), + access.argumentExpression, + ); const index = save( rawIndex.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) ? { kind: "libCall", fn: "dyn.toNumberCoerce", args: [rawIndex], type: F64, loc } diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index df055aed4..14dadfc4d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -194,6 +194,7 @@ import { lowerElementCompoundStatement, lowerIncDec, lowerIncDecTarget, + lowerOptionalNumber, lowerGroupsProjection, matchResultNamedGroupsOf, runtimeOptionalGuardIds, @@ -8835,7 +8836,12 @@ function lowerAssignTargetInto( if (arr.type.kind === "array") { const recv = lowerer.declareHiddenLocal("%dtRecv", arr.type); out.push({ kind: "varDecl", localId: recv.id, init: arr, loc }); - const index = lowerer.lowerExpr(t.argumentExpression); + const index = lowerOptionalNumber( + lowerer, + lowerer.lowerExpr(t.argumentExpression), + locOf(t.argumentExpression), + t.argumentExpression, + ); if (index.type.kind !== "f64") { lowerer.unsupported("SC1090", t.argumentExpression, "indexing with non-number keys"); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-index-element-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-index-element-writes.ts.json new file mode 100644 index 000000000..e748f8e75 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-index-element-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/optional-index-element-writes.ts" + ], + "diags": [] +} diff --git a/tests/corpus/optional-index-element-writes.ts b/tests/corpus/optional-index-element-writes.ts new file mode 100644 index 000000000..6f55d07a5 --- /dev/null +++ b/tests/corpus/optional-index-element-writes.ts @@ -0,0 +1,35 @@ +// Element writes whose index comes from for...of over a number[] field the +// class also writes by index (so the loop variable's storage may be +// undefined, a hole read): typed-array writes, compound and update writes, +// a write used as a value, and a destructuring target all convert the +// index with ToNumber, as the element reads do. +class Heap { + private k: number[] = []; + push(v: number): void { + const i = this.k.length; + this.k.push(v); + this.k[i] = v; + } + all(): number[] { + return this.k; + } +} +const h = new Heap(); +h.push(2); +h.push(0); +h.push(1); +const cost = new Float64Array(4); +for (const i of h.all()) cost[i] = i * 1.5; +console.log(Array.from(cost).join(",")); +const counts = new Int32Array(3); +for (const i of h.all()) counts[i] += 10; +for (const i of h.all()) counts[i]++; +console.log(Array.from(counts).join(",")); +const marks = new Uint8Array(3); +let last = 0; +for (const i of h.all()) last = marks[i] = i + 1; +console.log(Array.from(marks).join(","), last); +const plain = [0, 0, 0]; +for (const i of h.all()) [plain[i]] = [i * 2]; +for (const i of h.all()) plain[i] += 1; +console.log(plain.join(","));