mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Keep non-escaping number records and objects in frame storage
This commit is contained in:
@@ -14,6 +14,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `word-graph` | cpu | string-keyed Map/Set, BFS |
|
||||
| `alloc-trees` | cpu | allocation-heavy trees, immutable object-spread updates |
|
||||
| `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` |
|
||||
| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates |
|
||||
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
|
||||
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
|
||||
| `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe |
|
||||
|
||||
@@ -70,6 +70,13 @@
|
||||
"0.5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "value-objects",
|
||||
"entry": "workloads/value-objects.ts",
|
||||
"args": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "validate-errors",
|
||||
"entry": "workloads/validate-errors.ts",
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// Small immutable value objects: Vec3 records built by helper functions,
|
||||
// a Complex class whose methods return new instances, and an immutable
|
||||
// particle update. Most temporaries never leave the expression that
|
||||
// creates them.
|
||||
interface Vec3 {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
function vec(x: number, y: number, z: number): Vec3 {
|
||||
return { x, y, z };
|
||||
}
|
||||
function add(a: Vec3, b: Vec3): Vec3 {
|
||||
return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
|
||||
}
|
||||
function sub(a: Vec3, b: Vec3): Vec3 {
|
||||
return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
|
||||
}
|
||||
function scale(a: Vec3, k: number): Vec3 {
|
||||
return { x: a.x * k, y: a.y * k, z: a.z * k };
|
||||
}
|
||||
function dot(a: Vec3, b: Vec3): number {
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z;
|
||||
}
|
||||
function cross(a: Vec3, b: Vec3): Vec3 {
|
||||
return { x: a.y * b.z - a.z * b.y, y: a.z * b.x - a.x * b.z, z: a.x * b.y - a.y * b.x };
|
||||
}
|
||||
function normalize(a: Vec3): Vec3 {
|
||||
const len = Math.sqrt(dot(a, a));
|
||||
return len === 0 ? a : scale(a, 1 / len);
|
||||
}
|
||||
|
||||
interface Sphere {
|
||||
center: Vec3;
|
||||
radius: number;
|
||||
shade: number;
|
||||
}
|
||||
function hit(origin: Vec3, dir: Vec3, sphere: Sphere): number {
|
||||
const oc = sub(origin, sphere.center);
|
||||
const b = dot(oc, dir);
|
||||
const c = dot(oc, oc) - sphere.radius * sphere.radius;
|
||||
const disc = b * b - c;
|
||||
if (disc < 0) return -1;
|
||||
const t = -b - Math.sqrt(disc);
|
||||
return t > 0 ? t : -1;
|
||||
}
|
||||
function trace(width: number, height: number, spheres: Sphere[]): number {
|
||||
const eye = vec(0, 0, -4);
|
||||
const light = normalize(vec(-1, 1, -1));
|
||||
let total = 0;
|
||||
for (let py = 0; py < height; py++) {
|
||||
for (let px = 0; px < width; px++) {
|
||||
const dir = normalize(vec((px - width / 2) / width, (py - height / 2) / height, 1));
|
||||
let nearest = Infinity;
|
||||
let shade = 0;
|
||||
for (const sphere of spheres) {
|
||||
const t = hit(eye, dir, sphere);
|
||||
if (t > 0 && t < nearest) {
|
||||
nearest = t;
|
||||
const point = add(eye, scale(dir, t));
|
||||
const normal = normalize(sub(point, sphere.center));
|
||||
shade = sphere.shade * Math.max(0, dot(normal, light));
|
||||
}
|
||||
}
|
||||
total += shade;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
class Complex {
|
||||
re: number;
|
||||
im: number;
|
||||
constructor(re: number, im: number) {
|
||||
this.re = re;
|
||||
this.im = im;
|
||||
}
|
||||
plus(o: Complex): Complex {
|
||||
return new Complex(this.re + o.re, this.im + o.im);
|
||||
}
|
||||
times(o: Complex): Complex {
|
||||
return new Complex(this.re * o.re - this.im * o.im, this.re * o.im + this.im * o.re);
|
||||
}
|
||||
norm(): number {
|
||||
return this.re * this.re + this.im * this.im;
|
||||
}
|
||||
}
|
||||
function mandelbrot(size: number, limit: number): number {
|
||||
let inside = 0;
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
const c = new Complex((x / size) * 3 - 2, (y / size) * 3 - 1.5);
|
||||
let z = new Complex(0, 0);
|
||||
let n = 0;
|
||||
while (n < limit && z.norm() <= 4) {
|
||||
z = z.times(z).plus(c);
|
||||
n++;
|
||||
}
|
||||
if (n === limit) inside++;
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
function particles(count: number, steps: number): number {
|
||||
const positions: Vec3[] = [];
|
||||
const velocities: Vec3[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
positions.push(vec(i % 7, (i * 3) % 11, (i * 5) % 13));
|
||||
velocities.push(vec(((i % 5) - 2) / 10, ((i % 3) - 1) / 10, 0.05));
|
||||
}
|
||||
const gravity = vec(0, -0.01, 0);
|
||||
for (let s = 0; s < steps; s++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const v = add(velocities[i]!, gravity);
|
||||
velocities[i] = v;
|
||||
positions[i] = add(positions[i]!, scale(v, 0.5));
|
||||
}
|
||||
}
|
||||
let sum = 0;
|
||||
for (const p of positions) sum += dot(p, cross(p, vec(1, 2, 3))) + p.y;
|
||||
return sum;
|
||||
}
|
||||
|
||||
const scaleArg = Number(process.argv[2] ?? "1");
|
||||
const spheres: Sphere[] = [];
|
||||
for (let i = 0; i < 8; i++)
|
||||
spheres.push({ center: vec((i % 4) - 1.5, Math.floor(i / 4) - 0.5, 2 + (i % 3)), radius: 0.45, shade: 1 + i / 8 });
|
||||
const image = trace(Math.floor(320 * scaleArg), Math.floor(240 * scaleArg), spheres);
|
||||
const fractal = mandelbrot(Math.floor(220 * scaleArg), 200);
|
||||
const motion = particles(Math.floor(2000 * scaleArg), 200);
|
||||
console.log("trace", image.toFixed(6), "mandelbrot", fractal, "particles", motion.toFixed(6));
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
type StringSplit,
|
||||
} from "./split-loops.js";
|
||||
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
|
||||
import { addRecordResultEntries } from "./record-results.js";
|
||||
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
|
||||
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
import { analyzeIntegerRanges, INT32_RANGE, type IntegerRanges } from "../../ir/integer-ranges.js";
|
||||
@@ -123,6 +124,13 @@ import {
|
||||
} from "../mangle.js";
|
||||
import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js";
|
||||
import { canStackUnion, emitStackUnion, findLocalStackUnions } from "./stack-unions.js";
|
||||
import {
|
||||
emitStackObject,
|
||||
findLocalStackObjects,
|
||||
findReassignedStackObjects,
|
||||
stackObjectStruct,
|
||||
type StackObjectSource,
|
||||
} from "./stack-objects.js";
|
||||
import {
|
||||
allocateLocalUnion,
|
||||
emitScalarOptional,
|
||||
@@ -408,7 +416,9 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {})
|
||||
// Keep source storage intact for debugger inspection in dev builds.
|
||||
return new LlEmitter(
|
||||
options.debugSources === undefined
|
||||
? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
|
||||
? addRecordResultEntries(
|
||||
scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction)),
|
||||
)
|
||||
: mod,
|
||||
options,
|
||||
).emit();
|
||||
@@ -422,7 +432,9 @@ export function emitLlvmModuleSource(
|
||||
const mod = unshareNodes(input);
|
||||
const parts = new LlEmitter(
|
||||
options.debugSources === undefined
|
||||
? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
|
||||
? addRecordResultEntries(
|
||||
scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction)),
|
||||
)
|
||||
: mod,
|
||||
options,
|
||||
).emitParts();
|
||||
@@ -476,6 +488,10 @@ export class LlEmitter {
|
||||
private scalarStringSlices = new Map<string, IrExpr & { kind: "strIntrinsic" }>();
|
||||
readonly stringSlices = new Map<string, StringSliceSnapshot>();
|
||||
private localStackUnions = new Map<string, IrExpr & { kind: "unionWrap" }>();
|
||||
private localStackObjects = new Map<string, StackObjectSource>();
|
||||
private reassignedStackObjects = new Set<string>();
|
||||
/** The binding slot of each reassigned stack-object local. */
|
||||
private stackObjectSlots = new Map<string, string>();
|
||||
private localUnionStorageProofs = new Map<string, LocalUnionStorageProof>();
|
||||
private localUnionStorage = new Map<string, LocalUnionStorage>();
|
||||
integerArrayBindings = new Set<string>();
|
||||
@@ -529,6 +545,8 @@ export class LlEmitter {
|
||||
return lines;
|
||||
}
|
||||
private readonly debug: LlvmDebugInfo | null;
|
||||
/** Projection-only scalar objects may live in frame storage (stack-objects.ts). */
|
||||
readonly stackObjects: boolean;
|
||||
private debugScope: string | null = null;
|
||||
readonly sizeType: "i32" | "i64";
|
||||
readonly ffiExtendNarrowIntegers: boolean;
|
||||
@@ -931,6 +949,7 @@ export class LlEmitter {
|
||||
options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
|
||||
this.wasi = options.wasi === true;
|
||||
this.stacklessGenerators = !this.wasi && mod.workers !== true && this.debug === null;
|
||||
this.stackObjects = this.debug === null;
|
||||
// An empty triple targets the host, as for executableTls below: the
|
||||
// Node-hosted CLI passes none, the native CLI passes its toolchain's, and
|
||||
// both must emit the same module for the same target.
|
||||
@@ -5509,6 +5528,13 @@ export class LlEmitter {
|
||||
this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map();
|
||||
this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
|
||||
this.localStackUnions = findLocalStackUnions(fn, this.callLifetimes, this.boxedUnionsById);
|
||||
this.localStackObjects = this.stackObjects
|
||||
? findLocalStackObjects(this, fn, this.callLifetimes)
|
||||
: new Map();
|
||||
this.reassignedStackObjects = this.stackObjects
|
||||
? findReassignedStackObjects(this, fn, this.callLifetimes)
|
||||
: new Set();
|
||||
this.stackObjectSlots = new Map();
|
||||
this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.unionSources);
|
||||
this.localUnionStorage = new Map();
|
||||
this.walkBorrows = this.walkBorrowsByFunction.get(fn.name) ?? new Set();
|
||||
@@ -6430,6 +6456,17 @@ export class LlEmitter {
|
||||
);
|
||||
break;
|
||||
}
|
||||
const stackObject = this.localStackObjects.get(s.localId);
|
||||
if (stackObject) {
|
||||
B.line(`store ptr ${emitStackObject(this, stackObject).name}, ptr ${b.slot}`);
|
||||
break;
|
||||
}
|
||||
if (s.init && this.reassignedStackObjects.has(s.localId)) {
|
||||
const slot = this.stackObjectSlot(s.localId, s.init.type);
|
||||
emitStackObject(this, s.init as StackObjectSource, slot);
|
||||
B.line(`store ptr ${slot}, ptr ${b.slot}`);
|
||||
break;
|
||||
}
|
||||
const localUnion = this.localStackUnions.get(s.localId);
|
||||
if (localUnion) {
|
||||
const union = emitStackUnion(this, localUnion);
|
||||
@@ -6577,6 +6614,15 @@ export class LlEmitter {
|
||||
B.line(`store ptr ${v.name}, ptr ${owner}`);
|
||||
break;
|
||||
}
|
||||
if (this.reassignedStackObjects.has(s.localId)) {
|
||||
// The new value may read the old one: build it apart, then copy.
|
||||
const struct = stackObjectStruct(s.value.type);
|
||||
const value = emitStackObject(this, s.value as StackObjectSource);
|
||||
const bits = B.tmp();
|
||||
B.line(`${bits} = load ${struct}, ptr ${value.name}`);
|
||||
B.line(`store ${struct} ${bits}, ptr ${this.stackObjectSlot(s.localId, s.value.type)}`);
|
||||
break;
|
||||
}
|
||||
const localStorage = this.localUnionStorage.get(s.localId);
|
||||
if (localStorage) {
|
||||
storeLocalUnion(this, localStorage, s.value);
|
||||
@@ -8558,6 +8604,17 @@ export class LlEmitter {
|
||||
return this.emitExpr(e);
|
||||
}
|
||||
|
||||
/** The frame slot a reassigned stack-object local keeps for its value. */
|
||||
private stackObjectSlot(localId: string, type: IrType): string {
|
||||
let slot = this.stackObjectSlots.get(localId);
|
||||
if (slot === undefined) {
|
||||
slot = this.B.slot();
|
||||
this.B.entryAllocas.push(`${slot} = alloca ${stackObjectStruct(type)}, align 8`);
|
||||
this.stackObjectSlots.set(localId, slot);
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
/** A private stack union source with an independently owned payload. */
|
||||
canStackReceiver(e: IrExpr): boolean {
|
||||
return this.isStackUnionSource(e);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type IrFunction,
|
||||
} from "../../ir/ir.js";
|
||||
import { INT32_ABI_SUFFIX, SCALAR_UNION_RESULT } from "./int32-abi.js";
|
||||
import { canStackObject, emitStackObject } from "./stack-objects.js";
|
||||
import { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js";
|
||||
import {
|
||||
mangleAsyncAdoptCall,
|
||||
@@ -62,6 +63,9 @@ export function emitCallExpr(
|
||||
/** A projection consumes a scalar union result (Int32Abi.union): answer
|
||||
* a private stack box instead of a heap box. */
|
||||
stackResult = false,
|
||||
/** The frame storage a result-slot entry (record-results.ts) writes:
|
||||
* passed after the call's own arguments. */
|
||||
resultSlot?: LlValue,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
switch (e.kind) {
|
||||
@@ -125,6 +129,7 @@ export function emitCallExpr(
|
||||
return emitScalarOptional(host, a);
|
||||
if (a.kind === "call" && host.scalarResultCall(a))
|
||||
return emitCallExpr(host, a, false, true);
|
||||
if (host.stackObjects && canStackObject(host, a)) return emitStackObject(host, a);
|
||||
if (host.nullableFieldGet(a))
|
||||
return { name: host.emitOwnedNullableStack(a).box, type: a.type };
|
||||
}
|
||||
@@ -132,6 +137,7 @@ export function emitCallExpr(
|
||||
}
|
||||
return host.emitExpr(a);
|
||||
});
|
||||
if (resultSlot) args.push(resultSlot);
|
||||
// A borrowed parameter's owned argument stays in the caller frame.
|
||||
// This also preserves snapshots when a later argument mutates its
|
||||
// source, and releases earlier arguments if evaluation throws.
|
||||
@@ -913,60 +919,7 @@ export function emitCallExpr(
|
||||
const o = B.tmp();
|
||||
B.line(`${o} = call ptr @${mangleClassNew(e.className)}()`);
|
||||
const out = host.own({ name: o, type: e.type });
|
||||
const ctorName = `%${e.className}.constructor`;
|
||||
const borrowed = host.callLifetimes.borrowed.get(ctorName);
|
||||
if (borrowed) {
|
||||
// The borrowed constructor body, like a direct call: this frame
|
||||
// owns the new object across the call, borrowed arguments stay
|
||||
// owned by a call frame, and projection-only parameters (stores
|
||||
// into nullable-pointer fields, tests) receive private stack boxes.
|
||||
const projected = host.callLifetimes.parameters.get(ctorName);
|
||||
const inputs = borrowableInputs(
|
||||
host,
|
||||
e.args,
|
||||
host.referenceEffects.functions.has(ctorName),
|
||||
host.referenceWrites?.constructWrites(e.className),
|
||||
);
|
||||
host.frames.push([]);
|
||||
const args = e.args.map((a, i) => {
|
||||
if (borrowed.has(i + 1)) {
|
||||
if (projected?.has(i + 1) && canStackUnion(a, host.boxedUnionsById))
|
||||
return emitStackUnion(host, a).value;
|
||||
if (projected?.has(i + 1) && host.nullableFieldGet(a))
|
||||
return { name: host.emitOwnedNullableStack(a).box, type: a.type };
|
||||
if (inputs[i]) return host.emitReadReceiver(a);
|
||||
}
|
||||
return host.emitExpr(a);
|
||||
});
|
||||
args.forEach((a, i) => {
|
||||
if (!borrowed.has(i + 1)) host.moveTemp(a);
|
||||
});
|
||||
const self = borrowed.has(0)
|
||||
? o
|
||||
: (() => {
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @${mangleClassRetain(e.className)}(ptr ${o})`);
|
||||
return r;
|
||||
})();
|
||||
const argList = [`ptr ${self}`, ...ctorArguments(host, ctorName, ctor, args, e.args)].join(
|
||||
", ",
|
||||
);
|
||||
B.line(
|
||||
`call void @${mangleBorrowedFunction(ctorName)}${host.int32Abis.has(ctorName) ? INT32_ABI_SUFFIX : ""}(${argList})`,
|
||||
);
|
||||
if (host.mayThrow.has(ctorName)) host.emitPendingCheck();
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
return out;
|
||||
}
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
for (const a of args) host.moveTemp(a);
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @${mangleClassRetain(e.className)}(ptr ${o})`);
|
||||
const argList = [`ptr ${r}`, ...ctorArguments(host, ctorName, ctor, args, e.args)].join(", ");
|
||||
B.line(
|
||||
`call void @${mangleFunction(ctorName)}${host.int32Abis.has(ctorName) ? INT32_ABI_SUFFIX : ""}(${argList})`,
|
||||
);
|
||||
if (host.mayThrow.has(`%${e.className}.constructor`)) host.emitPendingCheck();
|
||||
emitConstruction(host, e, o, ctor);
|
||||
return out;
|
||||
}
|
||||
case "classRef": {
|
||||
@@ -1239,3 +1192,69 @@ export function emitBorrowedResultCall(host: LlvmEmitterContext, e: ExprOf<"call
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
/** Run the constructor of `new` over the allocated object `o` (fields
|
||||
* zeroed, header stamped). The ctor owns and releases its `this` param like
|
||||
* any callee, so it receives a +1 distinct from the caller's reference. */
|
||||
export function emitConstruction(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<"new">,
|
||||
o: string,
|
||||
ctor: IrFunction,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const ctorName = `%${e.className}.constructor`;
|
||||
const borrowed = host.callLifetimes.borrowed.get(ctorName);
|
||||
if (borrowed) {
|
||||
// The borrowed constructor body, like a direct call: this frame
|
||||
// owns the new object across the call, borrowed arguments stay
|
||||
// owned by a call frame, and projection-only parameters (stores
|
||||
// into nullable-pointer fields, tests) receive private stack boxes.
|
||||
const projected = host.callLifetimes.parameters.get(ctorName);
|
||||
const inputs = borrowableInputs(
|
||||
host,
|
||||
e.args,
|
||||
host.referenceEffects.functions.has(ctorName),
|
||||
host.referenceWrites?.constructWrites(e.className),
|
||||
);
|
||||
host.frames.push([]);
|
||||
const args = e.args.map((a, i) => {
|
||||
if (borrowed.has(i + 1)) {
|
||||
if (projected?.has(i + 1) && canStackUnion(a, host.boxedUnionsById))
|
||||
return emitStackUnion(host, a).value;
|
||||
if (projected?.has(i + 1) && host.nullableFieldGet(a))
|
||||
return { name: host.emitOwnedNullableStack(a).box, type: a.type };
|
||||
if (inputs[i]) return host.emitReadReceiver(a);
|
||||
}
|
||||
return host.emitExpr(a);
|
||||
});
|
||||
args.forEach((a, i) => {
|
||||
if (!borrowed.has(i + 1)) host.moveTemp(a);
|
||||
});
|
||||
const self = borrowed.has(0)
|
||||
? o
|
||||
: (() => {
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @${mangleClassRetain(e.className)}(ptr ${o})`);
|
||||
return r;
|
||||
})();
|
||||
const argList = [`ptr ${self}`, ...ctorArguments(host, ctorName, ctor, args, e.args)].join(
|
||||
", ",
|
||||
);
|
||||
B.line(
|
||||
`call void @${mangleBorrowedFunction(ctorName)}${host.int32Abis.has(ctorName) ? INT32_ABI_SUFFIX : ""}(${argList})`,
|
||||
);
|
||||
if (host.mayThrow.has(ctorName)) host.emitPendingCheck();
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
return;
|
||||
}
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
for (const a of args) host.moveTemp(a);
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @${mangleClassRetain(e.className)}(ptr ${o})`);
|
||||
const argList = [`ptr ${r}`, ...ctorArguments(host, ctorName, ctor, args, e.args)].join(", ");
|
||||
B.line(
|
||||
`call void @${mangleFunction(ctorName)}${host.int32Abis.has(ctorName) ? INT32_ABI_SUFFIX : ""}(${argList})`,
|
||||
);
|
||||
if (host.mayThrow.has(`%${e.className}.constructor`)) host.emitPendingCheck();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { isRefCounted, type IrExpr } from "../../ir/ir.js";
|
||||
import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
|
||||
import { arrNewCall, elemAccess } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
@@ -968,24 +968,7 @@ export function emitRecordExpr(
|
||||
const rec = B.tmp();
|
||||
B.line(`${rec} = call ptr @${mangleRecordNew(shapeId)}()`);
|
||||
const out = host.own({ name: rec, type: e.type });
|
||||
for (const f of e.fields) {
|
||||
const v = host.emitExpr(f.value);
|
||||
if (f.overflow) {
|
||||
const lit = host.internLiteral(f.name);
|
||||
const acc = v.type.kind === "f64" ? "f64" : v.type.kind === "bool" ? "bool" : "ref";
|
||||
if (acc === "ref") host.moveTemp(v);
|
||||
const ovf = host.recordOvfPtr(rec, shapeId);
|
||||
const argTy = acc === "f64" ? "double" : acc === "bool" ? "i1 zeroext" : "ptr";
|
||||
host.declare(`declare void @scr_map_set_str_${acc}(ptr, ptr, ${argTy})`);
|
||||
B.line(
|
||||
`call void @scr_map_set_str_${acc}(ptr ${ovf}, ptr ${lit}, ${argTy === "i1 zeroext" ? "i1" : argTy} ${v.name})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (isRefCounted(v.type)) host.moveTemp(v);
|
||||
const { ptr, type } = host.recordFieldPtr(rec, shapeId, f.name);
|
||||
host.storeField(ptr, type, v.name);
|
||||
}
|
||||
emitRecordLitFields(host, e, rec);
|
||||
return out;
|
||||
}
|
||||
case "recordClone": {
|
||||
@@ -1054,3 +1037,34 @@ export function emitRecordExpr(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Write a record literal's fields into the fresh record `rec`, IN SOURCE
|
||||
* ORDER (JS evaluates property values in source order). Ownership of
|
||||
* refcounted values moves in; the struct is fresh, so there is never an
|
||||
* old value to release. */
|
||||
export function emitRecordLitFields(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "recordLit" },
|
||||
rec: string,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const shapeId = (e.type as IrType & { kind: "record" }).shapeId;
|
||||
for (const f of e.fields) {
|
||||
const v = host.emitExpr(f.value);
|
||||
if (f.overflow) {
|
||||
const lit = host.internLiteral(f.name);
|
||||
const acc = v.type.kind === "f64" ? "f64" : v.type.kind === "bool" ? "bool" : "ref";
|
||||
if (acc === "ref") host.moveTemp(v);
|
||||
const ovf = host.recordOvfPtr(rec, shapeId);
|
||||
const argTy = acc === "f64" ? "double" : acc === "bool" ? "i1 zeroext" : "ptr";
|
||||
host.declare(`declare void @scr_map_set_str_${acc}(ptr, ptr, ${argTy})`);
|
||||
B.line(
|
||||
`call void @scr_map_set_str_${acc}(ptr ${ovf}, ptr ${lit}, ${argTy === "i1 zeroext" ? "i1" : argTy} ${v.name})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (isRefCounted(v.type)) host.moveTemp(v);
|
||||
const { ptr, type } = host.recordFieldPtr(rec, shapeId, f.name);
|
||||
host.storeField(ptr, type, v.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import { constructorEscapePoint } from "../cycle-mutability.js";
|
||||
import {
|
||||
VOID,
|
||||
type IrClassDef,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrModule,
|
||||
type IrRecordShape,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyExpr, everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
|
||||
/** Prefix of a function's result-slot entry: `%into.<name>(...params, dst)`
|
||||
* writes the function's fresh result object into `dst` and returns nothing. */
|
||||
export const RESULT_ENTRY_PREFIX = "%into.";
|
||||
|
||||
export function resultEntryName(name: string): string {
|
||||
return `${RESULT_ENTRY_PREFIX}${name}`;
|
||||
}
|
||||
|
||||
function scalarFields(fields: readonly { type: IrType }[]): boolean {
|
||||
return fields.every((field) => field.type.kind === "f64" || field.type.kind === "bool");
|
||||
}
|
||||
|
||||
/** A plain record shape whose fields are numbers and booleans. */
|
||||
export function scalarRecordShape(shape: IrRecordShape | undefined): boolean {
|
||||
return shape !== undefined && !shape.indexValue && !shape.tuple && scalarFields(shape.fields);
|
||||
}
|
||||
|
||||
/** A literal that stores every field of its shape, none of them through
|
||||
* the overflow map. */
|
||||
export function completeRecordLiteral(
|
||||
e: IrExpr & { kind: "recordLit" },
|
||||
shape: IrRecordShape | undefined,
|
||||
): boolean {
|
||||
if (!shape || e.fields.some((field) => field.overflow)) return false;
|
||||
const names = new Set(e.fields.map((field) => field.name));
|
||||
return shape.fields.every((field) => names.has(field.name));
|
||||
}
|
||||
|
||||
/** A standalone class with scalar fields whose constructor never lets
|
||||
* `this` escape: its instance can be built in caller-provided storage. */
|
||||
export function scalarStandaloneClass(
|
||||
cls: IrClassDef | undefined,
|
||||
ctor: IrFunction | undefined,
|
||||
isBase: boolean,
|
||||
programLifetime: boolean,
|
||||
): boolean {
|
||||
if (
|
||||
!cls ||
|
||||
!ctor ||
|
||||
isBase ||
|
||||
programLifetime ||
|
||||
cls.runtime ||
|
||||
cls.base !== undefined ||
|
||||
cls.localCaptures !== undefined ||
|
||||
cls.tracksOwnFields ||
|
||||
(cls.symbolFields?.length ?? 0) > 0 ||
|
||||
ctor.async ||
|
||||
ctor.generator ||
|
||||
!scalarFields(cls.fields)
|
||||
)
|
||||
return false;
|
||||
const self = ctor.params[0]?.localId;
|
||||
return self !== undefined && constructorEscapePoint(ctor, self, null) === ctor.body.length;
|
||||
}
|
||||
|
||||
/** Whether the constructor's top-level statements store every field of
|
||||
* `this` before anything reads a field it has not stored yet. Storage for
|
||||
* such an instance needs no zeroing first. */
|
||||
export function constructorInitializesFields(cls: IrClassDef, ctor: IrFunction): boolean {
|
||||
const self = ctor.params[0]?.localId;
|
||||
if (self === undefined) return false;
|
||||
const stored = new Set<string>();
|
||||
const unstoredRead = {
|
||||
expr: (node: IrExpr): boolean =>
|
||||
!(
|
||||
node.kind === "fieldGet" &&
|
||||
node.obj.kind === "varRef" &&
|
||||
node.obj.localId === self &&
|
||||
!stored.has(node.field)
|
||||
),
|
||||
stmt: () => true,
|
||||
};
|
||||
for (const stmt of ctor.body) {
|
||||
if (stmt.kind === "fieldSet" && stmt.obj.kind === "varRef" && stmt.obj.localId === self) {
|
||||
if (!everyExpr(stmt.value, unstoredRead)) return false;
|
||||
stored.add(stmt.field);
|
||||
} else if (!everyStmtList([stmt], unstoredRead)) return false;
|
||||
}
|
||||
return cls.fields.every((field) => stored.has(field.name));
|
||||
}
|
||||
|
||||
/** Add a result-slot entry for every function whose returns all build a
|
||||
* fresh scalar record or standalone scalar object. The original function
|
||||
* stays as it is; the backend calls the entry only where the result is
|
||||
* proven to be consumed by projections, passing frame storage. */
|
||||
export function addRecordResultEntries(mod: IrModule): IrModule {
|
||||
const shapes = new Map((mod.records ?? []).map((shape) => [shape.id, shape]));
|
||||
const classes = new Map((mod.classes ?? []).map((cls) => [cls.name, cls]));
|
||||
const bases = new Set((mod.classes ?? []).flatMap((cls) => (cls.base ? [cls.base] : [])));
|
||||
const lifetime = new Set(mod.contracts?.programLifetime ?? []);
|
||||
const functions = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const stackClass = (name: string): boolean =>
|
||||
scalarStandaloneClass(
|
||||
classes.get(name),
|
||||
functions.get(`%${name}.constructor`),
|
||||
bases.has(name),
|
||||
lifetime.has(name),
|
||||
);
|
||||
const fillable = (value: IrExpr): boolean => {
|
||||
switch (value.kind) {
|
||||
case "recordLit":
|
||||
return completeRecordLiteral(
|
||||
value,
|
||||
shapes.get((value.type as IrType & { kind: "record" }).shapeId),
|
||||
);
|
||||
case "new":
|
||||
return stackClass(value.className);
|
||||
case "ternary":
|
||||
return fillable(value.then) && fillable(value.else_);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const fill = (value: IrExpr, dst: IrExpr): IrStmt[] => {
|
||||
switch (value.kind) {
|
||||
case "recordLit":
|
||||
return value.fields.map((field) => ({
|
||||
kind: "recordSet",
|
||||
obj: dst,
|
||||
shapeId: (value.type as IrType & { kind: "record" }).shapeId,
|
||||
field: field.name,
|
||||
value: field.value,
|
||||
loc: field.value.loc,
|
||||
}));
|
||||
case "new":
|
||||
return [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "call",
|
||||
callee: `%${value.className}.constructor`,
|
||||
args: [dst, ...value.args],
|
||||
type: VOID,
|
||||
loc: value.loc,
|
||||
},
|
||||
loc: value.loc,
|
||||
},
|
||||
];
|
||||
case "ternary":
|
||||
return [
|
||||
{
|
||||
kind: "if",
|
||||
cond: value.cond,
|
||||
then: fill(value.then, dst),
|
||||
else_: fill(value.else_, dst),
|
||||
loc: value.loc,
|
||||
},
|
||||
];
|
||||
default:
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
};
|
||||
const added: IrFunction[] = [];
|
||||
for (const fn of mod.functions) {
|
||||
if (fn.async || fn.generator || fn.captures !== undefined || fn.classCaptures !== undefined)
|
||||
continue;
|
||||
const ret = fn.returnType;
|
||||
if (
|
||||
!(ret.kind === "record" && scalarRecordShape(shapes.get(ret.shapeId))) &&
|
||||
!(ret.kind === "object" && stackClass(ret.className))
|
||||
)
|
||||
continue;
|
||||
let returns = 0;
|
||||
const eligible = everyStmtList(fn.body, {
|
||||
expr: (e) => e.kind !== "seqExpr",
|
||||
stmt: (s) => {
|
||||
if (s.kind !== "return") return s.kind !== "tryCatch";
|
||||
returns++;
|
||||
return s.value !== null && fillable(s.value);
|
||||
},
|
||||
});
|
||||
if (!eligible || returns === 0) continue;
|
||||
const dstId = "%into.dst";
|
||||
const dst: IrExpr = { kind: "varRef", localId: dstId, type: ret, loc: fn.loc };
|
||||
const body = transformStmtList(fn.body, {
|
||||
expr: (e) => e,
|
||||
stmt: (s) =>
|
||||
s.kind === "return" && s.value !== null
|
||||
? { kind: "block", body: [...fill(s.value, dst), { ...s, value: null }], loc: s.loc }
|
||||
: s,
|
||||
});
|
||||
const { sourceName: _sourceName, ownsPrototype: _ownsPrototype, ...rest } = fn;
|
||||
added.push({
|
||||
...rest,
|
||||
name: resultEntryName(fn.name),
|
||||
params: [...fn.params, { localId: dstId, name: "dst", type: ret }],
|
||||
locals: [...fn.locals, { id: dstId, name: "dst", type: ret, mutable: false }],
|
||||
returnType: VOID,
|
||||
body,
|
||||
});
|
||||
}
|
||||
return added.length === 0 ? mod : { ...mod, functions: [...mod.functions, ...added] };
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../../index.js";
|
||||
import {
|
||||
F64,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrModule,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { validateModule } from "../../ir/validate.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
import { addRecordResultEntries, RESULT_ENTRY_PREFIX } from "./record-results.js";
|
||||
|
||||
async function moduleFor(source: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-stack-objects-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
return module;
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The body of a source function: its borrowing or i32 entry when it has one. */
|
||||
function body(ir: string, name: string): string {
|
||||
return new RegExp(`^define internal [^\\n]*@sc_b?f_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(
|
||||
ir,
|
||||
)![0];
|
||||
}
|
||||
|
||||
const VECTORS = `
|
||||
interface Vec { x: number; y: number }
|
||||
function add(a: Vec, b: Vec): Vec { return { x: a.x + b.x, y: a.y + b.y }; }
|
||||
function dot(a: Vec, b: Vec): number { return a.x * b.x + a.y * b.y; }
|
||||
function shortest(a: Vec): Vec { return a.x < a.y ? { x: a.x, y: 0 } : { x: 0, y: a.y }; }
|
||||
const kept: Vec[] = [];
|
||||
export function projected(n: number): number {
|
||||
const p = { x: n, y: 1 };
|
||||
const q = add(p, { x: 2, y: n });
|
||||
return dot(q, p) + dot(shortest(q), q);
|
||||
}
|
||||
export function escaping(n: number): number {
|
||||
const p = { x: n, y: 1 };
|
||||
kept.push(p);
|
||||
const q = add(p, p);
|
||||
kept.push(q);
|
||||
return q.x;
|
||||
}
|
||||
export function reassigned(n: number): number {
|
||||
let p = { x: n, y: 0 };
|
||||
for (let i = 0; i < n; i++) p = add(p, { x: 1, y: i });
|
||||
return p.x + p.y;
|
||||
}
|
||||
console.log(projected(2), escaping(3), reassigned(4), kept.length);
|
||||
`;
|
||||
|
||||
test("projection-only records live in frame storage and producers fill it", async () => {
|
||||
const ir = emitLlvmModule(await moduleFor(VECTORS));
|
||||
const projected = body(ir, "projected");
|
||||
expect(projected).toMatch(/alloca %sc_rs_\w+, align 8/);
|
||||
expect(projected).toContain("store i64 -1, ptr");
|
||||
expect(projected).not.toMatch(/@sc_rnew_/);
|
||||
expect(projected).toMatch(/call void @sc_\w*into_add\(/);
|
||||
expect(projected).toMatch(/call void @sc_\w*into_shortest\(/);
|
||||
// A literal stores every field: its storage is not zeroed first.
|
||||
expect(projected).not.toContain("zeroinitializer");
|
||||
// Stored values keep the heap path and the ordinary producer.
|
||||
const escaping = body(ir, "escaping");
|
||||
expect(escaping).toMatch(/call ptr @sc_rnew_/);
|
||||
expect(escaping).toMatch(/call ptr @sc_\w*f_add\(/);
|
||||
expect(escaping).not.toMatch(/alloca %sc_rs_/);
|
||||
});
|
||||
|
||||
test("reassigned bindings build each value apart and copy it into their slot", async () => {
|
||||
const ir = emitLlvmModule(await moduleFor(VECTORS));
|
||||
const reassigned = body(ir, "reassigned");
|
||||
expect(reassigned).not.toMatch(/@sc_rnew_/);
|
||||
expect(reassigned).toMatch(/load %sc_rs_\w+, ptr %s\d+\n\s+store %sc_rs_\w+ %t\d+, ptr %s\d+/);
|
||||
});
|
||||
|
||||
test("result-slot entries cover producers whose every return is fresh", async () => {
|
||||
const mod = await moduleFor(`
|
||||
interface Vec { x: number; y: number }
|
||||
function fresh(n: number): Vec { if (n > 1) return { x: n, y: n }; return { x: 0, y: 1 }; }
|
||||
function passthrough(v: Vec): Vec { return v; }
|
||||
function guarded(n: number): Vec { try { return { x: n, y: 0 }; } finally { console.log(n); } }
|
||||
console.log(fresh(1).x, passthrough({ x: 1, y: 2 }).y, guarded(3).x);
|
||||
`);
|
||||
const entries = addRecordResultEntries(mod)
|
||||
.functions.map((fn) => fn.name)
|
||||
.filter((name) => name.startsWith(RESULT_ENTRY_PREFIX));
|
||||
expect(entries).toEqual([`${RESULT_ENTRY_PREFIX}fresh`]);
|
||||
});
|
||||
|
||||
test("objects stay on the heap when their constructor or a method lets them escape", async () => {
|
||||
const ir = emitLlvmModule(
|
||||
await moduleFor(`
|
||||
const seen: Tracked[] = [];
|
||||
class Tracked { id: number; constructor(id: number) { this.id = id; seen.push(this); } }
|
||||
class Plain {
|
||||
a: number; b: number;
|
||||
constructor(a: number, b: number) { this.a = a; this.b = b; }
|
||||
sum(): number { return this.a + this.b; }
|
||||
self(): Plain { return this; }
|
||||
}
|
||||
const selves: Plain[] = [];
|
||||
export function run(n: number): number {
|
||||
const t = new Tracked(n);
|
||||
const p = new Plain(n, 1);
|
||||
const s = new Plain(n, 2);
|
||||
selves.push(s.self());
|
||||
return t.id + p.sum() + s.sum();
|
||||
}
|
||||
console.log(run(1), seen.length, selves.length);
|
||||
`),
|
||||
);
|
||||
const run = body(ir, "run");
|
||||
expect(run.match(/alloca %sc_o_\w+, align 8/g)).toHaveLength(1);
|
||||
expect(run).toContain("call ptr @sc_new_Tracked()");
|
||||
expect(run).toContain("call ptr @sc_new_Plain()");
|
||||
});
|
||||
|
||||
test("loop bindings are never reassigned into a fixed slot", () => {
|
||||
const loc = { file: "loop.ts", start: 0, end: 0 };
|
||||
const point: IrType = { kind: "record", shapeId: "point" };
|
||||
const points: IrType = { kind: "array", elem: point };
|
||||
const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc });
|
||||
const x = (obj: IrExpr): IrExpr => ({
|
||||
kind: "recordGet",
|
||||
obj,
|
||||
shapeId: "point",
|
||||
field: "x",
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const work: IrFunction = {
|
||||
name: "work",
|
||||
loc,
|
||||
returnType: F64,
|
||||
params: [{ localId: "items", name: "items", type: points }],
|
||||
locals: [
|
||||
{ id: "items", name: "items", type: points, mutable: true },
|
||||
{ id: "total", name: "total", type: F64, mutable: true },
|
||||
{ id: "p", name: "p", type: point, mutable: true },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "total",
|
||||
init: { kind: "numLit", value: 0, type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "forOf",
|
||||
localId: "p",
|
||||
iterable: ref("items", points),
|
||||
body: [
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "p",
|
||||
value: {
|
||||
kind: "recordLit",
|
||||
fields: [
|
||||
{
|
||||
name: "x",
|
||||
value: {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: x(ref("p", point)),
|
||||
right: { kind: "numLit", value: 10, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
},
|
||||
],
|
||||
type: point,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "total",
|
||||
value: {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: ref("total", F64),
|
||||
right: x(ref("p", point)),
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: ref("total", F64), loc },
|
||||
],
|
||||
};
|
||||
const mod: IrModule = {
|
||||
irVersion: 16,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
records: [{ id: "point", fields: [{ name: "x", type: F64 }] }],
|
||||
functions: [{ name: "main", params: [], locals: [], body: [], returnType: VOID, loc }, work],
|
||||
};
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
const ir = body(emitLlvmModule(mod), "work");
|
||||
// The literal keeps its ordinary record; the binding is the element.
|
||||
expect(ir).toContain("call ptr @sc_rnew_point()");
|
||||
expect(ir).not.toMatch(/alloca %sc_rs_point/);
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { VOID, typeEquals, type IrExpr, type IrFunction, type IrType } from "../../ir/ir.js";
|
||||
import {
|
||||
completeRecordLiteral,
|
||||
constructorInitializesFields,
|
||||
resultEntryName,
|
||||
scalarRecordShape,
|
||||
scalarStandaloneClass,
|
||||
} from "./record-results.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { mangleClassStruct, mangleRecordStruct } from "../mangle.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import { emitCallExpr, emitConstruction } from "./expr-calls.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { emitRecordLitFields } from "./expr-primitives.js";
|
||||
|
||||
export type StackObjectSource = IrExpr & { kind: "recordLit" | "new" | "call" };
|
||||
|
||||
/** A record literal, `new` or result-slot producer call whose object can
|
||||
* live in the frame's own storage: a plain scalar record shape, or a
|
||||
* standalone scalar class whose constructor never lets `this` escape
|
||||
* (record-results.ts). The caller proves that every use of the value only
|
||||
* projects it. */
|
||||
export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackObjectSource {
|
||||
// A producer with a result-slot entry builds its fresh result in the
|
||||
// caller's storage.
|
||||
if (e.kind === "call") return host.fnByName.has(resultEntryName(e.callee));
|
||||
if (e.kind === "recordLit") {
|
||||
if (e.type.kind !== "record") return false;
|
||||
const shape = host.recordsById.get(e.type.shapeId);
|
||||
return scalarRecordShape(shape) && completeRecordLiteral(e, shape);
|
||||
}
|
||||
if (e.kind !== "new") return false;
|
||||
const meta = host.classMeta.get(e.className);
|
||||
return (
|
||||
meta !== undefined &&
|
||||
scalarStandaloneClass(
|
||||
meta.def,
|
||||
host.fnByName.get(`%${e.className}.constructor`),
|
||||
meta.hierarchy,
|
||||
host.programLifetime.has(e.className),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** Locals initialized once by a stack-eligible object and only projected
|
||||
* afterwards (CallLifetimes.locals): field reads, and arguments to
|
||||
* parameters that are themselves only projected. */
|
||||
export function findLocalStackObjects(
|
||||
host: LlvmEmitterContext,
|
||||
fn: IrFunction,
|
||||
lifetimes: CallLifetimes,
|
||||
): Map<string, StackObjectSource> {
|
||||
const result = new Map<string, StackObjectSource>();
|
||||
const safe = lifetimes.locals.get(fn.name);
|
||||
if (!safe?.size || fn.async || fn.generator) return result;
|
||||
everyStmtList(fn.body, {
|
||||
expr: () => true,
|
||||
stmt: (stmt) => {
|
||||
if (
|
||||
stmt.kind === "varDecl" &&
|
||||
safe.has(stmt.localId) &&
|
||||
stmt.init &&
|
||||
canStackObject(host, stmt.init)
|
||||
)
|
||||
result.set(stmt.localId, stmt.init);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Reassigned locals declared with an initializer, whose every write
|
||||
* builds a stack-eligible object of the binding's own type, and whose uses
|
||||
* only project the value
|
||||
* (CallLifetimes.projectedLocals). The binding keeps one slot; each
|
||||
* assignment builds its value in fresh storage, which may read the old
|
||||
* value, and then copies it into the binding's slot. */
|
||||
export function findReassignedStackObjects(
|
||||
host: LlvmEmitterContext,
|
||||
fn: IrFunction,
|
||||
lifetimes: CallLifetimes,
|
||||
): Set<string> {
|
||||
const result = new Set<string>();
|
||||
const projected = lifetimes.projectedLocals.get(fn.name);
|
||||
const stable = lifetimes.locals.get(fn.name);
|
||||
if (!projected?.size || fn.async || fn.generator) return result;
|
||||
const types = new Map(fn.locals.map((local) => [local.id, local.type]));
|
||||
for (const id of projected) {
|
||||
const type = types.get(id);
|
||||
if (!stable?.has(id) && (type?.kind === "record" || type?.kind === "object")) result.add(id);
|
||||
}
|
||||
// The binding must be declared by its own initializer: a loop that binds
|
||||
// it (`for...of`) would point it at each element, not at the slot.
|
||||
const initialized = new Set<string>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: () => true,
|
||||
stmt: (stmt) => {
|
||||
if (stmt.kind === "forOf") result.delete(stmt.localId);
|
||||
if (stmt.kind === "varDecl" && stmt.init) initialized.add(stmt.localId);
|
||||
if ((stmt.kind === "varDecl" || stmt.kind === "assign") && result.has(stmt.localId)) {
|
||||
const value = stmt.kind === "varDecl" ? stmt.init : stmt.value;
|
||||
if (
|
||||
!value ||
|
||||
!canStackObject(host, value) ||
|
||||
!typeEquals(value.type, types.get(stmt.localId)!)
|
||||
)
|
||||
result.delete(stmt.localId);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
for (const id of result) if (!initialized.has(id)) result.delete(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function stackObjectStruct(type: IrType): string {
|
||||
return type.kind === "record"
|
||||
? `%${mangleRecordStruct(type.shapeId)}`
|
||||
: `%${mangleClassStruct((type as IrType & { kind: "object" }).className)}`;
|
||||
}
|
||||
|
||||
/** Build the object in an entry-block slot (or `into`, an existing one).
|
||||
* The immortal reference count makes every retain and release a no-op;
|
||||
* the proof that the value is only projected keeps the pointer from
|
||||
* outliving the frame. */
|
||||
export function emitStackObject(
|
||||
host: LlvmEmitterContext,
|
||||
e: StackObjectSource,
|
||||
into?: string,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const type = e.type;
|
||||
const struct = stackObjectStruct(type);
|
||||
let slot = into;
|
||||
if (slot === undefined) {
|
||||
slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${struct}, align 8`);
|
||||
}
|
||||
// Literals store every field; constructors that store every field
|
||||
// before reading one need no zeroed storage either.
|
||||
if (type.kind === "object") {
|
||||
const cls = host.classMeta.get(type.className)!.def;
|
||||
const ctor = host.fnByName.get(`%${type.className}.constructor`)!;
|
||||
if (!constructorInitializesFields(cls, ctor))
|
||||
B.line(`store ${struct} zeroinitializer, ptr ${slot}`);
|
||||
}
|
||||
B.line(`store ${host.sizeType} -1, ptr ${slot}`);
|
||||
if (e.kind === "call") {
|
||||
// The entry writes the result into the slot it receives last.
|
||||
emitCallExpr(
|
||||
host,
|
||||
{ ...e, callee: resultEntryName(e.callee), type: VOID },
|
||||
false,
|
||||
false,
|
||||
host.markImmortal({ name: slot, type }),
|
||||
);
|
||||
} else if (e.kind === "recordLit") emitRecordLitFields(host, e, slot);
|
||||
else emitConstruction(host, e, slot, host.fnByName.get(`%${e.className}.constructor`)!);
|
||||
return { name: slot, type: e.type };
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5120-frame-records.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5121-frame-objects.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5122-frame-result-slots.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5123-frame-reassigned.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5124-frame-objects-control.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5127-frame-loop-bindings.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Number and boolean records that only flow into field reads keep their
|
||||
// values; records that are stored, returned, captured, compared by
|
||||
// identity or printed stay observable exactly like Node.
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
interface Flagged {
|
||||
on: boolean;
|
||||
weight: number;
|
||||
}
|
||||
function length2(p: Point): number {
|
||||
return p.x * p.x + p.y * p.y;
|
||||
}
|
||||
function dot(a: Point, b: Point): number {
|
||||
return a.x * b.x + a.y * b.y;
|
||||
}
|
||||
function weigh(f: Flagged): number {
|
||||
return f.on ? f.weight : -f.weight;
|
||||
}
|
||||
const stored: Point[] = [];
|
||||
function keep(p: Point): number {
|
||||
stored.push(p);
|
||||
return p.x;
|
||||
}
|
||||
function same(p: Point): Point {
|
||||
return p;
|
||||
}
|
||||
function depth(p: Point, n: number): number {
|
||||
return n === 0 ? length2(p) : depth({ x: p.x + 1, y: p.y - 1 }, n - 1) + p.x;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
const readers: (() => number)[] = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const p = { x: i, y: i * 2 };
|
||||
const q: Point = { x: 1, y: -1 };
|
||||
total += length2(p) + dot(p, q) + dot({ x: 3, y: 4 }, { x: i, y: 1 });
|
||||
total += weigh({ on: i % 2 === 0, weight: i });
|
||||
if (i % 3 === 0) total += keep(p);
|
||||
const r = { x: i, y: 0 };
|
||||
readers.push(() => r.x * 10);
|
||||
const s = same({ x: i, y: i });
|
||||
stored.push(s);
|
||||
const a = { x: 1, y: 2 };
|
||||
const b = { x: 1, y: 2 };
|
||||
if (a === b) total += 1000;
|
||||
total += depth({ x: i, y: 5 }, 4);
|
||||
}
|
||||
console.log(total);
|
||||
console.log(stored.map((p) => `${p.x}:${p.y}`).join(" "));
|
||||
console.log(readers.map((read) => read()).join(" "));
|
||||
const shown = { x: 7, y: 8 };
|
||||
console.log(shown, JSON.stringify({ x: 1, y: 2 }));
|
||||
@@ -0,0 +1,86 @@
|
||||
// Instances of small classes with number fields that are only read keep
|
||||
// their values; constructors that register `this`, methods returning
|
||||
// `this`, subclasses and throwing constructors behave as in Node.
|
||||
class Vec {
|
||||
x: number;
|
||||
y: number;
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
dot(o: Vec): number {
|
||||
return this.x * o.x + this.y * o.y;
|
||||
}
|
||||
self(): Vec {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
const registry: Tracked[] = [];
|
||||
class Tracked {
|
||||
id: number;
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
registry.push(this);
|
||||
}
|
||||
twice(): number {
|
||||
return this.id * 2;
|
||||
}
|
||||
}
|
||||
class Base {
|
||||
v: number;
|
||||
constructor(v: number) {
|
||||
this.v = v;
|
||||
}
|
||||
get(): number {
|
||||
return this.v;
|
||||
}
|
||||
}
|
||||
class Derived extends Base {
|
||||
get(): number {
|
||||
return this.v * 100;
|
||||
}
|
||||
}
|
||||
class Checked {
|
||||
n: number;
|
||||
half: number;
|
||||
constructor(n: number) {
|
||||
this.n = n;
|
||||
if (n < 0) throw new RangeError(`negative ${n}`);
|
||||
this.half = n / 2;
|
||||
}
|
||||
sum(): number {
|
||||
return this.n + this.half;
|
||||
}
|
||||
}
|
||||
class Defaults {
|
||||
a = 1;
|
||||
b = 2;
|
||||
c: number;
|
||||
constructor(c: number) {
|
||||
this.c = c + this.a + this.b;
|
||||
}
|
||||
total(): number {
|
||||
return this.a + this.b + this.c;
|
||||
}
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
const selves: Vec[] = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const v = new Vec(i, i + 1);
|
||||
total += v.dot(new Vec(2, 3)) + new Vec(i, i).dot(v);
|
||||
const t = new Tracked(i);
|
||||
total += t.twice();
|
||||
selves.push(new Vec(i, -i).self());
|
||||
const b: Base = i % 2 === 0 ? new Base(i) : new Derived(i);
|
||||
total += b.get();
|
||||
try {
|
||||
const c = new Checked(i - 2);
|
||||
total += c.sum();
|
||||
} catch (error) {
|
||||
console.log("caught", (error as Error).message);
|
||||
}
|
||||
total += new Defaults(i).total();
|
||||
}
|
||||
console.log(total);
|
||||
console.log(registry.map((t) => t.id).join(","), selves.map((v) => `${v.x}/${v.y}`).join(","));
|
||||
@@ -0,0 +1,57 @@
|
||||
// Functions that return fresh number records or objects on every path,
|
||||
// including early returns in loops and conditional results, give the same
|
||||
// values whether their result is only read or kept.
|
||||
interface Pair {
|
||||
a: number;
|
||||
b: number;
|
||||
}
|
||||
class Money {
|
||||
cents: number;
|
||||
constructor(cents: number) {
|
||||
this.cents = cents;
|
||||
}
|
||||
plus(o: Money): Money {
|
||||
return new Money(this.cents + o.cents);
|
||||
}
|
||||
scaled(k: number): Money {
|
||||
return new Money(Math.round(this.cents * k));
|
||||
}
|
||||
}
|
||||
function pair(a: number, b: number): Pair {
|
||||
return { a, b };
|
||||
}
|
||||
function swap(p: Pair): Pair {
|
||||
return { a: p.b, b: p.a };
|
||||
}
|
||||
function pick(n: number): Pair {
|
||||
if (n > 4) return { a: n, b: -n };
|
||||
for (let i = 0; i < 3; i++) if (i === n) return { a: i, b: i * i };
|
||||
return n < 0 ? { a: 0, b: 0 } : { a: 1, b: 1 };
|
||||
}
|
||||
function risky(n: number): Pair {
|
||||
if (n === 2) throw new Error(`risky ${n}`);
|
||||
return { a: n, b: n + 0.5 };
|
||||
}
|
||||
function sum(p: Pair): number {
|
||||
return p.a + p.b;
|
||||
}
|
||||
function countdown(n: number): Pair {
|
||||
return n === 0 ? { a: 0, b: 0 } : { a: sum(countdown(n - 1)) + n, b: n };
|
||||
}
|
||||
const kept: Pair[] = [];
|
||||
let total = 0;
|
||||
for (let i = -1; i < 7; i++) {
|
||||
total += sum(swap(pair(i, 2 * i))) + sum(pick(i));
|
||||
const p = pick(i);
|
||||
total += p.a * 3 + p.b;
|
||||
kept.push(pick(i));
|
||||
try {
|
||||
total += sum(risky(i));
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
}
|
||||
const m = new Money(100 + i).plus(new Money(5)).scaled(1.5);
|
||||
total += m.cents;
|
||||
total += sum(countdown(i < 0 ? 0 : i));
|
||||
}
|
||||
console.log(total, kept.map((p) => `${p.a},${p.b}`).join(" "));
|
||||
@@ -0,0 +1,72 @@
|
||||
// Reassigned bindings of number records and objects: each new value may
|
||||
// be computed from the old one, aliases keep the object they were given,
|
||||
// and an assignment that throws leaves the previous value in place.
|
||||
class Complex {
|
||||
re: number;
|
||||
im: number;
|
||||
constructor(re: number, im: number) {
|
||||
this.re = re;
|
||||
this.im = im;
|
||||
}
|
||||
times(o: Complex): Complex {
|
||||
return new Complex(this.re * o.re - this.im * o.im, this.re * o.im + this.im * o.re);
|
||||
}
|
||||
plus(o: Complex): Complex {
|
||||
return new Complex(this.re + o.re, this.im + o.im);
|
||||
}
|
||||
swapped(): Complex {
|
||||
return new Complex(this.im, this.re);
|
||||
}
|
||||
}
|
||||
interface P {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
function rotate(p: P): P {
|
||||
return { x: -p.y, y: p.x };
|
||||
}
|
||||
function fail(p: P, n: number): P {
|
||||
if (n === 2) throw new Error("fail");
|
||||
return { x: p.x + n, y: p.y };
|
||||
}
|
||||
|
||||
let inside = 0;
|
||||
for (let y = -4; y <= 4; y++)
|
||||
for (let x = -8; x <= 4; x++) {
|
||||
const c = new Complex(x / 4, y / 4);
|
||||
let z = new Complex(0, 0);
|
||||
let n = 0;
|
||||
while (n < 50 && z.re * z.re + z.im * z.im <= 4) {
|
||||
z = z.times(z).plus(c);
|
||||
n++;
|
||||
}
|
||||
if (n === 50) inside++;
|
||||
}
|
||||
console.log("inside", inside);
|
||||
|
||||
let out = "";
|
||||
for (let k = 0; k < 3; k++) {
|
||||
let z = new Complex(k, 1);
|
||||
let p: P = { x: 1, y: 2 };
|
||||
for (let i = 0; i < 4; i++) {
|
||||
z = z.times(z).plus(new Complex(0.5, -0.25));
|
||||
if (i % 2 === 0) z = z.swapped();
|
||||
else p = rotate(p);
|
||||
p = { x: p.y, y: p.x + i };
|
||||
try {
|
||||
p = fail(p, i);
|
||||
} catch {
|
||||
out += "!";
|
||||
}
|
||||
}
|
||||
let q: P = { x: 0, y: 0 };
|
||||
if (k === 1) q = rotate(p);
|
||||
else if (k === 2) q = { x: 9, y: 9 };
|
||||
out += `${z.re.toFixed(3)},${z.im.toFixed(3)};${p.x},${p.y};${q.x},${q.y}|`;
|
||||
}
|
||||
console.log(out);
|
||||
|
||||
let w = new Complex(1, 2);
|
||||
const alias = w;
|
||||
w = new Complex(3, 4);
|
||||
console.log(alias.re, w.re);
|
||||
@@ -0,0 +1,46 @@
|
||||
// Number records and objects across try/catch/finally, labeled loops,
|
||||
// generators and async functions keep the same values and order of
|
||||
// effects as in Node.
|
||||
interface Box {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
function area(b: Box): number {
|
||||
return b.w * b.h;
|
||||
}
|
||||
function make(w: number, h: number): Box {
|
||||
return { w, h };
|
||||
}
|
||||
function guarded(n: number): number {
|
||||
const b = make(n, n + 1);
|
||||
try {
|
||||
if (n === 3) throw new Error("three");
|
||||
return area(b);
|
||||
} catch (error) {
|
||||
return -area({ w: (error as Error).message.length, h: 1 });
|
||||
} finally {
|
||||
console.log("finally", area(b));
|
||||
}
|
||||
}
|
||||
function* boxes(n: number): Generator<number> {
|
||||
for (let i = 0; i < n; i++) {
|
||||
const b = make(i, 2);
|
||||
yield area(b);
|
||||
}
|
||||
}
|
||||
async function later(n: number): Promise<number> {
|
||||
const b = make(n, 3);
|
||||
await null;
|
||||
return area(b) + area(make(1, 1));
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
outer: for (let i = 0; i < 5; i++) {
|
||||
for (let j = 0; j < 5; j++) {
|
||||
const b = { w: i, h: j };
|
||||
if (area(b) > 6) continue outer;
|
||||
total += area(b) + guarded(j);
|
||||
}
|
||||
}
|
||||
console.log(total, [...boxes(4)].join(","));
|
||||
later(4).then((value) => console.log("later", value));
|
||||
@@ -0,0 +1,61 @@
|
||||
// Loop bindings of number records and objects that the body reassigns:
|
||||
// for...of over arrays, holey arrays, Sets, Map values and generators,
|
||||
// destructured bindings, for-loop headers and for...in bodies all read
|
||||
// the value most recently assigned, and the iterated values are unchanged.
|
||||
interface P { x: number; y: number }
|
||||
class C { v: number; constructor(v: number) { this.v = v; } }
|
||||
function shift(p: P): P { return { x: p.x + 100, y: p.y }; }
|
||||
function read(p: P): number { return p.x * 10 + p.y; }
|
||||
const points: P[] = [{ x: 1, y: 2 }, { x: 3, y: 4 }];
|
||||
const objects = [new C(1), new C(2)];
|
||||
let out = "";
|
||||
for (let p of points) {
|
||||
out += read(p) + " ";
|
||||
p = { x: p.x + 10, y: 0 };
|
||||
out += read(p) + " ";
|
||||
p = shift(p);
|
||||
out += read(p) + ";";
|
||||
}
|
||||
for (let c of objects) {
|
||||
c = new C(c.v * 7);
|
||||
out += c.v + ";";
|
||||
}
|
||||
for (let { x, y } of points) {
|
||||
let q: P = { x, y };
|
||||
q = shift(q);
|
||||
out += read(q) + ";";
|
||||
}
|
||||
for (let i = 0, p: P = { x: 0, y: 0 }; i < 3; i++, p = { x: p.x + i, y: i }) out += read(p) + ",";
|
||||
for (const key in { a: 1, b: 2 }) {
|
||||
let r: P = { x: key.length, y: 0 };
|
||||
r = shift(r);
|
||||
out += read(r) + ";";
|
||||
}
|
||||
console.log(out, points.map(read).join(" "), objects.map((c) => c.v).join(" "));
|
||||
const set = new Set<P>([{ x: 1, y: 2 }, { x: 3, y: 4 }]);
|
||||
const map = new Map<string, C>([["a", new C(1)], ["b", new C(2)]]);
|
||||
function* gen(): Generator<P> { yield { x: 5, y: 6 }; yield { x: 7, y: 8 }; }
|
||||
let out2 = "";
|
||||
for (let p of set) {
|
||||
out2 += read(p) + " ";
|
||||
p = { x: p.x + 10, y: 0 };
|
||||
out2 += read(p) + " ";
|
||||
p = shift(p);
|
||||
out2 += read(p) + ";";
|
||||
}
|
||||
for (let c of map.values()) {
|
||||
c = new C(c.v * 7);
|
||||
out2 += c.v + ";";
|
||||
}
|
||||
for (let p of gen()) {
|
||||
p = shift(p);
|
||||
out2 += read(p) + ";";
|
||||
}
|
||||
const holey: P[] = [{ x: 1, y: 1 }];
|
||||
holey[3] = { x: 2, y: 2 };
|
||||
for (let p of holey) {
|
||||
if (p === undefined) continue;
|
||||
p = shift(p);
|
||||
out2 += read(p) + ";";
|
||||
}
|
||||
console.log(out2, [...set].map(read).join(" "), [...map.values()].map((c) => c.v).join(" "));
|
||||
Reference in New Issue
Block a user