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
4 changes: 4 additions & 0 deletions packages/compiler/src/backend/c/exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1706,6 +1706,10 @@ function emitContainerExpr(
switch (method) {
case "length":
return emitter.newTemp(e.type, `scr_arr_len(${r.name})`);
case "getNumber": {
const index = emitter.emitExpr(e.args[0]!);
return emitter.newTemp(e.type, `scr_arr_get_number(${r.name}, ${index.name})`);
}
case "nextPresent": {
const start = emitter.emitExpr(e.args[0]!);
return emitter.newTemp(e.type, `scr_arr_next_present(${r.name}, ${start.name})`);
Expand Down
7 changes: 7 additions & 0 deletions packages/compiler/src/backend/llvm/expr-containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,13 @@ export function emitArrIntrinsic(host: LlvmEmitterContext, e: IrExpr & { kind: "
B.line(`${t} = call double @scr_arr_len(ptr ${r.name})`);
return { name: t, type: e.type };
}
case "getNumber": {
const index = host.emitExpr(e.args[0]!);
host.declare(`declare double @scr_arr_get_number(ptr, double)`);
const t = B.tmp();
B.line(`${t} = call double @scr_arr_get_number(ptr ${r.name}, double ${index.name})`);
return { name: t, type: e.type };
}
case "nextPresent": {
const start = host.emitExpr(e.args[0]!);
host.declare(`declare double @scr_arr_next_present(ptr, double)`);
Expand Down
33 changes: 9 additions & 24 deletions packages/compiler/src/frontend/lowering/lower-containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1428,24 +1428,20 @@ function arrayUnionHofHelper(
}

/** 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. */
* holes, missing properties, and present undefined yield undefined. */
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;
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)}`;
const resultT = arrayValueType(lowerer, elem);
const key = `idxOr:${typeKey(elem)}`;
let name = lowerer.arrHofHelpers.get(key);
if (!name) {
name = `%arr.${numeric ? "idxNumber" : "idxOr"}.${lowerer.arrHofHelpers.size}`;
name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
const arrT = arr.type;
lowerer.liftedFns.push({
Expand All @@ -1459,21 +1455,7 @@ function arrayUnionHofHelper(
{ id: "a.0", name: "a", type: arrT, mutable: false },
{ id: "i.0", name: "i", type: F64, mutable: false },
],
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,
}],
body: [{ kind: "return", value: arrayValueRead(lowerer, varRef("a.0", arrT, loc), varRef("i.0", F64, loc), elem, loc), loc }],
loc,
});
}
Expand All @@ -1488,7 +1470,10 @@ export function tryLowerNumericIndexRead(lowerer: Lowerer, operand: IrExpr, loc:
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);
// The intrinsic owns the evaluated receiver through index evaluation,
// then borrows it for one slot lookup. No extra helper parameter or
// separate state/getter expressions need to retain the array again.
return { kind: "arrIntrinsic", method: "getNumber", receiver: arr, args: [index], type: F64, loc };
}

/** Backward-compatible name for the npm-static probe path. */
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/ir/ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,10 @@ export type IrStmt =
* the fresh array. */
export type IrArrIntrinsicMethod =
| "length"
/** Internal ToNumber(a[index]) for f64-backed arrays: one numeric index,
* returning the stored number or NaN for a hole/undefined/missing key.
* Borrows the receiver and never traps on missing values. */
| "getNumber"
| "push"
| "pushSpread"
| "concatSpread"
Expand Down
34 changes: 34 additions & 0 deletions packages/compiler/src/ir/validate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect, test } from "vitest";
import { BOOL, F64, STRING, VOID, arrayOf, type IrExpr, type IrModule } from "./ir.js";
import { deserializeModule, serializeModule } from "./serialize.js";
import { validateModule } from "./validate.js";

const loc = { file: "numeric-read.ts", start: 0, end: 0 };

function numericReadModule(overrides: Partial<IrExpr & { kind: "arrIntrinsic" }> = {}): IrModule {
const read: IrExpr = {
kind: "arrIntrinsic", method: "getNumber",
receiver: { kind: "arrayLit", elems: [], type: arrayOf(F64), loc },
args: [{ kind: "numLit", value: 0, type: F64, loc }],
type: F64, loc, ...overrides,
};
return {
irVersion: 11, sourceFile: loc.file, entry: "main",
functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [{ kind: "exprStmt", expr: read, loc }], loc }],
};
}

test("numeric array-read intrinsic validates and round-trips", () => {
const mod = numericReadModule();
expect(validateModule(mod)).toEqual([]);
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
});

test.each([
[{ receiver: { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc } }, "requires f64 elements"],
[{ args: [] }, "0 args, expected 1"],
[{ args: [{ kind: "strLit", value: "0", type: STRING, loc }] }, "arg 0: expected f64"],
[{ type: BOOL }, "must be f64"],
] satisfies [Partial<IrExpr & { kind: "arrIntrinsic" }>, string][])("numeric array-read intrinsic rejects malformed IR %#", (overrides, message) => {
expect(validateModule(numericReadModule(overrides)).some((error) => error.message.includes(message))).toBe(true);
});
5 changes: 4 additions & 1 deletion packages/compiler/src/ir/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2719,7 +2719,7 @@ function validateFunction(
? { argTypes: e.args.map(() => elem), result: F64 }
: e.method === "pushSpread" || e.method === "concatSpread" || e.method === "unshiftSpread"
? { argTypes: [e.receiver.type], result: F64 }
: e.method === "nextPresent"
: e.method === "nextPresent" || e.method === "getNumber"
? { argTypes: [F64], result: F64 }
: e.method === "pop"
? { argTypes: [], result: e.type } // union-checked below
Expand All @@ -2746,6 +2746,9 @@ function validateFunction(
: e.method === "shift"
? { argTypes: [], result: e.type } // union-checked below
: { argTypes: [], result: F64 }; // length
if (e.method === "getNumber" && elem.kind !== "f64") {
err(`arrIntrinsic getNumber requires f64 elements, got ${elem.kind}`, e.loc);
}
if (
e.method === "join" &&
elem.kind !== "f64" && elem.kind !== "string" && elem.kind !== "bool" &&
Expand Down
19 changes: 15 additions & 4 deletions packages/compiler/test/numeric-array-reads.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule, validateModule } from "../src/index.js";
import { emitCModule } from "../src/backend/c/c-emitter.js";
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";

test("ordinary numeric-array arithmetic has no reachable union allocation", async () => {
test("ordinary numeric-array arithmetic uses one lookup per read without boxing", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-numeric-array-reads-"));
try {
const entry = join(dir, "main.ts");
Expand All @@ -25,9 +27,11 @@ test("ordinary numeric-array arithmetic has no reachable union allocation", asyn
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 };
const node = value as { kind?: string; callee?: string; method?: string };
expect(node.kind).not.toBe("unionWrap");
if (node.kind === "arrayGet") arrayReads++;
expect(node.kind).not.toBe("arrayState");
expect(node.kind).not.toBe("arrayGet");
if (node.kind === "arrIntrinsic" && node.method === "getNumber") arrayReads++;
if (node.kind === "call" && node.callee) visitFunction(node.callee);
Object.values(value).forEach(visit);
}
Expand All @@ -39,7 +43,14 @@ test("ordinary numeric-array arithmetic has no reachable union allocation", asyn
visit(fn!.body);
}
visitFunction(mod.entry);
expect(arrayReads).toBeGreaterThan(0);
expect(arrayReads).toBe(3);
const reachable = { ...mod, functions: mod.functions.filter((fn) => visited.has(fn.name)) };
const c = emitCModule(reachable);
const llvm = emitLlvmModule(reachable);
expect(c.match(/scr_arr_get_number\(/g)).toHaveLength(3);
expect(llvm.match(/call double @scr_arr_get_number\(/g)).toHaveLength(3);
expect(c).not.toContain("scr_arr_state(");
expect(llvm).not.toContain("@scr_arr_state(");
} finally {
await rm(dir, { recursive: true, force: true });
}
Expand Down
13 changes: 13 additions & 0 deletions packages/runtime/src/scr_array.c
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,19 @@ double scr_arr_get_f64(ScrArr *a, double i) {
return scr_slot_to_f64(scr_arr_require_slot(a, i));
}

double scr_arr_get_number(const ScrArr *a, double i) {
size_t idx;
uint64_t slot;
uint8_t state;
if (scr_arr_valid_index(i, &idx)) {
if (idx >= a->len) return NAN;
state = scr_arr_state_at(a, idx, &slot);
} else if (!scr_arr_prop_get_state(a, i, &slot, &state)) {
return NAN;
}
return state == SCR_ARR_VALUE ? scr_slot_to_f64(slot) : NAN;
}

bool scr_arr_get_bool(ScrArr *a, double i) {
return scr_arr_require_slot(a, i) != 0;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/runtime/src/scr_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,8 @@ double scr_math_max(double a, double b);
double scr_math_random(void);

double scr_arr_get_f64(ScrArr *a, double i); /* trap missing/hole */
/* ToNumber(a[i]) for f64 storage: borrows a; missing/hole/undefined -> NaN. */
double scr_arr_get_number(const ScrArr *a, double i);
bool scr_arr_get_bool(ScrArr *a, double i); /* trap missing/hole */
void *scr_arr_get_ref(ScrArr *a, double i); /* trap missing/hole; +1 */

Expand Down
33 changes: 33 additions & 0 deletions packages/runtime/test/test_array.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,38 @@ static void test_f64_basics(void) {
scr_arr_release(a);
}

static void test_numeric_read(void) {
ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0);
check(isnan(scr_arr_get_number(a, 0)), "numeric read of empty array");
scr_arr_set_f64(a, 0, -0.0);
check(signbit(scr_arr_get_number(a, -0.0)), "numeric read preserves signed zero");
scr_arr_set_f64(a, 3, INFINITY);
check(isnan(scr_arr_get_number(a, 1)), "numeric read of dense hole");
check_f64(scr_arr_get_number(a, 3), INFINITY, "numeric read of infinity");
scr_arr_set_undefined(a, 2);
check(isnan(scr_arr_get_number(a, 2)) && scr_arr_has(a, 2),
"numeric read of present undefined keeps presence");
scr_arr_set_f64(a, 4294967294.0, -INFINITY);
check_f64(scr_arr_get_number(a, 4294967294.0), -INFINITY,
"numeric read of last sparse array index");
check(isnan(scr_arr_get_number(a, 4294967293.0)), "numeric read of sparse hole");
scr_arr_set_undefined(a, 4294967294.0);
check(isnan(scr_arr_get_number(a, 4294967294.0)), "numeric read of sparse undefined");
double keys[] = {-1, 0.5, 4294967295.0, NAN, INFINITY, -INFINITY};
for (size_t i = 0; i < sizeof keys / sizeof *keys; i++) {
check(isnan(scr_arr_get_number(a, keys[i])), "numeric read of missing property");
scr_arr_set_f64(a, keys[i], (double)i + 10);
check_f64(scr_arr_get_number(a, keys[i]), (double)i + 10,
"numeric read of ordinary numeric property");
scr_arr_set_undefined(a, keys[i]);
check(isnan(scr_arr_get_number(a, keys[i])) && scr_arr_has(a, keys[i]),
"numeric read of undefined numeric property");
}
scr_arr_set_f64(a, 0, NAN);
check(isnan(scr_arr_get_number(a, 0)), "numeric read of stored NaN");
scr_arr_release(a);
}

static void test_bool(void) {
ScrArr *a = scr_arr_new(SCR_ELEM_BOOL, 2);
scr_arr_push_bool(a, true);
Expand Down Expand Up @@ -592,6 +624,7 @@ int main(int argc, char **argv) {
}

test_f64_basics();
test_numeric_read();
test_bool();
test_unshift_reverse();
test_str_rc();
Expand Down
34 changes: 34 additions & 0 deletions tests/corpus/2967-numeric-array-read-lifetime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Retain the evaluated receiver until a side-effecting index finishes,
// including when that index replaces the array's only other reference.
let current: number[] = [12, -0];
let calls = 0;
function replace(): number {
calls++;
current = [90];
return 0;
}
console.log("replace", current[replace()] + 1, current[0], calls);

function nested(): number {
const before = current[replace()] * 2;
current = [30];
return before > 0 ? 0 : 1;
}
console.log("nested", current[nested()] - 1, current[0], calls);

let trace = "";
function temporary(): number[] { trace += "R"; return [7]; }
function fail(): number { trace += "I"; throw new Error("index"); }
function rhs(): number { trace += "V"; return 1; }
try { console.log(temporary()[fail()] + rhs()); } catch { console.log("throw", trace); }

// Cover the array-index boundary without allocating dense 2^32 storage.
const sparse: number[] = [-0];
sparse[4294967294] = Infinity;
sparse[4294967295] = -Infinity;
console.log("boundary", sparse.length, sparse[4294967294] + 1, sparse[4294967295] - 1, sparse[4294967293] * 2);
console.log("zero-index", 1 / (sparse[-0] * 1));
sparse[4294967294] = sparse[2];
console.log("undefined", sparse[4294967294] + 1, 4294967294 in sparse);
sparse.length = 1;
console.log("truncate", sparse[4294967294] + 1, sparse[4294967295] - 1);
Loading