Keep non-escaping number records and objects in frame storage

This commit is contained in:
Chris Tate
2026-10-14 02:44:11 -05:00
parent 6738693f3d
commit 7bc8a191f0
21 changed files with 1306 additions and 75 deletions
+1
View File
@@ -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 |
+7
View File
@@ -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));
+59 -2
View File
@@ -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 };
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5120-frame-records.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5121-frame-objects.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5122-frame-result-slots.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5123-frame-reassigned.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5124-frame-objects-control.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5127-frame-loop-bindings.ts"
],
"diags": []
}
+54
View File
@@ -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 }));
+86
View File
@@ -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(","));
+57
View File
@@ -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(" "));
+72
View File
@@ -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));
+61
View File
@@ -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(" "));