1
12
13import { tokenize, type Token } from "./lexer.js";
14import { FormulaError, type AstNode, MAX_NESTING } from "./types.js";
15import { FUNCTION_NAMES } from "./functions.js";
16
17class Parser {
18 private pos = 0;
19 private depth = 0;
20 private readonly tokens: Token[];
21
22 constructor(tokens: Token[]) {
23 this.tokens = tokens;
24 }
25
26 private peek(): Token {
27 return this.tokens[this.pos]!;
28 }
29 private next(): Token {
30 const t = this.tokens[this.pos]!;
31 this.pos++;
32 return t;
33 }
34 private isOp(value: string): boolean {
35 const t = this.peek();
36 return t.kind === "op" && t.value === value;
37 }
38 private eat(value: string): void {
39 const t = this.peek();
40 if (t.kind !== "op" || t.value !== value)
41 throw new FormulaError("PARSE", `expected "${value}"`, t.pos);
42 this.next();
43 }
44
45 parse(): AstNode {
46 const node = this.parseTernary(0);
47 const t = this.peek();
48 if (t.kind !== "eof")
49 throw new FormulaError("PARSE", `unexpected token at position ${t.pos}`, t.pos);
50 return node;
51 }
52
53 private checkDepth(): void {
54 this.depth++;
55 if (this.depth > MAX_NESTING)
56 throw new FormulaError("PARSE", `nesting depth exceeds ${MAX_NESTING}`, this.peek().pos);
57 }
58
59
60 private parseExpr(minBp: number): AstNode {
61 let left = this.parsePrefix();
62 for (;;) {
63 const t = this.peek();
64 if (t.kind !== "op") break;
65 const bp = BINARY_PRECEDENCE[t.value];
66 if (bp === undefined || bp[0] < minBp) break;
67 this.next();
68 const right = this.parseExpr(bp[1]);
69 left = { type: "BinaryOp", op: t.value, left, right };
70 }
71 return left;
72 }
73
74 private parsePrefix(): AstNode {
75 const t = this.next();
76 if (t.kind === "number") return { type: "Literal", value: t.value, literalType: "number" };
77 if (t.kind === "string") return { type: "Literal", value: t.value, literalType: "string" };
78 if (t.kind === "date") return { type: "Literal", value: t.value, literalType: "date" };
79 if (t.kind === "bool") return { type: "Literal", value: t.value, literalType: "bool" };
80 if (t.kind === "null") return { type: "NullLiteral" };
81 if (t.kind === "op" && t.value === "[") {
82 this.checkDepth();
83 const elements: AstNode[] = [];
84 if (!this.isOp("]")) {
85 for (;;) {
86 elements.push(this.parseTernary(0));
87 if (this.isOp(",")) {
88 this.next();
89 continue;
90 }
91 break;
92 }
93 }
94 this.depth--;
95 this.eat("]");
96 return { type: "ArrayLiteral", elements };
97 }
98 if (t.kind === "op" && t.value === "-") {
99 this.checkDepth();
100 const operand = this.parseExpr(UNARY_BP);
101 return { type: "UnaryOp", op: "-", operand };
102 }
103 if (t.kind === "op" && t.value === "not") {
104 this.checkDepth();
105 const operand = this.parseExpr(UNARY_BP);
106 return { type: "UnaryOp", op: "not", operand };
107 }
108 if (t.kind === "op" && t.value === "(") {
109 this.checkDepth();
110 const inner = this.parseTernary(0);
111 this.depth--;
112 this.eat(")");
113 return inner;
114 }
115 if (t.kind === "ident") {
116
117 if (t.value === "if" && this.isOp("(")) {
118 this.next();
119 this.checkDepth();
120 const cond = this.parseTernary(0);
121 this.eat(",");
122 const then = this.parseTernary(0);
123 this.eat(",");
124 const elseBranch = this.parseTernary(0);
125 this.depth--;
126 this.eat(")");
127 return { type: "If", cond, then, else: elseBranch };
128 }
129
130 if (this.isOp("(")) {
131 this.next();
132 this.checkDepth();
133 const args: AstNode[] = [];
134 if (!this.isOp(")")) {
135 for (;;) {
136 args.push(this.parseTernary(0));
137 if (this.isOp(",")) {
138 this.next();
139 continue;
140 }
141 break;
142 }
143 }
144 this.depth--;
145 this.eat(")");
146 if (!FUNCTION_NAMES.has(t.value)) {
147 throw new FormulaError("UNKNOWN_FUNCTION", `unknown function "${t.value}"`, t.pos);
148 }
149 return { type: "Call", name: t.value, args };
150 }
151 throw new FormulaError(
152 "PARSE",
153 `unexpected identifier "${t.value}" (use prop("${t.value}"))`,
154 t.pos,
155 );
156 }
157 throw new FormulaError("PARSE", "unexpected token", t.pos);
158 }
159
160
161 private parseTernary(minBp: number): AstNode {
162 const cond = this.parseExpr(minBp);
163 if (this.isOp("?")) {
164 this.next();
165 const then = this.parseTernary(0);
166 this.eat(":");
167 const elseBranch = this.parseTernary(TERNARY_RBP);
168 this.checkDepth();
169 return { type: "If", cond, then, else: elseBranch };
170 }
171 return cond;
172 }
173}
174
175
176const UNARY_BP = 70;
177const TERNARY_RBP = 10;
178const BINARY_PRECEDENCE: Record<string, [number, number]> = {
179 or: [10, 11],
180 and: [20, 21],
181 "==": [30, 31],
182 "!=": [30, 31],
183 "<": [30, 31],
184 "<=": [30, 31],
185 ">": [30, 31],
186 ">=": [30, 31],
187 "+": [40, 41],
188 "-": [40, 41],
189 "*": [50, 51],
190 "/": [50, 51],
191 "%": [50, 51],
192};
193
194export function parseAst(tokens: Token[]): AstNode {
195 return new Parser(tokens).parse();
196}
197
198export function parseFormula(src: string): AstNode {
199 return parseAst(tokenize(src));
200}
201