/** * Expr-lang recursive descent parser * Ported from Go's expr-lang parser with precedence climbing algorithm */ import { Lexer } from './lexer'; import { Token, TokenKind, ParseError, EOF_TOKEN, OPERATOR_PRECEDENCE, UNARY_OPERATORS, COMPARISON_OPERATORS } from './types'; export class ExprParser { private lexer: Lexer; private current: Token = { ...EOF_TOKEN }; private errors: ParseError[] = []; constructor() { this.lexer = new Lexer(); } /** * Parse an expr-lang expression and return any syntax errors. * Returns empty array if the expression is valid. */ parse(input: string): ParseError[] { this.errors = []; this.lexer.reset(input); this.advance(); if (input.trim().length === 0) { return []; } this.parseSequenceExpression(); // Check for unexpected tokens at the end if (this.curKind() !== TokenKind.EOF && this.errors.length === 0) { this.error(`unexpected token "${this.curVal()}"`); } return this.errors; } private advance(): void { this.current = this.lexer.next(); } private curKind(): TokenKind { return this.current.kind; } private curVal(): string { return this.current.value; } private expect(kind: TokenKind, value?: string): boolean { if (this.curKind() === kind && (value === undefined || this.curVal() === value)) { this.advance(); return true; } if (value) { this.error(`expected "${value}" but got "${this.curVal()}"`); } else { this.error(`unexpected token "${this.curVal()}"`); } return false; } private error(message: string): void { if (this.errors.length === 0) { // Calculate end position — approximate const endLine = this.current.line; const endColumn = this.current.column + this.curVal().length; this.errors.push({ message, startLine: this.current.line, startColumn: this.current.column, endLine, endColumn, }); } } private errorAt(token: Token, message: string): void { if (this.errors.length === 0) { const endColumn = token.column + token.value.length; this.errors.push({ message, startLine: token.line, startColumn: token.column, endLine: token.line, endColumn, }); } } // ========== Parsing Functions ========== /** * parseSequenceExpression parses multiple expressions separated by semicolons. */ private parseSequenceExpression(): void { if (this.errors.length > 0) return; this.parseExpression(0); while (this.curVal() === ';' && this.errors.length === 0) { this.advance(); if (this.curKind() === TokenKind.EOF) break; this.parseExpression(0); } } /** * parseExpression uses precedence climbing to parse binary expressions. */ private parseExpression(precedence: number): void { if (this.errors.length > 0) return; // Handle "let" at precedence 0 if (precedence === 0 && this.curVal() === 'let') { this.parseVariableDeclaration(); return; } // Handle "if" at precedence 0 if (precedence === 0 && this.curVal() === 'if') { this.parseConditionalIf(); return; } // Handle unary operators if (UNARY_OPERATORS.has(this.curVal()) && this.curKind() === TokenKind.Operator) { const unaryToken = this.current; this.advance(); if (this.curKind() === TokenKind.EOF) { this.errorAt(unaryToken, 'unexpected token EOF'); return; } if (this.errors.length > 0) return; this.parsePrimary(); if (this.errors.length > 0) return; this.parsePostfixExpression(); return; } this.parsePrimary(); if (this.errors.length > 0) return; this.parsePostfixExpression(); if (this.errors.length > 0) return; // Handle binary operators with precedence climbing while (this.curKind() === TokenKind.Operator && this.errors.length === 0) { const op = this.curVal(); const opToken = this.current; // Handle pipe operator | if (op === '|') { this.advance(); // skip | if (this.curKind() === TokenKind.EOF) { this.errorAt(opToken, 'unexpected token EOF'); return; } if (this.curKind() === TokenKind.Identifier) { this.advance(); // skip identifier this.parseArguments(); if (this.curVal() === ';') break; continue; } this.error(`expected identifier after pipe "|" but got "${this.curVal()}"`); return; } // Handle semicolon — end of this expression if (op === ';') break; // Handle "not" prefix for "not in", "not contains", etc. if (op === 'not') { this.advance(); const nextOp = this.curVal(); const negatedPrec = OPERATOR_PRECEDENCE[nextOp]; if (negatedPrec !== undefined && negatedPrec >= precedence) { this.advance(); this.parseExpression(negatedPrec + 1); continue; } this.error(`unexpected token "${nextOp}" after "not"`); return; } // Check precedence const opPrec = OPERATOR_PRECEDENCE[op]; if (opPrec === undefined || opPrec < precedence) break; // Handle chained comparisons: a < b < c => (a < b) && (b < c) if (COMPARISON_OPERATORS.has(op)) { this.advance(); if (this.curKind() === TokenKind.EOF) { this.errorAt(opToken, 'unexpected token EOF'); return; } this.parseExpression(opPrec + 1); // Keep parsing chained comparisons while (this.curKind() === TokenKind.Operator && COMPARISON_OPERATORS.has(this.curVal()) && this.errors.length === 0) { const chainOpToken = this.current; this.advance(); if (this.curKind() === TokenKind.EOF) { this.errorAt(chainOpToken, 'unexpected token EOF'); return; } this.parseExpression((OPERATOR_PRECEDENCE[this.curVal()] || 0) + 1); } continue; } this.advance(); if (this.curKind() === TokenKind.EOF) { this.errorAt(opToken, 'unexpected token EOF'); return; } // Right-associative operators bind to the right // For simplicity, parse with same precedence for right-assoc this.parseExpression(opPrec + 1); // Handle ternary ? : if (precedence === 0 && this.curVal() === '?') { this.parseConditional(); } } // Handle ternary ? : at precedence 0 if (precedence === 0 && this.curVal() === '?') { this.parseConditional(); } } /** * parsePrimary handles unary operators, parentheses, and the #/. pointer prefix. */ private parsePrimary(): void { if (this.errors.length > 0) return; // Unary operators are handled in parseExpression // Parenthesized expression if (this.curKind() === TokenKind.Bracket && this.curVal() === '(') { this.advance(); // skip ( this.parseSequenceExpression(); if (!this.expect(TokenKind.Bracket, ')')) { return; } return; } // Handle # or . prefix (in predicates) if ((this.curVal() === '#' || this.curVal() === '.') && this.curKind() === TokenKind.Operator) { this.advance(); if (this.curKind() === TokenKind.Identifier) { this.advance(); } this.parsePostfixExpression(); return; } this.parseSecondary(); } /** * parseSecondary handles identifiers, literals, arrays, and maps. */ private parseSecondary(): void { if (this.errors.length > 0) return; const token = this.current; switch (token.kind) { case TokenKind.Identifier: { this.advance(); // Check for function call if (this.curKind() === TokenKind.Bracket && this.curVal() === '(') { this.parseArguments(); } break; } case TokenKind.Number: { this.advance(); break; } case TokenKind.String: { this.advance(); break; } case TokenKind.Bracket: { if (token.value === '[') { this.parseArrayExpression(); } else if (token.value === '{') { this.parseMapExpression(); } else { this.error(`unexpected token "${token.value}"`); } break; } default: this.error(`unexpected token "${token.value}"`); break; } } /** * parsePostfixExpression handles .member, ?.member, [index], [from:to], and calls after the primary. */ private parsePostfixExpression(): void { while (this.errors.length === 0) { const token = this.current; // .member or ?.member if (token.kind === TokenKind.Operator && (token.value === '.' || token.value === '?.')) { this.advance(); // After . or ?., expect an identifier (or operator like "not" that can be a method name) if (this.curKind() === TokenKind.Identifier || this.curKind() === TokenKind.Operator) { this.advance(); // Check for method call: obj.method() if (this.curKind() === TokenKind.Bracket && this.curVal() === '(') { this.parseArguments(); } } else if (this.curKind() === TokenKind.Bracket && this.curVal() === '[' && token.value === '?.') { // obj?[index] — handle bracket after ?. this.advance(); this.parseExpression(0); this.expect(TokenKind.Bracket, ']'); } else { this.error(`expected property name after "${token.value}" but got "${this.curVal()}"`); return; } continue; } // [index] or [from:to] if (token.kind === TokenKind.Bracket && token.value === '[') { this.advance(); // Check for slice: [:] or [:to] if (this.curKind() === TokenKind.Operator && this.curVal() === ':') { this.advance(); if (this.curKind() !== TokenKind.Bracket || this.curVal() !== ']') { this.parseExpression(0); } this.expect(TokenKind.Bracket, ']'); } else { // Index expression this.parseExpression(0); // Check for slice: [from:] if (this.curKind() === TokenKind.Operator && this.curVal() === ':') { this.advance(); if (this.curKind() !== TokenKind.Bracket || this.curVal() !== ']') { this.parseExpression(0); } } this.expect(TokenKind.Bracket, ']'); } continue; } // Function call after member access if (token.kind === TokenKind.Bracket && token.value === '(') { this.parseArguments(); continue; } break; } } /** * parseVariableDeclaration parses "let name = value; rest" */ private parseVariableDeclaration(): void { this.expect(TokenKind.Operator, 'let'); if (this.errors.length > 0) return; if (this.curKind() !== TokenKind.Identifier) { this.error(`expected variable name after "let" but got "${this.curVal()}"`); return; } this.advance(); // skip variable name if (!this.expect(TokenKind.Operator, '=')) return; this.parseExpression(0); if (this.errors.length > 0) return; // Optional semicolon after value if (this.curVal() === ';') { this.advance(); if (this.curKind() !== TokenKind.EOF) { this.parseSequenceExpression(); } } } /** * parseConditionalIf parses "if expr { expr1 } else { expr2 }" or "if expr { expr1 } else if ..." */ private parseConditionalIf(): void { this.advance(); // skip 'if' if (this.errors.length > 0) return; this.parseExpression(0); if (this.errors.length > 0) return; if (!this.expect(TokenKind.Bracket, '{')) return; this.parseSequenceExpression(); if (this.errors.length > 0) return; if (!this.expect(TokenKind.Bracket, '}')) return; if (!this.expect(TokenKind.Operator, 'else')) return; // Nested if if (this.curVal() === 'if') { this.parseConditionalIf(); return; } if (!this.expect(TokenKind.Bracket, '{')) return; this.parseSequenceExpression(); if (this.errors.length > 0) return; this.expect(TokenKind.Bracket, '}'); } /** * parseConditional parses "expr ? expr1 : expr2" or "expr ?: expr2" */ private parseConditional(): void { if (this.curVal() !== '?') return; this.advance(); // skip ? // Elvis operator ?: if (this.curVal() === ':') { this.advance(); this.parseExpression(0); return; } this.parseExpression(0); if (this.errors.length > 0) return; if (!this.expect(TokenKind.Operator, ':')) return; this.parseExpression(0); } /** * parseArguments parses function call arguments: (arg1, arg2, ...) */ private parseArguments(): void { if (!this.expect(TokenKind.Bracket, '(')) return; while (this.curKind() !== TokenKind.Bracket || this.curVal() !== ')') { if (this.curKind() === TokenKind.EOF) { this.error('unexpected end of expression, expected ")"'); return; } if (this.curVal() === ',') { this.advance(); continue; } // Check for predicate { ... } argument if (this.curKind() === TokenKind.Bracket && this.curVal() === '{') { this.advance(); this.parseSequenceExpression(); this.expect(TokenKind.Bracket, '}'); } else { this.parseExpression(0); } if (this.errors.length > 0) return; if (this.curVal() === ',') { this.advance(); // Allow trailing comma if (this.curKind() === TokenKind.Bracket && this.curVal() === ')') { break; } } } this.expect(TokenKind.Bracket, ')'); } /** * parseArrayExpression parses "[elem1, elem2, ...]" */ private parseArrayExpression(): void { this.expect(TokenKind.Bracket, '['); if (this.errors.length > 0) return; if (this.curKind() === TokenKind.Bracket && this.curVal() === ']') { this.advance(); return; } while (this.errors.length === 0) { this.parseExpression(0); if (this.errors.length > 0) return; if (this.curKind() === TokenKind.Bracket && this.curVal() === ']') { break; } if (this.curVal() === ',') { this.advance(); // Allow trailing comma if (this.curKind() === TokenKind.Bracket && this.curVal() === ']') { break; } continue; } this.error(`expected "," or "]" but got "${this.curVal()}"`); return; } this.expect(TokenKind.Bracket, ']'); } /** * parseMapExpression parses "{key: value, ...}" */ private parseMapExpression(): void { this.expect(TokenKind.Bracket, '{'); if (this.errors.length > 0) return; if (this.curKind() === TokenKind.Bracket && this.curVal() === '}') { this.advance(); return; } while (this.errors.length === 0) { // Key: identifier, string, number, or parenthesized expression if (this.curKind() === TokenKind.Identifier || this.curKind() === TokenKind.String || this.curKind() === TokenKind.Number) { this.advance(); } else if (this.curKind() === TokenKind.Bracket && this.curVal() === '(') { this.advance(); this.parseExpression(0); this.expect(TokenKind.Bracket, ')'); } else { this.error(`map key must be a string, number, identifier, or parenthesized expression, got "${this.curVal()}"`); return; } if (!this.expect(TokenKind.Operator, ':')) return; this.parseExpression(0); if (this.errors.length > 0) return; if (this.curKind() === TokenKind.Bracket && this.curVal() === '}') { break; } if (this.curVal() === ',') { this.advance(); if (this.curKind() === TokenKind.Bracket && this.curVal() === '}') { break; } continue; } this.error(`expected "," or "}" but got "${this.curVal()}"`); return; } this.expect(TokenKind.Bracket, '}'); } }