Borrow typed-array locals read from fields and parameters (#1481)

This commit is contained in:
Chris Tate
2026-10-20 00:12:50 -05:00
committed by GitHub
4 changed files with 171 additions and 2 deletions
@@ -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);
@@ -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(");
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5891-typed-array-field-borrows.ts"
],
"diags": []
}
@@ -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);