Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 35 additions & 13 deletions packages/compiler/src/frontend/lowering/lower-containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1427,28 +1427,25 @@ function arrayUnionHofHelper(
return { kind: "call", callee: helper, args: [snapshot, fnArg], type: arrayOf(outElem), loc };
}

/** An OOB-SAFE indexed read: `xs[i]`
* answers the interned `elem | undefined` union — the element when `i`
* is an integer in [0, len), JS's property-miss undefined otherwise —
* instead of the trap divergence 4 documents for program code.
* Package JS is inference-typed, guard-style code (`registeredArguments
* .slice(-1)[0]`, commander's last-element probe), and the trap would
* fire on working Node idioms; program files keep the documented trap
* (their annotations can prove bounds). Existing element unions retag
* into the canonical union with an added undefined arm. */
/** An ordinary indexed read: values retain their element type, while
* holes, missing properties, and present undefined yield undefined.
* Numeric consumption may instead return the scalar ToNumber result,
* avoiding a temporary union without changing the array's storage. */
export function lowerSafeIndexRead(
lowerer: Lowerer,
arr: IrExpr & { type: { kind: "array" } },
index: IrExpr,
loc: SrcLoc,
numeric = false,
): IrExpr | null {
const elem = arr.type.elem;
if (elem.kind === "void" || elem.kind === "dyn") return null;
const resultT = arrayValueType(lowerer, elem);
const key = `idxOr:${typeKey(elem)}`;
if (numeric && elem.kind !== "f64") throw new InternalCompilerError("numeric index read requires f64 storage");
const resultT = numeric ? F64 : arrayValueType(lowerer, elem);
const key = `${numeric ? "idxNumber" : "idxOr"}:${typeKey(elem)}`;
let name = lowerer.arrHofHelpers.get(key);
if (!name) {
name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`;
name = `%arr.${numeric ? "idxNumber" : "idxOr"}.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
const arrT = arr.type;
lowerer.liftedFns.push({
Expand All @@ -1462,13 +1459,38 @@ function arrayUnionHofHelper(
{ id: "a.0", name: "a", type: arrT, mutable: false },
{ id: "i.0", name: "i", type: F64, mutable: false },
],
body: [{ kind: "return", value: arrayValueRead(lowerer, varRef("a.0", arrT, loc), varRef("i.0", F64, loc), elem, loc), loc }],
body: [{
kind: "return",
value: numeric ? {
kind: "ternary",
cond: {
kind: "bin", op: "===",
left: { kind: "arrayState", arr: varRef("a.0", arrT, loc), index: varRef("i.0", F64, loc), type: F64, loc },
right: numLit(1, loc), type: BOOL, loc,
},
then: { kind: "arrayGet", arr: varRef("a.0", arrT, loc), index: varRef("i.0", F64, loc), type: F64, loc },
else_: { kind: "bin", op: "/", left: numLit(0, loc), right: numLit(0, loc), type: F64, loc },
type: F64, loc,
} : arrayValueRead(lowerer, varRef("a.0", arrT, loc), varRef("i.0", F64, loc), elem, loc),
loc,
}],
loc,
});
}
return { kind: "call", callee: name, args: [arr, index], type: resultT, loc };
}

/** Fuse only our own f64-array read helper with immediate ToNumber.
* Reuse its arguments verbatim so the receiver and index still evaluate
* exactly once, in order, before the read. Do not specialize user calls,
* union-element arrays, or optional values stored in locals. */
export function tryLowerNumericIndexRead(lowerer: Lowerer, operand: IrExpr, loc: SrcLoc): IrExpr | null {
if (operand.kind !== "call" || operand.callee !== lowerer.arrHofHelpers.get(`idxOr:${typeKey(F64)}`)) return null;
const [arr, index] = operand.args;
if (operand.args.length !== 2 || arr?.type.kind !== "array" || arr.type.elem.kind !== "f64" || index?.type.kind !== "f64") return null;
return lowerSafeIndexRead(lowerer, arr as IrExpr & { type: { kind: "array" } }, index, loc, true);
}

/** Backward-compatible name for the npm-static probe path. */
export const lowerNpmStaticSafeIndexRead = lowerSafeIndexRead;

Expand Down
6 changes: 5 additions & 1 deletion packages/compiler/src/frontend/lowering/lower-exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReaso
import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModulesArrayLit, builtinModuleFnOf, COMPOUND_ASSIGN_OPS, CompoundOp, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js";
import { UNSUPPORTED, blockedBindingUseDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
import { lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall } from "./lower-containers.js";
import { lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall, tryLowerNumericIndexRead } from "./lower-containers.js";
import { arrayValueRead, arrayValueStore } from "./array-values.js";
import { npmStaticPackageOfPath } from "../npm-static.js";
import { unsupportedModuleFeatureOf } from "../builtin-modules.js";
Expand Down Expand Up @@ -4066,6 +4066,10 @@ export function lowerOptionalNumber(
): IrExpr {
if (operand.type.kind !== "union" || lowerer.armTag(operand.type.unionId, UNDEFINED_T) < 0) return operand;
const directNumber = lowerer.stripUndefinedArm(operand.type).kind === "f64";
if (directNumber) {
const scalarRead = tryLowerNumericIndexRead(lowerer, operand, loc);
if (scalarRead) return scalarRead;
}
const checkerNumber = narrowedNode !== undefined && lowerer.mapTypeOf(lowerer.typeOf(narrowedNode))?.kind === "f64";
if (!directNumber && (!checkerNumber || lowerer.armTag(operand.type.unionId, F64) < 0)) return operand;
const undefTag = lowerer.armTag(operand.type.unionId, UNDEFINED_T);
Expand Down
46 changes: 46 additions & 0 deletions packages/compiler/test/numeric-array-reads.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule, validateModule } from "../src/index.js";

test("ordinary numeric-array arithmetic has no reachable union allocation", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-numeric-array-reads-"));
try {
const entry = join(dir, "main.ts");
const outPath = join(dir, "main.ir.json");
await writeFile(entry, [
"function blend(a: number[], i: number, j: number): number {",
" return a[i] + (a[j] - a[i]) * 0.5;",
"}",
"console.log(blend([1, 3], 0, 1));",
"",
].join("\n"));
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const mod = deserializeModule(await readFile(outPath, "utf8"));
expect(validateModule(mod)).toEqual([]);
const visited = new Set<string>();
let arrayReads = 0;
function visit(value: unknown): void {
if (value === null || typeof value !== "object") return;
if (Array.isArray(value)) { value.forEach(visit); return; }
const node = value as { kind?: string; callee?: string };
expect(node.kind).not.toBe("unionWrap");
if (node.kind === "arrayGet") arrayReads++;
if (node.kind === "call" && node.callee) visitFunction(node.callee);
Object.values(value).forEach(visit);
}
function visitFunction(name: string): void {
if (visited.has(name)) return;
visited.add(name);
const fn = mod.functions.find((f) => f.name === name);
expect(fn, `missing function ${name}`).toBeDefined();
visit(fn!.body);
}
visitFunction(mod.entry);
expect(arrayReads).toBeGreaterThan(0);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
58 changes: 58 additions & 0 deletions tests/corpus/2966-numeric-array-read-fusion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Immediate numeric reads avoid boxing but retain JavaScript array and
// evaluation-order semantics, including holes and non-index properties.
const values: number[] = [-0, 1.5, NaN, Infinity, -Infinity];
console.log("ieee", 1 / (values[0] * 1), values[1] + 2, values[2] * 3, values[3] - 1, values[4] / 2);
console.log("operators", values[1] ** 2, values[1] % 1, values[1] | 4, values[1] << 2);
console.log("missing", values[20] + 1, values[20] * 0, values[20] < 1, values[20] | 7);
console.log("observable", values[20] === undefined, values[20] === values[21], values[20] ?? 9);
console.log("concat", values[20] + "!", "value:" + values[1]);

values.length = 8;
values[6] = values[20];
console.log("states", values[5] + 1, values[6] + 1, 5 in values, 6 in values);
values[5] = 12;
values[1] = values[20];
console.log("mutated", values[5] - 2, values[1] * 2);
values.length = 1;
console.log("truncated", values[5] + 1);

values[-1] = 4;
values[0.5] = 5;
values[4294967295] = 6;
values[NaN] = 7;
values[Infinity] = 8;
console.log("properties", values[-1] + 1, values[0.5] * 2, values[4294967295] - 1, values[NaN] / 2, values[Infinity] + 1);
values[-1] = values[20];
console.log("property-missing", values[-1] * 2, values[-2] + 1, values[-Infinity] + 1);
const sparse: number[] = [];
sparse[1000000] = 9;
console.log("sparse", sparse[1000000] * 2, sparse[999999] + 1);

let trace = "";
const original = [10];
const replacement = [100];
let current = original;
function receiver(): number[] { trace += "R"; return current; }
function index(): number { trace += "I"; current = replacement; original[0] = 12; return 0; }
function rhs(): number { trace += "V"; original[0] = 40; return 3; }
const result = receiver()[index()] + rhs();
console.log("order", result, trace, original[0], current[0]);

trace = "";
function temporary(): number[] { trace += "T"; return [6]; }
function zero(): number { trace += "Z"; return 0; }
console.log("temporary", temporary()[zero()] * temporary()[zero()], trace);
trace = "";
console.log("short-circuit", false && temporary()[zero()] > 0, true || temporary()[zero()] > 0, trace);
function fail(): number { trace += "F"; throw new Error("index"); }
try { console.log(temporary()[fail()] + rhs()); } catch { console.log("throw", trace); }

// Wider unions, saved reads, and user functions keep their tagged value.
const saved = values[20];
values[20] = 4;
console.log("saved", saved === undefined, saved + 1, values[20] + 1);
const mixed: (number | string)[] = [3, "x"];
const first = mixed[0];
if (typeof first === "number") console.log("mixed", first + 1);
function userRead(a: number[], i: number): number { return a[i]; }
console.log("user-call", userRead(values, 0) * 1);
Loading