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.
272 lines (239 loc) • 7.04 kB
JavaScript
import { TOP, TNUMBER, TSTRING, TPAREN, TCOMMA, TNAME } from './token';
import {
Instruction,
INUMBER,
IVAR,
IFUNCALL,
IEXPR,
IMEMBER,
ternaryInstruction,
binaryInstruction,
unaryInstruction,
} from './instruction';
import contains from './contains';
export function ParserState(parser, tokenStream, options) {
this.parser = parser;
this.tokens = tokenStream;
this.current = null;
this.nextToken = null;
this.next();
this.savedCurrent = null;
this.savedNextToken = null;
this.allowMemberAccess = options.allowMemberAccess !== false;
}
ParserState.prototype.next = function() {
this.current = this.nextToken;
this.nextToken = this.tokens.next();
return this.nextToken;
};
ParserState.prototype.tokenMatches = function(token, value) {
if (typeof value === 'undefined') {
return true;
} else if (Array.isArray(value)) {
return contains(value, token.value);
} else if (typeof value === 'function') {
return value(token);
} else {
return token.value === value;
}
};
ParserState.prototype.save = function() {
this.savedCurrent = this.current;
this.savedNextToken = this.nextToken;
this.tokens.save();
};
ParserState.prototype.restore = function() {
this.tokens.restore();
this.current = this.savedCurrent;
this.nextToken = this.savedNextToken;
};
ParserState.prototype.accept = function(type, value) {
if (
this.nextToken.type === type &&
this.tokenMatches(this.nextToken, value)
) {
this.next();
return true;
}
return false;
};
ParserState.prototype.expect = function(type, value) {
if (!this.accept(type, value)) {
var coords = this.tokens.getCoordinates();
throw new Error(
'parse error [' +
coords.line +
':' +
coords.column +
']: Expected ' +
(value || type),
);
}
};
ParserState.prototype.parseAtom = function(instr) {
if (this.accept(TNAME)) {
instr.push(new Instruction(IVAR, this.current.value));
} else if (this.accept(TNUMBER)) {
instr.push(new Instruction(INUMBER, this.current.value));
} else if (this.accept(TSTRING)) {
instr.push(new Instruction(INUMBER, this.current.value));
} else if (this.accept(TPAREN, '(')) {
this.parseExpression(instr);
this.expect(TPAREN, ')');
} else {
throw new Error('unexpected ' + this.nextToken);
}
};
ParserState.prototype.parseExpression = function(instr) {
this.parseConditionalExpression(instr);
};
ParserState.prototype.parseConditionalExpression = function(instr) {
this.parseOrExpression(instr);
while (this.accept(TOP, '?')) {
var trueBranch = [];
var falseBranch = [];
this.parseConditionalExpression(trueBranch);
this.expect(TOP, ':');
this.parseConditionalExpression(falseBranch);
instr.push(new Instruction(IEXPR, trueBranch));
instr.push(new Instruction(IEXPR, falseBranch));
instr.push(ternaryInstruction('?'));
}
};
ParserState.prototype.parseOrExpression = function(instr) {
this.parseAndExpression(instr);
while (this.accept(TOP, '||')) {
var falseBranch = [];
this.parseAndExpression(falseBranch);
instr.push(new Instruction(IEXPR, falseBranch));
instr.push(binaryInstruction('||'));
}
};
ParserState.prototype.parseAndExpression = function(instr) {
this.parseBitwise(instr);
while (this.accept(TOP, '&&')) {
var trueBranch = [];
this.parseBitwise(trueBranch);
instr.push(new Instruction(IEXPR, trueBranch));
instr.push(binaryInstruction('&&'));
}
};
var BITWISE_OPERATORS = ['&', '|', '^'];
ParserState.prototype.parseBitwise = function(instr) {
this.parseComparison(instr);
while (this.accept(TOP, BITWISE_OPERATORS)) {
var op = this.current;
this.parseComparison(instr);
instr.push(binaryInstruction(op.value));
}
};
var COMPARISON_OPERATORS = ['==', '!=', '<', '<=', '>=', '>', 'eq', 'ne'];
ParserState.prototype.parseComparison = function(instr) {
this.parseShift(instr);
while (this.accept(TOP, COMPARISON_OPERATORS)) {
var op = this.current;
this.parseShift(instr);
instr.push(binaryInstruction(op.value));
}
};
var SHIFT_OPERATORS = ['<<', '>>'];
ParserState.prototype.parseShift = function(instr) {
this.parseAddSub(instr);
while (this.accept(TOP, SHIFT_OPERATORS)) {
var op = this.current;
this.parseAddSub(instr);
instr.push(binaryInstruction(op.value));
}
};
var ADD_SUB_OPERATORS = ['+', '-'];
ParserState.prototype.parseAddSub = function(instr) {
this.parseTerm(instr);
while (this.accept(TOP, ADD_SUB_OPERATORS)) {
var op = this.current;
this.parseTerm(instr);
instr.push(binaryInstruction(op.value));
}
};
var TERM_OPERATORS = ['*', '/', '%'];
ParserState.prototype.parseTerm = function(instr) {
this.parseExponential(instr);
while (this.accept(TOP, TERM_OPERATORS)) {
var op = this.current;
this.parseExponential(instr);
instr.push(binaryInstruction(op.value));
}
};
ParserState.prototype.parseExponential = function(instr) {
this.parseFactor(instr);
while (this.accept(TOP, '**')) {
this.parseFactor(instr);
instr.push(binaryInstruction('**'));
}
};
ParserState.prototype.parseFactor = function(instr) {
var unaryOps = this.tokens.unaryOps;
function isPrefixOperator(token) {
return token.value in unaryOps;
}
this.save();
if (this.accept(TOP, isPrefixOperator)) {
if (
this.current.value !== '-' &&
this.current.value !== '+' &&
this.nextToken.type === TPAREN &&
this.nextToken.value === '('
) {
this.restore();
this.parseFunctionCall(instr);
} else {
var op = this.current;
this.parseFactor(instr);
instr.push(unaryInstruction(op.value));
}
} else {
this.parseFunctionCall(instr);
}
};
ParserState.prototype.parseFunctionCall = function(instr) {
var unaryOps = this.tokens.unaryOps;
function isPrefixOperator(token) {
return token.value in unaryOps;
}
if (this.accept(TOP, isPrefixOperator)) {
var op = this.current;
this.parseAtom(instr);
instr.push(unaryInstruction(op.value));
} else {
this.parseMemberExpression(instr);
while (this.accept(TPAREN, '(')) {
if (this.accept(TPAREN, ')')) {
instr.push(new Instruction(IFUNCALL, 0));
} else {
var argCount = this.parseArgumentList(instr);
instr.push(new Instruction(IFUNCALL, argCount));
}
}
}
};
ParserState.prototype.parseArgumentList = function(instr) {
var argCount = 0;
while (!this.accept(TPAREN, ')')) {
this.parseExpression(instr);
++argCount;
while (this.accept(TCOMMA)) {
this.parseExpression(instr);
++argCount;
}
}
return argCount;
};
ParserState.prototype.parseMemberExpression = function(instr) {
this.parseAtom(instr);
while (this.accept(TOP, '.')) {
if (!this.allowMemberAccess) {
throw new Error('unexpected ".", member access is not permitted');
}
this.expect(TNAME);
instr.push(new Instruction(IMEMBER, this.current.value));
}
};