mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Fix sparse array semantic gaps
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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(" | "),
|
||||
|
||||
Reference in New Issue
Block a user