mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Merge main
This commit is contained in:
@@ -13,6 +13,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
|
||||
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
|
||||
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
|
||||
| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass |
|
||||
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/dynamic-properties.mjs",
|
||||
"args": [
|
||||
"1000000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Untyped JavaScript reading and writing properties of values it never
|
||||
// declares types for: records built by object literals in two key orders,
|
||||
// nested records, property updates, and instances of a class and its
|
||||
// subclass read and written the same way. Argument: the number of
|
||||
// iterations.
|
||||
const rounds = Number(process.argv[2] ?? "1000000");
|
||||
class Cursor {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
step(dx, dy) {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
class Marker extends Cursor {
|
||||
constructor(x, y, label) {
|
||||
super(x, y);
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
function makeItem(i) {
|
||||
if (i % 3 === 0)
|
||||
return { id: i, price: i % 17, qty: i % 5, meta: { tag: i % 7, weight: i % 11 } };
|
||||
return { id: i, qty: i % 5, price: i % 13, meta: { weight: i % 9, tag: i % 5 } };
|
||||
}
|
||||
function cost(item) {
|
||||
return item.price * item.qty + item.meta.weight;
|
||||
}
|
||||
function restock(item, round) {
|
||||
item.qty = (item.qty + round) % 7;
|
||||
item.meta.tag = (item.meta.tag + 1) % 7;
|
||||
}
|
||||
function distance(a, b) {
|
||||
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
||||
}
|
||||
const items = [];
|
||||
for (let i = 0; i < 1024; i++) items.push(makeItem(i));
|
||||
const cursors = [];
|
||||
for (let i = 0; i < 64; i++)
|
||||
cursors.push(i % 2 === 0 ? new Cursor(i - 32, 7 - i) : new Marker(i - 32, 7 - i, "m" + i));
|
||||
const origin = { x: 0, y: 0 };
|
||||
let checksum = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
const item = items[r & 1023];
|
||||
checksum += cost(item);
|
||||
restock(item, r & 3);
|
||||
checksum += item.meta.tag;
|
||||
const cursor = cursors[r & 63];
|
||||
checksum += distance(cursor.step((r % 3) - 1, (r % 5) - 2), origin);
|
||||
if (cursor.x > 100 || cursor.x < -100) cursor.x = 0;
|
||||
if (cursor.y > 100 || cursor.y < -100) cursor.y = 0;
|
||||
}
|
||||
console.log(checksum, items[5].qty, items[6].meta.tag, cursors[3].x, cursors[3].y, cursors[3].label);
|
||||
Binary file not shown.
@@ -159,6 +159,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.optionalArrayReads,
|
||||
emitter.referenceWrites,
|
||||
emitter.fnByName,
|
||||
emitter.nodeIndex,
|
||||
)
|
||||
: new Set();
|
||||
emitter.localArrayReads = findLocalArrayReads(
|
||||
@@ -324,7 +325,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
|
||||
const paramIds = new Set(fn.params.map((p) => p.localId));
|
||||
if (emitter.debug === null) {
|
||||
for (const id of findIntegerViews(numericFn)) {
|
||||
for (const id of findIntegerViews(numericFn, emitter.nodeIndex)) {
|
||||
// An i32 slot is its own view.
|
||||
if (emitter.int32Locals.has(id)) continue;
|
||||
const slot = B.slot();
|
||||
|
||||
@@ -176,7 +176,7 @@ import { type MapEntryFusion } from "./map-entry-fusion.js";
|
||||
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
|
||||
import { emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import { findWalkBorrows, type WalkBorrowHost } from "./walk-borrows.js";
|
||||
import { IrNodeIndex } from "../../ir/node-index.js";
|
||||
import { eachNodeOfKinds, IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
import { lastUseRead } from "./local-extents.js";
|
||||
import { findReboundParameters } from "./rebound-parameters.js";
|
||||
import { ReferenceEffects } from "./reference-effects.js";
|
||||
@@ -621,6 +621,11 @@ export function llStrBytes(text: string): string {
|
||||
|
||||
const NO_BORROWED: ReadonlySet<number> = new Set();
|
||||
|
||||
/** The nodes that write a binding by its id (numberGlobalWriters). */
|
||||
const GLOBAL_WRITE_KINDS: readonly IrNodeKind[] = ["assign", "varDecl", "assignExpr", "incDec"];
|
||||
/** Runtime library calls (readsLoopMetrics). */
|
||||
const LIB_CALL_KINDS: readonly IrNodeKind[] = ["libCall"];
|
||||
|
||||
/** [A-Za-z0-9_] */
|
||||
function isIdentifierByte(code: number): boolean {
|
||||
return (
|
||||
@@ -1842,9 +1847,9 @@ export class LlEmitter {
|
||||
)
|
||||
: NO_INTEGER_CLONES;
|
||||
this.stackCallbacks = new StackCallbacks(this.fnByName);
|
||||
this.receiverReaders = new AmbientReceiverReaders(this.fnByName);
|
||||
this.receiverReaders = new AmbientReceiverReaders(this.fnByName, this.nodeIndex);
|
||||
this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
|
||||
this.lazyCaptures = new LazyCaptures(this.fnByName);
|
||||
this.lazyCaptures = new LazyCaptures(this.fnByName, this.nodeIndex);
|
||||
const traced = computeTraced(mod, dynamicStores, cycleMutability);
|
||||
this.tracedShapes = traced.shapes;
|
||||
this.tracedUnions = traced.unions;
|
||||
@@ -3621,15 +3626,14 @@ export class LlEmitter {
|
||||
set.add(fn.name);
|
||||
writers.set(localId, set);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (s) => {
|
||||
if (s.kind === "assign" || s.kind === "varDecl") note(s.localId);
|
||||
return true;
|
||||
},
|
||||
expr: (e) => {
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") note(e.localId);
|
||||
return true;
|
||||
},
|
||||
eachNodeOfKinds(fn, this.nodeIndex.scan(GLOBAL_WRITE_KINDS), (node) => {
|
||||
if (
|
||||
node.kind === "assign" ||
|
||||
node.kind === "varDecl" ||
|
||||
node.kind === "assignExpr" ||
|
||||
node.kind === "incDec"
|
||||
)
|
||||
note(node.localId);
|
||||
});
|
||||
}
|
||||
this.globalWriters = writers;
|
||||
@@ -5409,13 +5413,21 @@ export class LlEmitter {
|
||||
private loopMetricsRead: boolean | undefined;
|
||||
readsLoopMetrics(): boolean {
|
||||
if (this.loopMetricsRead !== undefined) return this.loopMetricsRead;
|
||||
const visitor = {
|
||||
expr: (e: IrExpr): boolean =>
|
||||
!(e.kind === "libCall" && (e.fn === "perf.loopStart" || e.fn === "perf.idleTime")),
|
||||
stmt: (): boolean => true,
|
||||
};
|
||||
this.loopMetricsRead = !this.mod.functions.every((fn) => everyStmtList(fn.body, visitor));
|
||||
return this.loopMetricsRead;
|
||||
let found = false;
|
||||
const scan = this.nodeIndex.scan(LIB_CALL_KINDS);
|
||||
for (const fn of this.mod.functions) {
|
||||
eachNodeOfKinds(fn, scan, (node) => {
|
||||
if (
|
||||
node.kind === "libCall" &&
|
||||
(node.fn === "perf.loopStart" || node.fn === "perf.idleTime")
|
||||
)
|
||||
found = true;
|
||||
return !found;
|
||||
});
|
||||
if (found) break;
|
||||
}
|
||||
this.loopMetricsRead = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
private lastUseMovesAllowed(): boolean {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
const LOOP_KINDS: readonly IrNodeKind[] = ["for", "forOf", "while", "doWhile"];
|
||||
|
||||
/** Keep a computed ToUint32 view beside an ordinary number when integer
|
||||
* consumers in loops can reuse it. The number remains authoritative for floating
|
||||
@@ -14,9 +17,16 @@ import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
* compare converts the stored number back, and LLVM moves that number's
|
||||
* conversion into the join of a NaN-coded typed-array read (`phi [sitofp x,
|
||||
* 0.0]`), where nothing folds it back to the integer. */
|
||||
export function findIntegerViews(fn: IrFunction): Set<string> {
|
||||
export function findIntegerViews(fn: IrFunction, nodes?: IrNodeIndex): Set<string> {
|
||||
const wanted = new Set<string>();
|
||||
if (fn.async || fn.generator) return wanted;
|
||||
// Every view is wanted inside a loop: a body without one wants none.
|
||||
let loops = false;
|
||||
eachNodeOfKinds(fn, nodes?.scan(LOOP_KINDS), (node, statement) => {
|
||||
if (statement && LOOP_KINDS.includes(node.kind)) loops = true;
|
||||
return !loops;
|
||||
});
|
||||
if (!loops) return wanted;
|
||||
const initialized = new Set(fn.params.map((p) => p.localId));
|
||||
const excluded = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
const CLOSURE_KINDS: readonly IrNodeKind[] = ["closure"];
|
||||
|
||||
/** The local a node rebinds. Writes through the bound reference, such as a
|
||||
* field store or an array push, leave the binding unchanged. Catch bindings
|
||||
@@ -37,7 +40,11 @@ export class LazyCaptures {
|
||||
private readonly pending = new Set<IrFunction>();
|
||||
private sites: Map<string, { owner: string; captures: string[] }[]> | null = null;
|
||||
|
||||
constructor(private readonly functions: ReadonlyMap<string, IrFunction>) {}
|
||||
constructor(
|
||||
private readonly functions: ReadonlyMap<string, IrFunction>,
|
||||
/** The module's node index, read instead of walking the bodies it holds. */
|
||||
private readonly nodes?: IrNodeIndex,
|
||||
) {}
|
||||
|
||||
/** The captures of closure body `fnName` that hold one value for the
|
||||
* closure's whole life: at every creation site the captured binding is
|
||||
@@ -103,16 +110,15 @@ export class LazyCaptures {
|
||||
private creationSites(): Map<string, { owner: string; captures: string[] }[]> {
|
||||
if (this.sites !== null) return this.sites;
|
||||
const sites = new Map<string, { owner: string; captures: string[] }[]>();
|
||||
const scan = this.nodes?.scan(CLOSURE_KINDS);
|
||||
for (const fn of this.functions.values()) {
|
||||
const visit = (node: IrStmt | IrExpr): boolean => {
|
||||
eachNodeOfKinds(fn, scan, (node) => {
|
||||
if (node.kind === "closure") {
|
||||
let list = sites.get(node.fnName);
|
||||
if (!list) sites.set(node.fnName, (list = []));
|
||||
list.push({ owner: fn.name, captures: node.captures });
|
||||
}
|
||||
return true;
|
||||
};
|
||||
everyStmtList(fn.body, { stmt: visit, expr: visit });
|
||||
});
|
||||
}
|
||||
this.sites = sites;
|
||||
return sites;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { F64, isRefCounted, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { ReferenceEffects } from "./reference-effects.js";
|
||||
import type { OptionalArrayReads } from "./local-array-reads.js";
|
||||
@@ -57,6 +58,8 @@ export function dynHoleRead(value: IrExpr): (IrExpr & { kind: "arrayGet" }) | nu
|
||||
return same(cond.left.arr, then.arr) && same(cond.left.index, then.index) ? then : null;
|
||||
}
|
||||
|
||||
const LOOP_KINDS: readonly IrNodeKind[] = ["forOf", "for", "while", "doWhile"];
|
||||
|
||||
/** Limit a borrow to a loop whose complete iteration preserves heap edges.
|
||||
* Initialization and unrelated work elsewhere in the function may mutate.
|
||||
* The array remains owned by its unchanged local or global binding; the
|
||||
@@ -72,6 +75,8 @@ export function findLoopArrayBorrows(
|
||||
optionalReads?: OptionalArrayReads,
|
||||
writes?: ReferenceWrites | null,
|
||||
functions?: ReadonlyMap<string, IrFunction>,
|
||||
/** The module's node index, read instead of walking for the loops. */
|
||||
nodes?: IrNodeIndex,
|
||||
): ReadonlySet<IrStmt> {
|
||||
const result = new Set<IrStmt>();
|
||||
if (fn.async || fn.generator) return result;
|
||||
@@ -88,9 +93,10 @@ export function findLoopArrayBorrows(
|
||||
(!local.boxed && !local.tdz && (bindings?.has(local.id) || parameters.has(local.id)))
|
||||
);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
expr: () => true,
|
||||
stmt: (loop) => {
|
||||
eachNodeOfKinds(fn, nodes?.scan(LOOP_KINDS), (node, statement) => {
|
||||
if (!statement) return true;
|
||||
const loop = node as IrStmt;
|
||||
{
|
||||
if (loop.kind === "forOf") {
|
||||
const local = locals.get(loop.localId);
|
||||
if (
|
||||
@@ -187,7 +193,7 @@ export function findLoopArrayBorrows(
|
||||
},
|
||||
});
|
||||
return true;
|
||||
},
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@ export function foldConstantGlobals(mod: IrModule): IrModule {
|
||||
if (value && (value.kind === "numLit" || value.kind === "boolLit")) values.set(id, value);
|
||||
else values.delete(id);
|
||||
};
|
||||
for (const fn of mod.functions)
|
||||
// Per function, the candidates it reads: the rewrite below visits only the
|
||||
// functions that read a constant.
|
||||
const readsByFunction = new Map<IrFunction, Set<string>>();
|
||||
for (const fn of mod.functions) {
|
||||
let read: Set<string> | undefined;
|
||||
everyStmtList(fn.body, {
|
||||
// Every node other than a read that names the binding is a write
|
||||
// (assignments, loop and catch bindings, compound updates); only a
|
||||
@@ -48,16 +52,21 @@ export function foldConstantGlobals(mod: IrModule): IrModule {
|
||||
return true;
|
||||
},
|
||||
expr: (e) => {
|
||||
if (e.kind !== "varRef" && "localId" in e && typeof e.localId === "string")
|
||||
write(e.localId, null);
|
||||
if (e.kind === "varRef") {
|
||||
if (candidates.has(e.localId)) {
|
||||
if (read === undefined) read = new Set();
|
||||
read.add(e.localId);
|
||||
}
|
||||
} else if ("localId" in e && typeof e.localId === "string") write(e.localId, null);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (read !== undefined) readsByFunction.set(fn, read);
|
||||
}
|
||||
const constants = new Map<string, Literal>();
|
||||
for (const [id, value] of values) if (writes.get(id) === 1) constants.set(id, value);
|
||||
if (constants.size === 0) return mod;
|
||||
|
||||
const reads = (e: IrExpr): boolean => !(e.kind === "varRef" && constants.has(e.localId));
|
||||
const rewriteExpr = (e: IrExpr): IrExpr => {
|
||||
if (e.kind === "varRef") {
|
||||
const value = constants.get(e.localId);
|
||||
@@ -68,7 +77,8 @@ export function foldConstantGlobals(mod: IrModule): IrModule {
|
||||
const rewriteStmt = (s: IrStmt): IrStmt => mapStmtChildren(s, rewriteExpr, rewriteStmt);
|
||||
let changed = false;
|
||||
const functions = mod.functions.map((fn): IrFunction => {
|
||||
if (everyStmtList(fn.body, { stmt: () => true, expr: reads })) return fn;
|
||||
const read = readsByFunction.get(fn);
|
||||
if (read === undefined || ![...read].some((id) => constants.has(id))) return fn;
|
||||
changed = true;
|
||||
return { ...fn, body: fn.body.map(rewriteStmt) };
|
||||
});
|
||||
|
||||
@@ -523,6 +523,12 @@ test("counting-down recursion proves its parameter from the call-site ranges", (
|
||||
);
|
||||
const rising = analyzeInt32Slots(mod([], [up, fn("user", [], [stmt(call("up", [num(-18)]))])]));
|
||||
expect(rising.facts("up").params?.get("d")).toEqual({ min: -18, max: 0 });
|
||||
// The same with the caller first, so the fixpoint analyzes it after the
|
||||
// parameter dropped: the narrowing still sees its argument.
|
||||
const callerFirst = analyzeInt32Slots(
|
||||
mod([], [fn("user", [], [stmt(call("up", [num(-18)]))]), up]),
|
||||
);
|
||||
expect(callerFirst.facts("up").params?.get("d")).toEqual({ min: -18, max: 0 });
|
||||
|
||||
// A negative start counts down past the smallest int32.
|
||||
const negative = analyzeInt32Slots(mod([], [count, even, odd, user([18, -1])]));
|
||||
|
||||
@@ -1208,8 +1208,11 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
// Each function's parameter and result writes under its latest analysis:
|
||||
// at the fixpoint, the ranges of every write under the final facts.
|
||||
const lastWrites = new Map<string, Map<string, IntegerRange | null>>();
|
||||
while (queue.length) {
|
||||
const fn = functions.get(queue.pop()!)!;
|
||||
// First in, first out: a function whose slots change while others wait
|
||||
// goes after them, so a caller of many callees is analyzed again once
|
||||
// for all of their changes, not once for each.
|
||||
for (let head = 0; head < queue.length; head++) {
|
||||
const fn = functions.get(queue[head]!)!;
|
||||
queued.delete(fn.name);
|
||||
const params = slots.facts(fn.name).params;
|
||||
if (params) for (const id of params.keys()) reads(`param:${fn.name}\0${id}`, fn.name);
|
||||
@@ -1663,8 +1666,14 @@ function narrowSlotRanges(
|
||||
// The ranges every function was last analyzed under.
|
||||
let analyzed: Map<string, IntegerRange> = new Map(proven);
|
||||
let verified: Map<string, IntegerRange> = new Map(proven);
|
||||
// Functions whose recorded writes predate their current facts.
|
||||
// Functions whose recorded writes predate their current facts: those
|
||||
// whose parameters rangedParameters proved, and their callers, which
|
||||
// wrote nothing into those parameters while they had no fact.
|
||||
const dirty = new Set(changed);
|
||||
for (const name of changed) {
|
||||
const callers = readersOf(`ret:${name}`);
|
||||
if (callers) for (const caller of callers) dirty.add(caller);
|
||||
}
|
||||
// The narrowing operator: a bound moves at most once from its proven
|
||||
// value (a revived result's from int32), so recursion that shifts its
|
||||
// argument (`f(d - 1)`) cannot walk a bound down one step per round.
|
||||
|
||||
@@ -126,6 +126,140 @@ test("property helpers whose class branches only test capsules are cache sites",
|
||||
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" });
|
||||
});
|
||||
|
||||
test("bags converted between dyn and jsval are still the receiver", () => {
|
||||
// A program that declares a class returns its bags as jsvals, and the
|
||||
// helpers read them through js.toDyn.
|
||||
const toDyn = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "js.toDyn",
|
||||
args: [value],
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const fromDyn = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "js.fromDyn",
|
||||
args: [value],
|
||||
type: { kind: "jsval" },
|
||||
loc,
|
||||
});
|
||||
const jsvalBag = helper(
|
||||
"%dyn.class.readPropertiesOrEmpty.js",
|
||||
["v"],
|
||||
[capsuleBranch("v"), ret(fromDyn(ref("v")))],
|
||||
);
|
||||
const staticBag = helper("%dyn.class.staticProperties", ["v"], [ret(ref("v"))]);
|
||||
const getter = getHelper(
|
||||
"%dyn.class.property.0",
|
||||
"x",
|
||||
[capsuleBranch("p.0")],
|
||||
call(staticBag.name, toDyn(call(jsvalBag.name, ref("p.0")))),
|
||||
);
|
||||
const setter = setHelper("%dyn.class.property.1", "y");
|
||||
const write = setter.body[0] as Extract<IrStmt, { kind: "exprStmt" }>;
|
||||
const keySet = write.expr as Extract<IrExpr, { kind: "libCall" }>;
|
||||
keySet.args[0] = toDyn(call(jsvalBag.name, ref("p.0")));
|
||||
// A converted value that is not the receiver stays a helper call.
|
||||
const foreign = getHelper(
|
||||
"%dyn.class.property.2",
|
||||
"x",
|
||||
[],
|
||||
toDyn(fromDyn({ kind: "dynObjLit", type: DYN, loc } as IrExpr)),
|
||||
);
|
||||
const sites = sitesOf(jsvalBag, staticBag, getter, setter, foreign);
|
||||
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" });
|
||||
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" });
|
||||
expect(sites.site("%dyn.class.property.2")).toBeNull();
|
||||
});
|
||||
|
||||
test("a bag helper may first rebind its parameter to a bag of it", () => {
|
||||
// Programs with subclasses normalize the value before the class branches.
|
||||
const normalize = helper("%dyn.class.normalize", ["v"], [capsuleBranch("v"), ret(ref("v"))]);
|
||||
const rebind = (value: IrExpr): IrStmt => ({ kind: "assign", localId: "v", value, loc });
|
||||
const normalized = helper(
|
||||
"%dyn.class.readPropertiesOrEmpty.normalized",
|
||||
["v"],
|
||||
[rebind(call(normalize.name, ref("v"))), capsuleBranch("v"), ret(ref("v"))],
|
||||
);
|
||||
const replaced = helper(
|
||||
"%dyn.class.readPropertiesOrEmpty.replaced",
|
||||
["v"],
|
||||
[rebind({ kind: "dynObjLit", type: DYN, loc } as IrExpr), ret(ref("v"))],
|
||||
);
|
||||
const sites = sitesOf(
|
||||
normalize,
|
||||
normalized,
|
||||
replaced,
|
||||
getHelper("%dyn.class.property.0", "x", [], call(normalized.name, ref("p.0"))),
|
||||
getHelper("%dyn.class.property.1", "x", [], call(replaced.name, ref("p.0"))),
|
||||
// The property helper itself normalizes its receiver first, too.
|
||||
getHelper("%dyn.class.property.2", "x", [
|
||||
{ kind: "assign", localId: "p.0", value: call(normalize.name, ref("p.0")), loc },
|
||||
capsuleBranch("p.0"),
|
||||
]),
|
||||
);
|
||||
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" });
|
||||
expect(sites.site("%dyn.class.property.1")).toBeNull();
|
||||
expect(sites.site("%dyn.class.property.2")).toEqual({ kind: "get", key: "x" });
|
||||
});
|
||||
|
||||
test("keyed helpers may test capsules between the key's coercion and the read", () => {
|
||||
const read = (object: IrExpr, key: string): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "js.fromDyn",
|
||||
args: [{ kind: "dynKeyGet", value: object, key: ref(key), type: DYN, loc }],
|
||||
type: { kind: "jsval" },
|
||||
loc,
|
||||
});
|
||||
const guard = (test: "symbol" | "nullish", tested: string, object: IrExpr): IrStmt => ({
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test, value: ref(tested), type: BOOL, loc },
|
||||
then: [ret(read(object, "p.key"))],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
const keyed = (name: string, between: IrStmt[]): IrFunction =>
|
||||
helper(
|
||||
name,
|
||||
["p.0", "p.key"],
|
||||
[
|
||||
capsuleBranch("p.0"),
|
||||
guard("symbol", "p.key", call(identityBag.name, ref("p.0"))),
|
||||
guard("nullish", "p.0", ref("p.0")),
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "key.string",
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.toStringCoerce",
|
||||
args: [ref("p.key")],
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
...between,
|
||||
ret(read(call(identityBag.name, ref("p.0")), "key.string")),
|
||||
],
|
||||
["key.string"],
|
||||
);
|
||||
const errorBranch: IrStmt = {
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "error", value: ref("p.0"), type: BOOL, loc },
|
||||
then: [ret(ref("p.0"))],
|
||||
else_: null,
|
||||
loc,
|
||||
};
|
||||
const sites = sitesOf(
|
||||
keyed("%dyn.class.computed.0", []),
|
||||
keyed("%dyn.class.computed.1", [capsuleBranch("p.0"), capsuleBranch("p.0", "dyn.classIs")]),
|
||||
keyed("%dyn.class.computed.2", [errorBranch]),
|
||||
);
|
||||
expect(sites.site("%dyn.class.computed.0")).toEqual({ kind: "keyed" });
|
||||
expect(sites.site("%dyn.class.computed.1")).toEqual({ kind: "keyed" });
|
||||
expect(sites.site("%dyn.class.computed.2")).toBeNull();
|
||||
});
|
||||
|
||||
test("any other prefix or tail keeps the helper call", () => {
|
||||
const errorBranch: IrStmt = {
|
||||
kind: "if",
|
||||
@@ -237,3 +371,57 @@ test("bag helpers that exit early for values no class branch matches are still t
|
||||
expect(sites.site("%dyn.class.property.2")).toBeNull();
|
||||
expect(sites.site("%dyn.class.property.3")).toBeNull();
|
||||
});
|
||||
|
||||
test("helpers that answer plain values first with the same operation are cache sites", () => {
|
||||
const negated = (test: "classCapsule" | "funcKind"): IrExpr => ({
|
||||
kind: "dynTest",
|
||||
test,
|
||||
negated: true,
|
||||
value: ref("p.0"),
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const exit = (body: IrStmt[], test: IrExpr = negated("classCapsule")): IrStmt => ({
|
||||
kind: "if",
|
||||
cond: test,
|
||||
then: [{ kind: "if", cond: negated("funcKind"), then: body, else_: null, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
const store = (key: string): IrStmt[] => [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySet",
|
||||
args: [ref("p.0"), str(key), ref("p.1")],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
ret(null),
|
||||
];
|
||||
const load = (key: string): IrStmt[] => [
|
||||
ret({ kind: "dynKeyGet", value: ref("p.0"), key: str(key), type: DYN, loc }),
|
||||
];
|
||||
const sites = sitesOf(
|
||||
setHelper("%dyn.class.property.0", "x", [exit(store("x")), capsuleBranch("p.0")]),
|
||||
getHelper("%dyn.class.property.1", "x", [exit(load("x")), capsuleBranch("p.0")]),
|
||||
// An exit for another key, or one plain values do not take.
|
||||
setHelper("%dyn.class.property.2", "x", [exit(store("y"))]),
|
||||
setHelper("%dyn.class.property.3", "x", [
|
||||
exit(store("x"), {
|
||||
kind: "dynTest",
|
||||
test: "classCapsule",
|
||||
value: ref("p.0"),
|
||||
type: BOOL,
|
||||
loc,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "set", key: "x" });
|
||||
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "get", key: "x" });
|
||||
expect(sites.site("%dyn.class.property.2")).toBeNull();
|
||||
expect(sites.site("%dyn.class.property.3")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -61,13 +61,68 @@ export class JsIcSites {
|
||||
if (fn.params.some((p) => p.type.kind !== "dyn")) return null;
|
||||
const receiver = fn.params[0]?.localId;
|
||||
if (receiver === undefined) return null;
|
||||
const second = fn.params[1]?.localId;
|
||||
const exits: JsIcSite[] = [];
|
||||
let i = 0;
|
||||
while (i < fn.body.length && this.capsuleBranch(fn.body[i]!, receiver)) i++;
|
||||
for (; i < fn.body.length; i++) {
|
||||
const s = fn.body[i]!;
|
||||
if (this.capsuleBranch(s, receiver) || this.rebinding(s, receiver)) continue;
|
||||
const exit = this.plainExit(s, receiver, second);
|
||||
if (exit === null) break;
|
||||
exits.push(exit);
|
||||
}
|
||||
const tail = fn.body.slice(i);
|
||||
if (fn.params.length === 1) return this.getTail(tail, receiver);
|
||||
if (fn.params.length !== 2) return null;
|
||||
const second = fn.params[1]!.localId;
|
||||
return this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second);
|
||||
let site: JsIcSite | null;
|
||||
if (fn.params.length === 1) site = this.getTail(tail, receiver);
|
||||
else if (fn.params.length !== 2 || second === undefined) return null;
|
||||
else site = this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second);
|
||||
if (site === null || site.kind === "keyed") return exits.length === 0 ? site : null;
|
||||
// An exit answers plain values with the operation the site caches.
|
||||
return exits.every((exit) => exit.kind === site.kind && exit.key === site.key) ? site : null;
|
||||
}
|
||||
|
||||
/** `if (!classCapsule(receiver)) { [if (!funcKind(receiver))] OP }`, no
|
||||
* else: an exit that runs the helper's generic operation directly on a
|
||||
* value no class table matches. OP is `return [js.fromDyn](dynKeyGet(R,
|
||||
* K))` or `dyn.keySet(R, K, value); return`, with R the receiver (or a
|
||||
* bag of it). */
|
||||
private plainExit(s: IrStmt, receiver: string, value: string | undefined): JsIcSite | null {
|
||||
const negated = (e: IrExpr, test: "classCapsule" | "funcKind"): boolean =>
|
||||
e.kind === "dynTest" && e.test === test && e.negated === true && isVar(e.value, receiver);
|
||||
if (s.kind !== "if" || s.else_ !== null || !negated(s.cond, "classCapsule")) return null;
|
||||
let body = s.then;
|
||||
const inner = body[0];
|
||||
if (
|
||||
body.length === 1 &&
|
||||
inner?.kind === "if" &&
|
||||
inner.else_ === null &&
|
||||
negated(inner.cond, "funcKind")
|
||||
)
|
||||
body = inner.then;
|
||||
const [first, ret] = body;
|
||||
if (body.length === 1 && first?.kind === "return") {
|
||||
const read = unwrapFromDyn(first.value);
|
||||
if (
|
||||
read?.kind !== "dynKeyGet" ||
|
||||
read.key.kind !== "strLit" ||
|
||||
!this.bagOf(read.value, receiver)
|
||||
)
|
||||
return null;
|
||||
return { kind: "get", key: read.key.value };
|
||||
}
|
||||
if (
|
||||
body.length !== 2 ||
|
||||
first?.kind !== "exprStmt" ||
|
||||
ret?.kind !== "return" ||
|
||||
ret.value !== null
|
||||
)
|
||||
return null;
|
||||
const call = first.expr;
|
||||
if (call.kind !== "libCall" || call.fn !== "dyn.keySet" || call.args.length !== 3) return null;
|
||||
const [bag, key, stored] = call.args;
|
||||
if (!bag || !this.bagOf(bag, receiver) || key?.kind !== "strLit") return null;
|
||||
if (value === undefined || !isVar(stored, value)) return null;
|
||||
return { kind: "set", key: key.value };
|
||||
}
|
||||
|
||||
/** `varDecl bag = BAG(receiver); return [js.fromDyn](bag === receiver ?
|
||||
@@ -109,10 +164,14 @@ export class JsIcSites {
|
||||
/** `if (typeof key === "symbol") return dynKeyGet(BAG(receiver), key);
|
||||
* if (receiver == null) return dynKeyGet(receiver, key);
|
||||
* const s = String(key); return dynKeyGet(BAG(receiver), s)`, each read
|
||||
* possibly under js.fromDyn. */
|
||||
* possibly under js.fromDyn. Capsule branches may precede the final read:
|
||||
* a program that declares classes looks the string key up in each class's
|
||||
* key table there. */
|
||||
private keyedTail(tail: IrStmt[], receiver: string, key: string): JsIcSite | null {
|
||||
const [symbol, nullish, decl, ret] = tail;
|
||||
if (tail.length !== 4) return null;
|
||||
const [symbol, nullish, decl] = tail;
|
||||
const ret = tail[tail.length - 1];
|
||||
if (tail.length < 4) return null;
|
||||
if (!tail.slice(3, -1).every((s) => this.capsuleBranch(s, receiver))) return null;
|
||||
if (!this.guard(symbol, "symbol", key, (read) => this.bagOf(read, receiver), key)) return null;
|
||||
if (!this.guard(nullish, "nullish", receiver, (read) => isVar(read, receiver), key))
|
||||
return null;
|
||||
@@ -159,9 +218,12 @@ export class JsIcSites {
|
||||
}
|
||||
|
||||
/** Whether `e` is the receiver itself for every ordinary object or array:
|
||||
* the receiver, or a chain of property-bag helpers applied to it. */
|
||||
* the receiver, or a chain of property-bag helpers applied to it. A
|
||||
* conversion between the dyn and jsval forms keeps the object it holds. */
|
||||
private bagOf(e: IrExpr, receiver: string): boolean {
|
||||
if (isVar(e, receiver)) return true;
|
||||
if (e.kind === "libCall" && (e.fn === "js.toDyn" || e.fn === "js.fromDyn"))
|
||||
return e.args.length === 1 && this.bagOf(e.args[0]!, receiver);
|
||||
return (
|
||||
e.kind === "call" &&
|
||||
e.args.length === 1 &&
|
||||
@@ -182,6 +244,13 @@ export class JsIcSites {
|
||||
return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param);
|
||||
}
|
||||
|
||||
/** `param = <bag of param>`, such as the subclass normalization
|
||||
* `value = %dyn.class.normalize(value)`, which leaves an ordinary object
|
||||
* or array in `param`. */
|
||||
private rebinding(s: IrStmt, param: string): boolean {
|
||||
return s.kind === "assign" && s.localId === param && this.bagOf(s.value, param);
|
||||
}
|
||||
|
||||
/** A property-bag helper that returns its argument unless a capsule
|
||||
* branch is taken. */
|
||||
private identityHelper(name: string): boolean {
|
||||
@@ -204,7 +273,10 @@ export class JsIcSites {
|
||||
identity =
|
||||
fn.body
|
||||
.slice(0, -1)
|
||||
.every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) &&
|
||||
.every(
|
||||
(s) =>
|
||||
this.capsuleBranch(s, param) || this.kindExit(s, param) || this.rebinding(s, param),
|
||||
) &&
|
||||
last?.kind === "return" &&
|
||||
last.value !== null &&
|
||||
this.bagOf(last.value, param);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isBorrowSafeMath } from "./analysis.js";
|
||||
import type { IrExpr, IrFunction, IrLibFn, IrStmt } from "./ir.js";
|
||||
import { everyStmtList } from "./traverse.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex } from "./node-index.js";
|
||||
|
||||
/** Expression kinds that run no user code of their own and never consult
|
||||
* the ambient receiver stack. Calls are admitted here and resolved through
|
||||
@@ -85,6 +85,25 @@ const RECEIVER_FREE_STMTS = new Set<IrStmt["kind"]>([
|
||||
"runtimeFence",
|
||||
]);
|
||||
|
||||
/** The kinds whose nodes the readers walk answers true for without looking
|
||||
* at them: every receiver-free statement, and the receiver-free expressions
|
||||
* other than calls, library calls, string conversions and data intrinsics. */
|
||||
const TRIVIALLY_RECEIVER_FREE = new Set<string>([
|
||||
...RECEIVER_FREE_STMTS,
|
||||
...[...RECEIVER_FREE_EXPRS].filter(
|
||||
(kind) =>
|
||||
kind !== "call" &&
|
||||
kind !== "new" &&
|
||||
kind !== "virtualCall" &&
|
||||
kind !== "libCall" &&
|
||||
kind !== "toString" &&
|
||||
kind !== "arrIntrinsic" &&
|
||||
kind !== "strIntrinsic" &&
|
||||
kind !== "mapIntrinsic" &&
|
||||
kind !== "setIntrinsic",
|
||||
),
|
||||
]);
|
||||
|
||||
/** Runtime library calls beyond scalar Math that only compute or build and
|
||||
* park a native error: the f64[] folds behind Math.max/min(...xs),
|
||||
* `new Error(message)` (scr_error_new), and the checked-read TypeError
|
||||
@@ -133,7 +152,11 @@ function opaqueOperand(e: IrExpr): boolean {
|
||||
export class AmbientReceiverReaders {
|
||||
private readers: Set<string> | null = null;
|
||||
|
||||
constructor(private readonly functions: ReadonlyMap<string, IrFunction>) {}
|
||||
constructor(
|
||||
private readonly functions: ReadonlyMap<string, IrFunction>,
|
||||
/** The module's node index, read instead of walking the bodies it holds. */
|
||||
private readonly nodes?: IrNodeIndex,
|
||||
) {}
|
||||
|
||||
ignores(fn: IrFunction): boolean {
|
||||
if (fn.async || fn.generator) return false;
|
||||
@@ -153,6 +176,7 @@ export class AmbientReceiverReaders {
|
||||
}
|
||||
const readers = new Set<string>();
|
||||
const callers = new Map<string, string[]>();
|
||||
const scan = this.nodes?.scanExcept(TRIVIALLY_RECEIVER_FREE);
|
||||
for (const fn of this.functions.values()) {
|
||||
const callees = new Set<string>();
|
||||
let reads = fn.async === true || fn.generator !== undefined;
|
||||
@@ -161,32 +185,36 @@ export class AmbientReceiverReaders {
|
||||
callees.add(name);
|
||||
return true;
|
||||
};
|
||||
const expr = (expr: IrExpr): boolean => {
|
||||
if (!RECEIVER_FREE_EXPRS.has(expr.kind)) return false;
|
||||
switch (expr.kind) {
|
||||
case "call":
|
||||
return edge(expr.callee);
|
||||
case "new":
|
||||
return edge(`%${expr.className}.constructor`);
|
||||
case "virtualCall": {
|
||||
// Any class's override may run: admit only when every
|
||||
// same-named method is itself receiver-free.
|
||||
const targets = byMethod.get(expr.method) ?? [];
|
||||
return targets.length > 0 && targets.every(edge);
|
||||
}
|
||||
case "libCall":
|
||||
return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn);
|
||||
case "toString":
|
||||
return !opaqueOperand(expr.operand);
|
||||
default: {
|
||||
const operands = dataIntrinsicOperands(expr);
|
||||
return operands === null || !operands.some(opaqueOperand);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!reads)
|
||||
reads = !everyStmtList(fn.body, {
|
||||
stmt: (stmt) => RECEIVER_FREE_STMTS.has(stmt.kind),
|
||||
expr: (expr) => {
|
||||
if (!RECEIVER_FREE_EXPRS.has(expr.kind)) return false;
|
||||
switch (expr.kind) {
|
||||
case "call":
|
||||
return edge(expr.callee);
|
||||
case "new":
|
||||
return edge(`%${expr.className}.constructor`);
|
||||
case "virtualCall": {
|
||||
// Any class's override may run: admit only when every
|
||||
// same-named method is itself receiver-free.
|
||||
const targets = byMethod.get(expr.method) ?? [];
|
||||
return targets.length > 0 && targets.every(edge);
|
||||
}
|
||||
case "libCall":
|
||||
return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn);
|
||||
case "toString":
|
||||
return !opaqueOperand(expr.operand);
|
||||
default: {
|
||||
const operands = dataIntrinsicOperands(expr);
|
||||
return operands === null || !operands.some(opaqueOperand);
|
||||
}
|
||||
}
|
||||
},
|
||||
eachNodeOfKinds(fn, scan, (node, statement) => {
|
||||
const free = statement
|
||||
? RECEIVER_FREE_STMTS.has((node as IrStmt).kind)
|
||||
: expr(node as IrExpr);
|
||||
if (!free) reads = true;
|
||||
return free;
|
||||
});
|
||||
if (reads) readers.add(fn.name);
|
||||
for (const callee of callees) {
|
||||
|
||||
@@ -118,22 +118,11 @@ export class NativeCache {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a marker or entry exists, without reading or checking it. */
|
||||
/** Whether an entry exists, without reading or checking it. */
|
||||
has(family: string, key: string): boolean {
|
||||
return existsSync(join(this.root, family, key));
|
||||
}
|
||||
|
||||
/** Records an empty marker that has() finds. */
|
||||
mark(family: string, key: string): void {
|
||||
try {
|
||||
const directory = join(this.root, family);
|
||||
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
writeFileSync(join(directory, key), "");
|
||||
} catch {
|
||||
/* Cache failures never change a build's result. */
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes `bytes` under their content digest, and returns the digest. */
|
||||
writeContent(family: string, bytes: Uint8Array): string {
|
||||
const digest = contentDigest(bytes);
|
||||
@@ -178,7 +167,7 @@ export class NativeCache {
|
||||
"object",
|
||||
"sanitizer",
|
||||
"executable",
|
||||
"linked",
|
||||
"kept",
|
||||
"runtime-pack",
|
||||
"binary",
|
||||
"dsym",
|
||||
|
||||
@@ -422,7 +422,9 @@ export class NativeCompiler {
|
||||
ffi,
|
||||
executablePack,
|
||||
);
|
||||
const restored = executableCache?.restore(stagedOutput) ?? false;
|
||||
// An unchanged rebuild keeps the executable the last build left.
|
||||
const kept = executableCache?.keep(output) ?? false;
|
||||
const restored = kept || (executableCache?.restore(stagedOutput) ?? false);
|
||||
timing(restored ? "executable-cache-hit" : "executable-cache-miss");
|
||||
if (!restored) {
|
||||
let programObjects = [object];
|
||||
@@ -547,7 +549,10 @@ export class NativeCompiler {
|
||||
irPath = join(options.outDir, `${stem}.ir.json`);
|
||||
writeTextPartsSync(irPath, prepared.ir!);
|
||||
}
|
||||
renameSync(stagedOutput, output);
|
||||
if (!kept) {
|
||||
renameSync(stagedOutput, output);
|
||||
executableCache?.recordOutput(output);
|
||||
}
|
||||
if (target.platform === "darwin" && (optimization !== "dev" || options.strip))
|
||||
rmSync(output + ".dSYM", { recursive: true, force: true });
|
||||
return {
|
||||
|
||||
@@ -224,7 +224,7 @@ test("a failed or empty link trace cannot publish a completed executable", () =>
|
||||
});
|
||||
|
||||
test.each(["ld", "crt", "libc"] as const)(
|
||||
"a Linux key's second link records the linker, startup objects and libc; replacing the %s misses",
|
||||
"a Linux key's first link records the linker, startup objects and libc; replacing the %s misses",
|
||||
(input) => {
|
||||
const f = fixture();
|
||||
const lib = join(f.root, "lib");
|
||||
@@ -257,26 +257,46 @@ test.each(["ld", "crt", "libc"] as const)(
|
||||
};
|
||||
});
|
||||
const open = () => new NativeExecutableCache(f.cache, f.key, "clang", false, [f.runtime], true);
|
||||
// A key's first link is not traced; the caller links the output itself.
|
||||
expect(open().trace(["-o", f.output], f.stage)).toBe(false);
|
||||
expect(spawnSync).not.toHaveBeenCalled();
|
||||
expect(open().restore(f.destination)).toBe(false);
|
||||
// Its next link traces both the driver and the linker in one run.
|
||||
// A key's first link traces the driver and the linker in one run, and
|
||||
// records the output it leaves instead of storing the executable.
|
||||
const entry = open();
|
||||
expect(entry.trace(["-o", f.output], f.stage)).toBe(true);
|
||||
expect(spawnSync).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(spawnSync).mock.calls[0]![1]).toEqual(["-o", f.output, "-v", "-Wl,-t"]);
|
||||
entry.publish(f.output);
|
||||
expect(f.cache.read("executable", f.key)).toBeNull();
|
||||
entry.recordOutput(f.output);
|
||||
expect(open().keep(f.output)).toBe(true);
|
||||
const recorded = (
|
||||
JSON.parse(f.cache.read("executable", f.key)!.toString()) as { inputs: { path: string }[] }
|
||||
JSON.parse(f.cache.read("kept", f.key)!.toString()) as { inputs: { path: string }[] }
|
||||
).inputs.map((saved) => saved.path);
|
||||
expect(recorded).toEqual(expect.arrayContaining([...Object.values(paths), nonshared, lib]));
|
||||
expect(recorded).not.toContain(program);
|
||||
expect(open().restore(f.destination)).toBe(true);
|
||||
const before = statSync(paths[input]);
|
||||
writeFileSync(paths[input] + ".new", "x".repeat(before.size));
|
||||
utimesSync(paths[input] + ".new", before.atime, before.mtime);
|
||||
renameSync(paths[input] + ".new", paths[input]);
|
||||
expect(open().restore(f.destination)).toBe(false);
|
||||
expect(open().keep(f.output)).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
test("a Linux build whose output changed links again, and then stores the executable", () => {
|
||||
const f = fixture();
|
||||
const open = () => new NativeExecutableCache(f.cache, f.key, "clang", false, [f.runtime], true);
|
||||
const first = open();
|
||||
expect(first.trace(["-o", f.output], f.stage)).toBe(true);
|
||||
first.publish(f.output);
|
||||
first.recordOutput(f.output);
|
||||
// The output is overwritten: it is not kept, and the next link publishes.
|
||||
writeFileSync(f.output, Buffer.from([1, 2, 3, 4]));
|
||||
expect(open().keep(f.output)).toBe(false);
|
||||
const second = open();
|
||||
expect(second.restore(f.destination)).toBe(false);
|
||||
expect(second.trace(["-o", f.output], f.stage)).toBe(true);
|
||||
second.publish(f.output);
|
||||
second.recordOutput(f.output);
|
||||
expect(open().keep(f.output)).toBe(true);
|
||||
// The stored executable restores at another path.
|
||||
expect(open().restore(f.destination)).toBe(true);
|
||||
expect(readFileSync(f.destination)).toEqual(readFileSync(f.output));
|
||||
});
|
||||
|
||||
@@ -124,6 +124,41 @@ function tracePaths(output: string, driver: boolean, roots: string[]): string[]
|
||||
return [...paths].sort();
|
||||
}
|
||||
|
||||
/** Linux: the output a build left, which an unchanged rebuild keeps while
|
||||
* the output and every input keep their identities. */
|
||||
interface KeptEntry {
|
||||
schema: "scriptc.native-kept.v1";
|
||||
output: InputIdentity;
|
||||
inputs: InputIdentity[];
|
||||
}
|
||||
|
||||
function validIdentity(input: InputIdentity): boolean {
|
||||
return (
|
||||
input !== null &&
|
||||
typeof input === "object" &&
|
||||
typeof input.path === "string" &&
|
||||
isAbsolute(input.path) &&
|
||||
typeof input.canonical === "string" &&
|
||||
(input.kind === "file" || input.kind === "directory") &&
|
||||
[input.dev, input.ino, input.size, input.mtime, input.ctime].every(
|
||||
(value) => typeof value === "number" && Number.isFinite(value),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function validKept(value: unknown): value is KeptEntry {
|
||||
if (value === null || typeof value !== "object") return false;
|
||||
const entry = value as Partial<KeptEntry>;
|
||||
return (
|
||||
entry.schema === "scriptc.native-kept.v1" &&
|
||||
entry.output !== undefined &&
|
||||
validIdentity(entry.output) &&
|
||||
Array.isArray(entry.inputs) &&
|
||||
entry.inputs.length > 0 &&
|
||||
entry.inputs.every(validIdentity)
|
||||
);
|
||||
}
|
||||
|
||||
function validEntry(value: unknown): value is ExecutableEntry {
|
||||
if (value === null || typeof value !== "object") return false;
|
||||
const entry = value as Partial<ExecutableEntry>;
|
||||
@@ -135,18 +170,7 @@ function validEntry(value: unknown): value is ExecutableEntry {
|
||||
(typeof entry.symbols === "string" && /^[0-9a-f]{64}$/.test(entry.symbols))) &&
|
||||
Array.isArray(entry.inputs) &&
|
||||
entry.inputs.length > 0 &&
|
||||
entry.inputs.every(
|
||||
(input) =>
|
||||
input !== null &&
|
||||
typeof input === "object" &&
|
||||
typeof input.path === "string" &&
|
||||
isAbsolute(input.path) &&
|
||||
typeof input.canonical === "string" &&
|
||||
(input.kind === "file" || input.kind === "directory") &&
|
||||
[input.dev, input.ino, input.size, input.mtime, input.ctime].every(
|
||||
(value) => typeof value === "number" && Number.isFinite(value),
|
||||
),
|
||||
)
|
||||
entry.inputs.every(validIdentity)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,8 +202,8 @@ function stageSymbols(bytes: Buffer, binary: string): void {
|
||||
export class NativeExecutableCache {
|
||||
private inputs: InputIdentity[];
|
||||
private traced = false;
|
||||
/** Linux: no entry exists and the key was never linked before. */
|
||||
private readonly firstLink: boolean;
|
||||
/** The inputs of the entry restore() used. */
|
||||
private restoredInputs: InputIdentity[] | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly cache: NativeCache,
|
||||
@@ -187,30 +211,46 @@ export class NativeExecutableCache {
|
||||
private readonly linker: string,
|
||||
private readonly debug: boolean,
|
||||
paths: string[],
|
||||
/** Linux: trace with one verbose link, from a key's second link on. */
|
||||
/** Linux: trace with one verbose link, keep an unchanged output, and
|
||||
* store the executable from a key's second link on. */
|
||||
private readonly linux = false,
|
||||
) {
|
||||
this.firstLink = linux && !cache.has("executable", key) && !cache.has("linked", key);
|
||||
// A first link neither restores nor traces, so it needs no identities.
|
||||
this.inputs = this.firstLink ? [] : [...new Set(paths)].sort().map(identity);
|
||||
this.inputs = [...new Set(paths)].sort().map(identity);
|
||||
}
|
||||
|
||||
/** Whether `recorded`, the inputs of an entry, still match and cover this
|
||||
* build's inputs. */
|
||||
private covers(recorded: InputIdentity[]): boolean {
|
||||
if (!stillMatches(recorded) || !stillMatches(this.inputs)) return false;
|
||||
const saved = new Map(recorded.map((input) => [input.path, input]));
|
||||
return this.inputs.every((input) => {
|
||||
const found = saved.get(input.path);
|
||||
return found !== undefined && sameIdentity(found, input);
|
||||
});
|
||||
}
|
||||
|
||||
/** Linux: true when `output` is the executable the last build of this key
|
||||
* left there, unchanged, and its inputs are unchanged; the caller keeps it. */
|
||||
keep(output: string): boolean {
|
||||
if (!this.linux) return false;
|
||||
try {
|
||||
const metadata = this.cache.read("kept", this.key);
|
||||
if (metadata === null) return false;
|
||||
const entry: unknown = JSON.parse(metadata.toString("utf8"));
|
||||
if (!validKept(entry)) return false;
|
||||
const current = identity(output);
|
||||
return sameIdentity(current, entry.output) && this.covers(entry.inputs);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
restore(output: string): boolean {
|
||||
if (this.firstLink) return false;
|
||||
try {
|
||||
const metadata = this.cache.read("executable", this.key);
|
||||
if (metadata === null) return false;
|
||||
const entry: unknown = JSON.parse(metadata.toString("utf8"));
|
||||
if (!validEntry(entry) || !stillMatches(entry.inputs) || !stillMatches(this.inputs))
|
||||
return false;
|
||||
const saved = new Map(entry.inputs.map((input) => [input.path, input]));
|
||||
if (
|
||||
!this.inputs.every((input) => {
|
||||
const recorded = saved.get(input.path);
|
||||
return recorded !== undefined && sameIdentity(recorded, input);
|
||||
})
|
||||
)
|
||||
return false;
|
||||
if (!validEntry(entry) || !this.covers(entry.inputs)) return false;
|
||||
if (this.debug !== (entry.symbols !== null)) return false;
|
||||
const binary = this.cache.read("binary", entry.binary);
|
||||
const symbols = entry.symbols === null ? null : this.cache.read("dsym", entry.symbols);
|
||||
@@ -219,6 +259,7 @@ export class NativeExecutableCache {
|
||||
if (symbols !== null) stageSymbols(symbols, output);
|
||||
writeFileSync(output, binary);
|
||||
chmodSync(output, 0o755);
|
||||
this.restoredInputs = entry.inputs;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -234,12 +275,6 @@ export class NativeExecutableCache {
|
||||
trace(args: string[], stage: string): boolean {
|
||||
this.traced = false;
|
||||
try {
|
||||
// On Linux a key's first link only records that it was linked; a later
|
||||
// link of the same key traces, so a build that runs once traces nothing.
|
||||
if (this.firstLink) {
|
||||
this.cache.mark("linked", this.key);
|
||||
return false;
|
||||
}
|
||||
const roots = [resolve(stage), realpathSync(stage)];
|
||||
let driverInputs = this.linux
|
||||
? []
|
||||
@@ -250,7 +285,14 @@ export class NativeExecutableCache {
|
||||
const paths = tracePaths(linked, false, roots);
|
||||
if (paths.length === 0 || !stillMatches(this.inputs) || !stillMatches(driverInputs))
|
||||
return false;
|
||||
this.inputs.push(...driverInputs, ...paths.map(identity));
|
||||
// The driver's job line and the linker's trace name many of the same
|
||||
// files; each is checked once.
|
||||
const listed = new Set(this.inputs.map((input) => input.path));
|
||||
for (const input of [...driverInputs, ...paths.map(identity)])
|
||||
if (!listed.has(input.path)) {
|
||||
listed.add(input.path);
|
||||
this.inputs.push(input);
|
||||
}
|
||||
this.traced = true;
|
||||
return true;
|
||||
} catch {
|
||||
@@ -260,7 +302,10 @@ export class NativeExecutableCache {
|
||||
|
||||
publish(output: string): void {
|
||||
try {
|
||||
if (!this.traced || !stillMatches(this.inputs)) return;
|
||||
// On Linux the first build of a key only records its output (see
|
||||
// recordOutput); the executable is stored once a later build relinks.
|
||||
if (!this.traced || (this.linux && !this.cache.has("kept", this.key))) return;
|
||||
if (!stillMatches(this.inputs)) return;
|
||||
const binary = readFileSync(output);
|
||||
const symbols = this.debug ? readSymbols(output) : null;
|
||||
const binaryKey = this.cache.writeContent("binary", binary);
|
||||
@@ -277,6 +322,24 @@ export class NativeExecutableCache {
|
||||
/* Build artifacts remain valid when caching is unavailable. */
|
||||
}
|
||||
}
|
||||
|
||||
/** Linux: records `output`, the executable this build left in place, for
|
||||
* keep(). */
|
||||
recordOutput(output: string): void {
|
||||
if (!this.linux) return;
|
||||
try {
|
||||
const inputs = this.traced ? this.inputs : this.restoredInputs;
|
||||
if (inputs === null || !stillMatches(inputs)) return;
|
||||
const entry: KeptEntry = {
|
||||
schema: "scriptc.native-kept.v1",
|
||||
output: identity(output),
|
||||
inputs,
|
||||
};
|
||||
this.cache.write("kept", this.key, JSON.stringify(entry));
|
||||
} catch {
|
||||
/* Build artifacts remain valid when caching is unavailable. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function openNativeExecutableCache(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { targetCpuField, targetCpuTargetRefusal } from "../backend/target-cpu.js
|
||||
import { nativeCodegenDiag, type ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { CompilationTiming } from "../timing.js";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { availableParallelism } from "node:os";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import type {
|
||||
CompileFailure,
|
||||
@@ -546,10 +547,12 @@ export function prepareNativeLibrary(
|
||||
return input;
|
||||
}
|
||||
|
||||
/** Worker processes that emit function bodies at a time
|
||||
* (SCRIPTC_EMIT_WORKERS; 0 or 1 emits in this process). */
|
||||
/** Worker processes that emit function bodies at a time: up to eight, one
|
||||
* per available core (SCRIPTC_EMIT_WORKERS sets the count; 0 or 1 emits in
|
||||
* this process). */
|
||||
function emitWorkers(): number {
|
||||
const value = Number(process.env["SCRIPTC_EMIT_WORKERS"] ?? "4");
|
||||
const configured = process.env["SCRIPTC_EMIT_WORKERS"];
|
||||
const value = configured === undefined ? Math.min(8, availableParallelism()) : Number(configured);
|
||||
return Number.isFinite(value) && value > 1 ? Math.floor(value) : 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,50 @@ console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n);
|
||||
}
|
||||
});
|
||||
|
||||
test("property sites keep their caches in programs that declare classes and subclasses", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-class-"));
|
||||
try {
|
||||
const entry = join(dir, "main.js");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
// Declared classes give every property helper class branches and
|
||||
// property-bag lookups, and a subclass adds a normalization step; plain
|
||||
// objects still read through the caches.
|
||||
await writeFile(
|
||||
entry,
|
||||
`
|
||||
class Point {
|
||||
constructor(x) { this.x = x; }
|
||||
}
|
||||
class Tagged extends Point {
|
||||
constructor(x, tag) { super(x); this.tag = tag; }
|
||||
}
|
||||
function dyn(v) { return v; }
|
||||
function read(o) { return o.x; }
|
||||
function write(o, v) { o.x = v; }
|
||||
function at(a, i) { return a[i]; }
|
||||
var o = dyn({ x: 1 });
|
||||
var p = dyn(new Point(4));
|
||||
var t = dyn(new Tagged(6, "t"));
|
||||
write(o, 3);
|
||||
write(p, 5);
|
||||
write(t, 7);
|
||||
console.log(read(o), read(p), read(t), at(dyn([5, 6]), 1), at(p, "x"), at(t, "tag"));
|
||||
`,
|
||||
);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
const llvm = emitLlvmModule(mod);
|
||||
expect(body(llvm, "read")).toContain("@scr_js_load_probe(");
|
||||
expect(body(llvm, "write")).toContain("@scr_js_store_probe(");
|
||||
expect(body(llvm, "at")).toContain("@scr_js_keyed_probe(");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("typed programs have no cache sites and keep the ordinary runtime", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-typed-"));
|
||||
try {
|
||||
|
||||
@@ -195,10 +195,13 @@ bool scr_js_truthy(ScrJsVal value);
|
||||
* index and which keys an object lacks.
|
||||
*
|
||||
* Each property site of compiled dynamic-tier code owns one zeroed,
|
||||
* thread-local ScrJsIc. The code at the site is fixed: it tests entry 0
|
||||
* inline and calls a probe for everything else. Probes never run user
|
||||
* code: they answer from plain data properties or return a miss (NULL /
|
||||
* false) with nothing done, and the site then runs its generic path.
|
||||
* thread-local ScrJsIc with static storage: the reset that frees a
|
||||
* thread's shapes (at thread exit, and when a library session ends) clears
|
||||
* every record that holds entries. The code at the site is fixed: it tests
|
||||
* entry 0 inline and calls a probe for everything else. Probes never run
|
||||
* user code: they answer from plain data properties or return a miss
|
||||
* (NULL / false) with nothing done, and the site then runs its generic
|
||||
* path.
|
||||
*
|
||||
* load entry: shape = receiver shape; target = NULL for an own property
|
||||
* (slot indexes the receiver's entries), else the shape of
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "scr_key.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* The compiler's inline cache tests read these in place (64-bit targets;
|
||||
* backend/llvm/js-ic.ts). */
|
||||
@@ -37,10 +38,10 @@ _Static_assert(sizeof(ScrJsIcEntry) == 40 && sizeof(ScrJsIc) == 168, "cache layo
|
||||
* take the generic path and its hash index. */
|
||||
#define SCR_JS_SHAPE_MAX 64
|
||||
|
||||
/* Shapes live until the thread exits, so a thread creates at most this
|
||||
* many, holding at most this many key bytes. Once either budget is spent,
|
||||
* objects whose key sequence has no shape yet stay unshaped; shapes that
|
||||
* exist keep serving. */
|
||||
/* Shapes live until the thread exits or a library session ends, so a thread
|
||||
* creates at most this many, holding at most this many key bytes. Once
|
||||
* either budget is spent, objects whose key sequence has no shape yet stay
|
||||
* unshaped; shapes that exist keep serving. */
|
||||
#define SCR_JS_SHAPE_BUDGET ((size_t)1 << 16)
|
||||
#define SCR_JS_SHAPE_KEY_BYTES_BUDGET ((size_t)4 << 20)
|
||||
|
||||
@@ -63,12 +64,25 @@ static SCR_TL size_t scr_js_transition_count;
|
||||
static SCR_TL size_t scr_js_shape_key_bytes;
|
||||
static SCR_TL bool scr_js_shapes_registered;
|
||||
|
||||
/* The cache records that hold entries. Entries name shapes and prototype
|
||||
* objects, so the reset that frees the shapes also clears these records: a
|
||||
* library's next session runs the same sites with the same records. */
|
||||
static SCR_TL ScrJsIc **scr_js_filled_ics;
|
||||
static SCR_TL size_t scr_js_filled_count;
|
||||
static SCR_TL size_t scr_js_filled_capacity;
|
||||
|
||||
/* The shape word of an object whose key sequence has no shape within the
|
||||
* budgets. No cache entry names it, so its accesses take the probes,
|
||||
* which answer with a miss until the key set changes. */
|
||||
static ScrJsShape scr_js_unshaped;
|
||||
|
||||
static void scr_js_shapes_reset(void) {
|
||||
for (size_t i = 0; i < scr_js_filled_count; i++)
|
||||
memset(scr_js_filled_ics[i], 0, sizeof *scr_js_filled_ics[i]);
|
||||
free(scr_js_filled_ics);
|
||||
scr_js_filled_ics = NULL;
|
||||
scr_js_filled_count = 0;
|
||||
scr_js_filled_capacity = 0;
|
||||
while (scr_js_all_shapes) {
|
||||
ScrJsShape *shape = scr_js_all_shapes;
|
||||
scr_js_all_shapes = shape->all_next;
|
||||
@@ -192,6 +206,16 @@ static size_t scr_js_shape_find(const ScrJsShape *shape, const char *key, size_t
|
||||
/* Fills a cache entry: `stale` (a matching entry whose hit failed) when
|
||||
* there is one, else a free or replaceable one. */
|
||||
static void scr_js_ic_add(ScrJsIc *ic, uint32_t stale, ScrJsIcEntry entry) {
|
||||
if (ic->count == 0) {
|
||||
if (scr_js_filled_count == scr_js_filled_capacity) {
|
||||
size_t capacity = scr_js_filled_capacity ? scr_js_filled_capacity * 2 : 64;
|
||||
ScrJsIc **grown = realloc(scr_js_filled_ics, capacity * sizeof *grown);
|
||||
if (!grown) scr_trap("scriptc: out of memory\n");
|
||||
scr_js_filled_ics = grown;
|
||||
scr_js_filled_capacity = capacity;
|
||||
}
|
||||
scr_js_filled_ics[scr_js_filled_count++] = ic;
|
||||
}
|
||||
uint32_t index;
|
||||
if (stale < ic->count) {
|
||||
index = stale;
|
||||
|
||||
@@ -24,11 +24,17 @@ static ScrDyn *object_with(const ScrStr *key, double n) {
|
||||
return object;
|
||||
}
|
||||
|
||||
/* Cache records have static storage, as the compiled sites' do: the reset
|
||||
* at exit clears every record that holds entries. Each check starts from
|
||||
* an empty record. */
|
||||
static ScrJsIc load_ic;
|
||||
static ScrJsIc store_ic;
|
||||
|
||||
/* Whether a load probe answers `object.key` and returns `n`. */
|
||||
static bool probe_reads(ScrDyn *object, const ScrStr *key, double n) {
|
||||
ScrJsIc ic;
|
||||
memset(&ic, 0, sizeof ic);
|
||||
ScrDyn *value = scr_js_load_probe(object, key, &ic);
|
||||
ScrJsIc *ic = &load_ic;
|
||||
memset(ic, 0, sizeof *ic);
|
||||
ScrDyn *value = scr_js_load_probe(object, key, ic);
|
||||
if (!value) return false;
|
||||
assert(value->kind == SCR_DYN_NUM && value->v.num == n);
|
||||
scr_dyn_release(value);
|
||||
@@ -66,14 +72,13 @@ static void budget_bounds_shapes(size_t key_len, size_t budget) {
|
||||
|
||||
/* A store probe that would add a new shape leaves the add to the generic
|
||||
* store; one that replaces through an existing shape still runs. */
|
||||
ScrJsIc ic;
|
||||
memset(&ic, 0, sizeof ic);
|
||||
memset(&store_ic, 0, sizeof store_ic);
|
||||
ScrStr *added = scr_str_new("added", 5);
|
||||
ScrDyn *value = scr_dyn_new_num(8);
|
||||
assert(!scr_js_store_probe(object, added, value, &ic));
|
||||
assert(!scr_js_store_probe(object, added, value, &store_ic));
|
||||
assert(object->v.obj.len == 1);
|
||||
memset(&ic, 0, sizeof ic);
|
||||
assert(scr_js_store_probe(object, first, value, &ic));
|
||||
memset(&store_ic, 0, sizeof store_ic);
|
||||
assert(scr_js_store_probe(object, first, value, &store_ic));
|
||||
assert(probe_reads(object, first, 8));
|
||||
scr_dyn_release(value);
|
||||
scr_str_release(added);
|
||||
@@ -90,10 +95,9 @@ static void immortal_objects_stay_unshaped(void) {
|
||||
object->rc = SIZE_MAX;
|
||||
assert(!probe_reads(object, key, 3));
|
||||
assert(object->v.obj.shape == NULL);
|
||||
ScrJsIc ic;
|
||||
memset(&ic, 0, sizeof ic);
|
||||
memset(&store_ic, 0, sizeof store_ic);
|
||||
ScrDyn *value = scr_dyn_new_num(4);
|
||||
assert(!scr_js_store_probe(object, key, value, &ic));
|
||||
assert(!scr_js_store_probe(object, key, value, &store_ic));
|
||||
assert(object->v.obj.shape == NULL);
|
||||
scr_dyn_release(value);
|
||||
scr_str_release(key);
|
||||
|
||||
@@ -395,8 +395,8 @@ export function dynamicTierSymbols(names) {
|
||||
* bench/channel-messages and bench/worker-messages, added later, link them
|
||||
* through worker messages:
|
||||
* node:worker_threads values are dyn objects. bench/json-unions, added
|
||||
* later too, decodes JSON.parse trees. bench/dynamic-methods is
|
||||
* untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and
|
||||
* later too, decodes JSON.parse trees. bench/dynamic-methods and
|
||||
* bench/dynamic-properties are untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and
|
||||
* tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also
|
||||
* fails when a listed workload stops linking them, so remove a workload here
|
||||
* when a leak fix makes its binary clean. */
|
||||
@@ -411,6 +411,7 @@ export const DYNAMIC_WORKLOADS = new Set([
|
||||
"bench/thread-slots",
|
||||
"bench/channel-messages",
|
||||
"bench/dynamic-methods",
|
||||
"bench/dynamic-properties",
|
||||
"bench/worker-messages",
|
||||
"bench/typed-array-scan-workers",
|
||||
"bench/log-summary",
|
||||
|
||||
@@ -37,9 +37,11 @@ export const eventLoopCases: EventLoopCase[] = [
|
||||
{
|
||||
title: "stdio values: 'readable' drains with read(), then fires once more at EOF",
|
||||
fixture: "stdio-readable.mjs",
|
||||
// The first chunk waits until both readers run: a reader that starts
|
||||
// after the second chunk arrives reads the two in one 'readable'.
|
||||
script: {
|
||||
writes: [
|
||||
{ delayMs: 0, data: "alpha " },
|
||||
{ delayMs: 500, data: "alpha " },
|
||||
{ delayMs: 60, data: "beta\n" },
|
||||
],
|
||||
end: true,
|
||||
|
||||
@@ -363,6 +363,17 @@ globals: undefined fenced
|
||||
expect(run.stdout).toBe(SESSION + SESSION + SESSION);
|
||||
});
|
||||
|
||||
test("K4: untyped property caches start empty in every session", async () => {
|
||||
// The shapes a session's cache entries name are freed when it ends;
|
||||
// the next session's sites must not find those entries.
|
||||
const { archive, outDir } = await buildLibrary("js-ic-sessions", emission);
|
||||
const probe = buildProbe("js-ic-sessions", archive, outDir);
|
||||
const run = runProbe(probe);
|
||||
expect(run.signal).toBeNull();
|
||||
expect(run.status).toBe(0);
|
||||
expect(run.stdout).toBe("run: 20000\n".repeat(3));
|
||||
});
|
||||
|
||||
/* ── K5/K6/K7: the trap channel ──────────────────────────────────────── */
|
||||
|
||||
test("K5: a trap delivers to the sink exactly once, host survives", async () => {
|
||||
@@ -582,6 +593,15 @@ survived, sink_calls=1
|
||||
expect(run.stdout).toBe(SESSION + SESSION + SESSION);
|
||||
});
|
||||
|
||||
platformTest("K10: property caches across sessions under ASan", async () => {
|
||||
const { archive, outDir } = await buildLibrary("js-ic-sessions", emission, { sanitize: true });
|
||||
const probe = buildProbe("js-ic-sessions", archive, outDir, { sanitize: true });
|
||||
const run = runProbe(probe);
|
||||
expect(run.signal).toBeNull();
|
||||
expect(run.status).toBe(0);
|
||||
expect(run.stdout).toBe("run: 20000\n".repeat(3));
|
||||
});
|
||||
|
||||
platformTest("K10: K5/K7 under ASan", async () => {
|
||||
const { archive, outDir } = await buildLibrary("traps", emission, { sanitize: true });
|
||||
const probe = buildProbe("traps", archive, outDir, { sanitize: true });
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// The library's typed export over untyped code (work.mjs).
|
||||
import { runAll } from "./work.mjs";
|
||||
|
||||
export function run(): number {
|
||||
return runAll() as number;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/* Three sessions of a library whose untyped code reads and writes through
|
||||
* property caches: each prints the same result, and under the sanitize
|
||||
* flavor no session reads a cache entry the previous session freed. */
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
extern void kj_init(void);
|
||||
extern void kj_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx);
|
||||
extern void kj_collect(void);
|
||||
extern double kj_run(void);
|
||||
|
||||
static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) {
|
||||
(void)ctx; (void)addr;
|
||||
printf("UNEXPECTED SINK: %.*s", (int)len, (const char *)msg);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
kj_set_panic_sink(sink, NULL);
|
||||
for (int session = 0; session < 3; session++) {
|
||||
kj_init();
|
||||
printf("run: %.0f\n", kj_run());
|
||||
kj_collect();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"profile_format": 1,
|
||||
"name": "conformance-js-ic-sessions",
|
||||
"entry": "lib.ts",
|
||||
"emission": "llvm",
|
||||
"abi": {
|
||||
"prefix": "kj_",
|
||||
"init_symbol": "kj_init",
|
||||
"sink_register_symbol": "kj_set_panic_sink",
|
||||
"collect_symbol": "kj_collect",
|
||||
"result_reset_symbol": null
|
||||
},
|
||||
"exports": [{ "export": "run", "symbol": "kj_run", "params": [], "returns": "f64" }]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Dynamic-tier property caches across library sessions: objects built empty
|
||||
// and filled through untyped stores, read back through untyped loads, and a
|
||||
// class instance at the same kind of sites. Each session must start with
|
||||
// empty caches, since the shapes their entries named are freed when the
|
||||
// previous session ends.
|
||||
class Box {
|
||||
constructor(v) {
|
||||
this.v = v;
|
||||
}
|
||||
}
|
||||
function fill(o, n) {
|
||||
o.a = n;
|
||||
o.b = n + 1;
|
||||
return o;
|
||||
}
|
||||
function sum(o) {
|
||||
return o.a + o.b;
|
||||
}
|
||||
function unbox(b) {
|
||||
return b.v;
|
||||
}
|
||||
export function runAll() {
|
||||
let total = 0;
|
||||
for (let i = 0; i < 100; i++) total += sum(fill({}, i));
|
||||
return total + unbox(new Box(total));
|
||||
}
|
||||
Reference in New Issue
Block a user