consys
Version:
consys is a flexible tool to evaluate models using generic and readable constraints.
480 lines (479 loc) • 16 kB
JavaScript
"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;