File size: 2,276 Bytes
f0634fb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | // src/budget.ts
//
// Parse budget: a hard cap on wall-clock time and on the number of syntax
// nodes a single parse may create. The parser calls `budget.tick()` every
// time it creates a node; long scan loops (word runs, strings, heredoc
// bodies) call `budget.progress()` periodically so a pathological single
// token still hits the deadline without inflating the node count. When
// either limit is exceeded the methods throw `Aborted`, which the `parse`
// entry point catches and reports as `{ ok: false, reason: 'aborted' }`.
export const DEFAULT_TIMEOUT_MS = 50;
export const DEFAULT_MAX_NODES = 50_000;
/** Internal control-flow error. Never escapes the `parse` entry point. */
export class Aborted extends Error {
constructor(message: string) {
super(message);
this.name = 'Aborted';
}
}
export interface BudgetOptions {
/** Wall-clock limit in milliseconds. `Infinity` disables the time check. */
timeoutMs?: number;
/** Maximum number of nodes the parse may create. */
maxNodes?: number;
}
export class ParseBudget {
private readonly deadline: number;
private readonly maxNodes: number;
private nodeCount = 0;
constructor(options: BudgetOptions = {}) {
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
this.deadline = Date.now() + timeoutMs;
this.maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
}
/** Number of nodes created so far. */
get nodesUsed(): number {
return this.nodeCount;
}
/**
* Account for one created node and re-check the deadline. Throws `Aborted`
* when the node cap is exceeded or the deadline has been reached.
*/
tick(): void {
this.nodeCount++;
if (this.nodeCount > this.maxNodes) {
throw new Aborted(`parse aborted: node budget exceeded (${this.nodeCount} > ${this.maxNodes})`);
}
if (Date.now() >= this.deadline) {
throw new Aborted(`parse aborted: timeout`);
}
}
/**
* Re-check the deadline WITHOUT counting a node. For long scan loops that
* run many iterations per produced node (character-level scanning), called
* at intervals so the deadline is still enforced promptly.
*/
progress(): void {
if (Date.now() >= this.deadline) {
throw new Aborted(`parse aborted: timeout`);
}
}
}
|