mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Store number arrays of proven int32s in 4-byte slots across direct calls (#1960)
This commit is contained in:
@@ -68,7 +68,7 @@ import {
|
||||
type StringSplit,
|
||||
} from "./split-loops.js";
|
||||
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
|
||||
import { addRecordResultEntries } from "./record-results.js";
|
||||
import { addRecordResultEntries, resultEntryName } from "./record-results.js";
|
||||
import { specializeCallbackCalls } from "../../ir/callback-specialization.js";
|
||||
import {
|
||||
type ChunkResult,
|
||||
@@ -82,6 +82,7 @@ import { versionLoopBoundParameters } from "../../ir/parameter-versions.js";
|
||||
import {
|
||||
allocatedLiteral,
|
||||
fillAllocationLength,
|
||||
int32ArrayKey,
|
||||
selectInt32Arrays,
|
||||
} from "../../ir/int32-arrays.js";
|
||||
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
@@ -510,6 +511,15 @@ function provenInt32Arrays(mod: IrModule): ReadonlySet<string> {
|
||||
return new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]);
|
||||
}
|
||||
|
||||
/** The functions whose calls the backend may emit as calls of their
|
||||
* result-slot entries (record-results.ts). */
|
||||
function resultEntryTargets(mod: IrModule): ReadonlySet<string> {
|
||||
const names = new Set(mod.functions.map((fn) => fn.name));
|
||||
const targets = new Set<string>();
|
||||
for (const name of names) if (names.has(resultEntryName(name))) targets.add(name);
|
||||
return targets;
|
||||
}
|
||||
|
||||
/** The emitter for `input` after the module passes every emission runs. */
|
||||
function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
// Facts are keyed by node identity: emit the same module whether it was
|
||||
@@ -545,6 +555,7 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
),
|
||||
),
|
||||
provenInt32Arrays,
|
||||
resultEntryTargets,
|
||||
)
|
||||
: mod,
|
||||
options,
|
||||
@@ -781,7 +792,7 @@ export class LlEmitter {
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (s) => {
|
||||
if (s.kind !== "varDecl" && s.kind !== "assign") return true;
|
||||
const key = `${fn.name}\0${s.localId}`;
|
||||
const key = int32ArrayKey(fn.name, s.localId);
|
||||
const checked = checkedInt32Arrays.has(key);
|
||||
if (!checked && !int32Arrays.has(key)) return true;
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
@@ -799,15 +810,16 @@ export class LlEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether `arr` is one of the current function's int32 arrays
|
||||
* (ir/int32-arrays.ts, kept by the int32 slot analysis). */
|
||||
/** Whether `arr` reads one of the int32 arrays (ir/int32-arrays.ts, kept
|
||||
* by the int32 slot analysis): a local or parameter of the current
|
||||
* function, or a module global. */
|
||||
int32Array(arr: IrExpr): boolean {
|
||||
if (arr.kind !== "varRef") return false;
|
||||
if (this.int32KindArray(arr)) return true;
|
||||
if (this.int32Slots.int32Arrays.size === 0) return false;
|
||||
return (
|
||||
this.int32ArrayTemps.has(arr.localId) ||
|
||||
this.int32Slots.int32Arrays.has(`${this.currentFunctionName}\0${arr.localId}`)
|
||||
this.int32Slots.int32Arrays.has(int32ArrayKey(this.currentFunctionName, arr.localId))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js";
|
||||
import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js";
|
||||
|
||||
type SwitchCase = Extract<IrStmt, { kind: "switch" }>["cases"][number];
|
||||
@@ -6,22 +7,29 @@ import {
|
||||
everyExprChild,
|
||||
everyStmtChild,
|
||||
everyStmtList,
|
||||
mapExprChildren,
|
||||
mapStmtChildren,
|
||||
transformExpr,
|
||||
transformStmt,
|
||||
transformStmtList,
|
||||
type IrTransform,
|
||||
} from "./traverse.js";
|
||||
|
||||
/** Local number arrays the backend may store as int32 slots.
|
||||
/** Number arrays the backend may store as int32 slots.
|
||||
*
|
||||
* A candidate is a local `number[]` of a synchronous function that no
|
||||
* other binding can hold and only compiled element operations reach:
|
||||
* every write of the local stores a fresh array (an array literal, or
|
||||
* `new Array(n).fill(v)`), and every read of it is the receiver of an
|
||||
* element read, write, state test, `length`, `push` or `pop` (a discarded
|
||||
* `pop` included), or the array of an element-reader helper call. The
|
||||
* array never reaches a runtime function that reads its slots, a library
|
||||
* call, a closure, a return or untyped code.
|
||||
* A candidate is a `number[]` binding that only compiled element
|
||||
* operations reach: a local of a synchronous function, a module global, or
|
||||
* a parameter. Every write of a local or a global stores a fresh array (an
|
||||
* array literal, or `new Array(n).fill(v)`); a parameter is never written.
|
||||
* Every read of a candidate is the receiver of an element read, write,
|
||||
* state test, `length`, `push` or `pop` (a discarded `pop` included), the
|
||||
* array of an element-reader helper call, or the argument of a direct call
|
||||
* whose parameter at that position is a candidate itself. A parameter is a
|
||||
* candidate only when its function is reached by direct calls alone and
|
||||
* every call passes a candidate there, so every binding that can hold one
|
||||
* of these arrays is a candidate: the array never reaches a runtime
|
||||
* function that reads its slots, a library call, a closure, a return or
|
||||
* untyped code.
|
||||
*
|
||||
* Candidates are only proposed here. The int32 slot analysis
|
||||
* (int32-slots.ts) keeps the ones whose every stored element is a proven
|
||||
@@ -33,6 +41,18 @@ import {
|
||||
* calls are inlined first (rewriteCandidates), so no shared helper ever
|
||||
* receives an int32 array. */
|
||||
|
||||
/** The candidate key of the binding `localId` as `fnName` reads it:
|
||||
* `function\0local` for a local or parameter, `\0global` for a module
|
||||
* global (`%g.` ids), which every function reads alike. */
|
||||
export function int32ArrayKey(fnName: string, localId: string): string {
|
||||
return localId.startsWith("%g.") ? `\0${localId}` : `${fnName}\0${localId}`;
|
||||
}
|
||||
|
||||
/** The int32 element slot (int32-slots.ts) of a candidate key. */
|
||||
export function int32ArraySlot(key: string): string {
|
||||
return key.startsWith("\0") ? `g:${key.slice(1)}` : `l:${key}`;
|
||||
}
|
||||
|
||||
/** Array methods a candidate may be the receiver of: the runtime paths
|
||||
* behind them read and write int32 slots through the double-valued slot
|
||||
* accessors. */
|
||||
@@ -202,10 +222,70 @@ function writesLocal(e: IrExpr, id: string): boolean {
|
||||
return !expr(e);
|
||||
}
|
||||
|
||||
/** The candidate locals of `fn` (see the module comment). */
|
||||
export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet<string>): Set<string> {
|
||||
/** Expression kinds an index can be built from without running any code. */
|
||||
const PURE_INDEX_KINDS: ReadonlySet<string> = new Set([
|
||||
"varRef",
|
||||
"numLit",
|
||||
"boolLit",
|
||||
"bin",
|
||||
"unary",
|
||||
"ternary",
|
||||
"logical",
|
||||
"arrayGet",
|
||||
"arrayState",
|
||||
]);
|
||||
|
||||
/** Whether evaluating an index runs no code (no call, accessor or
|
||||
* conversion): plain reads, literals and their operators only. */
|
||||
function pureIndex(e: IrExpr): boolean {
|
||||
return PURE_INDEX_KINDS.has(e.kind) && everyExprChild(e, pureIndex, () => false);
|
||||
}
|
||||
|
||||
/** The parameters and globals a module's functions share as candidates,
|
||||
* still assumed to qualify. */
|
||||
export interface SharedCandidates {
|
||||
/** Candidate parameters, as `function\0local` keys. */
|
||||
params: ReadonlySet<string>;
|
||||
/** Candidate globals, as `%g.` ids. */
|
||||
globals: ReadonlySet<string>;
|
||||
/** Whether the parameter at `index` of `callee` is a candidate. */
|
||||
paramAt: (callee: string, index: number) => boolean;
|
||||
}
|
||||
|
||||
const NO_SHARED: SharedCandidates = {
|
||||
params: new Set(),
|
||||
globals: new Set(),
|
||||
paramAt: () => false,
|
||||
};
|
||||
|
||||
/** The candidates of `fn` (see the module comment): its own locals, its
|
||||
* parameters in `shared.params`, and the globals of `shared.globals` that
|
||||
* it reads only in candidate positions. `written` receives the globals it
|
||||
* assigns fresh arrays. */
|
||||
export function int32ArrayCandidates(
|
||||
fn: IrFunction,
|
||||
readers: ReadonlySet<string>,
|
||||
shared: SharedCandidates = NO_SHARED,
|
||||
written: Set<string> = new Set(),
|
||||
): Set<string> {
|
||||
const result = new Set<string>();
|
||||
if (fn.async || fn.generator) return result;
|
||||
if (fn.async || fn.generator) {
|
||||
// Globals read here leave the candidates.
|
||||
for (const id of shared.globals) result.add(id);
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if ((e.kind === "varRef" || e.kind === "assignExpr") && result.has(e.localId))
|
||||
result.delete(e.localId);
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
if ((s.kind === "varDecl" || s.kind === "assign") && result.has(s.localId))
|
||||
result.delete(s.localId);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
const captured = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
@@ -217,11 +297,12 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet<string
|
||||
!local.boxed &&
|
||||
!local.tdz &&
|
||||
!captured.has(local.id) &&
|
||||
!params.has(local.id)
|
||||
(!params.has(local.id) || shared.params.has(`${fn.name}\0${local.id}`))
|
||||
)
|
||||
result.add(local.id);
|
||||
for (const id of shared.globals) result.add(id);
|
||||
if (result.size === 0) return result;
|
||||
const written = new Set<string>();
|
||||
const allocated = new Set<string>();
|
||||
// A receiver position: the candidate is read there without escaping.
|
||||
const receiver = (e: IrExpr): void => {
|
||||
if (e.kind !== "varRef") expr(e);
|
||||
@@ -255,13 +336,28 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet<string
|
||||
return true;
|
||||
}
|
||||
case "call": {
|
||||
if (!readers.has(e.callee) || e.args.length !== 2) break;
|
||||
const [array, index] = e.args;
|
||||
// Inlining evaluates the index before the array read.
|
||||
if (array!.kind === "varRef" && writesLocal(index!, array!.localId))
|
||||
result.delete(array!.localId);
|
||||
receiver(array!);
|
||||
expr(index!);
|
||||
if (readers.has(e.callee) && e.args.length === 2) {
|
||||
const [array, index] = e.args;
|
||||
// Inlining evaluates the index before the array read: an index
|
||||
// that rebinds the array keeps it out, and so does one that can run
|
||||
// code when the array is a module global (a call can reassign it).
|
||||
if (
|
||||
array!.kind === "varRef" &&
|
||||
(writesLocal(index!, array!.localId) ||
|
||||
(array!.localId.startsWith("%g.") && !pureIndex(index!)))
|
||||
)
|
||||
result.delete(array!.localId);
|
||||
receiver(array!);
|
||||
expr(index!);
|
||||
return true;
|
||||
}
|
||||
// An argument at a candidate parameter position passes the array
|
||||
// to another candidate binding (moduleInt32ArrayCandidates checks
|
||||
// every such call).
|
||||
e.args.forEach((arg, index) => {
|
||||
if (shared.paramAt(e.callee, index)) receiver(arg);
|
||||
else expr(arg);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -272,7 +368,8 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet<string
|
||||
case "varDecl":
|
||||
case "assign": {
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
if (value && int32ArrayAllocation(value)) written.add(s.localId);
|
||||
if (params.has(s.localId)) result.delete(s.localId);
|
||||
else if (value && int32ArrayAllocation(value)) allocated.add(s.localId);
|
||||
else if (value) result.delete(s.localId);
|
||||
if (value) expr(value);
|
||||
return true;
|
||||
@@ -297,10 +394,228 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet<string
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
fn.body.every(stmt);
|
||||
for (const id of result) if (!written.has(id)) result.delete(id);
|
||||
for (const id of result) {
|
||||
if (shared.globals.has(id)) {
|
||||
if (allocated.has(id)) written.add(id);
|
||||
} else if (!params.has(id) && !allocated.has(id)) result.delete(id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The members of `ids` (none when absent) that are in `live`. */
|
||||
function liveIn(ids: ReadonlySet<string> | undefined, live: ReadonlySet<string>): Set<string> {
|
||||
const result = new Set<string>();
|
||||
if (ids) for (const id of ids) if (live.has(id)) result.add(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The candidates of every function of `mod` that has any, only those
|
||||
* whose keys are in `keep` when given. Parameters and globals are shared:
|
||||
* starting from every one that may qualify, each pass drops the ones a
|
||||
* function reads outside candidate positions, the parameters some call
|
||||
* passes anything but a candidate, and the globals never assigned a fresh
|
||||
* array, until a pass drops nothing. Functions in `redirected` keep their
|
||||
* parameters: some of their calls reach another function. */
|
||||
export function moduleInt32ArrayCandidates(
|
||||
mod: IrModule,
|
||||
readers: ReadonlySet<string>,
|
||||
keep: ReadonlySet<string> | null = null,
|
||||
redirected: ReadonlySet<string> = new Set(),
|
||||
): Map<string, Set<string>> {
|
||||
const kept = (key: string): boolean => keep === null || keep.has(key);
|
||||
const arrayParams = new Map<string, number[]>();
|
||||
for (const fn of mod.functions) {
|
||||
// Class methods, constructors and runtime helpers (`%` names) keep
|
||||
// their parameters.
|
||||
if (fn.name.startsWith("%") || fn.async || fn.generator || redirected.has(fn.name)) continue;
|
||||
const indexes: number[] = [];
|
||||
fn.params.forEach((p, i) => {
|
||||
if (p.type.kind === "array" && p.type.elem.kind === "f64") indexes.push(i);
|
||||
});
|
||||
if (indexes.length > 0) arrayParams.set(fn.name, indexes);
|
||||
}
|
||||
let globals = new Set<string>();
|
||||
for (const g of mod.globals ?? [])
|
||||
if (
|
||||
g.type.kind === "array" &&
|
||||
g.type.elem.kind === "f64" &&
|
||||
g.initFlag === undefined &&
|
||||
kept(`\0${g.id}`)
|
||||
)
|
||||
globals.add(g.id);
|
||||
const result = new Map<string, Set<string>>();
|
||||
if (arrayParams.size === 0 && globals.size === 0) {
|
||||
for (const fn of mod.functions) {
|
||||
const candidates = int32ArrayCandidates(fn, readers);
|
||||
for (const id of candidates) if (!kept(`${fn.name}\0${id}`)) candidates.delete(id);
|
||||
if (candidates.size > 0) result.set(fn.name, candidates);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// One walk: the functions reached other than by direct calls, the direct
|
||||
// calls of functions with number[] parameters, the globals each function
|
||||
// reads or writes, and whether a module namespace object exists (its
|
||||
// member reads can reach any global by name).
|
||||
const reached = new Set(moduleFunctionReferences(mod));
|
||||
const calls: { caller: string; call: IrExpr & { kind: "call" } }[] = [];
|
||||
const globalsIn = new Map<string, Set<string>>();
|
||||
let namespaces = false;
|
||||
for (const fn of mod.functions) {
|
||||
const used = new Set<string>();
|
||||
const note = (id: string): void => {
|
||||
if (globals.has(id)) used.add(id);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
const name = exprFunctionReference(e);
|
||||
if (name !== undefined) reached.add(name);
|
||||
if (e.kind === "moduleNsRef") namespaces = true;
|
||||
if (e.kind === "call" && arrayParams.has(e.callee))
|
||||
calls.push({ caller: fn.name, call: e });
|
||||
if (e.kind === "varRef" || e.kind === "assignExpr") note(e.localId);
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
if (s.kind === "varDecl" || s.kind === "assign") note(s.localId);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (used.size > 0) globalsIn.set(fn.name, used);
|
||||
}
|
||||
if (namespaces) {
|
||||
globals = new Set();
|
||||
globalsIn.clear();
|
||||
}
|
||||
const paramIds = new Map<string, string[]>();
|
||||
let params = new Set<string>();
|
||||
for (const fn of mod.functions) {
|
||||
const indexes = arrayParams.get(fn.name);
|
||||
if (!indexes || reached.has(fn.name)) continue;
|
||||
paramIds.set(
|
||||
fn.name,
|
||||
fn.params.map((p) => p.localId),
|
||||
);
|
||||
for (const i of indexes) {
|
||||
const key = `${fn.name}\0${fn.params[i]!.localId}`;
|
||||
if (kept(key)) params.add(key);
|
||||
}
|
||||
}
|
||||
for (;;) {
|
||||
const current = params;
|
||||
const written = new Set<string>();
|
||||
const liveGlobals = new Set(globals);
|
||||
result.clear();
|
||||
for (const fn of mod.functions) {
|
||||
const shared: SharedCandidates = {
|
||||
params: current,
|
||||
globals: liveIn(globalsIn.get(fn.name), globals),
|
||||
paramAt: (callee, index) => {
|
||||
const id = paramIds.get(callee)?.[index];
|
||||
return id !== undefined && current.has(`${callee}\0${id}`);
|
||||
},
|
||||
};
|
||||
const candidates = int32ArrayCandidates(fn, readers, shared, written);
|
||||
for (const id of shared.globals) if (!candidates.has(id)) liveGlobals.delete(id);
|
||||
for (const id of candidates)
|
||||
if (!shared.globals.has(id) && !kept(`${fn.name}\0${id}`)) candidates.delete(id);
|
||||
if (candidates.size > 0) result.set(fn.name, candidates);
|
||||
}
|
||||
for (const id of liveGlobals) if (!written.has(id)) liveGlobals.delete(id);
|
||||
const live = new Set(
|
||||
[...current].filter((key) => {
|
||||
const cut = key.indexOf("\0");
|
||||
return result.get(key.slice(0, cut))?.has(key.slice(cut + 1)) === true;
|
||||
}),
|
||||
);
|
||||
// A parameter needs a candidate at that position of every call; a call
|
||||
// with fewer arguments passes undefined.
|
||||
for (const { caller, call } of calls) {
|
||||
const ids = paramIds.get(call.callee);
|
||||
if (!ids) continue;
|
||||
ids.forEach((id, i) => {
|
||||
const key = `${call.callee}\0${id}`;
|
||||
if (!live.has(key)) return;
|
||||
const arg = call.args[i];
|
||||
const ok =
|
||||
arg?.kind === "varRef" &&
|
||||
(arg.localId.startsWith("%g.")
|
||||
? liveGlobals.has(arg.localId)
|
||||
: result.get(caller)?.has(arg.localId) === true);
|
||||
if (!ok) live.delete(key);
|
||||
});
|
||||
}
|
||||
if (live.size === current.size && liveGlobals.size === globals.size) break;
|
||||
params = live;
|
||||
globals = liveGlobals;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The candidates one array can reach together: a direct call's argument
|
||||
* and the parameter it binds hold the same array, so their element facts
|
||||
* stand or fall together. `group` maps each of `keys` to its group's keys
|
||||
* (itself alone for most), and `globalReaders` each candidate global's
|
||||
* key to the functions that read or write it. */
|
||||
export function int32ArrayGroups(
|
||||
mod: IrModule,
|
||||
keys: ReadonlySet<string>,
|
||||
): { group: Map<string, string[]>; globalReaders: Map<string, Set<string>> } {
|
||||
const parent = new Map<string, string>();
|
||||
const root = (key: string): string => {
|
||||
let r = key;
|
||||
while (parent.has(r)) r = parent.get(r)!;
|
||||
if (r !== key) parent.set(key, r);
|
||||
return r;
|
||||
};
|
||||
const globalReaders = new Map<string, Set<string>>();
|
||||
const params = new Map(mod.functions.map((fn) => [fn.name, fn.params]));
|
||||
const paramKeys = new Set(
|
||||
mod.functions.flatMap((fn) => fn.params.map((p) => `${fn.name}\0${p.localId}`)),
|
||||
);
|
||||
if ([...keys].some((key) => key.startsWith("\0") || paramKeys.has(key))) {
|
||||
for (const fn of mod.functions) {
|
||||
const touch = (id: string): void => {
|
||||
const key = int32ArrayKey(fn.name, id);
|
||||
if (!key.startsWith("\0") || !keys.has(key)) return;
|
||||
const set = globalReaders.get(key) ?? new Set<string>();
|
||||
set.add(fn.name);
|
||||
globalReaders.set(key, set);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "varRef" || e.kind === "assignExpr") touch(e.localId);
|
||||
if (e.kind === "call")
|
||||
e.args.forEach((arg, i) => {
|
||||
const param = params.get(e.callee)?.[i];
|
||||
if (arg.kind !== "varRef" || !param) return;
|
||||
const a = int32ArrayKey(fn.name, arg.localId),
|
||||
p = `${e.callee}\0${param.localId}`;
|
||||
if (!keys.has(a) || !keys.has(p)) return;
|
||||
const ra = root(a),
|
||||
rp = root(p);
|
||||
if (ra !== rp) parent.set(ra, rp);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
if (s.kind === "varDecl" || s.kind === "assign") touch(s.localId);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
const members = new Map<string, string[]>();
|
||||
for (const key of keys) {
|
||||
const r = root(key);
|
||||
const list = members.get(r);
|
||||
if (list) list.push(key);
|
||||
else members.set(r, [key]);
|
||||
}
|
||||
const group = new Map<string, string[]>();
|
||||
for (const key of keys) group.set(key, members.get(root(key))!);
|
||||
return { group, globalReaders };
|
||||
}
|
||||
|
||||
/** The element values stored into each candidate: element writes, pushed
|
||||
* values, literal elements and fill values. */
|
||||
export function int32ArrayStores(
|
||||
@@ -341,16 +656,34 @@ export function int32ArrayStores(
|
||||
return stores;
|
||||
}
|
||||
|
||||
const REPEATED_INDEX_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]);
|
||||
|
||||
/** An index the inlined read may evaluate at each of its uses: a plain read,
|
||||
* a literal, or integer arithmetic over them. Repeating it keeps the forms
|
||||
* the backend derives from its operands at every use, such as the exact i64
|
||||
* of an int32 product, where a bound local holds only the double. */
|
||||
function repeatableIndex(e: IrExpr): boolean {
|
||||
if (e.kind === "varRef" || e.kind === "numLit") return true;
|
||||
if (e.kind === "unary") return e.op === "-" && repeatableIndex(e.operand);
|
||||
return (
|
||||
e.kind === "bin" &&
|
||||
REPEATED_INDEX_OPS.has(e.op) &&
|
||||
repeatableIndex(e.left) &&
|
||||
repeatableIndex(e.right)
|
||||
);
|
||||
}
|
||||
|
||||
/** Rewrite a function's candidates for the backend. A discarded pop uses
|
||||
* the candidate in place of its hidden local. An element-reader helper call
|
||||
* whose array is a candidate is inlined: the helper's returned ternary, with
|
||||
* the candidate for its array and the index for its index, so the read means
|
||||
* the same. An index that is not a plain read or literal is bound to a fresh
|
||||
* local first: in its own declaration just before the statement when the
|
||||
* call is the whole value of a declaration or assignment (the form lowering
|
||||
* gives `a[i]!`, and the one the backend reads without a box), otherwise in a
|
||||
* sequence around the ternary. Selection keeps out indexes that write the
|
||||
* array, so evaluating the index before the array read is unobservable. */
|
||||
* the same. An index that is not repeatable (repeatableIndex) is bound to a
|
||||
* fresh local first: in its own declaration just before the statement when
|
||||
* the call is the whole value of a declaration or assignment (the form
|
||||
* lowering gives `a[i]!`, and the one the backend reads without a box),
|
||||
* otherwise in a sequence around the ternary. Selection keeps out indexes
|
||||
* that write the array, so evaluating the index before the array read is
|
||||
* unobservable. */
|
||||
function rewriteCandidates(
|
||||
fn: IrFunction,
|
||||
candidates: ReadonlySet<string>,
|
||||
@@ -374,14 +707,13 @@ function rewriteCandidates(
|
||||
candidates.has(e.args[0]!.localId)
|
||||
? e
|
||||
: null;
|
||||
const simple = (e: IrExpr): boolean => e.kind === "varRef" || e.kind === "numLit";
|
||||
// The statements of a list, with complex read indexes declared first.
|
||||
const hoist = (list: IrStmt[]): IrStmt[] =>
|
||||
list.flatMap((s): IrStmt[] => {
|
||||
if (s.kind !== "varDecl" && s.kind !== "assign") return [s];
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
const call = readerCall(value);
|
||||
if (!call || simple(call.args[1]!)) return [s];
|
||||
if (!call || repeatableIndex(call.args[1]!)) return [s];
|
||||
const { decl, ref } = bind(call.args[1]!);
|
||||
const read: IrExpr = { ...call, args: [call.args[0]!, ref] };
|
||||
if (s.kind === "varDecl") return [decl, { ...s, init: read }];
|
||||
@@ -444,21 +776,23 @@ function rewriteCandidates(
|
||||
if (ret.kind !== "return" || !ret.value) return e;
|
||||
let index = call.args[1]!;
|
||||
const stmts: IrStmt[] = [];
|
||||
if (!simple(index)) {
|
||||
if (!repeatableIndex(index)) {
|
||||
const { decl, ref } = bind(index);
|
||||
stmts.push(decl);
|
||||
index = ref;
|
||||
}
|
||||
const operand = index;
|
||||
const body = transformExpr(reparseExpr(ret.value), {
|
||||
expr: (x) => {
|
||||
if (x.kind !== "varRef") return x;
|
||||
if (x.localId === arrayParam!.localId) return reparseExpr(array);
|
||||
if (x.localId === indexParam!.localId) return reparseExpr(operand);
|
||||
return x;
|
||||
},
|
||||
stmt: (x) => x,
|
||||
});
|
||||
// The arguments' copies are not searched again: an index may read a
|
||||
// local that has the id of one of the helper's parameters.
|
||||
const substituteExpr = (x: IrExpr): IrExpr => {
|
||||
if (x.kind !== "varRef") return mapExprChildren(x, substituteExpr, substituteStmt);
|
||||
if (x.localId === arrayParam!.localId) return reparseExpr(array);
|
||||
if (x.localId === indexParam!.localId) return reparseExpr(operand);
|
||||
return x;
|
||||
};
|
||||
const substituteStmt = (x: IrStmt): IrStmt =>
|
||||
mapStmtChildren(x, substituteExpr, substituteStmt);
|
||||
const body = substituteExpr(reparseExpr(ret.value));
|
||||
// The ternary is a fresh copy (reparseExpr): it takes the call's location.
|
||||
const typed = body;
|
||||
typed.loc = e.loc;
|
||||
@@ -471,40 +805,45 @@ function rewriteCandidates(
|
||||
return { ...fn, body, locals: locals.filter((local) => !popLocals.has(local.id)) };
|
||||
}
|
||||
|
||||
/** Propose the module's int32 array candidates (`function\0local` keys in
|
||||
/** Propose the module's int32 array candidates (int32ArrayKey keys in
|
||||
* `int32ArrayCandidates`), only those in `keep` when given, and rewrite
|
||||
* their functions. */
|
||||
export function markInt32ArrayCandidates(
|
||||
mod: IrModule,
|
||||
keep: ReadonlySet<string> | null = null,
|
||||
redirected: ReadonlySet<string> = new Set(),
|
||||
): IrModule {
|
||||
if (mod.lib !== undefined) return mod;
|
||||
const readers = new Map<string, IrFunction>();
|
||||
for (const fn of mod.functions) if (isElementReader(fn)) readers.set(fn.name, fn);
|
||||
const names = new Set(readers.keys());
|
||||
const keys: string[] = [];
|
||||
const all = moduleInt32ArrayCandidates(mod, new Set(readers.keys()), keep, redirected);
|
||||
if (all.size === 0) return mod;
|
||||
const keys = new Set<string>();
|
||||
const functions = mod.functions.map((fn) => {
|
||||
const candidates = int32ArrayCandidates(fn, names);
|
||||
for (const id of candidates)
|
||||
if (keep === null || keep.has(`${fn.name}\0${id}`)) keys.push(`${fn.name}\0${id}`);
|
||||
else candidates.delete(id);
|
||||
return candidates.size === 0 ? fn : rewriteCandidates(fn, candidates, readers);
|
||||
const candidates = all.get(fn.name);
|
||||
if (!candidates) return fn;
|
||||
for (const id of candidates) keys.add(int32ArrayKey(fn.name, id));
|
||||
return rewriteCandidates(fn, candidates, readers);
|
||||
});
|
||||
return keys.length === 0 ? mod : { ...mod, functions, int32ArrayCandidates: keys };
|
||||
return { ...mod, functions, int32ArrayCandidates: [...keys] };
|
||||
}
|
||||
|
||||
/** The module the backend emits: rewritten for exactly the candidates the
|
||||
* int32 slot analysis (`prove`, returning its int32Arrays) keeps, so a
|
||||
* module without int32 arrays is `mod` itself. A dropped candidate's
|
||||
* rewrite is undone by marking again without it, until every candidate
|
||||
* marked is kept. */
|
||||
* marked is kept. `redirected` names the functions some of whose calls the
|
||||
* backend emits as calls of another function (a result-slot entry): their
|
||||
* parameters keep doubles. */
|
||||
export function selectInt32Arrays(
|
||||
mod: IrModule,
|
||||
prove: (marked: IrModule) => ReadonlySet<string>,
|
||||
redirected: (mod: IrModule) => ReadonlySet<string> = () => new Set(),
|
||||
): IrModule {
|
||||
let keep: ReadonlySet<string> | null = null;
|
||||
const others = redirected(mod);
|
||||
for (let round = 0; round < 4; round++) {
|
||||
const marked = markInt32ArrayCandidates(mod, keep);
|
||||
const marked = markInt32ArrayCandidates(mod, keep, others);
|
||||
if (marked === mod) return mod;
|
||||
const kept = prove(marked);
|
||||
const proposed = marked.int32ArrayCandidates ?? [];
|
||||
|
||||
@@ -20,7 +20,13 @@ import {
|
||||
type IrNodeKind,
|
||||
} from "./node-index.js";
|
||||
import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js";
|
||||
import { checkableInt32Allocations, int32ArrayStores } from "./int32-arrays.js";
|
||||
import {
|
||||
checkableInt32Allocations,
|
||||
int32ArrayGroups,
|
||||
int32ArrayKey,
|
||||
int32ArraySlot,
|
||||
int32ArrayStores,
|
||||
} from "./int32-arrays.js";
|
||||
import { everyExprChild, everyStmtChild, everyTypeChild } from "./traverse.js";
|
||||
|
||||
/** The storage slot an array-valued expression reads, as an
|
||||
@@ -257,9 +263,10 @@ export class Int32Slots {
|
||||
/** Whether the module has int32 array candidates (the narrowedReads
|
||||
* range fact). */
|
||||
narrowedReads = false;
|
||||
/** Local number arrays (`function\0local`) stored as int32 slots: the
|
||||
* module's int32 array candidates whose every stored element the
|
||||
* fixpoint proved an int32. */
|
||||
/** Number arrays stored as int32 slots (int32ArrayKey keys: locals and
|
||||
* parameters as `function\0local`, globals as `\0global`): the module's
|
||||
* int32 array candidates whose every stored element, through every
|
||||
* binding that can hold the same array, the fixpoint proved an int32. */
|
||||
readonly int32Arrays = new Set<string>();
|
||||
/** Local number arrays that may hold int32 slots with checked stores: the
|
||||
* candidates whose element stores the fixpoint proved exact integers but
|
||||
@@ -847,17 +854,29 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
for (const key of contracts?.int32Locals ?? []) slots.addInt32Local(key);
|
||||
for (const key of contracts?.int32Elements ?? []) slots.addInt32Elements(key);
|
||||
// Int32 array candidates (ir/int32-arrays.ts): assumed to hold int32
|
||||
// elements until a store the fixpoint cannot prove drops one.
|
||||
// elements until a store the fixpoint cannot prove drops one, with every
|
||||
// candidate the same array can reach (its group).
|
||||
const arrayCandidates = new Map<string, Set<string>>();
|
||||
slots.narrowedReads = (mod.int32ArrayCandidates?.length ?? 0) > 0;
|
||||
for (const key of mod.int32ArrayCandidates ?? []) {
|
||||
const globalArrays = new Set<string>();
|
||||
const arrayKeys = new Set(mod.int32ArrayCandidates ?? []);
|
||||
slots.narrowedReads = arrayKeys.size > 0;
|
||||
for (const key of arrayKeys) {
|
||||
const cut = key.indexOf("\0");
|
||||
const fnName = key.slice(0, cut);
|
||||
const set = arrayCandidates.get(fnName) ?? new Set<string>();
|
||||
set.add(key.slice(cut + 1));
|
||||
arrayCandidates.set(fnName, set);
|
||||
slots.addInt32Elements(`l:${key}`);
|
||||
if (cut === 0) globalArrays.add(key.slice(1));
|
||||
else {
|
||||
const fnName = key.slice(0, cut);
|
||||
const set = arrayCandidates.get(fnName) ?? new Set<string>();
|
||||
set.add(key.slice(cut + 1));
|
||||
arrayCandidates.set(fnName, set);
|
||||
}
|
||||
slots.addInt32Elements(int32ArraySlot(key));
|
||||
}
|
||||
const arrayGroups = arrayKeys.size > 0 ? int32ArrayGroups(mod, arrayKeys) : null;
|
||||
// Functions that read or write a candidate global.
|
||||
const globalArrayFunctions = new Set<string>();
|
||||
if (arrayGroups)
|
||||
for (const names of arrayGroups.globalReaders.values())
|
||||
for (const name of names) globalArrayFunctions.add(name);
|
||||
|
||||
/* ── candidate fields ── */
|
||||
const children = new Map<string, string[]>();
|
||||
@@ -1139,6 +1158,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
const candidateWrites = nodes?.scan(CANDIDATE_WRITE_KINDS);
|
||||
const hasCandidateWrite = (fn: IrFunction): boolean => {
|
||||
if (arrayCandidates.has(fn.name)) return true;
|
||||
if (globalArrayFunctions.has(fn.name)) return true;
|
||||
const box = (localId: string): boolean => slots.hasBox(slots.boxGroup(fn.name, localId));
|
||||
const returns = slots.hasReturn(fn.name);
|
||||
let found = false;
|
||||
@@ -1209,25 +1229,49 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
// (checkableInt32Allocations), it keeps int32 slots with checked stores
|
||||
// (checkedInt32Arrays), which widen it to doubles in place at the first
|
||||
// store that is not an int32.
|
||||
const arrays = arrayCandidates.get(fn.name);
|
||||
if (arrays) {
|
||||
let dropped = false;
|
||||
const locals = arrayCandidates.get(fn.name);
|
||||
let arrays = locals;
|
||||
if (globalArrayFunctions.has(fn.name)) {
|
||||
const both = new Set<string>();
|
||||
if (locals) for (const id of locals) both.add(id);
|
||||
for (const id of globalArrays) both.add(id);
|
||||
arrays = both;
|
||||
}
|
||||
if (arrays && arrayGroups) {
|
||||
const requeue = (name: string): void => {
|
||||
if (queued.has(name)) return;
|
||||
queued.add(name);
|
||||
queue.push(name);
|
||||
};
|
||||
const params = new Set(fn.params.map((param) => param.localId));
|
||||
for (const [id, values] of int32ArrayStores(fn, arrays)) {
|
||||
const key = `l:${fn.name}\0${id}`;
|
||||
if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue;
|
||||
slots.dropInt32Elements(key);
|
||||
const key = int32ArrayKey(fn.name, id);
|
||||
if (!slots.hasInt32ElementSlot(int32ArraySlot(key)) || values.every(int32)) continue;
|
||||
// The array's group drops together: every function reading a
|
||||
// member is analyzed again without its facts.
|
||||
const group = arrayGroups.group.get(key) ?? [key];
|
||||
for (const member of group) {
|
||||
slots.dropInt32Elements(int32ArraySlot(member));
|
||||
if (member.startsWith("\0")) {
|
||||
const names = arrayGroups.globalReaders.get(member);
|
||||
if (names) for (const name of names) requeue(name);
|
||||
} else requeue(member.slice(0, member.indexOf("\0")));
|
||||
}
|
||||
// Checked stores serve a lone local: its allocations are the only
|
||||
// arrays it holds.
|
||||
const checkable = (value: IrExpr): boolean => {
|
||||
const range = ranges.get(value);
|
||||
return int32(value) || range != null;
|
||||
};
|
||||
if (values.every(checkable) && checkableInt32Allocations(fn, id))
|
||||
slots.checkedInt32Arrays.add(`${fn.name}\0${id}`);
|
||||
else reason(`array:${fn.name}\0${id}`, "an element store is not int32");
|
||||
dropped = true;
|
||||
}
|
||||
if (dropped && !queued.has(fn.name)) {
|
||||
queued.add(fn.name);
|
||||
queue.push(fn.name);
|
||||
if (
|
||||
group.length === 1 &&
|
||||
!key.startsWith("\0") &&
|
||||
!params.has(id) &&
|
||||
values.every(checkable) &&
|
||||
checkableInt32Allocations(fn, id)
|
||||
)
|
||||
slots.checkedInt32Arrays.add(key);
|
||||
else for (const member of group) reason(`array:${member}`, "an element store is not int32");
|
||||
}
|
||||
}
|
||||
// An int32 written into a slot still assumed non-negative: widen the
|
||||
@@ -1298,10 +1342,8 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
}
|
||||
for (const proven of provenByFunction.values())
|
||||
for (const value of proven) slots.provenWrites.add(value);
|
||||
for (const [fnName, ids] of arrayCandidates)
|
||||
for (const id of ids)
|
||||
if (slots.hasInt32ElementSlot(`l:${fnName}\0${id}`))
|
||||
slots.int32Arrays.add(`${fnName}\0${id}`);
|
||||
for (const key of arrayKeys)
|
||||
if (slots.hasInt32ElementSlot(int32ArraySlot(key))) slots.int32Arrays.add(key);
|
||||
const ranged = rangedParameters(
|
||||
slots,
|
||||
functions,
|
||||
|
||||
@@ -220,3 +220,42 @@ test("only the candidates the proof keeps are rewritten", async () => {
|
||||
for (const fn of selected.functions)
|
||||
if (fn.name !== "compose") expect(fn).toBe(mod.functions.find((f) => f.name === fn.name));
|
||||
});
|
||||
|
||||
const sharedSource = `
|
||||
const data: number[] = [];
|
||||
for (let i = 0; i < 64; i++) data.push((i * 7) & 255);
|
||||
function fnv(values: number[]): number {
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < values.length; i++) h = Math.imul(h ^ values[i]!, 16777619) >>> 0;
|
||||
return h;
|
||||
}
|
||||
function mean(values: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < values.length; i++) s += values[i]!;
|
||||
return s / values.length;
|
||||
}
|
||||
function first(values: number[]): number {
|
||||
return values[0]!;
|
||||
}
|
||||
const ints = [1, 2, 3];
|
||||
const halves = [0.5, 1.5];
|
||||
const pick: (values: number[]) => number = first;
|
||||
console.log(fnv(data), mean(ints), mean(halves), pick([4]));
|
||||
`;
|
||||
|
||||
test("arrays passed to helpers and held in module globals get 4-byte slots", async () => {
|
||||
const mod = await lowerIr(sharedSource);
|
||||
const proposed = markInt32ArrayCandidates(mod).int32ArrayCandidates ?? [];
|
||||
// A global and the parameter every call passes it to; a function used as
|
||||
// a value keeps its parameter.
|
||||
expect(proposed).toEqual(expect.arrayContaining(["\0%g.e.data", "fnv\0values.0"]));
|
||||
expect(proposed.some((key) => key.startsWith("first\0"))).toBe(false);
|
||||
const llvm = await emitLlvm(sharedSource);
|
||||
// The helper reads int32 slots; `mean`, also called with fractions,
|
||||
// reads doubles.
|
||||
expect(functions(llvm, "fnv")).toMatch(
|
||||
/getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/,
|
||||
);
|
||||
expect(functions(llvm, "mean")).not.toMatch(/getelementptr inbounds i32, ptr/);
|
||||
expect(llvm).toMatch(/@scr_arr_new(_small)?\(i32 7/);
|
||||
});
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7580-int32-arrays-across-calls.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Number arrays of proven int32s that reach other functions: passed to
|
||||
// helpers called directly, along chains of helpers and recursion, and held
|
||||
// in module globals that several functions read. A helper that also
|
||||
// receives an array of other numbers or one that is not a fresh array,
|
||||
// stores a fraction through its parameter, or escapes as a value keeps
|
||||
// every array it can reach in doubles, as does a global an async function
|
||||
// reads. Each case uses its own arrays and helpers.
|
||||
|
||||
const data: number[] = [];
|
||||
let seed = 7;
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
seed = (Math.imul(seed, 48271) + 11) & 0x7fffffff;
|
||||
data.push(seed % 1000);
|
||||
}
|
||||
|
||||
function fnv(values: number[]): number {
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
h ^= values[i]! & 0xff;
|
||||
h = Math.imul(h, 16777619) >>> 0;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
function histogram(values: number[], buckets: number): number[] {
|
||||
const counts: number[] = new Array(buckets).fill(0);
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const b = values[i]! % buckets;
|
||||
counts[b] = counts[b]! + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
// A helper that writes through its parameter and calls another helper.
|
||||
function scale(values: number[], factor: number): number {
|
||||
for (let i = 0; i < values.length; i++) values[i] = (values[i]! * factor) & 0xffff;
|
||||
return sum(values);
|
||||
}
|
||||
|
||||
function sum(values: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < values.length; i++) s = (s + values[i]!) | 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Recursion passes the array on, and the elements index another array.
|
||||
function chase(next: number[], at: number, steps: number): number {
|
||||
if (steps === 0) return at;
|
||||
return chase(next, next[at]!, steps - 1);
|
||||
}
|
||||
|
||||
function pushAll(target: number[], count: number): void {
|
||||
for (let i = 0; i < count; i++) target.push((i * 3) & 0xffff);
|
||||
}
|
||||
|
||||
// `n` is a power of two: the masked elements are indexes of `perm`.
|
||||
function locals(n: number): string {
|
||||
const perm: number[] = [];
|
||||
for (let i = 0; i < n; i++) perm.push((i * 37 + 11) & (n - 1));
|
||||
const copy: number[] = [];
|
||||
pushAll(copy, n);
|
||||
const s1 = scale(copy, 5);
|
||||
return `locals ${chase(perm, 0, 1000)} ${s1} ${copy[n - 1]} ${copy.length} ${sum(perm)}`;
|
||||
}
|
||||
|
||||
// Read only through the element reader (`a[i] ?? 0`) and states.
|
||||
function readSparse(values: number[], at: number): string {
|
||||
return `${values[at] ?? -1} ${at in values} ${values.length}`;
|
||||
}
|
||||
|
||||
// The same helper receives an array of other numbers too: the arrays it
|
||||
// can reach keep doubles.
|
||||
function mean(values: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < values.length; i++) s += values[i]!;
|
||||
return s / values.length;
|
||||
}
|
||||
|
||||
// A store through the parameter that is not an int32.
|
||||
function halve(values: number[]): void {
|
||||
for (let i = 0; i < values.length; i++) values[i] = values[i]! / 2;
|
||||
}
|
||||
|
||||
// A function used as a value keeps its parameters.
|
||||
function first(values: number[]): number {
|
||||
return values[0]!;
|
||||
}
|
||||
|
||||
let reassigned: number[] = [1, 2, 3];
|
||||
function regrow(n: number): number {
|
||||
reassigned = [];
|
||||
for (let i = 0; i < n; i++) reassigned.push(i & 15);
|
||||
let s = 0;
|
||||
for (let i = 0; i < reassigned.length; i++) s += reassigned[i]!;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Not a fresh array: the result of a call.
|
||||
function total(values: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < values.length; i++) s += values[i]!;
|
||||
return s;
|
||||
}
|
||||
|
||||
const awaited: number[] = [4, 5, 6];
|
||||
async function later(): Promise<number> {
|
||||
return awaited[0]! + awaited.length;
|
||||
}
|
||||
|
||||
// A helper whose record result its callers only project: the backend may
|
||||
// call its result-slot entry instead, so its parameter keeps doubles.
|
||||
interface Span {
|
||||
lo: number;
|
||||
hi: number;
|
||||
}
|
||||
function spanAt(values: number[], i: number): Span {
|
||||
return { lo: values[i]!, hi: values[i + 1]! };
|
||||
}
|
||||
const spans: number[] = [5, 9, 2, 7, 11];
|
||||
function spanTotal(): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const span = spanAt(spans, i);
|
||||
total += span.hi * 10 - span.lo;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
const ints: number[] = [3, 4, 5];
|
||||
const fractions: number[] = [0.5, 1.5, 2.5];
|
||||
const halved: number[] = [8, 6, 4];
|
||||
halve(halved);
|
||||
const pick: (values: number[]) => number = first;
|
||||
const viaValue: number[] = [9, 8, 7];
|
||||
|
||||
// An index that calls a function reassigning the global: the read uses the
|
||||
// array the global held before the index ran, as in Node.
|
||||
let swapped: number[] = [1, 2, 3];
|
||||
function swapIn(): number {
|
||||
swapped = [];
|
||||
for (let i = 0; i < 4; i++) swapped.push(i + 10);
|
||||
return 1;
|
||||
}
|
||||
function fillSwapped(n: number): number {
|
||||
swapped = [];
|
||||
for (let i = 0; i < n; i++) swapped.push(i & 15);
|
||||
let s = 0;
|
||||
for (let i = 0; i < swapped.length; i++) s += swapped[i]!;
|
||||
return s;
|
||||
}
|
||||
function readSwapped(): number | undefined {
|
||||
return swapped[swapIn()];
|
||||
}
|
||||
|
||||
console.log(`fnv ${fnv(data)} ${fnv(data)}`);
|
||||
const h = histogram(data, 10);
|
||||
console.log(`hist ${h.length} ${h[0]} ${h[9]} ${total(h)}`);
|
||||
console.log(locals(128));
|
||||
console.log(readSparse(data, 5), readSparse(data, 5000));
|
||||
console.log(`mean ${mean(ints)} ${mean(fractions)}`);
|
||||
console.log(`halved ${halved[0]} ${halved[2]} ${halved.length}`);
|
||||
console.log(`value ${pick(viaValue)} ${viaValue.length}`);
|
||||
console.log(`regrow ${regrow(40)} ${reassigned.length} ${reassigned[39]} ${regrow(5)}`);
|
||||
console.log(`span ${spanTotal()}`);
|
||||
console.log(`swap ${fillSwapped(6)} ${readSwapped()} ${swapped[1]}`);
|
||||
void later().then((v) => console.log(`later ${v}`));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3100, "san": 3500 }
|
||||
Reference in New Issue
Block a user