mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 01:02:01 +03:00
🧹 Refactor deeply nested parser methods to a flat Pratt parser
This patch addresses a code health issue where the Parser class relied on deep, chained recursive-descent methods (parseConv -> parseBit -> parseAdd -> parseMul -> parseUnary -> parsePostfix -> parsePrimary) to handle precedence. These methods have been removed and combined into a single, unified loop-based Pratt parser (parseExpr). This improves maintainability, consolidates precedence logic into a centralized constant mapping (PREC), and maintains all current mathematical and natural language parsing behavior flawlessly. Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
parent
c958f30662
commit
79b56dba66
1 changed files with 149 additions and 162 deletions
|
|
@ -97,6 +97,17 @@ export function parseLine(tokens: Token[], knownVars: Set<string>, line: string)
|
||||||
return { assign, expr: p.parseSeq() };
|
return { assign, expr: p.parseSeq() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const PREC = {
|
||||||
|
CONV: 10,
|
||||||
|
BIT: 20,
|
||||||
|
ADD: 30,
|
||||||
|
MUL: 40,
|
||||||
|
UNARY: 50,
|
||||||
|
POW: 55,
|
||||||
|
POSTFIX: 60,
|
||||||
|
};
|
||||||
|
|
||||||
class Parser {
|
class Parser {
|
||||||
private i = 0;
|
private i = 0;
|
||||||
constructor(private toks: Token[], private vars: Set<string>) {}
|
constructor(private toks: Token[], private vars: Set<string>) {}
|
||||||
|
|
@ -130,7 +141,7 @@ class Parser {
|
||||||
}
|
}
|
||||||
if (tk.t === "pctop" && (tk.op.startsWith("as_pct") || tk.op.endsWith("what_is")) && items.length > 0) {
|
if (tk.t === "pctop" && (tk.op.startsWith("as_pct") || tk.op.endsWith("what_is")) && items.length > 0) {
|
||||||
this.i++;
|
this.i++;
|
||||||
const r = this.parseAdd();
|
const r = this.parseExpr(PREC.ADD);
|
||||||
if (r) {
|
if (r) {
|
||||||
items = [{ k: "pctop", op: tk.op, l: mk(), r }];
|
items = [{ k: "pctop", op: tk.op, l: mk(), r }];
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -141,7 +152,7 @@ class Parser {
|
||||||
if (tk.t === "op" && items.length > 0) {
|
if (tk.t === "op" && items.length > 0) {
|
||||||
const save = this.i;
|
const save = this.i;
|
||||||
this.i++;
|
this.i++;
|
||||||
const r = tk.op === "plus" || tk.op === "minus" ? this.parseMul() : this.parseUnary();
|
const r = tk.op === "plus" || tk.op === "minus" ? this.parseExpr(PREC.MUL) : this.parseExpr(PREC.UNARY);
|
||||||
if (r) {
|
if (r) {
|
||||||
items = [{ k: "bin", op: tk.op, l: mk(), r }];
|
items = [{ k: "bin", op: tk.op, l: mk(), r }];
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -152,7 +163,7 @@ class Parser {
|
||||||
if (tk.t === "bitop" && items.length > 0) {
|
if (tk.t === "bitop" && items.length > 0) {
|
||||||
const save = this.i;
|
const save = this.i;
|
||||||
this.i++;
|
this.i++;
|
||||||
const r = this.parseAdd();
|
const r = this.parseExpr(PREC.ADD);
|
||||||
if (r) {
|
if (r) {
|
||||||
items = [{ k: "bit", op: tk.op, l: mk(), r }];
|
items = [{ k: "bit", op: tk.op, l: mk(), r }];
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -161,7 +172,7 @@ class Parser {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const before = this.i;
|
const before = this.i;
|
||||||
const node = this.parseBit();
|
const node = this.parseExpr(PREC.BIT);
|
||||||
if (node) items.push(node);
|
if (node) items.push(node);
|
||||||
if (this.i === before) this.i++; // skip unparsable token
|
if (this.i === before) this.i++; // skip unparsable token
|
||||||
}
|
}
|
||||||
|
|
@ -169,52 +180,144 @@ class Parser {
|
||||||
return mk();
|
return mk();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** bitwise level (one flat precedence, left-assoc): & | xor << >> */
|
/**
|
||||||
private parseBit(): Node | null {
|
* Pratt parser implementation for expressions.
|
||||||
let l = this.parseAdd();
|
*/
|
||||||
if (!l) return null;
|
private parseExpr(minPrec: number = 0): Node | null {
|
||||||
for (;;) {
|
let tk = this.peek();
|
||||||
const tk = this.peek();
|
if (!tk) return null;
|
||||||
if (tk?.t !== "bitop") break;
|
|
||||||
const save = this.i;
|
let lhs: Node | null = null;
|
||||||
this.i++;
|
|
||||||
const r = this.parseAdd();
|
if (tk.t === "op" && tk.op === "minus") {
|
||||||
if (!r) {
|
this.i++;
|
||||||
this.i = save;
|
const x = this.parseExpr(PREC.UNARY);
|
||||||
break;
|
if (!x) return null;
|
||||||
}
|
lhs = { k: "neg", x };
|
||||||
l = { k: "bit", op: tk.op, l, r };
|
} else if (tk.t === "op" && tk.op === "plus") {
|
||||||
}
|
this.i++;
|
||||||
return l;
|
lhs = this.parseExpr(PREC.UNARY);
|
||||||
}
|
if (!lhs) return null;
|
||||||
|
} else {
|
||||||
|
lhs = this.parsePrimary();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lhs) return null;
|
||||||
|
|
||||||
/** conversion level: `x in cm`, `x as a % of y`, `x of what is y` */
|
|
||||||
private parseConv(): Node | null {
|
|
||||||
let l = this.parseBit();
|
|
||||||
if (!l) return null;
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const tk = this.peek();
|
tk = this.peek();
|
||||||
if (tk?.t === "conv") {
|
if (!tk) break;
|
||||||
|
|
||||||
|
if (tk.t === "percent") {
|
||||||
|
if (PREC.POSTFIX < minPrec) break;
|
||||||
|
this.i++;
|
||||||
|
lhs = { k: "pct", x: lhs };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tk.t === "bang") {
|
||||||
|
if (PREC.POSTFIX < minPrec) break;
|
||||||
|
this.i++;
|
||||||
|
lhs = { k: "fact", x: lhs };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tk.t === "unit") {
|
||||||
|
if (PREC.POSTFIX < minPrec) break;
|
||||||
|
this.i++;
|
||||||
|
lhs = { k: "unit", x: lhs, unit: tk.unit };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tk.t === "currency") {
|
||||||
|
if (PREC.POSTFIX < minPrec) break;
|
||||||
|
this.i++;
|
||||||
|
lhs = { k: "curr", x: lhs, code: tk.code };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tk.t === "scale") {
|
||||||
|
if (PREC.POSTFIX < minPrec) break;
|
||||||
|
this.i++;
|
||||||
|
lhs = { k: "scale", x: lhs, mult: tk.mult, label: "" };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tk.t === "op" && tk.op === "pow") {
|
||||||
|
if (PREC.POW < minPrec) break;
|
||||||
|
const save = this.i;
|
||||||
|
this.i++;
|
||||||
|
const r = this.parseExpr(PREC.POW);
|
||||||
|
if (!r) { this.i = save; break; }
|
||||||
|
lhs = { k: "bin", op: "pow", l: lhs, r };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tk.t === "op" && (tk.op === "mul" || tk.op === "div" || tk.op === "mod")) {
|
||||||
|
if (PREC.MUL < minPrec) break;
|
||||||
|
const save = this.i;
|
||||||
|
this.i++;
|
||||||
|
const r = this.parseExpr(PREC.MUL + 1);
|
||||||
|
if (!r) { this.i = save; break; }
|
||||||
|
lhs = { k: "bin", op: tk.op, l: lhs, r };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tk.t === "pctop" && (tk.op === "of" || tk.op === "off" || tk.op === "on")) {
|
||||||
|
if (!(lhs.k === "pct" || lhs.k === "var" || lhs.k === "pctop")) {
|
||||||
|
this.i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (PREC.MUL < minPrec) break;
|
||||||
|
const save = this.i;
|
||||||
|
this.i++;
|
||||||
|
const r = this.parseExpr(PREC.MUL + 1);
|
||||||
|
if (!r) { this.i = save; break; }
|
||||||
|
lhs = { k: "pctop", op: tk.op, l: lhs, r };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tk.t === "lparen" || tk.t === "const" || tk.t === "func" || (tk.t === "word" && this.vars.has(tk.raw))) {
|
||||||
|
if (PREC.MUL < minPrec) break;
|
||||||
|
const save = this.i;
|
||||||
|
const r = this.parseExpr(PREC.MUL + 1);
|
||||||
|
if (!r) { this.i = save; break; }
|
||||||
|
lhs = { k: "bin", op: "mul", l: lhs, r };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tk.t === "op" && (tk.op === "plus" || tk.op === "minus")) {
|
||||||
|
if (PREC.ADD < minPrec) break;
|
||||||
|
const save = this.i;
|
||||||
|
this.i++;
|
||||||
|
const r = this.parseExpr(PREC.ADD + 1);
|
||||||
|
if (!r) { this.i = save; break; }
|
||||||
|
lhs = { k: "bin", op: tk.op, l: lhs, r };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tk.t === "bitop") {
|
||||||
|
if (PREC.BIT < minPrec) break;
|
||||||
|
const save = this.i;
|
||||||
|
this.i++;
|
||||||
|
const r = this.parseExpr(PREC.BIT + 1);
|
||||||
|
if (!r) { this.i = save; break; }
|
||||||
|
lhs = { k: "bit", op: tk.op, l: lhs, r };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tk.t === "conv") {
|
||||||
|
if (PREC.CONV < minPrec) break;
|
||||||
const save = this.i;
|
const save = this.i;
|
||||||
this.i++;
|
this.i++;
|
||||||
const target = this.parseTarget();
|
const target = this.parseTarget();
|
||||||
if (!target) {
|
if (!target) { this.i = save; break; }
|
||||||
this.i = save;
|
lhs = { k: "conv", x: lhs, target: this.tryHistoricalDate(target) };
|
||||||
break;
|
continue;
|
||||||
}
|
}
|
||||||
l = { k: "conv", x: l, target: this.tryHistoricalDate(target) };
|
if (tk.t === "pctop" && (tk.op.startsWith("as_pct") || tk.op.endsWith("what_is"))) {
|
||||||
} else if (tk?.t === "pctop" && (tk.op.startsWith("as_pct") || tk.op.endsWith("what_is"))) {
|
if (PREC.CONV < minPrec) break;
|
||||||
this.i++;
|
const save = this.i;
|
||||||
const r = this.parseAdd();
|
this.i++;
|
||||||
if (!r) break;
|
const r = this.parseExpr(PREC.ADD);
|
||||||
l = { k: "pctop", op: tk.op, l, r };
|
if (!r) { this.i = save; break; }
|
||||||
} else {
|
lhs = { k: "pctop", op: tk.op, l: lhs, r };
|
||||||
break;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return lhs;
|
||||||
|
}
|
||||||
/** If the current token is `pctop(on)` + `datelit`, consume them and attach `onDate`. */
|
/** If the current token is `pctop(on)` + `datelit`, consume them and attach `onDate`. */
|
||||||
private tryHistoricalDate(target: ConvTarget): ConvTarget {
|
private tryHistoricalDate(target: ConvTarget): ConvTarget {
|
||||||
if (target.type !== "currency") return target;
|
if (target.type !== "currency") return target;
|
||||||
|
|
@ -280,122 +383,6 @@ class Parser {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseAdd(): Node | null {
|
|
||||||
let l = this.parseMul();
|
|
||||||
if (!l) return null;
|
|
||||||
for (;;) {
|
|
||||||
const tk = this.peek();
|
|
||||||
if (tk?.t === "op" && (tk.op === "plus" || tk.op === "minus")) {
|
|
||||||
const save = this.i;
|
|
||||||
this.i++;
|
|
||||||
const r = this.parseMul();
|
|
||||||
if (!r) {
|
|
||||||
this.i = save;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
l = { k: "bin", op: tk.op, l, r };
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseMul(): Node | null {
|
|
||||||
let l = this.parseUnary();
|
|
||||||
if (!l) return null;
|
|
||||||
for (;;) {
|
|
||||||
const tk = this.peek();
|
|
||||||
if (tk?.t === "op" && (tk.op === "mul" || tk.op === "div" || tk.op === "mod")) {
|
|
||||||
const save = this.i;
|
|
||||||
this.i++;
|
|
||||||
const r = this.parseUnary();
|
|
||||||
if (!r) {
|
|
||||||
this.i = save;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
l = { k: "bin", op: tk.op, l, r };
|
|
||||||
} else if (tk?.t === "pctop" && (tk.op === "of" || tk.op === "off" || tk.op === "on")) {
|
|
||||||
// of/off/on only make sense with a percent on the left ("20% of x");
|
|
||||||
// otherwise the word is prose noise ("5 on coffee")
|
|
||||||
if (!(l.k === "pct" || l.k === "var" || l.k === "pctop")) {
|
|
||||||
this.i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const save = this.i;
|
|
||||||
this.i++;
|
|
||||||
const r = this.parseUnary();
|
|
||||||
if (!r) {
|
|
||||||
this.i = save;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
l = { k: "pctop", op: tk.op, l, r };
|
|
||||||
} else if (
|
|
||||||
tk &&
|
|
||||||
(tk.t === "lparen" || tk.t === "const" || tk.t === "func" || (tk.t === "word" && this.vars.has(tk.raw)))
|
|
||||||
) {
|
|
||||||
// implicit multiplication: 2(3+4), 2pi, 2x
|
|
||||||
const save = this.i;
|
|
||||||
const r = this.parseUnary();
|
|
||||||
if (!r) {
|
|
||||||
this.i = save;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
l = { k: "bin", op: "mul", l, r };
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseUnary(): Node | null {
|
|
||||||
const tk = this.peek();
|
|
||||||
if (tk?.t === "op" && tk.op === "minus") {
|
|
||||||
this.i++;
|
|
||||||
const x = this.parseUnary();
|
|
||||||
return x ? { k: "neg", x } : null;
|
|
||||||
}
|
|
||||||
if (tk?.t === "op" && tk.op === "plus") {
|
|
||||||
this.i++;
|
|
||||||
return this.parseUnary();
|
|
||||||
}
|
|
||||||
return this.parsePostfix();
|
|
||||||
}
|
|
||||||
|
|
||||||
private parsePostfix(): Node | null {
|
|
||||||
let x = this.parsePrimary();
|
|
||||||
if (!x) return null;
|
|
||||||
for (;;) {
|
|
||||||
const tk = this.peek();
|
|
||||||
if (!tk) break;
|
|
||||||
if (tk.t === "percent") {
|
|
||||||
this.i++;
|
|
||||||
x = { k: "pct", x };
|
|
||||||
} else if (tk.t === "bang") {
|
|
||||||
this.i++;
|
|
||||||
x = { k: "fact", x };
|
|
||||||
} else if (tk.t === "unit") {
|
|
||||||
this.i++;
|
|
||||||
x = { k: "unit", x, unit: tk.unit };
|
|
||||||
} else if (tk.t === "currency") {
|
|
||||||
this.i++;
|
|
||||||
x = { k: "curr", x, code: tk.code };
|
|
||||||
} else if (tk.t === "scale") {
|
|
||||||
this.i++;
|
|
||||||
x = { k: "scale", x, mult: tk.mult, label: "" };
|
|
||||||
} else if (tk.t === "op" && tk.op === "pow") {
|
|
||||||
this.i++;
|
|
||||||
const r = this.parseUnary();
|
|
||||||
if (!r) break;
|
|
||||||
x = { k: "bin", op: "pow", l: x, r };
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
private parsePrimary(): Node | null {
|
private parsePrimary(): Node | null {
|
||||||
const tk = this.peek();
|
const tk = this.peek();
|
||||||
if (!tk) return null;
|
if (!tk) return null;
|
||||||
|
|
@ -431,7 +418,7 @@ class Parser {
|
||||||
this.i++;
|
this.i++;
|
||||||
const args: Node[] = [];
|
const args: Node[] = [];
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const arg = this.parseConv();
|
const arg = this.parseExpr(PREC.CONV);
|
||||||
if (arg) args.push(arg);
|
if (arg) args.push(arg);
|
||||||
const nx = this.peek();
|
const nx = this.peek();
|
||||||
if (nx?.t === "semicolon") {
|
if (nx?.t === "semicolon") {
|
||||||
|
|
@ -452,12 +439,12 @@ class Parser {
|
||||||
// `sqrt 16` / `square root of 16` — the "of" is decorative
|
// `sqrt 16` / `square root of 16` — the "of" is decorative
|
||||||
const nx = this.peek();
|
const nx = this.peek();
|
||||||
if (nx?.t === "pctop" && nx.op === "of") this.i++;
|
if (nx?.t === "pctop" && nx.op === "of") this.i++;
|
||||||
const arg = this.parseUnary();
|
const arg = this.parseExpr(PREC.UNARY);
|
||||||
return arg ? { k: "call", name, args: [arg] } : null;
|
return arg ? { k: "call", name, args: [arg] } : null;
|
||||||
}
|
}
|
||||||
case "lparen": {
|
case "lparen": {
|
||||||
this.i++;
|
this.i++;
|
||||||
const inner = this.parseConv();
|
const inner = this.parseExpr(PREC.CONV);
|
||||||
if (this.peek()?.t === "rparen") this.i++;
|
if (this.peek()?.t === "rparen") this.i++;
|
||||||
return inner;
|
return inner;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue