diff --git a/packages/compiler/src/backend/llvm/nullable-unions.test.ts b/packages/compiler/src/backend/llvm/nullable-unions.test.ts index f4e6a6866..ea0517b56 100644 --- a/packages/compiler/src/backend/llvm/nullable-unions.test.ts +++ b/packages/compiler/src/backend/llvm/nullable-unions.test.ts @@ -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); +}); diff --git a/packages/compiler/src/backend/llvm/nullable-unions.ts b/packages/compiler/src/backend/llvm/nullable-unions.ts index eedb922b3..587f3c18c 100644 --- a/packages/compiler/src/backend/llvm/nullable-unions.ts +++ b/packages/compiler/src/backend/llvm/nullable-unions.ts @@ -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; } diff --git a/packages/compiler/src/backend/llvm/walk-borrows.test.ts b/packages/compiler/src/backend/llvm/walk-borrows.test.ts index caa6be94f..4df5f88fe 100644 --- a/packages/compiler/src/backend/llvm/walk-borrows.test.ts +++ b/packages/compiler/src/backend/llvm/walk-borrows.test.ts @@ -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, + ); +}); diff --git a/packages/compiler/src/backend/llvm/walk-borrows.ts b/packages/compiler/src/backend/llvm/walk-borrows.ts index 86b8f3d21..573eb775f 100644 --- a/packages/compiler/src/backend/llvm/walk-borrows.ts +++ b/packages/compiler/src/backend/llvm/walk-borrows.ts @@ -42,8 +42,9 @@ const WALK_KINDS: readonly IrNodeKind[] = [ export interface WalkFacts { locals: ReadonlySet; - /** 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 }; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-or-undefined.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-or-undefined.ts.json new file mode 100644 index 000000000..48f4fcde4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-or-undefined.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/typed-array-or-undefined.ts" + ], + "diags": [] +} diff --git a/tests/corpus/typed-array-or-undefined.ts b/tests/corpus/typed-array-or-undefined.ts new file mode 100644 index 000000000..11bee503f --- /dev/null +++ b/tests/corpus/typed-array-or-undefined.ts @@ -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(); +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])); diff --git a/tests/harness/corpus-costs/typed-array-or-undefined.ts.json b/tests/harness/corpus-costs/typed-array-or-undefined.ts.json new file mode 100644 index 000000000..ec575788c --- /dev/null +++ b/tests/harness/corpus-costs/typed-array-or-undefined.ts.json @@ -0,0 +1 @@ +{ "plain": 2800 }