Borrow typed-array pages and literal-ending getter returns (#2421)

This commit is contained in:
Chris Tate
2026-10-27 10:48:04 -05:00
committed by GitHub
7 changed files with 144 additions and 11 deletions
@@ -130,3 +130,19 @@ test("function arms are nullable pointers compared by identity root", () => {
expect(isPointerIdentityArm(single)).toBe(false);
expect(unions.armTypes().get("fu")).toEqual(fn);
});
test("typed array arms are nullable pointers compared by identity", () => {
const i32: IrType = { kind: "bytes", elem: "i32" };
const unions = new NullableUnions(
module([
{ id: "iu", arms: [i32, UNDEFINED_T] },
{ id: "nin", arms: [NULL_T, i32, UNDEFINED_T] },
{ id: "if", arms: [i32, F64] },
]),
);
expect(unions.single("iu")).toMatchObject({ refTag: 0, unitTag: 1, nullTag: -1, arm: i32 });
expect(unions.single("nin")).toMatchObject({ refTag: 1, unitTag: 2, nullTag: 0, arm: i32 });
expect(unions.get("if")).toBeNull();
expect(isObjectArm(unions.single("iu")!)).toBe(true);
expect(isPointerIdentityArm(unions.single("iu")!)).toBe(true);
});
@@ -88,17 +88,17 @@ export const NULLABLE_NULL = "@sc_nullable_null";
export const NULLABLE_ABSENT = "@sc_nullable_absent";
/** The reference arms whose instances are never NULL. Class and record
* entry points all tolerate NULL; strings, Stats snapshots, arrays, maps and
* sets retain through a NULL-skipping wrapper (shapes.ts), as do closures
* (function arms). Object arms are always truthy and compare by identity
* (function arms by identity root); string arms are truthy when non-empty
* and compare bytes. */
* entry points all tolerate NULL; strings, Stats snapshots, arrays, typed
* arrays, maps and sets retain through a NULL-skipping wrapper (shapes.ts),
* as do closures (function arms). Object arms are always truthy and compare
* by identity (function arms by identity root); string arms are truthy when
* non-empty and compare bytes. */
export type NullableRefArm =
| { kind: "object"; className: string }
| { kind: "record"; shapeId: string }
| { kind: "string" }
| { kind: "stats" }
| (IrType & { kind: "array" | "map" | "set" | "func" });
| (IrType & { kind: "array" | "bytes" | "map" | "set" | "func" });
/** The arm is a JS object: always truthy. */
export function isObjectArm(nullable: PointerUnion): boolean {
@@ -273,7 +273,13 @@ function nullableShape(
return { ...base, arm: { kind: "record", shapeId: arm.shapeId } };
if (arm.kind === "string") return { ...base, arm: { kind: "string" } };
if (arm.kind === "stats") return { ...base, arm: { kind: "stats" } };
if (arm.kind === "array" || arm.kind === "map" || arm.kind === "set" || arm.kind === "func")
if (
arm.kind === "array" ||
arm.kind === "bytes" ||
arm.kind === "map" ||
arm.kind === "set" ||
arm.kind === "func"
)
return { ...base, arm };
return null;
}
@@ -139,3 +139,24 @@ test("returns of walks, including calls to borrowed-return callees, return borro
);
expect(analyzeWalks(owned, borrowedHost).returnsWalk).toBe(true);
});
test("a string literal return keeps the body's other walks borrowed", () => {
const ret = (value: IrExpr | null): IrStmt => ({ kind: "return", value, loc });
const STRING: IrType = { kind: "string" };
const text: IrExpr = {
kind: "fieldGet",
obj: ref("node"),
className: "Node",
field: "text",
type: STRING,
loc,
};
const empty: IrExpr = { kind: "strLit", value: "", type: STRING, loc };
const getter = fn([ret(text), ret(empty)], []);
expect(analyzeWalks(getter, host).returnsWalk).toBe(true);
// The literal needs no unwritten location; the field read still does.
expect(analyzeWalks(getter, { ...host, unwritten: () => false }).returnsWalk).toBe(false);
expect(analyzeWalks(fn([ret(empty)], []), { ...host, unwritten: () => false }).returnsWalk).toBe(
true,
);
});
@@ -42,8 +42,9 @@ const WALK_KINDS: readonly IrNodeKind[] = [
export interface WalkFacts {
locals: ReadonlySet<string>;
/** Every return yields a walk (or a throw), so the body can return its
* pointer without a reference: no try statement, at least one return. */
/** Every return yields a walk, a throw or a string literal (an interned
* constant that nothing frees), so the body can return its pointer
* without a reference: no try statement, at least one return. */
returnsWalk: boolean;
}
@@ -176,6 +177,7 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts {
};
const isThrow = (e: IrExpr): boolean => e.kind === "libCall" && e.fn === "error.nodeThrow";
const walkOrThrow = (e: IrExpr): boolean => isThrow(e) || walk(e);
const constant = (e: IrExpr): boolean => isThrow(e) || e.kind === "strLit";
for (let changed = true; changed;) {
changed = false;
for (const id of candidates) {
@@ -199,8 +201,7 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts {
returns.every(
(value) =>
value !== null &&
walkOrThrow(value) &&
(isThrow(value) || host.unwritten?.(value, candidates) !== false),
(constant(value) || (walk(value) && host.unwritten?.(value, candidates) !== false)),
);
return { locals: result, returnsWalk };
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/typed-array-or-undefined.ts"
],
"diags": []
}
+82
View File
@@ -0,0 +1,82 @@
// Typed arrays in `T | undefined` slots: paged bit sets, optional fields,
// map values, comparisons, printing and JSON.
class PagedBits {
readonly pages: (Int32Array | undefined)[] = [];
has(id: number): boolean {
const page = this.pages[id >> 8];
return page !== undefined && (page[(id >> 5) & 7]! & (1 << (id & 31))) !== 0;
}
add(id: number): void {
const index = id >> 8;
let page = this.pages[index];
if (page === undefined) {
page = new Int32Array(8);
while (this.pages.length <= index) this.pages.push(undefined);
this.pages[index] = page;
}
page[(id >> 5) & 7]! |= 1 << (id & 31);
}
clearPage(index: number): void {
this.pages[index] = undefined;
}
}
const bits = new PagedBits();
for (let i = 0; i < 4000; i += 13) bits.add(i);
for (let i = 2000; i < 2600; i += 7) bits.add(i);
let count = 0;
for (let i = 0; i < 5000; i++) if (bits.has(i)) count++;
console.log("count", count, "pages", bits.pages.length);
console.log(
"present",
bits.pages.map((p) => (p === undefined ? "-" : p.length)).join(","),
);
bits.clearPage(3);
let after = 0;
for (let i = 0; i < 5000; i++) if (bits.has(i)) after++;
console.log("after clear", after, bits.pages[3] === undefined, bits.pages[99]);
// A page read past the end and from a hole.
const sparse: (Uint8Array | undefined)[] = [];
sparse[4] = Uint8Array.of(1, 2, 3);
console.log(sparse.length, sparse[0], sparse[4], sparse[9]);
console.log(sparse.findIndex((p) => p !== undefined), sparse.includes(undefined));
// Optional fields and parameters.
class Buffered {
data: Float64Array | undefined;
constructor(n: number) {
this.data = n > 0 ? new Float64Array(n).fill(0.5) : undefined;
}
total(): number {
const d = this.data;
if (!d) return -1;
let s = 0;
for (const x of d) s += x;
return s;
}
}
const buffers = [new Buffered(4), new Buffered(0), new Buffered(2)];
console.log(buffers.map((b) => b.total()));
console.log(buffers.map((b) => b.data?.length ?? "none"));
buffers[1]!.data = buffers[0]!.data;
console.log(buffers[1]!.data === buffers[0]!.data, buffers[2]!.data === buffers[0]!.data);
buffers[0]!.data![0] = 9;
console.log(buffers[1]!.total(), typeof buffers[1]!.data, typeof buffers[2]!.data?.[5]);
function pick(a: Int16Array | undefined, b: Int16Array | undefined): Int16Array | undefined {
return a ?? b;
}
const one = Int16Array.of(-1, 2);
console.log(pick(undefined, one), pick(one, undefined) === one, pick(undefined, undefined));
// Map values and printing.
const byName = new Map<string, Uint32Array | undefined>();
byName.set("a", Uint32Array.of(7, 8));
byName.set("b", undefined);
for (const [k, v] of byName) console.log(k, v, v ? v[1] : "absent");
console.log(byName.get("c"), byName.has("b"));
const row: (Int32Array | undefined)[] = [Int32Array.of(5), undefined];
console.log(row, JSON.stringify(row));
const copy = [...row, Int32Array.of(6)];
console.log(copy.filter((p) => p !== undefined).length, copy.indexOf(row[0]));
@@ -0,0 +1 @@
{ "plain": 2800 }