UNPKG

consys

Version:

consys is a flexible tool to evaluate models using generic and readable constraints.

480 lines (479 loc) 16 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const Token_1 = require("./Token"); const Expression_1 = require("./Expression"); const Util_1 = require("../Util"); /** * This class transforms a given list of tokens into an abstract syntax tree mirroring the dsl grammar. * Syntax definition for constraints: * * constraint -> activation ( ":" | "THEN" ) assertion ; * activation -> "ALWAYS" | "WHEN" | "IF" expression | functionExpr ; * assertion -> expression ; * * expression -> disjunction ; * disjunction -> conjunction ( ( "||" | "OR" ) conjunction )* ; * conjunction -> equality ( ( "&&" | "AND" ) equality )* ; * equality -> comparison ( ( "==" | "!=" ) comparison )? ; * comparison -> term ( ( "<" | "<=" | ">=" | ">" ) term )? ; * term -> factor ( ( "+" | "-" ) factor )* ; * factor -> unary ( ( "*" | "/" | "%" ) unary )* ; * unary -> ( "!" | "-" | "NOT" ) unary | primary ; * primary -> number | string | variable | function | "(" expression ")" ; * * variable -> model | state ; * model -> "$" ( nested )? ; * state -> "#" ( nested )? ; * nested -> identifier ( "." identifier )* ; * * function -> functionExpr | identifier "(" ( arguments )? ")" ; * arguments -> expression ( "," expression )* ; * functionExpr -> identifier ; * * number -> natural ( digit )* ( "." ( digit )+ )? ; * string -> "'" ( character )* "'" ; * identifier -> alpha ( alpha | digit | "_" )* ; * * digit -> zero | natural ; * character -> digit | alpha ; * zero -> "0" ; * natural -> "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; * alpha -> "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | * "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | * "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "a" | * "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | * "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | * "t" | "u" | "v" | "w" | "x" | "y" | "z" ; */ class Parser { /** * Creates a new parser instance from a given string of source code and a list of tokens. * * @param source source code * @param tokens list of tokens */ constructor(source, tokens) { this.current = 0; // statistics used to evaluate the generated constraint this.statistics = { counts: { model: {}, state: {}, }, }; this.source = source; this.tokens = tokens; } /** * Returns the ast for the given token list for a constraint. */ parse() { return this.run(() => this.constraint(), `Expected constraint`); } /** * Returns the ast for the given token list for a model. */ parseModel() { return this.run(() => { this.advance(); return this.variable(); }, `Expected model`); } /** * Returns the ast for the given token list for a state. */ parseState() { return this.run(() => { this.advance(); return this.variable(); }, `Expected state`); } /** * Returns the ast for the given token list for a function. */ parseFunction() { return this.run(() => { this.advance(); return this.functionCall(); }, `Expected function`); } /** * Returns the ast for the given token list for a function expression. */ parseFunctionExpr() { return this.run(() => { this.advance(); return this.functionExpr(); }, `Expected function expression`); } /** * Starts the parsing algorithm with a given starting rule. * * @param start starting rule * @param emptyMessage error message if an empty list is given (single eof token) * @private */ run(start, emptyMessage) { if (this.tokens.length === 1) { throw this.syntaxErrorOnToken(this.peek(), emptyMessage); } const root = start(); return new Expression_1.Expression.AST(root, this.source, this.statistics); } /** * Matches this rule: * constraint -> activation ( ":" | "THEN" ) assertion ; * @private */ constraint() { let activation = this.activation(); this.consumeAny("Expected ':' or 'THEN' after activation", Token_1.TokenType.COLON, Token_1.TokenType.THEN); let assertion = this.assertion(); if (!this.isAtEnd()) { let token = this.peek(); throw this.syntaxErrorOnToken(token, `Unexpected token '${token.lexeme}'`); } this.advance(); return new Expression_1.Expression.Constraint(activation, assertion); } /** * Matches this rule: * activation -> "ALWAYS" | "WHEN" | "IF" expression | functionExpr ; * @private */ activation() { if (this.matchAny(Token_1.TokenType.ALWAYS, Token_1.TokenType.WHEN, Token_1.TokenType.IF)) { let operator = this.previous(); if (operator.type === Token_1.TokenType.ALWAYS) { return new Expression_1.Expression.Literal(operator); } return this.expression(); } if (!this.check(Token_1.TokenType.IDENTIFIER)) { throw this.syntaxErrorOnToken(this.peek(), 'Expected activation'); } return this.functionExpr(); } /** * Matches this rule: * assertion -> expression ; * @private */ assertion() { return this.expression(); } /** * Matches this rule: * expression -> disjunction ; * @private */ expression() { return this.disjunction(); } /** * Matches this rule: * disjunction -> conjunction ( ( "||" | "OR" ) conjunction )* ; * @private */ disjunction() { return this.parseLeftAssociativeBinaryOperator(true, () => this.conjunction(), Token_1.TokenType.PIPE_PIPE, Token_1.TokenType.OR); } /** * Matches this rule: * conjunction -> equality ( ( "&&" | "AND" ) equality )* ; * @private */ conjunction() { return this.parseLeftAssociativeBinaryOperator(true, () => this.equality(), Token_1.TokenType.AMPERSAND_AMPERSAND, Token_1.TokenType.AND); } /** * Matches this rule: * equality -> comparison ( ( "==" | "!=" ) comparison )? ; * @private */ equality() { return this.parseLeftAssociativeOptionalBinaryOperator(() => this.comparison(), Token_1.TokenType.EQUAL_EQUAL, Token_1.TokenType.EXCLAMATION_MARK_EQUAL); } /** * Matches this rule: * comparison -> term ( ( "<" | "<=" | ">=" | ">" ) term )? ; * @private */ comparison() { return this.parseLeftAssociativeOptionalBinaryOperator(() => this.term(), Token_1.TokenType.LESS, Token_1.TokenType.LESS_EQUAL, Token_1.TokenType.GREATER_EQUAL, Token_1.TokenType.GREATER); } /** * Matches this rule: * term -> factor ( ( "+" | "-" ) factor )* ; * @private */ term() { return this.parseLeftAssociativeBinaryOperator(false, () => this.factor(), Token_1.TokenType.PLUS, Token_1.TokenType.MINUS); } /** * Matches this rule: * factor -> unary ( ( "*" | "/" | "%" ) unary )* ; * @private */ factor() { return this.parseLeftAssociativeBinaryOperator(false, () => this.unary(), Token_1.TokenType.STAR, Token_1.TokenType.SLASH, Token_1.TokenType.PERCENT); } /** * Matches this rule: * unary -> ( "!" | "-" | "NOT" ) unary | primary ; * @private */ unary() { if (this.matchAny(Token_1.TokenType.EXCLAMATION_MARK, Token_1.TokenType.MINUS, Token_1.TokenType.NOT)) { let operator = this.previous(); let right = this.unary(); return new Expression_1.Expression.Unary(operator, right); } return this.primary(); } /** * Matches this rule: * primary -> number | string | variable | function | "(" expression ")" ; * @private */ primary() { if (this.matchAny(Token_1.TokenType.IDENTIFIER)) { return this.functionCall(); } if (this.matchAny(Token_1.TokenType.DOLLAR, Token_1.TokenType.HASH)) { return this.variable(); } if (this.matchAny(Token_1.TokenType.NUMBER, Token_1.TokenType.STRING)) { return new Expression_1.Expression.Literal(this.previous()); } if (this.matchAny(Token_1.TokenType.PARENTHESIS_OPEN)) { let expression = this.expression(); this.consume(Token_1.TokenType.PARENTHESIS_CLOSE, "Expected ')' after expression"); return new Expression_1.Expression.Grouping(expression); } throw this.syntaxErrorOnToken(this.peek(), 'Expected expression'); } /** * Matches this rule: * variable -> model | state ; * model -> "$" ( nested )? ; * state -> "#" ( nested )? ; * nested -> identifier ( "." identifier )* ; * @private */ variable() { let type = this.previous(); let name = []; if (this.matchAny(Token_1.TokenType.IDENTIFIER)) { name.push(this.previous()); while (this.matchAny(Token_1.TokenType.DOT)) { let identifier = this.consume(Token_1.TokenType.IDENTIFIER, 'Expected identifier'); name.push(identifier); } } this.addStatisticsVariable(type, name); return new Expression_1.Expression.Variable(type, name); } /** * Matches this rule: * function -> functionExpr | identifier "(" ( arguments )? ")" ; * @private */ functionCall() { const name = this.previous(); let args = []; if (this.matchAny(Token_1.TokenType.PARENTHESIS_OPEN)) { args = this.args(); this.consume(Token_1.TokenType.PARENTHESIS_CLOSE, "Expected ')' after arguments"); } return new Expression_1.Expression.Function(name, args); } /** * Matches this rule: * arguments -> expression ( "," expression )* ; * @private */ args() { const args = []; if (this.check(Token_1.TokenType.PARENTHESIS_CLOSE)) { return args; } args.push(this.expression()); while (this.matchAny(Token_1.TokenType.COMMA)) { args.push(this.expression()); } return args; } /** * Matches this rule: * functionExpr -> identifier ; * @private */ functionExpr() { const name = this.advance(); return new Expression_1.Expression.Function(name, []); } /** * Parses a rule with the following format: * rule -> production ( ( operators[0] | operators[1] | ... ) production )* ; * * @param logical true if currently parsing a logical expression * @param production production function pointer * @param operators binary operators of this rule * @private */ parseLeftAssociativeBinaryOperator(logical, production, ...operators) { let expression = production(); while (this.matchAny(...operators)) { let operator = this.previous(); let right = production(); if (logical) { expression = new Expression_1.Expression.Logical(expression, operator, right); } else { expression = new Expression_1.Expression.Binary(expression, operator, right); } } return expression; } /** * Parses a rule with the following format: * rule -> production ( ( operators[0] | operators[1] | ... ) production )? ; * * @param production production function pointer * @param operators binary operators of this rule * @private */ parseLeftAssociativeOptionalBinaryOperator(production, ...operators) { let expression = production(); if (this.matchAny(...operators)) { let operator = this.previous(); let right = production(); expression = new Expression_1.Expression.Binary(expression, operator, right); } return expression; } /** * Adds a variable of given type (model or state) to the statistics counts. * * @param type variable type * @param name variable name * @private */ addStatisticsVariable(type, name) { if (name.length === 0) { return; } const identifierString = name.map(token => token.lexeme).join('.'); if (type.type === Token_1.TokenType.DOLLAR) { if (!this.statistics.counts.model[identifierString]) { this.statistics.counts.model[identifierString] = 0; } this.statistics.counts.model[identifierString]++; } else { if (!this.statistics.counts.state[identifierString]) { this.statistics.counts.state[identifierString] = 0; } this.statistics.counts.state[identifierString]++; } } /** * Consumes the current token if any of the given token types matches the current token. * * @param types token types to match * @private */ matchAny(...types) { for (let type of types) { if (this.check(type)) { this.advance(); return true; } } return false; } /** * Consumes the current token. * * @param type token type * @param message error message if current token is not of given type * @private */ consume(type, message) { if (this.check(type)) { return this.advance(); } throw this.syntaxErrorOnToken(this.peek(), message); } /** * Consumes the current token if it matches any of the given token types. * * @param message error message if current token is not of any given type * @param types token types * @private */ consumeAny(message, ...types) { for (const type of types) { if (this.check(type)) { return this.advance(); } } throw this.syntaxErrorOnToken(this.peek(), message); } /** * Check if the current token is of the given type. * * @param type token type to check * @private */ check(type) { if (this.isAtEnd()) { return false; } return this.peek().type === type; } /** * Consumes the current token. * @private */ advance() { if (!this.isAtEnd()) { this.current++; } return this.previous(); } /** * Checks if the parser is done. * @private */ isAtEnd() { return this.peek().type === Token_1.TokenType.EOF; } /** * Returns the current token. * @private */ peek() { return this.tokens[this.current]; } /** * Returns the previous token. * @private */ previous() { return this.tokens[this.current - 1]; } /** * Prints an error message and then throws an error. * * @param token token where the error occurred * @param message error message * @private */ syntaxErrorOnToken(token, message) { Util_1.Log.reportError('Syntax', this.source, message, token.position); return Error(); } } exports.default = Parser;