mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-23 06:10:02 +08:00
Native executables used to terminate with a signal when recursion exhausted the stack. Functions that can take part in unbounded recursion now compare the stack pointer against a per-stack guard on entry and throw Node's `RangeError: Maximum call stack size exceeded` through the ordinary exception path, so catch handlers, finally blocks, worker error events and the uncaught-error exit code behave as they do under Node. - The checked set is every function on a cycle of the call graph, where operations that can reenter compiled code indirectly (indirect calls, callbacks, dynamic and library operations) reach closure targets and class members. Non-recursive functions, including leaf callbacks, stay check-free, and the may-throw analysis accounts for the new throw. - The runtime installs the guard per context (main thread from the stack rlimit, workers from their thread attributes) and per async fiber, with a reserve for the throw, unwinding and error reporting. Worker threads now get an explicit 8 MiB stack. - Scalar call results pass through the unwind path as its dummy value, so calls in tail position keep tail-call and recursion elimination.
85 lines
2.4 KiB
TypeScript
85 lines
2.4 KiB
TypeScript
// Stack exhaustion through mutual recursion, closures, methods and
|
|
// callbacks throws the same catchable RangeError as direct recursion.
|
|
function isEven(n: number): boolean {
|
|
return n === 0 ? true : isOdd(n - 1);
|
|
}
|
|
function isOdd(n: number): boolean {
|
|
return n === 0 ? false : isEven(n - 1);
|
|
}
|
|
|
|
function ping(n: number): number {
|
|
return pong(n + 1) + 1;
|
|
}
|
|
function pong(n: number): number {
|
|
return ping(n + 1) + 1;
|
|
}
|
|
|
|
function report(label: string, run: () => unknown): void {
|
|
try {
|
|
console.log(label, "returned", run());
|
|
} catch (e) {
|
|
console.log(label, e instanceof RangeError, (e as Error).message);
|
|
}
|
|
}
|
|
|
|
report("mutual bounded", () => isEven(4000));
|
|
report("mutual unbounded", () => ping(0));
|
|
|
|
const countdown = (n: number): number => (n <= 0 ? 0 : countdown(n - 1) + 1);
|
|
report("closure bounded", () => countdown(3000));
|
|
let spiral: (n: number) => number = (n) => n;
|
|
spiral = (n) => spiral(n + 1) * 2;
|
|
report("closure unbounded", () => spiral(0));
|
|
|
|
function makeWalker(step: number): (n: number) => number {
|
|
const walk = (n: number): number => walk(n + step) + 1;
|
|
return walk;
|
|
}
|
|
report("captured unbounded", () => makeWalker(2)(0));
|
|
|
|
class Shape {
|
|
readonly sides: number;
|
|
constructor(sides: number) {
|
|
this.sides = sides;
|
|
}
|
|
grow(n: number): number {
|
|
return n === 0 ? this.sides : this.grow(n - 1) + 1;
|
|
}
|
|
forever(n: number): number {
|
|
return this.forever(n + 1) + this.sides;
|
|
}
|
|
}
|
|
class Square extends Shape {
|
|
constructor() {
|
|
super(4);
|
|
}
|
|
forever(n: number): number {
|
|
return super.forever(n + 1) + 1;
|
|
}
|
|
}
|
|
const square = new Square();
|
|
report("method bounded", () => square.grow(2500));
|
|
report("method unbounded", () => square.forever(0));
|
|
|
|
interface Tree {
|
|
value: number;
|
|
children: Tree[];
|
|
}
|
|
function chain(depth: number): Tree {
|
|
let node: Tree = { value: 0, children: [] };
|
|
for (let i = 1; i <= depth; i++) node = { value: i, children: [node] };
|
|
return node;
|
|
}
|
|
function total(tree: Tree): number {
|
|
return tree.value + tree.children.map(total).reduce((a, b) => a + b, 0);
|
|
}
|
|
report("callback bounded", () => total(chain(1500)));
|
|
function spin(values: number[]): number {
|
|
return values.map(() => spin(values)).length;
|
|
}
|
|
report("callback unbounded", () => spin([1]));
|
|
|
|
const looping: { next: () => number } = { next: () => looping.next() + 1 };
|
|
report("object member unbounded", () => looping.next());
|
|
console.log("done", isOdd(3001), countdown(10));
|