mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Read and write the "undefined" property for element indexes that are undefined (#1930)
This commit is contained in:
@@ -1583,6 +1583,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"regex.resultProp": "scr_regex_result_prop",
|
||||
"array.namedKeys": "scr_arr_named_keys",
|
||||
"array.hasNamedKey": "scr_arr_has_named_key",
|
||||
"array.undefinedKey": "scr_arr_undefined_key",
|
||||
// queueMicrotask's checked-dynamic form (borrowed dyn; a non-function
|
||||
// throws synchronously), the minted setImmediate value, and the
|
||||
// timers/promises immediate — all mark the loop live.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { lowerOptionalIndex } from "../undefined-index.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { DYN, type IrExpr, type IrStmt, isUnitType } from "../../../ir/ir.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { completeFuncValueArgs } from "../call-arguments.js";
|
||||
import { lowerOptionalNumber } from "../lower-exprs.js";
|
||||
import { lowerSafeIndexRead } from "../array-values.js";
|
||||
|
||||
/** Retain native function slots during indexed calls. The array remains
|
||||
@@ -37,7 +37,7 @@ export function lowerArrayCallbackCall(
|
||||
return varRef(local.id, local.type, loc);
|
||||
};
|
||||
const receiver = store(array, "%callbackArray");
|
||||
const index = lowerOptionalNumber(
|
||||
const index = lowerOptionalIndex(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(access.argumentExpression),
|
||||
locOf(access.argumentExpression),
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
lowerOptionalIndex,
|
||||
splitUndefinedIndex,
|
||||
undefinedTypedArrayKeyFence,
|
||||
} from "../undefined-index.js";
|
||||
import { cleanFuncAdaptable } from "../coercions/function-adapters.js";
|
||||
import { type Lowerer, jsFuncNameOf } from "../lowerer.js";
|
||||
import {
|
||||
@@ -1160,12 +1165,26 @@ 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 = lowerOptionalNumber(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(expr.left.argumentExpression),
|
||||
locOf(expr.left.argumentExpression),
|
||||
expr.left.argumentExpression,
|
||||
);
|
||||
// An undefined index names the "undefined" property
|
||||
// (undefined-index.ts), which a typed array cannot hold.
|
||||
const rawIndex = lowerer.lowerExpr(expr.left.argumentExpression);
|
||||
const split =
|
||||
recv.type.kind === "bytes"
|
||||
? splitUndefinedIndex(
|
||||
lowerer,
|
||||
rawIndex,
|
||||
expr.left.argumentExpression,
|
||||
locOf(expr.left.argumentExpression),
|
||||
)
|
||||
: null;
|
||||
const index =
|
||||
split?.index ??
|
||||
lowerOptionalIndex(
|
||||
lowerer,
|
||||
rawIndex,
|
||||
locOf(expr.left.argumentExpression),
|
||||
expr.left.argumentExpression,
|
||||
);
|
||||
if (index.type.kind !== "f64")
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1178,7 +1197,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
const result = varRef(value.id, raw.type, loc);
|
||||
const arr = varRef(receiver.id, recv.type, loc);
|
||||
const idx = varRef(key.id, F64, loc);
|
||||
const write: IrStmt =
|
||||
const store: IrStmt =
|
||||
recv.type.kind === "array"
|
||||
? arrayValueStore(lowerer, arr, idx, result, recv.type.elem, loc)
|
||||
: {
|
||||
@@ -1188,10 +1207,20 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
value: lowerer.coerceInto(expr.right, result, F64),
|
||||
loc,
|
||||
};
|
||||
const write: IrStmt = split
|
||||
? {
|
||||
kind: "if",
|
||||
cond: split.isUndefined,
|
||||
then: [undefinedTypedArrayKeyFence(lowerer, expr.left, loc)],
|
||||
else_: [store],
|
||||
loc,
|
||||
}
|
||||
: store;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: receiver.id, init: recv, loc },
|
||||
...(split?.stmts ?? []),
|
||||
{ kind: "varDecl", localId: key.id, init: index, loc },
|
||||
{ kind: "varDecl", localId: value.id, init: raw, loc },
|
||||
write,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/* Element reads and writes (`a[i]`, `a[i] = v`), compound element updates, and keyed record reads. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
lowerOptionalIndex,
|
||||
splitUndefinedIndex,
|
||||
undefinedTypedArrayKeyFence,
|
||||
} from "../undefined-index.js";
|
||||
import { type Lowerer, jsFuncNameOf, neverTaintedJsType } from "../lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -779,9 +784,9 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
}
|
||||
}
|
||||
// Array reads can themselves supply an index. A missing numeric value
|
||||
// is undefined, whose property lookup misses just like NaN; keep that
|
||||
// value instead of rejecting a checker-number index with optional IR.
|
||||
const index = lowerOptionalNumber(
|
||||
// is undefined, which names the array's "undefined" property
|
||||
// (undefined-index.ts).
|
||||
const index = lowerOptionalIndex(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(expr.argumentExpression),
|
||||
locOf(expr.argumentExpression),
|
||||
@@ -1531,12 +1536,26 @@ export function lowerElementCompoundParts(
|
||||
"compound assignment through a non-native array or byte view",
|
||||
);
|
||||
}
|
||||
const index = lowerOptionalNumber(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(target.argumentExpression),
|
||||
locOf(target.argumentExpression),
|
||||
target.argumentExpression,
|
||||
);
|
||||
// An undefined index names the "undefined" property (undefined-index.ts),
|
||||
// which a typed array cannot hold.
|
||||
const rawIndex = lowerer.lowerExpr(target.argumentExpression);
|
||||
const split =
|
||||
receiver.type.kind === "bytes"
|
||||
? splitUndefinedIndex(
|
||||
lowerer,
|
||||
rawIndex,
|
||||
target.argumentExpression,
|
||||
locOf(target.argumentExpression),
|
||||
)
|
||||
: null;
|
||||
const index =
|
||||
split?.index ??
|
||||
lowerOptionalIndex(
|
||||
lowerer,
|
||||
rawIndex,
|
||||
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);
|
||||
@@ -1645,15 +1664,25 @@ export function lowerElementCompoundParts(
|
||||
}
|
||||
const resultLocal = lowerer.declareHiddenLocal("%compoundResult", computed.type);
|
||||
const resultRef = (): IrExpr => varRef(resultLocal.id, computed.type, loc);
|
||||
const write: IrStmt =
|
||||
const store: IrStmt =
|
||||
receiver.type.kind === "array"
|
||||
? arrayValueStore(lowerer, receiverRef(), indexRef(), resultRef(), receiver.type.elem, loc)
|
||||
: { kind: "bytesSet", arr: receiverRef(), index: indexRef(), value: resultRef(), loc };
|
||||
const write: IrStmt = split
|
||||
? {
|
||||
kind: "if",
|
||||
cond: split.isUndefined,
|
||||
then: [undefinedTypedArrayKeyFence(lowerer, target, loc)],
|
||||
else_: [store],
|
||||
loc,
|
||||
}
|
||||
: store;
|
||||
return {
|
||||
expr: {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: receiverLocal.id, init: receiver, loc },
|
||||
...(split?.stmts ?? []),
|
||||
{ kind: "varDecl", localId: indexLocal.id, init: index, loc },
|
||||
{ kind: "varDecl", localId: oldLocal.id, init: oldValue, loc },
|
||||
{ kind: "varDecl", localId: resultLocal.id, init: computed, loc },
|
||||
@@ -1880,13 +1909,23 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
|
||||
// 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);
|
||||
// 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(
|
||||
// undefined (a hole read) converts with ToNumber, as the reads do; an
|
||||
// undefined index names a property the typed array cannot hold.
|
||||
const rawIndex = lowerer.lowerExpr(target.argumentExpression);
|
||||
const split = splitUndefinedIndex(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(target.argumentExpression),
|
||||
locOf(target.argumentExpression),
|
||||
rawIndex,
|
||||
target.argumentExpression,
|
||||
locOf(target.argumentExpression),
|
||||
);
|
||||
const index =
|
||||
split?.index ??
|
||||
lowerOptionalNumber(
|
||||
lowerer,
|
||||
rawIndex,
|
||||
locOf(target.argumentExpression),
|
||||
target.argumentExpression,
|
||||
);
|
||||
if (index.type.kind === "dyn" && recv.type.kind === "bytes") {
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
@@ -1902,7 +1941,34 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
|
||||
lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys");
|
||||
}
|
||||
const value = lowerer.lowerExprExpecting(expr.right, F64);
|
||||
return { kind: "bytesSet", arr: recv, index, value, loc: locOf(expr) };
|
||||
if (split === null) return { kind: "bytesSet", arr: recv, index, value, loc: locOf(expr) };
|
||||
const loc = locOf(expr);
|
||||
const receiver = lowerer.declareHiddenLocal("%indexedBytes", recv.type);
|
||||
const valueLocal = lowerer.declareHiddenLocal("%elementValue", F64);
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: receiver.id, init: recv, loc },
|
||||
...split.stmts,
|
||||
{ kind: "varDecl", localId: valueLocal.id, init: value, loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: split.isUndefined,
|
||||
then: [undefinedTypedArrayKeyFence(lowerer, target, loc)],
|
||||
else_: [
|
||||
{
|
||||
kind: "bytesSet",
|
||||
arr: varRef(receiver.id, recv.type, loc),
|
||||
index,
|
||||
value: varRef(valueLocal.id, F64, loc),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// Tuple element write `t[0] = v`: a positional-field write (recordSet)
|
||||
// — literal indices only, like the read.
|
||||
@@ -2197,7 +2263,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
|
||||
"assignment through a possibly missing nested array receiver",
|
||||
);
|
||||
}
|
||||
const index = lowerOptionalNumber(
|
||||
const index = lowerOptionalIndex(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(target.argumentExpression),
|
||||
locOf(target.argumentExpression),
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/* Increment, decrement and prefix unary operators, and compound field updates. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
lowerOptionalIndex,
|
||||
splitUndefinedIndex,
|
||||
undefinedTypedArrayKeyFence,
|
||||
} from "../undefined-index.js";
|
||||
import { isStrictCode } from "../strict-mode.js";
|
||||
import { isPrivateWriteReceiver } from "../private-brands.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
@@ -311,12 +316,27 @@ export function lowerIncDec(
|
||||
(raw.type.kind === "array" || raw.type.kind === "bytes")
|
||||
) {
|
||||
const receiver = save(raw, "%incrementArray");
|
||||
const rawIndex = lowerOptionalNumber(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(access.argumentExpression),
|
||||
locOf(access.argumentExpression),
|
||||
access.argumentExpression,
|
||||
);
|
||||
// An undefined index names the "undefined" property
|
||||
// (undefined-index.ts), which a typed array cannot hold.
|
||||
const loweredIndex = lowerer.lowerExpr(access.argumentExpression);
|
||||
const split =
|
||||
raw.type.kind === "bytes"
|
||||
? splitUndefinedIndex(
|
||||
lowerer,
|
||||
loweredIndex,
|
||||
access.argumentExpression,
|
||||
locOf(access.argumentExpression),
|
||||
)
|
||||
: null;
|
||||
if (split) stmts.push(...split.stmts);
|
||||
const rawIndex =
|
||||
split?.index ??
|
||||
lowerOptionalIndex(
|
||||
lowerer,
|
||||
loweredIndex,
|
||||
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 }
|
||||
@@ -340,7 +360,7 @@ export function lowerIncDec(
|
||||
loc,
|
||||
invalidNaN: true,
|
||||
};
|
||||
return finish(read, (value) =>
|
||||
const store = (value: IrExpr): IrStmt =>
|
||||
type.kind === "array"
|
||||
? arrayValueStore(
|
||||
lowerer,
|
||||
@@ -356,8 +376,15 @@ export function lowerIncDec(
|
||||
index,
|
||||
value: lowerer.coerceInto(access, value, F64),
|
||||
loc,
|
||||
},
|
||||
);
|
||||
};
|
||||
if (!split) return finish(read, store);
|
||||
return finish(read, (value) => ({
|
||||
kind: "if",
|
||||
cond: split.isUndefined,
|
||||
then: [undefinedTypedArrayKeyFence(lowerer, access, loc)],
|
||||
else_: [store(value)],
|
||||
loc,
|
||||
}));
|
||||
}
|
||||
// A TS member statically typed number on a checked-dynamic receiver (a
|
||||
// class-backed protocol view): the expanded form `o.v = o.v + 1`, its
|
||||
|
||||
@@ -25,6 +25,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* flow (if/while/for/for-of/do, switch, try/catch, abrupt completions),
|
||||
* explicit resource management, and blocked-binding poisoning. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { lowerOptionalIndex } from "./undefined-index.js";
|
||||
import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js";
|
||||
import { requireInitFlagStmt } from "./require-initialization.js";
|
||||
import { isJsonRequireDecl } from "./json-require.js";
|
||||
@@ -204,7 +205,6 @@ import {
|
||||
lowerElementCompoundStatement,
|
||||
lowerIncDec,
|
||||
lowerIncDecTarget,
|
||||
lowerOptionalNumber,
|
||||
lowerGroupsProjection,
|
||||
matchResultNamedGroupsOf,
|
||||
runtimeOptionalGuardIds,
|
||||
@@ -9363,7 +9363,7 @@ 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 = lowerOptionalNumber(
|
||||
const index = lowerOptionalIndex(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(t.argumentExpression),
|
||||
locOf(t.argumentExpression),
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { BOOL, DYN, F64, UNDEFINED_T, type IrExpr, type IrStmt, type SrcLoc } from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { lowerOptionalNumber } from "./lower-exprs.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
|
||||
/** An element index the checker types as a number can be undefined at run
|
||||
* time (an element read from a holey array). JavaScript then names the
|
||||
* property "undefined", not "NaN". */
|
||||
|
||||
/** The array element index `operand` (the lowered index of `node`) as a
|
||||
* number. An undefined index is the runtime's "undefined" key
|
||||
* (`array.undefinedKey`, a NaN the array's property table spells
|
||||
* "undefined"); other values convert with ToNumber, as lowerOptionalNumber
|
||||
* does. */
|
||||
export function lowerOptionalIndex(
|
||||
lowerer: Lowerer,
|
||||
operand: IrExpr,
|
||||
loc: SrcLoc,
|
||||
node: ts.Expression,
|
||||
): IrExpr {
|
||||
const split = splitUndefinedIndex(lowerer, operand, node, loc);
|
||||
if (split === null) return lowerOptionalNumber(lowerer, operand, loc, node);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: split.stmts,
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: split.isUndefined,
|
||||
then: { kind: "libCall", fn: "array.undefinedKey", args: [], type: F64, loc },
|
||||
else_: split.index,
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A typed-array element index that can be undefined: `stmts` evaluate it
|
||||
* once, `isUndefined` tests it, and `index` is the number a defined index
|
||||
* converts to. */
|
||||
export interface SplitIndex {
|
||||
stmts: IrStmt[];
|
||||
isUndefined: IrExpr;
|
||||
index: IrExpr;
|
||||
}
|
||||
|
||||
/** Splits `raw`, the lowered index of `node`, when it is a number that can
|
||||
* be undefined; null otherwise. */
|
||||
export function splitUndefinedIndex(
|
||||
lowerer: Lowerer,
|
||||
raw: IrExpr,
|
||||
node: ts.Expression,
|
||||
loc: SrcLoc,
|
||||
): SplitIndex | null {
|
||||
if (raw.type.kind !== "union") return null;
|
||||
const undefinedTag = lowerer.armTag(raw.type.unionId, UNDEFINED_T);
|
||||
if (undefinedTag < 0) return null;
|
||||
const local = lowerer.declareHiddenLocal("%elementIndex", raw.type);
|
||||
const stable = varRef(local.id, raw.type, loc);
|
||||
const index = lowerOptionalNumber(lowerer, stable, loc, node);
|
||||
if (index.type.kind !== "f64") return null;
|
||||
return {
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: raw, loc }],
|
||||
isUndefined: {
|
||||
kind: "unionIsTag",
|
||||
unionId: raw.type.unionId,
|
||||
tag: undefinedTag,
|
||||
negated: false,
|
||||
value: stable,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
||||
/** A typed array has no property named "undefined" to store into: the
|
||||
* store throws the catchable SC1090 error when it runs. A call, so that it
|
||||
* can sit among an expression's statements. */
|
||||
export function undefinedTypedArrayKeyFence(lowerer: Lowerer, node: ts.Node, loc: SrcLoc): IrStmt {
|
||||
const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1;
|
||||
const message = `typed array properties named 'undefined' (an element index that is undefined) are not supported yet [SC1090 at ${loc.file}:${line}]`;
|
||||
const key = `index.undefinedFence:${message}`;
|
||||
let name = lowerer.arrHofHelpers.get(key);
|
||||
if (name === undefined) {
|
||||
name = keyedHelperName(lowerer, "%index.undefinedFence", key);
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: DYN,
|
||||
body: [{ kind: "runtimeFence", code: "SC1090", message, loc }],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "exprStmt", expr: { kind: "call", callee: name, args: [], type: DYN, loc }, loc };
|
||||
}
|
||||
@@ -3813,6 +3813,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
// Whether the array (args[0]) still has the key (args[1]) from its
|
||||
// namedKeys list. Borrowed; never throws.
|
||||
"array.hasNamedKey": { argTypes: [null, STRING], result: BOOL },
|
||||
// The key of an element access whose index is undefined (the property
|
||||
// named "undefined"; frontend/lowering/undefined-index.ts).
|
||||
"array.undefinedKey": { argTypes: [], result: F64 },
|
||||
// node:events EventEmitter: receivers are emitter-hierarchy objects and
|
||||
// the chaining forms (on/off/removeAll/setMax) return the receiver's
|
||||
// own class — program-dependent object types, checked in the libCall
|
||||
|
||||
@@ -2099,8 +2099,9 @@ export type IrStmt =
|
||||
* and whose `code` carries the SC diagnostic code; unwinds exactly like
|
||||
* `throw`.
|
||||
* TypeScript sources produce it only as the tsc-unreachable fallthrough
|
||||
* trap (appendImplicitUndefinedReturn's SC9002) — their construct fences
|
||||
* stay compile errors. */
|
||||
* trap (appendImplicitUndefinedReturn's SC9002) and for a typed-array
|
||||
* store through an element index that is undefined at run time
|
||||
* (undefined-index.ts); their construct fences stay compile errors. */
|
||||
| { kind: "runtimeFence"; code: string; message: string; loc: SrcLoc }
|
||||
/** Rethrow of a catch binding (`throw e` where e is the binding):
|
||||
* re-raises the SAVED exception exactly — kind and payload preserved,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7540-undefined-element-index-keys.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -176,7 +176,22 @@ static size_t scr_arr_prop_index(const ScrArr *a, const char *key,
|
||||
return SIZE_MAX;
|
||||
}
|
||||
|
||||
#define SCR_ARR_UNDEFINED_KEY_BITS UINT64_C(0x7ff8000000756e64)
|
||||
|
||||
double scr_arr_undefined_key(void) {
|
||||
uint64_t bits = SCR_ARR_UNDEFINED_KEY_BITS;
|
||||
double key;
|
||||
memcpy(&key, &bits, sizeof key);
|
||||
return key;
|
||||
}
|
||||
|
||||
static size_t scr_arr_prop_key(double i, char *buf) {
|
||||
uint64_t bits;
|
||||
memcpy(&bits, &i, sizeof bits);
|
||||
if (bits == SCR_ARR_UNDEFINED_KEY_BITS) {
|
||||
memcpy(buf, "undefined", sizeof "undefined");
|
||||
return sizeof "undefined" - 1;
|
||||
}
|
||||
return scr_f64_to_str(i, buf);
|
||||
}
|
||||
|
||||
@@ -2012,7 +2027,7 @@ static uint64_t scr_arr_require_slot(ScrArr *a, double i) {
|
||||
if (scr_arr_prop_get_state(a, i, &prop, &state)) {
|
||||
if (state == SCR_ARR_UNDEFINED) {
|
||||
char buf[32];
|
||||
scr_f64_to_str(i, buf);
|
||||
scr_arr_prop_key(i, buf);
|
||||
scr_trap_fmt("scriptc: RangeError: numeric property %s is undefined\n", buf);
|
||||
}
|
||||
return prop;
|
||||
@@ -5121,6 +5136,10 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) {
|
||||
|
||||
bool scr_arr_numeric_key(const char *key, size_t len, double *out) {
|
||||
char text[64];
|
||||
if (len == sizeof "undefined" - 1 && memcmp(key, "undefined", len) == 0) {
|
||||
*out = scr_arr_undefined_key();
|
||||
return true;
|
||||
}
|
||||
if (len == 0 || len >= sizeof text) return false;
|
||||
memcpy(text, key, len);
|
||||
text[len] = '\0';
|
||||
|
||||
@@ -457,9 +457,15 @@ void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target);
|
||||
* properties of `target`, so neither is lost when the storage is swapped. */
|
||||
void scr_arr_adopt_view_properties(ScrArr *a, struct ScrDyn *view, ScrArr *target);
|
||||
/* True when `key` is the string spelling of a number that is not an array
|
||||
* index ("-1", "1.5", "NaN", "Infinity"), with the number in *out. Arrays
|
||||
* keep such properties in their noncanonical property table. */
|
||||
* index ("-1", "1.5", "NaN", "Infinity"), or "undefined", with the number in
|
||||
* *out (scr_arr_undefined_key for "undefined"). Arrays keep such properties
|
||||
* in their noncanonical property table. */
|
||||
bool scr_arr_numeric_key(const char *key, size_t len, double *out);
|
||||
/* The number compiled code passes as the key of an element access whose
|
||||
* index is undefined at run time: a NaN with its own payload, which the
|
||||
* property table spells "undefined" (JavaScript names that property, not
|
||||
* "NaN"). */
|
||||
double scr_arr_undefined_key(void);
|
||||
/* Whether the array still has the named or noncanonical numeric property
|
||||
* `key`: `for...in` skips a key deleted before its turn. Borrows both. */
|
||||
bool scr_arr_has_named_key(ScrArr *a, ScrStr *key);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// An element index the checker types as a number can be undefined at run
|
||||
// time (an element read from a holey array). JavaScript then names the
|
||||
// array's "undefined" property, not "NaN": plain, compound, update and
|
||||
// destructuring writes store it, a write used as a value yields the value,
|
||||
// and reads (number, string, record and array elements) read it back.
|
||||
const holey: number[] = [1];
|
||||
holey[3] = 3;
|
||||
|
||||
const values: number[] = [0, 0, 0, 0];
|
||||
values[NaN] = 10;
|
||||
for (const i of holey) values[i] = 7;
|
||||
console.log(values[NaN], values.join(","));
|
||||
let total = 0;
|
||||
for (const i of holey) total += values[i]!;
|
||||
console.log(total);
|
||||
|
||||
const counts: number[] = [0, 0, 0, 0];
|
||||
counts[NaN] = 10;
|
||||
let trace = "";
|
||||
for (const i of holey) trace += counts[i]++ + ",";
|
||||
for (const i of holey) counts[i] += 2;
|
||||
for (const i of holey) trace += --counts[i] + ";";
|
||||
console.log(counts[NaN], counts.join(","), trace);
|
||||
|
||||
let last = 0;
|
||||
for (const i of holey) last = values[i] = 5;
|
||||
for (const i of holey) [values[i]] = [8];
|
||||
console.log(last, values[NaN], values.join(","));
|
||||
|
||||
const names: string[] = ["a", "b", "c", "d"];
|
||||
for (const i of holey) names[i] += "!";
|
||||
const seen: (string | undefined)[] = [];
|
||||
for (const i of holey) seen.push(names[i]);
|
||||
console.log(names.join("|"), seen.join("|"));
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
}
|
||||
const points: Point[] = [{ x: 0 }, { x: 1 }];
|
||||
const shared: Point = { x: 9 };
|
||||
let same = "";
|
||||
for (const i of holey) {
|
||||
points[i] = shared;
|
||||
same += String(points[i] === shared) + " ";
|
||||
}
|
||||
shared.x = 10;
|
||||
for (const i of holey) same += String(points[i]?.x) + " ";
|
||||
console.log(same);
|
||||
|
||||
const fresh: number[] = [1, 2];
|
||||
let missing = "";
|
||||
for (const i of holey) missing += String(fresh[i]) + " ";
|
||||
console.log(missing);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3700, "san": 5300 }
|
||||
@@ -305,6 +305,26 @@ describe("checked catch-binding casts", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("a typed-array write through an undefined index is a named runtime fence", async () => {
|
||||
// Node stores an ordinary property named "undefined" on the typed
|
||||
// array, which a compiled typed array cannot hold; defined indexes
|
||||
// before it store as usual.
|
||||
const r = await compileAndRun(
|
||||
"typed-array-undefined-index",
|
||||
`const holey: number[] = [1];
|
||||
holey[3] = 3;
|
||||
const cells = new Float64Array(4);
|
||||
for (const i of holey) cells[i] = 9;
|
||||
console.log(cells.join(","));
|
||||
`,
|
||||
);
|
||||
expect(r.exitCode).toBe(1);
|
||||
expect(r.stdout).toBe("");
|
||||
expect(r.stderr).toMatch(
|
||||
/^Uncaught Error: typed array properties named 'undefined' \(an element index that is undefined\) are not supported yet \[SC1090 at .*typed-array-undefined-index\.ts:4\]\n$/,
|
||||
);
|
||||
});
|
||||
|
||||
test("an EFFECTFUL surplus argument is a named deferred fence (JS lane)", async () => {
|
||||
// Effect-free surplus args drop at compile time, JS-exact (corpus
|
||||
// 1760 — the test-string-decoder writeSequences shape). An effectful
|
||||
|
||||
Reference in New Issue
Block a user