tcl-js
Version:
tcl-js is a tcl intepreter written completely in Typescript. It is meant to replicate the tcl-sh interpreter as closely as possible.
512 lines (469 loc) • 12.6 kB
JavaScript
import { Token, TEOF, TOP, TNUMBER, TSTRING, TPAREN, TCOMMA, TNAME } from './token';
export function TokenStream(parser, expression) {
this.pos = 0;
this.current = null;
this.unaryOps = parser.unaryOps;
this.binaryOps = parser.binaryOps;
this.ternaryOps = parser.ternaryOps;
this.consts = parser.consts;
this.expression = expression;
this.savedPosition = 0;
this.savedCurrent = null;
this.options = parser.options;
}
TokenStream.prototype.newToken = function (type, value, pos) {
return new Token(type, value, pos != null ? pos : this.pos);
};
TokenStream.prototype.save = function () {
this.savedPosition = this.pos;
this.savedCurrent = this.current;
};
TokenStream.prototype.restore = function () {
this.pos = this.savedPosition;
this.current = this.savedCurrent;
};
TokenStream.prototype.next = function () {
if (this.pos >= this.expression.length) {
return this.newToken(TEOF, 'EOF');
}
if (this.isWhitespace() || this.isComment()) {
return this.next();
} else if (this.isRadixInteger() ||
this.isNumber() ||
this.isOperator() ||
this.isString() ||
this.isParen() ||
this.isComma() ||
this.isNamedOp() ||
this.isConst() ||
this.isName()) {
return this.current;
} else {
this.parseError('Unknown character "' + this.expression.charAt(this.pos) + '"');
}
};
TokenStream.prototype.isString = function () {
var r = false;
var startPos = this.pos;
var quote = this.expression.charAt(startPos);
if (quote === '\'' || quote === '"') {
var index = this.expression.indexOf(quote, startPos + 1);
while (index >= 0 && this.pos < this.expression.length) {
this.pos = index + 1;
if (this.expression.charAt(index - 1) !== '\\') {
var rawString = this.expression.substring(startPos + 1, index);
this.current = this.newToken(TSTRING, this.unescape(rawString), startPos);
r = true;
break;
}
index = this.expression.indexOf(quote, index + 1);
}
}
return r;
};
TokenStream.prototype.isParen = function () {
var c = this.expression.charAt(this.pos);
if (c === '(' || c === ')') {
this.current = this.newToken(TPAREN, c);
this.pos++;
return true;
}
return false;
};
TokenStream.prototype.isComma = function () {
var c = this.expression.charAt(this.pos);
if (c === ',') {
this.current = this.newToken(TCOMMA, ',');
this.pos++;
return true;
}
return false;
};
TokenStream.prototype.isConst = function () {
var startPos = this.pos;
var i = startPos;
for (; i < this.expression.length; i++) {
var c = this.expression.charAt(i);
if (c.toUpperCase() === c.toLowerCase()) {
if (i === this.pos || (c !== '_' && c !== '.' && (c < '0' || c > '9'))) {
break;
}
}
}
if (i > startPos) {
var str = this.expression.substring(startPos, i);
if (str in this.consts) {
this.current = this.newToken(TNUMBER, this.consts[str]);
this.pos += str.length;
return true;
}
}
return false;
};
TokenStream.prototype.isNamedOp = function () {
var startPos = this.pos;
var i = startPos;
for (; i < this.expression.length; i++) {
var c = this.expression.charAt(i);
if (c.toUpperCase() === c.toLowerCase()) {
if (i === this.pos || (c !== '_' && (c < '0' || c > '9'))) {
break;
}
}
}
if (i > startPos) {
var str = this.expression.substring(startPos, i);
if (this.isOperatorEnabled(str) && (str in this.binaryOps || str in this.unaryOps || str in this.ternaryOps)) {
this.current = this.newToken(TOP, str);
this.pos += str.length;
return true;
}
}
return false;
};
TokenStream.prototype.isName = function () {
var startPos = this.pos;
var i = startPos;
var hasLetter = false;
for (; i < this.expression.length; i++) {
var c = this.expression.charAt(i);
if (c.toUpperCase() === c.toLowerCase()) {
if (i === this.pos && (c === '$' || c === '_')) {
if (c === '_') {
hasLetter = true;
}
continue;
} else if (i === this.pos || !hasLetter || (c !== '_' && (c < '0' || c > '9'))) {
break;
}
} else {
hasLetter = true;
}
}
if (hasLetter) {
var str = this.expression.substring(startPos, i);
this.current = this.newToken(TNAME, str);
this.pos += str.length;
return true;
}
return false;
};
TokenStream.prototype.isWhitespace = function () {
var r = false;
var c = this.expression.charAt(this.pos);
while (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
r = true;
this.pos++;
if (this.pos >= this.expression.length) {
break;
}
c = this.expression.charAt(this.pos);
}
return r;
};
var codePointPattern = /^[0-9a-f]{4}$/i;
TokenStream.prototype.unescape = function (v) {
var index = v.indexOf('\\');
if (index < 0) {
return v;
}
var buffer = v.substring(0, index);
while (index >= 0) {
var c = v.charAt(++index);
switch (c) {
case '\'':
buffer += '\'';
break;
case '"':
buffer += '"';
break;
case '\\':
buffer += '\\';
break;
case '/':
buffer += '/';
break;
case 'b':
buffer += '\b';
break;
case 'f':
buffer += '\f';
break;
case 'n':
buffer += '\n';
break;
case 'r':
buffer += '\r';
break;
case 't':
buffer += '\t';
break;
case 'u':
// interpret the following 4 characters as the hex of the unicode code point
var codePoint = v.substring(index + 1, index + 5);
if (!codePointPattern.test(codePoint)) {
this.parseError('Illegal escape sequence: \\u' + codePoint);
}
buffer += String.fromCharCode(parseInt(codePoint, 16));
index += 4;
break;
default:
throw this.parseError('Illegal escape sequence: "\\' + c + '"');
}
++index;
var backslash = v.indexOf('\\', index);
buffer += v.substring(index, backslash < 0 ? v.length : backslash);
index = backslash;
}
return buffer;
};
TokenStream.prototype.isComment = function () {
var c = this.expression.charAt(this.pos);
if (c === '/' && this.expression.charAt(this.pos + 1) === '*') {
this.pos = this.expression.indexOf('*/', this.pos) + 2;
if (this.pos === 1) {
this.pos = this.expression.length;
}
return true;
}
return false;
};
TokenStream.prototype.isRadixInteger = function () {
var pos = this.pos;
if (pos >= this.expression.length - 2 || this.expression.charAt(pos) !== '0') {
return false;
}
++pos;
var radix;
var validDigit;
if (this.expression.charAt(pos) === 'x') {
radix = 16;
validDigit = /^[0-9a-f]$/i;
++pos;
} else if (this.expression.charAt(pos) === 'b') {
radix = 2;
validDigit = /^[01]$/i;
++pos;
} else {
return false;
}
var valid = false;
var startPos = pos;
while (pos < this.expression.length) {
var c = this.expression.charAt(pos);
if (validDigit.test(c)) {
pos++;
valid = true;
} else {
break;
}
}
if (valid) {
this.current = this.newToken(TNUMBER, parseInt(this.expression.substring(startPos, pos), radix));
this.pos = pos;
}
return valid;
};
TokenStream.prototype.isNumber = function () {
var valid = false;
var pos = this.pos;
var startPos = pos;
var resetPos = pos;
var foundDot = false;
var foundDigits = false;
var c;
while (pos < this.expression.length) {
c = this.expression.charAt(pos);
if ((c >= '0' && c <= '9') || (!foundDot && c === '.')) {
if (c === '.') {
foundDot = true;
} else {
foundDigits = true;
}
pos++;
valid = foundDigits;
} else {
break;
}
}
if (valid) {
resetPos = pos;
}
if (c === 'e' || c === 'E') {
pos++;
var acceptSign = true;
var validExponent = false;
while (pos < this.expression.length) {
c = this.expression.charAt(pos);
if (acceptSign && (c === '+' || c === '-')) {
acceptSign = false;
} else if (c >= '0' && c <= '9') {
validExponent = true;
acceptSign = false;
} else {
break;
}
pos++;
}
if (!validExponent) {
pos = resetPos;
}
}
if (valid) {
this.current = this.newToken(TNUMBER, parseFloat(this.expression.substring(startPos, pos)));
this.pos = pos;
} else {
this.pos = resetPos;
}
return valid;
};
TokenStream.prototype.isOperator = function () {
var startPos = this.pos;
var c = this.expression.charAt(this.pos);
if (
c === '+' ||
c === '-' ||
c === '/' ||
c === '%' ||
c === '^' ||
c === '?' ||
c === ':' ||
c === '~' ||
c === '.'
) {
this.current = this.newToken(TOP, c);
} else if (c === '*') {
if (this.expression.charAt(this.pos + 1) === '*') {
this.current = this.newToken(TOP, '**');
this.pos++;
} else {
this.current = this.newToken(TOP, '*');
}
} else if (c === '>') {
if (this.expression.charAt(this.pos + 1) === '=') {
this.current = this.newToken(TOP, '>=');
this.pos++;
} else if (this.expression.charAt(this.pos + 1) === '>') {
this.current = this.newToken(TOP, '>>');
this.pos++;
} else {
this.current = this.newToken(TOP, '>');
}
} else if (c === '<') {
if (this.expression.charAt(this.pos + 1) === '=') {
this.current = this.newToken(TOP, '<=');
this.pos++;
} else if (this.expression.charAt(this.pos + 1) === '<') {
this.current = this.newToken(TOP, '<<');
this.pos++;
} else {
this.current = this.newToken(TOP, '<');
}
} else if (c === '|') {
if (this.expression.charAt(this.pos + 1) === '|') {
this.current = this.newToken(TOP, '||');
this.pos++;
} else {
this.current = this.newToken(TOP, '|');
}
} else if (c === '&') {
if (this.expression.charAt(this.pos + 1) === '&') {
this.current = this.newToken(TOP, '&&');
this.pos++;
} else {
this.current = this.newToken(TOP, '&');
}
} else if (c === '=') {
if (this.expression.charAt(this.pos + 1) === '=') {
this.current = this.newToken(TOP, '==');
this.pos++;
} else {
return false;
}
} else if (c === '!') {
if (this.expression.charAt(this.pos + 1) === '=') {
this.current = this.newToken(TOP, '!=');
this.pos++;
} else {
this.current = this.newToken(TOP, c);
}
} else if (c === 'e') {
if (this.expression.charAt(this.pos + 1) === 'q') {
this.current = this.newToken(TOP, '==');
this.pos++;
} else {
return false;
}
} else if (c === 'n') {
if (this.expression.charAt(this.pos + 1) === 'e') {
this.current = this.newToken(TOP, '!=');
this.pos++;
} else {
return false;
}
} else {
return false;
}
this.pos++;
if (this.isOperatorEnabled(this.current.value)) {
return true;
} else {
this.pos = startPos;
return false;
}
};
var optionNameMap = {
'+': 'add',
'-': 'subtract',
'*': 'multiply',
'/': 'divide',
'%': 'remainder',
'**': 'power',
'<': 'comparison',
'>': 'comparison',
'<=': 'comparison',
'>=': 'comparison',
'==': 'comparison',
'!=': 'comparison',
'||': 'concatenate',
'&&': 'logical',
'||': 'logical',
'!': 'logical',
'?': 'conditional',
':': 'conditional',
'~': 'bitwise',
'>>': 'bitwise',
'<<': 'bitwise',
'|': 'bitwise',
'&': 'bitwise',
};
function getOptionName(op) {
return optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op;
}
TokenStream.prototype.isOperatorEnabled = function (op) {
var optionName = getOptionName(op);
var operators = this.options.operators || {};
// in is a special case for now because it's disabled by default
if (optionName === 'in') {
return !!operators['in'];
}
return !(optionName in operators) || !!operators[optionName];
};
TokenStream.prototype.getCoordinates = function () {
var line = 0;
var column;
var newline = -1;
do {
line++;
column = this.pos - newline;
newline = this.expression.indexOf('\n', newline + 1);
} while (newline >= 0 && newline < this.pos);
return {
line: line,
column: column
};
};
TokenStream.prototype.parseError = function (msg) {
var coords = this.getCoordinates();
throw new Error('parse error [' + coords.line + ':' + coords.column + ']: ' + msg);
};