diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index fabe70f37a..b1e65a23fc 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -19,6 +19,7 @@ * length, so reads never reach the state check for it; a write past the * bound tests the flag and, without states, appends at the length. */ +import { InternalCompilerError } from "../../errors.js"; import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js"; import { f64Lit, @@ -371,7 +372,9 @@ export function emitDenseArrayGet( /** emitDenseArrayGet, with the int32 of an int32 array's element: the * slot itself on the dense path, the runtime getter's double converted - * (exactly: the element is an int32) on the other. */ + * (exactly: the element is an int32) on the other. With `inexact`, the + * getter's double may be something other than an int32 (NaN from + * `scr_arr_get_number`): the read branches there when it is. */ export function emitDenseArrayRead( host: LlvmEmitterContext, array: string, @@ -382,6 +385,7 @@ export function emitDenseArrayRead( getter: string, retain: boolean, proven = false, + inexact?: string, ): { value: string; int32: string | null } { const B = host.B; const ty = valueType(acc); @@ -463,22 +467,68 @@ export function emitDenseArrayRead( : READ_FALLBACK; B.line(`${slowValue} = call ${ty} @${getter}(ptr ${array}, double ${index.name})${attrs}`); let slowInt: string | null = null; - if (fastInt !== null) { + let slowLabel = slow; + if (fastInt !== null && inexact !== undefined) { + // An int32 that is not -0, or the inexact branch. + host.declare("declare i32 @llvm.fptosi.sat.i32.f64(double)"); + slowInt = B.tmp(); + const back = B.tmp(), + exact = B.tmp(), + bits = B.tmp(), + notNegativeZero = B.tmp(), + ok = B.tmp(); + B.line(`${slowInt} = call i32 @llvm.fptosi.sat.i32.f64(double ${slowValue})`); + B.line(`${back} = sitofp i32 ${slowInt} to double`); + B.line(`${exact} = fcmp oeq double ${back}, ${slowValue}`); + B.line(`${bits} = bitcast double ${slowValue} to i64`); + B.line(`${notNegativeZero} = icmp ne i64 ${bits}, -9223372036854775808`); + B.line(`${ok} = and i1 ${exact}, ${notNegativeZero}`); + slowLabel = B.newLabel("arr.get.int32"); + B.condBr(ok, slowLabel, inexact); + B.startBlock(slowLabel); + } else if (fastInt !== null) { slowInt = B.tmp(); B.line(`${slowInt} = fptosi double ${slowValue} to i32`); } B.br(join); B.startBlock(join); const result = B.tmp(); - B.line(`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${slowValue}, %${slow} ]`); + B.line(`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${slowValue}, %${slowLabel} ]`); let int32: string | null = null; if (fastInt !== null && slowInt !== null) { int32 = B.tmp(); - B.line(`${int32} = phi i32 [ ${fastInt}, %${fastLabel} ], [ ${slowInt}, %${slow} ]`); + B.line(`${int32} = phi i32 [ ${fastInt}, %${fastLabel} ], [ ${slowInt}, %${slowLabel} ]`); } return { value: result, int32 }; } +/** emitDenseArrayNumber of an int32 array, with the element's int32: a + * read that answers NaN (or another value that is not an int32) branches + * to `inexact`. */ +export function emitDenseArrayNumberInt32( + host: LlvmEmitterContext, + array: string, + index: LlValue, + indexExpr: IrExpr | undefined, + proven: boolean, + inexact: string, +): { value: string; int32: string } { + const read = emitDenseArrayRead( + host, + array, + index, + indexExpr, + "i32", + { kind: "f64" }, + "scr_arr_get_number", + false, + proven, + inexact, + ); + if (read.int32 === null) throw new InternalCompilerError("llvm emitter bug: int32 read"); + return { value: read.value, int32: read.int32 }; +} + /** `scr_arr_get_number`: a numeric element, or NaN for every missing, * hole, or present-undefined slot (answered by the runtime). */ export function emitDenseArrayNumber( diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 7eec14e4e6..279afdcd05 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -62,12 +62,14 @@ import { import { exactInteger, widenInteger } from "./integer-values.js"; import { emitDenseArrayNumber, + emitDenseArrayNumberInt32, emitDenseArrayPop, emitDenseArrayPush, runtimeAccess, type ArrayAccess, } from "./dense-array-access.js"; import { INT32_ARRAY_METHODS } from "../../ir/int32-arrays.js"; +import { INT32_RANGE } from "../../ir/integer-ranges.js"; import { emitUnionWiden } from "./expr-records.js"; import { registerReplay } from "./emit-journal.js"; @@ -694,6 +696,25 @@ export function emitArrIntrinsic( )!; return { name: value, type: e.type }; } + // In a store statement of an int32 kind loop's fast copy, a read that + // is not an int32 (a missing element's NaN) leaves for the + // statement's resume point, so the element is at hand as an i32. + if (!table && elem.kind === "f64" && access === "i32" && host.int32KindResume !== null) { + const read = emitDenseArrayNumberInt32( + host, + r.name, + index, + e.args[0], + host.provenDenseIndex(e.receiver, e.args[0]!), + host.int32KindResume, + ); + return { + name: read.value, + type: e.type, + uint32: read.int32, + integer: { name: read.int32, type: "i32", signed: true, range: INT32_RANGE }, + }; + } if (!table && elem.kind === "f64") return { name: emitDenseArrayNumber( diff --git a/packages/compiler/src/backend/llvm/int32-kind-loops.ts b/packages/compiler/src/backend/llvm/int32-kind-loops.ts index 12e38c1f0e..d596a731db 100644 --- a/packages/compiler/src/backend/llvm/int32-kind-loops.ts +++ b/packages/compiler/src/backend/llvm/int32-kind-loops.ts @@ -57,6 +57,8 @@ function isPure(e: IrExpr): boolean { case "arrayGet": case "arrayHas": return e.arr.kind === "varRef" && isPure(e.index); + case "arrIntrinsic": + return e.method === "getNumber" && e.receiver.kind === "varRef" && isPure(e.args[0]!); default: return false; } diff --git a/packages/compiler/src/backend/llvm/integer-clones.ts b/packages/compiler/src/backend/llvm/integer-clones.ts index 8e90747367..0ea3947b21 100644 --- a/packages/compiler/src/backend/llvm/integer-clones.ts +++ b/packages/compiler/src/backend/llvm/integer-clones.ts @@ -192,6 +192,8 @@ function cloneShape(fn: IrFunction, borrowed?: BorrowedParameter): Set | case "ternary": return expr(e.cond, "bool") && expr(e.then, want) && expr(e.else_, want); case "arrIntrinsic": + if (e.method === "getNumber") + return want === "num" && arrayRef(e.receiver) && expr(e.args[0]!, "num"); return ( want === "num" && e.method === "length" && e.args.length === 0 && arrayRef(e.receiver) ); @@ -269,6 +271,9 @@ function callsMatch(fn: IrFunction, byName: ReadonlyMap): bo case "arrayGet": visit(e.index); return; + case "arrIntrinsic": + if (e.method === "getNumber") visit(e.args[0]!); + return; default: return; } @@ -433,6 +438,8 @@ function callsAnything(e: IrExpr): boolean { return callsAnything(e.cond) || callsAnything(e.then) || callsAnything(e.else_); case "arrayGet": return callsAnything(e.index); + case "arrIntrinsic": + return e.method === "getNumber" && callsAnything(e.args[0]!); default: return false; } @@ -1021,10 +1028,10 @@ class CloneEmitter { * keeps every slot below `filled` a present value) whose element is * exactly an int32. Everything else (holes, indices outside the prefix, * other numbers) bails: the ordinary body reads it. */ - private emitElement(e: Extract): string { + private emitElement(arr: IrExpr, indexExpr: IrExpr): string { const { B } = this; - const array = this.emitArray(e.arr); - const index = this.narrow(this.emitNum(e.index)); + const array = this.emitArray(arr); + const index = this.narrow(this.emitNum(indexExpr)); const filledField = B.tmp(); const filled = B.tmp(); const inside = B.tmp(); @@ -1188,9 +1195,13 @@ class CloneEmitter { if (this.inlines(e)) return { name: this.emitInlineCall(e), wide: false }; return { name: this.emitCall(e), wide: true }; case "arrIntrinsic": + // `getNumber` reads an element like `arrayGet`: a missing one bails, + // and the ordinary body reads it as NaN. + if (e.method === "getNumber") + return { name: this.emitElement(e.receiver, e.args[0]!), wide: false }; return { name: this.emitLength(this.emitArray(e.receiver)), wide: false }; case "arrayGet": - return { name: this.emitElement(e), wide: false }; + return { name: this.emitElement(e.arr, e.index), wide: false }; case "unary": { const operand = this.narrow(this.emitNum(e.operand)); const value = B.tmp(); diff --git a/packages/compiler/src/contracts/check.ts b/packages/compiler/src/contracts/check.ts index 6493967b92..6d8a199568 100644 --- a/packages/compiler/src/contracts/check.ts +++ b/packages/compiler/src/contracts/check.ts @@ -1584,6 +1584,15 @@ class ImmutableContracts { for (let steps = 0; steps < 16; steps++) { if (root.kind === "fieldGet" || root.kind === "recordGet") root = root.obj; else if (root.kind === "arrayGet") root = root.arr; + else if (root.kind === "seqExpr") root = root.result; + // A receiver that is the element when it is present and otherwise + // throws (`xs[i]!.push(v)`) is that element read. + else if ( + root.kind === "ternary" && + root.else_.kind === "libCall" && + root.else_.fn === "error.nodeThrow" + ) + root = root.then; else if ( root.kind === "upcast" || root.kind === "downcast" || diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index cfc317ccd4..5c326ab1e0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -37,6 +37,7 @@ import { dynCompoundValue, lowerAbsenceProbe, lowerDynMemberAssignment, + lowerDereferencedElementAssignment, lowerOptionalNumber, lowerPresenceOperand, nullishBranches, @@ -1068,6 +1069,8 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr if (prototypeWrite) return prototypeWrite; const callableWrite = lowerNativeFunctionAssignment(lowerer, expr); if (callableWrite) return callableWrite; + const dereferenced = lowerDereferencedElementAssignment(lowerer, expr); + if (dereferenced) return dereferenced; } if ( op === ts.SyntaxKind.QuestionQuestionEqualsToken || diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 55db761173..9eacfa2b22 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -83,6 +83,8 @@ import { unitOnlyUnion } from "../type-mapper.js"; import { lowerYield } from "./lower-generators.js"; import { unionWideningTags } from "../../ir/analysis.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { everyExprChild } from "../../ir/traverse.js"; +import { declareContextLocal } from "./function-context.js"; import { hasOptionalChainGuard, isOptionalChainTail } from "./expressions/optional-chains.js"; import { classToStringConversion, classUnionToString } from "./class-to-primitive.js"; import { lowerCjsTopLevelThis, lowerRequireCacheElement } from "./lower-node-module.js"; @@ -413,6 +415,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const probe = lowerAbsenceProbe(lowerer, expr.expression); if (probe?.type.kind === "union") return probe; } + const dereferenced = dereferencedElementReceiver(lowerer, expr, loc); + if (dereferenced) return dereferenced; + const element = assertedElementOperand(lowerer, expr, loc); + if (element) return element; const inner = lowerer.lowerExpr(expr.expression); if (inner.type.kind === "union") { // `!` performs no runtime check: a missing array element stays @@ -1075,18 +1081,10 @@ function assertionOperand( loc: SrcLoc, ): IrExpr | null { if (inner.type.kind !== "union" || target === null || target.kind === "union") return null; - const arms = lowerer.unions.get(inner.type.unionId)?.arms; - if ( - !arms?.some(isUnitType) || - !arms.every( - (arm) => - arm.kind === "f64" || arm.kind === "string" || arm.kind === "bool" || isUnitType(arm), - ) - ) - return null; - // Array element reads keep their own handling of a missing element. A - // typed-array read is optional here only where its use tells undefined - // from NaN (assertedReadObservesUndefined). + if (!scalarUnion(lowerer, inner.type.unionId)) return null; + // Array element reads take assertedElementOperand or keep their own + // handling of a missing element. A typed-array read is optional here only + // where its use tells undefined from NaN (assertedReadObservesUndefined). let operand: ts.Expression = expr.expression; while (ts.isParenthesizedExpression(operand)) operand = operand.expression; if (ts.isElementAccessExpression(operand)) { @@ -1095,19 +1093,489 @@ function assertionOperand( } const use = convertingUseOf(lowerer, expr); if (use === null) return null; - if (use === "value") return inner; + if (use === "value" || use === "string") return inner; return lowerPrimitiveUnionNumber(lowerer, inner, expr, loc); } -/** How the converting use of `x!` (see assertionOperand) reads it: - * "number" where the use applies ToNumber, "value" where the consumer - * lowers the union itself (strings, equality, truthiness), null for other +/** `xs[i]!` on an array as the operand of a converting use (see + * assertionOperand). The read answers undefined for a missing element, as + * in Node, instead of trapping: a numeric use reads NaN for it, and other + * uses get the optional element value. Returns null for other reads and * uses. */ -function convertingUseOf(lowerer: Lowerer, expr: ts.NonNullExpression): "number" | "value" | null { +function assertedElementOperand( + lowerer: Lowerer, + expr: ts.NonNullExpression, + loc: SrcLoc, +): IrExpr | null { + let read: ts.Expression = expr.expression; + while (ts.isParenthesizedExpression(read)) read = read.expression; + if (!ts.isElementAccessExpression(read) || read.questionDotToken) return null; + // Plain arrays only: regex match results and other array subtypes keep + // their own element reads. + const receiverType = lowerer.typeOf(read.expression); + if ( + !lowerer.checker.isArrayType(receiverType) || + lowerer.mapTypeOf(receiverType)?.kind !== "array" + ) + return null; + // A counter below the length reads an existing index: the read keeps + // its plain element access. + if (lowerer.nanCodedReads?.counterIndexInBounds(read)) return null; + const target = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (!target || !isScalarArm(target)) return null; + const use = convertingUseOf(lowerer, expr); + if (use === null) return null; + if (use === "string" && target.kind === "string") + return stringElementOrUndefined(lowerer, read, loc); + const probe = lowerAbsenceProbe(lowerer, read); + if (probe?.type.kind !== "union" || !scalarUnion(lowerer, probe.type.unionId)) return null; + if (use === "value" || use === "string") return probe; + return lowerer.stripUndefinedArm(probe.type).kind === "f64" + ? lowerOptionalNumber(lowerer, probe, loc) + : lowerPrimitiveUnionNumber(lowerer, probe, expr, loc); +} + +/** `xs[i]!` on an array as the receiver of a member read + * (`xs[i]!.name`, `xs[i]!.size()`, `xs[i]![0]`). `!` performs no runtime + * check, so a missing element is undefined there and the member access + * throws Node's TypeError ("Cannot read properties of undefined (reading + * 'name')") instead of the bounds trap. The receiver is the element when it + * is present. A write (`xs[i]!.name = v`) is lowered as a statement by + * lowerDereferencedElementWrite, which evaluates the value before the + * receiver throws; elsewhere it keeps the bounds trap. Returns null for + * other reads and uses. */ +function dereferencedElementReceiver( + lowerer: Lowerer, + expr: ts.NonNullExpression, + loc: SrcLoc, +): IrExpr | null { + const use = runtimeOptionalUseOf(expr); + if (!use || isAssignmentTarget(use.access)) return null; + const parts = dereferencedElementParts(lowerer, expr, loc, false); + if (parts?.message == null) return null; + const message = parts.message; + const value: IrExpr = { + kind: "ternary", + cond: parts.present, + then: parts.element, + else_: nodeTypeError(message, parts.element.type, loc), + type: parts.element.type, + loc, + }; + return parts.stmts.length === 0 + ? value + : { kind: "seqExpr", stmts: parts.stmts, result: value, type: value.type, loc }; +} + +/** The parts of a member access through `xs[i]!` (see + * dereferencedElementReceiver): statements that evaluate the array and the + * index, whether the element is present, the element read, and Node's + * TypeError message for a missing one. A `write` holds its key, so its + * message may be left for dereferencedElementWrite to build from the key. */ +export function dereferencedElementParts( + lowerer: Lowerer, + expr: ts.NonNullExpression, + loc: SrcLoc, + write: boolean, +): { stmts: IrStmt[]; present: IrExpr; element: IrExpr; message: IrExpr | null } | null { + let read: ts.Expression = expr.expression; + while (ts.isParenthesizedExpression(read)) read = read.expression; + if (!ts.isElementAccessExpression(read) || read.questionDotToken) return null; + const receiverType = lowerer.typeOf(read.expression); + if ( + !lowerer.checker.isArrayType(receiverType) || + lowerer.mapTypeOf(receiverType)?.kind !== "array" + ) + return null; + if (lowerer.nanCodedReads?.counterIndexInBounds(read)) return null; + const use = runtimeOptionalUseOf(expr); + if (!use || use.optional || use.complex || use.comma || use.kind === "call") return null; + // A write holds its key, so the message can name any number or string + // key (dereferencedElementWrite builds it). + const message = + memberAccessMessage(lowerer, use.access) ?? + (write && ts.isElementAccessExpression(use.access) && namedKey(lowerer, use.access) + ? null + : undefined); + if (message === undefined) return null; + if (lowerer.mapTypeOf(lowerer.typeOf(read.argumentExpression))?.kind !== "f64") return null; + // The element must already have the asserted type: an element union the + // assertion or a guard narrows keeps its checked narrow. + const target = lowerer.mapTypeOf(lowerer.typeOf(expr)); + const declared = lowerer.mapTypeOf(receiverType); + if ( + !target || + declared?.kind !== "array" || + declared.elem.kind === "void" || + !typeEquals(declared.elem, target) + ) + return null; + const arr = lowerer.lowerExpr(read.expression); + if (arr.type.kind !== "array" || !typeEquals(arr.type.elem, target)) return null; + const elem = arr.type.elem; + const index = lowerer.lowerExprExpecting(read.argumentExpression, F64); + // Plain reads (a local, a field of one) evaluate once more instead of + // through a hidden local: the receiver then stays a borrowed element + // read, like the plain `xs[i]` it replaces. An index that can assign + // can replace the array binding or field (`xs[(xs = other, 0)]!`), so the + // array is held before it, as JavaScript reads it first. + const stmts: IrStmt[] = []; + const plain = (value: IrExpr): boolean => isSafeToRepeat(value) || plainProjection(value); + const held = (value: IrExpr, name: string): IrExpr => { + const tmp = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: tmp.id, init: value, loc }); + return varRef(tmp.id, value.type, loc); + }; + const a = !mayAssign(index) && plain(arr) ? arr : held(arr, "%elementArray"); + const i = plain(index) ? index : held(index, "%elementIndex"); + return { + stmts, + present: { + kind: "bin", + op: "===", + left: { kind: "arrayState", arr: a, index: i, type: F64, loc }, + right: numLit(1, loc), + type: BOOL, + loc, + }, + element: { kind: "arrayGet", arr: a, index: i, type: elem, loc }, + message, + }; +} + +/** A write through `xs[i]!` (`xs[i]!.name = v`, `xs[i]![k] = v`): as in + * Node, the array, the index, the key and the value evaluate in order (the + * statements), then a missing element makes the write throw Node's + * TypeError and a present one takes the write, which reads the element, + * the key and the value through `overrides`. Null for other targets. */ +export function dereferencedElementWrite( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): { + stmts: IrStmt[]; + present: IrExpr; + message: IrExpr; + overrides: { node: ts.Expression; value: IrExpr }[]; +} | null { + if (expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null; + const target = expr.left; + if ( + !(ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target)) || + target.questionDotToken || + lowerer.chainRecvByNode.has(target.expression) + ) + return null; + let receiver: ts.Expression = target.expression; + while (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression; + if (!ts.isNonNullExpression(receiver)) return null; + const loc = locOf(expr); + const keyValue = ts.isElementAccessExpression(target) + ? lowerer.lowerExpr(target.argumentExpression) + : null; + const rightValue = lowerer.lowerExpr(expr.right); + const parts = dereferencedElementParts(lowerer, receiver, loc, true); + if (parts === null) return null; + const stmts = [...parts.stmts]; + let present = parts.present; + let element = parts.element; + // JavaScript reads the element before the key and the value. When either + // has an effect, which can replace, remove or add the element, its + // presence and value are read first; otherwise the write tests and reads + // it after them. + if (!unobservable(rightValue) || (keyValue !== null && !unobservable(keyValue))) { + const presentLocal = lowerer.declareHiddenLocal("%elementPresent", BOOL); + // Assigned only when the element is present. + const elementLocal = declareContextLocal( + lowerer.ctx, + "%elementValue", + element.type, + true, + undefined, + undefined, + ); + stmts.push( + { kind: "varDecl", localId: presentLocal.id, init: present, loc }, + { kind: "varDecl", localId: elementLocal.id, init: null, loc }, + ); + present = varRef(presentLocal.id, BOOL, loc); + stmts.push({ + kind: "if", + cond: present, + then: [{ kind: "assign", localId: elementLocal.id, value: element, loc }], + else_: null, + loc, + }); + element = varRef(elementLocal.id, element.type, loc); + } + const overrides: { node: ts.Expression; value: IrExpr }[] = []; + // The write lowers the node with or without its parentheses. + const override = (node: ts.Expression, value: IrExpr): void => { + for (let n: ts.Expression = node; ; n = n.expression) { + overrides.push({ node: n, value }); + if (!ts.isParenthesizedExpression(n)) break; + } + }; + const held = (value: IrExpr, name: string, inline: boolean): IrExpr => { + if (inline) return value; + // A call without a result evaluates to undefined. + if (value.type.kind === "void") { + stmts.push({ kind: "exprStmt", expr: value, loc }); + return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }; + } + const tmp = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: tmp.id, init: value, loc }); + return varRef(tmp.id, value.type, loc); + }; + override(target.expression, element); + let message = parts.message; + if (ts.isElementAccessExpression(target) && keyValue !== null) { + // The key evaluates before the value, which can assign the binding or + // field it reads; a literal, or an unobservable key the value cannot + // change, may evaluate in the write. + const key = held( + keyValue, + "%elementWriteKey", + keyValue.kind === "numLit" || + keyValue.kind === "strLit" || + (unobservable(keyValue) && !mayAssign(rightValue)), + ); + override(target.argumentExpression, key); + // The message names the key's value as the write evaluated it. + if (key !== keyValue || message === null) + message = keyMessage(messageHead(target), key, loc) ?? message; + } + // A value whose evaluation is unobservable may evaluate in the write + // itself, after the element is tested. + override( + expr.right, + held(rightValue, "%elementWriteValue", isSafeToRepeat(rightValue) || unobservable(rightValue)), + ); + if (message === null) return null; + return { stmts, present, message, overrides }; +} + +/** dereferencedElementWrite in value position (`f(xs[i]!.name = v)`). */ +export function lowerDereferencedElementAssignment( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): IrExpr | null { + const write = dereferencedElementWrite(lowerer, expr); + if (write === null) return null; + const loc = locOf(expr); + const value = lowerer.withExpressionOverrides(write.overrides, () => lowerer.lowerExpr(expr)); + return { + kind: "seqExpr", + stmts: write.stmts, + result: { + kind: "ternary", + cond: write.present, + then: value, + else_: nodeTypeError(write.message, value.type, loc), + type: value.type, + loc, + }, + type: value.type, + loc, + }; +} + +/** Whether evaluating `e` has no effect and cannot throw: a discardable + * expression, or the compiler's element-or-undefined read of one. */ +function unobservable(e: IrExpr): boolean { + if (e.kind === "call" && e.callee.startsWith("%arr.idxOr.")) return e.args.every(unobservable); + return isSafeToDiscard(e); +} + +/** Whether evaluating `e` can assign a binding or a field: it contains an + * assignment, an increment, statements, a suspension, or a call that can + * run program code (a library call only through a function it is given). */ +function mayAssign(e: IrExpr): boolean { + switch (e.kind) { + case "call": + // The compiler's element-or-undefined read and union narrows run no + // program code. + if (!e.callee.startsWith("%arr.idxOr.") && !e.callee.startsWith("%union.narrow.")) + return true; + break; + case "assignExpr": + case "incDec": + case "seqExpr": + case "callValue": + case "virtualCall": + case "dynCall": + case "dynInvoke": + case "ffiCall": + case "new": + case "newValue": + case "awaitExpr": + case "awaitUnionExpr": + case "yieldExpr": + return true; + case "closure": + return false; + case "libCall": + if (e.args.some((arg) => arg.type.kind === "func")) return true; + break; + } + return !everyExprChild( + e, + (child) => !mayAssign(child), + () => false, + ); +} + +/** Node's TypeError, thrown in place of a value of `type`. */ +export function nodeTypeError(message: IrExpr, type: IrType, loc: SrcLoc): IrExpr { + return { + kind: "libCall", + fn: "error.nodeThrow", + args: [numLit(1, loc), strLit("", loc), message], + type, + loc, + }; +} + +/** Whether `access` is the target of `=`. */ +function isAssignmentTarget(access: ts.Expression): boolean { + let outer: ts.Expression = access; + while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent; + const parent = outer.parent; + return ( + ts.isBinaryExpression(parent) && + parent.left === outer && + parent.operatorToken.kind === ts.SyntaxKind.EqualsToken + ); +} + +/** A local, a number literal, or a field or record member read through a + * local: reading it again has no effect and yields the same value. */ +function plainProjection(e: IrExpr): boolean { + if (e.kind === "varRef" || e.kind === "numLit") return true; + return (e.kind === "fieldGet" || e.kind === "recordGet") && plainProjection(e.obj); +} + +/** Node's TypeError message for a member access on undefined: reading the + * member, or setting it when the access is the target of `=`. Null when + * the key is not a name, a literal, or a plain read of a number or string + * (which the message reads a second time). */ +function memberAccessMessage( + lowerer: Lowerer, + access: ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.CallExpression, +): IrExpr | null { + const head = messageHead(access); + const loc = locOf(access); + if (ts.isPropertyAccessExpression(access)) { + if (ts.isPrivateIdentifier(access.name)) return null; + return strLit(`${head}${access.name.text}')`, loc); + } + if (!ts.isElementAccessExpression(access)) return null; + const key = runtimeOptionalElementKey(access.argumentExpression); + if (key !== null) return strLit(`${head}${key}')`, loc); + let keyNode = access.argumentExpression; + while (ts.isParenthesizedExpression(keyNode)) keyNode = keyNode.expression; + if (!ts.isIdentifier(keyNode)) return null; + const keyType = lowerer.mapTypeOf(lowerer.typeOf(keyNode))?.kind; + if (keyType !== "f64" && keyType !== "string") return null; + const keyValue = lowerer.lowerExpr(keyNode); + if (!isSafeToRepeat(keyValue) || keyValue.type.kind !== keyType) return null; + return keyMessage(head, keyValue, loc); +} + +/** The start of Node's TypeError message for a member access on undefined: + * setting the member when the access is the target of `=`, else reading + * it. */ +function messageHead(access: ts.Expression): string { + return isAssignmentTarget(access) + ? "Cannot set properties of undefined (setting '" + : "Cannot read properties of undefined (reading '"; +} + +/** Whether `access`'s key is typed a number or a string, which the message + * can name from its value. */ +function namedKey(lowerer: Lowerer, access: ts.ElementAccessExpression): boolean { + const kind = lowerer.mapTypeOf(lowerer.typeOf(access.argumentExpression))?.kind; + return kind === "f64" || kind === "string"; +} + +/** The message naming a key from its value (a number or a string), or null + * for another type. */ +function keyMessage(head: string, key: IrExpr, loc: SrcLoc): IrExpr | null { + if (key.type.kind !== "f64" && key.type.kind !== "string") return null; + const text: IrExpr = + key.type.kind === "string" ? key : { kind: "toString", operand: key, type: STRING, loc }; + const concat = (left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "strConcat", + left, + right, + type: STRING, + loc, + }); + return concat(concat(strLit(head, loc), text), strLit("')", loc)); +} + +/** A string array's element converted to a string: the element, or + * "undefined" for a missing index or a hole. */ +function stringElementOrUndefined( + lowerer: Lowerer, + read: ts.ElementAccessExpression, + loc: SrcLoc, +): IrExpr | null { + if (lowerer.mapTypeOf(lowerer.typeOf(read.argumentExpression))?.kind !== "f64") return null; + const arr = lowerer.lowerExpr(read.expression); + if (arr.type.kind !== "array" || arr.type.elem.kind !== "string") return null; + const index = lowerer.lowerExprExpecting(read.argumentExpression, F64); + const stmts: IrStmt[] = []; + const held = (value: IrExpr, name: string): IrExpr => { + const tmp = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: tmp.id, init: value, loc }); + return varRef(tmp.id, value.type, loc); + }; + // An index that can assign can replace the array binding or field, so + // the array is held before it, as JavaScript reads it first. + const a = !mayAssign(index) && isSafeToRepeat(arr) ? arr : held(arr, "%elementArray"); + const i = isSafeToRepeat(index) ? index : held(index, "%elementIndex"); + const value: IrExpr = { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: { kind: "arrayState", arr: a, index: i, type: F64, loc }, + right: numLit(1, loc), + type: BOOL, + loc, + }, + then: { kind: "arrayGet", arr: a, index: i, type: STRING, loc }, + else_: strLit("undefined", loc), + type: STRING, + loc, + }; + return stmts.length === 0 ? value : { kind: "seqExpr", stmts, result: value, type: STRING, loc }; +} + +function isScalarArm(arm: IrType): boolean { + return arm.kind === "f64" || arm.kind === "string" || arm.kind === "bool" || isUnitType(arm); +} + +/** A union of numbers, strings, booleans, null and undefined with at least + * one unit arm. */ +function scalarUnion(lowerer: Lowerer, unionId: string): boolean { + const arms = lowerer.unions.get(unionId)?.arms; + return !!arms?.some(isUnitType) && arms.every(isScalarArm); +} + +/** How the converting use of `x!` (see assertionOperand) reads it: + * "number" where the use applies ToNumber, "string" where it converts the + * value to a string (a template span, `+` with a string), "value" where the + * consumer lowers the union itself (equality, truthiness, a comparison with + * a string), null for other uses. */ +function convertingUseOf( + lowerer: Lowerer, + expr: ts.NonNullExpression, +): "number" | "string" | "value" | null { let outer: ts.Expression = expr; while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent; const parent = outer.parent; - if (ts.isTemplateSpan(parent)) return parent.expression === outer ? "value" : null; + if (ts.isTemplateSpan(parent)) return parent.expression === outer ? "string" : null; if (ts.isTypeOfExpression(parent)) return "value"; if (ts.isPrefixUnaryExpression(parent)) { if (parent.operator === ts.SyntaxKind.ExclamationToken) return "value"; @@ -1140,7 +1608,7 @@ function convertingUseOf(lowerer: Lowerer, expr: ts.NonNullExpression): "number" if (op === ts.SyntaxKind.PlusEqualsToken && parent.right !== outer) return null; let partner = other; while (ts.isParenthesizedExpression(partner)) partner = partner.expression; - if (isString(other)) return ts.isNonNullExpression(partner) ? null : "value"; + if (isString(other)) return ts.isNonNullExpression(partner) ? null : "string"; if (!numericPartner(lowerer.typeOf(other))) return "value"; return isString(expr) ? null : "number"; } @@ -1226,7 +1694,8 @@ export function assertedReadObservesUndefined( lowerer: Lowerer, node: ts.NonNullExpression, ): boolean { - if (convertingUseOf(lowerer, node) === "value") return true; + const use = convertingUseOf(lowerer, node); + if (use === "value" || use === "string") return true; let outer: ts.Expression = node; while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent; const parent = outer.parent; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index ad191f58ae..6883f6fd62 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -242,10 +242,12 @@ import { globalSymbolKey } from "./expressions/global-symbols.js"; import { identifiersNamed } from "./identifier-uses.js"; import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js"; import { + dereferencedElementWrite, jsHoldsDynObject, lowerEnvironmentKey, lowerNativeFunctionAssignment, lowerStrictEquality, + nodeTypeError, } from "./lower-exprs.js"; import { keptIdentityAssertion } from "./assertion-identity.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; @@ -7992,6 +7994,29 @@ function lowerCjsSingleValueExport( * assignment, and `({ a, b } = e);` (destructuring ASSIGNMENT, which JS * requires parenthesized in statement position) lands in the * object-literal-target branch below. */ +/** dereferencedElementWrite in statement position. */ +function lowerDereferencedElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt | null { + const write = dereferencedElementWrite(lowerer, expr); + if (write === null) return null; + const loc = locOf(expr); + return { + kind: "block", + body: [ + ...write.stmts, + { + kind: "if", + cond: write.present, + then: [ + lowerer.withExpressionOverrides(write.overrides, () => lowerExprStatement(lowerer, expr)), + ], + else_: [{ kind: "exprStmt", expr: nodeTypeError(write.message, VOID, loc), loc }], + loc, + }, + ], + loc, + }; +} + export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStmt { const stmtNode = expr.parent; while (ts.isParenthesizedExpression(expr)) expr = expr.expression; @@ -8254,6 +8279,10 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm const ex = lowerExpandoAssignStmt(lowerer, expr); if (ex) return ex; } + { + const dereferenced = lowerDereferencedElementWrite(lowerer, expr); + if (dereferenced) return dereferenced; + } if (ts.isElementAccessExpression(expr.left)) return lowerer.lowerElementWrite(expr); if (ts.isPropertyAccessExpression(expr.left)) { fenceNodeModuleMutation(lowerer, expr.left, "assignment"); diff --git a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts index 24c981bd31..9456cfa188 100644 --- a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts +++ b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts @@ -451,6 +451,19 @@ export class NanCodedReads { // ---- present reads ------------------------------------------------------ + /** `xs[i]` whose index is a non-negative integer counter of the reading + * function and that a dominating `i < xs.length` guard bounds by the + * length: the read names an existing index. */ + counterIndexInBounds(read: ts.ElementAccessExpression): boolean { + const index = peelTransparent(read.argumentExpression); + if (!ts.isIdentifier(index)) return false; + const symbol = this.symbolOf(index); + if (!symbol || !this.nonNegativeCounter(symbol)) return false; + const decl = this.host.declarationOf(symbol); + if (!decl || this.containingFunction(decl) !== this.containingFunction(read)) return false; + return indexReadInBounds(read); + } + /** `t[i]` names an existing element: `i` is a non-negative integer * counter and a dominating guard bounds it by `t.length` (or by the * length of a typed array that always has the same length). diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 1334b9b997..be5f29768f 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -1484,6 +1484,39 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): continue; } const int32 = (e: IrExpr): boolean => withinInt32(ranges.get(e)); + // The range of an exact integer that may also be NaN: an element read + // of a candidate through `getNumber` (`a[k]!` in arithmetic, NaN for a + // missing element), and `+`, `-` and `*` over such values and integers. + // A checked store widens the array at a NaN like at any value that is + // not an int32. + const integerOrNaN = (e: IrExpr): IntegerRange | null => { + const range = ranges.get(e); + if (range) return range; + if (e.kind === "numLit") + return Number.isInteger(e.value) ? { min: e.value, max: e.value } : null; + if (e.kind === "arrIntrinsic" && e.method === "getNumber" && e.receiver.kind === "varRef") + return slots.hasInt32ElementSlot(int32ArraySlot(int32ArrayKey(fn.name, e.receiver.localId))) + ? INT32_RANGE + : null; + if (e.kind !== "bin" || (e.op !== "+" && e.op !== "-" && e.op !== "*")) return null; + const left = integerOrNaN(e.left); + const right = left && integerOrNaN(e.right); + if (!left || !right) return null; + const bounds = + e.op === "+" + ? [left.min + right.min, left.max + right.max] + : e.op === "-" + ? [left.min - right.max, left.max - right.min] + : [ + left.min * right.min, + left.min * right.max, + left.max * right.min, + left.max * right.max, + ]; + const min = Math.min(...bounds); + const max = Math.max(...bounds); + return Math.abs(min) < 2 ** 53 && Math.abs(max) < 2 ** 53 ? { min, max } : null; + }; // Every element stored into an int32 array candidate must be an int32 // under the facts that assume the candidates' own elements are; a // failing store drops its array's element facts, and the function is @@ -1513,6 +1546,10 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): for (const [id, values] of int32ArrayStores(fn, arrays)) { const key = int32ArrayKey(fn.name, id); if (!slots.hasInt32ElementSlot(int32ArraySlot(key)) || values.every(int32)) continue; + // Checked stores serve a lone local: its allocations are the only + // arrays it holds. Its stores are judged under the element facts + // the ranges were computed with. + const checkable = values.every((value) => int32(value) || integerOrNaN(value) !== null); // The array's group drops together: every function reading a // member is analyzed again without its facts. const group = arrayGroups.group.get(key) ?? [key]; @@ -1523,17 +1560,11 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): if (names) for (const name of names) requeue(name); } else requeue(member.slice(0, member.indexOf("\0"))); } - // Checked stores serve a lone local: its allocations are the only - // arrays it holds. - const checkable = (value: IrExpr): boolean => { - const range = ranges.get(value); - return int32(value) || range != null; - }; if ( group.length === 1 && !key.startsWith("\0") && !params.has(id) && - values.every(checkable) && + checkable && checkableInt32Allocations(fn, id) ) slots.checkedInt32Arrays.add(key); diff --git a/packages/compiler/src/ir/parameter-versions.ts b/packages/compiler/src/ir/parameter-versions.ts index 5156d4bff1..ca29585853 100644 --- a/packages/compiler/src/ir/parameter-versions.ts +++ b/packages/compiler/src/ir/parameter-versions.ts @@ -122,7 +122,7 @@ function numericLoop(loop: IrStmt, functions: ReadonlyMap): const target = functions.get(e.callee); allowed = target !== undefined && simple(target); } else if (e.kind === "libCall") allowed = e.fn.startsWith("math."); - else if (e.kind === "arrIntrinsic") allowed = e.method === "length"; + else if (e.kind === "arrIntrinsic") allowed = e.method === "length" || e.method === "getNumber"; return allowed && everyExprChild(e, expr, stmt); }; const stmt = (s: IrStmt): boolean => diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7393-non-null-element-operands.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7393-non-null-element-operands.ts.json new file mode 100644 index 0000000000..e50ca92f4d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7393-non-null-element-operands.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7393-non-null-element-operands.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7395-non-null-element-dereference.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7395-non-null-element-dereference.ts.json new file mode 100644 index 0000000000..17551e6266 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7395-non-null-element-dereference.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7395-non-null-element-dereference.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7393-non-null-element-operands.ts b/tests/corpus/7393-non-null-element-operands.ts new file mode 100644 index 0000000000..0fd7190a36 --- /dev/null +++ b/tests/corpus/7393-non-null-element-operands.ts @@ -0,0 +1,81 @@ +// Non-null assertions on array element reads whose index is out of bounds +// or names a hole, used as operands of operators that convert them. `!` +// performs no runtime check, so the read is undefined, as in Node: NaN in +// arithmetic, false in comparisons, "undefined" in strings. + +const values = [3, 1, 4, 1, 5]; +const words = ["alpha", "beta"]; +const flags = [true, false]; +const holey: number[] = []; +holey[3] = 7; + +function show(v: unknown): string { + return Object.is(v, -0) ? "-0" : String(v); +} + +function line(label: string, parts: unknown[]): void { + console.log(label.padEnd(10), parts.map(show).join(" ")); +} + +for (const k of [0, 4, 5, -1, 1.5, 100]) { + line(`k=${k}`, [ + values[k]! + 1, + values[k]! * 2, + -values[k]!, + values[k]! | 0, + values[k]! < 3, + values[k]! >= 1, + values[k]! === 1, + values[k]! !== 1, + `${values[k]!}`, + "v=" + values[k]!, + Math.max(values[k]!, 0), + words[k]! + "!", + `<${words[k]!}>`, + words[k]! < "b", + !flags[k]!, + ]); +} + +line("holes", [holey[0]! + 1, holey[3]! + 1, `${holey[1]!}`, holey[2]! === undefined, holey[9]! > 0]); + +// `+` with an untyped partner concatenates or adds when it runs. +const untypedText: any = "x"; +const untypedNumber: any = 5; +for (const k of [1, 9]) { + line(`untyped ${k}`, [ + values[k]! + untypedText, + untypedText + values[k]!, + values[k]! + untypedNumber, + words[k]! + untypedNumber, + words[k]! + untypedText, + ]); +} + +let sum = 0; +let text = ""; +for (let i = 0; i <= values.length; i++) { + sum += values[i]!; + text += words[i % 3]!; + text += ","; +} +line("compound", [sum, text]); + +// A counter array: an index past the end reads NaN, and the store widens +// the array to doubles. +function counter(n: number, m: number): number[] { + const a = new Array(n).fill(0) as number[]; + for (let i = 0; i < 6000; i++) a[i % m] = a[i % m]! + 1; + return [a[0]!, a[m - 1]!, a.length, a[n + 1]! + 0]; +} +line("counter", counter(5000, 5000)); +line("past", counter(5000, 5003)); +line("zero", counter(4, 0)); + +const grid = [ + [1, 2], + [3, 4], +]; +let total = 0; +for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) total += grid[r % 2]![c]! * 1; +line("grid", [total]); diff --git a/tests/corpus/7395-non-null-element-dereference.ts b/tests/corpus/7395-non-null-element-dereference.ts new file mode 100644 index 0000000000..67d9249d18 --- /dev/null +++ b/tests/corpus/7395-non-null-element-dereference.ts @@ -0,0 +1,142 @@ +// Member reads and writes through non-null asserted array element reads. +// `!` performs no runtime check: a missing element is undefined, so the +// member access throws Node's TypeError, which the program can catch. + +class Cell { + count = 0; + label: string; + constructor(label: string) { + this.label = label; + } + bump(): number { + return ++this.count; + } +} + +function cell(label: string): Cell { + return new Cell(label); +} + +const cells = [cell("a"), cell("b")]; +const words = ["alpha", "beta"]; +const grid = [ + [1, 2], + [3, 4], +]; +const maps = [new Map([["x", 1]])]; +const holey: Cell[] = []; +holey[2] = cell("z"); +let reads = 0; +function at(n: number): number { + reads++; + return n; +} + +function attempt(label: string, f: () => unknown): void { + try { + console.log(label.padEnd(12), "ok", f()); + } catch (e) { + const error = e as Error; + console.log(label.padEnd(12), error instanceof TypeError, error.message); + } +} + +for (const k of [0, 1, 2, -1]) { + attempt(`label ${k}`, () => cells[k]!.label); + attempt(`bump ${k}`, () => cells[k]!.bump()); + attempt(`set ${k}`, () => (cells[k]!.label = "set")); + attempt(`count++ ${k}`, () => cells[k]!.count++); + attempt(`length ${k}`, () => words[k]!.length); + attempt(`upper ${k}`, () => words[k]!.toUpperCase()); + attempt(`grid ${k}`, () => grid[k]![0]); + attempt(`row ${k}`, () => { + const j = 1; + return grid[k]![j]; + }); + attempt(`push ${k}`, () => grid[k]!.push(9)); + attempt(`map ${k}`, () => maps[k]!.get("x")); +} + +attempt("hole", () => holey[0]!.label); +attempt("hole value", () => holey[2]!.label); +attempt("order", () => cells[at(7)]!.count + at(1)); +console.log("reads", reads); + +let total = 0; +try { + for (let i = 0; i < 4; i++) total += grid[i]![0]!; +} catch (e) { + console.log("loop", (e as Error).message, total); +} +console.log(cells.map((c) => `${c.label}:${c.count}`).join(" ")); + +// A write through a missing element evaluates the key and the value before +// it throws; an error the value throws is the one that propagates. +const events: string[] = []; +const note = (event: string, value: number): number => { + events.push(event); + return value; +}; +attempt("write order", () => { + cells[note("index", 7)]!.count = note("value", 1); +}); +attempt("write key order", () => { + grid[note("row", 9)]![note("column", 0)] = note("cell", 5); +}); +attempt("write literal key", () => { + grid[9]![0] = note("cell 0", 5); +}); +attempt("value throws", () => { + cells[7]!.count = (() => { + throw new Error("from the value"); + })(); +}); +attempt("present write", () => { + cells[0]!.count = note("present", 41) + 1; + return cells[0]!.count; +}); +console.log("events", events.join(",")); + +// The array is read before an index that replaces it. +let current = [new Cell("first")]; +const replace = (): number => { + current = []; + return 0; +}; +attempt("replaced read", () => current[replace()]!.label); +current = [new Cell("second")]; +attempt("replaced write", () => { + current[replace()]!.label = "written"; +}); +let names = ["kept"]; +attempt("replaced string", () => `${names[(names = [], 0)]!}`); +console.log(current.length, names.length); + +// The element is read before the key and the value: a value that replaces, +// removes or adds it writes to, or misses, the element the target read, and +// a key the value reassigns is the one read before it. +const slots = [new Cell("p"), new Cell("q")]; +const original = slots[0]!; +attempt("value replaces", () => { + slots[0]!.label = ((slots[0] = new Cell("r")), "set"); + return `${original.label} ${slots[0]!.label}`; +}); +const tail = slots[1]!; +attempt("value removes", () => { + slots[1]!.label = (slots.pop(), "gone"); + return `${tail.label} ${slots.length}`; +}); +attempt("value adds", () => { + slots[1]!.label = (slots.push(new Cell("s")), "added"); +}); +const table: number[][] = [[1, 2]]; +let column = 0; +attempt("value moves key", () => { + table[0]![column] = ((column = 1), 5); + return table[0]!.join(","); +}); +column = 0; +attempt("value moves missing key", () => { + table[4]![column] = ((column = 1), 5); +}); +console.log(slots.length, column); diff --git a/tests/harness/corpus-costs/7393-non-null-element-operands.ts.json b/tests/harness/corpus-costs/7393-non-null-element-operands.ts.json new file mode 100644 index 0000000000..e053c13c8a --- /dev/null +++ b/tests/harness/corpus-costs/7393-non-null-element-operands.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3800 } diff --git a/tests/harness/corpus-costs/7395-non-null-element-dereference.ts.json b/tests/harness/corpus-costs/7395-non-null-element-dereference.ts.json new file mode 100644 index 0000000000..e0d5a6a6d9 --- /dev/null +++ b/tests/harness/corpus-costs/7395-non-null-element-dereference.ts.json @@ -0,0 +1 @@ +{ "plain": 3585, "san": 5768 }