mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Allocate object groups that die together in arenas (#2348)
This commit is contained in:
@@ -134,7 +134,7 @@ Program objects define `main` and leave selected `scr_*` runtime functions undef
|
||||
|
||||
## Native runtime
|
||||
|
||||
- **Memory:** Values use reference counting. Acyclic values are freed when their last reference is released, and a cycle collector runs at defined collection points. Some cycles involving opaque native values can remain allocated. When the main thread's script ends, the process exits without freeing what the program still holds, and the operating system reclaims it; worker threads free their heaps when they exit. Builds with `--sanitize`, and processes run with `SCRIPTC_EXIT_FREE=1` (for external leak checkers), free everything at exit.
|
||||
- **Memory:** Values use reference counting. Acyclic values are freed when their last reference is released, and a cycle collector runs at defined collection points. When the compiler proves that a group of objects is released together, such as a tree that a recursive function builds and its caller only reads, the group is allocated in an arena and released at once, without freeing each object or collecting its cycles. Some cycles involving opaque native values can remain allocated. When the main thread's script ends, the process exits without freeing what the program still holds, and the operating system reclaims it; worker threads free their heaps when they exit. Builds with `--sanitize`, and processes run with `SCRIPTC_EXIT_FREE=1` (for external leak checkers), free everything at exit.
|
||||
- **Concurrency:** An `async` function that suspends only at its own `await` expressions compiles to a stackless coroutine whose heap frame holds the values live across those awaits; other suspending `async` functions and async generators use stackful fibers. The native event loop schedules microtasks, timers, and I/O, using kqueue on macOS and epoll on Linux.
|
||||
- **Networking:** Supported `net`, `http`, `https`, `tls`, `dgram`, and `dns` APIs use native implementations. TLS uses vendored mbedTLS.
|
||||
- **Numbers:** Values use JavaScript's double-precision representation. Number-to-string formatting uses shortest-roundtrip output and is tested against Node.js.
|
||||
|
||||
@@ -141,7 +141,7 @@ const CYC_GEN = 3;
|
||||
/** ScrCycHdr.buf, an i32 at this header offset. */
|
||||
export const CYC_BUF = 4;
|
||||
/** The emitted descriptor `{ trace, free_fn, kind, id }` (ScrCycType). */
|
||||
const CYC_TYPE = "{ ptr, ptr, i32, i32 }";
|
||||
export const CYC_TYPE = "{ ptr, ptr, i32, i32 }";
|
||||
|
||||
function declareState(host: AllocHost): void {
|
||||
if (host.threadLocalAlloc !== true) host.declare(`@scr_sa = external global ${SA_TYPE}`);
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
/* Group arenas (runtime scr_arena.h): objects that region-groups.ts proves
|
||||
* die together are bump-allocated in an arena and released at once.
|
||||
*
|
||||
* - A root (a fresh-result call or `new` whose value its caller keeps in
|
||||
* one scope) opens an arena before it is evaluated and closes it after.
|
||||
* The scope that holds the value drops the arena when it exits, on every
|
||||
* path. A statement-frame entry closes the arena too, so an exception
|
||||
* that skips the close never leaves it the allocation target.
|
||||
* - A call region (a call of a leak-free function that returns no
|
||||
* reference) opens an arena and drops it as soon as the call returns,
|
||||
* or when its statement frame unwinds.
|
||||
*
|
||||
* region-sites.ts keeps only the roots and call regions whose groups grow,
|
||||
* and picks the allocations that take the arena: those whose value the
|
||||
* group keeps. Arena-aware functions are the ones reached from the kept
|
||||
* roots and call regions. There, a kept `new`, record or array literal
|
||||
* calls the arena-aware constructor (`<new>.r`, `scr_arr_new*_r`), which
|
||||
* allocates in the current arena when one is open and on the heap
|
||||
* otherwise, and a call whose value the group does not keep runs with the
|
||||
* arena suspended. Every other allocation site stays as it is.
|
||||
*
|
||||
* Arenas are off in programs that construct weak collections, in programs
|
||||
* of more than ARENA_FUNCTION_LIMIT functions, in libraries, and with
|
||||
* lifetime regions. */
|
||||
import type { IrClassDef, IrFunction, IrModule, IrType } from "../../ir/ir.js";
|
||||
import {
|
||||
analyzeClosedGroups,
|
||||
analyzeRegionGroups,
|
||||
source,
|
||||
type RegionGroups,
|
||||
} from "./region-groups.js";
|
||||
import { CYC_BUF, CYC_HDR, CYC_TYPE } from "./alloc.js";
|
||||
import { ALLOCATIONS, children, forEachNode, isExpr, type Node } from "./region-ir.js";
|
||||
import { planRegionSites, reachableFrom, type RegionSites } from "./region-sites.js";
|
||||
|
||||
/** The count an arena object starts with (scr_arena.h SCR_ARENA_RC). */
|
||||
export const ARENA_RC = 2 ** 29;
|
||||
|
||||
/** Group arenas are on unless SCRIPTC_ARENAS=0 (for comparisons). */
|
||||
export function arenasRequested(): boolean {
|
||||
return process.env["SCRIPTC_ARENAS"] !== "0";
|
||||
}
|
||||
|
||||
export interface ArenaPlan {
|
||||
groups: RegionGroups;
|
||||
sites: RegionSites;
|
||||
/** Record shapes and classes that get an arena-aware constructor. */
|
||||
shapes: Set<string>;
|
||||
classes: Set<string>;
|
||||
}
|
||||
|
||||
export interface ArenaEligibility {
|
||||
shape(shapeId: string): boolean;
|
||||
cls(def: IrClassDef): boolean;
|
||||
/** The reference arm of a union stored as a nullable pointer, or null. */
|
||||
nullableArm(unionId: string): IrType | null;
|
||||
/** Whether values of `t` can be on a reference cycle (the collector
|
||||
* traces them). */
|
||||
traced(t: IrType): boolean;
|
||||
}
|
||||
|
||||
/** Whether `mod` can construct a WeakMap or WeakSet, directly or through
|
||||
* a stored global constructor. A weak collection's entry for a key goes
|
||||
* when the key is freed, and an arena object is never freed on its own, so
|
||||
* such programs use no arenas. */
|
||||
function constructsWeakCollections(mod: IrModule): boolean {
|
||||
let found = false;
|
||||
const visit = (node: Node): void => {
|
||||
if (found) return;
|
||||
if (node.kind === "libCall") {
|
||||
if (node.fn === "weakMap.new" || node.fn === "weakSet.new") found = true;
|
||||
else if (node.fn === "dyn.globalConstructor") {
|
||||
const name = node.args[0];
|
||||
if (name?.kind !== "strLit" || name.value === "WeakMap" || name.value === "WeakSet")
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
const kids: Node[] = [];
|
||||
children(node, kids);
|
||||
for (const k of kids) visit(k);
|
||||
};
|
||||
for (const fn of mod.functions) for (const s of fn.body) visit(s);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Programs with more functions than this use no arenas: the analysis
|
||||
* visits every function, and on programs this large it costs more compile
|
||||
* time than their arenas save at run time. */
|
||||
const ARENA_FUNCTION_LIMIT = 4000;
|
||||
|
||||
/** The arena plan of `mod`, or null when nothing would use an arena. */
|
||||
export function planArenas(mod: IrModule, eligible: ArenaEligibility): ArenaPlan | null {
|
||||
if (mod.functions.length > ARENA_FUNCTION_LIMIT || constructsWeakCollections(mod)) return null;
|
||||
const groups = analyzeRegionGroups(mod);
|
||||
const sites = planRegionSites(mod, groups);
|
||||
if (groups.roots.size === 0 && groups.calls.size === 0) return null;
|
||||
const shapes = new Set<string>();
|
||||
const classes = new Set<string>();
|
||||
const defs = new Map((mod.classes ?? []).map((c) => [c.name, c]));
|
||||
const note = (node: Node): void => {
|
||||
const kept = sites.arenaSites.has(node) || groups.roots.has(node);
|
||||
if (
|
||||
kept &&
|
||||
node.kind === "recordLit" &&
|
||||
node.type.kind === "record" &&
|
||||
eligible.shape(node.type.shapeId)
|
||||
)
|
||||
shapes.add(node.type.shapeId);
|
||||
if (kept && node.kind === "new") {
|
||||
const def = defs.get(node.className);
|
||||
if (def !== undefined && eligible.cls(def)) classes.add(node.className);
|
||||
}
|
||||
const kids: Node[] = [];
|
||||
children(node, kids);
|
||||
for (const k of kids) note(k);
|
||||
};
|
||||
for (const fn of mod.functions)
|
||||
if (groups.reachable.has(fn.name)) for (const s of fn.body) note(s);
|
||||
for (const r of groups.roots) if (r.kind === "new") note(r);
|
||||
// A group's heap objects die when its arena drops, unless heap objects
|
||||
// hold each other in a cycle: the collector frees those later, and by
|
||||
// then their references into the arena point at recycled memory. So a
|
||||
// group keeps its arena only if every cycle-capable object it can
|
||||
// allocate takes the arena.
|
||||
const takesArena = (node: Node): boolean =>
|
||||
(sites.arenaSites.has(node) || groups.roots.has(node)) &&
|
||||
((node.kind === "recordLit" && node.type.kind === "record" && shapes.has(node.type.shapeId)) ||
|
||||
(node.kind === "new" && classes.has(node.className)) ||
|
||||
node.kind === "arrayLit");
|
||||
const unsafe = cycleUnsafeFunctions(mod, groups.reachable, sites, takesArena, eligible.traced);
|
||||
for (const r of [...groups.roots, ...groups.calls]) {
|
||||
const runs = reachableFrom(mod, [r]);
|
||||
if ([...runs].some((f) => unsafe.has(f)) || (r.kind === "new" && !takesArena(r))) {
|
||||
groups.roots.delete(r);
|
||||
groups.calls.delete(r);
|
||||
groups.drops.delete(r);
|
||||
}
|
||||
}
|
||||
if (groups.roots.size === 0 && groups.calls.size === 0) return null;
|
||||
groups.reachable = reachableFrom(mod, [...groups.roots, ...groups.calls]);
|
||||
const arenaType = (t: IrType): boolean => {
|
||||
if (t.kind === "object") return classes.has(t.className);
|
||||
if (t.kind === "record") return shapes.has(t.shapeId);
|
||||
if (t.kind === "union") {
|
||||
const arm = eligible.nullableArm(t.unionId);
|
||||
return arm !== null && arenaType(arm);
|
||||
}
|
||||
// Arrays of arena objects: kept array literals of them take the arena.
|
||||
return t.kind === "array" && t.elem.kind !== "union" && arenaType(t.elem);
|
||||
};
|
||||
const keptSite = (e: Node): boolean => sites.arenaSites.has(e) || groups.roots.has(e);
|
||||
for (const r of analyzeClosedGroups(mod, groups, arenaType, keptSite)) groups.closed.add(r);
|
||||
return { groups, sites, shapes, classes };
|
||||
}
|
||||
|
||||
/** The functions in `reachable` that can allocate a cycle-capable object
|
||||
* on the heap while an arena is open: one that does not take the arena
|
||||
* (`takesArena`), or a call that runs with the arena suspended and reaches
|
||||
* any cycle-capable allocation. */
|
||||
function cycleUnsafeFunctions(
|
||||
mod: IrModule,
|
||||
reachable: ReadonlySet<string>,
|
||||
sites: RegionSites,
|
||||
takesArena: (node: Node) => boolean,
|
||||
traced: (t: IrType) => boolean,
|
||||
): Set<string> {
|
||||
const fns = new Map(mod.functions.map((f) => [f.name, f]));
|
||||
const allocatesTraced = (node: Node): boolean =>
|
||||
isExpr(node) &&
|
||||
traced(node.type) &&
|
||||
(node.kind === "closure"
|
||||
? node.captures.length > 0
|
||||
: (ALLOCATIONS.has(node.kind) || source(node) || node.kind === "libCall") &&
|
||||
node.kind !== "call" &&
|
||||
node.kind !== "callValue");
|
||||
// Functions with a cycle-capable allocation, and what they call.
|
||||
const direct = new Set<string>();
|
||||
const calls = new Map<string, Set<string>>();
|
||||
const heap = new Set<string>();
|
||||
for (const name of reachable) {
|
||||
const fn = fns.get(name);
|
||||
if (fn === undefined) continue;
|
||||
const out = new Set<string>();
|
||||
calls.set(name, out);
|
||||
forEachNode(fn.body, (node) => {
|
||||
if (allocatesTraced(node)) {
|
||||
direct.add(name);
|
||||
if (!takesArena(node)) heap.add(name);
|
||||
}
|
||||
if (node.kind === "call") out.add(node.callee);
|
||||
else if (node.kind === "new") out.add(`%${node.className}.constructor`);
|
||||
else if (node.kind === "closure") out.add(node.fnName);
|
||||
});
|
||||
}
|
||||
const tracedSomewhere = new Set(direct);
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const [name, out] of calls)
|
||||
if (!tracedSomewhere.has(name) && [...out].some((c) => tracedSomewhere.has(c))) {
|
||||
tracedSomewhere.add(name);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
for (const name of reachable) {
|
||||
const fn = fns.get(name);
|
||||
if (fn === undefined || heap.has(name)) continue;
|
||||
forEachNode(fn.body, (node) => {
|
||||
if (!sites.heapCalls.has(node)) return;
|
||||
const target =
|
||||
node.kind === "call"
|
||||
? node.callee
|
||||
: node.kind === "new"
|
||||
? `%${node.className}.constructor`
|
||||
: null;
|
||||
// A suspended call of an unknown function (a closure, a virtual call)
|
||||
// may allocate anything.
|
||||
if (target === null || tracedSomewhere.has(target)) heap.add(name);
|
||||
});
|
||||
}
|
||||
return heap;
|
||||
}
|
||||
|
||||
/** The arena-aware constructor of a record shape or class. */
|
||||
export function arenaNewSym(newSym: string): string {
|
||||
return `${newSym}.r`;
|
||||
}
|
||||
|
||||
/** The function that releases an arena object's fields at the drop. */
|
||||
export function arenaDisposeSym(newSym: string): string {
|
||||
return `${newSym}.dispose`;
|
||||
}
|
||||
|
||||
/** Whether `fn` allocates through arena-aware constructors. */
|
||||
export function arenaAware(plan: ArenaPlan | null, fn: IrFunction | undefined): boolean {
|
||||
return plan !== null && fn !== undefined && plan.groups.reachable.has(fn.name);
|
||||
}
|
||||
|
||||
/** What arenaNewLines needs from its host (a subset of ShapeHost). */
|
||||
export interface ArenaHost {
|
||||
declare(decl: string): void;
|
||||
threadLocalAlloc?: boolean;
|
||||
inlineAlloc?: boolean;
|
||||
/** The target C ABI's `size_t`: also the pointer width. */
|
||||
readonly sizeType: "i32" | "i64";
|
||||
}
|
||||
|
||||
/** Instructions that set `dest` to the address of the calling thread's
|
||||
* open arena pointer (scr_arena.c's scr_arena_cur), or null on targets
|
||||
* without the inline allocator, which call scr_arena_current,
|
||||
* scr_arena_suspend and scr_arena_resume instead. In worker programs the
|
||||
* pointer is thread-local. */
|
||||
export function arenaCurrentAddress(host: ArenaHost, dest: string): string[] | null {
|
||||
if (host.inlineAlloc !== true) return null;
|
||||
if (host.threadLocalAlloc !== true) {
|
||||
host.declare(`@scr_arena_cur = external global ptr`);
|
||||
return [`${dest} = getelementptr inbounds i8, ptr @scr_arena_cur, i64 0`];
|
||||
}
|
||||
host.declare(`@scr_arena_cur = external thread_local global ptr`);
|
||||
host.declare(`declare nonnull ptr @llvm.threadlocal.address.p0(ptr nonnull)`);
|
||||
return [`${dest} = call ptr @llvm.threadlocal.address.p0(ptr @scr_arena_cur)`];
|
||||
}
|
||||
|
||||
/**
|
||||
* The arena-aware constructor `<new>.r`: an object of `size` bytes (a
|
||||
* `size_t` constant expression) in the current arena, behind the collector header
|
||||
* `cyc` names (or none), which `init` sets up the way the heap constructor
|
||||
* does (with the count ARENA_RC). When `dispose` names a function and the
|
||||
* arena is not closed, the object goes on the arena's dispose list, which
|
||||
* the drop runs. The heap constructor's object when no arena is open or
|
||||
* the arena is full. The bump, the clear, the header and the dispose entry
|
||||
* are inline (scr_arena.c's ScrArena ABI); scr_arena_obj takes a new chunk
|
||||
* and scr_arena_disposal grows the list.
|
||||
*/
|
||||
export function arenaNewLines(
|
||||
host: ArenaHost,
|
||||
newSym: string,
|
||||
size: string,
|
||||
cyc: string | null,
|
||||
dispose: string | null,
|
||||
init: string[],
|
||||
result: string,
|
||||
): string[] {
|
||||
const S = host.sizeType;
|
||||
// ScrArena's fields and dispose entries are pointer-sized (scr_arena.c).
|
||||
const word = S === "i32" ? 4 : 8;
|
||||
host.declare(`declare ptr @scr_arena_obj(ptr, ${S}, ptr)`);
|
||||
host.declare(`declare void @llvm.memset.p0.${S}(ptr, i8, ${S}, i1)`);
|
||||
const cur = arenaCurrentAddress(host, "%cur");
|
||||
const current =
|
||||
cur === null
|
||||
? (host.declare(`declare ptr @scr_arena_current()`), [` %a = call ptr @scr_arena_current()`])
|
||||
: [...cur.map((line) => ` ${line}`), ` %a = load ptr, ptr %cur`];
|
||||
const lines: string[] = [
|
||||
`define internal ptr @${arenaNewSym(newSym)}() nounwind alwaysinline { ; in the open arena`,
|
||||
`entry:`,
|
||||
...current,
|
||||
` %on = icmp ne ptr %a, null`,
|
||||
` br i1 %on, label %arena, label %heap`,
|
||||
`arena:`,
|
||||
];
|
||||
// A traced type's first object registers its descriptor on the slow path.
|
||||
if (cyc !== null)
|
||||
lines.push(
|
||||
` %tidp = getelementptr inbounds ${CYC_TYPE}, ptr ${cyc}, i32 0, i32 3`,
|
||||
` %tid = load atomic i32, ptr %tidp acquire, align 4`,
|
||||
` %creg = icmp ne i32 %tid, 0`,
|
||||
);
|
||||
const hdr = cyc === null ? 0 : CYC_HDR;
|
||||
lines.push(
|
||||
` %n0 = add ${S} ${size}, ${hdr + 7}`,
|
||||
` %n = and ${S} %n0, -8`,
|
||||
` %bp = load ptr, ptr %a`,
|
||||
` %limp = getelementptr inbounds i8, ptr %a, ${S} ${word}`,
|
||||
` %lim = load ptr, ptr %limp`,
|
||||
` %bpi = ptrtoint ptr %bp to ${S}`,
|
||||
` %limi = ptrtoint ptr %lim to ${S}`,
|
||||
` %room = sub ${S} %limi, %bpi`,
|
||||
` %fits0 = icmp ule ${S} %n, %room`,
|
||||
);
|
||||
if (cyc !== null) lines.push(` %fits = and i1 %fits0, %creg`);
|
||||
lines.push(
|
||||
` br i1 ${cyc === null ? "%fits0" : "%fits"}, label %carve, label %slow`,
|
||||
`carve:`,
|
||||
` %end = getelementptr inbounds i8, ptr %bp, ${S} %n`,
|
||||
` store ptr %end, ptr %a`,
|
||||
` call void @llvm.memset.p0.${S}(ptr align 8 %bp, i8 0, ${S} %n, i1 false)`,
|
||||
);
|
||||
if (cyc !== null)
|
||||
lines.push(
|
||||
` %id = trunc i32 %tid to i16`,
|
||||
` store i16 %id, ptr %bp ; ScrCycHdr.id`,
|
||||
` %bufp = getelementptr inbounds i8, ptr %bp, i64 ${CYC_BUF}`,
|
||||
` store i32 -1, ptr %bufp ; ScrCycHdr.buf = SCR_CYC_ACYCLIC`,
|
||||
);
|
||||
lines.push(
|
||||
` %of = getelementptr inbounds i8, ptr %bp, ${S} ${hdr}`,
|
||||
` br label %init`,
|
||||
`slow:`,
|
||||
` %os = call ptr @scr_arena_obj(ptr %a, ${S} ${size}, ptr ${cyc ?? "null"})`,
|
||||
` %got = icmp ne ptr %os, null`,
|
||||
` br i1 %got, label %init, label %heap`,
|
||||
`init:`,
|
||||
` %o = phi ptr [ %of, %carve ], [ %os, %slow ]`,
|
||||
...init,
|
||||
);
|
||||
if (dispose !== null) {
|
||||
host.declare(`declare void @scr_arena_disposal(ptr, ptr, ptr)`);
|
||||
lines.push(
|
||||
` %closedp = getelementptr inbounds i8, ptr %a, ${S} ${4 * word} ; ScrArena.closed`,
|
||||
` %closed = load i32, ptr %closedp`,
|
||||
` %skip = icmp ne i32 %closed, 0`,
|
||||
` br i1 %skip, label %done, label %list`,
|
||||
`list:`,
|
||||
` %dpp = getelementptr inbounds i8, ptr %a, ${S} ${2 * word} ; ScrArena.disposals`,
|
||||
` %dp = load ptr, ptr %dpp`,
|
||||
` %dep = getelementptr inbounds i8, ptr %a, ${S} ${3 * word} ; ScrArena.disposals_end`,
|
||||
` %de = load ptr, ptr %dep`,
|
||||
` %full = icmp eq ptr %dp, %de`,
|
||||
` br i1 %full, label %grow, label %push`,
|
||||
`push:`,
|
||||
` store ptr %o, ptr %dp ; ScrArenaDisposal.obj`,
|
||||
` %dfp = getelementptr inbounds i8, ptr %dp, ${S} ${word}`,
|
||||
` store ptr @${dispose}, ptr %dfp ; ScrArenaDisposal.dispose`,
|
||||
` %dn = getelementptr inbounds i8, ptr %dp, ${S} ${2 * word}`,
|
||||
` store ptr %dn, ptr %dpp`,
|
||||
` br label %done`,
|
||||
`grow:`,
|
||||
` call void @scr_arena_disposal(ptr %a, ptr %o, ptr @${dispose})`,
|
||||
` br label %done`,
|
||||
`done:`,
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
` ret ptr ${result}`,
|
||||
`heap:`,
|
||||
` %h = call ptr @${newSym}()`,
|
||||
` ret ptr %h`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** Dispose lines for field `i` of `%o` at `ptr` (a getelementptr into
|
||||
* `%o`), released by `release`: a plain reference (no union encoding) is
|
||||
* released only when it is a heap object, since an arena or immortal
|
||||
* object's count (the rc word's low half) is at least 2^28 and its release
|
||||
* would change nothing. */
|
||||
export function disposeFieldLines(
|
||||
i: number,
|
||||
gep: string,
|
||||
plain: boolean,
|
||||
release: string,
|
||||
comment: string,
|
||||
): string[] {
|
||||
const load = [` %f${i} = ${gep}`, ` %v${i} = load ptr, ptr %f${i}`];
|
||||
if (!plain) return [...load, ` call void ${release}(ptr %v${i}) ; ${comment}`];
|
||||
return [
|
||||
...load,
|
||||
` %nn${i} = icmp ne ptr %v${i}, null`,
|
||||
` br i1 %nn${i}, label %chk${i}, label %next${i}`,
|
||||
`chk${i}:`,
|
||||
` %rc${i} = load i32, ptr %v${i}`,
|
||||
` %heap${i} = icmp ult i32 %rc${i}, ${2 ** 28}`,
|
||||
` br i1 %heap${i}, label %rel${i}, label %next${i}`,
|
||||
`rel${i}:`,
|
||||
` call void ${release}(ptr %v${i}) ; ${comment}`,
|
||||
` br label %next${i}`,
|
||||
`next${i}:`,
|
||||
];
|
||||
}
|
||||
|
||||
/** Whether values of `t` are plain pointers to counted objects. */
|
||||
export function plainRefType(t: IrType): boolean {
|
||||
return (
|
||||
t.kind === "object" ||
|
||||
t.kind === "record" ||
|
||||
t.kind === "array" ||
|
||||
t.kind === "map" ||
|
||||
t.kind === "set" ||
|
||||
t.kind === "func" ||
|
||||
t.kind === "string"
|
||||
);
|
||||
}
|
||||
|
||||
/** The dispose function: releases each reference field (`fields`, lines
|
||||
* that load `%v<i>` and release it) of the object `%o`. */
|
||||
export function arenaDisposeLines(newSym: string, fields: string[]): string[] {
|
||||
return [
|
||||
`define internal void @${arenaDisposeSym(newSym)}(ptr %o) nounwind { ; arena drop`,
|
||||
`entry:`,
|
||||
...fields,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
];
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
import {
|
||||
ARENA_RC,
|
||||
arenaDisposeLines,
|
||||
arenaDisposeSym,
|
||||
arenaNewLines,
|
||||
disposeFieldLines,
|
||||
plainRefType,
|
||||
} from "./arenas.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/** Class layouts, reference counting, hierarchy intervals, virtual tables and
|
||||
* constructor values. Opaque LLVM pointers allow virtual slots to store method
|
||||
@@ -1415,6 +1423,8 @@ export function emitClassShapes(
|
||||
storedHeaderBytes(meta),
|
||||
),
|
||||
);
|
||||
// The header and field set-up, shared with the arena-aware constructor.
|
||||
const initStart = nw.length;
|
||||
if (meta.compact && !meta.hierarchy)
|
||||
nw.push(` store i32 ${compactHeaderWord(meta)}, ptr %o ; no count (compactHeaderWord)`);
|
||||
else if (meta.compact) {
|
||||
@@ -1483,6 +1493,7 @@ export function emitClassShapes(
|
||||
// Young instances: program-owned traced classes. Classes rooted at a
|
||||
// runtime base (errors, emitters, events, streams) carry runtime state
|
||||
// and stay with trial deletion, like every runtime family.
|
||||
const init = nw.slice(initStart);
|
||||
if (
|
||||
traced &&
|
||||
!lifetime &&
|
||||
@@ -1492,6 +1503,52 @@ export function emitClassShapes(
|
||||
nw.push(...youngEntryLines(host));
|
||||
nw.push(` ret ptr ${meta.hierarchy ? "%o.vt" : "%o"}`, `}`, ``);
|
||||
defs.push(...nw);
|
||||
if (
|
||||
host.arenaClasses?.has(cls.name) === true &&
|
||||
cls.arrayElem === undefined &&
|
||||
!lifetime &&
|
||||
regions === null &&
|
||||
!meta.compact &&
|
||||
runtimePrefix === null &&
|
||||
!isStreamRooted &&
|
||||
!meta.threadSlot
|
||||
) {
|
||||
// The arena count in place of 1, in the rc word or its low half.
|
||||
const counted = meta.idInRc
|
||||
? ` store i64 ${classIdRcWord(meta, false)}, ptr %o.vt`
|
||||
: ` store ${host.sizeType} 1, ptr %o`;
|
||||
const arenaCount = meta.idInRc
|
||||
? ` store i64 ${(BigInt(meta.pre + 1) * 2n ** 32n + BigInt(ARENA_RC)).toString()}, ptr %o.vt`
|
||||
: ` store ${host.sizeType} ${ARENA_RC}, ptr %o`;
|
||||
const newSym = mangleClassNew(cls.name);
|
||||
const dispose = refFields.length > 0 ? arenaDisposeSym(newSym) : null;
|
||||
defs.push(
|
||||
...arenaNewLines(
|
||||
host,
|
||||
newSym,
|
||||
sizeOf,
|
||||
traced ? cycType : null,
|
||||
dispose,
|
||||
init.map((line) => (line.startsWith(counted) ? arenaCount : line)),
|
||||
meta.hierarchy ? "%o.vt" : "%o",
|
||||
),
|
||||
);
|
||||
if (dispose !== null)
|
||||
defs.push(
|
||||
...arenaDisposeLines(
|
||||
newSym,
|
||||
refFields.flatMap((f, t) =>
|
||||
disposeFieldLines(
|
||||
t,
|
||||
`getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`,
|
||||
plainRefType(f.type),
|
||||
releaseSym(host, f.type),
|
||||
llvmCommentText(f.name),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (traced) {
|
||||
// trace: visit exactly the cycle-capable fields; gcFree: release
|
||||
|
||||
@@ -525,6 +525,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
}
|
||||
}
|
||||
emitter.scopes.push(fnScope);
|
||||
emitter.beginArenaFunction(fn);
|
||||
// Stackful native fibers and native coroutine frames retain these frames
|
||||
// while suspended. The active exception context owns the chain, so
|
||||
// concurrent fibers isolate it.
|
||||
|
||||
@@ -105,8 +105,11 @@ const MAX_CALLBACK_VERSION_COST = 400;
|
||||
export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
const B = emitter.B;
|
||||
emitter.frames.push([]);
|
||||
if (emitter.arenas !== null)
|
||||
emitter.statementFrames.add(emitter.frames[emitter.frames.length - 1]!);
|
||||
switch (s.kind) {
|
||||
case "varDecl": {
|
||||
emitter.ownArenasOf(s.localId);
|
||||
if (s.init && emitter.int32Locals.has(s.localId)) {
|
||||
emitter.storeInt32Local(s.localId, emitter.emitExpr(s.init), s.init);
|
||||
break;
|
||||
|
||||
@@ -343,6 +343,15 @@ import {
|
||||
type LlClassMeta,
|
||||
} from "./classes.js";
|
||||
import { LifetimeRegions, REGION_TYPES } from "./regions.js";
|
||||
import {
|
||||
arenaAware,
|
||||
arenaCurrentAddress,
|
||||
arenaNewSym,
|
||||
arenasRequested,
|
||||
planArenas,
|
||||
type ArenaPlan,
|
||||
} from "./arenas.js";
|
||||
import { children, type Node } from "./region-ir.js";
|
||||
import { LlDyn, recordDynKeepsIdentity, type DynHost } from "./dyn.js";
|
||||
import { VirtualBorrows } from "./virtual-borrows.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
@@ -375,6 +384,7 @@ import {
|
||||
retainSym,
|
||||
traceArg,
|
||||
vAdapters,
|
||||
traceAdapter,
|
||||
} from "./shapes.js";
|
||||
import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlValue } from "./expr-context.js";
|
||||
import { INLINE_LITERAL_BYTES } from "./string-literal-equality.js";
|
||||
@@ -465,6 +475,9 @@ export interface LlScopeEntry {
|
||||
slot: string;
|
||||
type: IrType;
|
||||
boxed?: boolean;
|
||||
/** A group arena list slot (arenas.ts), dropped after the scope's other
|
||||
* entries. */
|
||||
arena?: boolean;
|
||||
/** A lazily created box: the slot stays null on paths that build no
|
||||
* environment, which skip the release call. */
|
||||
lazy?: boolean;
|
||||
@@ -2412,6 +2425,40 @@ export class LlEmitter {
|
||||
mod.lib === undefined &&
|
||||
options.lowRegion === true &&
|
||||
[...this.classMeta.values()].some((m) => m.compact);
|
||||
// Group arenas: off with lifetime regions, which own their classes'
|
||||
// allocation, and in libraries, whose exports any host code calls.
|
||||
if (arenasRequested() && mod.lifetimeRegions !== true && mod.lib === undefined) {
|
||||
this.arenas = planArenas(mod, {
|
||||
shape: (id) => {
|
||||
const shape = this.recordsById.get(id);
|
||||
return (
|
||||
shape !== undefined &&
|
||||
shape.freezable !== true &&
|
||||
shape.indexValue === undefined &&
|
||||
this.publishedTypes?.recordGuarded(id) !== true
|
||||
);
|
||||
},
|
||||
cls: (def) => {
|
||||
const meta = this.classMeta.get(def.name);
|
||||
return (
|
||||
meta !== undefined &&
|
||||
!meta.compact &&
|
||||
!meta.threadSlot &&
|
||||
// An Array subclass's instance is an array (the runtime's header).
|
||||
def.arrayElem === undefined &&
|
||||
meta.root.def.arrayElem === undefined &&
|
||||
!this.programLifetime.has(def.name) &&
|
||||
this.publishedTypes?.classGuarded(def.name) !== true
|
||||
);
|
||||
},
|
||||
nullableArm: (unionId) => this.nullableUnions.single(unionId)?.arm ?? null,
|
||||
traced: (t) => traceAdapter(this.shapeHost, t) !== null,
|
||||
});
|
||||
if (this.arenas !== null) {
|
||||
this.shapeHost.arenaShapes = this.arenas.shapes;
|
||||
this.shapeHost.arenaClasses = this.arenas.classes;
|
||||
}
|
||||
}
|
||||
for (const [name, rec] of RUNTIME_ERROR_CLASSES) {
|
||||
const meta = this.classMeta.get(name);
|
||||
if (!meta) break; // hand-written IR without the builtin defs: no stamps
|
||||
@@ -3527,6 +3574,7 @@ export class LlEmitter {
|
||||
/** Releases a statement frame's temps and owned slots. */
|
||||
releaseTemps(frame: LlValue[]): void {
|
||||
for (const v of frame) {
|
||||
if (v.arena !== undefined) continue;
|
||||
if (v.slot) {
|
||||
const t = this.B.tmp();
|
||||
this.B.line(`${t} = load ptr, ptr ${v.name}`);
|
||||
@@ -3539,10 +3587,19 @@ export class LlEmitter {
|
||||
this.releaseFrame(v.name, v.type);
|
||||
}
|
||||
}
|
||||
// Arena lists last: the frame's temps may be arena objects.
|
||||
for (const v of frame)
|
||||
if (v.arena === "drop") this.B.line(`call void @scr_arena_drop(ptr ${v.name})`);
|
||||
else if (v.arena === "close") {
|
||||
const head = this.B.tmp();
|
||||
this.B.line(`${head} = load ptr, ptr ${v.name}`);
|
||||
this.B.line(`call void @scr_arena_close(ptr ${head})`);
|
||||
}
|
||||
}
|
||||
|
||||
releaseScope(scope: LlScopeEntry[]): void {
|
||||
for (const e of scope) {
|
||||
if (e.arena === true) continue;
|
||||
const t = this.B.tmp();
|
||||
this.B.line(`${t} = load ptr, ptr ${e.slot}`);
|
||||
if (e.boxed && e.lazy) {
|
||||
@@ -3564,6 +3621,9 @@ export class LlEmitter {
|
||||
this.releaseFrame(t, e.type); // runtime releases are NULL-tolerant
|
||||
}
|
||||
}
|
||||
// Arena lists last: the scope's bindings may hold arena objects.
|
||||
for (const e of scope)
|
||||
if (e.arena === true) this.B.line(`call void @scr_arena_drop(ptr ${e.slot})`);
|
||||
}
|
||||
|
||||
/** THE release-on-jump path (break/continue/return): pending statement
|
||||
@@ -6335,6 +6395,7 @@ export class LlEmitter {
|
||||
type: local.type,
|
||||
slot: true,
|
||||
...(local.boxed ? { boxed: true } : {}),
|
||||
...(local.arena === true ? { arena: "drop" as const } : {}),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
@@ -7429,9 +7490,196 @@ export class LlEmitter {
|
||||
}
|
||||
|
||||
emitExpr(e: IrExpr): LlValue {
|
||||
return this.arenas === null ? emitExpr(this, e) : this.emitArenaExpr(e, this.arenas);
|
||||
}
|
||||
|
||||
/** emitExpr when group arenas are planned. Kept out of emitExpr, whose
|
||||
* frame each level of a nested expression holds while it is emitted. */
|
||||
private emitArenaExpr(e: IrExpr, arenas: ArenaPlan): LlValue {
|
||||
if (arenas.groups.calls.has(e)) {
|
||||
// A call region: everything the call allocates dies when it returns.
|
||||
// The statement drops the arena after its temps, which an
|
||||
// arena-aware caller can have allocated in it while evaluating the
|
||||
// arguments; any other caller drops it as soon as the call returns.
|
||||
const slot = this.arenaSlot(e);
|
||||
this.statementFrame().push({ name: slot, type: e.type, slot: true, arena: "drop" });
|
||||
this.B.line(`call ptr @scr_arena_open(ptr ${slot}, i32 ${this.arenaClosed(e)})`);
|
||||
const value = emitExpr(this, e);
|
||||
if (!arenaAware(arenas, this.fnByName.get(this.currentFunctionName)))
|
||||
this.B.line(`call void @scr_arena_drop(ptr ${slot})`);
|
||||
return value;
|
||||
}
|
||||
if (arenas.sites.heapCalls.has(e) && this.arenaSite(null)) {
|
||||
// A call whose value the group does not keep: everything it allocates
|
||||
// takes the heap.
|
||||
const saved = this.suspendArena();
|
||||
const value = emitExpr(this, e);
|
||||
this.resumeArena(saved);
|
||||
return value;
|
||||
}
|
||||
if (this.opensArena(e)) {
|
||||
// A root: its group lives until the scope holding its value exits
|
||||
// (the statement frame for a value used in place).
|
||||
const slot = this.arenaSlot(e);
|
||||
const local = arenas.groups.drops.get(e) ?? null;
|
||||
this.statementFrame().push({
|
||||
name: slot,
|
||||
type: e.type,
|
||||
slot: true,
|
||||
arena: local === null ? "drop" : "close",
|
||||
});
|
||||
const arena = this.B.tmp();
|
||||
this.B.line(`${arena} = call ptr @scr_arena_open(ptr ${slot}, i32 ${this.arenaClosed(e)})`);
|
||||
const value = emitExpr(this, e);
|
||||
this.B.line(`call void @scr_arena_close(ptr ${arena})`);
|
||||
return value;
|
||||
}
|
||||
return emitExpr(this, e);
|
||||
}
|
||||
|
||||
/** Group arenas (arenas.ts): the plan, the list slot of each root and
|
||||
* call region, and which locals' scopes drop which roots' arenas. */
|
||||
arenas: ArenaPlan | null = null;
|
||||
private arenaSlots = new Map<Node, string>();
|
||||
/** The frames emitStmtBody opens for whole statements (arenas only). */
|
||||
readonly statementFrames = new Set<LlValue[]>();
|
||||
|
||||
/** The frame of the statement being emitted: a nested frame (call
|
||||
* arguments, branches) ends before the values built in it do. */
|
||||
statementFrame(): LlValue[] {
|
||||
for (let i = this.frames.length - 1; i >= 0; i--)
|
||||
if (this.statementFrames.has(this.frames[i]!)) return this.frames[i]!;
|
||||
return this.currentFrame();
|
||||
}
|
||||
|
||||
/** The arena list slot of root or call region `e`: allocated, cleared and
|
||||
* (for a root dropped with a local) owned by that local's scope when the
|
||||
* function starts or the local is declared. */
|
||||
arenaSlot(e: Node): string {
|
||||
let slot = this.arenaSlots.get(e);
|
||||
if (slot === undefined) {
|
||||
slot = this.B.tmp();
|
||||
this.B.entryAllocas.push(`${slot} = alloca ptr`, `store ptr null, ptr ${slot}`);
|
||||
this.arenaSlots.set(e, slot);
|
||||
this.declare(`declare ptr @scr_arena_open(ptr, i32)`);
|
||||
this.declare(`declare void @scr_arena_close(ptr)`);
|
||||
this.declare(`declare void @scr_arena_drop(ptr)`);
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
/** Whether emitExpr opens an arena for `e`: a root call or `new` of a
|
||||
* counted value. */
|
||||
opensArena(e: Node): e is Extract<IrExpr, { kind: "call" | "new" }> {
|
||||
return (
|
||||
this.arenas !== null &&
|
||||
this.arenas.groups.roots.has(e) &&
|
||||
(e.kind === "call" || e.kind === "new") &&
|
||||
isRefCounted(e.type)
|
||||
);
|
||||
}
|
||||
|
||||
/** 1 when the group of root or call region `e` is closed: its objects
|
||||
* hold only each other and immortal values. */
|
||||
arenaClosed(e: Node): number {
|
||||
return this.arenas?.groups.closed.has(e) === true ? 1 : 0;
|
||||
}
|
||||
|
||||
/** Registers on the innermost scope the arena lists of the roots whose
|
||||
* arenas `localId`'s scope drops. */
|
||||
ownArenasOf(localId: string): void {
|
||||
const roots = this.arenaDropsByLocal.get(localId);
|
||||
if (roots === undefined) return;
|
||||
const scope = this.scopes[this.scopes.length - 1]!;
|
||||
for (const { root, type } of roots)
|
||||
scope.push({ slot: this.arenaSlot(root), type, arena: true });
|
||||
}
|
||||
|
||||
/** The roots of the function being emitted, by the local that drops
|
||||
* them; set when a function's emission starts. */
|
||||
arenaDropsByLocal = new Map<string, { root: Node; type: IrType }[]>();
|
||||
|
||||
/** Starts a function's arena bookkeeping: its roots by dropping local,
|
||||
* with the lists of locals the body never declares (parameters, loop
|
||||
* and catch bindings) owned by the function's own scope. */
|
||||
beginArenaFunction(fn: IrFunction): void {
|
||||
this.arenaSlots.clear();
|
||||
this.statementFrames.clear();
|
||||
this.arenaDropsByLocal = new Map();
|
||||
const arenas = this.arenas;
|
||||
if (arenas === null) return;
|
||||
const declared = new Set<string>();
|
||||
const visit = (node: Node): void => {
|
||||
if (node.kind === "varDecl") declared.add(node.localId);
|
||||
if (this.opensArena(node)) {
|
||||
const local = arenas.groups.drops.get(node);
|
||||
if (local !== undefined && local !== null) {
|
||||
const list = this.arenaDropsByLocal.get(local) ?? [];
|
||||
list.push({ root: node, type: node.type });
|
||||
this.arenaDropsByLocal.set(local, list);
|
||||
}
|
||||
}
|
||||
const kids: Node[] = [];
|
||||
children(node, kids);
|
||||
for (const kid of kids) visit(kid);
|
||||
};
|
||||
for (const s of fn.body) visit(s);
|
||||
for (const local of [...this.arenaDropsByLocal.keys()])
|
||||
if (!declared.has(local)) this.ownArenasOf(local);
|
||||
}
|
||||
|
||||
/** Whether allocation site `e` (null: any) in the function being
|
||||
* emitted takes the open arena: the function is arena-aware and the group
|
||||
* keeps the site's value (region-sites.ts). */
|
||||
arenaSite(e: IrExpr | null): boolean {
|
||||
const arenas = this.arenas;
|
||||
if (!arenaAware(arenas, this.fnByName.get(this.currentFunctionName))) return false;
|
||||
return e === null || arenas!.sites.arenaSites.has(e);
|
||||
}
|
||||
|
||||
/** Stops any arena being the allocation target; returns what restores it. */
|
||||
suspendArena(): string {
|
||||
const saved = this.B.tmp();
|
||||
const cur = this.B.tmp();
|
||||
const lines = arenaCurrentAddress(this.shapeHost, cur);
|
||||
if (lines === null) {
|
||||
this.declare(`declare ptr @scr_arena_suspend()`);
|
||||
this.B.line(`${saved} = call ptr @scr_arena_suspend()`);
|
||||
} else {
|
||||
for (const line of lines) this.B.line(line);
|
||||
this.B.line(`${saved} = load ptr, ptr ${cur}`);
|
||||
this.B.line(`store ptr null, ptr ${cur}`);
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
/** Makes the arena suspendArena stopped (`saved`) the target again. */
|
||||
resumeArena(saved: string): void {
|
||||
const cur = this.B.tmp();
|
||||
const lines = arenaCurrentAddress(this.shapeHost, cur);
|
||||
if (lines === null) {
|
||||
this.declare(`declare void @scr_arena_resume(ptr)`);
|
||||
this.B.line(`call void @scr_arena_resume(ptr ${saved})`);
|
||||
} else {
|
||||
for (const line of lines) this.B.line(line);
|
||||
this.B.line(`store ptr ${saved}, ptr ${cur}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The arena-aware constructor of `newSym` for an allocation of a
|
||||
* record shape or class in the function being emitted, or `newSym`. */
|
||||
allocSym(newSym: string, e: IrExpr): string {
|
||||
const arenas = this.arenas;
|
||||
if (arenas === null) return newSym;
|
||||
const planned =
|
||||
e.type.kind === "record"
|
||||
? arenas.shapes.has(e.type.shapeId)
|
||||
: e.kind === "new" && arenas.classes.has(e.className);
|
||||
if (!planned) return newSym;
|
||||
const aware = this.arenaSite(e) || arenas.groups.roots.has(e);
|
||||
return aware ? arenaNewSym(newSym) : newSym;
|
||||
}
|
||||
|
||||
/** The helper classification of this module's inline-cache sites. */
|
||||
get jsIcSites(): JsIcSites {
|
||||
return (this.jsIcSitesMemo ??= new JsIcSites(this.mod));
|
||||
|
||||
@@ -1104,7 +1104,7 @@ export function emitCallExpr(
|
||||
`llvm emitter bug: new ${e.className} without a constructor`,
|
||||
);
|
||||
const o = B.tmp();
|
||||
B.line(`${o} = call ptr @${mangleClassNew(e.className)}()`);
|
||||
B.line(`${o} = call ptr @${host.allocSym(mangleClassNew(e.className), e)}()`);
|
||||
const out = host.own({ name: o, type: e.type });
|
||||
emitConstruction(host, e, o, ctor);
|
||||
return out;
|
||||
|
||||
@@ -33,6 +33,9 @@ export interface LlValue {
|
||||
slot?: boolean;
|
||||
/** A sequence local held in a capture box; release the box itself. */
|
||||
boxed?: boolean;
|
||||
/** A group arena list slot (arenas.ts): the frame closes the newest
|
||||
* arena on it, or drops the whole list, after its other temps. */
|
||||
arena?: "close" | "drop";
|
||||
}
|
||||
|
||||
export interface WideInteger {
|
||||
|
||||
@@ -11,6 +11,10 @@ import { emitRecordKeyGetOr } from "./expr-records.js";
|
||||
import { NULLABLE_NULL, SHAPE_WORD_INDEX, shapeWordHeader } from "./nullable-unions.js";
|
||||
import { emitUnionTag } from "./union-repr.js";
|
||||
|
||||
/** Each kind emits in a function of its own: emitting a nested chain of
|
||||
* these expressions holds one emitControlExpr frame per level, and in an
|
||||
* unoptimized build of the compiler a frame holds every local of the
|
||||
* function. */
|
||||
export function emitControlExpr(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<
|
||||
@@ -25,446 +29,25 @@ export function emitControlExpr(
|
||||
| "nullish"
|
||||
>,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
switch (e.kind) {
|
||||
case "dynDestrCheck": {
|
||||
// RequireObjectCoercible with V8's destructuring TypeError. dyn
|
||||
// values check in the runtime helper and pass through unchanged.
|
||||
const v = host.emitExpr(e.value);
|
||||
const first = e.firstProp !== undefined ? host.cstr(e.firstProp) : "null";
|
||||
|
||||
const helper = host.dyn.dynDestrCheckHelper();
|
||||
B.line(`call void @${helper}(ptr ${v.name}, ptr ${host.cstr(e.spelling)}, ptr ${first})`);
|
||||
host.emitPendingCheck();
|
||||
return v;
|
||||
}
|
||||
case "dynIterN": {
|
||||
// GetIterator + first-N steps as a fresh array (V8's exact
|
||||
// not-iterable TypeError on non-iterables) through the native dyn helper.
|
||||
const v = emitBorrowedInput(host, e.value);
|
||||
|
||||
const helper = host.dyn.dynIterNHelper();
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @${helper}(ptr ${v.name}, ${host.sizeType} ${e.count}, ptr ${e.notIterableMessage ? host.cstr(e.notIterableMessage) : "null"})`,
|
||||
);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
case "dynDestrCheck":
|
||||
return emitDynDestrCheck(host, e);
|
||||
case "dynIterN":
|
||||
return emitDynIterN(host, e);
|
||||
case "toBool":
|
||||
return { name: host.truthy(host.emitUnionProjection(e.operand)), type: e.type };
|
||||
case "logical": {
|
||||
const tagSet = e.op === "||" ? unionTagSet(e) : null;
|
||||
if (tagSet) return { name: emitUnionTagSet(host, tagSet), type: e.type };
|
||||
// JS value semantics: the result is the deciding operand itself.
|
||||
// Left evaluates once, ownership moves into the result slot; when
|
||||
// the branch takes the right operand the stale left releases first
|
||||
// and the right runs in its own frame.
|
||||
const ty = host.llType(e.type);
|
||||
const l = host.emitExpr(e.left);
|
||||
host.toFrame(l);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
|
||||
const integerSlot = e.type.kind === "f64" && l.uint32 !== undefined ? B.slot() : undefined;
|
||||
if (integerSlot !== undefined) {
|
||||
B.entryAllocas.push(`${integerSlot} = alloca i32`);
|
||||
B.line(`store i32 ${l.uint32!}, ptr ${integerSlot}`);
|
||||
}
|
||||
const truthy = host.truthy(l);
|
||||
const rightLabel = B.newLabel("log.r");
|
||||
const joinLabel = B.newLabel("log.j");
|
||||
if (e.op === "&&") B.condBr(truthy, rightLabel, joinLabel);
|
||||
else B.condBr(truthy, joinLabel, rightLabel);
|
||||
B.startBlock(rightLabel);
|
||||
if (isRefCounted(e.type)) host.releaseFrame(l.name, e.type);
|
||||
const rightInteger = host.emitBranchInto(slot, e.right, integerSlot);
|
||||
B.br(joinLabel);
|
||||
B.startBlock(joinLabel);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
if (integerSlot !== undefined && rightInteger) {
|
||||
const uint32 = B.tmp();
|
||||
B.line(`${uint32} = load i32, ptr ${integerSlot}`);
|
||||
return { name: t, type: e.type, uint32 };
|
||||
}
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
case "ternary": {
|
||||
// Exactly one arm evaluates; each arm runs in its own frame and
|
||||
// moves the chosen value into the result slot.
|
||||
const ty = host.llType(e.type);
|
||||
const c = host.emitExpr(e.cond);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const integerSlot = e.type.kind === "f64" ? B.slot() : undefined;
|
||||
if (integerSlot !== undefined) B.entryAllocas.push(`${integerSlot} = alloca i32`);
|
||||
const lt = B.newLabel("tern.t");
|
||||
const lf = B.newLabel("tern.f");
|
||||
const lj = B.newLabel("tern.j");
|
||||
B.condBr(c.name, lt, lf);
|
||||
B.startBlock(lt);
|
||||
const thenInteger = host.emitBranchInto(slot, e.then, integerSlot);
|
||||
B.br(lj);
|
||||
B.startBlock(lf);
|
||||
const elseInteger = host.emitBranchInto(slot, e.else_, integerSlot);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
// A view is available only when every incoming branch wrote it.
|
||||
// Incomplete views are unused and disappear during promotion.
|
||||
if (integerSlot !== undefined && thenInteger && elseInteger) {
|
||||
const uint32 = B.tmp();
|
||||
B.line(`${uint32} = load i32, ptr ${integerSlot}`);
|
||||
return { name: t, type: e.type, uint32 };
|
||||
}
|
||||
// A choice between interned literals is an interned literal: like a
|
||||
// strLit operand, nothing owns it (retain and release are no-ops).
|
||||
if (literalChoice(e)) return host.markImmortal({ name: t, type: e.type });
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
case "optChain": {
|
||||
// `a?.b` / `f?.()`: the nullish test inverted. The receiver
|
||||
// evaluates once into an ordinary frame temp (borrowed); on a unit
|
||||
// tag the result is the interned undefined arm and the body never
|
||||
// runs; otherwise the narrowed payload fills the bind slot (+1,
|
||||
// frame-owned through a SLOT entry — NULL on the unit path, where
|
||||
// the frame's release is a no-op) and the body reads it through
|
||||
// chainRecv.
|
||||
if (e.receiver.type.kind === "dyn") {
|
||||
// A dyn (dyn) receiver — the `rawName?.match(re)` step: the
|
||||
// nullish test reads the node's kind tag; the unit path is the
|
||||
// undefined dyn singleton (dyn results) or nothing (void
|
||||
// bodies), the body runs over the bound receiver otherwise.
|
||||
const r = host.emitExpr(e.receiver);
|
||||
const kd = host.dynKind(r.name);
|
||||
const isU = B.tmp();
|
||||
const isN = B.tmp();
|
||||
const isUnit = B.tmp();
|
||||
B.line(`${isU} = icmp eq i32 ${kd}, ${DYN_KIND.UNDEF}`);
|
||||
B.line(`${isN} = icmp eq i32 ${kd}, ${DYN_KIND.NULL}`);
|
||||
B.line(`${isUnit} = or i1 ${isU}, ${isN}`);
|
||||
const bind = B.slot();
|
||||
B.entryAllocas.push(`${bind} = alloca ptr`);
|
||||
B.line(`store ptr null, ptr ${bind}`);
|
||||
host.ownSlot(bind, e.receiver.type);
|
||||
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
||||
if (e.type.kind === "void") {
|
||||
const lb = B.newLabel("ocd.b");
|
||||
const lj = B.newLabel("ocd.j");
|
||||
B.condBr(isUnit, lj, lb);
|
||||
B.startBlock(lb);
|
||||
const rr = B.tmp();
|
||||
B.line(`${rr} = call ptr @scr_dyn_retain_v(ptr ${r.name})`);
|
||||
B.line(`store ptr ${rr}, ptr ${bind}`);
|
||||
host.chainSlots.set(e.id, { name: bind, type: e.receiver.type, slot: true });
|
||||
host.frames.push([]);
|
||||
host.emitExpr(e.body);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
host.chainSlots.delete(e.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.type.kind !== "dyn")
|
||||
throw new InternalCompilerError("llvm emitter bug: dyn optChain result kind");
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
const lu = B.newLabel("ocd.u");
|
||||
const lb = B.newLabel("ocd.b");
|
||||
const lj = B.newLabel("ocd.j");
|
||||
B.condBr(isUnit, lu, lb);
|
||||
B.startBlock(lu);
|
||||
host.declare(`declare ptr @scr_dyn_undefined()`);
|
||||
const un = B.tmp();
|
||||
const ur = B.tmp();
|
||||
B.line(`${un} = call ptr @scr_dyn_undefined()`);
|
||||
B.line(`${ur} = call ptr @scr_dyn_retain_v(ptr ${un})`);
|
||||
B.line(`store ptr ${ur}, ptr ${slot}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lb);
|
||||
const rr = B.tmp();
|
||||
B.line(`${rr} = call ptr @scr_dyn_retain_v(ptr ${r.name})`);
|
||||
B.line(`store ptr ${rr}, ptr ${bind}`);
|
||||
host.chainSlots.set(e.id, { name: bind, type: e.receiver.type, slot: true });
|
||||
host.emitBranchInto(slot, e.body);
|
||||
host.chainSlots.delete(e.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
const { isUnit, bindPresent } = emitOptionalChainReceiver(host, e);
|
||||
if (e.type.kind === "void") {
|
||||
// Statement form (cb?.()): no result value at all.
|
||||
const lb = B.newLabel("oc.b");
|
||||
const lj = B.newLabel("oc.j");
|
||||
B.condBr(isUnit, lj, lb);
|
||||
B.startBlock(lb);
|
||||
bindPresent();
|
||||
host.frames.push([]);
|
||||
host.emitExpr(e.body);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
host.chainSlots.delete(e.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.type.kind === "dyn") {
|
||||
// A dyn-typed chain (`pricing?.[key]` over an unknown-valued
|
||||
// index signature): the unit path is the undefined dyn value —
|
||||
// dyn represents undefined directly, no union wrapper exists.
|
||||
const slotD = B.slot();
|
||||
B.entryAllocas.push(`${slotD} = alloca ptr`);
|
||||
const lu = B.newLabel("ocu.u");
|
||||
const lb = B.newLabel("ocu.b");
|
||||
const lj = B.newLabel("ocu.j");
|
||||
B.condBr(isUnit, lu, lb);
|
||||
B.startBlock(lu);
|
||||
host.declare(`declare ptr @scr_dyn_undefined()`);
|
||||
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
||||
const un = B.tmp();
|
||||
const ur = B.tmp();
|
||||
B.line(`${un} = call ptr @scr_dyn_undefined()`);
|
||||
B.line(`${ur} = call ptr @scr_dyn_retain_v(ptr ${un})`);
|
||||
B.line(`store ptr ${ur}, ptr ${slotD}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lb);
|
||||
bindPresent();
|
||||
host.emitBranchInto(slotD, e.body);
|
||||
host.chainSlots.delete(e.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${slotD}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
if (e.type.kind !== "union")
|
||||
throw new LlvmUnsupportedError(`optChainResult:${e.type.kind}`, e.loc);
|
||||
const undefTag = undefinedArmTag(e.type, host.unionsById);
|
||||
if (undefTag < 0)
|
||||
throw new InternalCompilerError(
|
||||
"llvm emitter bug: optChain result lacks its undefined arm",
|
||||
);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const lu = B.newLabel("oc.u");
|
||||
const lb = B.newLabel("oc.b");
|
||||
const lj = B.newLabel("oc.j");
|
||||
B.condBr(isUnit, lu, lb);
|
||||
B.startBlock(lu);
|
||||
B.line(`store ptr ${host.unitInstanceRef(e.type.unionId, undefTag)}, ptr ${slot}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lb);
|
||||
bindPresent();
|
||||
host.emitBranchInto(slot, e.body);
|
||||
host.chainSlots.delete(e.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
case "chainRecv": {
|
||||
const bound = host.chainSlots.get(e.id);
|
||||
if (!bound)
|
||||
throw new InternalCompilerError(`llvm emitter bug: chainRecv "${e.id}" outside its chain`);
|
||||
const v = B.tmp();
|
||||
B.line(`${v} = load ${host.llType(bound.type)}, ptr ${bound.name}`);
|
||||
if (!isRefCounted(e.type)) return { name: v, type: e.type };
|
||||
return host.own({ name: host.retainFrame(v, e.type), type: e.type });
|
||||
}
|
||||
case "orDefault": {
|
||||
// `u || d` narrowed to the single non-unit arm: nullish's dance
|
||||
// with the union TRUTHY switch as the test — truthy extracts the
|
||||
// arm (+1 for ref kinds), falsy releases and runs the default.
|
||||
if (e.left.type.kind !== "union")
|
||||
throw new InternalCompilerError("llvm emitter bug: orDefault left is not a union");
|
||||
if (
|
||||
e.retag === undefined &&
|
||||
!host.nullableUnions.has(e.left.type.unionId) &&
|
||||
host.isStackUnionSource(e.left)
|
||||
) {
|
||||
// A stack box (`arr[i] || d`): test truthiness and extract the
|
||||
// payload (+1). The box is never retained or released.
|
||||
const l = host.emitUnionProjection(e.left);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const lt = B.newLabel("ord.t"),
|
||||
lf = B.newLabel("ord.f"),
|
||||
lj = B.newLabel("ord.j");
|
||||
B.condBr(host.truthy(l), lt, lf);
|
||||
B.startBlock(lt);
|
||||
B.line(
|
||||
`store ${ty} ${host.unionExtract(l.name, e.left.type.unionId, e.type)}, ptr ${slot}`,
|
||||
);
|
||||
B.br(lj);
|
||||
B.startBlock(lf);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
const l = host.emitExpr(e.left);
|
||||
host.toFrame(l);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const truthy = host.truthy(l);
|
||||
const lt = B.newLabel("ord.t");
|
||||
const lf = B.newLabel("ord.f");
|
||||
const lj = B.newLabel("ord.j");
|
||||
B.condBr(truthy, lt, lf);
|
||||
B.startBlock(lt);
|
||||
if (e.retag !== undefined) {
|
||||
// Retagged shape: the whole box goes to the union→union helper,
|
||||
// which CONSUMES it (callees own their params) — no release on
|
||||
// this side. The store precedes the pending check so an unwind
|
||||
// leaves only the NULL dummy behind, in a slot nothing reads.
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ${ty} @${host.callTarget(e.retag)}(${host.llType(e.left.type)} ${l.name})`,
|
||||
);
|
||||
B.line(`store ${ty} ${t}, ptr ${slot}`);
|
||||
if (host.mayThrow.has(e.retag)) host.emitPendingCheck();
|
||||
} else if (host.nullableUnions.single(e.left.type.unionId)) {
|
||||
// The owned nullable pointer IS the owned arm value.
|
||||
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
|
||||
} else {
|
||||
B.line(
|
||||
`store ${ty} ${host.unionExtract(l.name, e.left.type.unionId, e.type)}, ptr ${slot}`,
|
||||
);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
}
|
||||
B.br(lj);
|
||||
B.startBlock(lf);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
case "nullish": {
|
||||
// `a ?? b`: logical's move/release dance with the left's runtime
|
||||
// TAG against its unit arms as the test. Pass-through shape: the
|
||||
// result IS the left box. Narrowed shape: the single non-unit
|
||||
// arm's payload extracts (+1 for ref kinds) and the box releases.
|
||||
|
||||
if (e.left.type.kind === "dyn") {
|
||||
// The native checked-value kind decides whether the left is
|
||||
// nullish; the right runs lazily in its branch.
|
||||
const l = host.emitExpr(e.left);
|
||||
host.toFrame(l);
|
||||
host.declare(`declare zeroext i1 @scr_dyn_is_nullish(ptr)`);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
const isN = B.tmp();
|
||||
B.line(`${isN} = call zeroext i1 @scr_dyn_is_nullish(ptr ${l.name})`);
|
||||
const lu = B.newLabel("nuld.u");
|
||||
const lv = B.newLabel("nuld.v");
|
||||
const lj = B.newLabel("nuld.j");
|
||||
B.condBr(isN, lu, lv);
|
||||
B.startBlock(lu);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lv);
|
||||
B.line(`store ptr ${l.name}, ptr ${slot}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
if (e.left.type.kind !== "union")
|
||||
throw new InternalCompilerError("llvm emitter bug: nullish left is not a union");
|
||||
const def = host.unionsById.get(e.left.type.unionId);
|
||||
if (!def)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: nullish of unknown union ${e.left.type.unionId}`,
|
||||
);
|
||||
const unitTags = def.arms.flatMap((a, i) => (isUnitType(a) ? [i] : []));
|
||||
if (unitTags.length === 0)
|
||||
throw new InternalCompilerError("llvm emitter bug: nullish union lacks unit arms");
|
||||
const fused = emitNullishOptionalChain(host, e);
|
||||
if (fused) return fused;
|
||||
if (
|
||||
e.left.kind === "recordKeyGet" &&
|
||||
e.left.missNaN !== true &&
|
||||
typeEquals(e.type, e.left.type) &&
|
||||
storesNoUnit(host, e.left)
|
||||
)
|
||||
return emitRecordKeyGetOr(host, e.left, e.right);
|
||||
if (
|
||||
!typeEquals(e.type, e.left.type) &&
|
||||
(e.left.kind === "varRef" ||
|
||||
host.isStackUnionSource(e.left) ||
|
||||
host.nullableFieldGet(e.left) !== null)
|
||||
) {
|
||||
// Narrowed shape over a binding or a stack box (`arr[i] ?? d`,
|
||||
// `map.get(k) ?? d`): test the tag and extract the payload (+1).
|
||||
// The box is never retained or released. A binding is borrowed:
|
||||
// no code runs between its read and the extraction, and the
|
||||
// default never touches it. Bindings proven projection-only may
|
||||
// hold stack boxes, which must never reach RC entry points.
|
||||
const l = host.emitUnionProjection(e.left);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const isUnit = host.tagInSet(l.name, def.id, unitTags);
|
||||
const lu = B.newLabel("nul.u");
|
||||
const lv = B.newLabel("nul.v");
|
||||
const lj = B.newLabel("nul.j");
|
||||
B.condBr(isUnit, lu, lv);
|
||||
B.startBlock(lu);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lv);
|
||||
B.line(`store ${ty} ${host.unionExtract(l.name, def.id, e.type)}, ptr ${slot}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
const l = host.emitExpr(e.left);
|
||||
host.toFrame(l);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const isUnit = host.tagInSet(l.name, def.id, unitTags);
|
||||
const lu = B.newLabel("nul.u");
|
||||
const lv = B.newLabel("nul.v");
|
||||
const lj = B.newLabel("nul.j");
|
||||
B.condBr(isUnit, lu, lv);
|
||||
B.startBlock(lu);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lv);
|
||||
if (typeEquals(e.type, e.left.type) || host.nullableUnions.single(def.id)) {
|
||||
// Pass-through, or a nullable pointer that IS its owned arm value.
|
||||
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
|
||||
} else {
|
||||
B.line(`store ${ty} ${host.unionExtract(l.name, def.id, e.type)}, ptr ${slot}`);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
}
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
case "logical":
|
||||
return emitLogical(host, e);
|
||||
case "ternary":
|
||||
return emitTernary(host, e);
|
||||
case "optChain":
|
||||
return emitOptChain(host, e);
|
||||
case "chainRecv":
|
||||
return emitChainRecv(host, e);
|
||||
case "orDefault":
|
||||
return emitOrDefault(host, e);
|
||||
case "nullish":
|
||||
return emitNullish(host, e);
|
||||
default: {
|
||||
const _exhaustive: never = e;
|
||||
void _exhaustive;
|
||||
@@ -473,6 +56,450 @@ export function emitControlExpr(
|
||||
}
|
||||
}
|
||||
|
||||
function emitDynDestrCheck(host: LlvmEmitterContext, e: ExprOf<"dynDestrCheck">): LlValue {
|
||||
const B = host.B;
|
||||
// RequireObjectCoercible with V8's destructuring TypeError. dyn
|
||||
// values check in the runtime helper and pass through unchanged.
|
||||
const v = host.emitExpr(e.value);
|
||||
const first = e.firstProp !== undefined ? host.cstr(e.firstProp) : "null";
|
||||
|
||||
const helper = host.dyn.dynDestrCheckHelper();
|
||||
B.line(`call void @${helper}(ptr ${v.name}, ptr ${host.cstr(e.spelling)}, ptr ${first})`);
|
||||
host.emitPendingCheck();
|
||||
return v;
|
||||
}
|
||||
|
||||
function emitDynIterN(host: LlvmEmitterContext, e: ExprOf<"dynIterN">): LlValue {
|
||||
const B = host.B;
|
||||
// GetIterator + first-N steps as a fresh array (V8's exact
|
||||
// not-iterable TypeError on non-iterables) through the native dyn helper.
|
||||
const v = emitBorrowedInput(host, e.value);
|
||||
|
||||
const helper = host.dyn.dynIterNHelper();
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @${helper}(ptr ${v.name}, ${host.sizeType} ${e.count}, ptr ${e.notIterableMessage ? host.cstr(e.notIterableMessage) : "null"})`,
|
||||
);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
|
||||
function emitLogical(host: LlvmEmitterContext, e: ExprOf<"logical">): LlValue {
|
||||
const B = host.B;
|
||||
const tagSet = e.op === "||" ? unionTagSet(e) : null;
|
||||
if (tagSet) return { name: emitUnionTagSet(host, tagSet), type: e.type };
|
||||
// JS value semantics: the result is the deciding operand itself.
|
||||
// Left evaluates once, ownership moves into the result slot; when
|
||||
// the branch takes the right operand the stale left releases first
|
||||
// and the right runs in its own frame.
|
||||
const ty = host.llType(e.type);
|
||||
const l = host.emitExpr(e.left);
|
||||
host.toFrame(l);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
|
||||
const integerSlot = e.type.kind === "f64" && l.uint32 !== undefined ? B.slot() : undefined;
|
||||
if (integerSlot !== undefined) {
|
||||
B.entryAllocas.push(`${integerSlot} = alloca i32`);
|
||||
B.line(`store i32 ${l.uint32!}, ptr ${integerSlot}`);
|
||||
}
|
||||
const truthy = host.truthy(l);
|
||||
const rightLabel = B.newLabel("log.r");
|
||||
const joinLabel = B.newLabel("log.j");
|
||||
if (e.op === "&&") B.condBr(truthy, rightLabel, joinLabel);
|
||||
else B.condBr(truthy, joinLabel, rightLabel);
|
||||
B.startBlock(rightLabel);
|
||||
if (isRefCounted(e.type)) host.releaseFrame(l.name, e.type);
|
||||
const rightInteger = host.emitBranchInto(slot, e.right, integerSlot);
|
||||
B.br(joinLabel);
|
||||
B.startBlock(joinLabel);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
if (integerSlot !== undefined && rightInteger) {
|
||||
const uint32 = B.tmp();
|
||||
B.line(`${uint32} = load i32, ptr ${integerSlot}`);
|
||||
return { name: t, type: e.type, uint32 };
|
||||
}
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
function emitTernary(host: LlvmEmitterContext, e: ExprOf<"ternary">): LlValue {
|
||||
const B = host.B;
|
||||
// Exactly one arm evaluates; each arm runs in its own frame and
|
||||
// moves the chosen value into the result slot.
|
||||
const ty = host.llType(e.type);
|
||||
const c = host.emitExpr(e.cond);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const integerSlot = e.type.kind === "f64" ? B.slot() : undefined;
|
||||
if (integerSlot !== undefined) B.entryAllocas.push(`${integerSlot} = alloca i32`);
|
||||
const lt = B.newLabel("tern.t");
|
||||
const lf = B.newLabel("tern.f");
|
||||
const lj = B.newLabel("tern.j");
|
||||
B.condBr(c.name, lt, lf);
|
||||
B.startBlock(lt);
|
||||
const thenInteger = host.emitBranchInto(slot, e.then, integerSlot);
|
||||
B.br(lj);
|
||||
B.startBlock(lf);
|
||||
const elseInteger = host.emitBranchInto(slot, e.else_, integerSlot);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
// A view is available only when every incoming branch wrote it.
|
||||
// Incomplete views are unused and disappear during promotion.
|
||||
if (integerSlot !== undefined && thenInteger && elseInteger) {
|
||||
const uint32 = B.tmp();
|
||||
B.line(`${uint32} = load i32, ptr ${integerSlot}`);
|
||||
return { name: t, type: e.type, uint32 };
|
||||
}
|
||||
// A choice between interned literals is an interned literal: like a
|
||||
// strLit operand, nothing owns it (retain and release are no-ops).
|
||||
if (literalChoice(e)) return host.markImmortal({ name: t, type: e.type });
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
function emitOptChain(host: LlvmEmitterContext, e: ExprOf<"optChain">): LlValue {
|
||||
const B = host.B;
|
||||
// `a?.b` / `f?.()`: the nullish test inverted. The receiver
|
||||
// evaluates once into an ordinary frame temp (borrowed); on a unit
|
||||
// tag the result is the interned undefined arm and the body never
|
||||
// runs; otherwise the narrowed payload fills the bind slot (+1,
|
||||
// frame-owned through a SLOT entry — NULL on the unit path, where
|
||||
// the frame's release is a no-op) and the body reads it through
|
||||
// chainRecv.
|
||||
if (e.receiver.type.kind === "dyn") {
|
||||
// A dyn (dyn) receiver — the `rawName?.match(re)` step: the
|
||||
// nullish test reads the node's kind tag; the unit path is the
|
||||
// undefined dyn singleton (dyn results) or nothing (void
|
||||
// bodies), the body runs over the bound receiver otherwise.
|
||||
const r = host.emitExpr(e.receiver);
|
||||
const kd = host.dynKind(r.name);
|
||||
const isU = B.tmp();
|
||||
const isN = B.tmp();
|
||||
const isUnit = B.tmp();
|
||||
B.line(`${isU} = icmp eq i32 ${kd}, ${DYN_KIND.UNDEF}`);
|
||||
B.line(`${isN} = icmp eq i32 ${kd}, ${DYN_KIND.NULL}`);
|
||||
B.line(`${isUnit} = or i1 ${isU}, ${isN}`);
|
||||
const bind = B.slot();
|
||||
B.entryAllocas.push(`${bind} = alloca ptr`);
|
||||
B.line(`store ptr null, ptr ${bind}`);
|
||||
host.ownSlot(bind, e.receiver.type);
|
||||
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
||||
if (e.type.kind === "void") {
|
||||
const lb = B.newLabel("ocd.b");
|
||||
const lj = B.newLabel("ocd.j");
|
||||
B.condBr(isUnit, lj, lb);
|
||||
B.startBlock(lb);
|
||||
const rr = B.tmp();
|
||||
B.line(`${rr} = call ptr @scr_dyn_retain_v(ptr ${r.name})`);
|
||||
B.line(`store ptr ${rr}, ptr ${bind}`);
|
||||
host.chainSlots.set(e.id, { name: bind, type: e.receiver.type, slot: true });
|
||||
host.frames.push([]);
|
||||
host.emitExpr(e.body);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
host.chainSlots.delete(e.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.type.kind !== "dyn")
|
||||
throw new InternalCompilerError("llvm emitter bug: dyn optChain result kind");
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
const lu = B.newLabel("ocd.u");
|
||||
const lb = B.newLabel("ocd.b");
|
||||
const lj = B.newLabel("ocd.j");
|
||||
B.condBr(isUnit, lu, lb);
|
||||
B.startBlock(lu);
|
||||
host.declare(`declare ptr @scr_dyn_undefined()`);
|
||||
const un = B.tmp();
|
||||
const ur = B.tmp();
|
||||
B.line(`${un} = call ptr @scr_dyn_undefined()`);
|
||||
B.line(`${ur} = call ptr @scr_dyn_retain_v(ptr ${un})`);
|
||||
B.line(`store ptr ${ur}, ptr ${slot}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lb);
|
||||
const rr = B.tmp();
|
||||
B.line(`${rr} = call ptr @scr_dyn_retain_v(ptr ${r.name})`);
|
||||
B.line(`store ptr ${rr}, ptr ${bind}`);
|
||||
host.chainSlots.set(e.id, { name: bind, type: e.receiver.type, slot: true });
|
||||
host.emitBranchInto(slot, e.body);
|
||||
host.chainSlots.delete(e.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
const { isUnit, bindPresent } = emitOptionalChainReceiver(host, e);
|
||||
if (e.type.kind === "void") {
|
||||
// Statement form (cb?.()): no result value at all.
|
||||
const lb = B.newLabel("oc.b");
|
||||
const lj = B.newLabel("oc.j");
|
||||
B.condBr(isUnit, lj, lb);
|
||||
B.startBlock(lb);
|
||||
bindPresent();
|
||||
host.frames.push([]);
|
||||
host.emitExpr(e.body);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
host.chainSlots.delete(e.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.type.kind === "dyn") {
|
||||
// A dyn-typed chain (`pricing?.[key]` over an unknown-valued
|
||||
// index signature): the unit path is the undefined dyn value —
|
||||
// dyn represents undefined directly, no union wrapper exists.
|
||||
const slotD = B.slot();
|
||||
B.entryAllocas.push(`${slotD} = alloca ptr`);
|
||||
const lu = B.newLabel("ocu.u");
|
||||
const lb = B.newLabel("ocu.b");
|
||||
const lj = B.newLabel("ocu.j");
|
||||
B.condBr(isUnit, lu, lb);
|
||||
B.startBlock(lu);
|
||||
host.declare(`declare ptr @scr_dyn_undefined()`);
|
||||
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
||||
const un = B.tmp();
|
||||
const ur = B.tmp();
|
||||
B.line(`${un} = call ptr @scr_dyn_undefined()`);
|
||||
B.line(`${ur} = call ptr @scr_dyn_retain_v(ptr ${un})`);
|
||||
B.line(`store ptr ${ur}, ptr ${slotD}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lb);
|
||||
bindPresent();
|
||||
host.emitBranchInto(slotD, e.body);
|
||||
host.chainSlots.delete(e.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${slotD}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
if (e.type.kind !== "union")
|
||||
throw new LlvmUnsupportedError(`optChainResult:${e.type.kind}`, e.loc);
|
||||
const undefTag = undefinedArmTag(e.type, host.unionsById);
|
||||
if (undefTag < 0)
|
||||
throw new InternalCompilerError("llvm emitter bug: optChain result lacks its undefined arm");
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const lu = B.newLabel("oc.u");
|
||||
const lb = B.newLabel("oc.b");
|
||||
const lj = B.newLabel("oc.j");
|
||||
B.condBr(isUnit, lu, lb);
|
||||
B.startBlock(lu);
|
||||
B.line(`store ptr ${host.unitInstanceRef(e.type.unionId, undefTag)}, ptr ${slot}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lb);
|
||||
bindPresent();
|
||||
host.emitBranchInto(slot, e.body);
|
||||
host.chainSlots.delete(e.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
function emitChainRecv(host: LlvmEmitterContext, e: ExprOf<"chainRecv">): LlValue {
|
||||
const B = host.B;
|
||||
const bound = host.chainSlots.get(e.id);
|
||||
if (!bound)
|
||||
throw new InternalCompilerError(`llvm emitter bug: chainRecv "${e.id}" outside its chain`);
|
||||
const v = B.tmp();
|
||||
B.line(`${v} = load ${host.llType(bound.type)}, ptr ${bound.name}`);
|
||||
if (!isRefCounted(e.type)) return { name: v, type: e.type };
|
||||
return host.own({ name: host.retainFrame(v, e.type), type: e.type });
|
||||
}
|
||||
|
||||
function emitOrDefault(host: LlvmEmitterContext, e: ExprOf<"orDefault">): LlValue {
|
||||
const B = host.B;
|
||||
// `u || d` narrowed to the single non-unit arm: nullish's dance
|
||||
// with the union TRUTHY switch as the test — truthy extracts the
|
||||
// arm (+1 for ref kinds), falsy releases and runs the default.
|
||||
if (e.left.type.kind !== "union")
|
||||
throw new InternalCompilerError("llvm emitter bug: orDefault left is not a union");
|
||||
if (
|
||||
e.retag === undefined &&
|
||||
!host.nullableUnions.has(e.left.type.unionId) &&
|
||||
host.isStackUnionSource(e.left)
|
||||
) {
|
||||
// A stack box (`arr[i] || d`): test truthiness and extract the
|
||||
// payload (+1). The box is never retained or released.
|
||||
const l = host.emitUnionProjection(e.left);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const lt = B.newLabel("ord.t"),
|
||||
lf = B.newLabel("ord.f"),
|
||||
lj = B.newLabel("ord.j");
|
||||
B.condBr(host.truthy(l), lt, lf);
|
||||
B.startBlock(lt);
|
||||
B.line(`store ${ty} ${host.unionExtract(l.name, e.left.type.unionId, e.type)}, ptr ${slot}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lf);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
const l = host.emitExpr(e.left);
|
||||
host.toFrame(l);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const truthy = host.truthy(l);
|
||||
const lt = B.newLabel("ord.t");
|
||||
const lf = B.newLabel("ord.f");
|
||||
const lj = B.newLabel("ord.j");
|
||||
B.condBr(truthy, lt, lf);
|
||||
B.startBlock(lt);
|
||||
if (e.retag !== undefined) {
|
||||
// Retagged shape: the whole box goes to the union→union helper,
|
||||
// which CONSUMES it (callees own their params) — no release on
|
||||
// this side. The store precedes the pending check so an unwind
|
||||
// leaves only the NULL dummy behind, in a slot nothing reads.
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ${ty} @${host.callTarget(e.retag)}(${host.llType(e.left.type)} ${l.name})`);
|
||||
B.line(`store ${ty} ${t}, ptr ${slot}`);
|
||||
if (host.mayThrow.has(e.retag)) host.emitPendingCheck();
|
||||
} else if (host.nullableUnions.single(e.left.type.unionId)) {
|
||||
// The owned nullable pointer IS the owned arm value.
|
||||
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
|
||||
} else {
|
||||
B.line(`store ${ty} ${host.unionExtract(l.name, e.left.type.unionId, e.type)}, ptr ${slot}`);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
}
|
||||
B.br(lj);
|
||||
B.startBlock(lf);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
function emitNullish(host: LlvmEmitterContext, e: ExprOf<"nullish">): LlValue {
|
||||
const B = host.B;
|
||||
// `a ?? b`: logical's move/release dance with the left's runtime
|
||||
// TAG against its unit arms as the test. Pass-through shape: the
|
||||
// result IS the left box. Narrowed shape: the single non-unit
|
||||
// arm's payload extracts (+1 for ref kinds) and the box releases.
|
||||
|
||||
if (e.left.type.kind === "dyn") {
|
||||
// The native checked-value kind decides whether the left is
|
||||
// nullish; the right runs lazily in its branch.
|
||||
const l = host.emitExpr(e.left);
|
||||
host.toFrame(l);
|
||||
host.declare(`declare zeroext i1 @scr_dyn_is_nullish(ptr)`);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
const isN = B.tmp();
|
||||
B.line(`${isN} = call zeroext i1 @scr_dyn_is_nullish(ptr ${l.name})`);
|
||||
const lu = B.newLabel("nuld.u");
|
||||
const lv = B.newLabel("nuld.v");
|
||||
const lj = B.newLabel("nuld.j");
|
||||
B.condBr(isN, lu, lv);
|
||||
B.startBlock(lu);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lv);
|
||||
B.line(`store ptr ${l.name}, ptr ${slot}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
if (e.left.type.kind !== "union")
|
||||
throw new InternalCompilerError("llvm emitter bug: nullish left is not a union");
|
||||
const def = host.unionsById.get(e.left.type.unionId);
|
||||
if (!def)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: nullish of unknown union ${e.left.type.unionId}`,
|
||||
);
|
||||
const unitTags = def.arms.flatMap((a, i) => (isUnitType(a) ? [i] : []));
|
||||
if (unitTags.length === 0)
|
||||
throw new InternalCompilerError("llvm emitter bug: nullish union lacks unit arms");
|
||||
const fused = emitNullishOptionalChain(host, e);
|
||||
if (fused) return fused;
|
||||
if (
|
||||
e.left.kind === "recordKeyGet" &&
|
||||
e.left.missNaN !== true &&
|
||||
typeEquals(e.type, e.left.type) &&
|
||||
storesNoUnit(host, e.left)
|
||||
)
|
||||
return emitRecordKeyGetOr(host, e.left, e.right);
|
||||
if (
|
||||
!typeEquals(e.type, e.left.type) &&
|
||||
(e.left.kind === "varRef" ||
|
||||
host.isStackUnionSource(e.left) ||
|
||||
host.nullableFieldGet(e.left) !== null)
|
||||
) {
|
||||
// Narrowed shape over a binding or a stack box (`arr[i] ?? d`,
|
||||
// `map.get(k) ?? d`): test the tag and extract the payload (+1).
|
||||
// The box is never retained or released. A binding is borrowed:
|
||||
// no code runs between its read and the extraction, and the
|
||||
// default never touches it. Bindings proven projection-only may
|
||||
// hold stack boxes, which must never reach RC entry points.
|
||||
const l = host.emitUnionProjection(e.left);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const isUnit = host.tagInSet(l.name, def.id, unitTags);
|
||||
const lu = B.newLabel("nul.u");
|
||||
const lv = B.newLabel("nul.v");
|
||||
const lj = B.newLabel("nul.j");
|
||||
B.condBr(isUnit, lu, lv);
|
||||
B.startBlock(lu);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lv);
|
||||
B.line(`store ${ty} ${host.unionExtract(l.name, def.id, e.type)}, ptr ${slot}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
const l = host.emitExpr(e.left);
|
||||
host.toFrame(l);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const isUnit = host.tagInSet(l.name, def.id, unitTags);
|
||||
const lu = B.newLabel("nul.u");
|
||||
const lv = B.newLabel("nul.v");
|
||||
const lj = B.newLabel("nul.j");
|
||||
B.condBr(isUnit, lu, lv);
|
||||
B.startBlock(lu);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lv);
|
||||
if (typeEquals(e.type, e.left.type) || host.nullableUnions.single(def.id)) {
|
||||
// Pass-through, or a nullable pointer that IS its owned arm value.
|
||||
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
|
||||
} else {
|
||||
B.line(`store ${ty} ${host.unionExtract(l.name, def.id, e.type)}, ptr ${slot}`);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
}
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
/** The receiver half of a union-receiver `a?.b`. The receiver evaluates
|
||||
* once; `isUnit` tests its unit arms, and `bindPresent` fills the chain's
|
||||
* bind slot with the narrowed payload for the body to read through
|
||||
|
||||
@@ -1080,16 +1080,21 @@ export function emitContainerExpr(
|
||||
B.line(`${fits} = icmp ule ${host.sizeType} ${capacity}, ${SMALL_ARRAY_SLOTS}`);
|
||||
B.condBr(fits, smallLabel, sizedLabel);
|
||||
B.startBlock(smallLabel);
|
||||
B.line(`${smallArr} = ${arrNewCall(host.shapeHost, elem, "0", true)}`);
|
||||
B.line(`${smallArr} = ${arrNewCall(host.shapeHost, elem, "0", true, host.arenaSite(e))}`);
|
||||
B.br(joined);
|
||||
B.startBlock(sizedLabel);
|
||||
B.line(`${sizedArr} = ${arrNewCall(host.shapeHost, elem, capacity)}`);
|
||||
B.line(
|
||||
`${sizedArr} = ${arrNewCall(host.shapeHost, elem, capacity, false, host.arenaSite(e))}`,
|
||||
);
|
||||
B.br(joined);
|
||||
B.startBlock(joined);
|
||||
B.line(
|
||||
`${arr} = phi ptr [ ${smallArr}, %${smallLabel} ], [ ${sizedArr}, %${sizedLabel} ]`,
|
||||
);
|
||||
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
} else
|
||||
B.line(
|
||||
`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small, host.arenaSite(e))}`,
|
||||
);
|
||||
}
|
||||
const out = host.own({ name: arr, type: e.type });
|
||||
const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem);
|
||||
@@ -1362,7 +1367,7 @@ export function emitRecordExpr(
|
||||
throw new InternalCompilerError("llvm emitter bug: recordLit of non-record type");
|
||||
const shapeId = e.type.shapeId;
|
||||
const rec = B.tmp();
|
||||
B.line(`${rec} = call ptr @${mangleRecordNew(shapeId)}()`);
|
||||
B.line(`${rec} = call ptr @${host.allocSym(mangleRecordNew(shapeId), e)}()`);
|
||||
const out = host.own({ name: rec, type: e.type });
|
||||
// A null-prototype record: SCR_MAP_NULL_PROTO on the fresh map.
|
||||
if (e.nullProto) B.line(`store i8 1, ptr ${host.recordMapFlagsPtr(rec, shapeId)}`);
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
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 type { IrExpr, IrModule, IrType } from "../../ir/ir.js";
|
||||
import { analyzeClosedGroups, analyzeRegionGroups } from "./region-groups.js";
|
||||
import { children, type Node } from "./region-ir.js";
|
||||
|
||||
async function moduleFor(source: string): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-region-groups-"));
|
||||
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"));
|
||||
return deserializeModule(await readFile(output, "utf8"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The calls of `callee` in the module, in order. */
|
||||
function callsOf(mod: IrModule, callee: string): IrExpr[] {
|
||||
const out: IrExpr[] = [];
|
||||
const visit = (node: Node): void => {
|
||||
if (node.kind === "call" && node.callee === callee) out.push(node);
|
||||
const kids: Node[] = [];
|
||||
children(node, kids);
|
||||
for (const k of kids) visit(k);
|
||||
};
|
||||
for (const fn of mod.functions) for (const s of fn.body) visit(s);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Every object and record is an arena type here, and every union a nullable
|
||||
// pointer to one (the emitter decides both from layouts).
|
||||
const everyObject = (t: IrType): boolean =>
|
||||
t.kind === "object" || t.kind === "record" || t.kind === "union";
|
||||
|
||||
const TREE = `
|
||||
class Node {
|
||||
left: Node | null;
|
||||
right: Node | null;
|
||||
constructor(left: Node | null, right: Node | null) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
count(): number {
|
||||
return 1 + (this.left === null ? 0 : this.left.count()) + (this.right === null ? 0 : this.right.count());
|
||||
}
|
||||
}
|
||||
function tree(depth: number): Node {
|
||||
return depth === 0 ? new Node(null, null) : new Node(tree(depth - 1), tree(depth - 1));
|
||||
}
|
||||
`;
|
||||
|
||||
test("a builder whose result its caller only reads is a closed root", async () => {
|
||||
const mod = await moduleFor(`${TREE}
|
||||
let total = 0;
|
||||
for (let d = 0; d < 4; d++) total += tree(d).count();
|
||||
console.log(total);
|
||||
`);
|
||||
const groups = analyzeRegionGroups(mod);
|
||||
expect(groups.fresh.has("tree")).toBe(true);
|
||||
const [root] = callsOf(mod, "tree").filter((c) => groups.roots.has(c));
|
||||
expect(root).toBeDefined();
|
||||
expect(groups.drops.get(root!)).toBeNull();
|
||||
expect(groups.reachable.has("tree")).toBe(true);
|
||||
expect(analyzeClosedGroups(mod, groups, everyObject).has(root!)).toBe(true);
|
||||
});
|
||||
|
||||
test("a result kept past its caller's frame is no root", async () => {
|
||||
const mod = await moduleFor(`${TREE}
|
||||
const kept: Node[] = [];
|
||||
for (let d = 0; d < 4; d++) kept.push(tree(d));
|
||||
console.log(kept.length);
|
||||
`);
|
||||
const groups = analyzeRegionGroups(mod);
|
||||
expect(callsOf(mod, "tree").some((c) => groups.roots.has(c))).toBe(false);
|
||||
});
|
||||
|
||||
test("a function that stores what it allocates in a global is not leak-free", async () => {
|
||||
const mod = await moduleFor(`
|
||||
interface P { x: number }
|
||||
const seen: P[] = [];
|
||||
function note(x: number): void {
|
||||
seen.push({ x });
|
||||
}
|
||||
function sum(n: number): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const p = { x: i };
|
||||
s += p.x;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
note(1);
|
||||
console.log(sum(3), seen.length);
|
||||
`);
|
||||
const groups = analyzeRegionGroups(mod);
|
||||
expect(groups.leakFree.has("note")).toBe(false);
|
||||
expect(callsOf(mod, "note").some((c) => groups.calls.has(c))).toBe(false);
|
||||
expect(groups.leakFree.has("sum")).toBe(true);
|
||||
});
|
||||
|
||||
test("a leak-free call that returns no reference is a call region", async () => {
|
||||
const mod = await moduleFor(`
|
||||
interface V { x: number; y: number }
|
||||
function add(a: V, b: V): V {
|
||||
return { x: a.x + b.x, y: a.y + b.y };
|
||||
}
|
||||
function walk(n: number): number {
|
||||
let at: V = { x: 0, y: 0 };
|
||||
for (let i = 0; i < n; i++) at = add(at, { x: i, y: 1 });
|
||||
return at.x + at.y;
|
||||
}
|
||||
console.log(walk(10));
|
||||
`);
|
||||
const groups = analyzeRegionGroups(mod);
|
||||
const [region] = callsOf(mod, "walk");
|
||||
expect(groups.calls.has(region!)).toBe(true);
|
||||
});
|
||||
|
||||
test("a graph that holds strings or outside objects is not closed", async () => {
|
||||
const mod = await moduleFor(`
|
||||
class Tag {
|
||||
name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
class Box {
|
||||
tag: Tag;
|
||||
owner: Tag | null;
|
||||
constructor(tag: Tag, owner: Tag | null) {
|
||||
this.tag = tag;
|
||||
this.owner = owner;
|
||||
}
|
||||
}
|
||||
const shared = new Tag("shared");
|
||||
function labelled(n: number): Box {
|
||||
let box = new Box(new Tag("t" + n), null);
|
||||
for (let i = 0; i < n; i++) box = new Box(new Tag("t" + i), null);
|
||||
return box;
|
||||
}
|
||||
function owned(n: number): Box {
|
||||
let box = new Box(new Tag("x"), shared);
|
||||
for (let i = 0; i < n; i++) box = new Box(new Tag("x"), shared);
|
||||
return box;
|
||||
}
|
||||
let n = 0;
|
||||
for (let i = 0; i < 3; i++) n += labelled(i).tag.name.length + owned(i).tag.name.length;
|
||||
console.log(n);
|
||||
`);
|
||||
const groups = analyzeRegionGroups(mod);
|
||||
const closed = analyzeClosedGroups(mod, groups, everyObject);
|
||||
const labelled = callsOf(mod, "labelled").filter((c) => groups.roots.has(c));
|
||||
const owned = callsOf(mod, "owned").filter((c) => groups.roots.has(c));
|
||||
expect(labelled.length).toBe(1);
|
||||
expect(owned.length).toBe(1);
|
||||
expect(closed.has(labelled[0]!)).toBe(false);
|
||||
expect(closed.has(owned[0]!)).toBe(false);
|
||||
});
|
||||
|
||||
test("a root bound in a loop body is dropped with that body's local", async () => {
|
||||
const mod = await moduleFor(`${TREE}
|
||||
function run(n: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = tree(i % 4);
|
||||
total += t.count();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
console.log(run(10));
|
||||
`);
|
||||
const groups = analyzeRegionGroups(mod);
|
||||
const [root] = callsOf(mod, "tree").filter((c) => groups.roots.has(c));
|
||||
const run = mod.functions.find((f) => f.name === "run")!;
|
||||
const local = groups.drops.get(root!);
|
||||
expect(run.locals.find((l) => l.id === local)?.name).toBe("t");
|
||||
});
|
||||
|
||||
test("a root whose value a callee writes into is not closed", async () => {
|
||||
const mod = await moduleFor(`${TREE}
|
||||
function graft(n: Node, extra: Node): void {
|
||||
n.left = extra;
|
||||
}
|
||||
const spare = new Node(null, null);
|
||||
let total = 0;
|
||||
for (let d = 0; d < 3; d++) {
|
||||
const t = tree(d);
|
||||
graft(t, spare);
|
||||
total += t.count();
|
||||
}
|
||||
console.log(total);
|
||||
`);
|
||||
const groups = analyzeRegionGroups(mod);
|
||||
const closed = analyzeClosedGroups(mod, groups, everyObject);
|
||||
for (const root of callsOf(mod, "tree").filter((c) => groups.roots.has(c)))
|
||||
expect(closed.has(root)).toBe(false);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,271 @@
|
||||
/* IR helpers shared by the region analyses (region-groups.ts): node
|
||||
* traversal, allocation kinds, and how a use of a value passes it on or
|
||||
* keeps it in the frame. */
|
||||
import type { IrExpr, IrStmt, IrType } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
|
||||
export type Node = IrExpr | IrStmt;
|
||||
|
||||
/** The kinds of heap allocation sites counted. */
|
||||
export const ALLOCATIONS = new Set([
|
||||
"arrayLit",
|
||||
"arrayNewLen",
|
||||
"mapNew",
|
||||
"setNew",
|
||||
"new",
|
||||
"recordLit",
|
||||
"recordClone",
|
||||
"closure",
|
||||
]);
|
||||
|
||||
/** Array methods that return a fresh array without keeping the receiver. */
|
||||
export const FRESH_ARRAY_METHODS = new Set([
|
||||
"slice",
|
||||
"concatSpread",
|
||||
"toReversed",
|
||||
"toSpliced",
|
||||
"with",
|
||||
"withUndefined",
|
||||
"flatCopy",
|
||||
"flatOne",
|
||||
"toSortedPrimitive",
|
||||
"splice",
|
||||
]);
|
||||
/** Array methods that return the receiver itself. */
|
||||
export const RECEIVER_RESULT = new Set([
|
||||
"reverse",
|
||||
"fill",
|
||||
"fillUndefined",
|
||||
"copyWithin",
|
||||
"sortPrimitive",
|
||||
]);
|
||||
/** Array methods whose arguments are only read (compared, copied from). */
|
||||
export const READ_ARGS = new Set([
|
||||
"includes",
|
||||
"indexOf",
|
||||
"indexEq",
|
||||
"getNumber",
|
||||
"join",
|
||||
"slice",
|
||||
"concatSpread",
|
||||
"pushSpread",
|
||||
"unshiftSpread",
|
||||
"nextPresent",
|
||||
"length",
|
||||
]);
|
||||
|
||||
export function refType(t: IrType): boolean {
|
||||
return (
|
||||
t.kind === "array" ||
|
||||
t.kind === "map" ||
|
||||
t.kind === "set" ||
|
||||
t.kind === "object" ||
|
||||
t.kind === "record" ||
|
||||
t.kind === "func" ||
|
||||
t.kind === "union"
|
||||
);
|
||||
}
|
||||
|
||||
export function children(node: Node, out: Node[]): void {
|
||||
const push = (child: Node): boolean => {
|
||||
out.push(child);
|
||||
return true;
|
||||
};
|
||||
if ("type" in node && (node as IrExpr).type !== undefined && isExpr(node))
|
||||
everyExprChild(node as IrExpr, push, push);
|
||||
else everyStmtChild(node as IrStmt, push, push);
|
||||
}
|
||||
|
||||
const STMT_KINDS = new Set([
|
||||
"varDecl",
|
||||
"assign",
|
||||
"exprStmt",
|
||||
"if",
|
||||
"while",
|
||||
"doWhile",
|
||||
"switch",
|
||||
"for",
|
||||
"arraySet",
|
||||
"arraySetLength",
|
||||
"arraySetUndefined",
|
||||
"arrayDelete",
|
||||
"bytesSet",
|
||||
"forOf",
|
||||
"return",
|
||||
"fieldSet",
|
||||
"recordSet",
|
||||
"recordKeySet",
|
||||
"recordKeyDelete",
|
||||
"break",
|
||||
"continue",
|
||||
"block",
|
||||
"throw",
|
||||
"runtimeFence",
|
||||
"rethrow",
|
||||
"tryCatch",
|
||||
]);
|
||||
export function isExpr(node: Node): node is IrExpr {
|
||||
return !STMT_KINDS.has(node.kind);
|
||||
}
|
||||
|
||||
/** Whether `a` and `b` are the same node. */
|
||||
export function same(a: Node | undefined, b: Node): boolean {
|
||||
return a !== undefined && a === b;
|
||||
}
|
||||
|
||||
/** Whether `parent` passes `child`'s value on as its own value (a branch of
|
||||
* a conditional, a union wrap or narrow, a class up- or downcast, the
|
||||
* result of a sequence), so the value's fate is the parent's. */
|
||||
export function transparent(child: Node, parent: Node | undefined): boolean {
|
||||
if (parent === undefined) return false;
|
||||
switch (parent.kind) {
|
||||
case "ternary":
|
||||
return same(parent.then, child) || same(parent.else_, child);
|
||||
case "nullish":
|
||||
case "logical":
|
||||
return true;
|
||||
case "seqExpr":
|
||||
return same(parent.result, child);
|
||||
case "unionWrap":
|
||||
case "unionRetype":
|
||||
case "unionNarrow":
|
||||
case "upcast":
|
||||
case "downcast":
|
||||
return same(parent.value, child);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a use of a local's value as `child` of `parent` keeps it in the
|
||||
* frame (given the current parameter summaries). */
|
||||
export function keepsInFrame(
|
||||
child: Node,
|
||||
parent: Node | undefined,
|
||||
summaries: ReadonlyMap<string, Set<number>>,
|
||||
): boolean {
|
||||
if (parent === undefined) return false;
|
||||
switch (parent.kind) {
|
||||
case "exprStmt":
|
||||
return true;
|
||||
case "arrayGet":
|
||||
case "arrayHas":
|
||||
case "arrayState":
|
||||
case "arraySetUndefined":
|
||||
case "arrayDelete":
|
||||
case "arraySetLength":
|
||||
case "arraySet":
|
||||
return same(parent.arr, child);
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
case "recordKeyGet":
|
||||
case "recordHas":
|
||||
case "recordOvfHas":
|
||||
case "recordOvfKeys":
|
||||
case "fieldSet":
|
||||
case "recordSet":
|
||||
case "recordKeySet":
|
||||
case "recordKeyDelete":
|
||||
return same(parent.obj, child);
|
||||
case "forOf":
|
||||
return same(parent.iterable, child);
|
||||
case "arrIntrinsic":
|
||||
if (same(parent.receiver, child)) return !RECEIVER_RESULT.has(parent.method);
|
||||
return READ_ARGS.has(parent.method);
|
||||
case "mapIntrinsic":
|
||||
if (same(parent.receiver, child)) return true;
|
||||
return parent.method === "get" || parent.method === "has" || parent.method === "delete";
|
||||
case "setIntrinsic":
|
||||
if (same(parent.receiver, child)) return true;
|
||||
return parent.method === "has" || parent.method === "delete";
|
||||
case "bin":
|
||||
return parent.op === "===" || parent.op === "!==";
|
||||
case "toBool":
|
||||
case "unary":
|
||||
case "unionIsTag":
|
||||
case "unionEq":
|
||||
return true;
|
||||
case "unionDisc":
|
||||
case "unionKeyGet":
|
||||
return same(parent.value, child);
|
||||
case "callValue":
|
||||
return same(parent.callee, child);
|
||||
case "call": {
|
||||
const index = indexOfNode(parent.args, child);
|
||||
return index >= 0 && summaries.get(parent.callee)?.has(index) === true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The index of `node` in `list`, or -1. */
|
||||
export function indexOfNode(list: readonly IrExpr[], node: Node): number {
|
||||
for (let i = 0; i < list.length; i++) if (same(list[i], node)) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** The arguments of a node that takes them (calls, constructions,
|
||||
* intrinsics), or null. */
|
||||
export function argsOf(node: Node): readonly IrExpr[] | null {
|
||||
switch (node.kind) {
|
||||
case "call":
|
||||
case "callValue":
|
||||
case "virtualCall":
|
||||
case "new":
|
||||
case "newValue":
|
||||
case "ffiCall":
|
||||
case "dynCall":
|
||||
case "dynInvoke":
|
||||
case "intrinsic":
|
||||
case "libCall":
|
||||
case "arrIntrinsic":
|
||||
case "bytesIntrinsic":
|
||||
case "mapIntrinsic":
|
||||
case "setIntrinsic":
|
||||
case "strIntrinsic":
|
||||
case "regexIntrinsic":
|
||||
return node.args;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const LOOP_STMTS = new Set(["while", "doWhile", "for", "forOf"]);
|
||||
|
||||
/** Calls `visit(node, parent, loops)` for every node of `body`, parents
|
||||
* before their children (siblings in no set order), where `loops` counts
|
||||
* the loops around `node`. Allocates nothing per node. */
|
||||
export function forEachNode(
|
||||
body: readonly IrStmt[],
|
||||
visit: (node: Node, parent: Node | undefined, loops: number) => void,
|
||||
): void {
|
||||
const nodes: Node[] = [];
|
||||
const parents: (Node | undefined)[] = [];
|
||||
const depths: number[] = [];
|
||||
let parent: Node | undefined = undefined;
|
||||
let depth = 0;
|
||||
const pushExpr = (child: IrExpr): boolean => {
|
||||
nodes.push(child);
|
||||
parents.push(parent);
|
||||
depths.push(depth);
|
||||
return true;
|
||||
};
|
||||
const pushStmt = (child: IrStmt): boolean => {
|
||||
nodes.push(child);
|
||||
parents.push(parent);
|
||||
depths.push(depth);
|
||||
return true;
|
||||
};
|
||||
for (const st of body) pushStmt(st);
|
||||
while (nodes.length > 0) {
|
||||
const node = nodes.pop()!;
|
||||
const above = parents.pop();
|
||||
const loops = depths.pop()!;
|
||||
visit(node, above, loops);
|
||||
parent = node;
|
||||
depth = loops + (LOOP_STMTS.has(node.kind) ? 1 : 0);
|
||||
if (isExpr(node)) everyExprChild(node, pushExpr, pushStmt);
|
||||
else everyStmtChild(node, pushExpr, pushStmt);
|
||||
}
|
||||
}
|
||||
@@ -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 type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import { analyzeRegionGroups } from "./region-groups.js";
|
||||
import { children, type Node } from "./region-ir.js";
|
||||
import { planRegionSites } from "./region-sites.js";
|
||||
|
||||
async function moduleFor(source: string): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-region-sites-"));
|
||||
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"));
|
||||
return deserializeModule(await readFile(output, "utf8"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The nodes in function `fn` matching `pick`, in order. */
|
||||
function nodesIn(mod: IrModule, fn: string, pick: (e: IrExpr) => boolean): IrExpr[] {
|
||||
const out: IrExpr[] = [];
|
||||
const visit = (node: Node): void => {
|
||||
if (pick(node as IrExpr)) out.push(node as IrExpr);
|
||||
const kids: Node[] = [];
|
||||
children(node, kids);
|
||||
for (const k of kids) visit(k);
|
||||
};
|
||||
for (const s of mod.functions.find((f) => f.name === fn)!.body) visit(s);
|
||||
return out;
|
||||
}
|
||||
|
||||
const calls = (callee: string) => (e: IrExpr) => e.kind === "call" && e.callee === callee;
|
||||
const records = (e: IrExpr) => e.kind === "recordLit";
|
||||
|
||||
function plan(mod: IrModule) {
|
||||
const groups = analyzeRegionGroups(mod);
|
||||
const sites = planRegionSites(mod, groups);
|
||||
return { groups, sites };
|
||||
}
|
||||
|
||||
const TREE = `
|
||||
class Node {
|
||||
left: Node | null;
|
||||
right: Node | null;
|
||||
constructor(left: Node | null, right: Node | null) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
count(): number {
|
||||
return 1 + (this.left === null ? 0 : this.left.count()) + (this.right === null ? 0 : this.right.count());
|
||||
}
|
||||
}
|
||||
function tree(depth: number): Node {
|
||||
return depth === 0 ? new Node(null, null) : new Node(tree(depth - 1), tree(depth - 1));
|
||||
}
|
||||
`;
|
||||
|
||||
test("a recursive builder's root takes an arena for every node", async () => {
|
||||
const mod = await moduleFor(`${TREE}
|
||||
let total = 0;
|
||||
for (let d = 0; d < 4; d++) total += tree(d).count();
|
||||
console.log(total);
|
||||
`);
|
||||
const { groups, sites } = plan(mod);
|
||||
const roots = nodesIn(mod, "%init.0", calls("tree")).filter((c) => groups.roots.has(c));
|
||||
expect(roots.length).toBe(1);
|
||||
for (const n of nodesIn(mod, "tree", (e) => e.kind === "new"))
|
||||
expect(sites.arenaSites.has(n)).toBe(true);
|
||||
for (const c of nodesIn(mod, "tree", calls("tree"))) expect(sites.arenaSites.has(c)).toBe(true);
|
||||
});
|
||||
|
||||
test("a builder of a bounded number of objects is no root", async () => {
|
||||
const mod = await moduleFor(`
|
||||
interface V { x: number; y: number }
|
||||
function add(a: V, b: V): V {
|
||||
return { x: a.x + b.x, y: a.y + b.y };
|
||||
}
|
||||
let at: V = { x: 0, y: 0 };
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const step = add(at, { x: i, y: 1 });
|
||||
sum += step.x;
|
||||
}
|
||||
console.log(sum, at.y);
|
||||
`);
|
||||
const { groups } = plan(mod);
|
||||
expect(groups.roots.size).toBe(0);
|
||||
expect(groups.calls.size).toBe(0);
|
||||
expect(groups.reachable.size).toBe(0);
|
||||
});
|
||||
|
||||
test("a list a builder appends to in a loop takes the arena", async () => {
|
||||
const mod = await moduleFor(`
|
||||
interface Row { id: number; score: number }
|
||||
function rows(n: number): Row[] {
|
||||
const out: Row[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const row: Row = { id: i, score: i * 2 };
|
||||
out.push(row);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
let total = 0;
|
||||
for (let k = 0; k < 3; k++) total += rows(10 + k).length;
|
||||
console.log(total);
|
||||
`);
|
||||
const { groups, sites } = plan(mod);
|
||||
expect(nodesIn(mod, "%init.0", calls("rows")).some((c) => groups.roots.has(c))).toBe(true);
|
||||
const [row] = nodesIn(mod, "rows", records);
|
||||
expect(sites.arenaSites.has(row!)).toBe(true);
|
||||
const [out] = nodesIn(mod, "rows", (e) => e.kind === "arrayLit");
|
||||
expect(sites.arenaSites.has(out!)).toBe(true);
|
||||
});
|
||||
|
||||
test("values that replace elements or die in the loop take the heap", async () => {
|
||||
const mod = await moduleFor(`
|
||||
interface V { x: number; y: number }
|
||||
function vec(x: number, y: number): V {
|
||||
return { x, y };
|
||||
}
|
||||
function add(a: V, b: V): V {
|
||||
return { x: a.x + b.x, y: a.y + b.y };
|
||||
}
|
||||
function simulate(n: number, steps: number): number {
|
||||
const points: V[] = [];
|
||||
for (let i = 0; i < n; i++) points.push(vec(i, i));
|
||||
for (let s = 0; s < steps; s++)
|
||||
for (let i = 0; i < n; i++) points[i] = add(points[i]!, vec(0, 1));
|
||||
let sum = 0;
|
||||
for (const p of points) sum += p.x + p.y;
|
||||
return sum;
|
||||
}
|
||||
console.log(simulate(100, 10));
|
||||
`);
|
||||
const { groups, sites } = plan(mod);
|
||||
// The list's elements are replaced, so it keeps nothing: no group grows.
|
||||
expect(groups.calls.size).toBe(0);
|
||||
expect(groups.roots.size).toBe(0);
|
||||
expect(sites.arenaSites.size).toBe(0);
|
||||
});
|
||||
|
||||
test("a value reassigned in a loop and returned takes the heap", async () => {
|
||||
const mod = await moduleFor(`
|
||||
class C {
|
||||
re: number;
|
||||
im: number;
|
||||
constructor(re: number, im: number) {
|
||||
this.re = re;
|
||||
this.im = im;
|
||||
}
|
||||
times(o: C): C {
|
||||
return new C(this.re * o.re - this.im * o.im, this.re * o.im + this.im * o.re);
|
||||
}
|
||||
}
|
||||
function power(c: C, n: number): C {
|
||||
let z = new C(1, 0);
|
||||
for (let i = 0; i < n; i++) z = z.times(c);
|
||||
return z;
|
||||
}
|
||||
let s = 0;
|
||||
for (let k = 0; k < 5; k++) s += power(new C(0.5, 0.5), k).re;
|
||||
console.log(s);
|
||||
`);
|
||||
const { groups } = plan(mod);
|
||||
expect(nodesIn(mod, "%init.0", calls("power")).some((c) => groups.roots.has(c))).toBe(false);
|
||||
});
|
||||
|
||||
test("calls in an arena-aware function whose values die take the heap", async () => {
|
||||
const mod = await moduleFor(`
|
||||
interface P { x: number; next: P | null }
|
||||
function weight(p: P): number {
|
||||
return p.x * 2;
|
||||
}
|
||||
function probe(x: number): P {
|
||||
return { x, next: null };
|
||||
}
|
||||
function chain(n: number): P {
|
||||
let head: P = { x: 0, next: null };
|
||||
const first = head;
|
||||
for (let i = 1; i < n; i++) {
|
||||
const node: P = { x: weight(probe(i)), next: null };
|
||||
head.next = node;
|
||||
head = node;
|
||||
}
|
||||
return first;
|
||||
}
|
||||
let total = 0;
|
||||
for (let k = 0; k < 3; k++) total += chain(5).x;
|
||||
console.log(total);
|
||||
`);
|
||||
const { groups, sites } = plan(mod);
|
||||
expect(nodesIn(mod, "%init.0", calls("chain")).some((c) => groups.roots.has(c))).toBe(true);
|
||||
const [probe] = nodesIn(mod, "chain", calls("probe"));
|
||||
expect(sites.arenaSites.has(probe!)).toBe(false);
|
||||
expect(sites.heapCalls.has(probe!)).toBe(true);
|
||||
for (const node of nodesIn(mod, "chain", records)) expect(sites.arenaSites.has(node)).toBe(true);
|
||||
});
|
||||
|
||||
test("a root assigned in a loop to a local declared outside it is no root", async () => {
|
||||
const mod = await moduleFor(`${TREE}
|
||||
function run(n: number): number {
|
||||
let t = tree(1);
|
||||
let total = t.count();
|
||||
for (let d = 0; d < n; d++) {
|
||||
t = tree(d);
|
||||
total += t.count();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
console.log(run(6));
|
||||
`);
|
||||
const { groups } = plan(mod);
|
||||
const [first, inLoop] = nodesIn(mod, "run", calls("tree"));
|
||||
expect(groups.roots.has(first!)).toBe(true);
|
||||
expect(groups.roots.has(inLoop!)).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,349 @@
|
||||
/* Which allocations of a region group take its arena (arenas.ts), and
|
||||
* which roots and call regions are worth one.
|
||||
*
|
||||
* An arena pays off for objects that live until the group is dropped: it
|
||||
* saves their frees and teardown, but holds their memory until the drop.
|
||||
* A temporary that dies early is cheaper on the heap, which reuses its
|
||||
* memory at once. So in an arena-aware function an allocation takes the
|
||||
* arena only when its value is kept by the group (its fate is the arena):
|
||||
* - it is returned;
|
||||
* - it is content of a new value that is kept;
|
||||
* - it is stored, other than by an element write, into the graph of a
|
||||
* local that is kept (`out.push(row)`, `last.next = member`);
|
||||
* - it is bound to a local that is kept.
|
||||
* A local is kept when its value is returned or stored or put into
|
||||
* something kept; a constructor's `this` is kept (its object follows its
|
||||
* `new`); and in the callee of a call region, a local declared at the top
|
||||
* of the body lives until the region drops. An element write (`a[i] = v`)
|
||||
* usually replaces a value, so its value takes the heap.
|
||||
*
|
||||
* Every other allocation takes the heap, and so does everything a call
|
||||
* allocates when the call's value is not kept: the emitter suspends the
|
||||
* arena around such a call (heapCalls). Heap objects are always safe in a
|
||||
* group; only arena objects need the group's proof.
|
||||
*
|
||||
* A root or call region is worth an arena when its group grows: a kept
|
||||
* allocation runs in a loop, or a kept value comes from a recursive call
|
||||
* or from a call of a function whose group grows. Other groups are small,
|
||||
* and opening and dropping their arenas costs more than their frees. */
|
||||
import type { IrFunction, IrModule } from "../../ir/ir.js";
|
||||
import { contentOf, source, storeOf, storeTarget, type RegionGroups } from "./region-groups.js";
|
||||
import {
|
||||
argsOf,
|
||||
children,
|
||||
indexOfNode,
|
||||
isExpr,
|
||||
refType,
|
||||
same,
|
||||
transparent,
|
||||
type Node,
|
||||
} from "./region-ir.js";
|
||||
import { VirtualTargets } from "./reference-effects.js";
|
||||
|
||||
export interface RegionSites {
|
||||
/** Allocation sites (and calls) whose value the group keeps. */
|
||||
arenaSites: Set<Node>;
|
||||
/** Calls, constructions and closure calls in arena-aware functions whose
|
||||
* value the group does not keep and whose callee may allocate: the
|
||||
* arena is suspended while they run. */
|
||||
heapCalls: Set<Node>;
|
||||
}
|
||||
|
||||
const LOOPS = new Set(["while", "doWhile", "for", "forOf"]);
|
||||
|
||||
/** Collection methods that remove what their receiver holds. */
|
||||
const REMOVALS = new Set(["pop", "shift", "splice", "delete", "clear"]);
|
||||
|
||||
/** The local whose graph `node` drops values from (an element write, a
|
||||
* length write, a delete, a removing method), or null. */
|
||||
function removalTarget(node: Node): string | null {
|
||||
switch (node.kind) {
|
||||
case "arraySet":
|
||||
case "arraySetLength":
|
||||
case "arraySetUndefined":
|
||||
case "arrayDelete":
|
||||
return storeTarget(node.arr);
|
||||
case "recordKeyDelete":
|
||||
return storeTarget(node.obj);
|
||||
case "arrIntrinsic":
|
||||
case "mapIntrinsic":
|
||||
case "setIntrinsic":
|
||||
return REMOVALS.has(node.method) ? storeTarget(node.receiver) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** The functions a call-like node may run, or null when unknown. */
|
||||
function targetsOf(vt: VirtualTargets, node: Node): readonly string[] | null {
|
||||
if (node.kind === "call") return [node.callee];
|
||||
if (node.kind === "new") return [`%${node.className}.constructor`];
|
||||
if (node.kind === "virtualCall") {
|
||||
const t = vt.targets(node.className, node.method);
|
||||
return t !== null && t.length > 0 ? t : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const callLike = (node: Node): boolean =>
|
||||
node.kind === "call" ||
|
||||
node.kind === "new" ||
|
||||
node.kind === "virtualCall" ||
|
||||
node.kind === "callValue" ||
|
||||
node.kind === "newValue";
|
||||
|
||||
/** The functions that run inside the arenas of `roots`: what they call,
|
||||
* transitively, with the closures they pass. */
|
||||
export function reachableFrom(mod: IrModule, roots: Iterable<Node>): Set<string> {
|
||||
const fns = new Map(mod.functions.map((f) => [f.name, f]));
|
||||
const vt = new VirtualTargets(mod.classes ?? []);
|
||||
const reachable = new Set<string>();
|
||||
const queue: string[] = [];
|
||||
const visit = (node: Node): void => {
|
||||
const reach = (name: string): void => {
|
||||
if (fns.has(name) && !reachable.has(name)) {
|
||||
reachable.add(name);
|
||||
queue.push(name);
|
||||
}
|
||||
};
|
||||
if (node.kind === "closure") reach(node.fnName);
|
||||
else for (const t of targetsOf(vt, node) ?? []) reach(t);
|
||||
const kids: Node[] = [];
|
||||
children(node, kids);
|
||||
for (const k of kids) visit(k);
|
||||
};
|
||||
for (const r of roots) visit(r);
|
||||
while (queue.length > 0) for (const st of fns.get(queue.pop()!)!.body) visit(st);
|
||||
return reachable;
|
||||
}
|
||||
|
||||
interface FnSites {
|
||||
/** Kept call-like nodes and allocations, with whether each is in a loop. */
|
||||
kept: Map<Node, boolean>;
|
||||
/** Call-like nodes whose value is not kept. */
|
||||
dropped: Node[];
|
||||
}
|
||||
|
||||
/** Classifies the allocations and calls of `fn`. */
|
||||
function classify(fn: IrFunction, topLocalsKept: boolean): FnSites {
|
||||
const locals = new Set(fn.locals.map((l) => l.id));
|
||||
// Alias classes: a local bound to another local shares its fate.
|
||||
const parent = new Map<string, string>();
|
||||
const find = (id: string): string => {
|
||||
let r = id;
|
||||
while (parent.has(r)) r = parent.get(r)!;
|
||||
return r;
|
||||
};
|
||||
const unite = (a: string, b: string): void => {
|
||||
const ra = find(a),
|
||||
rb = find(b);
|
||||
if (ra !== rb) parent.set(ra, rb);
|
||||
};
|
||||
const kept = new Set<string>();
|
||||
const keep = (id: string): boolean => {
|
||||
const r = find(id);
|
||||
if (kept.has(r)) return false;
|
||||
kept.add(r);
|
||||
return true;
|
||||
};
|
||||
const isKept = (id: string): boolean => kept.has(find(id));
|
||||
// Kept from the start (applied once the alias classes are known).
|
||||
const seeds: string[] = [];
|
||||
if (fn.name.endsWith(".constructor") && fn.params.length > 0) seeds.push(fn.params[0]!.localId);
|
||||
if (topLocalsKept) {
|
||||
const types = new Map(fn.locals.map((l) => [l.id, l.type]));
|
||||
for (const st of fn.body)
|
||||
if (st.kind === "varDecl" && refType(types.get(st.localId) ?? { kind: "void" }))
|
||||
seeds.push(st.localId);
|
||||
}
|
||||
|
||||
// Uses of locals and of value-producing nodes, recorded once.
|
||||
type Fate =
|
||||
| { kind: "kept" }
|
||||
| { kind: "local"; id: string }
|
||||
| { kind: "target"; id: string }
|
||||
| { kind: "node"; node: Node }
|
||||
| { kind: "heap" };
|
||||
const fateOf = (node: Node, parents: readonly Node[]): Fate => {
|
||||
let value: Node = node;
|
||||
let at = parents.length - 1;
|
||||
while (at >= 0 && transparent(value, parents[at])) value = parents[at--]!;
|
||||
const p = at >= 0 ? parents[at] : undefined;
|
||||
if (p === undefined) return { kind: "heap" };
|
||||
if (p.kind === "return") return { kind: "kept" };
|
||||
if (
|
||||
(p.kind === "varDecl" && p.init !== null && same(p.init, value)) ||
|
||||
(p.kind === "assign" && same(p.value, value))
|
||||
) {
|
||||
const id = p.localId;
|
||||
// A local assigned in a loop usually drops its old value.
|
||||
if (!locals.has(id) || (p.kind === "assign" && depth > 0)) return { kind: "heap" };
|
||||
return { kind: "local", id };
|
||||
}
|
||||
const target = storeOf(value, p);
|
||||
if (target !== undefined) {
|
||||
if (p.kind === "arraySet") return { kind: "heap" };
|
||||
const id = storeTarget(target);
|
||||
return id !== null && locals.has(id) ? { kind: "target", id } : { kind: "heap" };
|
||||
}
|
||||
if (isExpr(p) && contentOf(value, p)) return { kind: "node", node: p };
|
||||
// An argument of a call that returns a reference follows the call's
|
||||
// value: the callee may put it into its result.
|
||||
const args = argsOf(p);
|
||||
if (
|
||||
callLike(p) &&
|
||||
isExpr(p) &&
|
||||
refType(p.type) &&
|
||||
args !== null &&
|
||||
indexOfNode(args, value) >= 0
|
||||
)
|
||||
return { kind: "node", node: p };
|
||||
return { kind: "heap" };
|
||||
};
|
||||
|
||||
// Lossy locals: their graphs drop values (element writes, removals), so
|
||||
// what is stored into them is not kept.
|
||||
const lossy = new Set<string>();
|
||||
const fates = new Map<Node, Fate>();
|
||||
const loops = new Map<Node, boolean>();
|
||||
const localUses: [string, Fate][] = [];
|
||||
const parents: Node[] = [];
|
||||
let depth = 0;
|
||||
const walk = (node: Node): void => {
|
||||
const removal = removalTarget(node);
|
||||
if (removal !== null) lossy.add(removal);
|
||||
if (source(node) || callLike(node)) {
|
||||
fates.set(node, fateOf(node, parents));
|
||||
loops.set(node, depth > 0);
|
||||
} else if (node.kind === "varRef" && locals.has(node.localId) && refType(node.type)) {
|
||||
const f = fateOf(node, parents);
|
||||
if (f.kind !== "heap") localUses.push([node.localId, f]);
|
||||
}
|
||||
const loop = LOOPS.has(node.kind);
|
||||
if (loop) depth++;
|
||||
const kids: Node[] = [];
|
||||
children(node, kids);
|
||||
parents.push(node);
|
||||
for (const k of kids) walk(k);
|
||||
parents.pop();
|
||||
if (loop) depth--;
|
||||
};
|
||||
for (const st of fn.body) walk(st);
|
||||
|
||||
// A local bound to another local joins its class.
|
||||
for (const [id, f] of localUses) if (f.kind === "local") unite(id, f.id);
|
||||
for (const id of [...lossy]) lossy.add(find(id));
|
||||
for (const id of seeds) keep(id);
|
||||
const keptFate = (f: Fate, seen = 0): boolean => {
|
||||
if (seen > 64) return false;
|
||||
switch (f.kind) {
|
||||
case "kept":
|
||||
return true;
|
||||
case "local":
|
||||
return isKept(f.id);
|
||||
case "target":
|
||||
return isKept(f.id) && !lossy.has(find(f.id));
|
||||
case "node": {
|
||||
const g = fates.get(f.node);
|
||||
return g !== undefined && keptFate(g, seen + 1);
|
||||
}
|
||||
case "heap":
|
||||
return false;
|
||||
}
|
||||
};
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const [id, f] of localUses)
|
||||
if (f.kind !== "local" && keptFate(f) && keep(id)) changed = true;
|
||||
}
|
||||
const out: FnSites = { kept: new Map(), dropped: [] };
|
||||
for (const [e, f] of fates)
|
||||
if (keptFate(f)) out.kept.set(e, loops.get(e)!);
|
||||
else if (callLike(e)) out.dropped.push(e);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrows `groups` to the roots and call regions worth an arena (with
|
||||
* `reachable` recomputed for them) and classifies the allocations of the
|
||||
* functions that run in their arenas.
|
||||
*/
|
||||
export function planRegionSites(mod: IrModule, groups: RegionGroups): RegionSites {
|
||||
const fns = new Map(mod.functions.map((f) => [f.name, f]));
|
||||
const vt = new VirtualTargets(mod.classes ?? []);
|
||||
const regionCallees = new Set<string>();
|
||||
for (const c of groups.calls) if (c.kind === "call") regionCallees.add(c.callee);
|
||||
const sites = new Map<string, FnSites>();
|
||||
for (const name of groups.reachable) {
|
||||
const fn = fns.get(name);
|
||||
if (fn !== undefined) sites.set(name, classify(fn, regionCallees.has(name)));
|
||||
}
|
||||
// Which functions reach which (for recursion), over the reachable ones.
|
||||
const calleesOf = (name: string): Set<string> => {
|
||||
const out = new Set<string>();
|
||||
const s = sites.get(name);
|
||||
if (s === undefined) return out;
|
||||
for (const e of [...s.kept.keys(), ...s.dropped])
|
||||
for (const t of targetsOf(vt, e) ?? []) if (sites.has(t)) out.add(t);
|
||||
return out;
|
||||
};
|
||||
const edges = new Map([...sites.keys()].map((n) => [n, calleesOf(n)]));
|
||||
const reaches = (from: string, to: string): boolean => {
|
||||
const seen = new Set<string>();
|
||||
const stack = [from];
|
||||
while (stack.length > 0) {
|
||||
const n = stack.pop()!;
|
||||
if (n === to) return true;
|
||||
if (seen.has(n)) continue;
|
||||
seen.add(n);
|
||||
for (const m of edges.get(n) ?? new Set<string>()) stack.push(m);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const grows = new Set<string>();
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const [name, s] of sites) {
|
||||
if (grows.has(name)) continue;
|
||||
let g = false;
|
||||
for (const [e, inLoop] of s.kept) {
|
||||
if (inLoop) g = true;
|
||||
for (const t of targetsOf(vt, e) ?? []) if (grows.has(t) || reaches(t, name)) g = true;
|
||||
if (g) break;
|
||||
}
|
||||
if (g) {
|
||||
grows.add(name);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const worth = (r: Node): boolean => {
|
||||
// Module code runs once, and its groups hold the program's own state.
|
||||
if (r.kind === "call") return grows.has(r.callee) && !r.callee.startsWith("%init.");
|
||||
if (r.kind !== "new") return false;
|
||||
if (grows.has(`%${r.className}.constructor`)) return true;
|
||||
return r.args.some((a) => a.kind === "call" && grows.has(a.callee));
|
||||
};
|
||||
for (const r of [...groups.roots])
|
||||
if (!worth(r)) {
|
||||
groups.roots.delete(r);
|
||||
groups.drops.delete(r);
|
||||
}
|
||||
for (const c of [...groups.calls]) if (!worth(c)) groups.calls.delete(c);
|
||||
groups.reachable = reachableFrom(mod, [...groups.roots, ...groups.calls]);
|
||||
|
||||
const arenaSites = new Set<Node>();
|
||||
const heapCalls = new Set<Node>();
|
||||
const mayAllocate = (e: Node): boolean => {
|
||||
const t = targetsOf(vt, e);
|
||||
return t === null || t.some((n) => groups.allocates.has(n) || !fns.has(n));
|
||||
};
|
||||
for (const name of groups.reachable) {
|
||||
const s = sites.get(name);
|
||||
if (s === undefined) continue;
|
||||
for (const e of s.kept.keys()) arenaSites.add(e);
|
||||
for (const e of s.dropped)
|
||||
if (!groups.roots.has(e) && !groups.calls.has(e) && mayAllocate(e)) heapCalls.add(e);
|
||||
}
|
||||
return { arenaSites, heapCalls };
|
||||
}
|
||||
@@ -60,6 +60,14 @@ import {
|
||||
type InlineUnionFields,
|
||||
} from "./inline-unions.js";
|
||||
import { JS_BITS, JS_TAGS } from "./jsval-encoding.js";
|
||||
import {
|
||||
ARENA_RC,
|
||||
arenaDisposeLines,
|
||||
arenaDisposeSym,
|
||||
arenaNewLines,
|
||||
disposeFieldLines,
|
||||
plainRefType,
|
||||
} from "./arenas.js";
|
||||
import { recordMayKeepSource } from "./record-sources.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
@@ -137,6 +145,10 @@ export interface ShapeHost {
|
||||
* allocate in regions and count references in their region chunk instead
|
||||
* of being empty (llvm/regions.ts). */
|
||||
regions?: RegionHooks | null;
|
||||
/** Record shapes and classes with an arena-aware constructor
|
||||
* (arenas.ts). */
|
||||
arenaShapes?: ReadonlySet<string>;
|
||||
arenaClasses?: ReadonlySet<string>;
|
||||
/** The target C ABI's `size_t` / refcount integer type. */
|
||||
readonly sizeType: "i32" | "i64";
|
||||
/** Worker executables: the inline allocator state is thread-local. */
|
||||
@@ -1198,22 +1210,25 @@ export function arrNewCall(
|
||||
/** An empty array expected to receive a few elements (its first slots
|
||||
* live in its own block): capText must be "0". */
|
||||
small = false,
|
||||
/** In the current group arena when one is open (arenas.ts). */
|
||||
arena = false,
|
||||
): string {
|
||||
const r = arena ? "_r" : "";
|
||||
const useRef = arrUsesRef(host, elem);
|
||||
if (!useRef) {
|
||||
if (small) {
|
||||
host.declare(`declare ptr @scr_arr_new_small(i32)`);
|
||||
return `call ptr @scr_arr_new_small(i32 ${elemKindNum(elem)})`;
|
||||
host.declare(`declare ptr @scr_arr_new_small${r}(i32)`);
|
||||
return `call ptr @scr_arr_new_small${r}(i32 ${elemKindNum(elem)})`;
|
||||
}
|
||||
host.declare(`declare ptr @scr_arr_new(i32, ${host.sizeType})`);
|
||||
return `call ptr @scr_arr_new(i32 ${elemKindNum(elem)}, ${host.sizeType} ${capText})`;
|
||||
host.declare(`declare ptr @scr_arr_new${r}(i32, ${host.sizeType})`);
|
||||
return `call ptr @scr_arr_new${r}(i32 ${elemKindNum(elem)}, ${host.sizeType} ${capText})`;
|
||||
}
|
||||
if (small) {
|
||||
host.declare(`declare ptr @scr_arr_new_ops_small(ptr)`);
|
||||
return `call ptr @scr_arr_new_ops_small(ptr ${arrOps(host, elem)})`;
|
||||
host.declare(`declare ptr @scr_arr_new_ops_small${r}(ptr)`);
|
||||
return `call ptr @scr_arr_new_ops_small${r}(ptr ${arrOps(host, elem)})`;
|
||||
}
|
||||
host.declare(`declare ptr @scr_arr_new_ops(ptr, ${host.sizeType})`);
|
||||
return `call ptr @scr_arr_new_ops(ptr ${arrOps(host, elem)}, ${host.sizeType} ${capText})`;
|
||||
host.declare(`declare ptr @scr_arr_new_ops${r}(ptr, ${host.sizeType})`);
|
||||
return `call ptr @scr_arr_new_ops${r}(ptr ${arrOps(host, elem)}, ${host.sizeType} ${capText})`;
|
||||
}
|
||||
|
||||
/** The ScrElemKind and ops descriptor (or `null`) an array of `elem`
|
||||
@@ -2327,6 +2342,34 @@ export function emitRecordShapes(
|
||||
if (traced) nw.push(...youngEntryLines(host));
|
||||
nw.push(` ret ptr %o`, `}`, ``);
|
||||
defs.push(...nw);
|
||||
if (host.arenaShapes?.has(shape.id) === true && !shape.indexValue) {
|
||||
const newSym = mangleRecordNew(shape.id);
|
||||
const dispose = refMembers.length > 0 ? arenaDisposeSym(newSym) : null;
|
||||
const init = [` store ${host.sizeType} ${ARENA_RC}, ptr %o`];
|
||||
if (shapeNumber !== 0)
|
||||
init.push(
|
||||
` %shp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
` store i32 ${shapeNumber}, ptr %shp ; shape number`,
|
||||
);
|
||||
defs.push(
|
||||
...arenaNewLines(host, newSym, sizeOf, traced ? cycType : null, dispose, init, "%o"),
|
||||
);
|
||||
if (dispose !== null)
|
||||
defs.push(
|
||||
...arenaDisposeLines(
|
||||
newSym,
|
||||
refMembers.flatMap((m, i) =>
|
||||
disposeFieldLines(
|
||||
i,
|
||||
`getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${m.index}`,
|
||||
plainRefType(m.type),
|
||||
releaseSym(host, m.type),
|
||||
llvmCommentText(m.name),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (host.recordCloneShapes.has(shape.id)) {
|
||||
const attrs = shape.fields.length >= 16 ? "#2" : FN_ATTRS;
|
||||
|
||||
@@ -57,8 +57,9 @@ export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackO
|
||||
if (e.type.kind === "record" && host.recordsById.get(e.type.shapeId)?.freezable === true)
|
||||
return false;
|
||||
// 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));
|
||||
// caller's storage, unless the call roots a group: then the result goes
|
||||
// in the group's arena with the objects it holds (arenas.ts).
|
||||
if (e.kind === "call") return host.fnByName.has(resultEntryName(e.callee)) && !host.opensArena(e);
|
||||
if (e.kind === "recordLit") {
|
||||
if (e.type.kind !== "record") return false;
|
||||
const shape = host.recordsById.get(e.type.shapeId);
|
||||
|
||||
@@ -70,6 +70,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [
|
||||
"scr_cycle.c",
|
||||
"scr_publish.c",
|
||||
"scr_region.c",
|
||||
"scr_arena.c",
|
||||
"scr_itable.c",
|
||||
"scr_slots.c",
|
||||
"scr_alloc.c",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/arena-array-subclass.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/arena-arrays.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/arena-cap.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/arena-escapes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/arena-mixed.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/arena-outside.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/arena-region-throws.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/arena-regions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/arena-thrown-member.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/arena-throws.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/arena-trees.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/arena-workers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -47,6 +47,7 @@ export const BASE_RUNTIME_SOURCES = [
|
||||
"scr_cycle.c",
|
||||
"scr_publish.c",
|
||||
"scr_region.c",
|
||||
"scr_arena.c",
|
||||
"scr_itable.c",
|
||||
"scr_slots.c",
|
||||
"scr_alloc.c",
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
/* Group arenas: see scr_arena.h. */
|
||||
#include "scr_runtime.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* The first chunk lives inside the arena, so a small group (one result
|
||||
* record, a short builder) touches no other memory. */
|
||||
#define SCR_ARENA_INLINE 512
|
||||
#define SCR_ARENA_CHUNK_MIN ((size_t)4096)
|
||||
#define SCR_ARENA_CHUNK_MAX ((size_t)1 << 20)
|
||||
/* Bytes an arena that is not closed takes before its call allocates on the
|
||||
* heap again: a group whose objects die before it is dropped holds at most
|
||||
* this much dead memory. A closed arena has no cap: once on the heap, its
|
||||
* later objects could come to be held by earlier ones, which are not on
|
||||
* its dispose list. Neither do objects with a collector header: on the
|
||||
* heap, they could form cycles that outlive the drop (llvm/arenas.ts). */
|
||||
#define SCR_ARENA_CAP ((size_t)32 << 20)
|
||||
/* What a thread keeps for reuse: arenas, chunk bytes, and dispose list
|
||||
* entries per pooled arena. */
|
||||
#define SCR_ARENA_POOL 64
|
||||
#define SCR_ARENA_POOL_BYTES ((size_t)8 << 20)
|
||||
#define SCR_ARENA_POOL_DISPOSALS ((size_t)4096)
|
||||
|
||||
/* A chunk's objects run from scr_arena_chunk_start to its end. */
|
||||
typedef struct ScrArenaChunk {
|
||||
struct ScrArenaChunk *next;
|
||||
size_t size; /* bytes, this header included */
|
||||
} ScrArenaChunk;
|
||||
|
||||
/* The leading fields are an ABI: emitted arena-aware constructors bump
|
||||
* `bump` up to `limit`, test `closed`, and append to the dispose list at
|
||||
* `disposals` up to `disposals_end` inline (llvm/arenas.ts). Without the
|
||||
* small-object allocator `bump` and `limit` stay NULL, so every object
|
||||
* takes scr_arena_obj and is a block of its own. */
|
||||
struct ScrArena {
|
||||
char *bump, *limit;
|
||||
ScrArenaDisposal *disposals, *disposals_end;
|
||||
uint32_t closed;
|
||||
uint32_t open; /* still the allocation target, or under one that is */
|
||||
ScrArena *outer; /* the allocation target before it opened */
|
||||
ScrArena *next; /* its scope's list, or the pool */
|
||||
ScrArenaDisposal *disposal_base;
|
||||
ScrArenaChunk *chunks; /* newest first */
|
||||
size_t bytes;
|
||||
#if !SCR_SMALL_ALLOC
|
||||
char **blocks;
|
||||
size_t nblocks, cap;
|
||||
#endif
|
||||
_Alignas(16) char inline_chunk[SCR_ARENA_INLINE];
|
||||
};
|
||||
/* llvm/arenas.ts addresses these fields in pointer-sized units. */
|
||||
#define SCR_ARENA_PTR sizeof(void *)
|
||||
_Static_assert(offsetof(ScrArena, bump) == 0 && offsetof(ScrArena, limit) == SCR_ARENA_PTR,
|
||||
"llvm/arenas.ts bumps the arena inline");
|
||||
_Static_assert(offsetof(ScrArena, disposals) == 2 * SCR_ARENA_PTR &&
|
||||
offsetof(ScrArena, disposals_end) == 3 * SCR_ARENA_PTR &&
|
||||
offsetof(ScrArena, closed) == 4 * SCR_ARENA_PTR,
|
||||
"llvm/arenas.ts appends to the dispose list inline");
|
||||
_Static_assert(sizeof(ScrArenaDisposal) == 2 * SCR_ARENA_PTR,
|
||||
"llvm/arenas.ts stores dispose entries of two pointers");
|
||||
_Static_assert(offsetof(ScrArenaHead, disposals) == offsetof(ScrArena, disposals) &&
|
||||
offsetof(ScrArenaHead, closed) == offsetof(ScrArena, closed),
|
||||
"ScrArenaHead is ScrArena's prefix");
|
||||
|
||||
SCR_TL ScrArena *scr_arena_cur = NULL;
|
||||
static SCR_TL ScrArena *scr_arena_pool;
|
||||
static SCR_TL unsigned scr_arena_pooled;
|
||||
static SCR_TL ScrArenaChunk *scr_arena_chunk_pool;
|
||||
static SCR_TL size_t scr_arena_chunk_bytes;
|
||||
|
||||
void *scr_arena_current(void) { return scr_arena_cur; }
|
||||
|
||||
void *scr_arena_suspend(void) {
|
||||
ScrArena *a = scr_arena_cur;
|
||||
scr_arena_cur = NULL;
|
||||
return a;
|
||||
}
|
||||
|
||||
void scr_arena_resume(void *arena) { scr_arena_cur = (ScrArena *)arena; }
|
||||
|
||||
void *scr_arena_open(void **list, int32_t closed) {
|
||||
ScrArena *a = scr_arena_pool;
|
||||
if (a) {
|
||||
scr_arena_pool = a->next;
|
||||
scr_arena_pooled--;
|
||||
} else {
|
||||
a = malloc(sizeof *a);
|
||||
if (!a) scr_trap("scriptc: out of memory\n");
|
||||
a->disposal_base = NULL;
|
||||
a->disposals_end = NULL;
|
||||
#if !SCR_SMALL_ALLOC
|
||||
a->blocks = NULL;
|
||||
a->cap = 0;
|
||||
#endif
|
||||
}
|
||||
a->closed = closed != 0;
|
||||
a->chunks = NULL;
|
||||
a->bytes = 0;
|
||||
a->disposals = a->disposal_base;
|
||||
#if SCR_SMALL_ALLOC
|
||||
a->bump = a->inline_chunk;
|
||||
a->limit = a->inline_chunk + SCR_ARENA_INLINE;
|
||||
#else
|
||||
a->bump = a->limit = NULL;
|
||||
a->nblocks = 0;
|
||||
#endif
|
||||
a->open = 1;
|
||||
a->outer = scr_arena_cur;
|
||||
scr_arena_cur = a;
|
||||
a->next = (ScrArena *)*list;
|
||||
*list = a;
|
||||
return a;
|
||||
}
|
||||
|
||||
/* Makes `a`'s outer target current again, if `a` is still the target or
|
||||
* under it: an exception can leave the arenas a call opened unclosed until
|
||||
* their scopes drop them. */
|
||||
static void scr_arena_unlink(ScrArena *a) {
|
||||
if (!a->open) return;
|
||||
a->open = 0;
|
||||
for (ScrArena *t = scr_arena_cur; t; t = t->outer)
|
||||
if (t == a) {
|
||||
scr_arena_cur = a->outer;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void scr_arena_close(void *arena) {
|
||||
if (arena) scr_arena_unlink((ScrArena *)arena);
|
||||
}
|
||||
|
||||
void scr_arena_disposal(void *arena, void *obj, ScrArenaDispose dispose) {
|
||||
ScrArena *a = (ScrArena *)arena;
|
||||
if (a->disposals == a->disposals_end) {
|
||||
size_t n = (size_t)(a->disposals - a->disposal_base);
|
||||
size_t cap = n ? n * 2 : 64;
|
||||
ScrArenaDisposal *grown = realloc(a->disposal_base, cap * sizeof *grown);
|
||||
if (!grown) scr_trap("scriptc: out of memory\n");
|
||||
a->disposal_base = grown;
|
||||
a->disposals = grown + n;
|
||||
a->disposals_end = grown + cap;
|
||||
}
|
||||
a->disposals->obj = obj;
|
||||
a->disposals->dispose = dispose;
|
||||
a->disposals++;
|
||||
}
|
||||
|
||||
#if SCR_SMALL_ALLOC
|
||||
static ScrArenaChunk *scr_arena_chunk(size_t want) {
|
||||
for (ScrArenaChunk **p = &scr_arena_chunk_pool; *p; p = &(*p)->next)
|
||||
if ((*p)->size >= want) {
|
||||
ScrArenaChunk *c = *p;
|
||||
*p = c->next;
|
||||
scr_arena_chunk_bytes -= c->size;
|
||||
return c;
|
||||
}
|
||||
ScrArenaChunk *c = malloc(want);
|
||||
if (!c) scr_trap("scriptc: out of memory\n");
|
||||
c->size = want;
|
||||
return c;
|
||||
}
|
||||
|
||||
static char *scr_arena_chunk_start(ScrArenaChunk *c) {
|
||||
return (char *)(((uintptr_t)(c + 1) + 15) & ~(uintptr_t)15);
|
||||
}
|
||||
|
||||
/* A new current chunk with room for `n` bytes, or false past the cap
|
||||
* when `capped`. */
|
||||
static bool scr_arena_refill(ScrArena *a, size_t n, bool capped) {
|
||||
size_t want = a->chunks ? a->chunks->size * 2 : SCR_ARENA_CHUNK_MIN;
|
||||
if (want > SCR_ARENA_CHUNK_MAX) want = SCR_ARENA_CHUNK_MAX;
|
||||
size_t need = n + sizeof(ScrArenaChunk) + 15;
|
||||
if (want < need) want = need;
|
||||
if (capped && a->bytes + want > SCR_ARENA_CAP) return false;
|
||||
ScrArenaChunk *c = scr_arena_chunk(want);
|
||||
a->bytes += c->size;
|
||||
c->next = a->chunks;
|
||||
a->chunks = c;
|
||||
a->bump = scr_arena_chunk_start(c);
|
||||
a->limit = (char *)c + c->size;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void *scr_arena_obj(void *arena, size_t size, void *cyc) {
|
||||
ScrArena *a = (ScrArena *)arena;
|
||||
size_t hdr = cyc ? sizeof(ScrCycHdr) : 0;
|
||||
bool capped = !a->closed && !cyc;
|
||||
if (capped && size > SCR_ARENA_CAP) return NULL;
|
||||
size_t n = (hdr + size + 7) & ~(size_t)7;
|
||||
#if SCR_SMALL_ALLOC
|
||||
if ((size_t)(a->limit - a->bump) < n && !scr_arena_refill(a, n, capped)) return NULL;
|
||||
char *block = a->bump;
|
||||
a->bump += n;
|
||||
memset(block, 0, n);
|
||||
#else
|
||||
if (capped && a->bytes + n > SCR_ARENA_CAP) return NULL;
|
||||
a->bytes += n;
|
||||
if (a->nblocks == a->cap) {
|
||||
size_t cap = a->cap ? a->cap * 2 : 16;
|
||||
char **grown = realloc(a->blocks, cap * sizeof *grown);
|
||||
if (!grown) scr_trap("scriptc: out of memory\n");
|
||||
a->blocks = grown;
|
||||
a->cap = cap;
|
||||
}
|
||||
char *block = calloc(1, n);
|
||||
if (!block) scr_trap("scriptc: out of memory\n");
|
||||
a->blocks[a->nblocks++] = block;
|
||||
#endif
|
||||
if (!cyc) return block;
|
||||
ScrCycHdr *ch = (ScrCycHdr *)block;
|
||||
ch->id = (uint16_t)scr_cyc_type_id((ScrCycType *)cyc);
|
||||
ch->buf = SCR_CYC_ACYCLIC;
|
||||
return block + hdr;
|
||||
}
|
||||
|
||||
static void scr_arena_free(ScrArena *a) {
|
||||
/* Every entry before any memory: a release can reach another object of
|
||||
* the arena. Newest first, the reverse of construction. */
|
||||
for (ScrArenaDisposal *d = a->disposals; d != a->disposal_base;) {
|
||||
d--;
|
||||
d->dispose(d->obj);
|
||||
}
|
||||
if ((size_t)(a->disposals_end - a->disposal_base) > SCR_ARENA_POOL_DISPOSALS) {
|
||||
free(a->disposal_base);
|
||||
a->disposal_base = a->disposals_end = NULL;
|
||||
}
|
||||
#if SCR_SMALL_ALLOC
|
||||
for (ScrArenaChunk *c = a->chunks, *next; c; c = next) {
|
||||
next = c->next;
|
||||
if (scr_arena_chunk_bytes + c->size <= SCR_ARENA_POOL_BYTES) {
|
||||
c->next = scr_arena_chunk_pool;
|
||||
scr_arena_chunk_pool = c;
|
||||
scr_arena_chunk_bytes += c->size;
|
||||
} else
|
||||
free(c);
|
||||
}
|
||||
if (scr_arena_pooled < SCR_ARENA_POOL) {
|
||||
a->next = scr_arena_pool;
|
||||
scr_arena_pool = a;
|
||||
scr_arena_pooled++;
|
||||
return;
|
||||
}
|
||||
#else
|
||||
for (size_t i = 0; i < a->nblocks; i++) free(a->blocks[i]);
|
||||
free(a->blocks);
|
||||
#endif
|
||||
free(a->disposal_base);
|
||||
free(a);
|
||||
}
|
||||
|
||||
void scr_arena_drop(void **list) {
|
||||
ScrArena *a = (ScrArena *)*list;
|
||||
*list = NULL;
|
||||
while (a) {
|
||||
ScrArena *next = a->next;
|
||||
scr_arena_unlink(a);
|
||||
scr_arena_free(a);
|
||||
a = next;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
#ifndef SCR_ARENA_H
|
||||
#define SCR_ARENA_H
|
||||
/* Group arenas (llvm/region-groups.ts, llvm/arenas.ts): the objects a call
|
||||
* allocates while the compiler has proven they die together, bump-allocated
|
||||
* and released at once.
|
||||
*
|
||||
* Two kinds of call open one:
|
||||
* - a fresh-result call (a root) whose value its caller keeps in one scope:
|
||||
* the arena stays the allocation target while the call runs, and is
|
||||
* dropped when that scope exits, on every path;
|
||||
* - a call region, a call of a function that keeps nothing it allocates
|
||||
* and returns no reference: the arena is dropped when the call returns.
|
||||
*
|
||||
* Only allocation sites in functions the compiler reached from such a call
|
||||
* consult the target (scr_arena_cur), and only those whose value the group
|
||||
* keeps (llvm/region-sites.ts), so runtime-internal allocations and
|
||||
* temporaries never land in an arena.
|
||||
*
|
||||
* An arena object is an ordinary object with an ordinary count, starting
|
||||
* at SCR_ARENA_RC instead of 1, so no release reaches zero while it lives:
|
||||
* no teardown, no free, and (with `buf` SCR_CYC_ACYCLIC) never a collector
|
||||
* candidate. The count is far below SCR_RC_PUBLISHED, so the arena object
|
||||
* stays mutable and thread-local like any other. Dropping the arena runs
|
||||
* its dispose list, whose entries release their objects' fields (and
|
||||
* storage) without freeing the objects, and then recycles the memory. A
|
||||
* closed arena's objects hold only each other and immortal values, so only
|
||||
* objects with storage of their own (arrays) go on its list.
|
||||
*
|
||||
* Without the small-object allocator (sanitized and audit builds), every
|
||||
* arena object is a block of its own that the drop frees, so a use after
|
||||
* the drop is a use after free that the sanitizer reports. */
|
||||
#include "scr_core.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* The starting count of an arena object (a count, in the low 32 bits of
|
||||
* the rc word, so it also suits classes that keep their id in the high
|
||||
* half). */
|
||||
#define SCR_ARENA_RC ((uint32_t)1 << 29)
|
||||
|
||||
typedef struct ScrArena ScrArena;
|
||||
/* Releases what an arena object holds (its fields, an array's storage)
|
||||
* without freeing the object. */
|
||||
typedef void (*ScrArenaDispose)(void *obj);
|
||||
|
||||
/* An entry of an arena's dispose list. The emitted arena-aware
|
||||
* constructors append one inline for an object whose type has fields to
|
||||
* release, unless the arena is closed (llvm/arenas.ts). */
|
||||
typedef struct ScrArenaDisposal {
|
||||
void *obj;
|
||||
ScrArenaDispose dispose;
|
||||
} ScrArenaDisposal;
|
||||
|
||||
/* The arena that allocation sites in arena-aware functions use, or NULL.
|
||||
* Emitted code reads scr_arena_cur directly outside worker programs. */
|
||||
extern SCR_TL ScrArena *scr_arena_cur;
|
||||
void *scr_arena_current(void);
|
||||
/* Stops any arena being the allocation target while a call whose value
|
||||
* its group does not keep runs; returns the target, which
|
||||
* scr_arena_resume restores. */
|
||||
void *scr_arena_suspend(void);
|
||||
void scr_arena_resume(void *arena);
|
||||
/* Opens an arena as the allocation target and links it onto `*list`, the
|
||||
* list its scope drops. Returns it. A `closed` arena's objects hold only
|
||||
* each other and immortal values (the compiler proved it), so dropping it
|
||||
* releases nothing. */
|
||||
void *scr_arena_open(void **list, int32_t closed);
|
||||
/* Stops `arena` (may be NULL) being the allocation target; its objects
|
||||
* live on until its list is dropped. */
|
||||
void scr_arena_close(void *arena);
|
||||
/* Drops every arena on `*list`, newest first, and clears the list. */
|
||||
void scr_arena_drop(void **list);
|
||||
/* A zeroed object of `size` bytes in `arena` (behind a collector header
|
||||
* when `cyc` is not NULL, its `buf` SCR_CYC_ACYCLIC), for when the emitted
|
||||
* bump finds no room: a new chunk. Returns NULL when an arena that is not
|
||||
* closed is past its byte cap and `cyc` is NULL, and the caller allocates
|
||||
* on the heap. The caller stores the count (SCR_ARENA_RC). */
|
||||
void *scr_arena_obj(void *arena, size_t size, void *cyc);
|
||||
/* Appends `obj` to `arena`'s dispose list, which the drop runs. */
|
||||
void scr_arena_disposal(void *arena, void *obj, ScrArenaDispose dispose);
|
||||
|
||||
/* The leading fields of an arena (scr_arena.c's ScrArena), for the
|
||||
* runtime's own inline allocation. */
|
||||
typedef struct ScrArenaHead {
|
||||
char *bump, *limit;
|
||||
ScrArenaDisposal *disposals, *disposals_end;
|
||||
uint32_t closed;
|
||||
} ScrArenaHead;
|
||||
|
||||
/* `n` bytes (a multiple of 8) from `arena`'s current chunk, not cleared,
|
||||
* or NULL when the chunk has no room (scr_arena_obj takes a new one). */
|
||||
static inline void *scr_arena_bump(void *arena, size_t n) {
|
||||
ScrArenaHead *h = (ScrArenaHead *)arena;
|
||||
if ((size_t)(h->limit - h->bump) < n) return NULL;
|
||||
char *p = h->bump;
|
||||
h->bump = p + n;
|
||||
return p;
|
||||
}
|
||||
|
||||
/* scr_arena_disposal with the append inline. */
|
||||
static inline void scr_arena_dispose_later(void *arena, void *obj, ScrArenaDispose dispose) {
|
||||
ScrArenaHead *h = (ScrArenaHead *)arena;
|
||||
if (h->disposals == h->disposals_end) {
|
||||
scr_arena_disposal(arena, obj, dispose);
|
||||
return;
|
||||
}
|
||||
h->disposals->obj = obj;
|
||||
h->disposals->dispose = dispose;
|
||||
h->disposals++;
|
||||
}
|
||||
|
||||
static inline int scr_arena_is_closed(void *arena) { return ((ScrArenaHead *)arena)->closed != 0; }
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,108 @@
|
||||
/* Group arenas (scr_arena.h): allocation, nesting, closing and dropping,
|
||||
* and the drop's dispose list. Prints one line per check. */
|
||||
#include "scr_runtime.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
int64_t rc;
|
||||
int64_t id;
|
||||
} TestObj;
|
||||
|
||||
static int disposed;
|
||||
static int64_t disposed_sum;
|
||||
static void test_dispose(void *obj) {
|
||||
disposed++;
|
||||
disposed_sum += ((TestObj *)obj)->id;
|
||||
}
|
||||
/* What an emitted arena-aware constructor does: the object, and its
|
||||
* dispose entry unless the arena is closed. */
|
||||
static TestObj *obj(void *arena, int64_t id, int closed) {
|
||||
TestObj *o = scr_arena_obj(arena, sizeof *o, NULL);
|
||||
if (!o) return NULL;
|
||||
o->rc = SCR_ARENA_RC;
|
||||
o->id = id;
|
||||
if (!closed) scr_arena_disposal(arena, o, test_dispose);
|
||||
return o;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
/* An open arena is the allocation target until it closes. */
|
||||
void *list = NULL;
|
||||
void *a = scr_arena_open(&list, 0);
|
||||
printf("current while open: %s\n", scr_arena_current() == a ? "yes" : "no");
|
||||
TestObj *first = obj(a, 1, 0);
|
||||
int zero = 1, adjacent = 1;
|
||||
TestObj *prev = first;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
TestObj *o = scr_arena_obj(a, sizeof *o, NULL);
|
||||
if (o->rc != 0 || o->id != 0) zero = 0;
|
||||
if ((char *)o != (char *)(prev + 1)) adjacent = 0;
|
||||
prev = o;
|
||||
}
|
||||
printf("objects start zeroed: %s\n", zero ? "yes" : "no");
|
||||
#if !SCR_SMALL_ALLOC
|
||||
adjacent = 1; /* a block per object */
|
||||
#endif
|
||||
printf("objects without headers: %s\n", adjacent ? "yes" : "no");
|
||||
/* A nested arena makes the outer one current again when it closes. */
|
||||
void *inner_list = NULL;
|
||||
void *b = scr_arena_open(&inner_list, 0);
|
||||
for (int i = 0; i < 10; i++) obj(b, 100, 0);
|
||||
scr_arena_close(b);
|
||||
scr_arena_close(b);
|
||||
printf("outer current after inner close: %s\n", scr_arena_current() == a ? "yes" : "no");
|
||||
scr_arena_drop(&inner_list);
|
||||
printf("inner drop disposed %d (sum %lld)\n", disposed, (long long)disposed_sum);
|
||||
/* Many objects span chunks; each is disposed once. */
|
||||
disposed = 0;
|
||||
disposed_sum = 0;
|
||||
for (int i = 0; i < 100000; i++) obj(a, 1, 0);
|
||||
scr_arena_close(a);
|
||||
printf("current after close: %s\n", scr_arena_current() == NULL ? "none" : "some");
|
||||
scr_arena_drop(&list);
|
||||
printf("drop disposed %d (sum %lld), list %s\n", disposed, (long long)disposed_sum,
|
||||
list == NULL ? "cleared" : "kept");
|
||||
|
||||
/* A closed arena's objects take no dispose entries, and it has no cap. */
|
||||
disposed = 0;
|
||||
void *closed = scr_arena_open(&list, 1);
|
||||
int held = 0;
|
||||
for (int i = 0; i < 3000000; i++)
|
||||
if (obj(closed, 1, 1)) held++;
|
||||
scr_arena_drop(&list);
|
||||
printf("closed arena held %d, disposed %d, current %s\n", held, disposed,
|
||||
scr_arena_current() == NULL ? "none" : "some");
|
||||
|
||||
/* Past the byte cap an arena that is not closed refuses, and its drop
|
||||
* disposes everything it holds. */
|
||||
disposed = 0;
|
||||
void *capped = scr_arena_open(&list, 0);
|
||||
int refused = 0;
|
||||
held = 0;
|
||||
for (int i = 0; i < 3000000 && !refused; i++) {
|
||||
if (obj(capped, 1, 0))
|
||||
held++;
|
||||
else
|
||||
refused = 1;
|
||||
}
|
||||
scr_arena_drop(&list);
|
||||
printf("cap refused: %s, all disposed: %s\n", refused ? "yes" : "no", disposed == held ? "yes" : "no");
|
||||
|
||||
/* A drop of an arena that is still open (an exception skipped its close)
|
||||
* restores the target it replaced. */
|
||||
void *outer_list = NULL, *skipped_list = NULL;
|
||||
void *outer = scr_arena_open(&outer_list, 0);
|
||||
scr_arena_open(&skipped_list, 0);
|
||||
scr_arena_drop(&skipped_list);
|
||||
printf("target after dropping an open arena: %s\n", scr_arena_current() == outer ? "outer" : "other");
|
||||
/* A suspended arena is no target until it resumes. */
|
||||
void *saved = scr_arena_suspend();
|
||||
printf("target while suspended: %s\n", scr_arena_current() == NULL ? "none" : "some");
|
||||
scr_arena_resume(saved);
|
||||
printf("target after resume: %s\n", scr_arena_current() == outer ? "outer" : "other");
|
||||
scr_arena_close(outer);
|
||||
scr_arena_drop(&outer_list);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { EXECUTABLE_RUNTIME_SOURCES } from "../../compiler/src/backend/native-toolchain.js";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
||||
const EXPECTED = [
|
||||
"current while open: yes",
|
||||
"objects start zeroed: yes",
|
||||
"objects without headers: yes",
|
||||
"outer current after inner close: yes",
|
||||
"inner drop disposed 10 (sum 1000)",
|
||||
"current after close: none",
|
||||
"drop disposed 100001 (sum 100001), list cleared",
|
||||
"closed arena held 3000000, disposed 0, current none",
|
||||
"cap refused: yes, all disposed: yes",
|
||||
"target after dropping an open arena: outer",
|
||||
"target while suspended: none",
|
||||
"target after resume: outer",
|
||||
];
|
||||
|
||||
// The size-class allocator build (chunks, the inline bump's fields), and
|
||||
// the sanitized lane's build (a block per object, freed by the drop).
|
||||
describe.skipIf(process.platform === "win32")("group arenas", () => {
|
||||
test.each([
|
||||
["size-class", ["-fsanitize=undefined"]],
|
||||
["sanitized", ["-fsanitize=address,undefined", "-DSCR_RC_AUDIT"]],
|
||||
])(
|
||||
"open, allocate, close and drop (%s)",
|
||||
async (_lane, flags) => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-arena-"));
|
||||
try {
|
||||
const bin = join(dir, "arena");
|
||||
await exec("clang", [
|
||||
"-std=c11",
|
||||
"-O1",
|
||||
"-pthread",
|
||||
"-Wno-deprecated-declarations",
|
||||
...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []),
|
||||
...flags,
|
||||
...["scr_arena.test.c", ...EXECUTABLE_RUNTIME_SOURCES].map((source) =>
|
||||
join(import.meta.dirname, source),
|
||||
),
|
||||
...(process.platform === "linux" ? ["-lm"] : []),
|
||||
"-o",
|
||||
bin,
|
||||
]);
|
||||
const { stdout } = await exec(bin, [], {
|
||||
timeout: 60000,
|
||||
env: {
|
||||
...process.env,
|
||||
ASAN_OPTIONS: "halt_on_error=1",
|
||||
UBSAN_OPTIONS: "halt_on_error=1",
|
||||
},
|
||||
});
|
||||
expect(stdout.trim().split("\n")).toEqual(EXPECTED);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
300000,
|
||||
);
|
||||
});
|
||||
@@ -1426,15 +1426,53 @@ static void scr_arr_put_slots(ScrArr *a, size_t to, size_t count, const uint64_t
|
||||
|
||||
/* Inlined into each constructor, so `small` and the slot unit fold to
|
||||
* constants there. */
|
||||
static void scr_arr_arena_dispose(void *object);
|
||||
static void scr_arr_arena_dispose_closed(void *object);
|
||||
static ScrCycType scr_arr_cyc_type;
|
||||
|
||||
/* An array block of `size` bytes in the current group arena, or NULL.
|
||||
* Its storage beyond the block is the heap's, so it goes on the arena's
|
||||
* dispose list even when the arena is closed; there, its elements are
|
||||
* arena objects and immortal values, which need no release. */
|
||||
static inline ScrArr *scr_arr_arena_block(size_t size, bool cyc) {
|
||||
void *arena = scr_arena_cur;
|
||||
if (!arena) return NULL;
|
||||
size_t hdr = cyc ? sizeof(ScrCycHdr) : 0;
|
||||
size_t n = (hdr + size + 7) & ~(size_t)7;
|
||||
uint32_t id = cyc ? scr_cyc_type_id(&scr_arr_cyc_type) : 0;
|
||||
char *block = scr_arena_bump(arena, n);
|
||||
ScrArr *a;
|
||||
if (block) {
|
||||
/* The constructor sets every header field, as on the heap; only the
|
||||
* collector header needs setting here. */
|
||||
if (cyc) *(ScrCycHdr *)block = (ScrCycHdr){.id = (uint16_t)id, .buf = SCR_CYC_ACYCLIC};
|
||||
a = (ScrArr *)(block + hdr);
|
||||
} else {
|
||||
a = scr_arena_obj(arena, size, cyc ? &scr_arr_cyc_type : NULL);
|
||||
if (!a) return NULL;
|
||||
}
|
||||
scr_arena_dispose_later(arena, a,
|
||||
scr_arena_is_closed(arena) ? scr_arr_arena_dispose_closed
|
||||
: scr_arr_arena_dispose);
|
||||
return a;
|
||||
}
|
||||
|
||||
static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind elem,
|
||||
size_t initial_cap,
|
||||
bool small) {
|
||||
bool small,
|
||||
bool arena) {
|
||||
size_t unit = scr_elem_slot_width(elem);
|
||||
size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, unit)
|
||||
: scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, unit);
|
||||
ScrArr *a = scr_mem_alloc(inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr));
|
||||
if (!a) scr_arr_oom();
|
||||
a->rc = 1;
|
||||
size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr);
|
||||
ScrArr *a = arena ? scr_arr_arena_block(size, false) : NULL;
|
||||
if (a)
|
||||
a->rc = SCR_ARENA_RC;
|
||||
else {
|
||||
a = scr_mem_alloc(size);
|
||||
if (!a) scr_arr_oom();
|
||||
a->rc = 1;
|
||||
}
|
||||
a->len = 0;
|
||||
a->cap = 0;
|
||||
a->elem = (uint8_t)elem;
|
||||
@@ -1451,10 +1489,16 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind el
|
||||
}
|
||||
|
||||
ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap) {
|
||||
return scr_arr_make(elem, initial_cap, false);
|
||||
return scr_arr_make(elem, initial_cap, false, false);
|
||||
}
|
||||
|
||||
ScrArr *scr_arr_new_small(ScrElemKind elem) { return scr_arr_make(elem, 0, true); }
|
||||
ScrArr *scr_arr_new_small(ScrElemKind elem) { return scr_arr_make(elem, 0, true, false); }
|
||||
|
||||
ScrArr *scr_arr_new_r(ScrElemKind elem, size_t initial_cap) {
|
||||
return scr_arr_make(elem, initial_cap, false, true);
|
||||
}
|
||||
|
||||
ScrArr *scr_arr_new_small_r(ScrElemKind elem) { return scr_arr_make(elem, 0, true, true); }
|
||||
|
||||
/* A non-cycle array whose final length the runtime knows when it builds
|
||||
* it (regex match results): its `cap` slots share the header's block, so
|
||||
@@ -1463,7 +1507,7 @@ ScrArr *scr_arr_new_small(ScrElemKind elem) { return scr_arr_make(elem, 0, true)
|
||||
* `cap` moves the slots out like any inline array. */
|
||||
ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap, bool states) {
|
||||
if (cap == 0) {
|
||||
ScrArr *a = scr_arr_make(elem, 0, false);
|
||||
ScrArr *a = scr_arr_make(elem, 0, false, false);
|
||||
if (states) a->spare |= SCR_ARR_STATES;
|
||||
return a;
|
||||
}
|
||||
@@ -1572,13 +1616,16 @@ static void scr_arr_gc_free(void *a0) {
|
||||
scr_cyc_free_inline(a);
|
||||
}
|
||||
|
||||
static ScrCycType scr_arr_cyc_type = {&scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY};
|
||||
|
||||
/* Inlined into each constructor like scr_arr_make: with `small`, the count
|
||||
* and `elem` constant, scr_arr_new_ops_small's size class folds to a table
|
||||
* index and the slot sizes to constants. */
|
||||
static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops_kind(const ScrArrOps *ops,
|
||||
size_t initial_cap,
|
||||
bool small,
|
||||
uint8_t elem) {
|
||||
uint8_t elem,
|
||||
bool arena) {
|
||||
ScrArr *a;
|
||||
bool cyc = ops->trace != NULL;
|
||||
/* The array starts without states: slots of the value width alone. */
|
||||
@@ -1587,14 +1634,18 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops_kind(const
|
||||
size_t inline_cap =
|
||||
small ? scr_arr_small_cap(prefix, unit) : scr_arr_known_cap(prefix, initial_cap, unit);
|
||||
size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr);
|
||||
if (cyc) {
|
||||
static ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY };
|
||||
a = scr_cyc_alloc_inline(size, &cyc_type);
|
||||
} else {
|
||||
a = scr_mem_alloc(size);
|
||||
if (!a) scr_arr_oom();
|
||||
a = arena ? scr_arr_arena_block(size, cyc) : NULL;
|
||||
if (a)
|
||||
a->rc = SCR_ARENA_RC;
|
||||
else {
|
||||
if (cyc)
|
||||
a = scr_cyc_alloc_inline(size, &scr_arr_cyc_type);
|
||||
else {
|
||||
a = scr_mem_alloc(size);
|
||||
if (!a) scr_arr_oom();
|
||||
}
|
||||
a->rc = 1;
|
||||
}
|
||||
a->rc = 1;
|
||||
a->len = 0;
|
||||
a->cap = 0;
|
||||
a->elem = elem;
|
||||
@@ -1622,17 +1673,26 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops_kind(const
|
||||
* branch inlines scr_arr_make_ops_kind with a constant kind. */
|
||||
static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops,
|
||||
size_t initial_cap,
|
||||
bool small) {
|
||||
bool small,
|
||||
bool arena) {
|
||||
if (__builtin_expect(scr_arr_ops_compact(ops), 0))
|
||||
return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_CREF);
|
||||
return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_REF);
|
||||
return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_CREF, arena);
|
||||
return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_REF, arena);
|
||||
}
|
||||
|
||||
ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap) {
|
||||
return scr_arr_make_ops(ops, initial_cap, false);
|
||||
return scr_arr_make_ops(ops, initial_cap, false, false);
|
||||
}
|
||||
|
||||
ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops) { return scr_arr_make_ops(ops, 0, true); }
|
||||
ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops) { return scr_arr_make_ops(ops, 0, true, false); }
|
||||
|
||||
/* The constructors arena-aware functions call (llvm/arenas.ts): in the
|
||||
* current group arena when one is open. */
|
||||
ScrArr *scr_arr_new_ops_r(const ScrArrOps *ops, size_t initial_cap) {
|
||||
return scr_arr_make_ops(ops, initial_cap, false, true);
|
||||
}
|
||||
|
||||
ScrArr *scr_arr_new_ops_small_r(const ScrArrOps *ops) { return scr_arr_make_ops(ops, 0, true, true); }
|
||||
|
||||
/* Frees an acyclic array whose elements are already released. */
|
||||
static inline __attribute__((always_inline)) void scr_arr_free(ScrArr *a) {
|
||||
@@ -1746,6 +1806,26 @@ static void scr_arr_destroy(void *object) {
|
||||
else scr_arr_free(a);
|
||||
}
|
||||
|
||||
/* An arena array at its arena's drop: its contents and the storage beyond
|
||||
* its block go; the block goes with the arena. */
|
||||
static void scr_arr_arena_dispose(void *object) {
|
||||
ScrArr *a = object;
|
||||
scr_arr_release_contents(a);
|
||||
scr_arr_free_parts(a);
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_live_arrays--;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* The same in a closed arena, whose objects hold only each other and
|
||||
* immortal values: only the storage beyond the block goes. */
|
||||
static void scr_arr_arena_dispose_closed(void *object) {
|
||||
scr_arr_free_parts((ScrArr *)object);
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_live_arrays--;
|
||||
#endif
|
||||
}
|
||||
|
||||
void scr_arr_subclass_init(ScrArr *a, uint8_t elem, const ScrArrOps *ops, const ScrArrClass *cls,
|
||||
bool cyc) {
|
||||
a->len = 0;
|
||||
|
||||
@@ -490,6 +490,12 @@ ScrArr *scr_arr_new_small(ScrElemKind elem);
|
||||
* SCR_ELEM_F64); no-op for any other kind. */
|
||||
void scr_arr_widen_i32(ScrArr *a);
|
||||
ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops);
|
||||
/* The same constructors in the current group arena, when one is open
|
||||
* (scr_arena.h): what arena-aware functions call. */
|
||||
ScrArr *scr_arr_new_r(ScrElemKind elem, size_t initial_cap);
|
||||
ScrArr *scr_arr_new_small_r(ScrElemKind elem);
|
||||
ScrArr *scr_arr_new_ops_r(const ScrArrOps *ops, size_t initial_cap);
|
||||
ScrArr *scr_arr_new_ops_small_r(const ScrArrOps *ops);
|
||||
/* scr_arr_new(elem, cap) for a known final length: the `cap` slots share
|
||||
* the array's own block (one allocation). With `states`, the array starts
|
||||
* with states (for holes or present undefined). Returns +1. */
|
||||
|
||||
@@ -29,6 +29,7 @@ test("layouts stay within the layout and key-byte budgets, and dynamic-tier abse
|
||||
"scr_json.c",
|
||||
"scr_string.c",
|
||||
"scr_array.c",
|
||||
"scr_arena.c",
|
||||
"scr_map.c",
|
||||
"scr_exception.c",
|
||||
"scr_error.c",
|
||||
|
||||
@@ -30,6 +30,7 @@ test.each([false, true])(
|
||||
"scr_json.c",
|
||||
"scr_string.c",
|
||||
"scr_array.c",
|
||||
"scr_arena.c",
|
||||
"scr_map.c",
|
||||
"scr_exception.c",
|
||||
"scr_error.c",
|
||||
|
||||
@@ -27,6 +27,7 @@ test("number-key maps keep Map semantics across linear, direct and hashed indexe
|
||||
"scr_json.c",
|
||||
"scr_string.c",
|
||||
"scr_array.c",
|
||||
"scr_arena.c",
|
||||
"scr_map.c",
|
||||
"scr_exception.c",
|
||||
"scr_error.c",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
#include "scr_blob.h"
|
||||
#include "scr_object.h"
|
||||
#include "scr_region.h"
|
||||
#include "scr_arena.h"
|
||||
#include "scr_itable.h"
|
||||
#include "scr_slots.h"
|
||||
#include "scr_async.h"
|
||||
|
||||
@@ -24,6 +24,7 @@ beforeAll(async () => {
|
||||
bin,
|
||||
join(testDir, "test_array.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
// Checked-value collection equality shares the native JSON value runtime.
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
|
||||
@@ -32,6 +32,7 @@ beforeAll(async () => {
|
||||
// which pull in the closure/array/box machinery
|
||||
join(testDir, "../src/scr_union.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
// Checked-value collection equality shares the native JSON value runtime.
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
join(testDir, "../src/scr_bigint.c"),
|
||||
|
||||
@@ -40,6 +40,7 @@ beforeAll(async () => {
|
||||
join(testDir, "../src/scr_hash.c"),
|
||||
join(testDir, "../src/scr_string.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
join(testDir, "../src/scr_exception.c"),
|
||||
join(testDir, "../src/scr_error.c"),
|
||||
|
||||
@@ -20,6 +20,7 @@ const runtimeSources = [
|
||||
"scr_hash.c",
|
||||
"scr_string.c",
|
||||
"scr_array.c",
|
||||
"scr_arena.c",
|
||||
"scr_map.c",
|
||||
"scr_exception.c",
|
||||
"scr_error.c",
|
||||
|
||||
@@ -25,6 +25,7 @@ test("closure/box runtime: RC cascades clean under ASan + audit", async () => {
|
||||
join(srcDir, "scr_closure.c"),
|
||||
join(srcDir, "scr_string.c"),
|
||||
join(srcDir, "scr_array.c"),
|
||||
join(srcDir, "scr_arena.c"),
|
||||
join(srcDir, "scr_json.c"),
|
||||
join(srcDir, "scr_bigint.c"),
|
||||
join(srcDir, "scr_map.c"),
|
||||
|
||||
@@ -20,6 +20,7 @@ const runtimeSources = [
|
||||
"scr_hash.c",
|
||||
"scr_string.c",
|
||||
"scr_array.c",
|
||||
"scr_arena.c",
|
||||
"scr_map.c",
|
||||
"scr_exception.c",
|
||||
"scr_error.c",
|
||||
|
||||
@@ -35,6 +35,7 @@ beforeAll(async () => {
|
||||
join(testDir, "../src/scr_string.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
join(testDir, "../src/scr_bigint.c"),
|
||||
join(testDir, "../src/scr_exception.c"),
|
||||
|
||||
@@ -35,6 +35,7 @@ beforeAll(async () => {
|
||||
join(testDir, "../src/scr_string.c"),
|
||||
join(testDir, "../src/scr_number.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
join(testDir, "../src/scr_bytes.c"),
|
||||
join(testDir, "../src/scr_shared.c"),
|
||||
join(testDir, "../src/scr_lib.c"),
|
||||
|
||||
@@ -29,6 +29,7 @@ beforeAll(async () => {
|
||||
join(testDir, "../src/scr_bigint.c"),
|
||||
join(testDir, "../src/scr_string.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
join(testDir, "../src/scr_exception.c"),
|
||||
join(testDir, "../src/scr_error.c"),
|
||||
|
||||
@@ -35,6 +35,7 @@ beforeAll(async () => {
|
||||
join(testDir, "../src/scr_path.c"),
|
||||
join(testDir, "../src/scr_string.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
join(testDir, "../src/scr_exception.c"),
|
||||
join(testDir, "../src/scr_error.c"),
|
||||
|
||||
@@ -33,6 +33,7 @@ beforeAll(async () => {
|
||||
// which pull in the closure/array/box machinery
|
||||
join(testDir, "../src/scr_union.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
// Checked-value collection equality shares the native JSON value runtime.
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
join(testDir, "../src/scr_bigint.c"),
|
||||
|
||||
@@ -29,6 +29,7 @@ beforeAll(async () => {
|
||||
join(testDir, "../src/scr_string.c"),
|
||||
join(testDir, "../src/scr_number.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
// Checked-value collection equality shares the native JSON value runtime.
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
|
||||
@@ -36,6 +36,7 @@ beforeAll(async () => {
|
||||
join(testDir, "../src/scr_string.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
join(testDir, "../src/scr_bigint.c"),
|
||||
join(testDir, "../src/scr_exception.c"),
|
||||
|
||||
@@ -13,6 +13,7 @@ const RUNTIME_SOURCES = [
|
||||
"scr_bigint.c",
|
||||
"scr_string.c",
|
||||
"scr_array.c",
|
||||
"scr_arena.c",
|
||||
"scr_bytes.c",
|
||||
"scr_shared.c",
|
||||
"scr_bytes_io.c",
|
||||
|
||||
@@ -19,6 +19,7 @@ const runtimeSources = [
|
||||
"scr_hash.c",
|
||||
"scr_string.c",
|
||||
"scr_array.c",
|
||||
"scr_arena.c",
|
||||
"scr_map.c",
|
||||
"scr_exception.c",
|
||||
"scr_error.c",
|
||||
|
||||
@@ -24,6 +24,7 @@ beforeAll(async () => {
|
||||
bin,
|
||||
join(testDir, "test_sort.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
// Checked-value collection equality shares the native JSON value runtime.
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
|
||||
@@ -30,6 +30,7 @@ beforeAll(async () => {
|
||||
// scr_str_split returns a real string[]: the array module and its
|
||||
// own dependencies join the link (the array.test.ts set).
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
// Checked-value collection equality shares the native JSON value runtime.
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
|
||||
@@ -37,6 +37,7 @@ test("scr_string_to_number and scr_parse_float match Node on committed oracle ca
|
||||
// scr_string.c's own link closure (the string.test.ts set):
|
||||
// scr_str_split pulls the array module and its dependencies.
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
// Checked-value collection equality shares the native JSON value runtime.
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
|
||||
@@ -38,6 +38,7 @@ beforeAll(async () => {
|
||||
join(testDir, "../src/scr_string.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
join(testDir, "../src/scr_bigint.c"),
|
||||
join(testDir, "../src/scr_exception.c"),
|
||||
|
||||
@@ -29,6 +29,7 @@ beforeAll(async () => {
|
||||
join(testDir, "../src/scr_string.c"),
|
||||
join(testDir, "../src/scr_number.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_arena.c"),
|
||||
// Checked-value collection equality shares the native JSON value runtime.
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Builders whose groups hold instances of a class that extends Array,
|
||||
// beside a builder whose group takes an arena. An Array subclass's
|
||||
// instance is an array, so a group that allocates one keeps its objects on
|
||||
// the heap; the results and their teardown match Node either way.
|
||||
class Row extends Array<number> {
|
||||
sum(): number {
|
||||
let s = 0;
|
||||
for (const v of this) s += v;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
class Branch {
|
||||
row: Row;
|
||||
left: Branch | null;
|
||||
right: Branch | null;
|
||||
constructor(row: Row, left: Branch | null, right: Branch | null) {
|
||||
this.row = row;
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
total(): number {
|
||||
return this.row.sum() + (this.left ? this.left.total() : 0) + (this.right ? this.right.total() : 0);
|
||||
}
|
||||
}
|
||||
|
||||
class Pair {
|
||||
left: Pair | null;
|
||||
right: Pair | null;
|
||||
constructor(left: Pair | null, right: Pair | null) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
count(): number {
|
||||
return 1 + (this.left ? this.left.count() : 0) + (this.right ? this.right.count() : 0);
|
||||
}
|
||||
}
|
||||
|
||||
function row(n: number): Row {
|
||||
const r = new Row();
|
||||
for (let j = 0; j <= n; j++) r.push(j);
|
||||
return r;
|
||||
}
|
||||
|
||||
function branches(depth: number): Branch {
|
||||
return depth === 0
|
||||
? new Branch(row(depth), null, null)
|
||||
: new Branch(row(depth), branches(depth - 1), branches(depth - 1));
|
||||
}
|
||||
|
||||
function pairs(depth: number): Pair {
|
||||
return depth === 0 ? new Pair(null, null) : new Pair(pairs(depth - 1), pairs(depth - 1));
|
||||
}
|
||||
|
||||
let checks = 0;
|
||||
for (let round = 0; round < 20; round++) {
|
||||
const b = branches(6);
|
||||
const p = pairs(8);
|
||||
const r = row(round);
|
||||
checks += b.total() + p.count() + r.sum() + r.length;
|
||||
if (round === 0) console.log(b.total(), b.row.length, b.row instanceof Row, p.count(), r.sum());
|
||||
}
|
||||
console.log(checks);
|
||||
@@ -0,0 +1,49 @@
|
||||
// Groups with arrays: child lists that grow past their first storage,
|
||||
// back references to parents and siblings, and lists of lists.
|
||||
class Node {
|
||||
parent: Node | undefined = undefined;
|
||||
children: Node[] = [];
|
||||
sibling: Node | undefined = undefined;
|
||||
weight: number;
|
||||
constructor(weight: number) {
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
|
||||
function wide(depth: number, fan: number, parent: Node | undefined, weight: number): Node {
|
||||
const node = new Node(weight);
|
||||
node.parent = parent;
|
||||
if (depth > 0) {
|
||||
for (let i = 0; i < fan; i++) node.children.push(wide(depth - 1, fan, node, weight * 3 + i));
|
||||
for (let i = 0; i < fan; i++) node.children[i]!.sibling = node.children[(i + 1) % fan]!;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function weigh(node: Node): number {
|
||||
let total = node.weight;
|
||||
for (const child of node.children) total += weigh(child);
|
||||
return total % 1000003;
|
||||
}
|
||||
|
||||
let w = 0;
|
||||
for (let round = 0; round < 30; round++) w = (w + weigh(wide(2 + (round % 3), 3 + (round % 20), undefined, round))) % 1000003;
|
||||
console.log("weights", w);
|
||||
|
||||
function grid(rows: number, cols: number): number[][] {
|
||||
const out: number[][] = [];
|
||||
for (let r = 0; r < rows; r++) {
|
||||
const row: number[] = [];
|
||||
for (let c = 0; c < cols; c++) row.push(r * cols + c);
|
||||
out.push(row);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
let cells = 0;
|
||||
for (let k = 1; k < 40; k++) {
|
||||
const g = grid(k, 70);
|
||||
cells += g.length + g[k - 1]![69]! + g[0]!.length;
|
||||
}
|
||||
console.log("cells", cells);
|
||||
const t = wide(2, 4, undefined, 1);
|
||||
console.log(t.children.length, t.children[1]!.parent === t, t.children[3]!.sibling === t.children[0], t.children[2]!.children[3]!.weight);
|
||||
@@ -0,0 +1,30 @@
|
||||
// A group too large for one arena: past its byte cap the rest of the
|
||||
// group goes to the heap, and both parts are released together.
|
||||
interface Cell {
|
||||
index: number;
|
||||
label: string;
|
||||
next: Cell | null;
|
||||
}
|
||||
|
||||
function list(n: number): Cell {
|
||||
let head: Cell = { index: 0, label: "c0", next: null };
|
||||
const first = head;
|
||||
for (let i = 1; i < n; i++) {
|
||||
const cell: Cell = { index: i, label: i % 1000 === 0 ? `c${i}` : "c", next: null };
|
||||
head.next = cell;
|
||||
head = cell;
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
function walk(c: Cell | null): number {
|
||||
let total = 0;
|
||||
let labels = 0;
|
||||
for (let at = c; at !== null; at = at.next) {
|
||||
total += at.index;
|
||||
if (at.label.length > 1) labels++;
|
||||
}
|
||||
return total + labels;
|
||||
}
|
||||
|
||||
for (let round = 0; round < 3; round++) console.log("cells", walk(list(900000 + round)));
|
||||
@@ -0,0 +1,72 @@
|
||||
// Values built in a group that leave it: handed to callbacks that keep
|
||||
// them, stored by a method into a module list, kept from a getter, pushed
|
||||
// into a caller's list, or captured by a returned closure. None of those
|
||||
// groups takes an arena; the one whose values stay in the frame does.
|
||||
class Node {
|
||||
kids: Node[] = [];
|
||||
label: string;
|
||||
constructor(label: string) {
|
||||
this.label = label;
|
||||
}
|
||||
get first(): Node {
|
||||
return this.kids[0]!;
|
||||
}
|
||||
register(): void {
|
||||
registry.push(this);
|
||||
}
|
||||
}
|
||||
const registry: Node[] = [];
|
||||
const saved: Node[] = [];
|
||||
const held = new Map<number, Node>();
|
||||
function build(depth: number, label: string): Node {
|
||||
const n = new Node(label);
|
||||
for (let i = 0; depth > 0 && i < 3; i++) n.kids.push(build(depth - 1, `${label}.${i}`));
|
||||
return n;
|
||||
}
|
||||
function each(depth: number, cb: (n: Node) => void): number {
|
||||
const t = build(depth, "e");
|
||||
cb(t);
|
||||
return t.kids.length;
|
||||
}
|
||||
function visit(n: Node, cb: (x: Node) => void): void {
|
||||
cb(n);
|
||||
for (const k of n.kids) visit(k, cb);
|
||||
}
|
||||
function forward(depth: number, cb: (x: Node) => void): number {
|
||||
const t = build(depth, "f");
|
||||
visit(t, cb);
|
||||
return t.kids.length;
|
||||
}
|
||||
function buildInto(sink: Node[], depth: number): number {
|
||||
const n = build(depth, "s");
|
||||
sink.push(n.kids[0]!);
|
||||
return n.kids.length;
|
||||
}
|
||||
function closureOf(depth: number): () => string {
|
||||
const n = build(depth, "c");
|
||||
return () => n.kids[1]!.label;
|
||||
}
|
||||
let total = 0;
|
||||
for (let r = 0; r < 8; r++) {
|
||||
total += each(2, (n) => saved.push(n.kids[1]!));
|
||||
total += forward(2, (x) => {
|
||||
if (x.label.endsWith(".2")) saved.push(x);
|
||||
});
|
||||
const t = build(2, `t${r}`);
|
||||
t.kids.forEach((k) => {
|
||||
if (k.label.endsWith(".0")) saved.push(k);
|
||||
});
|
||||
const u = build(2, `u${r}`);
|
||||
for (const k of u.kids) if (k.label.endsWith(".1")) saved.push(k);
|
||||
const v = build(2, `v${r}`);
|
||||
held.set(r, v.first);
|
||||
const a = build(2, `a${r}`);
|
||||
if (r % 3 === 0) a.kids[0]!.register();
|
||||
total += buildInto(saved, 2);
|
||||
const w = build(2, `w${r}`);
|
||||
total += w.kids.length + w.first.kids.length;
|
||||
}
|
||||
console.log(total, saved.length, saved.map((n) => n.label + "/" + n.kids.length).slice(0, 10).join(","));
|
||||
const f = closureOf(2);
|
||||
console.log(f(), registry.map((n) => n.label + n.kids.length).join(","));
|
||||
console.log([...held.values()].map((n) => n.label + n.kids[0]!.label).join(","));
|
||||
@@ -0,0 +1,80 @@
|
||||
// Groups built through virtual calls and sorted with callbacks, nested
|
||||
// groups inside a group, and temporaries the group does not keep.
|
||||
abstract class Shape {
|
||||
abstract area(): number;
|
||||
abstract scaled(k: number): Shape;
|
||||
}
|
||||
class Square extends Shape {
|
||||
side: number;
|
||||
constructor(side: number) {
|
||||
super();
|
||||
this.side = side;
|
||||
}
|
||||
area(): number {
|
||||
return this.side * this.side;
|
||||
}
|
||||
scaled(k: number): Shape {
|
||||
return new Square(this.side * k);
|
||||
}
|
||||
}
|
||||
class Circle extends Shape {
|
||||
radius: number;
|
||||
constructor(radius: number) {
|
||||
super();
|
||||
this.radius = radius;
|
||||
}
|
||||
area(): number {
|
||||
return Math.round(Math.PI * this.radius * this.radius * 100) / 100;
|
||||
}
|
||||
scaled(k: number): Shape {
|
||||
return new Circle(this.radius * k);
|
||||
}
|
||||
}
|
||||
|
||||
interface Box {
|
||||
label: string;
|
||||
shapes: Shape[];
|
||||
}
|
||||
|
||||
function measure(s: Shape): { area: number } {
|
||||
return { area: s.area() };
|
||||
}
|
||||
|
||||
function boxes(n: number): Box[] {
|
||||
const out: Box[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const shapes: Shape[] = [];
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const base: Shape = j % 2 === 0 ? new Square(j + i) : new Circle(j + 1);
|
||||
shapes.push(base.scaled(2));
|
||||
}
|
||||
// Temporaries the group does not keep.
|
||||
let total = 0;
|
||||
for (const s of shapes) total += measure(s).area;
|
||||
shapes.sort((a, b) => a.area() - b.area());
|
||||
out.push({ label: `box${i}:${Math.round(total)}`, shapes });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function summarize(n: number): string {
|
||||
const list = boxes(n);
|
||||
let largest = "";
|
||||
let best = -1;
|
||||
for (const b of list) {
|
||||
const top = b.shapes[b.shapes.length - 1]!.area();
|
||||
if (top > best) {
|
||||
best = top;
|
||||
largest = b.label;
|
||||
}
|
||||
}
|
||||
return `${largest}@${best}`;
|
||||
}
|
||||
|
||||
let out = "";
|
||||
for (let round = 1; round < 30; round++) {
|
||||
const all = boxes(round);
|
||||
out = `${all.length} ${all[0]!.label} ${all[round - 1]!.shapes[0]!.area()} ${summarize(round)}`;
|
||||
if (round % 7 === 0) console.log(out);
|
||||
}
|
||||
console.log(out);
|
||||
@@ -0,0 +1,49 @@
|
||||
// Groups whose members refer to objects outside them: a long-lived
|
||||
// registry, strings built along the way, and a parameter's object. The
|
||||
// arena's drop releases those references; the outside objects stay usable.
|
||||
class Category {
|
||||
members = 0;
|
||||
name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
class Member {
|
||||
next: Member | undefined = undefined;
|
||||
category: Category;
|
||||
label: string;
|
||||
rank: number;
|
||||
constructor(category: Category, label: string, rank: number) {
|
||||
this.category = category;
|
||||
this.label = label;
|
||||
this.rank = rank;
|
||||
category.members++;
|
||||
}
|
||||
}
|
||||
|
||||
function ring(categories: Category[], size: number, seed: number): Member {
|
||||
const first = new Member(categories[seed % categories.length]!, `m${seed}`, seed);
|
||||
let last = first;
|
||||
for (let i = 1; i < size; i++) {
|
||||
const member = new Member(categories[(seed + i * 7) % categories.length]!, `m${seed}-${i}`, seed + i);
|
||||
last.next = member;
|
||||
last = member;
|
||||
}
|
||||
last.next = first;
|
||||
return first;
|
||||
}
|
||||
|
||||
const categories: Category[] = [];
|
||||
for (let i = 0; i < 5; i++) categories.push(new Category(`cat-${i}`));
|
||||
|
||||
let ranks = 0;
|
||||
let labels = "";
|
||||
for (let round = 0; round < 200; round++) {
|
||||
const r = ring(categories, 30, round);
|
||||
ranks += r.next!.rank + r.next!.next!.rank;
|
||||
if (round % 50 === 0) labels += `${r.label}:${r.category.name}:${r.next!.label} `;
|
||||
}
|
||||
console.log("ranks", ranks);
|
||||
console.log("labels", labels.trim());
|
||||
for (const c of categories) console.log(c.name, c.members);
|
||||
@@ -0,0 +1,33 @@
|
||||
// Exceptions inside call regions: the region's arena is dropped as the
|
||||
// exception leaves the call, and the caller's own objects are untouched.
|
||||
interface Entry {
|
||||
key: string;
|
||||
value: number;
|
||||
next: Entry | null;
|
||||
}
|
||||
|
||||
function tally(keys: string[], limit: number): number {
|
||||
const entries: Entry[] = [];
|
||||
for (const k of keys) {
|
||||
entries.push({ key: k, value: k.length, next: entries.length > 0 ? entries[entries.length - 1]! : null });
|
||||
if (entries.length > limit) throw new RangeError(`too many entries: ${entries.length}`);
|
||||
}
|
||||
let sum = 0;
|
||||
for (const e of entries) sum += e.value + (e.next === null ? 0 : e.next.value);
|
||||
return sum;
|
||||
}
|
||||
|
||||
const kept: Entry[] = [{ key: "outside", value: 1, next: null }];
|
||||
const keys = "alpha beta gamma delta epsilon zeta eta theta iota kappa".split(" ");
|
||||
let total = 0;
|
||||
let errors = 0;
|
||||
for (let round = 0; round < 40; round++) {
|
||||
try {
|
||||
total += tally(keys, 3 + (round % 10));
|
||||
} catch (e) {
|
||||
errors++;
|
||||
if (round < 6) console.log((e as Error).name, (e as Error).message);
|
||||
}
|
||||
kept.push({ key: `k${round}`, value: round, next: kept[kept.length - 1]! });
|
||||
}
|
||||
console.log("total", total, "errors", errors, "kept", kept.length, kept[kept.length - 1]!.next!.key);
|
||||
@@ -0,0 +1,44 @@
|
||||
// Call regions: calls of functions that build what they need and return
|
||||
// a number, so everything they allocate is dead when they return.
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
interface Segment {
|
||||
from: Point;
|
||||
to: Point;
|
||||
}
|
||||
|
||||
function perimeter(n: number, scale: number): number {
|
||||
const points: Point[] = [];
|
||||
for (let i = 0; i < n; i++) points.push({ x: Math.cos(i) * scale, y: Math.sin(i) * scale });
|
||||
const segments: Segment[] = [];
|
||||
for (let i = 0; i < n; i++) segments.push({ from: points[i]!, to: points[(i + 1) % n]! });
|
||||
let total = 0;
|
||||
for (const s of segments) total += Math.hypot(s.to.x - s.from.x, s.to.y - s.from.y);
|
||||
return Math.round(total * 1000) / 1000;
|
||||
}
|
||||
|
||||
let sum = 0;
|
||||
for (let k = 3; k < 300; k++) sum += perimeter(k, k / 10);
|
||||
console.log("perimeters", sum.toFixed(3));
|
||||
|
||||
function histogram(words: string[]): number {
|
||||
const buckets: { word: string; count: number }[] = [];
|
||||
for (const w of words) {
|
||||
let found = false;
|
||||
for (const b of buckets)
|
||||
if (b.word === w) {
|
||||
b.count++;
|
||||
found = true;
|
||||
}
|
||||
if (!found) buckets.push({ word: w, count: 1 });
|
||||
}
|
||||
let best = 0;
|
||||
for (const b of buckets) best = Math.max(best, b.count * 100 + b.word.length);
|
||||
return best;
|
||||
}
|
||||
const words = "the quick brown fox jumps over the lazy dog the end".split(" ");
|
||||
let h = 0;
|
||||
for (let i = 0; i < 200; i++) h += histogram(words);
|
||||
console.log("histogram", h);
|
||||
@@ -0,0 +1,36 @@
|
||||
// A thrown value that holds an object built in a group: the group cannot
|
||||
// take an arena, because the value outlives it in the catch handler.
|
||||
class Node {
|
||||
label: string;
|
||||
weight: number;
|
||||
constructor(label: string, weight: number) {
|
||||
this.label = label;
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
class BuildError {
|
||||
node: Node;
|
||||
constructor(node: Node) {
|
||||
this.node = node;
|
||||
}
|
||||
}
|
||||
function build(n: number, failAt: number): Node[] {
|
||||
const out: Node[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const node = new Node(`n${i}`, i);
|
||||
out.push(node);
|
||||
if (i === failAt) throw new BuildError(node);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
let total = 0;
|
||||
const saved: string[] = [];
|
||||
for (let r = 0; r < 20; r++) {
|
||||
try {
|
||||
total += build(50, r % 4 === 0 ? 30 : -1).length;
|
||||
} catch (e) {
|
||||
const b = e as BuildError;
|
||||
saved.push(b.node.label + ":" + b.node.weight);
|
||||
}
|
||||
}
|
||||
console.log(total, saved.join(","));
|
||||
@@ -0,0 +1,40 @@
|
||||
// A builder that throws part way: the arena of the group it was building
|
||||
// is dropped as the exception leaves, and building goes on afterwards.
|
||||
interface Item {
|
||||
id: number;
|
||||
tags: string[];
|
||||
next: Item | null;
|
||||
}
|
||||
|
||||
function chain(n: number, failAt: number): Item {
|
||||
let head: Item = { id: 0, tags: ["head"], next: null };
|
||||
const first = head;
|
||||
for (let i = 1; i < n; i++) {
|
||||
if (i === failAt) throw new Error(`failed at ${i} of ${n}`);
|
||||
const item: Item = { id: i, tags: [`t${i}`, `u${i % 3}`], next: null };
|
||||
head.next = item;
|
||||
head = item;
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
function length(item: Item | null): number {
|
||||
let n = 0;
|
||||
for (let at = item; at !== null; at = at.next) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
let built = 0;
|
||||
let failed = 0;
|
||||
for (let round = 0; round < 60; round++) {
|
||||
try {
|
||||
const c = chain(20 + (round % 7), round % 5 === 0 ? 10 + (round % 3) : -1);
|
||||
built += length(c) + c.next!.tags.length;
|
||||
} catch (e) {
|
||||
failed++;
|
||||
if (round < 20) console.log("caught", (e as Error).message);
|
||||
}
|
||||
}
|
||||
console.log("built", built, "failed", failed);
|
||||
const last = chain(5, -1);
|
||||
console.log(last.id, last.next!.id, last.next!.tags.join("/"), length(last));
|
||||
@@ -0,0 +1,70 @@
|
||||
// Recursive builders whose results their callers only read: each tree is a
|
||||
// group that lives in an arena until the statement (or the loop body that
|
||||
// holds it) ends.
|
||||
class TreeNode {
|
||||
left: TreeNode | null;
|
||||
right: TreeNode | null;
|
||||
value: number;
|
||||
constructor(left: TreeNode | null, right: TreeNode | null, value: number) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.value = value;
|
||||
}
|
||||
sum(): number {
|
||||
return this.value + (this.left === null ? 0 : this.left.sum()) + (this.right === null ? 0 : this.right.sum());
|
||||
}
|
||||
depth(): number {
|
||||
return 1 + Math.max(this.left === null ? 0 : this.left.depth(), this.right === null ? 0 : this.right.depth());
|
||||
}
|
||||
}
|
||||
|
||||
function build(depth: number, value: number): TreeNode {
|
||||
return depth === 0
|
||||
? new TreeNode(null, null, value)
|
||||
: new TreeNode(build(depth - 1, value * 2), build(depth - 1, value * 2 + 1), value);
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (let d = 0; d < 12; d++) total += build(d, 1).sum();
|
||||
console.log("sums", total);
|
||||
|
||||
for (let d = 2; d <= 4; d++) {
|
||||
const t = build(d, 1);
|
||||
console.log("tree", d, t.sum(), t.depth(), t.left!.value, t.right!.right!.value);
|
||||
}
|
||||
|
||||
function deepest(n: number): number {
|
||||
let best = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = build(i % 6, i);
|
||||
best = Math.max(best, t.depth() * 1000 + (t.sum() % 1000));
|
||||
}
|
||||
return best;
|
||||
}
|
||||
console.log("deepest", deepest(40));
|
||||
|
||||
// Record-literal trees: a root call whose result its caller only reads
|
||||
// builds the whole tree, its root node included, in the group's arena.
|
||||
interface Node2 {
|
||||
left: Node2 | null;
|
||||
right: Node2 | null;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function buildRecord(depth: number, value: number): Node2 {
|
||||
return depth === 0
|
||||
? { left: null, right: null, value }
|
||||
: { left: buildRecord(depth - 1, value * 2), right: buildRecord(depth - 1, value * 2 + 1), value };
|
||||
}
|
||||
|
||||
function sumRecord(node: Node2): number {
|
||||
return node.value + (node.left === null ? 0 : sumRecord(node.left)) + (node.right === null ? 0 : sumRecord(node.right));
|
||||
}
|
||||
|
||||
let recordTotal = 0;
|
||||
for (let d = 0; d < 12; d++) recordTotal += sumRecord(buildRecord(d, 1));
|
||||
console.log("record sums", recordTotal);
|
||||
for (let d = 1; d <= 3; d++) {
|
||||
const t = buildRecord(d, 1);
|
||||
console.log("record tree", d, sumRecord(t), t.value, t.left!.value, t.right!.value);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Arena groups in worker threads: each worker builds and reads trees of
|
||||
// classes and record literals, groups that live in the worker's own arena
|
||||
// until the statement or loop body that holds them ends. The main thread
|
||||
// builds its own groups meanwhile and keeps one tree for the whole run.
|
||||
import { isMainThread, parentPort, Worker, workerData } from "node:worker_threads";
|
||||
|
||||
class Pair {
|
||||
left: Pair | null;
|
||||
right: Pair | null;
|
||||
label: string;
|
||||
constructor(left: Pair | null, right: Pair | null, label: string) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
|
||||
interface Leaf {
|
||||
left: Leaf | null;
|
||||
right: Leaf | null;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
function pairs(depth: number, label: string): Pair {
|
||||
return depth === 0 ? new Pair(null, null, label) : new Pair(pairs(depth - 1, label + "l"), pairs(depth - 1, label + "r"), label);
|
||||
}
|
||||
|
||||
function leaves(depth: number, weight: number): Leaf {
|
||||
return depth === 0
|
||||
? { left: null, right: null, weight }
|
||||
: { left: leaves(depth - 1, weight + 1), right: leaves(depth - 1, weight * 2), weight };
|
||||
}
|
||||
|
||||
function countPairs(p: Pair): number {
|
||||
return 1 + (p.left === null ? 0 : countPairs(p.left)) + (p.right === null ? 0 : countPairs(p.right));
|
||||
}
|
||||
|
||||
function rightmost(p: Pair): string {
|
||||
return p.right === null ? p.label : rightmost(p.right);
|
||||
}
|
||||
|
||||
function weigh(l: Leaf): number {
|
||||
return l.weight + (l.left === null ? 0 : weigh(l.left)) + (l.right === null ? 0 : weigh(l.right));
|
||||
}
|
||||
|
||||
function task(depth: number, rounds: number): string {
|
||||
let nodes = 0;
|
||||
let total = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
nodes += countPairs(pairs(depth, "p"));
|
||||
total += weigh(leaves(depth, i));
|
||||
}
|
||||
const kept = pairs(depth, "k");
|
||||
return `depth ${depth}: ${nodes} pairs, weight ${total}, rightmost ${rightmost(kept)}, kept ${countPairs(kept)}`;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
const longLived = leaves(10, 1);
|
||||
const depths = [3, 5, 7, 9];
|
||||
const lines: string[] = new Array(depths.length).fill("");
|
||||
let pending = depths.length;
|
||||
depths.forEach((depth, index) => {
|
||||
const worker = new Worker(new URL(import.meta.url), { workerData: depth });
|
||||
worker.on("message", (line: string) => {
|
||||
lines[index] = line;
|
||||
if (--pending === 0) {
|
||||
for (const l of lines) console.log(l);
|
||||
console.log("main", task(6, 20));
|
||||
console.log("long lived", weigh(longLived));
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
parentPort!.postMessage(task(workerData as number, 50));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3100 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3600, "san": 18500 }
|
||||
Reference in New Issue
Block a user