mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Extend integer clones to arrays, mutual tail calls and proven ranges
Clones now cover more recursive number shapes: - Recursion over array indices: a function may take number arrays it borrows (the caller keeps them alive for the call) and read `length` and elements. An element inside the array's hole-free prefix that is exactly an int32 is read inline; holes, other indices and other numbers bail to the ordinary body. Borrowed entries get dispatchers too. - Mutual tail recursion: tail calls between members of a recursive component return the callee's result as is, so LLVM inlines them into one loop (isEven/isOdd), as it already does for the ordinary body. - The proven int32-slots ranges of number parameters (valid at every call site, recursive ones included) become `llvm.assume`s in each activation, so `%` of non-negative values divides unsigned without -0 handling (gcd). A clone now overflows the stack where the ordinary body would, instead of bailing after 2^14 loop iterations. LLVM turns the ordinary body's plain tail calls into loops, so the clone's plain tail calls (self or mutual) run unchecked too: endless tail recursion runs forever on either path, as it did before clones. An accumulating call (`1 + f(n - 1)`, fib's `f(n - 1) + f(n - 2)`) is real recursion in the ordinary body, because a double sum is not associative. Its clone loop carries a virtual stack pointer that starts at the real one (an inlined activation one frame below its parent's) and drops 32 bytes, about an ordinary frame of these functions, per iteration; passing the stack guard throws the stack-overflow RangeError. Deep finite chains stay on the fast path, and `1 + down(n - 1)` with n = 1e7 still throws like Node (4984-generator-exceptions, corpus 5342). Corpus 5343 covers array-index recursion, binomials, holes and non-integer elements.
This commit is contained in:
@@ -80,7 +80,12 @@ import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
|
||||
import { addRecordResultEntries } from "./record-results.js";
|
||||
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
|
||||
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
import { analyzeIntegerRanges, INT32_RANGE, type IntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import {
|
||||
analyzeIntegerRanges,
|
||||
INT32_RANGE,
|
||||
type IntegerRange,
|
||||
type IntegerRanges,
|
||||
} from "../../ir/integer-ranges.js";
|
||||
import { unshareNodes } from "../../ir/unshare.js";
|
||||
import { analyzeInt32Slots, type Int32Slots } from "../../ir/int32-slots.js";
|
||||
import { analyzeFinalFields, constructorClass, type FinalFields } from "../../ir/final-fields.js";
|
||||
@@ -867,6 +872,10 @@ export class LlEmitter {
|
||||
readonly integerClones: IntegerClones;
|
||||
/** The IR function being emitted. */
|
||||
currentFunctionName = "";
|
||||
|
||||
integerParamRange(fnName: string, localId: string): IntegerRange | null {
|
||||
return this.int32Slots.facts(fnName).params?.get(localId) ?? null;
|
||||
}
|
||||
/** Immutable module constants (contract types) whose graphs become
|
||||
* immortal once initialized: their reads need no reference counting. */
|
||||
readonly immortalGlobals: ReadonlySet<string>;
|
||||
@@ -1031,17 +1040,6 @@ export class LlEmitter {
|
||||
this.mayThrow = mt.fns;
|
||||
this.workerEntryPolls = mt.workerEntryPolls ?? null;
|
||||
this.stackChecks = mt.stackChecks ?? null;
|
||||
// Integer clones signal a stack overflow through their result, which
|
||||
// needs the single-threaded guard; worker programs also poll for
|
||||
// termination, and debug builds and stack traces keep every frame.
|
||||
this.integerClones =
|
||||
this.debug === null &&
|
||||
this.stackChecks !== null &&
|
||||
mod.workers !== true &&
|
||||
mod.lib === undefined &&
|
||||
!this.stackTraces
|
||||
? findIntegerClones(mod)
|
||||
: NO_INTEGER_CLONES;
|
||||
this.publishedTypes = computePublishedTypes(mod);
|
||||
this.immutableFields = computeImmutableFields(mod);
|
||||
this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`));
|
||||
@@ -1138,6 +1136,23 @@ export class LlEmitter {
|
||||
},
|
||||
);
|
||||
this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes);
|
||||
// Integer clones signal a stack overflow through their result, which
|
||||
// needs the single-threaded guard; worker programs also poll for
|
||||
// termination, and debug builds and stack traces keep every frame.
|
||||
// Clones read number arrays their function borrows (64-bit layouts).
|
||||
this.integerClones =
|
||||
this.debug === null &&
|
||||
this.stackChecks !== null &&
|
||||
mod.workers !== true &&
|
||||
mod.lib === undefined &&
|
||||
!this.stackTraces
|
||||
? findIntegerClones(
|
||||
mod,
|
||||
this.sizeType === "i64"
|
||||
? (fn, index) => this.callLifetimes.borrowed.get(fn)?.has(index) === true
|
||||
: undefined,
|
||||
)
|
||||
: NO_INTEGER_CLONES;
|
||||
this.stackCallbacks = new StackCallbacks(this.fnByName);
|
||||
this.receiverReaders = new AmbientReceiverReaders(this.fnByName);
|
||||
this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
|
||||
@@ -6039,7 +6054,7 @@ export class LlEmitter {
|
||||
const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
|
||||
let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`;
|
||||
if (int32Abi) body += "\n" + this.emitInt32AbiWrapper(fn, symbol, int32Abi);
|
||||
if (!borrowed && this.integerClones.members.has(fn.name)) {
|
||||
if (this.integerClones.members.has(fn.name)) {
|
||||
body += "\n" + emitIntegerClone(fn, this, this.stackChecks?.has(fn.name) === true);
|
||||
if (this.integerClones.dispatched.has(fn.name))
|
||||
body +=
|
||||
|
||||
@@ -184,11 +184,11 @@ export function emitCallExpr(
|
||||
// never unwinds — rejections surface at await (computeMayThrow's
|
||||
// async exclusion).
|
||||
// Callers outside a recursive integer-clone member's recursion enter
|
||||
// its dispatcher (integer-clones.ts), which has the same signature.
|
||||
const dispatch =
|
||||
!borrowed && dispatchesCall(host.integerClones, host.currentFunctionName, e.callee)
|
||||
? INTEGER_DISPATCH_SUFFIX
|
||||
: "";
|
||||
// its dispatcher (integer-clones.ts), which has the signature of the
|
||||
// entry it replaces (ordinary or borrowed, double or i32).
|
||||
const dispatch = dispatchesCall(host.integerClones, host.currentFunctionName, e.callee)
|
||||
? INTEGER_DISPATCH_SUFFIX
|
||||
: "";
|
||||
const target = `@${borrowed ? mangleBorrowedFunction(e.callee) : host.callTarget(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}${dispatch}`;
|
||||
if (e.type.kind === "void") {
|
||||
B.line(`call void ${target}(${argList})`);
|
||||
|
||||
@@ -216,7 +216,9 @@ test("the clone loops over its accumulating self call and outside callers dispat
|
||||
const clone = define(llvm, "sc_f_fib.int");
|
||||
expect(clone).toContain("define internal { i32, i1 } @sc_f_fib.int(i32 %a0)");
|
||||
// One recursive call per level: fib(n - 2) is the loop's next iteration.
|
||||
expect(clone.match(/call \{ i32, i1 \} @sc_f_fib\.int\(/g)).toHaveLength(1);
|
||||
expect(clone.match(/= call \{ i32, i1 \} @sc_f_fib\.int\(/g)).toHaveLength(1);
|
||||
// Accumulating iterations lower the virtual stack pointer by a frame.
|
||||
expect(clone).toMatch(/sub i64 %t\d+, 32/);
|
||||
expect(clone).toMatch(/br label %tail\d+/);
|
||||
expect(clone).toContain("call void @scr_stack_overflow()");
|
||||
expect(clone).toContain("ret { i32, i1 } { i32 poison, i1 true }");
|
||||
@@ -233,10 +235,15 @@ test("the clone loops over its accumulating self call and outside callers dispat
|
||||
expect(define(llvm, "sc_f_fib")).not.toContain(".spec(");
|
||||
// The closure keeps the ordinary entry.
|
||||
expect(llvm).not.toMatch(/ptr @sc_f_fib\.(spec|int)\b/);
|
||||
// Boolean clones forward BAIL from mutual calls.
|
||||
// Boolean clones forward BAIL from mutual calls: a plain mutual tail call
|
||||
// returns the callee's result as is and, like the ordinary body's tail
|
||||
// call, carries no virtual stack.
|
||||
const even = define(llvm, "sc_f_isEven.int");
|
||||
expect(even).toContain("call { i32, i1 } @sc_f_isOdd.int(");
|
||||
expect(even).toMatch(/extractvalue \{ i32, i1 \} %t\d+, 1/);
|
||||
expect(even).toContain("define internal { i32, i1 } @sc_f_isEven.int(i32 %a0)");
|
||||
expect(even).toMatch(
|
||||
/= tail call \{ i32, i1 \} @sc_f_isOdd\.int\(i32 %t\d+\)\n\s*ret \{ i32, i1 \} %t\d+/,
|
||||
);
|
||||
expect(even).not.toContain("virtual stack pointer");
|
||||
expect(define(llvm, "sc_f_helped.int")).toContain("call { i32, i1 } @sc_f_helper.int(");
|
||||
expect(llvm).not.toContain("@sc_f_helper.spec");
|
||||
});
|
||||
@@ -248,3 +255,43 @@ test("debug and library builds emit no clones", () => {
|
||||
expect(debug).not.toContain(".int(");
|
||||
expect(debug).not.toContain(".spec(");
|
||||
});
|
||||
|
||||
test("borrowed number arrays: length and element reads qualify", () => {
|
||||
const arr: IrType = { kind: "array", elem: F64 };
|
||||
const a = ref("a", arr);
|
||||
const length: IrExpr = {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: a,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const element: IrExpr = { kind: "arrayGet", arr: a, index: ref("i"), type: F64, loc };
|
||||
// sum(a, i) = i < a.length ? a[i] + sum(a, i + 1) : 0
|
||||
const sum = fn(
|
||||
"sum",
|
||||
["a", "i"],
|
||||
[
|
||||
ret(
|
||||
ternary(
|
||||
bin("<", ref("i"), length),
|
||||
bin("+", element, call("sum", [a, bin("+", ref("i"), num(1))])),
|
||||
num(0),
|
||||
),
|
||||
),
|
||||
],
|
||||
F64,
|
||||
[arr, F64],
|
||||
);
|
||||
const mod: IrModule = { irVersion: 16, sourceFile: loc.file, entry: "sum", functions: [sum] };
|
||||
expect(findIntegerClones(mod).members.has("sum")).toBe(false);
|
||||
expect(findIntegerClones(mod, () => false).members.has("sum")).toBe(false);
|
||||
const clones = findIntegerClones(mod, (name, index) => name === "sum" && index === 0);
|
||||
expect(clones.dispatched.has("sum")).toBe(true);
|
||||
// Arrays of other elements never qualify.
|
||||
const strings: IrType = { kind: "array", elem: { kind: "string" } };
|
||||
const other = fn("other", ["s"], [ret(num(1))], F64, [strings]);
|
||||
const withOther: IrModule = { ...mod, functions: [sum, other] };
|
||||
expect(findIntegerClones(withOther, () => true).members.has("other")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { IrExpr, IrFunction, IrLocal, IrModule, IrStmt, IrType } from "../../ir/ir.js";
|
||||
import { mangleFunction } from "../mangle.js";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
import { SCR_ARR_FIELD } from "./common.js";
|
||||
|
||||
/** Speculative integer clones of pure number-valued functions.
|
||||
*
|
||||
* A function qualifies when its parameters, locals and result are numbers
|
||||
* or booleans and its body only reads and writes its own locals, branches,
|
||||
* computes with numbers and booleans, and directly calls other qualifying
|
||||
* functions. Such a body has no observable effect besides its result, a
|
||||
* or booleans (parameters may also be number arrays it borrows) and its
|
||||
* body only reads and writes its own locals, reads `length` and elements of
|
||||
* those arrays, branches, computes with numbers and booleans, and directly
|
||||
* calls other qualifying functions. Such a body has no observable effect besides its result, a
|
||||
* stack-overflow RangeError, or not terminating.
|
||||
*
|
||||
* Its clone (`<symbol>.int`) takes its number parameters as i32 and keeps
|
||||
@@ -36,10 +38,19 @@ import { BlockBuilder } from "./blocks.js";
|
||||
* can arise, so calls between clones need no pending test.
|
||||
*
|
||||
* Self tail calls (`return f(...)`) and accumulating self calls (`return
|
||||
* x + f(...)`, also `f(...) + x` when `x` calls nothing) become a loop.
|
||||
* The loop counts its iterations and bails beyond TAIL_LIMIT, so recursion
|
||||
* that would overflow the stack (or never end) reruns as ordinary
|
||||
* recursion and throws there. Its int32 accumulator is overflow checked.
|
||||
* x + f(...)`, also `f(...) + x` when `x` calls nothing) become a loop, and
|
||||
* tail calls between members of a component return the callee's result as
|
||||
* is, so LLVM inlines them into one loop.
|
||||
*
|
||||
* A clone overflows the stack where the ordinary body would. LLVM already
|
||||
* turns the ordinary body's plain tail calls into loops, so the clone's
|
||||
* plain tail calls run unchecked too (endless tail recursion runs forever
|
||||
* on either path, like before clones). An accumulating call is real
|
||||
* recursion in the ordinary body (a double sum is not associative), so its
|
||||
* loop carries a virtual stack pointer, starting at the real one, that
|
||||
* every accumulating iteration lowers by one VIRTUAL_FRAME; passing the
|
||||
* stack guard throws Node's RangeError like the real overflow would. The
|
||||
* int32 accumulator is overflow checked.
|
||||
* That is exact: with every partial sum and the final result in the int32
|
||||
* range, every value the ordinary body adds is the final result minus a
|
||||
* partial sum, within 2^32, so each double addition is exact. */
|
||||
@@ -50,20 +61,25 @@ export interface IntegerClones {
|
||||
readonly dispatched: ReadonlySet<string>;
|
||||
/** The recursive component of each dispatched member. */
|
||||
readonly component: ReadonlyMap<string, number>;
|
||||
/** Members of components with tail calls between different members,
|
||||
* which return the callee's result as is. */
|
||||
readonly tailLinked: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
export const NO_INTEGER_CLONES: IntegerClones = {
|
||||
members: new Set(),
|
||||
dispatched: new Set(),
|
||||
component: new Map(),
|
||||
tailLinked: new Set(),
|
||||
};
|
||||
|
||||
/** A clone's result: the value and whether the clone bailed. */
|
||||
export const CLONE_RESULT = "{ i32, i1 }";
|
||||
/** The result of a clone that bailed: no caller reads its value. */
|
||||
const BAIL = `${CLONE_RESULT} { i32 poison, i1 true }`;
|
||||
/** Self tail-call iterations one clone frame runs before it bails (2^14). */
|
||||
const TAIL_LIMIT = 16384;
|
||||
/** The stack one replaced recursion level is charged, in bytes. Ordinary
|
||||
* frames of these functions are about that size. */
|
||||
const VIRTUAL_FRAME = 32;
|
||||
|
||||
/** The single-threaded stack guard, declared as the emitter's ordinary
|
||||
* stack checks declare it (clones only exist without workers). */
|
||||
@@ -73,11 +89,26 @@ export const INTEGER_CLONE_SUFFIX = ".int";
|
||||
export const INTEGER_DISPATCH_SUFFIX = ".spec";
|
||||
|
||||
type Scalar = "num" | "bool";
|
||||
/** A value kind: an int32 number, a boolean, or a borrowed number array. */
|
||||
type Kind = Scalar | "arr";
|
||||
|
||||
function scalarKind(type: IrType): Scalar | null {
|
||||
return type.kind === "f64" ? "num" : type.kind === "bool" ? "bool" : null;
|
||||
}
|
||||
|
||||
function valueKind(type: IrType): Kind | null {
|
||||
return type.kind === "array" && type.elem.kind === "f64" ? "arr" : scalarKind(type);
|
||||
}
|
||||
|
||||
/** The LLVM type of a value of `kind` inside a clone. */
|
||||
function kindType(kind: Kind): string {
|
||||
return kind === "num" ? "i32" : kind === "bool" ? "i1" : "ptr";
|
||||
}
|
||||
|
||||
/** Whether the function borrows its parameter at `index` (callers keep the
|
||||
* array alive and unchanged for the call); absent, clones take no arrays. */
|
||||
export type BorrowedParameter = (fnName: string, index: number) => boolean;
|
||||
|
||||
function isInt32Literal(value: number): boolean {
|
||||
return (
|
||||
Number.isInteger(value) && value >= -0x80000000 && value <= 0x7fffffff && !Object.is(value, -0)
|
||||
@@ -85,19 +116,29 @@ function isInt32Literal(value: number): boolean {
|
||||
}
|
||||
|
||||
/** The call targets of `fn` when its body has the qualifying shape. */
|
||||
function cloneShape(fn: IrFunction): Set<string> | null {
|
||||
function cloneShape(fn: IrFunction, borrowed?: BorrowedParameter): Set<string> | null {
|
||||
if (fn.async || fn.generator !== undefined || fn.captures !== undefined) return null;
|
||||
if (fn.classCaptures !== undefined && fn.classCaptures.length > 0) return null;
|
||||
const result = scalarKind(fn.returnType);
|
||||
if (result === null) return null;
|
||||
const locals = new Map<string, IrLocal>(fn.locals.map((l) => [l.id, l]));
|
||||
// Array parameters are only read: never assigned, and their locals are
|
||||
// the parameters themselves.
|
||||
const arrays = new Set<string>();
|
||||
for (const [index, p] of fn.params.entries())
|
||||
if (valueKind(p.type) === "arr") {
|
||||
if (borrowed?.(fn.name, index) !== true) return null;
|
||||
arrays.add(p.localId);
|
||||
}
|
||||
const localKind = (id: string): Scalar | null => {
|
||||
const local = locals.get(id);
|
||||
if (!local || local.boxed || local.tdz) return null;
|
||||
return scalarKind(local.type);
|
||||
};
|
||||
const arrayRef = (e: IrExpr): boolean => e.kind === "varRef" && arrays.has(e.localId);
|
||||
for (const p of fn.params)
|
||||
if (localKind(p.localId) === null || scalarKind(p.type) === null) return null;
|
||||
if (!arrays.has(p.localId) && (localKind(p.localId) === null || scalarKind(p.type) === null))
|
||||
return null;
|
||||
const callees = new Set<string>();
|
||||
const expr = (e: IrExpr, want: Scalar): boolean => {
|
||||
if (scalarKind(e.type) !== want) return false;
|
||||
@@ -147,11 +188,17 @@ function cloneShape(fn: IrFunction): Set<string> | null {
|
||||
return want === "bool" && expr(e.left, "bool") && expr(e.right, "bool");
|
||||
case "ternary":
|
||||
return expr(e.cond, "bool") && expr(e.then, want) && expr(e.else_, want);
|
||||
case "arrIntrinsic":
|
||||
return (
|
||||
want === "num" && e.method === "length" && e.args.length === 0 && arrayRef(e.receiver)
|
||||
);
|
||||
case "arrayGet":
|
||||
return want === "num" && arrayRef(e.arr) && expr(e.index, "num");
|
||||
case "call":
|
||||
callees.add(e.callee);
|
||||
return e.args.every((a) => {
|
||||
const kind = scalarKind(a.type);
|
||||
return kind !== null && expr(a, kind);
|
||||
const kind = valueKind(a.type);
|
||||
return kind === "arr" ? arrayRef(a) : kind !== null && expr(a, kind);
|
||||
});
|
||||
default:
|
||||
return false;
|
||||
@@ -196,7 +243,7 @@ function callsMatch(fn: IrFunction, byName: ReadonlyMap<string, IrFunction>): bo
|
||||
!callee ||
|
||||
callee.params.length !== e.args.length ||
|
||||
scalarKind(callee.returnType) !== scalarKind(e.type) ||
|
||||
callee.params.some((p, i) => scalarKind(p.type) !== scalarKind(e.args[i]!.type))
|
||||
callee.params.some((p, i) => valueKind(p.type) !== valueKind(e.args[i]!.type))
|
||||
)
|
||||
ok = false;
|
||||
e.args.forEach(visit);
|
||||
@@ -216,6 +263,9 @@ function callsMatch(fn: IrFunction, byName: ReadonlyMap<string, IrFunction>): bo
|
||||
visit(e.then);
|
||||
visit(e.else_);
|
||||
return;
|
||||
case "arrayGet":
|
||||
visit(e.index);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
@@ -237,11 +287,11 @@ function callsMatch(fn: IrFunction, byName: ReadonlyMap<string, IrFunction>): bo
|
||||
return ok;
|
||||
}
|
||||
|
||||
export function findIntegerClones(mod: IrModule): IntegerClones {
|
||||
export function findIntegerClones(mod: IrModule, borrowed?: BorrowedParameter): IntegerClones {
|
||||
const byName = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const edges = new Map<string, Set<string>>();
|
||||
for (const fn of mod.functions) {
|
||||
const callees = cloneShape(fn);
|
||||
const callees = cloneShape(fn, borrowed);
|
||||
if (callees && callsMatch(fn, byName)) edges.set(fn.name, callees);
|
||||
}
|
||||
// Drop functions that call a non-member until none does.
|
||||
@@ -292,7 +342,38 @@ export function findIntegerClones(mod: IrModule): IntegerClones {
|
||||
}
|
||||
};
|
||||
for (const name of edges.keys()) if (!index.has(name)) strong(name);
|
||||
return { members: new Set(edges.keys()), dispatched, component };
|
||||
// Components with a tail call from one member to another.
|
||||
const mutual = new Set<number>();
|
||||
for (const name of dispatched)
|
||||
for (const callee of tailCallees(byName.get(name)!))
|
||||
if (callee !== name && component.get(callee) === component.get(name))
|
||||
mutual.add(component.get(name)!);
|
||||
const tailLinked = new Set([...dispatched].filter((name) => mutual.has(component.get(name)!)));
|
||||
return { members: new Set(edges.keys()), dispatched, component, tailLinked };
|
||||
}
|
||||
|
||||
/** The functions `fn` calls in tail position: as a returned value, directly
|
||||
* or as an arm of a returned conditional. */
|
||||
function tailCallees(fn: IrFunction): Set<string> {
|
||||
const result = new Set<string>();
|
||||
const value = (e: IrExpr): void => {
|
||||
if (e.kind === "call") result.add(e.callee);
|
||||
else if (e.kind === "ternary") {
|
||||
value(e.then);
|
||||
value(e.else_);
|
||||
}
|
||||
};
|
||||
const stmts = (list: readonly IrStmt[]): void => {
|
||||
for (const s of list) {
|
||||
if (s.kind === "return" && s.value) value(s.value);
|
||||
else if (s.kind === "if") {
|
||||
stmts(s.then);
|
||||
if (s.else_) stmts(s.else_);
|
||||
} else if (s.kind === "block") stmts(s.body);
|
||||
}
|
||||
};
|
||||
stmts(fn.body);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Whether a direct call from `caller` to `callee` enters the callee's
|
||||
@@ -305,6 +386,10 @@ export function dispatchesCall(clones: IntegerClones, caller: string, callee: st
|
||||
}
|
||||
|
||||
export interface IntegerCloneHost {
|
||||
readonly integerClones: IntegerClones;
|
||||
/** The range every caller passes to a number parameter (int32-slots
|
||||
* facts, proven over every call site, recursive ones included), if any. */
|
||||
integerParamRange(fnName: string, localId: string): { min: number; max: number } | null;
|
||||
declare(decl: string): void;
|
||||
/** Lines setting the i1 `dest` to "an exception is pending". */
|
||||
pendingTestLines(dest: string): string[];
|
||||
@@ -330,6 +415,8 @@ function callsAnything(e: IrExpr): boolean {
|
||||
return callsAnything(e.operand);
|
||||
case "ternary":
|
||||
return callsAnything(e.cond) || callsAnything(e.then) || callsAnything(e.else_);
|
||||
case "arrayGet":
|
||||
return callsAnything(e.index);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -344,7 +431,7 @@ function mayBail(e: IrExpr): boolean {
|
||||
/** One activation of the body: the clone's own, or a self call inlined
|
||||
* into it. */
|
||||
interface Frame {
|
||||
slots: Map<string, { slot: string; kind: Scalar }>;
|
||||
slots: Map<string, { slot: string; kind: Kind }>;
|
||||
loopLabel: string | null;
|
||||
accSlot: string | null;
|
||||
depthSlot: string | null;
|
||||
@@ -393,9 +480,14 @@ class CloneEmitter {
|
||||
private bailLabel: string | null = null;
|
||||
private frame!: Frame;
|
||||
private slotCount = 0;
|
||||
/** The real stack pointer at the clone's entry (i64), once the stack
|
||||
* check read it for an accumulating loop. */
|
||||
private entrySp: string | null = null;
|
||||
/** Inline one level of non-tail self calls: each removes the call, the
|
||||
* stack check and the frame of a recursion level (leaf calls entirely). */
|
||||
private readonly inlineSelf: boolean;
|
||||
/** Tail calls to other members pass through (IntegerClones.tailLinked). */
|
||||
private readonly tailLinked: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly fn: IrFunction,
|
||||
@@ -403,6 +495,7 @@ class CloneEmitter {
|
||||
private readonly stackCheck: boolean,
|
||||
) {
|
||||
this.result = scalarKind(fn.returnType)!;
|
||||
this.tailLinked = host.integerClones.tailLinked.has(fn.name);
|
||||
const size = stmtsSize(fn.body);
|
||||
this.inlineSelf = size * this.selfCallSites() <= INLINE_BUDGET;
|
||||
}
|
||||
@@ -464,6 +557,7 @@ class CloneEmitter {
|
||||
* opens its tail loop. */
|
||||
private openFrame(args: readonly string[], inline: Frame["inline"]): void {
|
||||
const { B, fn } = this;
|
||||
const parent = inline === null ? null : this.frame;
|
||||
const frame: Frame = {
|
||||
slots: new Map(),
|
||||
loopLabel: null,
|
||||
@@ -471,25 +565,27 @@ class CloneEmitter {
|
||||
depthSlot: null,
|
||||
inline,
|
||||
};
|
||||
const params = new Set(fn.params.map((p) => p.localId));
|
||||
for (const local of fn.locals) {
|
||||
const kind = scalarKind(local.type);
|
||||
const kind = params.has(local.id) ? valueKind(local.type) : scalarKind(local.type);
|
||||
if (kind === null || local.boxed) continue;
|
||||
const slot = `%l${this.slotCount++}`;
|
||||
B.entryAllocas.push(`${slot} = alloca ${kind === "num" ? "i32" : "i1"} ; ${local.name}`);
|
||||
B.entryAllocas.push(`${slot} = alloca ${kindType(kind)} ; ${local.name}`);
|
||||
frame.slots.set(local.id, { slot, kind });
|
||||
}
|
||||
fn.params.forEach((p, i) => {
|
||||
const { slot, kind } = frame.slots.get(p.localId)!;
|
||||
B.line(`store ${kind === "num" ? "i32" : "i1"} ${args[i]!}, ptr ${slot}`);
|
||||
B.line(`store ${kindType(kind)} ${args[i]!}, ptr ${slot}`);
|
||||
});
|
||||
this.frame = frame;
|
||||
if (inline === null && this.stackCheck) this.emitStackCheck();
|
||||
const tail = this.tailShape();
|
||||
if (!tail.loop) return;
|
||||
frame.depthSlot = `%l${this.slotCount++}`;
|
||||
B.entryAllocas.push(`${frame.depthSlot} = alloca i32 ; tail iterations left`);
|
||||
B.line(`store i32 ${TAIL_LIMIT}, ptr ${frame.depthSlot}`);
|
||||
if (!tail.loop) {
|
||||
this.assumeParamRanges();
|
||||
return;
|
||||
}
|
||||
if (tail.accumulates) {
|
||||
this.openVirtualStack(frame, parent);
|
||||
frame.accSlot = `%l${this.slotCount++}`;
|
||||
B.entryAllocas.push(`${frame.accSlot} = alloca i32 ; accumulator`);
|
||||
B.line(`store i32 0, ptr ${frame.accSlot}`);
|
||||
@@ -497,18 +593,64 @@ class CloneEmitter {
|
||||
frame.loopLabel = B.newLabel("tail");
|
||||
B.br(frame.loopLabel);
|
||||
B.startBlock(frame.loopLabel);
|
||||
// Tested here rather than at each tail call, where LLVM would merge the
|
||||
// test with the accumulator's overflow test into one select.
|
||||
const depth = B.tmp();
|
||||
const left = B.tmp();
|
||||
B.line(`${depth} = load i32, ptr ${frame.depthSlot}`);
|
||||
B.line(`${left} = icmp ne i32 ${depth}, 0`);
|
||||
this.require(left);
|
||||
this.assumeParamRanges();
|
||||
}
|
||||
|
||||
/** Starts the virtual stack pointer of an activation with an accumulating
|
||||
* loop. */
|
||||
private openVirtualStack(frame: Frame, parent: Frame | null): void {
|
||||
const { B } = this;
|
||||
frame.depthSlot = `%l${this.slotCount++}`;
|
||||
B.entryAllocas.push(`${frame.depthSlot} = alloca i64 ; virtual stack pointer`);
|
||||
// The activation starts at the stack pointer its entry check read; an
|
||||
// inlined one a frame below its parent's virtual stack pointer, or at
|
||||
// the real one read afresh.
|
||||
let start = frame.inline === null ? this.entrySp : null;
|
||||
if (parent !== null && parent.depthSlot !== null) {
|
||||
const at = B.tmp();
|
||||
start = B.tmp();
|
||||
B.line(`${at} = load i64, ptr ${parent.depthSlot}`);
|
||||
B.line(`${start} = sub i64 ${at}, ${VIRTUAL_FRAME}`);
|
||||
} else if (start === null) {
|
||||
this.host.declare(`declare ptr @llvm.stacksave.p0()`);
|
||||
const sp = B.tmp();
|
||||
start = B.tmp();
|
||||
B.line(`${sp} = call ptr @llvm.stacksave.p0()`);
|
||||
B.line(`${start} = ptrtoint ptr ${sp} to i64`);
|
||||
}
|
||||
B.line(`store i64 ${start}, ptr ${frame.depthSlot}`);
|
||||
}
|
||||
|
||||
/** Tells LLVM the proven ranges of the current activation's number
|
||||
* parameters (`%` of non-negative operands then needs no sign handling). */
|
||||
private assumeParamRanges(): void {
|
||||
const { B } = this;
|
||||
for (const p of this.fn.params) {
|
||||
const range = this.host.integerParamRange(this.fn.name, p.localId);
|
||||
const slot = this.frame.slots.get(p.localId);
|
||||
if (!range || slot?.kind !== "num") continue;
|
||||
const bounds: string[] = [];
|
||||
const value = B.tmp();
|
||||
B.line(`${value} = load i32, ptr ${slot.slot}`);
|
||||
if (range.min > -0x80000000) {
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = icmp sge i32 ${value}, ${range.min}`);
|
||||
bounds.push(t);
|
||||
}
|
||||
if (range.max < 0x7fffffff) {
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = icmp sle i32 ${value}, ${range.max}`);
|
||||
bounds.push(t);
|
||||
}
|
||||
if (bounds.length === 0) continue;
|
||||
this.host.declare(`declare void @llvm.assume(i1)`);
|
||||
for (const bound of bounds) B.line(`call void @llvm.assume(i1 ${bound})`);
|
||||
}
|
||||
}
|
||||
|
||||
emit(): string {
|
||||
const { B, fn } = this;
|
||||
const params = fn.params.map((p, i) => `${scalarKind(p.type) === "num" ? "i32" : "i1"} %a${i}`);
|
||||
const params = fn.params.map((p, i) => `${kindType(valueKind(p.type)!)} %a${i}`);
|
||||
this.openFrame(
|
||||
fn.params.map((_, i) => `%a${i}`),
|
||||
null,
|
||||
@@ -520,6 +662,12 @@ class CloneEmitter {
|
||||
B.startBlock(this.bailLabel);
|
||||
B.terminate(`ret ${BAIL}`);
|
||||
}
|
||||
if (this.overflowLabel !== null) {
|
||||
this.host.declare(`declare void @scr_stack_overflow()`);
|
||||
B.startBlock(this.overflowLabel);
|
||||
B.line(`call void @scr_stack_overflow()`);
|
||||
B.terminate(`ret ${BAIL}`);
|
||||
}
|
||||
const symbol = `${mangleFunction(fn.name)}${INTEGER_CLONE_SUFFIX}`;
|
||||
return `define internal ${CLONE_RESULT} @${symbol}(${params.join(", ")}) #0 { ; ${fn.name} (integer clone)\n${B.render()}\n}`;
|
||||
}
|
||||
@@ -617,12 +765,45 @@ class CloneEmitter {
|
||||
B.line(`${guard} = load ptr, ptr @scr_stack_guard`);
|
||||
B.line(`${low} = icmp ult ptr ${sp}, ${guard}`);
|
||||
B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${low}, i1 false)`);
|
||||
const over = B.newLabel("stack.over");
|
||||
const ok = B.newLabel("stack.ok");
|
||||
B.condBr(cold, over, ok);
|
||||
B.startBlock(over);
|
||||
B.line(`call void @scr_stack_overflow()`);
|
||||
B.terminate(`ret ${BAIL}`);
|
||||
B.condBr(cold, this.overflow(), ok);
|
||||
B.startBlock(ok);
|
||||
if (!this.tailShape().accumulates) return;
|
||||
const top = B.tmp();
|
||||
B.line(`${top} = ptrtoint ptr ${sp} to i64`);
|
||||
this.entrySp = top;
|
||||
}
|
||||
|
||||
private overflowLabel: string | null = null;
|
||||
|
||||
/** The shared block that throws Node's stack-overflow RangeError and
|
||||
* bails (the dispatcher forwards the pending exception). */
|
||||
private overflow(): string {
|
||||
return (this.overflowLabel ??= this.B.newLabel("stack.over"));
|
||||
}
|
||||
|
||||
/** Lowers the current activation's virtual stack pointer by one frame,
|
||||
* continuing at the overflow block past the guard. */
|
||||
private consumeFrame(): void {
|
||||
const { B } = this;
|
||||
this.host.declare(STACK_GUARD_DECL);
|
||||
const depth = B.tmp();
|
||||
const next = B.tmp();
|
||||
const guardPtr = B.tmp();
|
||||
const guard = B.tmp();
|
||||
const more = B.tmp();
|
||||
const likely = B.tmp();
|
||||
B.line(`${depth} = load i64, ptr ${this.frame.depthSlot!}`);
|
||||
B.line(`${next} = sub i64 ${depth}, ${VIRTUAL_FRAME}`);
|
||||
B.line(`store i64 ${next}, ptr ${this.frame.depthSlot!}`);
|
||||
B.line(`${guardPtr} = load ptr, ptr @scr_stack_guard`);
|
||||
B.line(`${guard} = ptrtoint ptr ${guardPtr} to i64`);
|
||||
// Unsigned: a pointer that wrapped below zero is far below the guard.
|
||||
B.line(`${more} = icmp uge i64 ${next}, ${guard}`);
|
||||
this.host.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
|
||||
B.line(`${likely} = call i1 @llvm.expect.i1(i1 ${more}, i1 true)`);
|
||||
const ok = B.newLabel("frame.ok");
|
||||
B.condBr(likely, ok, this.overflow());
|
||||
B.startBlock(ok);
|
||||
}
|
||||
|
||||
@@ -690,14 +871,23 @@ class CloneEmitter {
|
||||
this.emitReturn(e.else_);
|
||||
return;
|
||||
}
|
||||
if (e.kind === "call" && this.passesThrough(e)) return this.emitMutualTailCall(e);
|
||||
if (this.frame.loopLabel !== null) {
|
||||
if (this.isSelfCall(e)) return this.emitTailCall(e, null);
|
||||
const accumulating = this.accumulatingCall(e);
|
||||
if (accumulating) return this.emitTailCall(accumulating.call, accumulating.term);
|
||||
}
|
||||
if (this.result === "bool") this.finishReturn(this.emitBool(e));
|
||||
else this.finishReturn(this.emitNum(e));
|
||||
}
|
||||
|
||||
/** Returns `value` from the current activation: a number result adds the
|
||||
* activation's accumulator. */
|
||||
private finishReturn(result: NumValue | string): void {
|
||||
const { B } = this;
|
||||
const inline = this.frame.inline;
|
||||
if (this.result === "bool") {
|
||||
const value = this.emitBool(e);
|
||||
if (typeof result === "string") {
|
||||
const value = result;
|
||||
if (inline) {
|
||||
B.line(`store i1 ${value}, ptr ${inline.result}`);
|
||||
B.br(inline.done);
|
||||
@@ -708,7 +898,7 @@ class CloneEmitter {
|
||||
B.terminate(`ret ${CLONE_RESULT} ${this.result32(wide)}`);
|
||||
return;
|
||||
}
|
||||
let value = this.emitNum(e);
|
||||
let value = result;
|
||||
if (this.frame.accSlot !== null) {
|
||||
const acc = B.tmp();
|
||||
B.line(`${acc} = load i32, ptr ${this.frame.accSlot}`);
|
||||
@@ -726,6 +916,28 @@ class CloneEmitter {
|
||||
B.terminate(`ret ${CLONE_RESULT} ${this.result32(value.name)}`);
|
||||
}
|
||||
|
||||
/** A tail call to another member of a tail-linked component, from the
|
||||
* clone's own activation with nothing accumulated: it returns the
|
||||
* callee's result as is. */
|
||||
private passesThrough(call: Extract<IrExpr, { kind: "call" }>): boolean {
|
||||
const clones = this.host.integerClones;
|
||||
return (
|
||||
this.tailLinked &&
|
||||
this.frame.inline === null &&
|
||||
this.frame.accSlot === null &&
|
||||
call.callee !== this.fn.name &&
|
||||
clones.tailLinked.has(call.callee) &&
|
||||
clones.component.get(call.callee) === clones.component.get(this.fn.name)
|
||||
);
|
||||
}
|
||||
|
||||
private emitMutualTailCall(call: Extract<IrExpr, { kind: "call" }>): void {
|
||||
const { B } = this;
|
||||
const args = this.emitArgs(call);
|
||||
const result = this.emitCloneCall(call.callee, args, call.args, "tail ");
|
||||
B.terminate(`ret ${CLONE_RESULT} ${result}`);
|
||||
}
|
||||
|
||||
private emitTailCall(call: Extract<IrExpr, { kind: "call" }>, term: IrExpr | null): void {
|
||||
const { B } = this;
|
||||
// `f(...) + x` evaluates its call-free term first; the order is
|
||||
@@ -742,31 +954,119 @@ class CloneEmitter {
|
||||
}
|
||||
this.fn.params.forEach((p, i) => {
|
||||
const { slot, kind } = this.frame.slots.get(p.localId)!;
|
||||
B.line(`store ${kind === "num" ? "i32" : "i1"} ${args[i]!}, ptr ${slot}`);
|
||||
B.line(`store ${kindType(kind)} ${args[i]!}, ptr ${slot}`);
|
||||
});
|
||||
const depth = B.tmp();
|
||||
const next = B.tmp();
|
||||
B.line(`${depth} = load i32, ptr ${this.frame.depthSlot!}`);
|
||||
B.line(`${next} = sub i32 ${depth}, 1`);
|
||||
B.line(`store i32 ${next}, ptr ${this.frame.depthSlot!}`);
|
||||
// Only an accumulating call is real recursion in the ordinary body.
|
||||
if (addend !== null) this.consumeFrame();
|
||||
B.br(this.frame.loopLabel!);
|
||||
}
|
||||
|
||||
private emitArgs(call: Extract<IrExpr, { kind: "call" }>): string[] {
|
||||
return call.args.map((a) =>
|
||||
scalarKind(a.type) === "num" ? this.narrow(this.emitNum(a)) : this.emitBool(a),
|
||||
/** A call of `callee`'s clone with evaluated arguments. */
|
||||
private emitCloneCall(
|
||||
callee: string,
|
||||
args: readonly string[],
|
||||
exprs: readonly IrExpr[],
|
||||
marker = "",
|
||||
): string {
|
||||
const list = args.map((a, i) => `${kindType(valueKind(exprs[i]!.type)!)} ${a}`);
|
||||
const result = this.B.tmp();
|
||||
this.B.line(
|
||||
`${result} = ${marker}call ${CLONE_RESULT} @${mangleFunction(callee)}${INTEGER_CLONE_SUFFIX}(${list.join(", ")})`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The borrowed array an array parameter holds. */
|
||||
private emitArray(e: IrExpr): string {
|
||||
const value = this.B.tmp();
|
||||
if (e.kind !== "varRef") throw new Error("integer clone: unexpected array expression");
|
||||
this.B.line(`${value} = load ptr, ptr ${this.frame.slots.get(e.localId)!.slot}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
/** `a.length`, bailing beyond the int32 range. */
|
||||
private emitLength(array: string): string {
|
||||
const { B } = this;
|
||||
const field = B.tmp();
|
||||
const len = B.tmp();
|
||||
const fits = B.tmp();
|
||||
const value = B.tmp();
|
||||
B.line(
|
||||
`${field} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.len}`,
|
||||
);
|
||||
B.line(`${len} = load i64, ptr ${field}`);
|
||||
B.line(`${fits} = icmp ule i64 ${len}, 2147483647`);
|
||||
this.require(fits);
|
||||
B.line(`${value} = trunc i64 ${len} to i32`);
|
||||
return value;
|
||||
}
|
||||
|
||||
/** `a[i]` for an index inside the array's hole-free prefix (the runtime
|
||||
* keeps every slot below `filled` a present value) whose element is
|
||||
* exactly an int32. Everything else (holes, indices outside the prefix,
|
||||
* other numbers) bails: the ordinary body reads it. */
|
||||
private emitElement(e: Extract<IrExpr, { kind: "arrayGet" }>): string {
|
||||
const { B } = this;
|
||||
const array = this.emitArray(e.arr);
|
||||
const index = this.narrow(this.emitNum(e.index));
|
||||
const filledField = B.tmp();
|
||||
const filled = B.tmp();
|
||||
const inside = B.tmp();
|
||||
B.line(
|
||||
`${filledField} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.filled}`,
|
||||
);
|
||||
B.line(`${filled} = load i32, ptr ${filledField}`);
|
||||
// Unsigned: a negative index lies above every bound.
|
||||
B.line(`${inside} = icmp ult i32 ${index}, ${filled}`);
|
||||
this.require(inside);
|
||||
const dataField = B.tmp();
|
||||
const data = B.tmp();
|
||||
const offset = B.tmp();
|
||||
const slot = B.tmp();
|
||||
const number = B.tmp();
|
||||
B.line(
|
||||
`${dataField} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.data}`,
|
||||
);
|
||||
B.line(`${data} = load ptr, ptr ${dataField}`);
|
||||
B.line(`${offset} = zext i32 ${index} to i64`);
|
||||
B.line(`${slot} = getelementptr inbounds double, ptr ${data}, i64 ${offset}`);
|
||||
B.line(`${number} = load double, ptr ${slot}`);
|
||||
return this.exactInt32(number);
|
||||
}
|
||||
|
||||
/** The int32 the double `number` equals bit for bit (never -0, NaN, a
|
||||
* fraction or out of range), bailing otherwise. */
|
||||
private exactInt32(number: string): string {
|
||||
const { B } = this;
|
||||
this.host.declare(`declare i32 @llvm.fptosi.sat.i32.f64(double)`);
|
||||
const value = B.tmp();
|
||||
const back = B.tmp();
|
||||
const a = B.tmp();
|
||||
const b = B.tmp();
|
||||
const exact = B.tmp();
|
||||
B.line(`${value} = call i32 @llvm.fptosi.sat.i32.f64(double ${number})`);
|
||||
B.line(`${back} = sitofp i32 ${value} to double`);
|
||||
B.line(`${a} = bitcast double ${back} to i64`);
|
||||
B.line(`${b} = bitcast double ${number} to i64`);
|
||||
B.line(`${exact} = icmp eq i64 ${a}, ${b}`);
|
||||
this.require(exact);
|
||||
return value;
|
||||
}
|
||||
|
||||
private emitArgs(call: Extract<IrExpr, { kind: "call" }>): string[] {
|
||||
return call.args.map((a) => {
|
||||
const kind = valueKind(a.type);
|
||||
return kind === "arr"
|
||||
? this.emitArray(a)
|
||||
: kind === "num"
|
||||
? this.narrow(this.emitNum(a))
|
||||
: this.emitBool(a);
|
||||
});
|
||||
}
|
||||
|
||||
private emitCall(call: Extract<IrExpr, { kind: "call" }>): string {
|
||||
const { B } = this;
|
||||
const args = this.emitArgs(call);
|
||||
const types = call.args.map((a) => (scalarKind(a.type) === "num" ? "i32" : "i1"));
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
`${result} = call ${CLONE_RESULT} @${mangleFunction(call.callee)}${INTEGER_CLONE_SUFFIX}(${args.map((a, i) => `${types[i]} ${a}`).join(", ")})`,
|
||||
);
|
||||
return result;
|
||||
return this.emitCloneCall(call.callee, args, call.args);
|
||||
}
|
||||
|
||||
/** The successful CLONE_RESULT of the i32 `value`. */
|
||||
@@ -841,6 +1141,10 @@ class CloneEmitter {
|
||||
case "call":
|
||||
if (this.inlines(e)) return { name: this.emitInlineCall(e), wide: false };
|
||||
return { name: this.emitCall(e), wide: true };
|
||||
case "arrIntrinsic":
|
||||
return { name: this.emitLength(this.emitArray(e.receiver)), wide: false };
|
||||
case "arrayGet":
|
||||
return { name: this.emitElement(e), wide: false };
|
||||
case "unary": {
|
||||
const operand = this.narrow(this.emitNum(e.operand));
|
||||
const value = B.tmp();
|
||||
@@ -1069,10 +1373,13 @@ export function emitIntegerDispatcher(
|
||||
const cloneArgs: string[] = [];
|
||||
const exact: string[] = [];
|
||||
fn.params.forEach((p, i) => {
|
||||
if (scalarKind(p.type) === "bool") {
|
||||
params.push(`i1 %p${i}`);
|
||||
entryArgs.push(`i1 %p${i}`);
|
||||
cloneArgs.push(`i1 %p${i}`);
|
||||
const kind = valueKind(p.type);
|
||||
if (kind === "bool" || kind === "arr") {
|
||||
// Borrowed arrays pass through to the clone and the ordinary entry.
|
||||
const type = kindType(kind);
|
||||
params.push(`${type} %p${i}`);
|
||||
entryArgs.push(`${type} %p${i}`);
|
||||
cloneArgs.push(`${type} %p${i}`);
|
||||
} else if (int32Params[i]) {
|
||||
params.push(`i32 %p${i}`);
|
||||
entryArgs.push(`i32 %p${i}`);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5343-integer-recursion-array-index.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -13,12 +13,14 @@ function spin(n: number): number {
|
||||
return spin(n + 0) + 1;
|
||||
}
|
||||
|
||||
// An accumulating chain; for negative input it outruns the loop's limit and
|
||||
// reruns as ordinary recursion, which overflows the stack.
|
||||
// An accumulating chain: deep or endless input overflows the stack even
|
||||
// though the recursion runs as a loop.
|
||||
function count(n: number): number {
|
||||
return n === 0 ? 0 : 1 + count(n - 1);
|
||||
}
|
||||
|
||||
// Plain mutual tail calls (kept within Node's stack depth: compiled code
|
||||
// runs them as a loop, so an endless chain would not overflow).
|
||||
function isEven(n: number): boolean {
|
||||
return n === 0 ? true : isOdd(n - 1);
|
||||
}
|
||||
@@ -63,10 +65,11 @@ attempt("down 10 after overflow", () => down(10));
|
||||
attempt("spin", () => spin(2));
|
||||
attempt("count 3000", () => count(3000));
|
||||
attempt("count -1", () => count(-1));
|
||||
attempt("count 1e7", () => count(1e7));
|
||||
attempt("count 5 after overflow", () => count(5));
|
||||
attempt("isEven 7", () => isEven(7));
|
||||
attempt("isOdd 7", () => isOdd(7));
|
||||
attempt("isEven 1001", () => isEven(1001));
|
||||
attempt("isEven -1", () => isEven(-1));
|
||||
for (const n of [0, 1, 5, 20, 40, 60, 61, 62, 100]) attempt(`ping ${n}`, () => ping(n));
|
||||
for (const n of [0, 1, 2, 3, 4, 5, 6, 7]) attempt(`alternate ${n}`, () => alternate(true, n));
|
||||
attempt("ack 2 3", () => ack(2, 3));
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Pure number recursion that walks an array by index, and multi-argument
|
||||
// recursion (binomial coefficients).
|
||||
|
||||
function sumFrom(a: number[], i: number): number {
|
||||
return i >= a.length ? 0 : a[i]! + sumFrom(a, i + 1);
|
||||
}
|
||||
|
||||
function maxFrom(a: number[], i: number, best: number): number {
|
||||
if (i >= a.length) return best;
|
||||
const v = a[i]!;
|
||||
return maxFrom(a, i + 1, v > best ? v : best);
|
||||
}
|
||||
|
||||
function countAbove(a: number[], i: number, limit: number): number {
|
||||
if (i === a.length) return 0;
|
||||
return (a[i]! > limit ? 1 : 0) + countAbove(a, i + 1, limit);
|
||||
}
|
||||
|
||||
// Divide and conquer over an index range.
|
||||
function rangeSum(a: number[], lo: number, hi: number): number {
|
||||
if (hi - lo <= 1) return lo < hi ? a[lo]! : 0;
|
||||
const mid = (lo + hi) >> 1;
|
||||
return rangeSum(a, lo, mid) + rangeSum(a, mid, hi);
|
||||
}
|
||||
|
||||
function binomial(n: number, k: number): number {
|
||||
if (k === 0 || k === n) return 1;
|
||||
return binomial(n - 1, k - 1) + binomial(n - 1, k);
|
||||
}
|
||||
|
||||
const ints: number[] = [];
|
||||
for (let i = 0; i < 2000; i++) ints.push(((i * 7919) % 1000) - 500);
|
||||
console.log(sumFrom(ints, 0), maxFrom(ints, 0, -1e9), countAbove(ints, 0, 250), rangeSum(ints, 0, ints.length));
|
||||
|
||||
// Elements that are not int32: fractions, -0, NaN and huge values.
|
||||
const mixed = [1, 2.5, 3, -0, 4];
|
||||
console.log(sumFrom(mixed, 0), Object.is(sumFrom([-0], 0), 0), rangeSum(mixed, 0, 5));
|
||||
console.log(sumFrom([1, NaN, 2], 0), maxFrom([3, NaN, 9], 0, 0), sumFrom([2 ** 31, 1], 0));
|
||||
const big = [2147483647, 2147483647, 5];
|
||||
console.log(sumFrom(big, 0), rangeSum(big, 0, 3), countAbove(big, 0, 6));
|
||||
const precise = [2 ** 53, 1, 1];
|
||||
console.log(sumFrom(precise, 0), rangeSum(precise, 0, 3));
|
||||
// A hole-free array whose prefix grew by pushes and shrank by pops.
|
||||
const grown: number[] = [];
|
||||
for (let i = 0; i < 50; i++) grown.push(i * i);
|
||||
grown.pop();
|
||||
console.log(sumFrom(grown, 0), rangeSum(grown, 10, 40), sumFrom(grown, 49));
|
||||
console.log(sumFrom([], 0), maxFrom([], 0, 7));
|
||||
|
||||
for (const [n, k] of [
|
||||
[5, 2],
|
||||
[10, 5],
|
||||
[20, 10],
|
||||
[24, 12],
|
||||
]) {
|
||||
console.log("binomial", n, k, binomial(n, k));
|
||||
}
|
||||
console.log(binomial(4.5, 0), binomial(6, 6), binomial(-1, -1));
|
||||
try {
|
||||
console.log(binomial(3, 5));
|
||||
} catch (e) {
|
||||
console.log("binomial 3 5:", e instanceof RangeError, (e as Error).message);
|
||||
}
|
||||
Reference in New Issue
Block a user