UNPKG

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
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); };