diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 64a535027..1d745b981 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -5385,8 +5385,8 @@ export class LlEmitter { } /** A reference held as one plain pointer that emitReadReceiver can read - * without owning: class instances, records, arrays, strings and - * nullable-pointer unions. Tagged union boxes, closures and dynamic + * without owning: class instances, records, arrays, strings, typed arrays + * and nullable-pointer unions. Tagged union boxes, closures and dynamic * values keep ordinary ownership. */ plainReference(type: IrType): boolean { switch (type.kind) { @@ -5394,6 +5394,7 @@ export class LlEmitter { case "record": case "array": case "string": + case "bytes": return true; case "union": return this.nullableUnions.has(type.unionId); diff --git a/packages/compiler/test/bytes-field-borrow-emission.test.ts b/packages/compiler/test/bytes-field-borrow-emission.test.ts new file mode 100644 index 000000000..168cd78ac --- /dev/null +++ b/packages/compiler/test/bytes-field-borrow-emission.test.ts @@ -0,0 +1,94 @@ +import { expect, test } from "vitest"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; +import { + F64, + VOID, + bytesOf, + type IrExpr, + type IrFunction, + type IrModule, + type IrStmt, +} from "../src/ir/ir.js"; +import { validateModule } from "../src/ir/validate.js"; + +const loc = { file: "bytes-field-borrow.ts", start: 0, end: 0 }; +const units = bytesOf("u16"); +const scanner = { kind: "object" as const, className: "Scanner" }; +const self = (): IrExpr => ({ kind: "varRef", localId: "this", type: scanner, loc }); +const chars = (): IrExpr => ({ + kind: "fieldGet", + obj: self(), + className: "Scanner", + field: "chars", + type: units, + loc, +}); +const local = (): IrExpr => ({ kind: "varRef", localId: "c", type: units, loc }); +const read = (index: number): IrExpr => ({ + kind: "bytesIntrinsic", + method: "get", + receiver: local(), + args: [{ kind: "numLit", value: index, type: F64, loc }], + type: F64, + loc, +}); + +/** `const c = this.chars; ...between; return c[0] + c[1]`. */ +function method(name: string, between: IrStmt[]): IrFunction { + return { + name, + params: [{ localId: "this", name: "this", type: scanner }], + returnType: F64, + locals: [ + { id: "this", name: "this", type: scanner, mutable: false }, + { id: "c", name: "c", type: units, mutable: false }, + ], + body: [ + { kind: "varDecl", localId: "c", init: chars(), loc }, + ...between, + { + kind: "return", + value: { kind: "bin", op: "+", left: read(0), right: read(1), type: F64 }, + loc, + }, + ], + loc, + } as IrFunction; +} + +test("a typed-array local read from a field the rest of its block never writes borrows it", () => { + const replace: IrStmt = { + kind: "fieldSet", + obj: self(), + className: "Scanner", + field: "chars", + value: { + kind: "bytesNew", + source: { kind: "numLit", value: 4, type: F64, loc }, + type: units, + loc, + }, + loc, + }; + const mod: IrModule = { + irVersion: 16, + sourceFile: loc.file, + entry: "main", + functions: [ + { name: "main", params: [], returnType: VOID, locals: [], body: [], loc }, + method("%Scanner.peek", []), + method("%Scanner.swap", [replace]), + ], + classes: [{ name: "Scanner", fields: [{ name: "chars", type: units }], methods: [], loc }], + }; + expect(validateModule(mod)).toEqual([]); + const llvm = emitLlvmModule(mod, { pointerBits: 64 }); + const body = (name: string): string => + new RegExp(`^define [^\\n]*@sc_bf__x25_Scanner_${name}\\b[^]*?^}`, "m").exec(llvm)?.[0] ?? ""; + expect(body("peek")).not.toBe(""); + // The field keeps the array alive while the borrowed local reads it. + expect(body("peek")).not.toContain("bytes_retain"); + expect(body("peek")).not.toContain("bytes_release"); + // Replacing the field could free the array the local holds: it keeps a reference. + expect(body("swap")).toContain("call ptr @scr_bytes_retain_v("); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5891-typed-array-field-borrows.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5891-typed-array-field-borrows.ts.json new file mode 100644 index 000000000..efd0044d0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5891-typed-array-field-borrows.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5891-typed-array-field-borrows.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5891-typed-array-field-borrows.ts b/tests/corpus/5891-typed-array-field-borrows.ts new file mode 100644 index 000000000..cdd315191 --- /dev/null +++ b/tests/corpus/5891-typed-array-field-borrows.ts @@ -0,0 +1,68 @@ +// Typed-array locals read from fields and parameters: reads while nothing +// replaces the field, the field replaced directly or by a callee while the +// local is in use, rebound parameters, views and detached-length reads. + +class Scanner { + chars: Uint16Array; + pos = 0; + constructor(text: string) { + this.chars = Scanner.units(text); + } + static units(text: string): Uint16Array { + const out = new Uint16Array(text.length + 2); + for (let i = 0; i < text.length; i++) out[i] = text.charCodeAt(i); + return out; + } + sum(): number { + const chars = this.chars; + let total = 0; + for (let i = 0; i < chars.length; i++) total += chars[i]!; + return total; + } + replaceThenRead(): number { + const chars = this.chars; + this.chars = Scanner.units("zz"); + return chars[0]! * 1000 + this.chars[0]!; + } + reset(text: string): void { + this.chars = Scanner.units(text); + } + readAcrossReset(): string { + const chars = this.chars; + const before = chars[0]!; + this.reset("q"); + return before + ":" + chars[1] + ":" + this.chars[0] + ":" + chars.length; + } +} + +function firstOf(units: Uint16Array, fallback: Uint16Array): number { + if (units.length === 0) units = fallback; + let n = 0; + while (n < units.length && units[n] !== 0) n++; + return n; +} + +function walk(a: Uint8Array): number { + let total = 0; + while (a.length > 1) { + total += a[0]!; + a = a.subarray(1); + } + return total + a.length; +} + +const s = new Scanner("hello"); +console.log(s.sum(), s.replaceThenRead(), s.sum()); +const t = new Scanner("abc"); +console.log(t.readAcrossReset(), t.sum()); +console.log(firstOf(new Uint16Array(0), Scanner.units("xyz")), firstOf(Scanner.units("ab"), new Uint16Array(1))); +console.log(walk(new Uint8Array([1, 2, 3, 4, 5])), walk(new Uint8Array(0))); + +const holders: Scanner[] = []; +for (let i = 0; i < 50; i++) holders.push(new Scanner("item" + i)); +let acc = 0; +for (const h of holders) { + acc += h.sum(); + acc += h.replaceThenRead(); +} +console.log(acc);