Fix sparse array semantic gaps

This commit is contained in:
Jimmy Miller
2026-08-04 16:31:31 -04:00
parent afb870cf8c
commit 46fc79e201
32 changed files with 897 additions and 147 deletions
@@ -45,12 +45,15 @@ function streamTypedRefCommitAdapter(
` size_t sc_target_len = sc_target->len;`,
` size_t sc_target_cap = sc_target->cap;`,
` uint64_t *sc_target_data = sc_target->data;`,
` uint8_t *sc_target_present = sc_target->present;`,
` sc_target->len = sc_next->len;`,
` sc_target->cap = sc_next->cap;`,
` sc_target->data = sc_next->data;`,
` sc_target->present = sc_next->present;`,
` sc_next->len = sc_target_len;`,
` sc_next->cap = sc_target_cap;`,
` sc_next->data = sc_target_data;`,
` sc_next->present = sc_target_present;`,
` scr_arr_release(sc_next);`,
`}`,
``,
@@ -200,6 +203,7 @@ function streamTypedRefAdapter(
lines.push(
` ScrDyn *d = scr_dyn_new_arr();`,
` for (size_t sc_i = 0; sc_i < v->len; sc_i++) {`,
` if (!scr_arr_has(v, (double)sc_i)) { scr_dyn_arr_push_hole(d); continue; }`,
);
if (elem.kind === "f64") {
lines.push(
@@ -360,8 +360,16 @@ export function emitStmt(E: CEmitter, s: IrStmt): void {
const idx = E.emitExpr(s.index);
const v = E.emitExpr(s.value);
if (s.arr.type.kind !== "array") throw new Error("emitter bug: arraySet on non-array");
const acc = elemAccess(s.arr.type.elem);
const elem = s.arr.type.elem;
const acc = elemAccess(elem);
if (acc === "ref") E.moveTemp(v);
if (elem.kind === "union") {
const tag = E.undefinedArmTag(elem);
if (tag >= 0) {
E.line(`scr_arr_set_ref_fill(${arr.name}, ${idx.name}, ${v.name}, ${E.unitInstanceRef(elem.unionId, tag)});${E.srcComment(s.loc)}`);
break;
}
}
E.line(`scr_arr_set_${acc}(${arr.name}, ${idx.name}, ${v.name});${E.srcComment(s.loc)}`);
break;
}
@@ -260,6 +260,7 @@ import { OVERFLOW_MEMBER } from "./emit-shapes.js";
` size_t n = (size_t)scr_arr_len(a);`,
` for (size_t i = 0; i < n; i++) {`,
` if (i) for (size_t j = 0; j < sep->len; j++) scr_jb_putc(&b, sep->data[j]);`,
` if (!scr_arr_has(a, (double)i)) continue;`,
` ScrUnion *u = (ScrUnion *)scr_arr_get_ref(a, (double)i);`,
...(unitTags.length > 0
? [` if (${unitTags.map((t) => `u->tag == ${t}`).join(" || ")}) { scr_union_release(u); continue; }`]
@@ -1197,12 +1198,15 @@ export function jsonWriteHelper(E: CEmitter, t: IrType): string {
` size_t sc_old_len = sc_cached->len;`,
` size_t sc_old_cap = sc_cached->cap;`,
` uint64_t *sc_old_data = sc_cached->data;`,
` uint8_t *sc_old_present = sc_cached->present;`,
` sc_cached->len = sc_out->len;`,
` sc_cached->cap = sc_out->cap;`,
` sc_cached->data = sc_out->data;`,
` sc_cached->present = sc_out->present;`,
` sc_out->len = sc_old_len;`,
` sc_out->cap = sc_old_cap;`,
` sc_out->data = sc_old_data;`,
` sc_out->present = sc_old_present;`,
` scr_arr_release(sc_out);`,
` return sc_cached;`,
` }`,
@@ -668,6 +668,7 @@ export class LlDyn {
{ index: 1, type: "i64" as const, name: "length" },
{ index: 2, type: "i64" as const, name: "capacity" },
{ index: 7, type: "ptr" as const, name: "data" },
{ index: 8, type: "ptr" as const, name: "present" },
];
members.forEach((member) => {
const cachedPtr = B.tmp();
+33 -4
View File
@@ -3169,9 +3169,18 @@ class LlEmitter {
const idx = this.emitExpr(s.index);
const v = this.emitExpr(s.value);
if (s.arr.type.kind !== "array") throw new Error("llvm emitter bug: arraySet on non-array");
const acc = elemAccess(s.arr.type.elem);
const elem = s.arr.type.elem;
const acc = elemAccess(elem);
if (acc === "ref") this.moveTemp(v);
const argTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
if (elem.kind === "union") {
const tag = this.undefinedArmTag(elem);
if (tag >= 0) {
this.declare(`declare void @scr_arr_set_ref_fill(ptr, double, ptr, ptr)`);
B.line(`call void @scr_arr_set_ref_fill(ptr ${arr.name}, double ${idx.name}, ptr ${v.name}, ptr ${this.unitInstanceRef(elem.unionId, tag)})`);
break;
}
}
this.declare(`declare void @scr_arr_set_${acc}(ptr, double, ${argTy === "i1" ? "i1 zeroext" : argTy})`);
B.line(`call void @scr_arr_set_${acc}(ptr ${arr.name}, double ${idx.name}, ${argTy} ${v.name})`);
break;
@@ -10148,6 +10157,12 @@ class LlEmitter {
` %next_data = load ptr, ptr %next_data_ptr`,
` store ptr %next_data, ptr %target_data_ptr`,
` store ptr %target_data, ptr %next_data_ptr`,
` %target_present_ptr = getelementptr inbounds %ScrArr, ptr %target, i64 0, i32 8`,
` %next_present_ptr = getelementptr inbounds %ScrArr, ptr %next, i64 0, i32 8`,
` %target_present = load ptr, ptr %target_present_ptr`,
` %next_present = load ptr, ptr %next_present_ptr`,
` store ptr %next_present, ptr %target_present_ptr`,
` store ptr %target_present, ptr %next_present_ptr`,
` call void @scr_arr_release(ptr %next)`,
` br label %done`,
`done:`,
@@ -10416,7 +10431,9 @@ class LlEmitter {
const elem = t.elem;
this.declare(`declare ptr @scr_dyn_new_arr()`);
this.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
this.declare(`declare void @scr_dyn_arr_push_hole(ptr)`);
this.declare(`declare double @scr_arr_len(ptr)`);
this.declare(`declare zeroext i1 @scr_arr_has(ptr, double)`);
const out = B.tmp();
B.line(`${out} = call ptr @scr_dyn_new_arr()`);
const len = B.tmp();
@@ -10426,6 +10443,9 @@ class LlEmitter {
B.line(`store double ${f64Lit(0)}, ptr ${iSlot}`);
const condition = B.newLabel("strm.c");
const body = B.newLabel("strm.b");
const present = B.newLabel("strm.p");
const hole = B.newLabel("strm.h");
const next = B.newLabel("strm.n");
const done = B.newLabel("strm.e");
B.br(condition);
B.startBlock(condition);
@@ -10435,6 +10455,13 @@ class LlEmitter {
B.line(`${more} = fcmp olt double ${index}, ${len}`);
B.condBr(more, body, done);
B.startBlock(body);
const has = B.tmp();
B.line(`${has} = call zeroext i1 @scr_arr_has(ptr %p, double ${index})`);
B.condBr(has, present, hole);
B.startBlock(hole);
B.line(`call void @scr_dyn_arr_push_hole(ptr ${out})`);
B.br(next);
B.startBlock(present);
let value: string;
if (elem.kind === "f64" || elem.kind === "bool") {
const valueTy = elem.kind === "f64" ? "double" : "i1";
@@ -10453,9 +10480,11 @@ class LlEmitter {
if (isRefCounted(elem)) {
B.line(`call void ${releaseSym(this, elem)}(ptr ${value})`);
}
const next = B.tmp();
B.line(`${next} = fadd double ${index}, ${f64Lit(1)}`);
B.line(`store double ${next}, ptr ${iSlot}`);
B.br(next);
B.startBlock(next);
const nextIndex = B.tmp();
B.line(`${nextIndex} = fadd double ${index}, ${f64Lit(1)}`);
B.line(`store double ${nextIndex}, ptr ${iSlot}`);
B.br(condition);
B.startBlock(done);
B.terminate(`ret ptr ${out}`);
+10 -4
View File
@@ -874,6 +874,7 @@ export class LlWalkers {
host.declare(`declare void @scr_jb_init(ptr)`);
host.declare(`declare ptr @scr_jb_finish(ptr)`);
host.declare(`declare double @scr_arr_len(ptr)`);
host.declare(`declare zeroext i1 @scr_arr_has(ptr, double)`);
host.declare(`declare ptr @scr_arr_get_ref(ptr, double)`);
host.declare(`declare void @scr_union_release(ptr)`);
host.declare(`declare void @scr_str_release(ptr)`);
@@ -900,13 +901,18 @@ export class LlWalkers {
// `if (i) put sep bytes` — separators between elements only.
const nz = B.tmp();
const lsep = B.newLabel("uj.s");
const lel = B.newLabel("uj.v");
const lhas = B.newLabel("uj.h");
const lval = B.newLabel("uj.v");
B.line(`${nz} = fcmp ogt double ${i}, ${f64Lit(0)}`);
B.condBr(nz, lsep, lel);
B.condBr(nz, lsep, lhas);
B.startBlock(lsep);
this.putScrStr(B, buf, "%sep");
B.br(lel);
B.startBlock(lel);
B.br(lhas);
B.startBlock(lhas);
const present = B.tmp();
B.line(`${present} = call i1 @scr_arr_has(ptr %a, double ${i})`);
B.condBr(present, lval, ln);
B.startBlock(lval);
const u = B.tmp();
B.line(`${u} = call ptr @scr_arr_get_ref(ptr %a, double ${i}) ; element (+1)`);
if (unitTags.length > 0) {
@@ -0,0 +1,166 @@
import * as ts from "../ts7/adapter.js";
import type { IrType } from "../../ir/nodes.js";
import type { Lowerer } from "./lowerer.js";
/** A hole can be read as JavaScript undefined only when the element slot has
* an actual undefined value. */
export function arrayElementCanRepresentUndefined(L: Lowerer, elem: IrType): boolean {
if (elem.kind === "jsval" || elem.kind === "dyn" || elem.kind === "undefinedT") return true;
return elem.kind === "union" &&
(L.unions.get(elem.unionId)?.arms.some((arm) => arm.kind === "undefinedT") ?? false);
}
export function requireArrayGetSafe(
L: Lowerer,
source: ts.Expression,
elem: IrType,
blame: ts.Node,
construct: string,
): void {
if (arrayElementCanRepresentUndefined(L, elem) || !arrayExprIsProvablySparse(L, source)) return;
L.unsupported(
"SC1090",
blame,
`${construct} on a sparse '${L.fmt({ kind: "array", elem })}' value ` +
`(a hole must be observed as undefined, which '${L.fmt(elem)}' element slots cannot represent - ` +
"use an undefined-capable element type or a dense array)",
);
}
function stripWrappers(expr: ts.Expression): ts.Expression {
let value = expr;
while (
ts.isParenthesizedExpression(value) || ts.isNonNullExpression(value) ||
ts.isAsExpression(value) || ts.isTypeAssertion(value) || ts.isSatisfiesExpression(value)
) {
value = value.expression;
}
return value;
}
function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
}
function isWriteTarget(node: ts.Expression): boolean {
let value = node;
let parent = value.parent;
while (
parent &&
(ts.isParenthesizedExpression(parent) || ts.isNonNullExpression(parent) ||
ts.isAsExpression(parent) || ts.isTypeAssertion(parent) || ts.isSatisfiesExpression(parent))
) {
value = parent;
parent = parent.parent;
}
if (!parent) return false;
if (ts.isDeleteExpression(parent) && parent.expression === value) return true;
if (
(ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) &&
parent.operand === value &&
(parent.operator === ts.SyntaxKind.PlusPlusToken || parent.operator === ts.SyntaxKind.MinusMinusToken)
) {
return true;
}
return ts.isBinaryExpression(parent) && parent.left === value && isAssignmentOperator(parent.operatorToken.kind);
}
const READ_ONLY_ARRAY_METHODS = new Set([
"at", "concat", "entries", "every", "filter", "find", "findIndex", "findLast",
"findLastIndex", "flat", "flatMap", "forEach", "includes", "indexOf", "join", "keys",
"lastIndexOf", "map", "reduce", "reduceRight", "slice", "some", "toReversed", "toSorted",
"toSpliced", "values", "with",
]);
function symbolUsePreservesSparsity(L: Lowerer, node: ts.Identifier): boolean {
const parent = node.parent;
if (
(ts.isVariableDeclaration(parent) || ts.isParameter(parent) || ts.isBindingElement(parent) ||
ts.isFunctionDeclaration(parent)) && parent.name === node
) {
return true;
}
if (ts.isPropertyAccessExpression(parent) && parent.expression === node) {
if (isWriteTarget(parent)) return false;
if (parent.name.text === "length") return true;
return ts.isCallExpression(parent.parent) && parent.parent.expression === parent &&
READ_ONLY_ARRAY_METHODS.has(parent.name.text);
}
if (ts.isElementAccessExpression(parent) && parent.expression === node) {
return !isWriteTarget(parent);
}
if ((ts.isForOfStatement(parent) || ts.isForInStatement(parent)) && parent.expression === node) return true;
if (ts.isSpreadElement(parent) && parent.expression === node) return true;
if (ts.isBinaryExpression(parent)) {
if (parent.right === node && parent.operatorToken.kind === ts.SyntaxKind.InKeyword) return true;
return !isAssignmentOperator(parent.operatorToken.kind) &&
(parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken ||
parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken ||
parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken);
}
return parent.kind === ts.SyntaxKind.TypeQuery;
}
function symbolUsesPreserveSparsity(
L: Lowerer,
symbol: ts.Symbol,
file: ts.SourceFile,
before: number,
): boolean {
let safe = true;
const visit = (node: ts.Node): void => {
if (!safe) return;
if (
ts.isIdentifier(node) && node.pos < before &&
node.text === symbol.name && L.resolveValueSymbol(node) === symbol
) {
safe = symbolUsePreservesSparsity(L, node);
if (!safe) return;
}
node.forEachChild(visit);
};
file.forEachChild(visit);
return safe;
}
function isSparseArrayInitializer(L: Lowerer, source: ts.Expression): boolean {
const value = stripWrappers(source);
if (ts.isArrayLiteralExpression(value)) return value.elements.some(ts.isOmittedExpression);
if (!ts.isCallExpression(value) && !ts.isNewExpression(value)) return false;
const callee = value.expression;
if (
!ts.isIdentifier(callee) || callee.text !== "Array" ||
!L.isStdlibSymbol(L.resolveValueSymbol(callee) ?? undefined)
) {
return false;
}
const args = value.arguments ?? [];
if (args.length !== 1) return false;
const length = stripWrappers(args[0]!);
return ts.isNumericLiteral(length) && Number.isInteger(Number(length.text)) && Number(length.text) > 0;
}
/** Prove only the narrow case needed to fence unrepresentable hole reads.
* Unknown provenance deliberately keeps the historical lowering behavior. */
function arrayExprIsProvablySparse(L: Lowerer, source: ts.Expression): boolean {
const value = stripWrappers(source);
if (isSparseArrayInitializer(L, value)) return true;
if (!ts.isIdentifier(value)) return false;
const symbol = L.resolveValueSymbol(value);
if (!symbol) return false;
const declarations = L.checker.declarationsOf(symbol);
const declaration = declarations.length === 1 ? declarations[0] : undefined;
if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer) return false;
const list = declaration.parent;
const statement = list.parent;
if (
!ts.isVariableDeclarationList(list) || list.declarations.length !== 1 ||
!ts.isVariableStatement(statement) ||
(ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Export) !== 0 ||
declaration.end > value.pos || !isSparseArrayInitializer(L, declaration.initializer)
) {
return false;
}
return symbolUsesPreserveSparsity(L, symbol, declaration.getSourceFile(), value.pos);
}
@@ -1340,15 +1340,26 @@ function deepEqHelper(L: Lowerer, t: IrType, loc: SrcLoc): string {
case "array": {
const len = (arr: IrExpr): IrExpr => ({ kind: "arrIntrinsic", method: "length", receiver: arr, args: [], type: F64, loc });
const at = (arr: IrExpr, i: IrExpr): IrExpr => ({ kind: "arrayGet", arr, index: i, type: t.elem, loc });
const has = (arr: IrExpr, i: IrExpr): IrExpr => ({ kind: "arrayHas", arr, index: i, type: BOOL, loc });
const i = (): IrExpr => ref("i.0", F64);
locals.push({ id: "i.0", name: "i", type: F64, mutable: true });
body = [
bailIf(neq(len(a()), len(b()))),
{
kind: "for",
init: { kind: "varDecl", localId: "i.0", init: num(0), loc },
cond: { kind: "bin", op: "<", left: ref("i.0", F64), right: len(a()), type: BOOL, loc },
update: { kind: "assign", localId: "i.0", value: { kind: "bin", op: "+", left: ref("i.0", F64), right: num(1), type: F64, loc }, loc },
body: [bailIf(not(deq(t.elem, at(a(), ref("i.0", F64)), at(b(), ref("i.0", F64)))))],
cond: { kind: "bin", op: "<", left: i(), right: len(a()), type: BOOL, loc },
update: { kind: "assign", localId: "i.0", value: { kind: "bin", op: "+", left: i(), right: num(1), type: F64, loc }, loc },
body: [
bailIf(neq(has(a(), i()), has(b(), i()))),
{
kind: "if",
cond: has(a(), i()),
then: [bailIf(not(deq(t.elem, at(a(), i()), at(b(), i()))))],
else_: null,
loc,
},
],
loc,
},
ret(boolLit(true)),
@@ -32,6 +32,7 @@ import { CRYPTO_CIPHERS, CRYPTO_CONSTANTS, CRYPTO_CURVES, CRYPTO_HASHES } from "
import { timerStyleCallback } from "./lower-calls.js";
import { registerHttpClientFnBinding } from "./lower-server.js";
import { BOOL, BYTES_U8, CHILD_T, CHILDSTREAM_T, DYN, F64, FSWATCHER_T, PROCSTREAM_T, IrExpr, IrFunction, IrLibFn, IrLocal, IrStmt, IrType, JSVAL, NULL_T, SEARCH_PARAMS_T, SPAWNRES_T, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, funcOf, isUnitType, typeEquals, typeKey } from "../../ir/nodes.js";
import { requireArrayGetSafe } from "./array-density.js";
@@ -1326,6 +1327,10 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
if (spread) {
if (expr.arguments.length === 1) {
// The whole-array form forwards the operand directly (no copy).
const sourceType = L.mapTypeOf(L.typeOf(spread.expression));
if (sourceType?.kind === "array") {
requireArrayGetSafe(L, spread.expression, sourceType.elem, spread, "array argument spread");
}
const packed = L.lowerExprExpecting(spread.expression, arrayOf(STRING));
return { kind: "libCall", fn: fn.fn, args: [packed], type: fn.result, loc };
}
@@ -1333,6 +1338,13 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
// tmpdir.resolve: plain arguments and spread arrays pack into one
// fresh string[] (arrayLit's spread positions copy element-wise,
// JS-exact; a dyn spread source rides the validated extraction).
for (const arg of expr.arguments) {
if (!ts.isSpreadElement(arg)) continue;
const sourceType = L.mapTypeOf(L.typeOf(arg.expression));
if (sourceType?.kind === "array") {
requireArrayGetSafe(L, arg.expression, sourceType.elem, arg, "array argument spread");
}
}
const elems = expr.arguments.map((a) =>
ts.isSpreadElement(a)
? L.lowerExprExpecting(a.expression, arrayOf(STRING))
@@ -6415,6 +6427,7 @@ type TextCodecCtor = {
return { kind: "libCall", fn: "string.fromCharCode", args: [packed], type: STRING, loc };
}
const packed = L.lowerExprExpecting(spread.expression, arrayOf(F64));
requireArrayGetSafe(L, spread.expression, F64, spread, "array argument spread");
return { kind: "libCall", fn: "string.fromCharCode", args: [packed], type: STRING, loc };
}
const elems = call.arguments.map((a) => L.lowerExprExpecting(a, F64));
@@ -27,6 +27,7 @@ import { ambientNsRootOf, ambientUndefReadType, ambientUndefVarRootOf, ambientUn
import { declSymbolOf } from "./lower-modules.js";
import { expandoMemberRead } from "./lower-expando.js";
import { npmStaticPackageOfPath } from "../npm-static.js";
import { requireArrayGetSafe } from "./array-density.js";
/** How a parameter participates in CALL-SITE COMPLETION (the frontend
* completes every call to the one full signature, so the IR and backends
@@ -479,6 +480,10 @@ export interface GenericInstance {
`spreading '${L.fmt(src.type)}' into a '${L.fmt(restType)}' rest parameter (only a same-element-type array spreads)`,
);
}
const sourceType = L.mapTypeOf(L.typeOf(a.expression));
if (sourceType?.kind === "array") {
requireArrayGetSafe(L, a.expression, sourceType.elem, a, "array argument spread");
}
spreads.push(i);
return src;
}
@@ -12,6 +12,7 @@ import { isJsSourceFile, locOf } from "../program.js";
import { DYN_DISPATCH_METHODS, islandPrimitiveExit } from "./lower-calls.js";
import { typeKey } from "../types.js";
import { dynUndefinedExpr, own, WidthLift } from "./lowerer.js";
import { requireArrayGetSafe } from "./array-density.js";
/** Lower an expression whose checker type is statically `undefined`/`void`.
* Optional builtin arguments use this before their ordinary expected-type
@@ -190,6 +191,7 @@ function lowerOptionalDefaultArg(
if (call.arguments.length !== 0) {
L.noLowering(`.toReversed with ${call.arguments.length} arguments`, call);
}
requireArrayGetSafe(L, access.expression, elem, call, "Array.toReversed");
return {
kind: "arrIntrinsic",
method: "toReversed",
@@ -203,6 +205,7 @@ function lowerOptionalDefaultArg(
if (call.arguments.length !== 2 || call.arguments.some(ts.isSpreadElement)) {
L.noLowering(`.with with ${call.arguments.length} arguments`, call);
}
requireArrayGetSafe(L, access.expression, elem, call, "Array.with");
const receiver = L.lowerExpr(access.expression);
const index = L.lowerExprExpecting(call.arguments[0]!, F64);
const value = L.coerceInto(
@@ -223,6 +226,7 @@ function lowerOptionalDefaultArg(
if (call.arguments.some(ts.isSpreadElement)) {
L.unsupported("SC1090", call, "spread arguments to Array.toSpliced");
}
requireArrayGetSafe(L, access.expression, elem, call, "Array.toSpliced");
const receiver = L.lowerExpr(access.expression);
const start = call.arguments[0]
? L.lowerExprExpecting(call.arguments[0], F64)
@@ -320,6 +324,9 @@ function lowerOptionalDefaultArg(
`pushing a spread of '${L.fmt(src.type)}' onto a '${L.fmt(receiverIr)}' array (only a same-element-type array spreads)`,
);
}
if (L.mapTypeOf(L.typeOf(spreadArg.expression))?.kind === "array") {
requireArrayGetSafe(L, spreadArg.expression, elem, spreadArg, "array argument spread");
}
return { kind: "arrIntrinsic", method: "pushSpread", receiver, args: [src], type: F64, loc };
}
// Pushed values flow into the element slot like an assignment would:
@@ -1015,6 +1022,9 @@ function lowerOptionalDefaultArg(
const loc = locOf(call);
const receiver = L.lowerExpr(access.expression);
const arrT = arrayOf(elem);
if (method !== "some" && method !== "every") {
requireArrayGetSafe(L, access.expression, elem, call, `Array.${method}`);
}
const argNode = call.arguments[0];
if (!argNode) L.unsupported("SC1090", call, "this call form"); // tsc-guarded
const { fnArg, arity } = hofCallbackArg(L, argNode, [elem], arrT);
@@ -1583,6 +1593,7 @@ function lowerOptionalDefaultArg(
const loc = locOf(call);
const method = access.name.text === "toSorted" ? "toSorted" : "sort";
const copyFirst = method === "toSorted";
if (copyFirst) requireArrayGetSafe(L, access.expression, elem, call, "Array.toSorted");
if (call.arguments.length === 0 && elem.kind === "string") {
const receiver = L.lowerExpr(access.expression);
const cmp = defaultStringCmpHelper(L, loc);
@@ -1691,14 +1702,16 @@ function lowerOptionalDefaultArg(
return name;
}
/** The insertion-sort loop, from existing IR nodes. Present elements compact
* to the front first; CompareArrayElements then sinks undefined past every
* value without calling the comparator, so the order is values, then
* undefineds. The tail beyond the present count is deleted for the
* in-place sort (holes land last, SortIndexedProperties' SKIP-HOLES) and
* assigned the interned undefined arm for toSorted (`undefExpr`, dense
* per READ-THROUGH-HOLES); a toSorted element type with no undefined arm
* cannot represent a read-through hole and keeps the holes.
/** The insertion-sort loop, from existing IR nodes. Both methods first take a
* shallow working snapshot. Present elements compact within that private
* array; CompareArrayElements then sinks undefined past every value without
* calling the comparator, so the order is values, then undefineds. sort
* commits the sorted prefix and hole tail to the receiver only after every
* comparator call succeeds. This preserves the original receiver during
* comparator observation and leaves it untouched when a comparator throws.
* toSorted returns the working array directly; its hole tail becomes present
* undefined (the call site fences scalar receivers unless they are proven
* dense).
*
* n = a.length;
* for (i = 1; i < n; i++) {
@@ -1728,7 +1741,9 @@ function lowerOptionalDefaultArg(
const i = ref("i.0", F64);
const j = ref("j.0", F64);
const m = ref("m.0", F64);
const at = (index: IrExpr): IrExpr => ({ kind: "arrayGet", arr: ref("a.0", arrT), index, type: elem, loc });
const working = (): IrExpr => ref("w.0", arrT);
const at = (index: IrExpr): IrExpr => ({ kind: "arrayGet", arr: working(), index, type: elem, loc });
const hasWorking: IrExpr = { kind: "arrayHas", arr: working(), index: i, type: BOOL, loc };
const jPlus1: IrExpr = { kind: "bin", op: "+", left: j, right: num(1), type: F64, loc };
const isUndefined = (value: IrExpr): IrExpr | null => {
if (elem.kind === "union" && undefinedTag !== null) {
@@ -1806,7 +1821,7 @@ function lowerOptionalDefaultArg(
kind: "if",
cond: shouldShift,
then: [
{ kind: "arraySet", arr: ref("a.0", arrT), index: jPlus1, value: at(j), loc },
{ kind: "arraySet", arr: working(), index: jPlus1, value: at(j), loc },
{ kind: "assign", localId: "j.0", value: { kind: "bin", op: "-", left: j, right: num(1), type: F64, loc }, loc },
],
else_: [{ kind: "break", loc }],
@@ -1816,28 +1831,26 @@ function lowerOptionalDefaultArg(
loc,
};
const body: IrStmt[] = [
...(copyFirst
? [{
kind: "assign" as const,
localId: "a.0",
value: {
kind: "arrIntrinsic" as const,
method: "slice" as const,
receiver: ref("a.0", arrT),
args: [],
type: arrT,
loc,
},
loc,
}]
: []),
readLenStmt(arrT, loc),
{
kind: "varDecl",
localId: "w.0",
init: {
kind: "arrIntrinsic",
method: "slice",
receiver: ref("a.0", arrT),
args: [],
type: arrT,
loc,
},
loc,
},
{ kind: "varDecl", localId: "m.0", init: num(0), loc },
countedForLoop(loc, [{
kind: "if",
cond: hasElemExpr(arrT, loc),
cond: hasWorking,
then: [
{ kind: "arraySet", arr: ref("a.0", arrT), index: m, value: at(i), loc },
{ kind: "arraySet", arr: working(), index: m, value: at(i), loc },
{ kind: "assign", localId: "m.0", value: { kind: "bin", op: "+", left: m, right: num(1), type: F64, loc }, loc },
],
else_: null,
@@ -1850,8 +1863,8 @@ function lowerOptionalDefaultArg(
update: { kind: "assign", localId: "i.0", value: { kind: "bin", op: "+", left: i, right: num(1), type: F64, loc }, loc },
body: [
copyFirst && undefExpr !== null
? { kind: "arraySet", arr: ref("a.0", arrT), index: i, value: undefExpr, loc }
: { kind: "arrayDelete", arr: ref("a.0", arrT), index: i, loc },
? { kind: "arraySet", arr: working(), index: i, value: undefExpr, loc }
: { kind: "arrayDelete", arr: working(), index: i, loc },
],
loc,
},
@@ -1866,14 +1879,44 @@ function lowerOptionalDefaultArg(
loc,
},
body: [
{ kind: "varDecl", localId: "v.0", init: getElemExpr(arrT, elem, loc), loc },
{ kind: "varDecl", localId: "v.0", init: at(i), loc },
{ kind: "varDecl", localId: "j.0", init: { kind: "bin", op: "-", left: ref("i.0", F64), right: num(1), type: F64, loc }, loc },
shiftLoop,
{ kind: "arraySet", arr: ref("a.0", arrT), index: jPlus1, value: ref("v.0", elem), loc },
{ kind: "arraySet", arr: working(), index: jPlus1, value: ref("v.0", elem), loc },
],
loc,
},
{ kind: "return", value: ref("a.0", arrT), loc },
...(copyFirst
? []
: [
{
kind: "for" as const,
init: { kind: "varDecl" as const, localId: "i.0", init: num(0), loc },
cond: { kind: "bin" as const, op: "<" as const, left: i, right: m, type: BOOL, loc },
update: {
kind: "assign" as const,
localId: "i.0",
value: { kind: "bin" as const, op: "+" as const, left: i, right: num(1), type: F64, loc },
loc,
},
body: [{ kind: "arraySet" as const, arr: ref("a.0", arrT), index: i, value: at(i), loc }],
loc,
},
{
kind: "for" as const,
init: { kind: "varDecl" as const, localId: "i.0", init: m, loc },
cond: { kind: "bin" as const, op: "<" as const, left: i, right: ref("n.0", F64), type: BOOL, loc },
update: {
kind: "assign" as const,
localId: "i.0",
value: { kind: "bin" as const, op: "+" as const, left: i, right: num(1), type: F64, loc },
loc,
},
body: [{ kind: "arrayDelete" as const, arr: ref("a.0", arrT), index: i, loc }],
loc,
},
]),
{ kind: "return", value: copyFirst ? working() : ref("a.0", arrT), loc },
];
return {
name,
@@ -1885,6 +1928,7 @@ function lowerOptionalDefaultArg(
locals: [
{ id: "a.0", name: "a", type: arrT, mutable: true },
{ id: "f.0", name: "f", type: fnT, mutable: true },
{ id: "w.0", name: "w", type: arrT, mutable: false },
{ id: "n.0", name: "n", type: F64, mutable: false },
{ id: "i.0", name: "i", type: F64, mutable: true },
{ id: "m.0", name: "m", type: F64, mutable: true },
@@ -2392,6 +2436,13 @@ function lowerOptionalDefaultArg(
else_: null,
loc,
},
{
kind: "if",
cond: { kind: "unary", op: "!", operand: { kind: "arrayHas", arr: ref("a.0", arrT), index: t, type: BOOL, loc }, type: BOOL, loc },
then: [{ kind: "return", value: miss, loc }],
else_: null,
loc,
},
{
kind: "varDecl",
localId: "v.0",
@@ -25,6 +25,7 @@ import { mixinFnOfCallee } from "./lower-mixins.js";
import { isConstAssertionTypeNode, isGenericCallableMemberType, underConstAssertion, unitOnlyUnion } from "../types.js";
import { lowerYield } from "./lower-generators.js";
import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js";
import { requireArrayGetSafe } from "./array-density.js";
/** An assignable `obj.field` target — a class field, a record field, or a
* class ACCESSOR property (reads become getter calls, writes setter calls;
@@ -3680,6 +3681,10 @@ export function lowerOptionalChain(L: Lowerer, expr: ts.CallExpression | ts.Prop
`spreading '${L.fmt(src.type)}' into a '${L.fmt(type)}' literal (only a same-element-type array spreads)`,
);
}
const sourceType = L.mapTypeOf(L.typeOf(el.expression));
if (sourceType?.kind === "array") {
requireArrayGetSafe(L, el.expression, sourceType.elem, el, "array spread");
}
spreads.push(i);
return src;
}
@@ -10,6 +10,7 @@ import { requiresDynamicApiDiag, requiresDynamicPackageDiag } from "../../diagno
import { isCjsJsFile, isJsSourceFile, locOf, npmPackageNameOf } from "../program.js";
import { foldedStringKeyOf, lowerDynObjectLiteral, pureReemittable } from "./lower-exprs.js";
import { PoisonError, dynUndefinedExpr, newFnCtx, nodeThrowExpr, own } from "./lowerer.js";
import { requireArrayGetSafe } from "./array-density.js";
import {
NODE24_FETCH_COMPAT_PROFILE,
STATIC_HEADERS_CALLS,
@@ -2947,6 +2948,7 @@ export function lowerStaticReadableStreamReaderCall(
`spreading '${L.fmt(src.type)}' into Math.${name} (only a number[] spreads)`,
);
}
requireArrayGetSafe(L, spread.expression, F64, spread, `array spread into Math.${name}`);
return {
kind: "libCall",
fn: name === "max" ? "math.maxArr" : "math.minArr",
@@ -27,6 +27,7 @@ import { UNSUPPORTED, checkerPanicDiag, isCheckerPanic, requiresDynamicDiag } fr
import { isUnitOnlyTsType, unitOnlyUnion } from "../types.js";
import { canonicalBuiltinModule, isRelativeSpecifier } from "../shared.js";
import { probeNodeRequireRefusal } from "../npm.js";
import { requireArrayGetSafe } from "./array-density.js";
/** `const X = /* @__PURE__ *\/ makeX()` at a module's top level: every
* declarator initialized by a call (or `new`) carrying the bundler PURE
@@ -997,6 +998,9 @@ export function lowerStmt(L: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | n
) {
L.badType(sourceNode, L.typeOf(sourceNode));
}
if (source.type.kind === "array") {
requireArrayGetSafe(L, sourceNode, source.type.elem, decl.initializer!, "stored array value iteration");
}
const loc = locOf(decl);
const indexedSource = L.declareHiddenLocal("%numiterSource", source.type);
const index = L.declareHiddenLocal("%numiterIndex", F64);
@@ -5898,6 +5902,15 @@ function isEsModuleStamp(expr: ts.Expression): boolean {
if (recv?.kind === "array" && (proj === "keys" || proj === "entries")) {
const container = L.lowerExpr(src.expression.expression);
if (container.type.kind === "array") {
if (proj === "entries") {
requireArrayGetSafe(
L,
src.expression.expression,
container.type.elem,
stmt.expression,
"Array.entries iteration",
);
}
return lowerForOfArrayIter(L, stmt, container as IrExpr & { type: IrType & { kind: "array" } }, proj);
}
}
@@ -6102,6 +6115,7 @@ function isEsModuleStamp(expr: ts.Expression): boolean {
L.badType(stmt.expression, L.typeOf(stmt.expression));
}
}
requireArrayGetSafe(L, iterSrc, iterable.type.elem, stmt.expression, "array for-of iteration");
if (!ts.isVariableDeclarationList(stmt.initializer)) {
// `for (x of xs)` over a PRE-DECLARED writable binding: JS assigns
// the existing binding once per pass — one shared binding across
+4 -1
View File
@@ -88,7 +88,10 @@ function markFunction(fn: IrFunction): void {
case "exprStmt": expr(s.expr); break;
case "arraySet": {
const id = directCandidate(s.arr);
if (id === null) expr(s.arr);
// A dynamic index can now grow past length and introduce holes.
// Without a range proof, later reads must inspect presence.
if (id !== null) invalidate(id);
else expr(s.arr);
expr(s.index);
expr(s.value);
break;
+4 -4
View File
@@ -1155,10 +1155,10 @@ export type IrStmt =
labels?: string[];
loc: SrcLoc;
}
/** Element write `a[i] = v` — statement-only, like `assign`. Valid indices
* are [0, length]; i == length appends (JS would create a hole past that —
* scriptc traps instead, see SEMANTICS.md). Ownership of a refcounted
* value MOVES into the array; the replaced element is released. */
/** Element write `a[i] = v` — statement-only, like `assign`. Valid array
* indices are [0, 2^32-2]; writes at or beyond length grow through i + 1
* and leave intermediate holes. Ownership of a refcounted value MOVES
* into the array; the replaced element is released. */
| { kind: "arraySet"; arr: IrExpr; index: IrExpr; value: IrExpr; loc: SrcLoc }
/** `delete a[i]`: clears presence without changing length. */
| { kind: "arrayDelete"; arr: IrExpr; index: IrExpr; loc: SrcLoc }
+6
View File
@@ -208,11 +208,17 @@ test("dense-array pass bypasses hole checks only for untouched packed locals", (
name: "holey", params: [], returnType: F64, locals: [{ id: "a.0", name: "a", type: arr, mutable: false }], loc,
body: [{ kind: "varDecl", localId: "a.0", init: { kind: "arrayLit", elems: [{ kind: "numLit", value: 1, type: F64, loc }], type: arr, loc }, loc }, { kind: "arrayDelete", arr: { kind: "varRef", localId: "a.0", type: arr, loc }, index: { kind: "numLit", value: 0, type: F64, loc }, loc }, { kind: "return", value: get("a.0"), loc }],
},
{
name: "written", params: [], returnType: F64, locals: [{ id: "a.0", name: "a", type: arr, mutable: false }], loc,
body: [{ kind: "varDecl", localId: "a.0", init: { kind: "arrayLit", elems: [{ kind: "numLit", value: 1, type: F64, loc }], type: arr, loc }, loc }, { kind: "arraySet", arr: { kind: "varRef", localId: "a.0", type: arr, loc }, index: { kind: "numLit", value: 4, type: F64, loc }, value: { kind: "numLit", value: 5, type: F64, loc }, loc }, { kind: "return", value: get("a.0"), loc }],
},
],
};
markDenseArrayReads(mod);
const dense = mod.functions[0]!.body[1]!;
const holey = mod.functions[1]!.body[2]!;
const written = mod.functions[2]!.body[2]!;
expect(dense.kind === "return" && dense.value?.kind === "arrayGet" && dense.value.dense).toBe(true);
expect(holey.kind === "return" && holey.value?.kind === "arrayGet" && holey.value.dense).toBeFalsy();
expect(written.kind === "return" && written.value?.kind === "arrayGet" && written.value.dense).toBeFalsy();
});
@@ -5482,12 +5482,36 @@
],
"diags": []
},
"<repo>/tests/corpus/2672-http-request-response-callback.ts": {
"order": [
"<repo>/tests/corpus/2672-http-request-response-callback.ts"
],
"diags": []
},
"<repo>/tests/corpus/2673-sparse-array-js.js": {
"order": [
"<repo>/tests/corpus/2673-sparse-array-js.js"
],
"diags": []
},
"<repo>/tests/corpus/2684-sparse-live-array-payload.ts": {
"order": [
"<repo>/tests/corpus/2684-sparse-live-array-payload.ts"
],
"diags": []
},
"<repo>/tests/corpus/2693-sparse-far-index-copy.ts": {
"order": [
"<repo>/tests/corpus/2693-sparse-far-index-copy.ts"
],
"diags": []
},
"<repo>/tests/corpus/2695-sparse-frontend-review-fixes.ts": {
"order": [
"<repo>/tests/corpus/2695-sparse-frontend-review-fixes.ts"
],
"diags": []
},
"<repo>/tests/corpus/300-if-else.ts": {
"order": [
"<repo>/tests/corpus/300-if-else.ts"
@@ -7461,6 +7485,12 @@
],
"diags": []
},
"<repo>/tests/diagnostics/sparse-get-scalar-fences.ts": {
"order": [
"<repo>/tests/diagnostics/sparse-get-scalar-fences.ts"
],
"diags": []
},
"<repo>/tests/diagnostics/spi-off/main.ts": {
"order": [
"<repo>/tests/diagnostics/spi-off/main.ts"
+50 -23
View File
@@ -16,9 +16,9 @@ static void scr_arr_oom(void) {
scr_trap("scriptc: out of memory\n");
}
/* JS would return undefined for an OOB read and create holes for a far OOB
* write; both are unrepresentable here (see SEMANTICS.md), so any invalid
* index — negative, fractional, NaN, or past the allowed end — traps. */
/* Reads outside length and values that are not array indices remain
* unrepresentable. Writes accept every JS array index (through 2^32-2),
* growing length and creating holes as needed. */
static void scr_arr_trap_oob(double i, size_t len) {
char buf[32];
scr_f64_to_str(i, buf);
@@ -26,12 +26,10 @@ static void scr_arr_trap_oob(double i, size_t len) {
buf, len);
}
/* Validate i as an element index. limit is a->len for reads, a->len + 1 for
* writes (i == len appends). NaN fails the >= 0 test; fractional indices
* fail the trunc test. */
static size_t scr_arr_check_index(const ScrArr *a, double i, bool allow_append) {
size_t limit = a->len + (allow_append ? 1 : 0);
if (!(i >= 0) || i != trunc(i) || i >= (double)limit) {
/* 2^32-1 is the maximum array length and therefore not an array index. */
static size_t scr_arr_check_index(const ScrArr *a, double i, bool write) {
double limit = write ? 4294967295.0 : (double)a->len;
if (!(i >= 0) || i != trunc(i) || i >= limit) {
scr_arr_trap_oob(i, a->len);
}
return (size_t)i;
@@ -95,6 +93,16 @@ static bool scr_arr_present_at(const ScrArr *a, size_t i) {
return !a->present || (a->present[i >> 3] & (uint8_t)(1u << (i & 7))) != 0;
}
static void scr_arr_make_sparse(ScrArr *a) {
if (a->present) return;
size_t bytes = (a->cap + 7) / 8;
a->present = calloc(bytes ? bytes : 1, 1);
if (!a->present) scr_arr_oom();
for (size_t i = 0; i < a->len; i++) {
a->present[i >> 3] |= (uint8_t)(1u << (i & 7));
}
}
static void scr_arr_mark_present(ScrArr *a, size_t i, bool present) {
if (!a->present) {
if (present) return;
@@ -233,12 +241,7 @@ void scr_arr_set_sparse_len(ScrArr *a, double n, void *fill) {
}
if (len == a->len) return;
scr_arr_grow(a, len);
if (!a->present) {
size_t bytes = (a->cap + 7) / 8;
a->present = calloc(bytes ? bytes : 1, 1);
if (!a->present) scr_arr_oom();
for (size_t i = 0; i < a->len; i++) scr_arr_mark_present(a, i, true);
}
scr_arr_make_sparse(a);
uint64_t slot = scr_slot_from_ptr(fill);
for (size_t i = a->len; i < len; i++) {
a->data[i] = slot;
@@ -360,15 +363,35 @@ void *scr_arr_get_dense_ref(ScrArr *a, double i) {
return p;
}
/* ── writes: i == len appends ──────────────────────────────────────────── */
/* ── writes: far indices grow length and create holes ──────────────────── */
static void scr_arr_set_slot(ScrArr *a, double i, uint64_t slot) {
static void scr_arr_set_slot(ScrArr *a, double i, uint64_t slot,
uint64_t hole) {
size_t idx = scr_arr_check_index(a, i, true);
if (idx == a->len) {
scr_arr_grow(a, a->len + 1);
a->len++;
if (idx >= a->len) {
size_t old_len = a->len;
/* An already-sparse REF array can carry the undefined union singleton
* in its holes. Extend with the same inert backing when the caller did
* not supply the static element type's undefined arm. */
if (idx > old_len && hole == 0 && a->present && a->elem == SCR_ELEM_REF) {
for (size_t j = 0; j < old_len; j++) {
if (!scr_arr_present_at(a, j)) {
hole = a->data[j];
break;
}
}
}
scr_arr_grow(a, idx + 1);
if (idx > old_len) {
scr_arr_make_sparse(a);
for (size_t j = old_len; j < idx; j++) {
a->data[j] = hole;
scr_arr_mark_present(a, j, false);
}
}
a->data[idx] = slot;
scr_arr_mark_present(a, idx, true);
a->len = idx + 1;
return;
}
/* Unlink-then-release: a release can trigger a cycle collection, which
@@ -382,15 +405,19 @@ static void scr_arr_set_slot(ScrArr *a, double i, uint64_t slot) {
}
void scr_arr_set_f64(ScrArr *a, double i, double v) {
scr_arr_set_slot(a, i, scr_slot_from_f64(v));
scr_arr_set_slot(a, i, scr_slot_from_f64(v), 0);
}
void scr_arr_set_bool(ScrArr *a, double i, bool v) {
scr_arr_set_slot(a, i, (uint64_t)(v ? 1 : 0));
scr_arr_set_slot(a, i, (uint64_t)(v ? 1 : 0), 0);
}
void scr_arr_set_ref(ScrArr *a, double i, void *v) {
scr_arr_set_slot(a, i, scr_slot_from_ptr(v));
scr_arr_set_slot(a, i, scr_slot_from_ptr(v), 0);
}
void scr_arr_set_ref_fill(ScrArr *a, double i, void *v, void *fill) {
scr_arr_set_slot(a, i, scr_slot_from_ptr(v), scr_slot_from_ptr(fill));
}
/* ── push / pop ────────────────────────────────────────────────────────── */
+23 -52
View File
@@ -44,29 +44,15 @@ static bool copying_arr_present_at(const ScrArr *a, size_t i) {
return !a->present || (a->present[i >> 3] & (uint8_t)(1u << (i & 7))) != 0;
}
static void copying_arr_mark_hole(ScrArr *a, size_t i) {
if (!a->present) {
size_t bytes = (a->cap + 7) / 8;
a->present = malloc(bytes ? bytes : 1);
if (!a->present) scr_trap("scriptc: out of memory\n");
memset(a->present, 0xff, bytes);
if (a->cap & 7) a->present[bytes - 1] &= (uint8_t)((1u << (a->cap & 7)) - 1u);
}
a->present[i >> 3] &= (uint8_t)~(1u << (i & 7));
}
/* The ES2023 copying methods read every index (Get), so a source hole
* densifies to its backing where that carries the value Get answers — the
* interned undefined arm of a union element (nonzero). A zero backing
* (f64/bool/plain ref) has no undefined to give: the slot stays a hole so
* reads keep the deliberate trap instead of silently answering 0/NULL. */
* densifies to the undefined value carried in a REF hole's backing slot.
* Scalar and NULL-backed holes cannot encode undefined in their typed ABI;
* fail explicitly rather than fabricating 0/NULL or preserving the hole. */
static void copying_arr_copy_slot(ScrArr *out, const ScrArr *src, size_t i) {
uint64_t slot = src->data[i];
if (!copying_arr_present_at(src, i) && slot == 0) {
out->data[out->len] = 0;
copying_arr_mark_hole(out, out->len);
out->len++;
return;
if (!copying_arr_present_at(src, i) &&
(src->elem != SCR_ELEM_REF || slot == 0)) {
scr_trap("scriptc: TypeError: array hole cannot be represented as undefined by the element type\n");
}
out->data[out->len++] = copying_arr_retain_slot(src, slot);
}
@@ -95,31 +81,25 @@ ScrArr *scr_arr_to_spliced(const ScrArr *a, double start,
size_t out_len = a->len - ndelete + items->len;
ScrArr *out = copying_arr_new_like(a, out_len);
for (size_t i = 0; i < from; i++) copying_arr_copy_slot(out, a, i);
for (size_t i = 0; i < items->len; i++) {
out->data[out->len++] =
copying_arr_retain_slot(a, items->data[i]);
}
for (size_t i = 0; i < items->len; i++)
copying_arr_copy_slot(out, items, i);
for (size_t i = from + ndelete; i < a->len; i++) {
copying_arr_copy_slot(out, a, i);
}
return out;
}
/* `with` builds on slice (which preserves holes); densify the copy under
* the same representability rule as copying_arr_copy_slot. */
static void copying_arr_densify(ScrArr *out) {
if (!out->present) return;
bool all_present = true;
for (size_t i = 0; i < out->len; i++) {
uint8_t mask = (uint8_t)(1u << (i & 7));
if (out->present[i >> 3] & mask) continue;
if (out->data[i] != 0) out->present[i >> 3] |= mask;
else all_present = false;
}
if (all_present) {
free(out->present);
out->present = NULL;
static ScrArr *copying_arr_with_slot(ScrArr *a, size_t index,
uint64_t replacement) {
ScrArr *out = copying_arr_new_like(a, a->len);
for (size_t i = 0; i < a->len; i++) {
if (i == index) {
out->data[out->len++] = copying_arr_retain_slot(a, replacement);
} else {
copying_arr_copy_slot(out, a, i);
}
}
return out;
}
static bool copying_arr_with_index(const ScrArr *a, double index,
@@ -142,30 +122,21 @@ static bool copying_arr_with_index(const ScrArr *a, double index,
ScrArr *scr_arr_with_f64(ScrArr *a, double index, double value) {
size_t i;
if (!copying_arr_with_index(a, index, &i)) return NULL;
ScrArr *out = scr_arr_slice(a, 0, INFINITY);
copying_arr_densify(out);
scr_arr_set_f64(out, (double)i, value);
return out;
uint64_t slot;
memcpy(&slot, &value, sizeof slot);
return copying_arr_with_slot(a, i, slot);
}
ScrArr *scr_arr_with_bool(ScrArr *a, double index, bool value) {
size_t i;
if (!copying_arr_with_index(a, index, &i)) return NULL;
ScrArr *out = scr_arr_slice(a, 0, INFINITY);
copying_arr_densify(out);
scr_arr_set_bool(out, (double)i, value);
return out;
return copying_arr_with_slot(a, i, value ? 1 : 0);
}
ScrArr *scr_arr_with_ref(ScrArr *a, double index, void *value) {
size_t i;
if (!copying_arr_with_index(a, index, &i)) return NULL;
ScrArr *out = scr_arr_slice(a, 0, INFINITY);
copying_arr_densify(out);
uint64_t retained =
copying_arr_retain_slot(a, copying_slot_from_ptr(value));
scr_arr_set_ref(out, (double)i, copying_slot_to_ptr(retained));
return out;
return copying_arr_with_slot(a, i, copying_slot_from_ptr(value));
}
ScrBytes *scr_bytes_to_reversed(const ScrBytes *b) {
+1 -1
View File
@@ -2695,7 +2695,7 @@ void scr_sc_validate_options(const ScrDyn *options) {
if (tr == NULL || tr->kind == SCR_DYN_UNDEF) return;
if (tr->kind != SCR_DYN_ARR) {
static const char msg[] =
"Failed to execute 'structuredClone': transfer in Options can not be converted to sequence.";
"Failed to execute 'structuredClone': transfer in Options cannot be converted to sequence.";
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_TYPE");
return;
}
+15 -9
View File
@@ -719,10 +719,10 @@ ScrStr *scr_b64_missing_arg(void);
* layout is untouched (interned-literal statics depend on it), so the
* element kind lives here instead of in a shared object header.
*
* Index arguments are doubles (JS numbers). A valid index is a non-negative
* integer; reads must be < len, writes may also be == len (append — JS
* would create a hole past the end; scriptc traps instead, see
* SEMANTICS.md). Anything else prints
* Index arguments are doubles (JS numbers). A valid array index is an
* integer in [0, 2^32-2]. Reads must also be < len; writes grow length to
* index + 1 and create holes between the old end and the written slot.
* Anything else prints
* scriptc: RangeError: array index <i> out of bounds (length <n>)
* to stderr and abort()s. pop() on an empty array likewise traps (JS
* returns undefined — unrepresentable here).
@@ -825,20 +825,26 @@ double scr_arr_get_dense_f64(ScrArr *a, double i);
bool scr_arr_get_dense_bool(ScrArr *a, double i);
void *scr_arr_get_dense_ref(ScrArr *a, double i);
/* i == len appends; the _ref variant releases the old element (when
* replacing) and takes ownership of the new one. */
/* A write at or beyond len grows through i + 1, leaving intermediate slots
* as holes. The _ref variants release the old element (when replacing) and
* take ownership of the new one. _ref_fill stores fill as inert backing for
* new holes, allowing a statically known undefined union arm to answer Get. */
void scr_arr_set_f64(ScrArr *a, double i, double v);
void scr_arr_set_bool(ScrArr *a, double i, bool v);
void scr_arr_set_ref(ScrArr *a, double i, void *v);
void scr_arr_set_ref_fill(ScrArr *a, double i, void *v, void *fill);
/* slice(start?, end?): a fresh +1 shallow copy of the index range —
* ToIntegerOrInfinity indices, negatives from the end, clamping; ref
* elements retain into the copy. Borrows a. */
ScrArr *scr_arr_slice(ScrArr *a, double start, double end);
/* ES2023 copying methods. All borrow their inputs and return fresh +1
* shallow copies. with() raises Node's catchable RangeError for an invalid
* relative index; the ref variant retains the borrowed replacement. */
/* ES2023 copying methods. All borrow their inputs and return fresh packed +1
* shallow copies; source holes densify to present undefined values via a
* REF array's non-NULL hole backing. A hole whose typed element ABI cannot
* encode undefined traps rather than becoming 0/NULL. with() raises Node's
* catchable RangeError for an invalid relative index; the ref variant
* retains the borrowed replacement. */
ScrArr *scr_arr_to_reversed(const ScrArr *a);
ScrArr *scr_arr_to_spliced(const ScrArr *a, double start,
double delete_count, const ScrArr *items);
+1 -1
View File
@@ -1212,7 +1212,7 @@ static const char web_prelude[] =
" if (typeof tr === 'string') throw 0;\n"
" list = [...tr];\n"
" } catch (e) {\n"
" throw scErr(\"Failed to execute 'structuredClone': transfer in Options can not be converted to sequence.\");\n"
" throw scErr(\"Failed to execute 'structuredClone': transfer in Options cannot be converted to sequence.\");\n"
" }\n"
" if (list.length > 0) {\n"
" throw new DOMException('Found invalid value in transferList.', 'DataCloneError');\n"
+7 -7
View File
@@ -19,6 +19,7 @@ beforeAll(async () => {
"-o", bin,
join(testDir, "test_array.c"),
join(testDir, "../src/scr_array.c"),
join(testDir, "../src/scr_copying.c"),
join(testDir, "../src/scr_string.c"),
join(testDir, "../src/scr_number.c"),
join(testDir, "../src/scr_bytes.c"),
@@ -35,13 +36,12 @@ test("array runtime: push/pop/set/get, RC recursion, growth", async () => {
expect(stderr.trim()).toMatch(/^(\d+)\/\1 cases passed$/);
});
// JS returns undefined for OOB reads and creates holes for far writes; both
// are unrepresentable, so the runtime must trap (documented divergence).
test.each([
["--crash-get-oob", "array index 1 out of bounds (length 1)"],
["--crash-get-frac", "array index 0.5 out of bounds (length 1)"],
["--crash-set-oob", "array index 2 out of bounds (length 1)"],
["--crash-pop-empty", "pop() on an empty array"],
["--crash-get-oob", "RangeError: array index 1 out of bounds (length 1)"],
["--crash-get-frac", "RangeError: array index 0.5 out of bounds (length 1)"],
["--crash-set-max", "RangeError: array index 4294967295 out of bounds (length 1)"],
["--crash-pop-empty", "RangeError: pop() on an empty array"],
["--crash-copy-hole", "TypeError: array hole cannot be represented as undefined by the element type"],
])("trap aborts (%s)", async (mode, message) => {
const err = await execFileAsync(bin, [mode]).then(
() => {
@@ -50,5 +50,5 @@ test.each([
(e: Error & { signal?: string; stderr?: string }) => e,
);
expect(err.signal).toBe("SIGABRT");
expect(err.stderr).toContain(`scriptc: RangeError: ${message}`);
expect(err.stderr).toContain(`scriptc: ${message}`);
});
+103 -3
View File
@@ -4,8 +4,9 @@
* (no args) run all assertions; prints "N/N cases passed"
* --crash-get-oob read past the end → RangeError + abort()
* --crash-get-frac read a fractional index → RangeError + abort()
* --crash-set-oob write past len (holes) → RangeError + abort()
* --crash-set-max write at 2^32-1 → RangeError + abort()
* --crash-pop-empty pop an empty array → RangeError + abort()
* --crash-copy-hole densify a scalar hole → TypeError + abort()
*
* The RC-recursion cases (array of strings, array of arrays of strings)
* assert live counts directly: releasing the outer array must release
@@ -436,6 +437,30 @@ static void test_sparse(void) {
scr_arr_release(slice);
scr_arr_release(a);
ScrArr *far = scr_arr_new(SCR_ELEM_F64, 0);
scr_arr_push_f64(far, 1);
scr_arr_set_f64(far, 4, 5);
check_f64(scr_arr_len(far), 5, "far indexed write grows length");
check(scr_arr_has(far, 0) && !scr_arr_has(far, 1) &&
!scr_arr_has(far, 2) && !scr_arr_has(far, 3) &&
scr_arr_has(far, 4),
"far indexed write creates intermediate holes");
check_f64(scr_arr_get_f64(far, 0), 1, "far write preserves prefix");
check_f64(scr_arr_get_f64(far, 4), 5, "far write stores endpoint");
scr_arr_set_f64(far, 5, 6);
check_f64(scr_arr_len(far), 6, "write at sparse length appends");
check(!scr_arr_has(far, 3) && scr_arr_has(far, 5),
"sparse append preserves existing holes");
scr_arr_release(far);
ScrArr *far_refs = scr_arr_new(SCR_ELEM_STR, 0);
scr_arr_push_ref(far_refs, scr_str_new("a", 1));
scr_arr_set_ref(far_refs, 3, scr_str_new("d", 1));
check_f64(scr_arr_len(far_refs), 4, "far ref write grows length");
check(!scr_arr_has(far_refs, 1) && !scr_arr_has(far_refs, 2),
"far ref write leaves unowned holes");
scr_arr_release(far_refs);
ScrArr *dst = scr_arr_new(SCR_ELEM_F64, 0);
ScrArr *src = scr_arr_new(SCR_ELEM_F64, 1);
scr_arr_push_f64(src, 1);
@@ -453,6 +478,76 @@ static void test_sparse(void) {
#endif
}
static void test_copying_holes(void) {
long live0 = mock_live;
MockRec *undefined_fill = mock_new(-1);
ScrArr *src = scr_arr_new_ref(&mock_retain, &mock_release, NULL, 0);
scr_arr_push_ref(src, mock_new(1));
scr_arr_set_sparse_len(src, 4, undefined_fill);
scr_arr_set_ref(src, 2, mock_new(3));
ScrArr *reversed = scr_arr_to_reversed(src);
check(reversed->present == NULL, "toReversed densifies represented holes");
check(scr_arr_has(reversed, 0) && scr_arr_has(reversed, 1) &&
scr_arr_has(reversed, 2) && scr_arr_has(reversed, 3),
"toReversed result has every index");
MockRec *v = scr_arr_get_ref(reversed, 0);
check(v == undefined_fill, "toReversed reads a hole as undefined backing");
mock_release(v);
check(undefined_fill->rc == 3,
"toReversed retains each densified undefined backing");
scr_arr_release(reversed);
check(undefined_fill->rc == 1,
"toReversed releases densified undefined backings");
ScrArr *items = scr_arr_new_ref(&mock_retain, &mock_release, NULL, 0);
scr_arr_push_ref(items, mock_new(2));
ScrArr *spliced = scr_arr_to_spliced(src, 1, 1, items);
check(spliced->present == NULL, "toSpliced densifies kept holes");
check_f64(scr_arr_len(spliced), 4, "toSpliced keeps expected length");
v = scr_arr_get_ref(spliced, 3);
check(v == undefined_fill, "toSpliced reads a kept hole as undefined backing");
mock_release(v);
check(undefined_fill->rc == 2,
"toSpliced retains a kept undefined backing");
scr_arr_release(spliced);
check(undefined_fill->rc == 1,
"toSpliced releases a kept undefined backing");
MockRec *replacement = mock_new(9);
ScrArr *with = scr_arr_with_ref(src, 0, replacement);
check(with->present == NULL, "with densifies represented holes");
check(undefined_fill->rc == 3, "with retains densified undefined backings");
check(replacement->rc == 2, "with retains the borrowed replacement");
scr_arr_release(with);
check(undefined_fill->rc == 1, "with releases densified undefined backings");
check(replacement->rc == 1, "with releases its replacement reference");
mock_release(replacement);
ScrArr *plain = scr_arr_new(SCR_ELEM_F64, 0);
scr_arr_push_f64(plain, 1);
scr_arr_set_f64(plain, 2, 3);
ScrArr *no_items = scr_arr_new(SCR_ELEM_F64, 0);
ScrArr *deleted = scr_arr_to_spliced(plain, 1, 1, no_items);
check(deleted->present == NULL && scr_arr_len(deleted) == 2,
"toSpliced does not Get a deleted unrepresentable hole");
check_f64(scr_arr_get_f64(deleted, 0), 1, "toSpliced deleted-hole prefix");
check_f64(scr_arr_get_f64(deleted, 1), 3, "toSpliced deleted-hole suffix");
ScrArr *replaced = scr_arr_with_f64(plain, 1, 2);
check(replaced->present == NULL && scr_arr_has(replaced, 1),
"with does not Get its replaced hole");
check_f64(scr_arr_get_f64(replaced, 1), 2, "with stores over a scalar hole");
scr_arr_release(replaced);
scr_arr_release(deleted);
scr_arr_release(no_items);
scr_arr_release(plain);
scr_arr_release(items);
scr_arr_release(src);
mock_release(undefined_fill);
check(mock_live == live0, "copying methods balance ref elements");
}
int main(int argc, char **argv) {
if (argc > 1) {
ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0);
@@ -461,11 +556,15 @@ int main(int argc, char **argv) {
scr_arr_get_f64(a, 1); /* len is 1 */
} else if (strcmp(argv[1], "--crash-get-frac") == 0) {
scr_arr_get_f64(a, 0.5);
} else if (strcmp(argv[1], "--crash-set-oob") == 0) {
scr_arr_set_f64(a, 2, 9); /* would create a hole */
} else if (strcmp(argv[1], "--crash-set-max") == 0) {
scr_arr_set_f64(a, 4294967295.0, 9);
} else if (strcmp(argv[1], "--crash-pop-empty") == 0) {
scr_arr_pop_f64(a);
scr_arr_pop_f64(a); /* now empty */
} else if (strcmp(argv[1], "--crash-copy-hole") == 0) {
scr_arr_set_sparse_len(a, 2, NULL);
ScrArr *copy = scr_arr_to_reversed(a);
scr_arr_release(copy);
} else {
fprintf(stderr, "unknown mode %s\n", argv[1]);
return 2;
@@ -481,6 +580,7 @@ int main(int argc, char **argv) {
test_index_of_includes();
test_ref_elements();
test_ref_cycle();
test_copying_holes();
test_join();
test_sparse();
+1 -1
View File
@@ -8,7 +8,7 @@ const assert = require('assert');
assert.throws(() => structuredClone(), { code: 'ERR_MISSING_ARGS' });
assert.throws(() => structuredClone(undefined, ''), { code: 'ERR_INVALID_ARG_TYPE', message: "Failed to execute 'structuredClone': Options cannot be converted to a dictionary" });
assert.throws(() => structuredClone(undefined, 1), { code: 'ERR_INVALID_ARG_TYPE' });
assert.throws(() => structuredClone(undefined, { transfer: 1 }), { code: 'ERR_INVALID_ARG_TYPE', message: "Failed to execute 'structuredClone': transfer in Options can not be converted to sequence." });
assert.throws(() => structuredClone(undefined, { transfer: 1 }), { code: 'ERR_INVALID_ARG_TYPE', message: /Failed to execute 'structuredClone': transfer in Options can ?not be converted to sequence\./ });
assert.throws(() => structuredClone(undefined, { transfer: null }), { code: 'ERR_INVALID_ARG_TYPE' });
assert.strictEqual(structuredClone(undefined), undefined);
assert.strictEqual(structuredClone(undefined, null), undefined);
@@ -0,0 +1,42 @@
// Live Web references must carry an array's hole bitmap through dynamic
// materialization, mutation commits, and cached structural-cast refreshes.
const materializedSource: number[] = new Array(4);
materializedSource[1] = 11;
materializedSource[3] = 33;
const materialized: unknown = AbortSignal.abort(materializedSource).reason;
if (Array.isArray(materialized)) {
console.log(
"materialized",
materialized.length,
Object.keys(materialized).join(","),
JSON.stringify(materialized),
);
}
const committedSource: number[] = new Array(3);
committedSource[0] = 1;
committedSource[2] = 3;
const committed: unknown = AbortSignal.abort(committedSource).reason;
if (Array.isArray(committed)) committed[1] = 2;
console.log(
"committed",
committedSource.length,
Object.keys(committedSource).join(","),
JSON.stringify(committedSource),
);
const refreshedSource: number[] = new Array(4);
refreshedSource[1] = 10;
const refreshValue: unknown = AbortSignal.abort(refreshedSource).reason;
const first = refreshValue as (number | string)[];
delete refreshedSource[1];
refreshedSource[2] = 20;
const second = refreshValue as (number | string)[];
console.log(
"refreshed",
first === second,
second.length,
Object.keys(second).join(","),
JSON.stringify(second),
);
@@ -0,0 +1,47 @@
// Far indexed writes grow ordinary arrays and leave intermediate holes.
const numbers = [1];
numbers[4] = 5;
console.log(
"numbers",
numbers.length,
0 in numbers,
1 in numbers,
3 in numbers,
4 in numbers,
numbers.join(","),
);
const words = ["a"];
words[3] = "d";
console.log("words", words.length, 1 in words, 2 in words, words.join("|"));
// A far write on an initially empty optional REF array gives each new hole
// the undefined arm as backing. Join skips the holes, while copying methods
// Get and densify them as present undefined values.
const optional: (number | undefined)[] = [];
optional[2] = 7;
console.log(
"optional",
optional.length,
0 in optional,
1 in optional,
2 in optional,
optional.join(","),
);
const reversed = optional.toReversed();
console.log(
"copy",
reversed.length,
0 in reversed,
1 in reversed,
2 in reversed,
reversed[0],
reversed[1] === undefined,
reversed[2] === undefined,
);
// Join must not read a hole even when its REF representation has no
// undefined arm and therefore leaves zero backing.
const mixed: (number | string)[] = [];
mixed[2] = "x";
console.log("mixed", mixed.join("|"));
@@ -0,0 +1,79 @@
import assert from "node:assert";
// Static deep equality compares indexed-property presence before values. Two
// scalar holes never reach an unrepresentable arrayGet, while a hole remains
// distinct from a present undefined in an undefined-capable array.
const scalarHolesA: number[] = new Array(3);
const scalarHolesB: number[] = new Array(3);
assert.deepStrictEqual(scalarHolesA, scalarHolesB);
scalarHolesB[1] = 0;
assert.notDeepStrictEqual(scalarHolesA, scalarHolesB);
const unionHole: (number | undefined)[] = new Array(1);
const unionUndefined: (number | undefined)[] = new Array(1);
unionUndefined[0] = undefined;
assert.notDeepStrictEqual(unionHole, unionUndefined);
console.log("deep-presence", Object.keys(scalarHolesA).length, 0 in unionHole, 0 in unionUndefined);
// In-place sort works privately until all comparisons succeed. Comparator
// observation sees the original receiver, and a throw commits no writes.
const observed = [3, 1, 2];
let receiverStayedOriginal = true;
observed.sort((a, b) => {
receiverStayedOriginal = receiverStayedOriginal && observed.join(",") === "3,1,2";
return a - b;
});
console.log("sort-observe", receiverStayedOriginal, observed.join(","));
const throwing = [4, 2, 3];
let caught = false;
try {
throwing.sort((_a, _b): number => {
throw new Error("stop sort");
});
} catch (error) {
caught = error instanceof Error && error.message === "stop sort";
}
console.log("sort-throw", caught, throwing.join(","));
const sparseSort: number[] = new Array(5);
sparseSort[1] = 3;
sparseSort[3] = 1;
const sparseKeys = Object.keys(sparseSort).join(",");
try {
sparseSort.sort((_a, _b): number => {
throw new Error("sparse stop");
});
} catch {}
console.log("sparse-throw", Object.keys(sparseSort).join(",") === sparseKeys, sparseSort.length);
sparseSort.sort((a, b) => a - b);
console.log("sparse-commit", sparseSort.join(","), Object.keys(sparseSort).join(","), sparseSort.length);
// at has an undefined-capable result even when the element slot is scalar,
// so a presence miss returns that result arm without touching the slot.
const scalarAt: number[] = new Array(2);
scalarAt[1] = 7;
console.log("at-hole", scalarAt.at(0) === undefined, scalarAt.at(1), scalarAt.at(-2) === undefined);
// Undefined-capable element storage supports Get-based callback visits to
// holes directly.
const searchable: (number | undefined)[] = new Array(3);
searchable[2] = 5;
let visits = "";
const holeIndex = searchable.findIndex((value, index) => {
visits += `${index}${value === undefined ? "u" : "v"}`;
return value === undefined;
});
const lastHole = searchable.findLastIndex((value) => value === undefined);
console.log("find-holes", holeIndex, lastHole, visits);
// Packed provenance discharges scalar Get fences through a direct parameter,
// for-of, spread, and toSorted.
function firstEven(values: number[]): number {
return values.findIndex((value) => value % 2 === 0);
}
const packed = [3, 2, 1];
let packedSum = 0;
for (const value of packed) packedSum += value;
const packedCopy = [...packed];
console.log("packed", firstEven(packed), packedSum, packedCopy.join(","), packed.toSorted((a, b) => a - b).join(","));
@@ -0,0 +1,44 @@
const scalarHoles: number[] = new Array(3);
// Direct Get-based methods cannot materialize undefined in number slots.
scalarHoles.find((value) => value > 0);
scalarHoles.findIndex((value) => value > 0);
scalarHoles.findLast((value) => value > 0);
scalarHoles.findLastIndex((value) => value > 0);
// Iteration and spread have the same read-through-holes behavior.
for (const value of scalarHoles) console.log(value);
const copied = [...scalarHoles];
console.log(copied.length);
function takeAll(...values: number[]): number {
return values.length;
}
takeAll(...scalarHoles);
Math.max(...scalarHoles);
String.fromCharCode(...scalarHoles);
// Copying sort reads through holes and produces present undefined entries.
scalarHoles.toSorted((a, b) => a - b);
// A later write cannot make this earlier Get safe.
const writtenLater = new Array<number>(2);
writtenLater.find((value) => value > 0);
writtenLater[0] = 1;
// Dense construction is safe even with scalar element storage.
const denseLiteral = [3, 1, 2];
denseLiteral.find((value) => value > 0);
for (const value of denseLiteral) console.log(value);
console.log([...denseLiteral].length, denseLiteral.toSorted((a, b) => a - b).length);
const filled = new Array<number>(3);
filled[0] = 0;
filled[1] = 0;
filled[2] = 0;
filled.find((value) => value > 0);
for (const value of filled) console.log(value);
console.log([...filled].length, filled.toSorted((a, b) => a - b).length);
// Parameters and other unknown provenance retain historical lowering.
function readUnknown(values: number[]): void {
values.find((value) => value > 0);
for (const value of values) console.log(value);
console.log([...values].length, values.toSorted((a, b) => a - b).length);
}
@@ -0,0 +1,76 @@
sparse-get-scalar-fences.ts:3:1 - error SC1090: Array.find on a sparse 'number[]' value (a hole must be observed as undefined, which 'number' element slots cannot represent - use an undefined-capable element type or a dense array) is not supported yet
2 | // Direct Get-based methods cannot materialize undefined in number slots.
3 | scalarHoles.find((value) => value > 0);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 | scalarHoles.findIndex((value) => value > 0);
sparse-get-scalar-fences.ts:4:1 - error SC1090: Array.findIndex on a sparse 'number[]' value (a hole must be observed as undefined, which 'number' element slots cannot represent - use an undefined-capable element type or a dense array) is not supported yet
3 | scalarHoles.find((value) => value > 0);
4 | scalarHoles.findIndex((value) => value > 0);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 | scalarHoles.findLast((value) => value > 0);
sparse-get-scalar-fences.ts:5:1 - error SC1090: Array.findLast on a sparse 'number[]' value (a hole must be observed as undefined, which 'number' element slots cannot represent - use an undefined-capable element type or a dense array) is not supported yet
4 | scalarHoles.findIndex((value) => value > 0);
5 | scalarHoles.findLast((value) => value > 0);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6 | scalarHoles.findLastIndex((value) => value > 0);
sparse-get-scalar-fences.ts:6:1 - error SC1090: Array.findLastIndex on a sparse 'number[]' value (a hole must be observed as undefined, which 'number' element slots cannot represent - use an undefined-capable element type or a dense array) is not supported yet
5 | scalarHoles.findLast((value) => value > 0);
6 | scalarHoles.findLastIndex((value) => value > 0);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7 | // Iteration and spread have the same read-through-holes behavior.
sparse-get-scalar-fences.ts:8:21 - error SC1090: array for-of iteration on a sparse 'number[]' value (a hole must be observed as undefined, which 'number' element slots cannot represent - use an undefined-capable element type or a dense array) is not supported yet
7 | // Iteration and spread have the same read-through-holes behavior.
8 | for (const value of scalarHoles) console.log(value);
| ^~~~~~~~~~~
9 | const copied = [...scalarHoles];
sparse-get-scalar-fences.ts:9:17 - error SC1090: array spread on a sparse 'number[]' value (a hole must be observed as undefined, which 'number' element slots cannot represent - use an undefined-capable element type or a dense array) is not supported yet
8 | for (const value of scalarHoles) console.log(value);
9 | const copied = [...scalarHoles];
| ^~~~~~~~~~~~~~
10 | console.log(copied.length);
sparse-get-scalar-fences.ts:15:9 - error SC1090: array argument spread on a sparse 'number[]' value (a hole must be observed as undefined, which 'number' element slots cannot represent - use an undefined-capable element type or a dense array) is not supported yet
14 | }
15 | takeAll(...scalarHoles);
| ^~~~~~~~~~~~~~
16 | Math.max(...scalarHoles);
sparse-get-scalar-fences.ts:16:10 - error SC1090: array spread into Math.max on a sparse 'number[]' value (a hole must be observed as undefined, which 'number' element slots cannot represent - use an undefined-capable element type or a dense array) is not supported yet
15 | takeAll(...scalarHoles);
16 | Math.max(...scalarHoles);
| ^~~~~~~~~~~~~~
17 | String.fromCharCode(...scalarHoles);
sparse-get-scalar-fences.ts:17:21 - error SC1090: array argument spread on a sparse 'number[]' value (a hole must be observed as undefined, which 'number' element slots cannot represent - use an undefined-capable element type or a dense array) is not supported yet
16 | Math.max(...scalarHoles);
17 | String.fromCharCode(...scalarHoles);
| ^~~~~~~~~~~~~~
18 | // Copying sort reads through holes and produces present undefined entries.
sparse-get-scalar-fences.ts:19:1 - error SC1090: Array.toSorted on a sparse 'number[]' value (a hole must be observed as undefined, which 'number' element slots cannot represent - use an undefined-capable element type or a dense array) is not supported yet
18 | // Copying sort reads through holes and produces present undefined entries.
19 | scalarHoles.toSorted((a, b) => a - b);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
20 | // A later write cannot make this earlier Get safe.
sparse-get-scalar-fences.ts:22:1 - error SC1090: Array.find on a sparse 'number[]' value (a hole must be observed as undefined, which 'number' element slots cannot represent - use an undefined-capable element type or a dense array) is not supported yet
21 | const writtenLater = new Array<number>(2);
22 | writtenLater.find((value) => value > 0);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
23 | writtenLater[0] = 1;
+1 -1
View File
@@ -129,7 +129,7 @@ describe(`island web globals (scriptc-only${sanitize ? ", sanitized" : ""})`, ()
"DataCloneError:t:true",
'TypeError/ERR_MISSING_ARGS: The "The value argument must be specified" argument must be specified',
"TypeError/ERR_INVALID_ARG_TYPE: Failed to execute 'structuredClone': Options cannot be converted to a dictionary",
"TypeError/ERR_INVALID_ARG_TYPE: Failed to execute 'structuredClone': transfer in Options can not be converted to sequence.",
"TypeError/ERR_INVALID_ARG_TYPE: Failed to execute 'structuredClone': transfer in Options cannot be converted to sequence.",
"true",
"DataCloneError/25: () => {} could not be cloned.",
].join(" | "),