From 741ea16e40653628f6562158d53766141dee386c Mon Sep 17 00:00:00 2001 From: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:02:09 +0300 Subject: [PATCH] keep evaluateDocument's error boundary around tokenize/parse, not just evaluate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tokenize() and parseLine() ran outside the try/catch, and the parser has no recursion-depth guard for nested parens. A pathologically nested line overflowed the stack with an uncaught RangeError, killing the whole document instead of nulling just that line — the exact thing evaluateDocument is supposed to prevent. --- src/engine/edge.test.ts | 10 +++++++ src/engine/index.ts | 61 +++++++++++++++++++++-------------------- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/engine/edge.test.ts b/src/engine/edge.test.ts index a559c5e..f97dd07 100644 --- a/src/engine/edge.test.ts +++ b/src/engine/edge.test.ts @@ -282,3 +282,13 @@ describe("review fixes", () => { }); }); }); + +describe("regression 2026-08-12", () => { + it("a deeply nested-parens line nulls out just that line, not the whole document", () => { + const deep = "(".repeat(50000) + "1" + ")".repeat(50000); + const r = eng.evaluateDocument(`1+1\n${deep}\n2+2`).map((x) => x.text); + expect(r[0]).toBe("2"); + expect(r[1]).toBeNull(); + expect(r[2]).toBe("4"); + }); +}); diff --git a/src/engine/index.ts b/src/engine/index.ts index 2cc981e..e507dd9 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -3,7 +3,7 @@ import { DateOrder, Decimal, EngineSettings, EvalError, Quantity, Unit, Value, XRefError, defaultSettings, qty } from "./types"; import { Registry, buildRegistry, Completion } from "./registry"; import { tokenize, Token } from "./tokenizer"; -import { parseLine } from "./parser"; +import { parseLine, ParsedLine } from "./parser"; import { evaluate, EvalCtx, XRefResolver, toBase, fromBase } from "./evaluator"; import { formatValue } from "./formatter"; import { detectDateOrder } from "./datetime"; @@ -118,39 +118,42 @@ export class SumEngine { continue; } - const tokens = tokenize(line, this.reg, this.dateOrder); - const knownVars = new Set(vars.keys()); - const parsed = parseLine(tokens, knownVars, line); - + // tokenize/parseLine/evaluate all run under one try/catch: pathological + // input (e.g. stack-overflowing nested parens in the parser) must null + // out this one line, not throw past evaluateDocument and kill the rest. + let tokens: Token[] = []; + let parsed: ParsedLine = { expr: null }; let value: Value | null = null; let lineError: string | undefined; - if (parsed.expr) { - const ctx: EvalCtx = { - reg: this.reg, - vars, - line: { lineValues, lineKinds, index: i, lineText: rawLine }, - historicalRates: this.historicalRates, - resolveXRef, - }; - try { - value = evaluate(parsed.expr, ctx); + try { + tokens = tokenize(line, this.reg, this.dateOrder); + const knownVars = new Set(vars.keys()); + parsed = parseLine(tokens, knownVars, line); + if (parsed.expr) { + const ctx: EvalCtx = { + reg: this.reg, + vars, + line: { lineValues, lineKinds, index: i, lineText: rawLine }, + historicalRates: this.historicalRates, + resolveXRef, + }; // a line without a result beats a dead sheet. - // surface unexpected bugs so the user knows why it failed - } catch (e) { - if (e instanceof XRefError) { - lineError = e.message; - } else if (e instanceof EvalError) { - // Expected evaluation failure; remain silent - } else { - // Surface unexpected bugs/BigInt errors and log them - lineError = e instanceof Error ? e.message : String(e); - console.warn("evaluate failed:", e); - } - value = null; + value = evaluate(parsed.expr, ctx); } - if (value?.kind === "quantity" && !value.value.isFinite()) value = null; - if (value && parsed.assign) vars.set(parsed.assign, value); + } catch (e) { + if (e instanceof XRefError) { + lineError = e.message; + } else if (e instanceof EvalError) { + // Expected evaluation failure; remain silent + } else { + // Surface unexpected bugs/BigInt errors/RangeErrors and log them + lineError = e instanceof Error ? e.message : String(e); + console.warn("evaluate failed:", e); + } + value = null; } + if (value?.kind === "quantity" && !value.value.isFinite()) value = null; + if (value && parsed.assign) vars.set(parsed.assign, value); lineValues.push(value); lineKinds.push("normal");