import {Binary, ContextAccess, Expr, FunctionCall, Grouping, IndexAccess, Literal, Logical, Star, Unary} from "./ast"; import * as data from "./data"; import {ErrorType, ExpressionError, MAX_PARSER_DEPTH} from "./errors"; import {ParseContext, validateFunction} from "./funcs"; import {FunctionInfo} from "./funcs/info"; import {Token, TokenType} from "./lexer"; export class Parser { private extContexts: Map; private extFuncs: Map; private offset = 0; private depth = 0; private context: ParseContext; /** * Constructs a new parser for the given tokens * * @param tokens Tokens to build a parse tree from * @param extensionContexts Available context names * @param extensionFunctions Available functions (beyond the built-in ones) */ constructor(private tokens: Token[], extensionContexts: string[], extensionFunctions: FunctionInfo[]) { this.extContexts = new Map(); this.extFuncs = new Map(); for (const contextName of extensionContexts) { this.extContexts.set(contextName.toLowerCase(), true); } for (const {name, func} of extensionFunctions.map(x => ({ name: x.name, func: x }))) { this.extFuncs.set(name.toLowerCase(), func); } this.context = { allowUnknownKeywords: false, extensionContexts: this.extContexts, extensionFunctions: this.extFuncs }; } public parse(): Expr { // eslint-disable-next-line prefer-const let result!: Expr; // No tokens if (this.atEnd()) { return result; } result = this.expression(); if (!this.atEnd()) { throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek()); } return result; } private expression(): Expr { this.incrDepth(); try { return this.logicalOr(); } finally { this.decrDepth(); } } private logicalOr(): Expr { // && is higher precedence than || let expr = this.logicalAnd(); if (this.check(TokenType.OR)) { // Track depth this.incrDepth(); try { const logical = new Logical(this.peek(), [expr]); expr = logical; while (this.match(TokenType.OR)) { const right = this.logicalAnd(); logical.args.push(right); } } finally { this.decrDepth(); } } return expr; } private logicalAnd(): Expr { // == and != are higher precedence than && let expr = this.equality(); if (this.check(TokenType.AND)) { // Track depth this.incrDepth(); try { const logical = new Logical(this.peek(), [expr]); expr = logical; while (this.match(TokenType.AND)) { const right = this.equality(); logical.args.push(right); } } finally { this.decrDepth(); } } return expr; } private equality(): Expr { // >, >=, <, <= are higher precedence than == and != let expr = this.comparison(); while (this.match(TokenType.BANG_EQUAL, TokenType.EQUAL_EQUAL)) { const operator = this.previous(); const right = this.comparison(); expr = new Binary(expr, operator, right); } return expr; } private comparison(): Expr { // ! is higher precedence than >, >=, <, <= let expr = this.unary(); while (this.match(TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL)) { const operator = this.previous(); const right = this.unary(); expr = new Binary(expr, operator, right); } return expr; } private unary(): Expr { if (this.match(TokenType.BANG)) { // Track depth this.incrDepth(); const operator = this.previous(); const unary = this.unary(); try { return new Unary(operator, unary); } finally { this.decrDepth(); } } return this.index(); } private index(): Expr { let expr = this.call(); let depthIncreased = 0; if (expr instanceof Grouping || expr instanceof FunctionCall || expr instanceof ContextAccess) { let cont = true; while (cont) { switch (true) { case this.match(TokenType.LEFT_BRACKET): { let indexExpr: Expr; if (this.match(TokenType.STAR)) { indexExpr = new Star(); } else { indexExpr = this.expression(); } this.consume(TokenType.RIGHT_BRACKET, ErrorType.ErrorUnexpectedSymbol); // Track depth this.incrDepth(); depthIncreased++; expr = new IndexAccess(expr, indexExpr); break; } case this.match(TokenType.DOT): // Track depth this.incrDepth(); depthIncreased++; if (this.match(TokenType.IDENTIFIER)) { const property = this.previous(); expr = new IndexAccess(expr, new Literal(new data.StringData(property.lexeme), property)); } else if (this.match(TokenType.STAR)) { expr = new IndexAccess(expr, new Star()); } else { throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek()); } break; default: cont = false; } } } for (let i = 0; i < depthIncreased; i++) { this.decrDepth(); } return expr; } private call(): Expr { if (!this.check(TokenType.IDENTIFIER)) { return this.primary(); } const identifier = this.next(); if (!this.match(TokenType.LEFT_PAREN)) { if (!this.extContexts.has(identifier.lexeme.toLowerCase())) { throw this.buildError(ErrorType.ErrorUnrecognizedContext, identifier); } return new ContextAccess(identifier); } // Function call const args: Expr[] = []; // Arguments while (!this.match(TokenType.RIGHT_PAREN)) { const aexp = this.expression(); args.push(aexp); if (!this.check(TokenType.RIGHT_PAREN)) { this.consume(TokenType.COMMA, ErrorType.ErrorUnexpectedSymbol); } } validateFunction(this.context, identifier, args.length); return new FunctionCall(identifier, args); } private primary(): Expr { switch (true) { case this.match(TokenType.FALSE): return new Literal(new data.BooleanData(false), this.previous()); case this.match(TokenType.TRUE): return new Literal(new data.BooleanData(true), this.previous()); case this.match(TokenType.NULL): return new Literal(new data.Null(), this.previous()); case this.match(TokenType.NUMBER): return new Literal(new data.NumberData(this.previous().value as number), this.previous()); case this.match(TokenType.STRING): return new Literal(new data.StringData(this.previous().value as string), this.previous()); case this.match(TokenType.LEFT_PAREN): { const expr = this.expression(); if (this.atEnd()) { throw this.buildError(ErrorType.ErrorUnexpectedEndOfExpression, this.previous()); // Back up to get the last token before the EOF } this.consume(TokenType.RIGHT_PAREN, ErrorType.ErrorUnexpectedSymbol); return new Grouping(expr); } case this.atEnd(): throw this.buildError(ErrorType.ErrorUnexpectedEndOfExpression, this.previous()); // Back up to get the last token before the EOF } throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek()); } // match consumes the next token if it matches any of the given types private match(...tokenTypes: TokenType[]): boolean { for (const tokenType of tokenTypes) { if (this.check(tokenType)) { this.next(); return true; } } return false; } // check peeks whether the next token is of the given type private check(tokenType: TokenType): boolean { if (this.atEnd()) { return false; } return this.peek().type == tokenType; } // atEnd peeks whether the next token is EOF private atEnd(): boolean { return this.peek().type == TokenType.EOF; } private next(): Token { if (!this.atEnd()) { this.offset++; } return this.previous(); } private peek(): Token { return this.tokens[this.offset]; } // previous returns the previous token private previous(): Token { return this.tokens[this.offset - 1]; } // consume attempts to consume the next token if it matches the given type. It returns an error of // the given ParseErrorKind otherwise. private consume(tokenType: TokenType, errorType: ErrorType) { if (this.check(tokenType)) { this.next(); return; } throw this.buildError(errorType, this.peek()); } private incrDepth() { this.depth++; if (this.depth > MAX_PARSER_DEPTH) { throw this.buildError(ErrorType.ErrorExceededMaxDepth, this.peek()); } } private decrDepth() { this.depth--; } private buildError(errType: ErrorType, token: Token): Error { return new ExpressionError(errType, token); } }