mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Answer undefined for out-of-range typed array reads like Node
Typed-array element reads at an index that is not an integer in [0, length) now yield undefined instead of throwing a RangeError. The read is `number | undefined`, like ordinary array reads; numeric and truthiness consumers fuse back to the plain element read, which answers NaN for an invalid index without touching storage. The may-throw seed for typed reads is removed. Invalid writes stay ignored. The corpus pins reads in printing, comparison, arithmetic, defaulting, truthiness, bindings and templates, plus ignored writes, and a received worker message is checked to arrive unfrozen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a9d026fa0d
commit
ee2832afae
@@ -144,11 +144,11 @@ Compiled code has runtime behavior that can differ from Node.js. Account for the
|
||||
|
||||
### Array bounds and runtime errors
|
||||
|
||||
Statically typed numeric reads from typed arrays throw a catchable `RangeError` for invalid indexes, because a typed numeric read cannot produce `undefined`. Node.js returns `undefined` for these reads. Invalid numeric indexed writes are ignored, while the assignment expression retains its original value.
|
||||
Typed-array reads at an index that is not an integer in the array's range return `undefined`, as in Node.js, and invalid numeric indexed writes are ignored, while the assignment expression retains its original value. A non-null assertion on such a read (`bytes[i]!`) yields `NaN` instead of `undefined`.
|
||||
|
||||
Ordinary arrays track holes separately from present `undefined` values. Missing reads and empty `pop()`/`shift()` return `undefined`, indexed writes can grow the array, and length growth creates holes.
|
||||
|
||||
Runtime traps cannot be caught. User-thrown values and supported exceptions, including JSON parse, checked-cast, filesystem, regex, invalid typed-array read, and `String.prototype.repeat` count errors, can be caught; hard traps such as out-of-range reads of statically typed ordinary arrays terminate the process.
|
||||
Runtime traps cannot be caught. User-thrown values and supported exceptions, including JSON parse, checked-cast, filesystem, regex, and `String.prototype.repeat` count errors, can be caught; hard traps such as out-of-range reads of statically typed ordinary arrays terminate the process.
|
||||
|
||||
Deep recursion that exhausts the native stack terminates the process with a signal instead of throwing Node's catchable `RangeError: Maximum call stack size exceeded`.
|
||||
|
||||
|
||||
@@ -6733,8 +6733,9 @@ export class LlEmitter {
|
||||
index: LlValue,
|
||||
expr?: IrExpr,
|
||||
inBounds = false,
|
||||
invalidNaN = false,
|
||||
): LlValue {
|
||||
return emitBytesGet(this, elem, receiver, index, expr, inBounds);
|
||||
return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN);
|
||||
}
|
||||
|
||||
emitToUint32(value: string, expr?: IrExpr, uint32?: string): string {
|
||||
|
||||
@@ -108,7 +108,9 @@ export function emitBytesIndex(
|
||||
B.startBlock(invalid);
|
||||
if (skipInvalid) B.br(skipInvalid);
|
||||
else {
|
||||
emitInvalidBytesRead(host, receiver, index);
|
||||
host.declare(`declare double @scr_bytes_get(ptr, double)`);
|
||||
B.line(`call double @scr_bytes_get(ptr ${receiver}, double ${index})`);
|
||||
B.terminate("unreachable");
|
||||
}
|
||||
B.startBlock(valid);
|
||||
if (host.sizeType === "i64") return wideIndex;
|
||||
@@ -142,22 +144,16 @@ export function emitBytesIndex(
|
||||
B.startBlock(invalid);
|
||||
if (skipInvalid) B.br(skipInvalid);
|
||||
else {
|
||||
emitInvalidBytesRead(host, receiver, index);
|
||||
// No caller-supplied invalid path: never touch storage.
|
||||
host.declare(`declare void @llvm.trap()`);
|
||||
B.line(`call void @llvm.trap()`);
|
||||
B.terminate("unreachable");
|
||||
}
|
||||
|
||||
B.startBlock(valid);
|
||||
return idx;
|
||||
}
|
||||
|
||||
/** An invalid typed-array read: the runtime throws Node's catchable
|
||||
* RangeError, and this path unwinds to the handler (it never continues). */
|
||||
function emitInvalidBytesRead(host: LlvmEmitterContext, receiver: string, index: string): void {
|
||||
host.declare(`declare double @scr_bytes_get(ptr, double)`);
|
||||
host.B.line(`call double @scr_bytes_get(ptr ${receiver}, double ${index})`);
|
||||
host.emitPendingCheck();
|
||||
host.B.terminate("unreachable");
|
||||
}
|
||||
|
||||
export function emitBytesData(host: LlvmEmitterContext, receiver: string): string {
|
||||
const p = host.B.tmp();
|
||||
const data = host.B.tmp();
|
||||
@@ -192,16 +188,51 @@ export function emitBytesGet(
|
||||
index: LlValue,
|
||||
expr?: IrExpr,
|
||||
inBounds = false,
|
||||
invalidNaN = false,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
if (host.mod.workers) {
|
||||
host.declare(`declare double @scr_bytes_get(ptr, double)`);
|
||||
const fn = invalidNaN ? "scr_bytes_get_or_nan" : "scr_bytes_get";
|
||||
host.declare(`declare double @${fn}(ptr, double)`);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = call double @scr_bytes_get(ptr ${receiver}, double ${index.name})`);
|
||||
host.emitPendingCheck();
|
||||
B.line(`${result} = call double @${fn}(ptr ${receiver}, double ${index.name})`);
|
||||
return { name: result, type: F64 };
|
||||
}
|
||||
const idx = host.emitBytesIndex(receiver, index, expr, inBounds);
|
||||
// Any invalid index traps (the array runtime's discipline).
|
||||
if (!invalidNaN) return emitBytesLoad(host, elem, receiver, index, expr, inBounds, undefined);
|
||||
// The numeric view of an ordinary read: an invalid index never touches
|
||||
// storage and reads as NaN — ToNumber of Node's undefined.
|
||||
const invalid = B.newLabel("bytes.read.invalid");
|
||||
const done = B.newLabel("bytes.read.done");
|
||||
const loaded = emitBytesLoad(host, elem, receiver, index, expr, inBounds, invalid);
|
||||
const ok = B.newLabel("bytes.read.ok");
|
||||
B.br(ok);
|
||||
B.startBlock(ok);
|
||||
B.br(done);
|
||||
B.startBlock(invalid);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = phi double [ ${loaded.name}, %${ok} ], [ ${f64Lit(NaN)}, %${invalid} ]`);
|
||||
// NaN has no integer value: the integer hint survives only when the read
|
||||
// is proven valid (the invalid block is then unreachable).
|
||||
if (loaded.uint32 === undefined || !inBounds) return { name: out, type: F64 };
|
||||
const u32 = B.tmp();
|
||||
B.line(`${u32} = phi i32 [ ${loaded.uint32}, %${ok} ], [ 0, %${invalid} ]`);
|
||||
return { name: out, type: F64, uint32: u32 };
|
||||
}
|
||||
|
||||
function emitBytesLoad(
|
||||
host: LlvmEmitterContext,
|
||||
elem: IrBytesElem,
|
||||
receiver: string,
|
||||
index: LlValue,
|
||||
expr: IrExpr | undefined,
|
||||
inBounds: boolean,
|
||||
invalid: string | undefined,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds);
|
||||
const data = host.emitBytesData(receiver);
|
||||
const p = B.tmp();
|
||||
if (BYTES_ELEMENT_SIZE[elem] < 4) {
|
||||
@@ -503,6 +534,7 @@ export function emitBytesIntrinsic(
|
||||
args[0]!,
|
||||
e.args[0],
|
||||
host.bytesBounds.has(e),
|
||||
e.invalidNaN === true,
|
||||
);
|
||||
case "slice":
|
||||
return call(
|
||||
|
||||
@@ -231,8 +231,7 @@ export function computeMayThrow(mod: IrModule): {
|
||||
// setFrom and the numeric read/write families throw catchable
|
||||
// RangeErrors (Node's bounds discipline); the rest trap or
|
||||
// cannot fail.
|
||||
// An out-of-range `get` throws the catchable RangeError too.
|
||||
if (MAY_THROW_BYTES_METHODS.has(rec.method) || rec.method === "get") {
|
||||
if (MAY_THROW_BYTES_METHODS.has(rec.method)) {
|
||||
f.throws = true;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -226,6 +226,108 @@ export function lowerSafeIndexRead(
|
||||
return { kind: "call", callee: name, args: [arr, index], type: resultT, loc };
|
||||
}
|
||||
|
||||
/** An ordinary typed-array element read `t[i]`: like Node, an index that
|
||||
* is not an integer in [0, length) answers undefined (the storage is
|
||||
* never touched out of bounds). The result is `number | undefined`, the
|
||||
* same optional shape ordinary array reads carry; numeric consumers fuse
|
||||
* back to the plain element read (tryLowerNumericIndexRead), where the
|
||||
* invalid case reads as NaN — exactly ToNumber(undefined). */
|
||||
export function lowerSafeBytesRead(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
index: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const bytesT = receiver.type;
|
||||
if (bytesT.kind !== "bytes") throw new InternalCompilerError("typed read requires bytes");
|
||||
const resultT = lowerer.withUndefinedArm(F64);
|
||||
const key = `bytesIdxOr:${typeKey(bytesT)}`;
|
||||
let name = lowerer.arrHofHelpers.get(key);
|
||||
if (!name) {
|
||||
name = `%bytes.idxOr.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
const b = varRef("b.0", bytesT, loc);
|
||||
const i = varRef("i.0", F64, loc);
|
||||
const cmp = (op: "<" | ">=" | "===", left: IrExpr, right: IrExpr): IrExpr => ({
|
||||
kind: "bin",
|
||||
op,
|
||||
left,
|
||||
right,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const inRange: IrExpr = {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: cmp(">=", i, { kind: "numLit", value: 0, type: F64, loc }),
|
||||
right: {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: cmp("<", i, {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "length",
|
||||
receiver: b,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
right: cmp("===", { kind: "libCall", fn: "math.trunc", args: [i], type: F64, loc }, i),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const read: IrExpr = {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver: b,
|
||||
args: [i],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [
|
||||
{ localId: "b.0", name: "b", type: bytesT },
|
||||
{ localId: "i.0", name: "i", type: F64 },
|
||||
],
|
||||
returnType: resultT,
|
||||
locals: [
|
||||
{ id: "b.0", name: "b", type: bytesT, mutable: false },
|
||||
{ id: "i.0", name: "i", type: F64, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond: inRange,
|
||||
then: lowerer.coerceToExpected(read, resultT),
|
||||
else_: lowerer.wrappedUndefined(resultT, loc)!,
|
||||
type: resultT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [receiver, index], type: resultT, loc };
|
||||
}
|
||||
|
||||
/** Whether `operand` is a call of the typed-array read helper. */
|
||||
function isBytesReadHelperCall(lowerer: Lowerer, operand: IrExpr): boolean {
|
||||
if (operand.kind !== "call" || operand.args.length !== 2) return false;
|
||||
const receiver = operand.args[0]!;
|
||||
return (
|
||||
receiver.type.kind === "bytes" &&
|
||||
operand.args[1]!.type.kind === "f64" &&
|
||||
operand.callee === lowerer.arrHofHelpers.get(`bytesIdxOr:${typeKey(receiver.type)}`)
|
||||
);
|
||||
}
|
||||
|
||||
/** Fuse only our own f64-array read helper with immediate ToNumber.
|
||||
* Reuse its arguments verbatim so the receiver and index still evaluate
|
||||
* exactly once, in order, before the read. Do not specialize user calls,
|
||||
@@ -235,6 +337,20 @@ export function tryLowerNumericIndexRead(
|
||||
operand: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (operand.kind === "call" && isBytesReadHelperCall(lowerer, operand)) {
|
||||
// The plain element read: an invalid index reads as NaN, which is
|
||||
// ToNumber(undefined).
|
||||
const [receiver, index] = operand.args;
|
||||
return {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver: receiver!,
|
||||
args: [index!],
|
||||
type: F64,
|
||||
loc,
|
||||
invalidNaN: true,
|
||||
};
|
||||
}
|
||||
if (
|
||||
operand.kind !== "call" ||
|
||||
operand.callee !== lowerer.arrHofHelpers.get(`idxOr:${typeKey(F64)}`)
|
||||
@@ -275,6 +391,11 @@ export function tryLowerIndexTruthiness(
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (operand.kind !== "call" || operand.args.length !== 2) return null;
|
||||
if (isBytesReadHelperCall(lowerer, operand)) {
|
||||
// NaN (the invalid read) is falsy exactly like undefined.
|
||||
const number = tryLowerNumericIndexRead(lowerer, operand, loc);
|
||||
return number ? lowerer.ensureBool(number, node) : null;
|
||||
}
|
||||
const [arr, index] = operand.args;
|
||||
if (arr?.type.kind !== "array" || index?.type.kind !== "f64") return null;
|
||||
const elem = arr.type.elem;
|
||||
|
||||
@@ -110,6 +110,7 @@ import {
|
||||
lowerSafeIndexRead,
|
||||
tryLowerIndexTruthiness,
|
||||
tryLowerNumericIndexRead,
|
||||
lowerSafeBytesRead,
|
||||
} from "./array-values.js";
|
||||
import { strCharsCall } from "./containers/array-construction.js";
|
||||
import { lowerOptionalStringIndex } from "./string-index.js";
|
||||
@@ -6661,14 +6662,21 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
if (index.type.kind !== "f64") {
|
||||
lowerer.unsupported("SC1090", expr.argumentExpression, "indexing with non-number keys");
|
||||
}
|
||||
return {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver: recv,
|
||||
args: [index],
|
||||
type: F64,
|
||||
loc: locOf(expr),
|
||||
};
|
||||
// `t[i]!` asserts presence: the numeric element read (NaN when
|
||||
// invalid). Every other read answers undefined for an invalid index,
|
||||
// like Node.
|
||||
if (ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr) {
|
||||
return {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver: recv,
|
||||
args: [index],
|
||||
type: F64,
|
||||
loc: locOf(expr),
|
||||
invalidNaN: true,
|
||||
};
|
||||
}
|
||||
return lowerSafeBytesRead(lowerer, recv, index, locOf(expr));
|
||||
}
|
||||
// Tuple element read `t[0]`: a positional-field read of the tuple's
|
||||
// record shape — LITERAL indices only (the checker's per-index types
|
||||
@@ -9225,7 +9233,15 @@ export function lowerIncDec(
|
||||
const read: IrExpr =
|
||||
type.kind === "array"
|
||||
? arrayValueRead(lowerer, receiver, index, type.elem, loc)
|
||||
: { kind: "bytesIntrinsic", method: "get", receiver, args: [index], type: F64, loc };
|
||||
: {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver,
|
||||
args: [index],
|
||||
type: F64,
|
||||
loc,
|
||||
invalidNaN: true,
|
||||
};
|
||||
return finish(read, (value) =>
|
||||
type.kind === "array"
|
||||
? arrayValueStore(
|
||||
|
||||
@@ -3993,7 +3993,8 @@ export class Lowerer {
|
||||
if (!ts.isElementAccessExpression(e)) return false;
|
||||
const kind = this.mapTypeOf(this.typeOf(e.expression))?.kind;
|
||||
if (kind === "string") return !this.stringIndexBounds.inBounds(e);
|
||||
return kind === "array";
|
||||
// Typed-array reads answer undefined for an invalid index too.
|
||||
return kind === "array" || kind === "bytes";
|
||||
};
|
||||
const isDynamicObjectEntryRead = (node: ts.Expression): boolean => {
|
||||
const read = peel(node);
|
||||
|
||||
@@ -268,7 +268,7 @@ export function analyzeIntegerRanges(fn: IrFunction): IntegerRanges {
|
||||
if (numeric && !numeric.write) range = byteNumberRange(numeric);
|
||||
else if (e.method === "length" || e.method === "byteLength" || e.method === "byteOffset")
|
||||
range = { min: 0, max: Number.MAX_SAFE_INTEGER };
|
||||
else if (e.method === "get" && e.receiver.type.kind === "bytes") {
|
||||
else if (e.method === "get" && e.invalidNaN !== true && e.receiver.type.kind === "bytes") {
|
||||
const elem = e.receiver.type.elem;
|
||||
if (elem === "u8" || elem === "u8c") range = { min: 0, max: 255 };
|
||||
else if (elem === "i8") range = { min: -128, max: 127 };
|
||||
|
||||
@@ -2500,8 +2500,10 @@ export type IrExpr =
|
||||
/** Typed-array/Buffer method or property on a bytes receiver — see
|
||||
* IrBytesIntrinsicMethod for the surface and conventions. Methods in
|
||||
* MAY_THROW_BYTES_METHODS raise catchable RangeErrors (may-throw
|
||||
* seeds); `get` on an invalid index throws a catchable RangeError too
|
||||
* (a typed read has no undefined to answer with; also a seed). */
|
||||
* seeds); `get` traps on an invalid index unless `invalidNaN` is set,
|
||||
* in which case the invalid read answers NaN without touching storage
|
||||
* (the numeric view of Node's undefined; ordinary reads test the index
|
||||
* first and answer undefined itself). */
|
||||
| {
|
||||
kind: "bytesIntrinsic";
|
||||
method: IrBytesIntrinsicMethod;
|
||||
@@ -2509,6 +2511,7 @@ export type IrExpr =
|
||||
args: IrExpr[];
|
||||
type: IrType;
|
||||
loc: SrcLoc;
|
||||
invalidNaN?: true;
|
||||
}
|
||||
/** `new Map<K, V>()` — allocate an empty map. `type` is the map type
|
||||
* (key/value fences already enforced by the frontend); the result is
|
||||
|
||||
@@ -254,22 +254,16 @@ double scr_bytes_byte_len(const ScrBytes *b) {
|
||||
|
||||
/* ── element access ────────────────────────────────────────────────────
|
||||
* Invalid writes are ignored. Typed numeric reads cannot represent
|
||||
* undefined (Node's answer), so an invalid read throws a catchable
|
||||
* RangeError instead (the caller checks the pending exception; the NaN
|
||||
* result is never observed); checked reads return undefined. */
|
||||
* undefined, so invalid reads trap; checked reads return undefined. */
|
||||
|
||||
static bool scr_bytes_check_index(const ScrBytes *b, double i, size_t *out) {
|
||||
static size_t scr_bytes_check_index(const ScrBytes *b, double i) {
|
||||
if (!(i >= 0) || i != trunc(i) || i >= (double)b->len) {
|
||||
char buf[32];
|
||||
scr_f64_to_str(i, buf);
|
||||
char msg[96];
|
||||
int len = snprintf(msg, sizeof msg, "typed array index %s out of bounds (length %zu)", buf,
|
||||
b->len);
|
||||
scr_throw_error_msg(SCR_ERR_RANGE, msg, (size_t)len);
|
||||
return false;
|
||||
scr_trap_fmt("scriptc: RangeError: typed array index %s out of bounds (length %zu)\n",
|
||||
buf, b->len);
|
||||
}
|
||||
*out = (size_t)i;
|
||||
return true;
|
||||
return (size_t)i;
|
||||
}
|
||||
|
||||
/* ToUint32: NaN/±Infinity → 0, truncate toward zero, wrap mod 2^32.
|
||||
@@ -280,8 +274,7 @@ static uint32_t scr_bytes_to_u32(double v) {
|
||||
|
||||
double scr_bytes_get(const ScrBytes *b, double i) {
|
||||
SCR_SHARED_GUARD(b, NULL);
|
||||
size_t idx;
|
||||
if (!scr_bytes_check_index(b, i, &idx)) return NAN;
|
||||
size_t idx = scr_bytes_check_index(b, i);
|
||||
switch (b->elem) {
|
||||
case SCR_BYTES_U8: case SCR_BYTES_U8C:
|
||||
return (double)b->data[idx];
|
||||
@@ -324,6 +317,14 @@ double scr_bytes_get(const ScrBytes *b, double i) {
|
||||
return 0; /* unreachable */
|
||||
}
|
||||
|
||||
/* The numeric view of an ordinary typed-array read: Node answers undefined
|
||||
* for an index that is not an integer in [0, length), which ToNumber turns
|
||||
* into NaN. Never touches storage out of bounds. */
|
||||
double scr_bytes_get_or_nan(const ScrBytes *b, double i) {
|
||||
if (!(i >= 0) || i != trunc(i) || i >= (double)b->len) return NAN;
|
||||
return scr_bytes_get(b, i);
|
||||
}
|
||||
|
||||
void scr_bytes_set(ScrBytes *b, double i, double v) {
|
||||
SCR_SHARED_GUARD(b, NULL);
|
||||
if (!(i >= 0) || i != trunc(i) || i >= (double)b->len) return;
|
||||
|
||||
@@ -6404,6 +6404,7 @@ void scr_dataview_set(ScrBytes *b, double byte_off, double value, ScrDataViewGet
|
||||
* JS-exactly: u8/u32 by modular truncation (NaN/±Infinity → 0, truncate
|
||||
* toward zero, wrap mod 2^8/2^32), f32 by double→float rounding. */
|
||||
double scr_bytes_get(const ScrBytes *b, double i);
|
||||
double scr_bytes_get_or_nan(const ScrBytes *b, double i); /* NaN when the index is invalid */
|
||||
void scr_bytes_set(ScrBytes *b, double i, double v);
|
||||
/* Replace a live typed-array capsule's fixed-size payload from its dyn
|
||||
* snapshot. Source and target have the same compiler-checked bytes type. */
|
||||
|
||||
@@ -74,13 +74,18 @@ test("bytes runtime: coercions, encodings, zlib, fs, RC", async () => {
|
||||
expect(stderr.trim().split("\n").pop()).toMatch(/^(\d+)\/\1 cases passed$/);
|
||||
});
|
||||
|
||||
// Statically typed numeric reads cannot represent undefined; an invalid
|
||||
// index throws a catchable RangeError instead of aborting. Invalid writes
|
||||
// are ignored and covered by the runtime assertions above.
|
||||
// Statically typed numeric reads cannot represent undefined and still trap.
|
||||
// Invalid writes are ignored and covered by the runtime assertions above.
|
||||
test.each([
|
||||
["--crash-get-oob", "typed array index 1 out of bounds (length 1)"],
|
||||
["--crash-get-frac", "typed array index 0.5 out of bounds (length 1)"],
|
||||
])("invalid reads throw a catchable RangeError (%s)", async (mode, message) => {
|
||||
const { stderr } = await execFileAsync(bin, [mode]);
|
||||
expect(stderr).toContain(`Uncaught RangeError: ${message}`);
|
||||
])("trap aborts (%s)", async (mode, message) => {
|
||||
const err = await execFileAsync(bin, [mode]).then(
|
||||
() => {
|
||||
throw new Error(`expected ${mode} to abort`);
|
||||
},
|
||||
(e: Error & { signal?: string; stderr?: string }) => e,
|
||||
);
|
||||
expect(err.signal).toBe("SIGABRT");
|
||||
expect(err.stderr).toContain(`scriptc: RangeError: ${message}`);
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* bytes.test.ts, which also asserts the trap modes abort:
|
||||
*
|
||||
* <scratch-dir> run all assertions; prints "N/N cases passed"
|
||||
* --crash-get-oob element read past the end → pending RangeError
|
||||
* --crash-get-frac fractional element index → pending RangeError
|
||||
* --crash-get-oob element read past the end → RangeError + abort()
|
||||
* --crash-get-frac fractional element index → RangeError + abort()
|
||||
*
|
||||
* The coercion matrix mirrors Node exactly (verified by hand and by the
|
||||
* differential corpus): ToUint8/ToUint32 modular truncation on writes,
|
||||
@@ -393,13 +393,8 @@ int main(int argc, char **argv) {
|
||||
fprintf(stderr, "unknown mode %s\n", argv[1]);
|
||||
return 2;
|
||||
}
|
||||
if (!scr_exc_pending()) {
|
||||
fprintf(stderr, "expected a pending RangeError\n");
|
||||
return 2;
|
||||
}
|
||||
scr_exc_print_uncaught();
|
||||
scr_bytes_release(b);
|
||||
return 0;
|
||||
fprintf(stderr, "expected a trap, still alive\n");
|
||||
return 2;
|
||||
}
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: test_bytes <scratch-dir> | --crash-*\n");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// "x".repeat with a negative or infinite count and an out-of-range typed
|
||||
// array read throw catchable RangeErrors: the program keeps running after
|
||||
// the catch.
|
||||
// "x".repeat with a negative or infinite count throws a catchable
|
||||
// RangeError, and typed-array reads at an index that is not an integer in
|
||||
// [0, length) answer undefined (writes there are ignored), like Node.
|
||||
function attempt(label: string, f: () => unknown): void {
|
||||
try {
|
||||
console.log(label, "ok", f());
|
||||
@@ -24,5 +24,30 @@ try {
|
||||
|
||||
const bytes = new Uint8Array(4);
|
||||
bytes[1] = 9;
|
||||
console.log("in range", bytes[1]);
|
||||
const far: number = process.argv.length + 10;
|
||||
const negative: number = -process.argv.length;
|
||||
const fraction: number = process.argv.length + 0.5;
|
||||
console.log("in range", bytes[1], bytes[3]);
|
||||
console.log("out of range", bytes[far], bytes[negative], bytes[fraction], bytes[4]);
|
||||
console.log("is undefined", bytes[far] === undefined, bytes[1] === undefined);
|
||||
console.log("arithmetic", bytes[far] + 1, bytes[1] + 1, Number.isNaN(bytes[far] * 2));
|
||||
console.log("defaulted", bytes[far] ?? -1, bytes[1] ?? -1);
|
||||
if (bytes[far]) console.log("not reached");
|
||||
else console.log("falsy");
|
||||
const missing = bytes[far];
|
||||
console.log("binding", missing, `${missing}`, [missing], JSON.stringify({ value: bytes[far] }));
|
||||
bytes[far] = 7;
|
||||
bytes[negative] = 7;
|
||||
bytes[fraction] = 7;
|
||||
console.log("ignored writes", bytes.join(","), bytes.length);
|
||||
|
||||
const floats = new Float64Array([1.5, -2]);
|
||||
let sum = 0;
|
||||
for (let i = 0; i <= floats.length; i++) sum += floats[i] !== undefined ? floats[i]! : 100;
|
||||
console.log("floats", sum, floats[2], floats[-0]);
|
||||
const words = new Int32Array(2);
|
||||
words[0] = -5;
|
||||
const picked: number[] = [];
|
||||
for (let i = -1; i < 3; i++) picked.push(words[i] ?? 0);
|
||||
console.log("words", picked.join(","), words.length);
|
||||
console.log("after", bytes.length);
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// as a structured clone: a plain object with the instance's own enumerable
|
||||
// data properties. Methods, accessors, private fields and the prototype
|
||||
// chain are not transferred, so instanceof is false in the receiver, and
|
||||
// repeated references inside one message stay one object.
|
||||
// repeated references inside one message stay one object. A frozen
|
||||
// message arrives unfrozen.
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
|
||||
class Fixture {
|
||||
@@ -49,13 +50,15 @@ if (isMainThread) {
|
||||
else console.log("reply", inspect(message));
|
||||
});
|
||||
worker.on("exit", (code: number) => console.log("exit", code, lamp.describe(), lamp.label));
|
||||
worker.postMessage([lamp, lamp]);
|
||||
worker.postMessage(Object.freeze([lamp, lamp]));
|
||||
} else {
|
||||
const port = parentPort!;
|
||||
port.postMessage(`workerData ${inspect(workerData)}`);
|
||||
port.postMessage(`workerData ${inspect(workerData)} ${Object.isFrozen(workerData)}`);
|
||||
port.once("message", (message: unknown) => {
|
||||
const pair = message as unknown[];
|
||||
port.postMessage(`pair ${pair.length} ${pair[0] === pair[1]} ${inspect(pair[0])}`);
|
||||
port.postMessage(
|
||||
`pair ${pair.length} ${pair[0] === pair[1]} ${Object.isFrozen(message)} ${inspect(pair[0])}`,
|
||||
);
|
||||
port.postMessage(new Lamp("floor", ["base"]));
|
||||
port.close();
|
||||
});
|
||||
|
||||
@@ -170,25 +170,6 @@ throw v;
|
||||
expect(num.stderr).toBe("Uncaught 42\n");
|
||||
});
|
||||
|
||||
test("an out-of-range typed array read throws a catchable RangeError", async () => {
|
||||
// Node answers undefined; a typed numeric read cannot, so the native
|
||||
// program throws a catchable RangeError instead of aborting.
|
||||
const r = await compileAndRun(
|
||||
"typed-read-out-of-range",
|
||||
`const t = new Uint8Array(4);
|
||||
const i: number = process.argv.length + 10;
|
||||
try {
|
||||
console.log(t[i]);
|
||||
} catch (e) {
|
||||
console.log(e instanceof RangeError, (e as Error).message);
|
||||
}
|
||||
console.log("after", t.length);
|
||||
`,
|
||||
);
|
||||
expect(r.exitCode).toBe(0);
|
||||
expect(r.stdout).toMatch(/^true typed array index \d+ out of bounds \(length 4\)\nafter 4\n$/);
|
||||
});
|
||||
|
||||
test("an unhandled ERROR rejection renders name: message too", async () => {
|
||||
const r = await compileAndRun(
|
||||
"unhandled-error-rejection",
|
||||
|
||||
Reference in New Issue
Block a user