mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
feat: support indexed compound assignments
- Compile array, typed-array, and Buffer compounds in statements and expressions. - Preserve JavaScript evaluation order and add differential coverage for supported operators.
This commit is contained in:
@@ -12,11 +12,11 @@ import type { Lowerer } from "./lowerer.js";
|
||||
import { wasiGuestPath } from "../../wasi-paths.js";
|
||||
import { BIGINT_T, BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, IrExpr, IrFunction, IrJsOp, IrLocal, IrRecordShape, IrStmt, IrType, JSVAL, NULL_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_ERROR_CLASSES, SEARCH_PARAMS_T, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, canAdaptDynFuncTo, canBoxFuncIntoDyn, funcOf, isDynTypedRefType, isJsonSafeType, isSupportedArrayElem, isUnitType, jsOpResultKind, shapeHasAccessorSlots, typeEquals, typeKey, unionFuncSetArmsOk } from "../../ir/ir.js";
|
||||
import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReason, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeEsmFile, locOf } from "../program.js";
|
||||
import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModulesArrayLit, builtinModuleFnOf, CompoundOp, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js";
|
||||
import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModulesArrayLit, builtinModuleFnOf, COMPOUND_ASSIGN_OPS, CompoundOp, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js";
|
||||
import { UNSUPPORTED, blockedBindingUseDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
|
||||
import { lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall } from "./lower-containers.js";
|
||||
import { arrayValueStore } from "./array-values.js";
|
||||
import { arrayValueRead, arrayValueStore } from "./array-values.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
import { unsupportedModuleFeatureOf } from "../builtin-modules.js";
|
||||
import { fenceEnumObjectValue, lowerEnumAccess } from "./lower-enums.js";
|
||||
@@ -4902,6 +4902,75 @@ export function lowerOptionalNumber(
|
||||
return Number.isInteger(n) && n >= 0 ? n : null;
|
||||
}
|
||||
|
||||
/** `a[i] op= rhs` over native arrays and byte views. A compound assignment
|
||||
* evaluates the receiver and index, reads the old element, evaluates rhs,
|
||||
* then writes. Capture each stage so a call in the index or rhs cannot
|
||||
* change the target or the value used by the operator. The expression yields
|
||||
* the computed value before a typed array or Buffer coerces it for storage. */
|
||||
export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression, op: CompoundOp): IrExpr {
|
||||
const target = expr.left as ts.ElementAccessExpression;
|
||||
fenceNodeModuleMutation(lowerer, target, "assignment");
|
||||
const loc = locOf(expr);
|
||||
const receiverType = lowerer.mapTypeOf(lowerer.typeOf(target.expression));
|
||||
if (receiverType?.kind !== "array" && receiverType?.kind !== "bytes") {
|
||||
lowerer.unsupported("SC1090", target, "compound assignment to non-array elements");
|
||||
}
|
||||
let receiver = lowerer.lowerExpr(target.expression);
|
||||
if (receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0) {
|
||||
const present = lowerer.stripUndefinedArm(receiver.type);
|
||||
const helper = present.kind === receiverType.kind
|
||||
? lowerer.narrowedArmHelper(receiver.type.unionId, present, locOf(target.expression))
|
||||
: null;
|
||||
if (helper) receiver = { kind: "call", callee: helper, args: [receiver], type: present, loc: locOf(target.expression) };
|
||||
}
|
||||
if (receiver.type.kind !== receiverType.kind) {
|
||||
lowerer.unsupported("SC1090", target.expression, "compound assignment through a non-native array or byte view");
|
||||
}
|
||||
const index = lowerer.lowerExpr(target.argumentExpression);
|
||||
if (index.type.kind !== "f64") lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys");
|
||||
const receiverLocal = lowerer.declareHiddenLocal("%compoundArray", receiver.type);
|
||||
const indexLocal = lowerer.declareHiddenLocal("%compoundIndex", F64);
|
||||
const receiverRef = (): IrExpr => varRef(receiverLocal.id, receiver.type, loc);
|
||||
const indexRef = (): IrExpr => varRef(indexLocal.id, F64, loc);
|
||||
const oldValue: IrExpr = receiver.type.kind === "array"
|
||||
? arrayValueRead(lowerer, receiverRef(), indexRef(), receiver.type.elem, locOf(target))
|
||||
: { kind: "bytesIntrinsic", method: "get", receiver: receiverRef(), args: [indexRef()], type: F64, loc: locOf(target) };
|
||||
const oldLocal = lowerer.declareHiddenLocal("%compoundOld", oldValue.type);
|
||||
const oldRef = (): IrExpr => varRef(oldLocal.id, oldValue.type, loc);
|
||||
const rhs = lowerer.lowerExpr(expr.right);
|
||||
const numericRhs = lowerOptionalNumber(lowerer, rhs, loc);
|
||||
const elementType = receiver.type.kind === "array" ? receiver.type.elem : F64;
|
||||
const valueType = elementType.kind === "union" && lowerer.armTag(elementType.unionId, UNDEFINED_T) >= 0
|
||||
? lowerer.stripUndefinedArm(elementType)
|
||||
: elementType;
|
||||
let computed: IrExpr;
|
||||
if (op === "+" && valueType.kind === "string") {
|
||||
computed = { kind: "strConcat", left: lowerer.ensureString(oldRef(), target), right: lowerer.ensureString(rhs, expr.right), type: STRING, loc };
|
||||
} else if (valueType.kind === "f64" && numericRhs.type.kind === "f64") {
|
||||
computed = { kind: "bin", op, left: lowerOptionalNumber(lowerer, oldRef(), loc), right: numericRhs, type: F64, loc };
|
||||
} else {
|
||||
lowerer.unsupported("SC1043", expr);
|
||||
}
|
||||
const resultLocal = lowerer.declareHiddenLocal("%compoundResult", computed.type);
|
||||
const resultRef = (): IrExpr => varRef(resultLocal.id, computed.type, loc);
|
||||
const write: IrStmt = receiver.type.kind === "array"
|
||||
? arrayValueStore(lowerer, receiverRef(), indexRef(), resultRef(), receiver.type.elem, loc)
|
||||
: { kind: "bytesSet", arr: receiverRef(), index: indexRef(), value: resultRef(), loc };
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: receiverLocal.id, init: receiver, loc },
|
||||
{ kind: "varDecl", localId: indexLocal.id, init: index, loc },
|
||||
{ kind: "varDecl", localId: oldLocal.id, init: oldValue, loc },
|
||||
{ kind: "varDecl", localId: resultLocal.id, init: computed, loc },
|
||||
write,
|
||||
],
|
||||
result: resultRef(),
|
||||
type: computed.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `a[i] = v` in statement position → arraySet (element writes, like local
|
||||
* assignment, produce no value in our subset). */
|
||||
export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt {
|
||||
@@ -5814,8 +5883,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
// globals, captured/boxed included); the RHS coerces into the binding's
|
||||
// type exactly like statement position, and the expression's value is
|
||||
// the coerced binding-typed value (representation change only — never
|
||||
// observably different from JS's raw-RHS yield). Compound operators,
|
||||
// property/element targets, and destructuring targets stay fenced.
|
||||
// observably different from JS's raw-RHS yield). Indexed compounds
|
||||
// use a sequenced read/modify/write; other member targets and
|
||||
// destructuring targets stay fenced.
|
||||
if (op === ts.SyntaxKind.EqualsToken && ts.isIdentifier(expr.left)) {
|
||||
const target = lowerer.resolveWritable(expr.left);
|
||||
if (!target) {
|
||||
@@ -5826,6 +5896,10 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
if (lowerer.runtimeOptionalStorageLocals.has(runtimeOptionalRoot)) lowerer.runtimeOptionalLocals.add(runtimeOptionalRoot);
|
||||
return { kind: "assignExpr", localId: target.id, value, type: target.type, loc };
|
||||
}
|
||||
const indexedCompound = COMPOUND_ASSIGN_OPS[op];
|
||||
if (indexedCompound !== undefined && ts.isElementAccessExpression(expr.left)) {
|
||||
return lowerElementCompound(lowerer, expr, indexedCompound);
|
||||
}
|
||||
// `events.defaultMaxListeners = v` — the module-property write
|
||||
// Node validates (validateNumber(n, 'defaultMaxListeners', 0)):
|
||||
// the value crosses into the checked-dynamic tree and the runtime ladder throws
|
||||
|
||||
@@ -25,7 +25,7 @@ import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-strea
|
||||
import { lowerHttpResPropertyAssignment, lowerHttpServerTimeoutAssignment, lowerServerCloseOverrideAssignment } from "./lower-server.js";
|
||||
import { builtinMemberRequireDecl, builtinNamespaceDestructureModuleOf, createRequireBindingDecl, createRequireCalleeFileOf, createRequireNamespaceDecl, createRequireProgramModuleDecl, createRequireProgramModuleOf, lowerNodeModuleCall, registerBuiltinCallableAlias, textCodecBindingDecl } from "./lower-builtins.js";
|
||||
import { lowerEnumDeclaration } from "./lower-enums.js";
|
||||
import { abstractPropertyDeclOf, aliasTypeofNarrows, isMatchSliceType, lowerAbsenceProbe, lowerGroupsProjection, lowerOptionalNumber, matchResultNamedGroupsOf, runtimeOptionalTrueIds, symbolFieldInfo, withRuntimeOptionalNarrowed } from "./lower-exprs.js";
|
||||
import { abstractPropertyDeclOf, aliasTypeofNarrows, isMatchSliceType, lowerAbsenceProbe, lowerElementCompound, lowerGroupsProjection, lowerOptionalNumber, matchResultNamedGroupsOf, runtimeOptionalTrueIds, symbolFieldInfo, withRuntimeOptionalNarrowed } from "./lower-exprs.js";
|
||||
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { UNSUPPORTED, checkerPanicDiag, isCheckerPanic, requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
|
||||
@@ -5340,12 +5340,12 @@ function isEsModuleStamp(expr: ts.Expression): boolean {
|
||||
// Desugar `x op= e` to `x = x op e` — reads x before e, like JS.
|
||||
if (ts.isElementAccessExpression(expr.left)) {
|
||||
// `this[kLimit] += v` — a declared symbol-keyed class field is
|
||||
// the dotted compound in element spelling; every other element
|
||||
// target keeps the fence.
|
||||
// the dotted compound in element spelling. Array and byte-view
|
||||
// elements use the sequenced indexed compound path.
|
||||
if (symbolFieldInfo(lowerer, expr.left)) {
|
||||
return lowerer.lowerFieldCompound(expr.left, compound, expr.right, locOf(expr));
|
||||
}
|
||||
lowerer.unsupported("SC1090", expr.left, "compound array-element assignment (a[i] += v)");
|
||||
return { kind: "exprStmt", expr: lowerElementCompound(lowerer, expr, compound), loc: locOf(expr) };
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr.left)) {
|
||||
// `N.x += v` — the namespace-qualified spelling of a module-
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Indexed compounds use one receiver and index evaluation, read before the
|
||||
// RHS, and yield the pre-storage value for typed arrays and Buffers.
|
||||
const numbers = [10, 13, 6, 24, 17, 3, 14, 8, 12, 3, -16, -1];
|
||||
numbers[0] += 5;
|
||||
numbers[1] -= 4;
|
||||
numbers[2] *= 3;
|
||||
numbers[3] /= 4;
|
||||
numbers[4] %= 6;
|
||||
numbers[5] **= 3;
|
||||
numbers[6] &= 11;
|
||||
numbers[7] |= 3;
|
||||
numbers[8] ^= 5;
|
||||
numbers[9] <<= 2;
|
||||
numbers[10] >>= 2;
|
||||
numbers[11] >>>= 1;
|
||||
console.log("operators", numbers.join(","));
|
||||
|
||||
const words = ["first", "second"];
|
||||
const joined = words[0] += "!";
|
||||
console.log("strings", joined, words[0]);
|
||||
const missingWord = words[3] += "?";
|
||||
console.log("missing-string", missingWord, words.length);
|
||||
|
||||
const holes: number[] = [4];
|
||||
const missing = holes[2] += 5;
|
||||
console.log("missing", Number.isNaN(missing), Number.isNaN(holes[2]), holes.length, 1 in holes);
|
||||
|
||||
let trace = "";
|
||||
const original = [8];
|
||||
const other = [100];
|
||||
let current = original;
|
||||
let nextIndex = 0;
|
||||
function receiver(): number[] { trace += "R"; return current; }
|
||||
function index(): number { trace += "I"; return nextIndex++; }
|
||||
function rhs(): number { trace += "V"; original[0] = 50; current = other; return 3; }
|
||||
const changed = receiver()[index()] += rhs();
|
||||
console.log("order", trace, changed, original[0], other[0], nextIndex);
|
||||
|
||||
const grid = [[4]];
|
||||
const nested = grid[0][0] += 2;
|
||||
console.log("nested", nested, grid[0][0]);
|
||||
|
||||
const bytes = new Uint8Array([255, 10]);
|
||||
const byteResult = bytes[0] += 2;
|
||||
bytes[1] <<= 2;
|
||||
console.log("uint8", byteResult, bytes[0], bytes[1]);
|
||||
|
||||
const unsigned = new Uint32Array(1);
|
||||
unsigned[0] = 4294967295;
|
||||
const unsignedResult = unsigned[0] += 2;
|
||||
console.log("uint32", unsignedResult, unsigned[0]);
|
||||
|
||||
const signed = new Int32Array(1);
|
||||
signed[0] = -4;
|
||||
signed[0] >>= 1;
|
||||
console.log("int32", signed[0]);
|
||||
|
||||
const singles = new Float32Array(1);
|
||||
singles[0] = 0.5;
|
||||
singles[0] *= 1.5;
|
||||
console.log("float32", singles[0]);
|
||||
|
||||
const floats = new Float64Array(1);
|
||||
floats[0] = 0.5;
|
||||
floats[0] *= 1.5;
|
||||
console.log("float64", floats[0]);
|
||||
|
||||
const buffer = Buffer.from([254, 3]);
|
||||
const bufferResult = buffer[0] += 5;
|
||||
buffer[1] ^= 6;
|
||||
console.log("buffer", bufferResult, buffer[0], buffer[1]);
|
||||
Reference in New Issue
Block a user