UNPKG

consys

Version:

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

295 lines (294 loc) 9.59 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const Token_1 = __importStar(require("./Token")); const Util_1 = require("../Util"); /** * The lexer is responsible for reading in source code of the dsl and turn it into a list of meaningful tokens. */ class Lexer { /** * Creates a new lexer instance for a given string of source code * * @param source source code * @param offset index offset into the source code */ constructor(source, offset = 0) { // lexer state this.tokens = []; this.source = ''; this.offset = 0; this.start = 0; this.current = 0; this.source = source; this.offset = offset; } /** * Checks if the given symbol is a digit from 0-9. * * @param symbol character to check * @private */ static isDigit(symbol) { return !!symbol.match(/\d/); } /** * Checks if the given symbol is an alphabetic lower- or uppercase value. * * @param symbol character to check * @private */ static isAlpha(symbol) { return !!symbol.match(/[a-zA-Z]|_/); } /** * Checks if the given symbol is either a digit or alphabetic. * * @param symbol character to check * @private */ static isAlphaNumeric(symbol) { return Lexer.isDigit(symbol) || Lexer.isAlpha(symbol); } /** * Scans the source code and turns it into a list of tokens. */ scan() { while (!this.isAtEnd()) { this.start = this.current; this.scanToken(); } this.tokens.push(new Token_1.default(Token_1.TokenType.EOF, '', null, this.source.length + this.offset)); return this.tokens; } /** * Scans a single token on the current index. * @private */ scanToken() { let currentSymbol = this.advance(); if (Lexer.singleCharTokens[currentSymbol] !== undefined) { this.emitToken(Lexer.singleCharTokens[currentSymbol]); return; } switch (currentSymbol) { case '!': this.emitToken(this.match('=') ? Token_1.TokenType.EXCLAMATION_MARK_EQUAL : Token_1.TokenType.EXCLAMATION_MARK); break; case '=': if (!this.match('=')) { throw this.errorOnPosition("Single '=' is not allowed, did you mean '=='?"); } this.emitToken(Token_1.TokenType.EQUAL_EQUAL); break; case '<': this.emitToken(this.match('=') ? Token_1.TokenType.LESS_EQUAL : Token_1.TokenType.LESS); break; case '>': this.emitToken(this.match('=') ? Token_1.TokenType.GREATER_EQUAL : Token_1.TokenType.GREATER); break; case '|': if (!this.match('|')) { throw this.errorOnPosition("Single '|' is not allowed, did you mean '||'?"); } this.emitToken(Token_1.TokenType.PIPE_PIPE); break; case '&': if (!this.match('&')) { throw this.errorOnPosition("Single '&' is not allowed, did you mean '&&'?"); } this.emitToken(Token_1.TokenType.AMPERSAND_AMPERSAND); break; case "'": this.handleString(); break; case ' ': case '\r': case '\t': case '\n': // ignore whitespace and new lines break; default: if (Lexer.isDigit(currentSymbol)) { this.handleNumber(); } else if (Lexer.isAlpha(currentSymbol)) { this.handleIdentifier(); } else { throw this.errorOnPosition(`Unexpected character '${currentSymbol}'`); } break; } } /** * Should the lexer come to a single quote char, it interprets as a string the next symbols until the next quote. * @private */ handleString() { // ignore symbols until we find the matching quote while (this.peek() !== "'" && !this.isAtEnd()) { this.advance(); } if (this.isAtEnd()) { throw this.errorOnPosition('Unterminated string', this.start); } // we are on the enclosing quote, so go to the next symbol this.advance(); const begin = this.start + 1; const end = this.current - 1; this.emitToken(Token_1.TokenType.STRING, this.source.substring(begin, end), begin, end, this.start); } /** * Scans a number token. * @private */ handleNumber() { while (Lexer.isDigit(this.peek())) { this.advance(); } // for non-integer values, look for the dot separator if (this.peek() == '.' && Lexer.isDigit(this.peekNext())) { this.advance(); while (Lexer.isDigit(this.peek())) { this.advance(); } } const number = this.source.substring(this.start, this.current); this.emitToken(Token_1.TokenType.NUMBER, Number.parseFloat(number)); } /** * Scans an identifier token. * @private */ handleIdentifier() { while (Lexer.isAlphaNumeric(this.peek())) { this.advance(); } let identifier = this.source.substring(this.start, this.current); let type = Lexer.keywords[identifier.toUpperCase()]; if (!type) { type = Token_1.TokenType.IDENTIFIER; } this.emitToken(type, type === Token_1.TokenType.ALWAYS ? true : null); } /** * Checks if the lexer is done scanning. * @private */ isAtEnd() { return this.current >= this.source.length; } /** * Consume the current symbol. * @private */ advance() { return this.source.charAt(this.current++); } /** * Consume the current symbol if it matches a given character, else return false. * @param expected expected character * @private */ match(expected) { if (this.isAtEnd()) { return false; } if (this.source.charAt(this.current) !== expected) { return false; } this.current++; return true; } /** * Returns the current symbol. * @private */ peek() { if (this.isAtEnd()) { return '\0'; } return this.source.charAt(this.current); } /** * Returns the symbol after the current symbol. * @private */ peekNext() { if (this.current + 1 >= this.source.length) { return '\0'; } return this.source.charAt(this.current + 1); } /** * Push a new token to the output list. * * @param type token type * @param literal token value * @param begin start index in source * @param end end index in source * @param position position in source (may be different from begin) * @private */ emitToken(type, literal = null, begin = this.start, end = this.current, position = begin) { this.tokens.push(new Token_1.default(type, this.source.substring(begin, end), literal, position + this.offset)); } /** * Prints an error message to the console, then throws an error. * * @param message error message * @param position position in source where the error occurred * @private */ errorOnPosition(message, position = this.current - 1) { Util_1.Log.reportError('Syntax', this.source, message, position + this.offset); return Error(); } } exports.default = Lexer; // all tokens that have a single character Lexer.singleCharTokens = { '(': Token_1.TokenType.PARENTHESIS_OPEN, ')': Token_1.TokenType.PARENTHESIS_CLOSE, '+': Token_1.TokenType.PLUS, '-': Token_1.TokenType.MINUS, ',': Token_1.TokenType.COMMA, '.': Token_1.TokenType.DOT, ':': Token_1.TokenType.COLON, '/': Token_1.TokenType.SLASH, '*': Token_1.TokenType.STAR, '%': Token_1.TokenType.PERCENT, $: Token_1.TokenType.DOLLAR, '#': Token_1.TokenType.HASH, }; // all keywords Lexer.keywords = { ALWAYS: Token_1.TokenType.ALWAYS, WHEN: Token_1.TokenType.WHEN, THEN: Token_1.TokenType.THEN, IF: Token_1.TokenType.IF, AND: Token_1.TokenType.AND, OR: Token_1.TokenType.OR, NOT: Token_1.TokenType.NOT, };