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