mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Run lone array callbacks as loops with their bodies inlined (#1997)
This commit is contained in:
@@ -245,7 +245,7 @@ $ scriptc build scale.ts
|
||||
1 error.
|
||||
```
|
||||
|
||||
A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of callbacks are not followed, so an array built by `map` or `Array.from` with a mapping function cannot be `Int32[]`; build it with a loop that stores `value | 0`. scriptc refuses an `Int32[]` slot whose array can come from code it cannot see, such as `JSON.parse`, dynamic values or the callers of a function used as a value, and an array that can reach one and escapes to such code, such as a call through a function value (SC7003).
|
||||
A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of a callback are followed only when scriptc copies it into the loop that runs the array method: a callback written at a `map` or `filter` call whose captured variables are not reassigned, such as `xs.map((x) => (x * 2) | 0)`. Other callback results are not followed, so for example an array built by `Array.from` with a mapping function cannot be `Int32[]`; build it with a loop that stores `value | 0`. scriptc refuses an `Int32[]` slot whose array can come from code it cannot see, such as `JSON.parse`, dynamic values or the callers of a function used as a value, and an array that can reach one and escapes to such code, such as a call through a function value (SC7003).
|
||||
|
||||
## Immutable
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Contract types](/docs/contracts) are checked only where they are written as the annotation of a parameter, variable, class instance field or function result, `Acyclic` only on classes, interfaces and object type aliases, and `ProgramLifetime` only in the `implements` clause of a class declaration; other aliases, interface members and nested type positions are reported.
|
||||
In programs that use worker threads, a surrounding `try`/`catch` does not discharge `NoThrow`.
|
||||
`Int32` refuses parameters of functions used as values and fields of classes whose instances are stored in `any` or `unknown` values.
|
||||
`Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, does not follow callback results (such as `map` results), and checks a copy made by `slice` or a spread as if it were its source array.
|
||||
`Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, follows callback results only for callbacks written at a `map` or `filter` call that scriptc runs inline, and checks a copy made by `slice` or a spread as if it were its source array.
|
||||
Element reads that find a value are int32s, but a scalar `Int32` slot still needs `a[i] | 0`, because a read can miss and produce `undefined`.
|
||||
`Immutable` refuses values stored in `any` or `unknown` or that another reference can still change.
|
||||
`ProgramLifetime` applies to root classes of executables only; instances the program drops are never reclaimed unless they belong to a released [lifetime region](/docs/experimental#lifetimeregion).
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* one directly or through a binding read from it. The choice never changes
|
||||
* behavior. */
|
||||
|
||||
import type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
const APPENDING_METHODS: ReadonlySet<string> = new Set([
|
||||
@@ -120,3 +120,36 @@ export function findAppendedArrayLiterals(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** `const xs = []` directly followed by a capacity hint for `xs` (an
|
||||
* `arraySetLength` with `reserve`) whose length reads only literals and
|
||||
* other locals: the literal can be constructed with that capacity, and the
|
||||
* hint then has nothing left to do. */
|
||||
export function reservedArrayLength(
|
||||
stmts: readonly IrStmt[],
|
||||
i: number,
|
||||
): { literal: IrExpr; hint: IrStmt & { kind: "arraySetLength" } } | null {
|
||||
const decl = stmts[i];
|
||||
const next = stmts[i + 1];
|
||||
if (decl?.kind !== "varDecl" || decl.init === null || !isEmptyLiteral(decl.init)) return null;
|
||||
if (next?.kind !== "arraySetLength" || next.reserve !== true) return null;
|
||||
if (next.arr.kind !== "varRef" || next.arr.localId !== decl.localId) return null;
|
||||
return plainLength(next.length, decl.localId) ? { literal: decl.init, hint: next } : null;
|
||||
}
|
||||
|
||||
function plainLength(e: IrExpr, array: string): boolean {
|
||||
switch (e.kind) {
|
||||
case "numLit":
|
||||
return true;
|
||||
case "varRef":
|
||||
return e.localId !== array;
|
||||
case "bin":
|
||||
return plainLength(e.left, array) && plainLength(e.right, array);
|
||||
case "ternary":
|
||||
return (
|
||||
plainLength(e.cond, array) && plainLength(e.then, array) && plainLength(e.else_, array)
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,6 +478,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
emitter.publishedTypes?.containerGuarded(s.arr.type) !== true
|
||||
)
|
||||
break;
|
||||
// The literal it follows was constructed with this capacity
|
||||
// (reservedArrayLength).
|
||||
if (emitter.consumedReserves.delete(s)) break;
|
||||
const arr = emitBorrowedInput(emitter, s.arr);
|
||||
const length = emitter.emitExpr(s.length);
|
||||
if (s.arr.type.kind !== "array")
|
||||
|
||||
@@ -202,7 +202,7 @@ import { ReferenceEffects } from "./reference-effects.js";
|
||||
import { ReferenceWrites, type Writes } from "./reference-writes.js";
|
||||
import { LlvmDebugInfo } from "./debug-info.js";
|
||||
import { StackCallbacks } from "./stack-callbacks.js";
|
||||
import { findAppendedArrayLiterals } from "./appended-array-literals.js";
|
||||
import { findAppendedArrayLiterals, reservedArrayLength } from "./appended-array-literals.js";
|
||||
import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js";
|
||||
import {
|
||||
findLentRecordFields,
|
||||
@@ -1479,6 +1479,11 @@ export class LlEmitter {
|
||||
/** Locals whose array literal emitStmts left for the `await Promise.all`
|
||||
* that reads them (deferredPromiseAllLocal). */
|
||||
private readonly deferredPromiseAllEntries = new Map<string, ExprOf<"arrayLit">>();
|
||||
/** Empty array literals that a capacity hint follows directly
|
||||
* (reservedArrayLength), with that hint. Constructing the literal with
|
||||
* the hinted capacity consumes the hint, which then emits nothing. */
|
||||
readonly reservedArrayLengths = new Map<IrExpr, IrStmt & { kind: "arraySetLength" }>();
|
||||
readonly consumedReserves = new Set<IrStmt>();
|
||||
/** The promise a `return` emitted past its finally regions already
|
||||
* produced for its adopting await (borrowed from the return's parked
|
||||
* slot); that await takes it instead of evaluating its operand. */
|
||||
@@ -6044,6 +6049,8 @@ export class LlEmitter {
|
||||
if (this.B.isTerminated()) return;
|
||||
if (cursor) cursor.index = i;
|
||||
this.stmtCursor = cursor;
|
||||
const reserve = reservedArrayLength(stmts, i);
|
||||
if (reserve !== null) this.reservedArrayLengths.set(reserve.literal, reserve.hint);
|
||||
const deferred = this.deferredPromiseAllLocal(stmts, i);
|
||||
if (deferred !== null) {
|
||||
this.deferredPromiseAllEntries.set((stmts[i] as { localId: string }).localId, deferred);
|
||||
|
||||
@@ -946,6 +946,34 @@ export function emitStringExpr(
|
||||
}
|
||||
}
|
||||
|
||||
/** scr_arr_reserve_len reserves dense storage only up to this many slots
|
||||
* (SCR_ARR_DENSE_LIMIT, scr_array.c). */
|
||||
const RESERVE_LIMIT = 2 ** 20;
|
||||
|
||||
/** Elements the inline slots of a small array construction
|
||||
* (scr_arr_new_small, scr_arr_new_ops_small) hold at least
|
||||
* (SCR_ARR_SMALL_SLOTS, scr_array.c). */
|
||||
const SMALL_ARRAY_SLOTS = 3;
|
||||
|
||||
/** The capacity scr_arr_reserve_len would reserve for `length` on an empty
|
||||
* array: the length truncated when it is at least 1 and at most
|
||||
* RESERVE_LIMIT, else 0. */
|
||||
function reservedCapacity(host: LlvmEmitterContext, length: IrExpr): string {
|
||||
const B = host.B;
|
||||
const n = host.emitExpr(length);
|
||||
const atLeastOne = B.tmp(),
|
||||
withinLimit = B.tmp(),
|
||||
reserves = B.tmp(),
|
||||
slots = B.tmp(),
|
||||
capacity = B.tmp();
|
||||
B.line(`${atLeastOne} = fcmp oge double ${n.name}, ${f64Lit(1)}`);
|
||||
B.line(`${withinLimit} = fcmp ole double ${n.name}, ${f64Lit(RESERVE_LIMIT)}`);
|
||||
B.line(`${reserves} = and i1 ${atLeastOne}, ${withinLimit}`);
|
||||
B.line(`${slots} = select i1 ${reserves}, double ${n.name}, double ${f64Lit(0)}`);
|
||||
B.line(`${capacity} = fptoui double ${slots} to ${host.sizeType}`);
|
||||
return capacity;
|
||||
}
|
||||
|
||||
export function emitContainerExpr(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<
|
||||
@@ -1017,7 +1045,34 @@ export function emitContainerExpr(
|
||||
? `${arr} = call ptr @scr_arr_new_small(i32 ${SCR_ELEM_I32})`
|
||||
: `${arr} = call ptr @scr_arr_new(i32 ${SCR_ELEM_I32}, ${host.sizeType} ${capacity})`,
|
||||
);
|
||||
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
} else {
|
||||
const hint = host.reservedArrayLengths.get(e);
|
||||
if (hint !== undefined) {
|
||||
host.reservedArrayLengths.delete(e);
|
||||
host.consumedReserves.add(hint);
|
||||
const capacity = reservedCapacity(host, hint.length);
|
||||
// The small construction's inline slots hold at least
|
||||
// SMALL_ARRAY_SLOTS elements; larger capacities are sized.
|
||||
const fits = B.tmp(),
|
||||
smallArr = B.tmp(),
|
||||
sizedArr = B.tmp();
|
||||
const smallLabel = B.newLabel("arr.cap.small"),
|
||||
sizedLabel = B.newLabel("arr.cap.sized"),
|
||||
joined = B.newLabel("arr.cap.j");
|
||||
B.line(`${fits} = icmp ule ${host.sizeType} ${capacity}, ${SMALL_ARRAY_SLOTS}`);
|
||||
B.condBr(fits, smallLabel, sizedLabel);
|
||||
B.startBlock(smallLabel);
|
||||
B.line(`${smallArr} = ${arrNewCall(host.shapeHost, elem, "0", true)}`);
|
||||
B.br(joined);
|
||||
B.startBlock(sizedLabel);
|
||||
B.line(`${sizedArr} = ${arrNewCall(host.shapeHost, elem, capacity)}`);
|
||||
B.br(joined);
|
||||
B.startBlock(joined);
|
||||
B.line(
|
||||
`${arr} = phi ptr [ ${smallArr}, %${smallLabel} ], [ ${sizedArr}, %${sizedLabel} ]`,
|
||||
);
|
||||
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
}
|
||||
const out = host.own({ name: arr, type: e.type });
|
||||
const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem);
|
||||
let pending: LlValue[] = [];
|
||||
|
||||
@@ -827,15 +827,16 @@ function buildArrayHofFn(
|
||||
);
|
||||
returnType = outT;
|
||||
body = [
|
||||
readLen,
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: { kind: "arrayLit", elems: [], type: outT, loc },
|
||||
loc,
|
||||
},
|
||||
readLen,
|
||||
// map's result has the receiver's length, holes where the receiver
|
||||
// has them. Storage for it is reserved first and the length is set
|
||||
// has them. Storage for it is reserved first (with the result's
|
||||
// construction, which directly precedes it) and the length is set
|
||||
// after the loop, so a result without holes is filled by appends and
|
||||
// gets no state bytes.
|
||||
...(method === "map"
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { CONTRACT_TYPES_MODULE } from "../../contract-types.js";
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { writtenLocals } from "../../../ir/callback-specialization.js";
|
||||
import { AmbientReceiverReaders } from "../../../ir/receiver-readers.js";
|
||||
import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
VOID,
|
||||
type IrBindingSource,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrGlobal,
|
||||
@@ -16,9 +21,12 @@ import {
|
||||
typeKey,
|
||||
} from "../../../ir/ir.js";
|
||||
import {
|
||||
everyExpr,
|
||||
everyExprChild,
|
||||
everyStmt,
|
||||
everyStmtChild,
|
||||
everyStmtList,
|
||||
transformExpr,
|
||||
transformStmtList,
|
||||
} from "../../../ir/traverse.js";
|
||||
import {
|
||||
@@ -537,6 +545,65 @@ function matchChain(lowerer: Lowerer, e: IrExpr): Chain | null {
|
||||
};
|
||||
}
|
||||
|
||||
/** The outermost array-HOF call of `e` on its own, over its receiver. */
|
||||
function loneChain(lowerer: Lowerer, e: IrExpr): Chain | null {
|
||||
const top = stageOf(lowerer, e);
|
||||
if (!top) return null;
|
||||
const { stage, receiver } = top;
|
||||
if (receiver.type.kind !== "array") return null;
|
||||
const stages: Stage[] = [];
|
||||
let terminal: Terminal;
|
||||
if (TERMINALS.has(stage.info.method)) {
|
||||
const init =
|
||||
stage.info.method === "reduce" && stage.info.hasInit && e.kind === "call" ? e.args[2]! : null;
|
||||
terminal = { kind: "call", stage, init };
|
||||
} else if (PRODUCERS.has(stage.info.method)) {
|
||||
terminal = { kind: "collect" };
|
||||
stages.push(stage);
|
||||
} else return null;
|
||||
return {
|
||||
source: receiver,
|
||||
sourceType: receiver.type,
|
||||
slice: null,
|
||||
stages,
|
||||
terminal,
|
||||
type: e.type,
|
||||
loc: e.loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The callbacks of `chain` in order, or null when its stages cannot run
|
||||
* in one loop: element types that do not line up, an array argument past
|
||||
* the first stage, reduce without a seed, or findIndex over holes. */
|
||||
function chainCallbacks(chain: Chain): Stage[] | null {
|
||||
const { stages, terminal } = chain;
|
||||
const terminalStage = terminal.kind === "call" ? terminal.stage : null;
|
||||
const callbacks = terminalStage ? [...stages, terminalStage] : stages;
|
||||
// Element types must line up stage to stage.
|
||||
let elem = chain.sourceType.elem;
|
||||
for (const stage of callbacks) {
|
||||
if (!typeEquals(stage.info.elem, elem)) return null;
|
||||
if (stage.info.method === "filter" && !typeEquals(stage.info.outElem, stage.info.elem))
|
||||
return null;
|
||||
if (
|
||||
(stage.info.method === "map" || stage.info.method === "flatMap") &&
|
||||
!typeEquals(stage.info.outElem, stage.info.fnRet)
|
||||
)
|
||||
return null;
|
||||
elem = stage.info.outElem;
|
||||
}
|
||||
// Index arguments are reproduced; an array argument only names the source.
|
||||
for (let k = 0; k < callbacks.length; k++) {
|
||||
const stage = callbacks[k]!;
|
||||
const lead = stage.info.method === "reduce" ? 1 : 0;
|
||||
if (stage.info.arity > lead + 3) return null;
|
||||
if (stage.info.arity === lead + 3 && (k !== 0 || chain.slice)) return null;
|
||||
}
|
||||
if (terminalStage?.info.method === "reduce" && !terminalStage.info.hasInit) return null;
|
||||
if (terminalStage?.info.method === "findIndex" && holeCarrying(chain)) return null;
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
/* ── the fusion pass ─────────────────────────────────────────────────── */
|
||||
|
||||
/** The values a fused loop hands from one stage to the next: a local of a
|
||||
@@ -550,6 +617,10 @@ interface Plan {
|
||||
writes: Set<string>;
|
||||
/** Some callback may throw or loop; at most one does. */
|
||||
risky: boolean;
|
||||
/** How the loop inlines callbacks, when not by their effect summaries. */
|
||||
inline?: InlineTarget;
|
||||
/** Stack frame name of the loop: the inlined callback's, for a lone call. */
|
||||
frame?: string;
|
||||
}
|
||||
|
||||
/** Fuse chains of array-HOF helper calls into one loop.
|
||||
@@ -596,12 +667,35 @@ class PipelineFusion {
|
||||
readonly added: IrFunction[] = [];
|
||||
private readonly effects: CallbackEffects;
|
||||
private readonly helpers = new Map<string, string>();
|
||||
/** Bindings each function, or a closure it creates, writes after their
|
||||
* declaration. */
|
||||
private readonly written: Map<string, Set<string>>;
|
||||
/** classCaptured's answers by function name. */
|
||||
private readonly classCaptures = new Map<string, ReadonlySet<string>>();
|
||||
/** Whether a module imports or re-exports the contract types. */
|
||||
private readonly contractTypes: boolean;
|
||||
/** Bindings of the function being fused that inlined callbacks captured. */
|
||||
private inlinedCaptures = new Set<string>();
|
||||
/** The helper calls each callback with an undefined receiver; a copied
|
||||
* body runs under the receiver around the array call instead. */
|
||||
private readonly receivers: AmbientReceiverReaders;
|
||||
|
||||
constructor(
|
||||
private readonly lowerer: Lowerer,
|
||||
functions: readonly IrFunction[],
|
||||
) {
|
||||
this.effects = new CallbackEffects(functions, lowerer.globalsList);
|
||||
this.written = writtenLocals({ functions });
|
||||
this.receivers = new AmbientReceiverReaders(new Map(functions.map((fn) => [fn.name, fn])));
|
||||
this.contractTypes = lowerer.moduleOrder.some((sf) =>
|
||||
sf.statements.nodes.some(
|
||||
(st) =>
|
||||
(ts.isImportDeclaration(st) || ts.isExportDeclaration(st)) &&
|
||||
st.moduleSpecifier !== undefined &&
|
||||
ts.isStringLiteral(st.moduleSpecifier) &&
|
||||
st.moduleSpecifier.text === CONTRACT_TYPES_MODULE,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
run(fn: IrFunction): void {
|
||||
@@ -612,6 +706,7 @@ class PipelineFusion {
|
||||
});
|
||||
if (!candidate) return;
|
||||
const removed = new Set<string>();
|
||||
this.inlinedCaptures = new Set();
|
||||
let body = this.lists(fn.body, fn, removed);
|
||||
body = transformStmtList(body, {
|
||||
stmt: (s) => s,
|
||||
@@ -619,6 +714,7 @@ class PipelineFusion {
|
||||
});
|
||||
fn.body = body;
|
||||
if (removed.size > 0) fn.locals = fn.locals.filter((l) => !removed.has(l.id));
|
||||
if (this.inlinedCaptures.size > 0) unboxUncaptured(fn, this.inlinedCaptures);
|
||||
}
|
||||
|
||||
/* ── statement lists: fold single-use chain bindings ── */
|
||||
@@ -733,40 +829,51 @@ class PipelineFusion {
|
||||
/* ── expressions ── */
|
||||
|
||||
private fuse(e: IrExpr, fn: IrFunction): IrExpr | null {
|
||||
const plan = this.plan(e, fn);
|
||||
const plan = this.plan(e, fn) ?? this.lonePlan(e, fn);
|
||||
return plan ? this.emit(plan) : null;
|
||||
}
|
||||
|
||||
/** One array-HOF call on its own (or the last call of a chain that does
|
||||
* not fuse), when every callback is a closure literal whose body can be
|
||||
* copied into the loop. A single stage reorders nothing, so the
|
||||
* callbacks' effects need no model; their captured bindings must keep
|
||||
* one value while the loop runs (stableTarget), since the loop reads
|
||||
* them once, when it starts. The loop keeps the helper's order: the
|
||||
* length read once, each element read when it is visited, holes
|
||||
* skipped (and kept by map). */
|
||||
private lonePlan(e: IrExpr, fn: IrFunction): Plan | null {
|
||||
const chain = loneChain(this.lowerer, e);
|
||||
if (!chain) return null;
|
||||
const callbacks = chainCallbacks(chain);
|
||||
if (!callbacks) return null;
|
||||
const inline = this.stableTarget(fn);
|
||||
let frame: string | undefined;
|
||||
for (const stage of callbacks) {
|
||||
for (const arg of [stage.slow, stage.fast]) {
|
||||
if (!arg) continue;
|
||||
const target = inline(arg);
|
||||
if (!target) return null;
|
||||
frame ??= target.sourceName;
|
||||
}
|
||||
}
|
||||
return {
|
||||
chain,
|
||||
callbacks,
|
||||
writes: new Set(),
|
||||
risky: true,
|
||||
inline,
|
||||
...(frame === undefined ? {} : { frame }),
|
||||
};
|
||||
}
|
||||
|
||||
/** Decide whether `e` is a fusable chain; null when it is not. */
|
||||
private plan(e: IrExpr, fn: IrFunction): Plan | null {
|
||||
const chain = matchChain(this.lowerer, e);
|
||||
if (!chain) return null;
|
||||
const callbacks = chainCallbacks(chain);
|
||||
if (!callbacks) return null;
|
||||
const { stages, terminal } = chain;
|
||||
const terminalStage = terminal.kind === "call" ? terminal.stage : null;
|
||||
const callbacks = terminalStage ? [...stages, terminalStage] : stages;
|
||||
// Element types must line up stage to stage.
|
||||
let elem = chain.sourceType.elem;
|
||||
for (const stage of callbacks) {
|
||||
if (!typeEquals(stage.info.elem, elem)) return null;
|
||||
if (stage.info.method === "filter" && !typeEquals(stage.info.outElem, stage.info.elem))
|
||||
return null;
|
||||
if (
|
||||
(stage.info.method === "map" || stage.info.method === "flatMap") &&
|
||||
!typeEquals(stage.info.outElem, stage.info.fnRet)
|
||||
)
|
||||
return null;
|
||||
elem = stage.info.outElem;
|
||||
}
|
||||
// Index arguments are reproduced; an array argument only names the source.
|
||||
for (let k = 0; k < callbacks.length; k++) {
|
||||
const stage = callbacks[k]!;
|
||||
const lead = stage.info.method === "reduce" ? 1 : 0;
|
||||
if (stage.info.arity > lead + 3) return null;
|
||||
if (stage.info.arity === lead + 3 && (k !== 0 || chain.slice)) return null;
|
||||
}
|
||||
if (terminalStage?.info.method === "reduce" && !terminalStage.info.hasInit) return null;
|
||||
const holes = holeCarrying(chain);
|
||||
if (terminalStage?.info.method === "findIndex" && holes) return null;
|
||||
// Effects of each callback, in chain order.
|
||||
const scope = this.effects.scope(fn, true);
|
||||
const summaries: Effects[] = [];
|
||||
@@ -831,30 +938,89 @@ class PipelineFusion {
|
||||
* are passed in by value, read when the fused call starts; no callback
|
||||
* in the chain assigns a binding another one reads). */
|
||||
private readonly inlineTarget: InlineTarget = (arg) => {
|
||||
const fn = this.closureTarget(arg);
|
||||
if (!fn) return null;
|
||||
const captures = new Set((fn.captures ?? []).map((c) => c.localId));
|
||||
const effects = this.effects.ofFunction(fn);
|
||||
if (!effects || intersects(effects.writes, captures)) return null;
|
||||
return inlinableBody(fn, captures) ? fn : null;
|
||||
};
|
||||
|
||||
/** inlineTarget for a callback created in `fn` whose captured bindings
|
||||
* nothing assigns after their declaration: `fn`'s own locals and
|
||||
* parameters, written neither by `fn` nor by any closure that shares
|
||||
* them, and not by the callback body. A binding a local class captures
|
||||
* counts as written, since its methods may assign it; so does a binding
|
||||
* a method reads through its class's captures. The callback must
|
||||
* not observe the ambient receiver (AmbientReceiverReaders). */
|
||||
private stableTarget(fn: IrFunction): InlineTarget {
|
||||
const locals = new Map(fn.locals.map((l) => [l.id, l]));
|
||||
const inherited = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
const written = this.written.get(fn.name);
|
||||
const classCaptured = this.classCaptured(fn);
|
||||
return (arg) => {
|
||||
const target = this.closureTarget(arg);
|
||||
if (!target || !written || arg.kind !== "closure") return null;
|
||||
if (!this.receivers.ignores(target)) return null;
|
||||
for (const id of arg.captures) {
|
||||
const local = locals.get(id);
|
||||
if (!local || local.tdz || inherited.has(id) || written.has(id) || classCaptured.has(id))
|
||||
return null;
|
||||
}
|
||||
const captures = new Set((target.captures ?? []).map((c) => c.localId));
|
||||
const assigns = !everyStmtList(target.body, {
|
||||
stmt: (st) => !((st.kind === "assign" || st.kind === "forOf") && captures.has(st.localId)),
|
||||
expr: (ex) =>
|
||||
!((ex.kind === "assignExpr" || ex.kind === "incDec") && captures.has(ex.localId)),
|
||||
});
|
||||
return !assigns && inlinableBody(target, captures) ? target : null;
|
||||
};
|
||||
}
|
||||
|
||||
/** The bindings of `fn` that a local class it creates captures. */
|
||||
private classCaptured(fn: IrFunction): ReadonlySet<string> {
|
||||
const known = this.classCaptures.get(fn.name);
|
||||
if (known) return known;
|
||||
const ids = new Set<string>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "classRef") for (const id of e.captures ?? []) ids.add(id);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
this.classCaptures.set(fn.name, ids);
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** The function behind a closure-literal callback the loop can inline. */
|
||||
private closureTarget(arg: IrExpr): IrFunction | null {
|
||||
if (arg.kind !== "closure" || arg.adapts) return null;
|
||||
const fn = this.effects.fn(arg.fnName);
|
||||
if (!fn || fn.async || fn.generator || fn.classCaptures?.length) return null;
|
||||
const captures = new Set((fn.captures ?? []).map((c) => c.localId));
|
||||
if (captures.size !== arg.captures.length) return null;
|
||||
const effects = this.effects.ofFunction(fn);
|
||||
if (!effects || intersects(effects.writes, captures)) return null;
|
||||
if (fn.locals.some((l) => l.tdz || (l.boxed && !captures.has(l.id)))) return null;
|
||||
let size = 0;
|
||||
const small = everyStmtList(fn.body, {
|
||||
expr: (e) => ++size <= MAX_INLINE_NODES && e.kind !== "closure" && e.kind !== "selfRef",
|
||||
stmt: (s) => ++size <= MAX_INLINE_NODES && s.kind !== "tryCatch",
|
||||
});
|
||||
return small ? fn : null;
|
||||
};
|
||||
if ((fn.captures ?? []).length !== arg.captures.length) return null;
|
||||
return fn;
|
||||
}
|
||||
|
||||
private emit(plan: Plan): IrExpr | null {
|
||||
const built = new FusedLoop(this.lowerer, plan.chain, this.inlineTarget).build();
|
||||
const built = new FusedLoop(
|
||||
this.lowerer,
|
||||
plan.chain,
|
||||
plan.inline ?? this.inlineTarget,
|
||||
this.contractTypes,
|
||||
).build();
|
||||
if (!built) return null;
|
||||
for (const id of built.inlinedCaptures) this.inlinedCaptures.add(id);
|
||||
let name = this.helpers.get(built.key);
|
||||
if (!name) {
|
||||
name = keyedHelperName(this.lowerer, "%arr.fused", built.key);
|
||||
name = keyedHelperName(this.lowerer, plan.inline ? "%arr.loop" : "%arr.fused", built.key);
|
||||
this.helpers.set(built.key, name);
|
||||
this.added.push({ ...built.fn, name });
|
||||
const helper: IrFunction = { ...built.fn, name };
|
||||
// A lone call's loop stands in the inlined callback's stack frame.
|
||||
if (plan.frame !== undefined) helper.sourceName = plan.frame;
|
||||
this.added.push(helper);
|
||||
}
|
||||
return {
|
||||
kind: "call",
|
||||
@@ -866,6 +1032,48 @@ class PipelineFusion {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a statement inside `e` (a sequence expression's) returns. */
|
||||
function returns(e: IrExpr): boolean {
|
||||
return !everyExpr(e, { expr: () => true, stmt: (s) => s.kind !== "return" });
|
||||
}
|
||||
|
||||
/** Drops the box of each binding in `inlined` that no closure or class
|
||||
* created in `fn` captures any more: the callbacks that captured it were
|
||||
* copied into loops, which receive its value. A dead-zone binding keeps the
|
||||
* box that represents its uninitialized state, and bindings `fn` itself
|
||||
* captures keep theirs. */
|
||||
function unboxUncaptured(fn: IrFunction, inlined: ReadonlySet<string>): void {
|
||||
const captured = new Set<string>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "closure" || e.kind === "classRef")
|
||||
for (const id of e.captures ?? []) captured.add(id);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
const inherited = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
fn.locals = fn.locals.map((local): IrLocal => {
|
||||
if (local.boxed !== true || local.tdz === true || !inlined.has(local.id)) return local;
|
||||
if (captured.has(local.id) || inherited.has(local.id)) return local;
|
||||
const { boxed: _boxed, ...plain } = local;
|
||||
return plain;
|
||||
});
|
||||
}
|
||||
|
||||
/** A callback body the loop can copy: small, no nested closures, recursion
|
||||
* or try statements, and no boxed locals besides its captures. */
|
||||
function inlinableBody(fn: IrFunction, captures: ReadonlySet<string>): boolean {
|
||||
if (fn.locals.some((l) => l.tdz || (l.boxed && !captures.has(l.id)))) return false;
|
||||
let size = 0;
|
||||
return everyStmtList(fn.body, {
|
||||
expr: (e) => ++size <= MAX_INLINE_NODES && e.kind !== "closure" && e.kind !== "selfRef",
|
||||
stmt: (s) => ++size <= MAX_INLINE_NODES && s.kind !== "tryCatch",
|
||||
});
|
||||
}
|
||||
|
||||
function intersects(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
|
||||
for (const id of a) if (b.has(id)) return true;
|
||||
return false;
|
||||
@@ -988,10 +1196,13 @@ class FusedLoop {
|
||||
private next = 0;
|
||||
private failed = false;
|
||||
|
||||
/** `contractTypes`: the program writes contract types, which the
|
||||
* contract checker binds to source bindings. */
|
||||
constructor(
|
||||
private readonly lowerer: Lowerer,
|
||||
private readonly chain: Chain,
|
||||
private readonly inlineTarget: InlineTarget,
|
||||
private readonly contractTypes: boolean,
|
||||
) {
|
||||
this.loc = chain.loc;
|
||||
}
|
||||
@@ -1000,9 +1211,9 @@ class FusedLoop {
|
||||
return `${name}.${this.next++}`;
|
||||
}
|
||||
|
||||
private local(name: string, type: IrType, mutable = false): string {
|
||||
private local(name: string, type: IrType, mutable = false, source?: IrBindingSource): string {
|
||||
const id = this.fresh(name);
|
||||
this.locals.push({ id, name, type, mutable });
|
||||
this.locals.push(source ? { id, name, type, mutable, source } : { id, name, type, mutable });
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -1069,21 +1280,50 @@ class FusedLoop {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
// Copies keep their source bindings, so contract types written on
|
||||
// them are checked against what the loop stores. In a program that
|
||||
// writes contract types, a parameter with a source binding keeps its
|
||||
// own local for the same reason.
|
||||
const sources = new Map(fn.locals.map((l) => [l.id, l.source]));
|
||||
const aliased = new Set<string>();
|
||||
fn.params.forEach((p, index) => {
|
||||
const arg = args[index]!;
|
||||
if (arg.kind === "varRef" && !assigned.has(p.localId) && typeEquals(arg.type, p.type)) {
|
||||
if (
|
||||
arg.kind === "varRef" &&
|
||||
!assigned.has(p.localId) &&
|
||||
typeEquals(arg.type, p.type) &&
|
||||
!(this.contractTypes && sources.get(p.localId) !== undefined)
|
||||
) {
|
||||
renamed.set(p.localId, arg.localId);
|
||||
aliased.add(p.localId);
|
||||
}
|
||||
});
|
||||
for (const l of fn.locals) {
|
||||
if (renamed.has(l.id)) continue;
|
||||
const id = this.local(l.name, l.type, true);
|
||||
const id = this.local(l.name, l.type, true, l.source);
|
||||
renamed.set(l.id, id);
|
||||
}
|
||||
const rename = (id: string): string => renamed.get(id) ?? id;
|
||||
const renameExpr = (e: IrExpr): IrExpr => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
return { ...e, localId: rename(e.localId) };
|
||||
default:
|
||||
return e;
|
||||
}
|
||||
};
|
||||
// A bound parameter's value is an element or index with no source text
|
||||
// of its own; it is located at the parameter, without a span.
|
||||
const bind: IrStmt[] = fn.params.flatMap((p, index): IrStmt[] => {
|
||||
if (aliased.has(p.localId)) return [];
|
||||
const source = sources.get(p.localId);
|
||||
const arg = args[index]!;
|
||||
const init = source ? { ...arg, loc: { ...source.loc, end: source.loc.start } } : arg;
|
||||
return [{ kind: "varDecl", localId: rename(p.localId), init, loc }];
|
||||
});
|
||||
const labels = new Map<string, string>();
|
||||
const exit = this.fresh("%fused.exit");
|
||||
const label = (name: string): string => {
|
||||
let id = labels.get(name);
|
||||
if (!id) {
|
||||
@@ -1092,19 +1332,53 @@ class FusedLoop {
|
||||
}
|
||||
return id;
|
||||
};
|
||||
const result = fn.returnType.kind === "void" ? null : this.local("ret", fn.returnType, true);
|
||||
const rename = (id: string): string => renamed.get(id) ?? id;
|
||||
const body = transformStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
return { ...e, localId: rename(e.localId) };
|
||||
default:
|
||||
return e;
|
||||
/** Locals and labels of a copied statement; returns are the caller's. */
|
||||
const renameStmt = (st: IrStmt): IrStmt => {
|
||||
switch (st.kind) {
|
||||
case "varDecl":
|
||||
case "assign":
|
||||
return { ...st, localId: rename(st.localId) };
|
||||
case "break":
|
||||
case "continue":
|
||||
return st.label ? { ...st, label: label(st.label) } : st;
|
||||
case "forOf": {
|
||||
const renamedFor = { ...st, localId: rename(st.localId) };
|
||||
return st.labels ? { ...renamedFor, labels: st.labels.map(label) } : renamedFor;
|
||||
}
|
||||
},
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "switch":
|
||||
case "block":
|
||||
return st.labels ? { ...st, labels: st.labels.map(label) } : st;
|
||||
default:
|
||||
return st;
|
||||
}
|
||||
};
|
||||
// A body whose only return is its last statement (`(x) => x * k`,
|
||||
// `{ const y = x * 2; return y > 4; }`) needs no exit block: its other
|
||||
// statements run after the bindings, and the call's value is the
|
||||
// returned expression.
|
||||
const last = fn.body[fn.body.length - 1];
|
||||
const prefix = fn.body.slice(0, -1);
|
||||
if (
|
||||
last !== undefined &&
|
||||
last.kind === "return" &&
|
||||
last.value !== null &&
|
||||
typeEquals(last.value.type, fn.returnType) &&
|
||||
!returns(last.value) &&
|
||||
prefix.every((st) => everyStmt(st, { expr: () => true, stmt: (x) => x.kind !== "return" }))
|
||||
) {
|
||||
const copy = { expr: renameExpr, stmt: renameStmt };
|
||||
return {
|
||||
pre: [...bind, ...transformStmtList(prefix, copy)],
|
||||
value: transformExpr(last.value, copy),
|
||||
};
|
||||
}
|
||||
const exit = this.fresh("%fused.exit");
|
||||
const result = fn.returnType.kind === "void" ? null : this.local("ret", fn.returnType, true);
|
||||
const body = transformStmtList(fn.body, {
|
||||
expr: renameExpr,
|
||||
stmt: (s): IrStmt => {
|
||||
switch (s.kind) {
|
||||
case "return": {
|
||||
@@ -1124,27 +1398,14 @@ class FusedLoop {
|
||||
loc: s.loc,
|
||||
};
|
||||
}
|
||||
case "varDecl":
|
||||
case "assign":
|
||||
return { ...s, localId: rename(s.localId) };
|
||||
case "break":
|
||||
case "continue":
|
||||
return s.label && s.label !== exit ? { ...s, label: label(s.label) } : s;
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "block":
|
||||
return s.labels ? { ...s, labels: s.labels.map(label) } : s;
|
||||
default:
|
||||
return s;
|
||||
return renameStmt(s);
|
||||
}
|
||||
},
|
||||
});
|
||||
const bind: IrStmt[] = fn.params.flatMap((p, index): IrStmt[] =>
|
||||
aliased.has(p.localId)
|
||||
? []
|
||||
: [{ kind: "varDecl", localId: rename(p.localId), init: args[index]!, loc }],
|
||||
);
|
||||
const pre: IrStmt[] = [];
|
||||
if (result !== null) pre.push({ kind: "varDecl", localId: result, init: null, loc });
|
||||
pre.push({ kind: "block", labels: [exit], body: [...bind, ...body], loc });
|
||||
@@ -1157,7 +1418,13 @@ class FusedLoop {
|
||||
};
|
||||
}
|
||||
|
||||
build(): { fn: Omit<IrFunction, "name">; args: IrExpr[]; key: string } | null {
|
||||
build(): {
|
||||
fn: Omit<IrFunction, "name">;
|
||||
args: IrExpr[];
|
||||
key: string;
|
||||
/** Bindings the callbacks copied into the loop captured at this site. */
|
||||
inlinedCaptures: string[];
|
||||
} | null {
|
||||
const { chain, loc } = this;
|
||||
const arrT = chain.sourceType;
|
||||
const a = this.param("a", chain.source, "src");
|
||||
@@ -1642,7 +1909,10 @@ class FusedLoop {
|
||||
};
|
||||
foldKnownUnions(fn);
|
||||
scalarizeRecordTemporaries(this.lowerer, fn);
|
||||
return { fn, args: this.args, key: this.keyParts.join("|") };
|
||||
const inlinedCaptures = this.callees.flatMap((c) =>
|
||||
c.inline && c.arg.kind === "closure" ? c.arg.captures : [],
|
||||
);
|
||||
return { fn, args: this.args, key: this.keyParts.join("|"), inlinedCaptures };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1741,8 +2011,11 @@ function foldKnownUnions(fn: Omit<IrFunction, "name">): void {
|
||||
case "for":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "forOf":
|
||||
case "block":
|
||||
return { ...s, body: prune(s.body) };
|
||||
case "switch":
|
||||
return { ...s, cases: s.cases.map((c) => ({ ...c, body: prune(c.body) })) };
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
@@ -1930,6 +2203,19 @@ function scalarizeRecordTemporaries(lowerer: Lowerer, fn: Omit<IrFunction, "name
|
||||
body: rewriteList(s.body),
|
||||
},
|
||||
];
|
||||
case "forOf":
|
||||
return [{ ...s, iterable: rewriteExpr(s.iterable), body: rewriteList(s.body) }];
|
||||
case "switch":
|
||||
return [
|
||||
{
|
||||
...s,
|
||||
disc: rewriteExpr(s.disc),
|
||||
cases: s.cases.map((c) => ({
|
||||
test: c.test === null ? null : rewriteExpr(c.test),
|
||||
body: rewriteList(c.body),
|
||||
})),
|
||||
},
|
||||
];
|
||||
case "block":
|
||||
return [{ ...s, body: rewriteList(s.body) }];
|
||||
default: {
|
||||
|
||||
@@ -16,7 +16,10 @@ const COPIED_NODE_FRACTION = 0.25;
|
||||
* closure it creates writes through its environment (and transitively the
|
||||
* closures those create). A parameter or capture absent from its function's
|
||||
* set holds the value it entered with for the whole call. */
|
||||
export function writtenLocals(mod: IrModule, nodes?: IrNodeIndex): Map<string, Set<string>> {
|
||||
export function writtenLocals(
|
||||
mod: { readonly functions: readonly IrFunction[] },
|
||||
nodes?: IrNodeIndex,
|
||||
): Map<string, Set<string>> {
|
||||
return rebindings(mod, nodes).written;
|
||||
}
|
||||
|
||||
@@ -31,7 +34,7 @@ const REBINDING_KINDS: readonly IrNodeKind[] = [
|
||||
];
|
||||
|
||||
function rebindings(
|
||||
mod: IrModule,
|
||||
mod: { readonly functions: readonly IrFunction[] },
|
||||
nodes?: IrNodeIndex,
|
||||
): {
|
||||
written: Map<string, Set<string>>;
|
||||
|
||||
@@ -108,5 +108,161 @@ const kept = rows.filter((r) => r.value > 3);
|
||||
console.log(kept.map((r) => r.id).length, kept.length);
|
||||
${postlude}`);
|
||||
expect(init(mod).locals.some((l) => l.name === "kept")).toBe(true);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.filter", "%arr.fused"]);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.fused", "%arr.loop"]);
|
||||
});
|
||||
|
||||
test("a lone call with a stable closure-literal callback runs as a loop", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
const scale = 3;
|
||||
const scaled = rows.map((r) => r.value * scale);
|
||||
let total = 0;
|
||||
rows.forEach((r) => { total += r.value; });
|
||||
const big = rows.filter((r) => r.value > scale);
|
||||
const sum = scaled.reduce((acc, v) => acc + v, 0);
|
||||
console.log(scaled.length, total, big.length, sum, rows.some((r) => r.id === scale));
|
||||
${postlude}`);
|
||||
// forEach assigns \`total\`, a binding of \`run\`: it keeps its helper.
|
||||
expect(helperCalls(init(mod))).toEqual([
|
||||
"%arr.forEach",
|
||||
"%arr.loop",
|
||||
"%arr.loop",
|
||||
"%arr.loop",
|
||||
"%arr.loop",
|
||||
]);
|
||||
const loops = mod.functions.filter((f) => f.name.startsWith("%arr.loop."));
|
||||
expect(loops).toHaveLength(4);
|
||||
// The callback bodies are copied into the loops: no closure value is
|
||||
// created or called, and the captured \`scale\` is a plain parameter.
|
||||
for (const fn of loops) {
|
||||
expect(count(fn, "callValue")).toBe(0);
|
||||
expect(count(fn, "closure")).toBe(0);
|
||||
}
|
||||
expect(count(init(mod), "closure")).toBe(1);
|
||||
// No closure captures \`scale\` any more, so it needs no box.
|
||||
expect(init(mod).locals.find((l) => l.name === "scale")?.boxed).toBeUndefined();
|
||||
});
|
||||
|
||||
test("a lone call keeps its helper when a capture can change while it runs", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
let limit = 2;
|
||||
const bump = (): void => { limit++; };
|
||||
const a = rows.filter((r) => { bump(); return r.id < limit; });
|
||||
let step = 1;
|
||||
step = 2;
|
||||
const b = rows.map((r) => r.id * step);
|
||||
console.log(a.length, b.length, limit);
|
||||
${postlude}`);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.filter", "%arr.map"]);
|
||||
});
|
||||
|
||||
test("a lone call keeps its helper when a local class's method can assign a capture", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
let k = 1;
|
||||
class Bump {
|
||||
go(): void { k = 10; }
|
||||
}
|
||||
const b = new Bump();
|
||||
const out = rows.map((r) => { if (r.id === 2) b.go(); return r.id * k; });
|
||||
console.log(out.join(","));
|
||||
${postlude}`);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.map"]);
|
||||
});
|
||||
|
||||
test("a copied callback keeps its switch and for-of labels and bindings", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
const out = rows.map((r) => {
|
||||
let n = 0;
|
||||
outer: switch (r.id % 3) {
|
||||
case 0:
|
||||
n = 10;
|
||||
break outer;
|
||||
case 1:
|
||||
inner: for (const v of [1, 2, 3]) {
|
||||
n += v;
|
||||
if (v === 2) break outer;
|
||||
if (v === 3) continue inner;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
n = r.value;
|
||||
}
|
||||
return n;
|
||||
});
|
||||
console.log(out.join(","));
|
||||
${postlude}`);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.loop"]);
|
||||
});
|
||||
|
||||
test("a copied callback's switch-local records and known unions are rewritten in its cases", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
type Shape = { kind: "sq"; side: number } | { kind: "rect"; w: number; h: number };
|
||||
const out = rows.map((r) => {
|
||||
switch (r.id % 3) {
|
||||
case 0:
|
||||
const p = { a: r.id, b: 2 };
|
||||
return p.a + p.b;
|
||||
case 1:
|
||||
const sq = { kind: "sq" as const, side: r.value };
|
||||
const s: Shape = sq;
|
||||
return s.kind === "sq" ? s.side * 10 : 0;
|
||||
default:
|
||||
return r.value;
|
||||
}
|
||||
});
|
||||
console.log(out.join(","));
|
||||
${postlude}`);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.loop"]);
|
||||
});
|
||||
|
||||
test("a copied callback with early returns keeps its switch labels", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
const out = rows.map((r) => {
|
||||
let n = 0;
|
||||
outer: switch (r.id % 4) {
|
||||
case 0:
|
||||
return -1;
|
||||
case 1:
|
||||
n = 10;
|
||||
break outer;
|
||||
default:
|
||||
inner: switch (r.value % 2) {
|
||||
case 0:
|
||||
n = r.value;
|
||||
break outer;
|
||||
default:
|
||||
break inner;
|
||||
}
|
||||
n = 1;
|
||||
}
|
||||
return n;
|
||||
});
|
||||
const hit = rows.some((r) => {
|
||||
pick: switch (r.id) {
|
||||
case 3:
|
||||
break pick;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
console.log(out.join(","), hit);
|
||||
${postlude}`);
|
||||
expect(helperCalls(init(mod))).toEqual(["%arr.loop", "%arr.loop"]);
|
||||
});
|
||||
|
||||
test("a lone call in a local class's method keeps its helper for the class's captures", async () => {
|
||||
const mod = await lowerSource(`${prelude}
|
||||
let k = 1;
|
||||
class Box {
|
||||
set(v: number): void { k = v; }
|
||||
go(): number[] {
|
||||
const b = new Box();
|
||||
return rows.map((r) => { b.set(r.id); return r.value + k; });
|
||||
}
|
||||
}
|
||||
console.log(new Box().go().join(","));
|
||||
${postlude}`);
|
||||
const go = mod.functions.find((f) => f.name.endsWith(".Box.go"));
|
||||
expect(go).toBeDefined();
|
||||
expect(helperCalls(go!)).toEqual(["%arr.map"]);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,9 @@ import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
// A specialized array helper calls its known callback directly after
|
||||
// checking the closure's entry. A callback that cannot throw skips the
|
||||
// pending test on that path, and the fallback through the closure's own
|
||||
// entry keeps it; a callback that can throw keeps it on both paths.
|
||||
// entry keeps it; a callback that can throw keeps it on both paths. The
|
||||
// callbacks are passed through bindings: a closure literal at the call
|
||||
// would run as a loop with its body copied in (array-pipelines.ts).
|
||||
test("known callbacks outside the may-throw set test the pending flag only on the fallback", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-callback-pending-"));
|
||||
try {
|
||||
@@ -18,13 +20,16 @@ test("known callbacks outside the may-throw set test the pending flag only on th
|
||||
writeFileSync(
|
||||
entry,
|
||||
`const values = [1, 2, 3, 4];
|
||||
console.log(values.every((v) => v > 0));
|
||||
console.log(values.map((v) => v * 2).join(","));
|
||||
const positive = (v: number): boolean => v > 0;
|
||||
const double = (v: number): number => v * 2;
|
||||
const stopAtThree = (v: number): boolean => {
|
||||
if (v > 2) throw new Error("stop at " + v);
|
||||
return true;
|
||||
};
|
||||
console.log(values.every(positive));
|
||||
console.log(values.map(double).join(","));
|
||||
try {
|
||||
console.log(values.every((v) => {
|
||||
if (v > 2) throw new Error("stop at " + v);
|
||||
return true;
|
||||
}));
|
||||
console.log(values.every(stopAtThree));
|
||||
} catch (error) {
|
||||
console.log("caught", (error as Error).message);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule, validateModule } from "../src/index.js";
|
||||
import { everyStmtList } from "../src/ir/traverse.js";
|
||||
|
||||
test("util.format %f lowers a boolean argument once", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-format-float-"));
|
||||
@@ -22,14 +23,20 @@ test("util.format %f lowers a boolean argument once", async () => {
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
// One lifted helper per argument: the evaluated-for-effect statement
|
||||
// reuses the lowered value instead of lowering the argument again.
|
||||
const helpers = mod.functions
|
||||
.map((f) => f.name)
|
||||
.filter((name) => name.startsWith("%arr."))
|
||||
.map((name) => name.replace(/\.[0-9a-f]+$/, ""))
|
||||
.sort();
|
||||
expect(helpers).toEqual(["%arr.every", "%arr.some"]);
|
||||
// One lifted helper call per argument: the evaluated-for-effect
|
||||
// statement reuses the lowered value instead of lowering the argument
|
||||
// again. Each call runs its callback as a loop (array-pipelines.ts).
|
||||
const helpers: string[] = [];
|
||||
for (const fn of mod.functions)
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "call" && e.callee.startsWith("%arr."))
|
||||
helpers.push(e.callee.replace(/\.[0-9a-f]+$/, ""));
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
expect(helpers).toEqual(["%arr.loop", "%arr.loop"]);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -158,7 +158,9 @@ test("an unchanged captured parameter is borrowed and boxed only where a closure
|
||||
const module = await lower(`${ITEM}
|
||||
function heavier(item: Item, limits: number[]): boolean {
|
||||
if (limits.length === 0) return false;
|
||||
return limits.some((limit) => weigh(item) > limit);
|
||||
// A closure value: a closure literal at the call would run as a loop.
|
||||
const over = (limit: number): boolean => weigh(item) > limit;
|
||||
return limits.some(over);
|
||||
}
|
||||
function replaced(item: Item, other: Item): number {
|
||||
const read = () => item.weight;
|
||||
|
||||
@@ -14,4 +14,5 @@ class Box {
|
||||
const box = new Box();
|
||||
const loose: any = box;
|
||||
loose.size = 0.5;
|
||||
console.log([1.5, 2].map(double).join(","), box.size);
|
||||
const pick: (x: number) => number = double;
|
||||
console.log([1.5, 2].map(pick).join(","), box.size);
|
||||
|
||||
@@ -29,8 +29,10 @@ function bump(): void {
|
||||
|
||||
const sizes = [1, 2, 3];
|
||||
const len: Int32 = sizes.length;
|
||||
const weights = [1.5, 2];
|
||||
const shifted = weights.map((w: Int32) => w + 1);
|
||||
|
||||
const span = new Span(1.5);
|
||||
span.advance();
|
||||
bump();
|
||||
console.log(width(span), half(7), count, len);
|
||||
console.log(width(span), half(7), count, len, shifted);
|
||||
|
||||
@@ -34,14 +34,14 @@ contract-int32-arrays.ts:16:13 - error SC7002: the element stored into 'counts'
|
||||
|
||||
hint: make the int32 conversion explicit, for example 'Math.PI | 0', so Node stores the same value
|
||||
|
||||
contract-int32-arrays.ts:18:26 - error SC7002: the elements 'counts.map((x) => x * 2)' stores are not provably int32s (the array can reach 'doubled', declared Int32[])
|
||||
contract-int32-arrays.ts:18:44 - error SC7002: the element stored into 'counts.map((x) => x * 2)' is not provably an int32: 'x * 2' (the array can reach 'doubled', declared Int32[])
|
||||
|
||||
17 | halveInto(counts, ids);
|
||||
18 | const doubled: Int32[] = counts.map((x) => x * 2);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~
|
||||
| ^~~~~
|
||||
19 | const parsed: Int32[] = JSON.parse("[1, 2]");
|
||||
|
||||
hint: callback results are not followed; build the array with a loop that stores 'value | 0'
|
||||
hint: make the int32 conversion explicit, for example '(x * 2) | 0', so Node stores the same value
|
||||
|
||||
contract-int32-arrays.ts:19:15 - error SC7003: the Int32[] contract on 'parsed' cannot be checked: the array can come from dynamic (any or unknown) code (contract-int32-arrays.ts:19:25), whose element stores scriptc does not see
|
||||
|
||||
|
||||
@@ -21,6 +21,6 @@ contract-int32-escapes.ts:16:1 - error SC7002: the value stored into 'Box.size'
|
||||
15 | const loose: any = box;
|
||||
16 | loose.size = 0.5;
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
17 | console.log([1.5, 2].map(double).join(","), box.size);
|
||||
17 | const pick: (x: number) => number = double;
|
||||
|
||||
hint: make the int32 conversion explicit, for example 'loose.size = 0.5 | 0', so Node computes the same value
|
||||
@@ -48,6 +48,15 @@ contract-int32.ts:31:20 - error SC7002: the value stored into 'len' (Int32) is n
|
||||
30 | const sizes = [1, 2, 3];
|
||||
31 | const len: Int32 = sizes.length;
|
||||
| ^~~~~~~~~~~~
|
||||
32 |
|
||||
32 | const weights = [1.5, 2];
|
||||
|
||||
hint: make the int32 conversion explicit, for example 'sizes.length | 0', so Node computes the same value
|
||||
hint: make the int32 conversion explicit, for example 'sizes.length | 0', so Node computes the same value
|
||||
|
||||
contract-int32.ts:33:30 - error SC7002: the value stored into 'w' (Int32) is not provably an int32
|
||||
|
||||
32 | const weights = [1.5, 2];
|
||||
33 | const shifted = weights.map((w: Int32) => w + 1);
|
||||
| ^
|
||||
34 |
|
||||
|
||||
hint: make the int32 conversion explicit with '| 0', so Node computes the same value
|
||||
Reference in New Issue
Block a user