1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2024-05-28 08:41:30 +00:00
8bitworkshop/gen/common/basic/compiler.js

1814 lines
67 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DIALECTS = exports.MODERN_BASIC = exports.BASIC80 = exports.APPLESOFT_BASIC = exports.ALTAIR_BASIC41 = exports.BASICODE = exports.DEC_BASIC_PLUS = exports.DEC_BASIC_11 = exports.HP_TIMESHARED_BASIC = exports.TINY_BASIC = exports.DARTMOUTH_4TH_EDITION = exports.ECMA55_MINIMAL = exports.BASICParser = exports.TokenType = exports.CompileError = void 0;
class CompileError extends Error {
constructor(msg, loc) {
super(msg);
Object.setPrototypeOf(this, CompileError.prototype);
this.$loc = loc;
}
}
exports.CompileError = CompileError;
// Lexer regular expression -- each (capture group) handles a different token type
// FLOAT INT HEXOCTAL REMARK IDENT STRING RELOP EXP OPERATORS OTHER WS
const re_toks = /([0-9.]+[E][+-]?\d+|\d+[.][E0-9]*|[.][E0-9]+)|[0]*(\d+)|&([OH][0-9A-F]+)|(['].*)|([A-Z_]\w*[$]?)|(".*?")|([<>]?[=<>#])|(\*\*)|([-+*/^,;:()\[\]\?\\])|(\S+)|(\s+)/gi;
var TokenType;
(function (TokenType) {
TokenType[TokenType["EOL"] = 0] = "EOL";
TokenType[TokenType["Float"] = 1] = "Float";
TokenType[TokenType["Int"] = 2] = "Int";
TokenType[TokenType["HexOctalInt"] = 3] = "HexOctalInt";
TokenType[TokenType["Remark"] = 4] = "Remark";
TokenType[TokenType["Ident"] = 5] = "Ident";
TokenType[TokenType["String"] = 6] = "String";
TokenType[TokenType["Relational"] = 7] = "Relational";
TokenType[TokenType["DoubleStar"] = 8] = "DoubleStar";
TokenType[TokenType["Operator"] = 9] = "Operator";
TokenType[TokenType["CatchAll"] = 10] = "CatchAll";
TokenType[TokenType["Whitespace"] = 11] = "Whitespace";
TokenType[TokenType["_LAST"] = 12] = "_LAST";
})(TokenType || (exports.TokenType = TokenType = {}));
class Token {
}
const OPERATORS = {
'IMP': { f: 'bimp', p: 4 },
'EQV': { f: 'beqv', p: 5 },
'XOR': { f: 'bxor', p: 6 },
'OR': { f: 'bor', p: 7 },
'AND': { f: 'band', p: 8 },
'||': { f: 'lor', p: 17 },
'&&': { f: 'land', p: 18 },
'=': { f: 'eq', p: 50 },
'==': { f: 'eq', p: 50 },
'<>': { f: 'ne', p: 50 },
'><': { f: 'ne', p: 50 },
'!=': { f: 'ne', p: 50 },
'#': { f: 'ne', p: 50 },
'<': { f: 'lt', p: 50 },
'>': { f: 'gt', p: 50 },
'<=': { f: 'le', p: 50 },
'>=': { f: 'ge', p: 50 },
'MIN': { f: 'min', p: 75 },
'MAX': { f: 'max', p: 75 },
'+': { f: 'add', p: 100 },
'-': { f: 'sub', p: 100 },
'%': { f: 'mod', p: 140 },
'MOD': { f: 'mod', p: 140 },
'\\': { f: 'idiv', p: 150 },
'*': { f: 'mul', p: 200 },
'/': { f: 'div', p: 200 },
'^': { f: 'pow', p: 300 },
'**': { f: 'pow', p: 300 },
};
function getOperator(op) {
return OPERATORS[op];
}
function getPrecedence(tok) {
switch (tok.type) {
case TokenType.Operator:
case TokenType.DoubleStar:
case TokenType.Relational:
case TokenType.Ident:
let op = getOperator(tok.str);
if (op)
return op.p;
}
return -1;
}
// is token an end of statement marker? (":" or end of line)
function isEOS(tok) {
return tok.type == TokenType.EOL || tok.type == TokenType.Remark
|| tok.str == ':' || tok.str == 'ELSE'; // TODO: only ELSE if ifElse==true
}
function stripQuotes(s) {
// TODO: assert
return s.substr(1, s.length - 2);
}
function isLiteral(arg) {
return arg.value != null;
}
function isLookup(arg) {
return arg.name != null;
}
function isBinOp(arg) {
return arg.op != null && arg.left != null && arg.right != null;
}
function isUnOp(arg) {
return arg.op != null && arg.expr != null;
}
function mergeLocs(a, b) {
return {
line: Math.min(a.line, b.line),
start: Math.min(a.start, b.start),
end: Math.max(a.end, b.end),
label: a.label || b.label,
path: a.path || b.path,
};
}
///// BASIC PARSER
class BASICParser {
constructor() {
this.opts = exports.DIALECTS['DEFAULT'];
this.maxlinelen = 255; // maximum line length (some like HP use 72 chars)
this.optionCount = 0;
this.lineno = 0;
this.curlabel = null;
this.stmts = [];
this.labels = {};
this.targets = {};
this.errors = [];
this.listings = {};
this.vardefs = {};
this.varrefs = {};
this.fnrefs = {};
this.scopestack = [];
this.elseifcount = 0;
}
addError(msg, loc) {
var tok = this.lasttoken || this.peekToken();
if (!loc)
loc = tok.$loc;
this.errors.push({ path: loc.path, line: loc.line, label: this.curlabel, start: loc.start, end: loc.end, msg: msg });
}
compileError(msg, loc, loc2) {
this.addError(msg, loc);
//if (loc2 != null) this.addError(`...`, loc2);
throw new CompileError(msg, loc);
}
dialectError(what, loc) {
this.compileError(`${what} in this dialect of BASIC (${this.opts.dialectName}).`, loc);
}
dialectErrorNoSupport(what, loc) {
this.compileError(`You can't use ${what} in this dialect of BASIC (${this.opts.dialectName}).`, loc); // TODO
}
consumeToken() {
var tok = this.lasttoken = (this.tokens.shift() || this.eol);
return tok;
}
expectToken(str, msg) {
var tok = this.consumeToken();
var tokstr = tok.str;
if (str != tokstr) {
this.compileError(msg || `There should be a "${str}" here.`);
}
return tok;
}
expectTokens(strlist, msg) {
var tok = this.consumeToken();
var tokstr = tok.str;
if (strlist.indexOf(tokstr) < 0) {
this.compileError(msg || `There should be a ${strlist.map((s) => `"${s}"`).join(' or ')} here.`);
}
return tok;
}
peekToken(lookahead) {
var tok = this.tokens[lookahead || 0];
return tok ? tok : this.eol;
}
pushbackToken(tok) {
this.tokens.unshift(tok);
}
// this parses either a line number or "label:" -- or adds a default label to a line
parseOptLabel() {
let tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
if (this.opts.optionalLabels || tok.str == 'OPTION') {
// is it a "label :" and not a keyword like "PRINT : "
if (this.peekToken().str == ':' && !this.supportsCommand(tok.str)) {
this.consumeToken(); // eat the ":"
// fall through to the next case
}
else {
this.pushbackToken(tok); // nope
break;
}
}
else
this.dialectError(`Each line must begin with a line number`);
case TokenType.Int:
this.addLabel(tok.str);
return;
// label added, return from function... other cases add default label
case TokenType.HexOctalInt:
case TokenType.Float:
this.compileError(`Line numbers must be positive integers.`);
break;
case TokenType.Operator:
if (this.supportsCommand(tok.str) && this.validKeyword(tok.str)) {
this.pushbackToken(tok);
break; // "?" is allowed
}
default:
if (this.opts.optionalLabels)
this.compileError(`A line must start with a line number, command, or label.`);
else
this.compileError(`A line must start with a line number.`);
case TokenType.Remark:
break;
}
// add default label
this.addLabel('#' + this.lineno);
}
getPC() {
return this.stmts.length;
}
addStatement(stmt, cmdtok, endtok) {
// set location for statement, adding offset (PC) field
if (endtok == null)
endtok = this.peekToken();
stmt.$loc = { path: cmdtok.$loc.path, line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: endtok.$loc.start,
label: this.curlabel,
offset: this.stmts.length };
// check IF/THEN WHILE/WEND FOR/NEXT etc
this.modifyScope(stmt);
// add to list
this.stmts.push(stmt);
}
addLabel(str, offset) {
if (this.labels[str] != null)
this.compileError(`There's a duplicated label named "${str}".`);
this.labels[str] = this.getPC() + (offset || 0);
this.curlabel = str;
this.tokens.forEach((tok) => tok.$loc.label = str);
}
parseFile(file, path) {
this.path = path;
var txtlines = file.split(/\n|\r\n?/);
txtlines.forEach((line) => this.parseLine(line));
var program = { opts: this.opts, stmts: this.stmts, labels: this.labels };
this.checkAll(program);
this.listings[path] = this.generateListing(file, program);
return program;
}
parseLine(line) {
try {
this.tokenize(line);
this.parse();
}
catch (e) {
if (!(e instanceof CompileError))
throw e; // ignore compile errors since errors[] list captures them
}
}
_tokenize(line) {
// split identifier regex (if token-crunching enabled)
let splitre = this.opts.optionalWhitespace && new RegExp('(' + this.opts.validKeywords.map(s => `${s}`).join('|') + ')');
// iterate over each token via re_toks regex
var lastTokType = TokenType.CatchAll;
var m;
while (m = re_toks.exec(line)) {
for (var i = 1; i <= lastTokType; i++) {
let s = m[i];
if (s != null) {
let loc = { path: this.path, line: this.lineno, start: m.index, end: m.index + s.length };
// maybe we don't support unicode in 1975?
if (this.opts.asciiOnly && !/^[\x00-\x7F]*$/.test(s))
this.dialectErrorNoSupport(`non-ASCII characters`);
// uppercase all identifiers, and maybe more
if (i == TokenType.Ident || i == TokenType.HexOctalInt || this.opts.uppercaseOnly) {
s = s.toUpperCase();
// DATA statement captures whitespace too
if (s == 'DATA')
lastTokType = TokenType.Whitespace;
// certain keywords shouldn't split for rest of line
if (s == 'DATA')
splitre = null;
if (s == 'OPTION')
splitre = null;
// REM means ignore rest of statement
if (lastTokType == TokenType.CatchAll && s.startsWith('REM')) {
s = 'REM';
lastTokType = TokenType.EOL;
}
}
// convert brackets
if (s == '[' || s == ']') {
if (!this.opts.squareBrackets)
this.dialectErrorNoSupport(`square brackets`);
if (s == '[')
s = '(';
if (s == ']')
s = ')';
}
// un-crunch tokens?
if (splitre && i == TokenType.Ident) {
var splittoks = s.split(splitre).filter((s) => s != ''); // only non-empties
if (splittoks.length > 1) {
splittoks.forEach((ss) => {
// check to see if leftover might be integer, or identifier
if (/^[0-9]+$/.test(ss))
i = TokenType.Int;
else if (/^[A-Z_]\w*[$]?$/.test(ss))
i = TokenType.Ident;
else
this.compileError(`Try adding whitespace before "${ss}".`);
this.tokens.push({ str: ss, type: i, $loc: loc });
});
s = null;
}
}
// add token to list
if (s)
this.tokens.push({ str: s, type: i, $loc: loc });
break;
}
}
}
}
tokenize(line) {
this.lineno++;
this.tokens = []; // can't have errors until this is set
this.eol = { type: TokenType.EOL, str: "", $loc: { path: this.path, line: this.lineno, start: line.length } };
if (line.length > this.maxlinelen)
this.compileError(`A line should be no more than ${this.maxlinelen} characters long.`);
this._tokenize(line);
}
parse() {
// not empty line?
if (this.tokens.length) {
this.parseOptLabel();
if (this.tokens.length) {
this.parseCompoundStatement();
}
var next = this.peekToken();
if (!isEOS(next))
this.compileError(`Expected end of line or ':'`, next.$loc);
this.curlabel = null;
}
}
parseCompoundStatement() {
if (this.opts.multipleStmtsPerLine) {
this.parseList(this.parseStatement, ':');
}
else {
this.parseList(this.parseStatement, '\0');
if (this.peekToken().str == ':')
this.dialectErrorNoSupport(`multiple statements on a line`);
}
}
validKeyword(keyword) {
return (this.opts.validKeywords && this.opts.validKeywords.indexOf(keyword) < 0) ? null : keyword;
}
validFunction(funcname) {
return (this.opts.validFunctions && this.opts.validFunctions.indexOf(funcname) < 0) ? null : funcname;
}
supportsCommand(cmd) {
if (cmd == '?')
return this.stmt__PRINT;
else
return this['stmt__' + cmd];
}
parseStatement() {
// eat extra ":" (should have separate property for this)
if (this.opts.optionalWhitespace && this.peekToken().str == ':')
return null;
// get the command word
var cmdtok = this.consumeToken();
var cmd = cmdtok.str;
var stmt;
switch (cmdtok.type) {
case TokenType.Remark:
if (cmdtok.str.startsWith("'") && !this.opts.tickComments)
this.dialectErrorNoSupport(`tick comments`);
return null;
case TokenType.Operator:
// "?" is alias for "PRINT" on some platforms
if (cmd == this.validKeyword('?'))
cmd = 'PRINT';
case TokenType.Ident:
// ignore remarks
if (cmd == 'REM')
return null;
// look for "GO TO" and "GO SUB"
if (cmd == 'GO' && this.peekToken().str == 'TO') {
this.consumeToken();
cmd = 'GOTO';
}
else if (cmd == 'GO' && this.peekToken().str == 'SUB') {
this.consumeToken();
cmd = 'GOSUB';
}
// lookup JS function for command
var fn = this.supportsCommand(cmd);
if (fn) {
if (this.validKeyword(cmd) == null)
this.dialectErrorNoSupport(`the ${cmd} statement`);
stmt = fn.bind(this)();
break;
}
else if (this.peekToken().str == '=' || this.peekToken().str == '(') {
if (!this.opts.optionalLet)
this.dialectError(`Assignments must have a preceding LET`);
// 'A = expr' or 'A(X) = expr'
this.pushbackToken(cmdtok);
stmt = this.stmt__LET();
break;
}
else {
this.compileError(`I don't understand the command "${cmd}".`);
}
case TokenType.EOL:
if (this.opts.optionalWhitespace)
return null;
default:
this.compileError(`There should be a command here.`);
return null;
}
// add statement to list
if (stmt != null)
this.addStatement(stmt, cmdtok);
return stmt;
}
// check scope stuff (if compiledBlocks is true)
modifyScope(stmt) {
if (this.opts.compiledBlocks) {
var cmd = stmt.command;
if (cmd == 'FOR' || cmd == 'WHILE' || cmd == 'SUB') {
this.scopestack.push(this.getPC()); // has to be before adding statment to list
}
else if (cmd == 'NEXT') {
this.popScope(stmt, 'FOR');
}
else if (cmd == 'WEND') {
this.popScope(stmt, 'WHILE');
}
}
}
popScope(close, open) {
var popidx = this.scopestack.pop();
var popstmt = popidx != null ? this.stmts[popidx] : null;
if (popstmt == null)
this.compileError(`There's a ${close.command} without a matching ${open}.`, close.$loc);
else if (popstmt.command != open)
this.compileError(`There's a ${close.command} paired with ${popstmt.command}, but it should be paired with ${open}.`, close.$loc, popstmt.$loc);
else if (close.command == 'NEXT' && !this.opts.optionalNextVar
&& close.lexpr.name != popstmt.lexpr.name)
this.compileError(`This NEXT statement is matched with the wrong FOR variable (${close.lexpr.name}).`, close.$loc, popstmt.$loc);
// set start + end locations
close.startpc = popidx;
popstmt.endpc = this.getPC(); // has to be before adding statment to list
}
popIfThenScope(nextpc) {
var popidx = this.scopestack.pop();
var popstmt = popidx != null ? this.stmts[popidx] : null;
if (popstmt == null)
this.compileError(`There's an END IF without a matching IF or ELSE.`);
if (popstmt.command == 'ELSE') {
popstmt.endpc = this.getPC();
this.popIfThenScope(popidx + 1); // IF goes to ELSE+1
}
else if (popstmt.command == 'IF') {
popstmt.endpc = nextpc != null ? nextpc : this.getPC();
}
else {
this.compileError(`There's an END IF paired with a ${popstmt.command}, not IF or ELSE.`, this.lasttoken.$loc, popstmt.$loc);
}
}
parseVarSubscriptOrFunc() {
var tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
let args = null;
if (this.peekToken().str == '(') {
this.expectToken('(');
args = this.parseExprList();
this.expectToken(')', `There should be another expression or a ")" here.`);
}
var loc = mergeLocs(tok.$loc, this.lasttoken.$loc);
var valtype = this.exprTypeForSubscript(tok.str, args, loc);
return { valtype: valtype, name: tok.str, args: args, $loc: loc };
default:
this.compileError(`There should be a variable name here.`);
break;
}
}
parseLexpr() {
var lexpr = this.parseVarSubscriptOrFunc();
this.vardefs[lexpr.name] = lexpr;
this.validateVarName(lexpr);
return lexpr;
}
parseForNextLexpr() {
var lexpr = this.parseLexpr();
if (lexpr.args || lexpr.name.endsWith('$'))
this.compileError(`A FOR ... NEXT loop can only use numeric variables.`, lexpr.$loc);
return lexpr;
}
parseList(parseFunc, delim) {
var sep;
var list = [];
do {
var el = parseFunc.bind(this)(); // call parse function
if (el != null)
list.push(el); // add parsed element to list
sep = this.consumeToken(); // consume seperator token
} while (sep.str == delim);
this.pushbackToken(sep);
return list;
}
parseLexprList() {
return this.parseList(this.parseLexpr, ',');
}
parseExprList() {
return this.parseList(this.parseExpr, ',');
}
parseLabelList() {
return this.parseList(this.parseLabel, ',');
}
parseLabel() {
// parse full expr?
if (this.opts.computedGoto) {
// parse expression, but still add to list of label targets if constant
var expr = this.parseExpr();
if (isLiteral(expr))
this.targets[expr.value] = this.lasttoken.$loc;
return expr;
}
else {
// parse a single number or ident label
var tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
if (!this.opts.optionalLabels)
this.dialectError(`All labels must be line numbers`);
case TokenType.Int:
var label = tok.str;
this.targets[label] = tok.$loc;
return { valtype: 'label', value: label };
default:
var what = this.opts.optionalLabels ? "label or line number" : "line number";
this.compileError(`There should be a ${what} here.`);
}
}
}
parseDatumList() {
return this.parseList(this.parseDatum, ',');
}
parseDatum() {
var tok = this.consumeToken();
// get rid of leading whitespace
while (tok.type == TokenType.Whitespace)
tok = this.consumeToken();
if (isEOS(tok))
this.compileError(`There should be a datum here.`);
// parse constants
if (tok.type <= TokenType.HexOctalInt) {
return this.parseValue(tok);
}
if (tok.str == '-' && this.peekToken().type <= TokenType.HexOctalInt) {
tok = this.consumeToken();
return { valtype: 'number', value: -this.parseValue(tok).value };
}
if (tok.str == '+' && this.peekToken().type <= TokenType.HexOctalInt) {
tok = this.consumeToken();
return this.parseValue(tok);
}
// concat all stuff including whitespace
// TODO: should trim whitespace only if not quoted string
var s = '';
while (!isEOS(tok) && tok.str != ',') {
s += this.parseValue(tok).value;
tok = this.consumeToken();
}
this.pushbackToken(tok);
return { valtype: 'string', value: s }; // trim leading and trailing whitespace
}
parseValue(tok) {
switch (tok.type) {
case TokenType.HexOctalInt:
if (!this.opts.hexOctalConsts)
this.dialectErrorNoSupport(`hex/octal constants`);
let base = tok.str.startsWith('H') ? 16 : 8;
return { valtype: 'number', value: parseInt(tok.str.substr(1), base) };
case TokenType.Int:
case TokenType.Float:
return { valtype: 'number', value: this.parseNumber(tok.str) };
case TokenType.String:
return { valtype: 'string', value: stripQuotes(tok.str) };
default:
return { valtype: 'string', value: tok.str }; // only used in DATA statement
}
}
parsePrimary() {
let tok = this.consumeToken();
switch (tok.type) {
case TokenType.HexOctalInt:
case TokenType.Int:
case TokenType.Float:
case TokenType.String:
return this.parseValue(tok);
case TokenType.Ident:
if (tok.str == 'NOT') {
let expr = this.parsePrimary();
return { valtype: 'number', op: this.opts.bitwiseLogic ? 'bnot' : 'lnot', expr: expr };
}
else {
this.pushbackToken(tok);
return this.parseVarSubscriptOrFunc();
}
case TokenType.Operator:
if (tok.str == '(') {
let expr = this.parseExpr();
this.expectToken(')', `There should be another expression or a ")" here.`);
return expr;
}
else if (tok.str == '-') {
let expr = this.parsePrimary(); // TODO: -2^2=-4 and -2-2=-4
return { valtype: 'number', op: 'neg', expr: expr };
}
else if (tok.str == '+') {
return this.parsePrimary(); // ignore unary +
}
default:
this.compileError(`The expression is incomplete.`);
return;
}
}
parseNumber(str) {
var n = parseFloat(str);
if (isNaN(n))
this.compileError(`The number ${str} is not a valid floating-point number.`);
if (this.opts.checkOverflow && !isFinite(n))
this.compileError(`The number ${str} is too big to fit into a floating-point value.`);
return n;
}
parseExpr1(left, minPred) {
let look = this.peekToken();
while (getPrecedence(look) >= minPred) {
let op = this.consumeToken();
if (this.opts.validOperators && this.opts.validOperators.indexOf(op.str) < 0)
this.dialectErrorNoSupport(`the "${op.str}" operator`);
let right = this.parsePrimary();
look = this.peekToken();
while (getPrecedence(look) > getPrecedence(op)) {
right = this.parseExpr1(right, getPrecedence(look));
look = this.peekToken();
}
var opfn = getOperator(op.str).f;
// use logical operators instead of bitwise?
if (!this.opts.bitwiseLogic && op.str == 'AND')
opfn = 'land';
if (!this.opts.bitwiseLogic && op.str == 'OR')
opfn = 'lor';
var valtype = this.exprTypeForOp(opfn, left, right, op);
left = { valtype: valtype, op: opfn, left: left, right: right };
}
return left;
}
parseExpr() {
var startloc = this.peekToken().$loc;
var expr = this.parseExpr1(this.parsePrimary(), 0);
var endloc = this.lasttoken.$loc;
expr.$loc = mergeLocs(startloc, endloc);
return expr;
}
parseExprWithType(expecttype) {
var expr = this.parseExpr();
if (expr.valtype != expecttype)
this.compileError(`There should be a ${expecttype} here, but this expression evaluates to a ${expr.valtype}.`, expr.$loc);
return expr;
}
validateVarName(lexpr) {
switch (this.opts.varNaming) {
case 'A': // TINY BASIC, no strings
if (!/^[A-Z]$/i.test(lexpr.name))
this.dialectErrorNoSupport(`variable names other than a single letter`);
break;
case 'A1':
if (lexpr.args == null && !/^[A-Z][0-9]?[$]?$/i.test(lexpr.name))
this.dialectErrorNoSupport(`variable names other than a letter followed by an optional digit`);
if (lexpr.args != null && !/^[A-Z]?[$]?$/i.test(lexpr.name))
this.dialectErrorNoSupport(`array names other than a single letter`);
break;
case 'A1$':
if (!/^[A-Z][0-9]?[$]?$/i.test(lexpr.name))
this.dialectErrorNoSupport(`variable names other than a letter followed by an optional digit`);
break;
case 'AA':
if (lexpr.args == null && !/^[A-Z][A-Z0-9]?[$]?$/i.test(lexpr.name))
this.dialectErrorNoSupport(`variable names other than a letter followed by an optional letter or digit`);
break;
case '*':
break;
}
}
visitExpr(expr, callback) {
if (isBinOp(expr)) {
this.visitExpr(expr.left, callback);
this.visitExpr(expr.right, callback);
}
if (isUnOp(expr)) {
this.visitExpr(expr.expr, callback);
}
if (isLookup(expr) && expr.args != null) {
for (var arg of expr.args)
this.visitExpr(arg, callback);
}
callback(expr);
}
// type-checking
exprTypeForOp(fnname, left, right, optok) {
if (left.valtype == 'string' || right.valtype == 'string') {
if (fnname == 'add') {
if (this.opts.stringConcat)
return 'string'; // concat strings
else
this.dialectErrorNoSupport(`the "+" operator to concatenate strings`, optok.$loc);
}
else if (fnname.length != 2) // only relops are 2 chars long!
this.compileError(`You can't do math on strings until they're converted to numbers.`, optok.$loc);
}
return 'number';
}
exprTypeForSubscript(fnname, args, loc) {
args = args || [];
// first check the built-in functions
var defs = BUILTIN_MAP[fnname];
if (defs != null) {
if (!this.validFunction(fnname))
this.dialectErrorNoSupport(`the ${fnname} function`, loc);
for (var def of defs) {
if (args.length == def.args.length)
return def.result; // TODO: check arg types
}
// TODO: check func arg types
this.compileError(`The ${fnname} function takes ${def.args.length} arguments, but ${args.length} are given.`, loc);
}
// no function found, assume it's an array ref
// TODO: validateVarName() later?
this.varrefs[fnname] = loc;
return fnname.endsWith('$') ? 'string' : 'number';
}
//// STATEMENTS
stmt__LET() {
var lexprs = [this.parseLexpr()];
this.expectToken("=");
// look for A=B=expr (TODO: doesn't work on arrays)
while (this.opts.chainAssignments && this.peekToken().type == TokenType.Ident && this.peekToken(1).str == '=') {
lexprs.push(this.parseLexpr());
this.expectToken("=");
}
var right = this.parseExprWithType(lexprs[0].valtype);
return { command: "LET", lexprs: lexprs, right: right };
}
stmt__PRINT() {
var sep, lastsep;
var list = [];
do {
sep = this.peekToken();
if (isEOS(sep)) {
break;
}
else if (sep.str == ';') {
this.consumeToken();
lastsep = sep;
}
else if (sep.str == ',') {
this.consumeToken();
list.push({ value: '\t' });
lastsep = sep;
}
else {
list.push(this.parseExpr());
lastsep = null;
}
} while (true);
if (!(lastsep && (lastsep.str == ';' || sep.str != ','))) {
list.push({ value: '\n' });
}
return { command: "PRINT", args: list };
}
stmt__GOTO() {
return this.__GO("GOTO");
}
stmt__GOSUB() {
return this.__GO("GOSUB");
}
__GO(cmd) {
var expr = this.parseLabel();
// GOTO (expr) OF (labels...)
if (this.peekToken().str == this.validKeyword('OF')) {
this.expectToken('OF');
let newcmd = (cmd == 'GOTO') ? 'ONGOTO' : 'ONGOSUB';
return { command: newcmd, expr: expr, labels: this.parseLabelList() };
}
else {
// regular GOTO or GOSUB
return { command: cmd, label: expr };
}
}
stmt__IF() {
var cmdtok = this.lasttoken;
var cond = this.parseExprWithType("number");
var ifstmt = { command: "IF", cond: cond };
this.addStatement(ifstmt, cmdtok);
// we accept GOTO or THEN if line number provided (DEC accepts GO TO)
var thengoto = this.expectTokens(['THEN', 'GOTO', 'GO']);
if (thengoto.str == 'GO')
this.expectToken('TO');
// multiline IF .. THEN? push it to scope stack
if (this.opts.multilineIfThen && isEOS(this.peekToken())) {
this.scopestack.push(this.getPC() - 1); // we already added stmt to list, so - 1
}
else {
// parse line number or statement clause
this.parseGotoOrStatements();
// is the next statement an ELSE?
// gotta parse it now because it's an end-of-statement token
if (this.peekToken().str == 'ELSE') {
this.expectToken('ELSE');
ifstmt.endpc = this.getPC() + 1;
this.stmt__ELSE();
}
else {
ifstmt.endpc = this.getPC();
}
}
}
stmt__ELSE() {
var elsestmt = { command: "ELSE" };
this.addStatement(elsestmt, this.lasttoken);
// multiline ELSE? or ELSE IF?
var nexttok = this.peekToken();
if (this.opts.multilineIfThen && isEOS(nexttok)) {
this.scopestack.push(this.getPC() - 1); // we already added stmt to list, so - 1
}
else if (this.opts.multilineIfThen && nexttok.str == 'IF') {
this.scopestack.push(this.getPC() - 1); // we already added stmt to list, so - 1
this.parseGotoOrStatements();
this.elseifcount++;
}
else {
// parse line number or statement clause
this.parseGotoOrStatements();
elsestmt.endpc = this.getPC();
}
}
parseGotoOrStatements() {
var lineno = this.peekToken();
// assume GOTO if number given after THEN
if (lineno.type == TokenType.Int) {
this.parseLabel();
var gotostmt = { command: 'GOTO', label: { valtype: 'label', value: lineno.str } };
this.addStatement(gotostmt, lineno);
}
else {
// parse rest of IF clause
this.parseCompoundStatement();
}
}
stmt__FOR() {
var lexpr = this.parseForNextLexpr();
this.expectToken('=');
var init = this.parseExprWithType("number");
this.expectToken('TO');
var targ = this.parseExprWithType("number");
if (this.peekToken().str == 'STEP') {
this.consumeToken();
var step = this.parseExprWithType("number");
}
return { command: 'FOR', lexpr: lexpr, initial: init, target: targ, step: step };
}
stmt__NEXT() {
var lexpr = null;
// NEXT var might be optional
if (!this.opts.optionalNextVar || !isEOS(this.peekToken())) {
lexpr = this.parseForNextLexpr();
// convert ',' to ':' 'NEXT'
if (this.opts.multipleNextVars && this.peekToken().str == ',') {
this.consumeToken(); // consume ','
this.tokens.unshift({ type: TokenType.Ident, str: 'NEXT', $loc: this.peekToken().$loc });
this.tokens.unshift({ type: TokenType.Operator, str: ':', $loc: this.peekToken().$loc });
}
}
return { command: 'NEXT', lexpr: lexpr };
}
stmt__WHILE() {
var cond = this.parseExprWithType("number");
return { command: 'WHILE', cond: cond };
}
stmt__WEND() {
return { command: 'WEND' };
}
stmt__DIM() {
var lexprs = this.parseLexprList();
lexprs.forEach((arr) => {
if (arr.args == null || arr.args.length == 0)
this.compileError(`An array defined by DIM must have at least one dimension.`);
else if (arr.args.length > this.opts.maxDimensions)
this.dialectErrorNoSupport(`arrays with more than ${this.opts.maxDimensions} dimensionals`);
for (var arrdim of arr.args) {
if (arrdim.valtype != 'number')
this.compileError(`Array dimensions must be numeric.`, arrdim.$loc);
if (isLiteral(arrdim) && typeof arrdim.value === 'number' && arrdim.value < this.opts.defaultArrayBase)
this.compileError(`An array dimension cannot be less than ${this.opts.defaultArrayBase}.`, arrdim.$loc);
}
});
return { command: 'DIM', args: lexprs };
}
stmt__INPUT() {
var prompt = this.consumeToken();
var promptstr;
if (prompt.type == TokenType.String) {
this.expectTokens([';', ',']);
promptstr = stripQuotes(prompt.str);
}
else {
this.pushbackToken(prompt);
promptstr = "";
}
return { command: 'INPUT', prompt: { valtype: 'string', value: promptstr }, args: this.parseLexprList() };
}
/* for HP BASIC only */
stmt__ENTER() {
var timeout = this.parseExpr();
this.expectToken(',');
var elapsed = this.parseLexpr(); // TODO: this has to go somewheres
this.expectToken(',');
return { command: 'INPUT', prompt: null, args: this.parseLexprList(), timeout: timeout, elapsed: elapsed };
}
// TODO: DATA statement doesn't read unquoted strings
stmt__DATA() {
return { command: 'DATA', datums: this.parseDatumList() };
}
stmt__READ() {
return { command: 'READ', args: this.parseLexprList() };
}
stmt__RESTORE() {
var label = null;
if (this.opts.restoreWithLabel && !isEOS(this.peekToken()))
label = this.parseLabel();
return { command: 'RESTORE', label: label };
}
stmt__RETURN() {
return { command: 'RETURN' };
}
stmt__STOP() {
return { command: 'STOP' };
}
stmt__END() {
if (this.opts.multilineIfThen && this.scopestack.length) {
let endtok = this.expectTokens(['IF', 'SUB']);
if (endtok.str == 'IF') {
this.popIfThenScope();
while (this.elseifcount--)
this.popIfThenScope(); // pop additional ELSE IF blocks?
this.elseifcount = 0;
}
else if (endtok.str == 'SUB') {
this.addStatement({ command: 'RETURN' }, endtok);
this.popScope({ command: 'END' }, 'SUB'); // fake command to avoid null
}
}
else {
return { command: 'END' };
}
}
stmt__ON() {
var expr = this.parseExprWithType("number");
var gotok = this.consumeToken();
var cmd = { GOTO: 'ONGOTO', THEN: 'ONGOTO', GOSUB: 'ONGOSUB' }[gotok.str]; // THEN only for DEC basic?
if (!cmd)
this.compileError(`There should be a GOTO or GOSUB here.`);
var labels = this.parseLabelList();
return { command: cmd, expr: expr, labels: labels };
}
stmt__DEF() {
var lexpr = this.parseVarSubscriptOrFunc(); // TODO: only allow parameter names, not exprs
if (lexpr.args && lexpr.args.length > this.opts.maxDefArgs)
this.compileError(`There can be no more than ${this.opts.maxDefArgs} arguments to a function or subscript.`, lexpr.$loc);
if (!lexpr.name.startsWith('FN'))
this.compileError(`Functions defined with DEF must begin with the letters "FN".`, lexpr.$loc);
this.markVarDefs(lexpr); // local variables need to be marked as referenced (TODO: only for this scope)
this.expectToken("=");
var func = this.parseExpr();
// build call graph to detect cycles
this.visitExpr(func, (expr) => {
if (isLookup(expr) && expr.name.startsWith('FN')) {
if (!this.fnrefs[lexpr.name])
this.fnrefs[lexpr.name] = [];
this.fnrefs[lexpr.name].push(expr.name);
}
});
this.checkCallGraph(lexpr.name, new Set());
return { command: 'DEF', lexpr: lexpr, def: func };
}
stmt__SUB() {
var lexpr = this.parseVarSubscriptOrFunc(); // TODO: only allow parameter names, not exprs
this.markVarDefs(lexpr); // local variables need to be marked as referenced (TODO: only for this scope)
this.addLabel(lexpr.name, 1); // offset +1 to skip SUB command
return { command: 'SUB', lexpr: lexpr };
}
stmt__CALL() {
return { command: 'CALL', call: this.parseVarSubscriptOrFunc() };
}
markVarDefs(lexpr) {
this.vardefs[lexpr.name] = lexpr;
if (lexpr.args != null)
for (let arg of lexpr.args) {
if (isLookup(arg) && arg.args == null)
this.vardefs[arg.name] = arg;
else
this.compileError(`A definition can only define symbols, not expressions.`);
}
}
// detect cycles in call graph starting at function 'name'
checkCallGraph(name, visited) {
if (visited.has(name))
this.compileError(`There was a cycle in the function definition graph for ${name}.`);
visited.add(name);
var refs = this.fnrefs[name] || [];
for (var ref of refs)
this.checkCallGraph(ref, visited); // recurse
visited.delete(name);
}
stmt__POP() {
return { command: 'POP' };
}
stmt__GET() {
var lexpr = this.parseLexpr();
return { command: 'GET', lexpr: lexpr };
}
stmt__CLEAR() {
return { command: 'CLEAR' };
}
stmt__RANDOMIZE() {
return { command: 'RANDOMIZE' };
}
stmt__CHANGE() {
var src = this.parseExpr();
this.expectToken('TO');
var dest = this.parseLexpr();
if (dest.valtype == src.valtype)
this.compileError(`CHANGE can only convert strings to numeric arrays, or vice-versa.`, mergeLocs(src.$loc, dest.$loc));
return { command: 'CHANGE', src: src, dest: dest };
}
stmt__CONVERT() {
var src = this.parseExpr();
this.expectToken('TO');
var dest = this.parseLexpr();
if (dest.valtype == src.valtype)
this.compileError(`CONVERT can only convert strings to numbers, or vice-versa.`, mergeLocs(src.$loc, dest.$loc));
return { command: 'CONVERT', src: src, dest: dest };
}
// TODO: CHANGE A TO A$ (4th edition, A(0) is len and A(1..) are chars)
stmt__OPTION() {
this.optionCount++;
var tokname = this.consumeToken();
var optname = tokname.str.toUpperCase();
if (tokname.type != TokenType.Ident)
this.compileError(`There must be a name after the OPTION statement.`);
var tokarg = this.consumeToken();
var arg = tokarg.str.toUpperCase();
switch (optname) {
case 'DIALECT':
if (this.optionCount > 1)
this.compileError(`OPTION DIALECT must be the first OPTION statement in the file.`, tokname.$loc);
let dname = arg || "";
if (dname == "")
this.compileError(`OPTION DIALECT requires a dialect name.`, tokname.$loc);
let dialect = exports.DIALECTS[dname.toUpperCase()];
if (dialect)
this.opts = dialect;
else
this.compileError(`${dname} is not a valid dialect.`);
break;
case 'BASE':
let base = parseInt(arg);
if (base == 0 || base == 1)
this.opts.defaultArrayBase = base;
else
this.compileError("OPTION BASE can only be 0 or 1.");
break;
case 'CPUSPEED':
if (!(this.opts.commandsPerSec = Math.min(1e7, arg == 'MAX' ? Infinity : parseFloat(arg))))
this.compileError(`OPTION CPUSPEED takes a positive number or MAX.`);
break;
default:
// maybe it's one of the options?
let propname = Object.getOwnPropertyNames(this.opts).find((n) => n.toUpperCase() == optname);
if (propname == null)
this.compileError(`${optname} is not a valid option.`, tokname.$loc);
if (arg == null)
this.compileError(`OPTION ${optname} requires a parameter.`);
switch (typeof this.opts[propname]) {
case 'boolean':
this.opts[propname] = arg.toUpperCase().startsWith("T") || arg > 0;
return;
case 'number':
this.opts[propname] = parseFloat(arg);
return;
case 'string':
this.opts[propname] = arg;
return;
case 'object':
if (Array.isArray(this.opts[propname]) && arg == 'ALL') {
this.opts[propname] = null;
return;
}
this.compileError(`OPTION ${optname} ALL is the only option supported.`);
}
break;
}
return { command: 'OPTION', optname: optname, optargs: [arg] };
}
// for workermain
generateListing(file, program) {
var srclines = [];
var laststmt;
program.stmts.forEach((stmt, idx) => {
laststmt = stmt;
srclines.push(stmt.$loc);
});
if (this.opts.endStmtRequired && (laststmt == null || laststmt.command != 'END'))
this.dialectError(`All programs must have a final END statement`);
return { lines: srclines };
}
getListings() {
return this.listings;
}
// LINT STUFF
checkAll(program) {
this.checkLabels();
this.checkScopes();
this.checkVarRefs();
}
checkLabels() {
for (let targ in this.targets) {
if (this.labels[targ] == null) {
var what = this.opts.optionalLabels && isNaN(parseInt(targ)) ? "label named" : "line number";
this.addError(`There isn't a ${what} ${targ}.`, this.targets[targ]);
}
}
}
checkScopes() {
if (this.opts.compiledBlocks && this.scopestack.length) {
var open = this.stmts[this.scopestack.pop()];
var close = { FOR: "NEXT", WHILE: "WEND", IF: "END IF", SUB: "END SUB" };
this.compileError(`Don't forget to add a matching ${close[open.command]} statement.`, open.$loc);
}
}
checkVarRefs() {
if (!this.opts.defaultValues) {
for (var varname in this.varrefs) {
if (this.vardefs[varname] == null)
this.compileError(`The variable ${varname} isn't defined anywhere in the program.`, this.varrefs[varname]);
}
}
}
}
exports.BASICParser = BASICParser;
///// BASIC DIALECTS
exports.ECMA55_MINIMAL = {
dialectName: "ECMA55",
asciiOnly: true,
uppercaseOnly: true,
optionalLabels: false,
optionalWhitespace: false,
multipleStmtsPerLine: false,
varNaming: "A1",
staticArrays: true,
sharedArrayNamespace: true,
defaultArrayBase: 0,
defaultArraySize: 11,
defaultValues: false,
stringConcat: false,
maxDimensions: 2,
maxDefArgs: 255,
maxStringLength: 255,
tickComments: false,
hexOctalConsts: false,
validKeywords: [
'BASE', 'DATA', 'DEF', 'DIM', 'END',
'FOR', 'GO', 'GOSUB', 'GOTO', 'IF', 'INPUT', 'LET', 'NEXT', 'ON', 'OPTION', 'PRINT',
'RANDOMIZE', 'READ', 'REM', 'RESTORE', 'RETURN', 'STEP', 'STOP', 'THEN', 'TO' // 'SUB'
],
validFunctions: [
'ABS', 'ATN', 'COS', 'EXP', 'INT', 'LOG', 'RND', 'SGN', 'SIN', 'SQR', 'TAB', 'TAN'
],
validOperators: [
'=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'
],
printZoneLength: 15,
numericPadding: true,
checkOverflow: true,
testInitialFor: true,
optionalNextVar: false,
multipleNextVars: false,
bitwiseLogic: false,
checkOnGotoIndex: true,
computedGoto: false,
restoreWithLabel: false,
squareBrackets: false,
arraysContainChars: false,
endStmtRequired: true,
chainAssignments: false,
optionalLet: false,
compiledBlocks: true,
};
exports.DARTMOUTH_4TH_EDITION = {
dialectName: "DARTMOUTH4",
asciiOnly: true,
uppercaseOnly: true,
optionalLabels: false,
optionalWhitespace: false,
multipleStmtsPerLine: false,
varNaming: "A1",
staticArrays: true,
sharedArrayNamespace: false,
defaultArrayBase: 0,
defaultArraySize: 11,
defaultValues: false,
stringConcat: false,
maxDimensions: 2,
maxDefArgs: 255,
maxStringLength: 255,
tickComments: true,
hexOctalConsts: false,
validKeywords: [
'BASE', 'DATA', 'DEF', 'DIM', 'END',
'FOR', 'GO', 'GOSUB', 'GOTO', 'IF', 'INPUT', 'LET', 'NEXT', 'ON', 'OPTION', 'PRINT',
'RANDOMIZE', 'READ', 'REM', 'RESTORE', 'RETURN', 'STEP', 'STOP', 'THEN', 'TO',
'CHANGE', 'MAT', 'RANDOM', 'RESTORE$', 'RESTORE*',
],
validFunctions: [
'ABS', 'ATN', 'COS', 'EXP', 'INT', 'LOG', 'RND', 'SGN', 'SIN', 'SQR', 'TAB', 'TAN',
'TRN', 'INV', 'DET', 'NUM', 'ZER', // NUM = # of strings input for MAT INPUT
],
validOperators: [
'=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'
],
printZoneLength: 15,
numericPadding: true,
checkOverflow: true,
testInitialFor: true,
optionalNextVar: false,
multipleNextVars: false,
bitwiseLogic: false,
checkOnGotoIndex: true,
computedGoto: false,
restoreWithLabel: false,
squareBrackets: false,
arraysContainChars: false,
endStmtRequired: true,
chainAssignments: true,
optionalLet: false,
compiledBlocks: true,
};
// TODO: only integers supported
exports.TINY_BASIC = {
dialectName: "TINY",
asciiOnly: true,
uppercaseOnly: true,
optionalLabels: false,
optionalWhitespace: false,
multipleStmtsPerLine: false,
varNaming: "A",
staticArrays: false,
sharedArrayNamespace: true,
defaultArrayBase: 0,
defaultArraySize: 0,
defaultValues: true,
stringConcat: false,
maxDimensions: 0,
maxDefArgs: 255,
maxStringLength: 255,
tickComments: false,
hexOctalConsts: false,
validKeywords: [
'OPTION',
'PRINT', 'IF', 'THEN', 'GOTO', 'INPUT', 'LET', 'GOSUB', 'RETURN', 'CLEAR', 'END'
],
validFunctions: [],
validOperators: [
'=', '<>', '><', '<', '>', '<=', '>=', '+', '-', '*', '/',
],
printZoneLength: 1,
numericPadding: false,
checkOverflow: false,
testInitialFor: false,
optionalNextVar: false,
multipleNextVars: false,
bitwiseLogic: false,
checkOnGotoIndex: false,
computedGoto: true,
restoreWithLabel: false,
squareBrackets: false,
arraysContainChars: false,
endStmtRequired: false,
chainAssignments: false,
optionalLet: false,
compiledBlocks: false,
};
exports.HP_TIMESHARED_BASIC = {
dialectName: "HP2000",
asciiOnly: true,
uppercaseOnly: true,
optionalLabels: false,
optionalWhitespace: false,
multipleStmtsPerLine: true,
varNaming: "A1$",
staticArrays: true,
sharedArrayNamespace: false,
defaultArrayBase: 1,
defaultArraySize: 11,
defaultValues: false,
stringConcat: false,
maxDimensions: 2,
maxDefArgs: 255,
maxStringLength: 255,
tickComments: false,
hexOctalConsts: false,
validKeywords: [
'BASE', 'DATA', 'DEF', 'DIM', 'END',
'FOR', 'GO', 'GOSUB', 'GOTO', 'IF', 'INPUT', 'LET', 'NEXT', 'OPTION', 'PRINT',
'RANDOMIZE', 'READ', 'REM', 'RESTORE', 'RETURN', 'STEP', 'STOP', 'THEN', 'TO',
'ENTER', 'MAT', 'CONVERT', 'OF', 'IMAGE', 'USING'
],
validFunctions: [
'ABS', 'ATN', 'BRK', 'COS', 'CTL', 'EXP', 'INT', 'LEN', 'LIN', 'LOG', 'NUM',
'POS', 'RND', 'SGN', 'SIN', 'SPA', 'SQR', 'TAB', 'TAN', 'TIM', 'TYP', 'UPS$',
'NFORMAT$', // non-standard, substitute for PRINT USING
],
validOperators: [
'=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^',
'**', '#', 'NOT', 'AND', 'OR', 'MIN', 'MAX',
],
printZoneLength: 15,
numericPadding: true,
checkOverflow: false,
testInitialFor: true,
optionalNextVar: false,
multipleNextVars: false,
bitwiseLogic: false,
checkOnGotoIndex: false,
computedGoto: true,
restoreWithLabel: true,
squareBrackets: true,
arraysContainChars: true,
endStmtRequired: true,
chainAssignments: true,
optionalLet: true,
compiledBlocks: true,
maxArrayElements: 5000,
// TODO: max line number
};
exports.DEC_BASIC_11 = {
dialectName: "DEC11",
asciiOnly: true,
uppercaseOnly: true,
optionalLabels: false,
optionalWhitespace: false,
multipleStmtsPerLine: false,
varNaming: "A1",
staticArrays: true,
sharedArrayNamespace: false,
defaultArrayBase: 0,
defaultArraySize: 11,
defaultValues: true,
stringConcat: true,
maxDimensions: 2,
maxDefArgs: 255,
maxStringLength: 255,
tickComments: false,
hexOctalConsts: false,
validKeywords: [
'OPTION',
'DATA', 'DEF', 'DIM', 'END', 'FOR', 'STEP', 'GOSUB', 'GOTO', 'GO', 'TO',
'IF', 'THEN', 'INPUT', 'LET', 'NEXT', 'ON', 'PRINT', 'RANDOMIZE',
'READ', 'REM', 'RESET', 'RESTORE', 'RETURN', 'STOP',
],
validFunctions: [
'ABS', 'ATN', 'COS', 'EXP', 'INT', 'LOG', 'LOG10', 'PI', 'RND', 'SGN', 'SIN', 'SQR', 'TAB',
'ASC', 'BIN', 'CHR$', 'CLK$', 'DAT$', 'LEN', 'OCT', 'POS', 'SEG$', 'STR$', 'TRM$', 'VAL',
'NFORMAT$', // non-standard, substitute for PRINT USING
],
validOperators: [
'=', '<>', '><', '<', '>', '<=', '>=', '+', '-', '*', '/', '^',
],
printZoneLength: 14,
numericPadding: true,
checkOverflow: true,
testInitialFor: true,
optionalNextVar: false,
multipleNextVars: false,
bitwiseLogic: false,
checkOnGotoIndex: true,
computedGoto: false,
restoreWithLabel: false,
squareBrackets: false,
arraysContainChars: false,
endStmtRequired: false,
chainAssignments: false,
optionalLet: true,
compiledBlocks: true,
// TODO: max line number 32767
// TODO: \ separator, % int vars and constants, 'single' quoted
// TODO: can't compare strings and numbers
};
exports.DEC_BASIC_PLUS = {
dialectName: "DECPLUS",
asciiOnly: true,
uppercaseOnly: false,
optionalLabels: false,
optionalWhitespace: false,
multipleStmtsPerLine: true,
varNaming: "A1",
staticArrays: true,
sharedArrayNamespace: false,
defaultArrayBase: 0,
defaultArraySize: 11,
defaultValues: true,
stringConcat: true,
maxDimensions: 2,
maxDefArgs: 255,
maxStringLength: 255,
tickComments: true,
hexOctalConsts: false,
validKeywords: [
'OPTION',
'REM', 'LET', 'DIM', 'RANDOM', 'RANDOMIZE', 'IF', 'THEN', 'ELSE',
'FOR', 'TO', 'STEP', 'WHILE', 'UNTIL', 'NEXT', 'DEF', 'ON', 'GOTO', 'GOSUB',
'RETURN', 'CHANGE', 'READ', 'DATA', 'RESTORE', 'PRINT', 'USING',
'INPUT', 'LINE', 'NAME', 'AS', 'ERROR', 'RESUME', 'CHAIN', 'STOP', 'END',
'MAT', 'UNLESS', 'SLEEP', 'WAIT',
],
validFunctions: [
'ABS', 'ATN', 'COS', 'EXP', 'INT', 'LOG', 'LOG10', 'PI', 'RND', 'SGN', 'SIN', 'SQR', 'TAB', 'TAN',
'POS', 'TAB', 'ASCII', 'CHR$', 'CVT%$', 'CVTF$', 'CVT$%', 'CVT$F',
'LEFT$', 'RIGHT$', 'MID$', 'LEN', 'INSTR', 'SPACE$', 'NUM$', 'VAL', 'XLATE',
'DATE$', 'TIME$', 'TIME', 'ERR', 'ERL', 'SWAP%', 'RAD$',
'NFORMAT$', // non-standard, substitute for PRINT USING
],
validOperators: [
'=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^',
'**', '==',
'NOT', 'AND', 'OR', 'XOR', 'IMP', 'EQV',
],
printZoneLength: 14,
numericPadding: true,
checkOverflow: true,
testInitialFor: true,
optionalNextVar: false,
multipleNextVars: false,
bitwiseLogic: false,
checkOnGotoIndex: true,
computedGoto: false,
restoreWithLabel: false,
squareBrackets: false,
arraysContainChars: false,
endStmtRequired: false,
chainAssignments: false,
optionalLet: true,
compiledBlocks: true,
// TODO: max line number 32767
// TODO: \ separator, % int vars and constants, 'single' quoted
// TODO: can't compare strings and numbers
// TODO: WHILE/UNTIL/FOR extra statements, etc
};
exports.BASICODE = {
dialectName: "BASICODE",
asciiOnly: true,
uppercaseOnly: false,
optionalLabels: false,
optionalWhitespace: true,
multipleStmtsPerLine: true,
varNaming: "AA",
staticArrays: true,
sharedArrayNamespace: false,
defaultArrayBase: 0,
defaultArraySize: 11,
defaultValues: false,
stringConcat: true,
maxDimensions: 2,
maxDefArgs: 255,
maxStringLength: 255,
tickComments: false,
hexOctalConsts: false,
validKeywords: [
'BASE', 'DATA', 'DEF', 'DIM', 'END',
'FOR', 'GO', 'GOSUB', 'GOTO', 'IF', 'INPUT', 'LET', 'NEXT', 'ON', 'OPTION', 'PRINT',
'READ', 'REM', 'RESTORE', 'RETURN', 'STEP', 'STOP', 'THEN', 'TO',
'AND', 'NOT', 'OR'
],
validFunctions: [
'ABS', 'ASC', 'ATN', 'CHR$', 'COS', 'EXP', 'INT', 'LEFT$', 'LEN', 'LOG',
'MID$', 'RIGHT$', 'SGN', 'SIN', 'SQR', 'TAB', 'TAN', 'VAL'
],
validOperators: [
'=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^', 'AND', 'NOT', 'OR'
],
printZoneLength: 15,
numericPadding: true,
checkOverflow: true,
testInitialFor: true,
optionalNextVar: false,
multipleNextVars: false,
bitwiseLogic: false,
checkOnGotoIndex: true,
computedGoto: false,
restoreWithLabel: false,
squareBrackets: false,
arraysContainChars: false,
endStmtRequired: false,
chainAssignments: false,
optionalLet: true,
compiledBlocks: false,
};
exports.ALTAIR_BASIC41 = {
dialectName: "ALTAIR41",
asciiOnly: true,
uppercaseOnly: true,
optionalLabels: false,
optionalWhitespace: true,
multipleStmtsPerLine: true,
varNaming: "*",
staticArrays: false,
sharedArrayNamespace: true,
defaultArrayBase: 0,
defaultArraySize: 11,
defaultValues: true,
stringConcat: true,
maxDimensions: 128,
maxDefArgs: 255,
maxStringLength: 255,
tickComments: false,
hexOctalConsts: false,
validKeywords: [
'OPTION',
'CONSOLE', 'DATA', 'DEF', 'DEFUSR', 'DIM', 'END', 'ERASE', 'ERROR',
'FOR', 'GOTO', 'GOSUB', 'IF', 'THEN', 'ELSE', 'INPUT', 'LET', 'LINE',
'PRINT', 'LPRINT', 'USING', 'NEXT', 'ON', 'OUT', 'POKE',
'READ', 'REM', 'RESTORE', 'RESUME', 'RETURN', 'STOP', 'SWAP',
'TROFF', 'TRON', 'WAIT',
'TO', 'STEP',
'AND', 'NOT', 'OR', 'XOR', 'IMP', 'EQV', 'MOD',
'RANDOMIZE' // not in Altair BASIC, but we add it anyway
],
validFunctions: [
'ABS', 'ASC', 'ATN', 'CDBL', 'CHR$', 'CINT', 'COS', 'ERL', 'ERR',
'EXP', 'FIX', 'FRE', 'HEX$', 'INP', 'INSTR', 'INT',
'LEFT$', 'LEN', 'LOG', 'LPOS', 'MID$',
'OCT$', 'POS', 'RIGHT$', 'RND', 'SGN', 'SIN', 'SPACE$', 'SPC',
'SQR', 'STR$', 'STRING$', 'TAB', 'TAN', 'USR', 'VAL', 'VARPTR'
],
validOperators: [
'=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^', '\\',
'AND', 'NOT', 'OR', 'XOR', 'IMP', 'EQV', 'MOD'
],
printZoneLength: 15,
numericPadding: true,
checkOverflow: true,
testInitialFor: false,
optionalNextVar: true,
multipleNextVars: true,
bitwiseLogic: true,
checkOnGotoIndex: false,
computedGoto: false,
restoreWithLabel: false,
squareBrackets: false,
arraysContainChars: false,
endStmtRequired: false,
chainAssignments: false,
optionalLet: true,
compiledBlocks: false,
};
exports.APPLESOFT_BASIC = {
dialectName: "APPLESOFT",
asciiOnly: true,
uppercaseOnly: false,
optionalLabels: false,
optionalWhitespace: true,
multipleStmtsPerLine: true,
varNaming: "*",
staticArrays: false,
sharedArrayNamespace: false,
defaultArrayBase: 0,
defaultArraySize: 11,
defaultValues: true,
stringConcat: true,
maxDimensions: 88,
maxDefArgs: 1,
maxStringLength: 255,
tickComments: false,
hexOctalConsts: false,
validKeywords: [
'OPTION',
'CLEAR', 'LET', 'DIM', 'DEF', 'GOTO', 'GOSUB', 'RETURN', 'ON', 'POP',
'FOR', 'NEXT', 'IF', 'THEN', 'END', 'STOP', 'ONERR', 'RESUME',
'PRINT', 'INPUT', 'GET', 'HOME', 'HTAB', 'VTAB',
'INVERSE', 'FLASH', 'NORMAL', 'TEXT',
'GR', 'COLOR', 'PLOT', 'HLIN', 'VLIN',
'HGR', 'HGR2', 'HPLOT', 'HCOLOR', 'AT',
'DATA', 'READ', 'RESTORE',
'REM', 'TRACE', 'NOTRACE',
'TO', 'STEP',
'AND', 'NOT', 'OR'
],
validFunctions: [
'ABS', 'ATN', 'COS', 'EXP', 'INT', 'LOG', 'RND', 'SGN', 'SIN', 'SQR', 'TAN',
'LEN', 'LEFT$', 'MID$', 'RIGHT$', 'STR$', 'VAL', 'CHR$', 'ASC',
'FRE', 'SCRN', 'PDL', 'PEEK', 'POS'
],
validOperators: [
'=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^',
'AND', 'NOT', 'OR'
],
printZoneLength: 16,
numericPadding: false,
checkOverflow: true,
testInitialFor: false,
optionalNextVar: true,
multipleNextVars: true,
bitwiseLogic: false,
checkOnGotoIndex: false,
computedGoto: false,
restoreWithLabel: false,
squareBrackets: false,
arraysContainChars: false,
endStmtRequired: false,
chainAssignments: false,
optionalLet: true,
compiledBlocks: false,
};
exports.BASIC80 = {
dialectName: "BASIC80",
asciiOnly: true,
uppercaseOnly: false,
optionalLabels: false,
optionalWhitespace: true,
multipleStmtsPerLine: true,
varNaming: "*",
staticArrays: false,
sharedArrayNamespace: true,
defaultArrayBase: 0,
defaultArraySize: 11,
defaultValues: true,
stringConcat: true,
maxDimensions: 255,
maxDefArgs: 255,
maxStringLength: 255,
//maxElements : 32767, // TODO
tickComments: true,
hexOctalConsts: true,
validKeywords: [
'OPTION',
'CONSOLE', 'DATA', 'DEF', 'DEFUSR', 'DIM', 'END', 'ERASE', 'ERROR',
'FOR', 'GOTO', 'GOSUB', 'IF', 'THEN', 'ELSE', 'INPUT', 'LET', 'LINE',
'PRINT', 'LPRINT', 'USING', 'NEXT', 'ON', 'OUT', 'POKE',
'READ', 'REM', 'RESTORE', 'RESUME', 'RETURN', 'STOP', 'SWAP',
'TROFF', 'TRON', 'WAIT',
'CALL', 'CHAIN', 'COMMON', 'WHILE', 'WEND', 'WRITE', 'RANDOMIZE',
'TO', 'STEP',
'AND', 'NOT', 'OR', 'XOR', 'IMP', 'EQV', 'MOD'
],
validFunctions: [
'ABS', 'ASC', 'ATN', 'CDBL', 'CHR$', 'CINT', 'COS', 'CSNG', 'CVI', 'CVS', 'CVD',
'EOF', 'EXP', 'FIX', 'FRE', 'HEX$', 'INP', 'INPUT$', 'INSTR', 'INT',
'LEFT$', 'LEN', 'LOC', 'LOG', 'LPOS', 'MID$', 'MKI$', 'MKS$', 'MKD$',
'OCT$', 'PEEK', 'POS', 'RIGHT$', 'RND', 'SGN', 'SIN', 'SPACE$', 'SPC',
'SQR', 'STR$', 'STRING$', 'TAB', 'TAN', 'USR', 'VAL', 'VARPTR'
],
validOperators: [
'=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^', '\\',
'AND', 'NOT', 'OR', 'XOR', 'IMP', 'EQV', 'MOD'
],
printZoneLength: 14,
numericPadding: true,
checkOverflow: false,
testInitialFor: true,
optionalNextVar: true,
multipleNextVars: true,
bitwiseLogic: true,
checkOnGotoIndex: false,
computedGoto: false,
restoreWithLabel: true,
squareBrackets: false,
arraysContainChars: false,
endStmtRequired: false,
chainAssignments: false,
optionalLet: true,
compiledBlocks: false,
};
exports.MODERN_BASIC = {
dialectName: "MODERN",
asciiOnly: false,
uppercaseOnly: false,
optionalLabels: true,
optionalWhitespace: false,
multipleStmtsPerLine: true,
varNaming: "*",
staticArrays: false,
sharedArrayNamespace: false,
defaultArrayBase: 0,
defaultArraySize: 0,
defaultValues: false,
stringConcat: true,
maxDimensions: 255,
maxDefArgs: 255,
maxStringLength: 2048,
tickComments: true,
hexOctalConsts: true,
validKeywords: null,
validFunctions: null,
validOperators: null,
printZoneLength: 16,
numericPadding: false,
checkOverflow: true,
testInitialFor: true,
optionalNextVar: true,
multipleNextVars: true,
bitwiseLogic: true,
checkOnGotoIndex: true,
computedGoto: false,
restoreWithLabel: true,
squareBrackets: true,
arraysContainChars: false,
endStmtRequired: false,
chainAssignments: true,
optionalLet: true,
compiledBlocks: true,
multilineIfThen: true,
};
const BUILTIN_DEFS = [
['ABS', ['number'], 'number'],
['ASC', ['string'], 'number'],
['ATN', ['number'], 'number'],
['CHR$', ['number'], 'string'],
['CINT', ['number'], 'number'],
['COS', ['number'], 'number'],
['COT', ['number'], 'number'],
['CTL', ['number'], 'string'],
['EXP', ['number'], 'number'],
['FIX', ['number'], 'number'],
['HEX$', ['number'], 'string'],
['INSTR', ['number', 'string', 'string'], 'number'],
['INSTR', ['string', 'string'], 'number'],
['INT', ['number'], 'number'],
['LEFT$', ['string', 'number'], 'string'],
['LEN', ['string'], 'number'],
['LIN', ['number'], 'string'],
['LOG', ['number'], 'number'],
['LOG10', ['number'], 'number'],
['MID$', ['string', 'number'], 'string'],
['MID$', ['string', 'number', 'number'], 'string'],
['OCT$', ['number'], 'string'],
['PI', [], 'number'],
['POS', ['number'], 'number'],
['POS', ['string', 'string'], 'number'],
['RIGHT$', ['string', 'number'], 'string'],
['RND', [], 'number'],
['RND', ['number'], 'number'],
['ROUND', ['number'], 'number'],
['SGN', ['number'], 'number'],
['SIN', ['number'], 'number'],
['SPACE$', ['number'], 'string'],
['SPC', ['number'], 'string'],
['SQR', ['number'], 'number'],
['STR$', ['number'], 'string'],
['STRING$', ['number', 'number'], 'string'],
['STRING$', ['number', 'string'], 'string'],
['TAB', ['number'], 'string'],
['TAN', ['number'], 'number'],
['TIM', ['number'], 'number'],
['TIMER', [], 'number'],
['UPS$', ['string'], 'string'],
['VAL', ['string'], 'number'],
['LPAD$', ['string', 'number'], 'string'],
['RPAD$', ['string', 'number'], 'string'],
['NFORMAT$', ['number', 'number'], 'string'],
];
var BUILTIN_MAP = {};
BUILTIN_DEFS.forEach((def, idx) => {
let [name, args, result] = def;
if (!BUILTIN_MAP[name])
BUILTIN_MAP[name] = [];
BUILTIN_MAP[name].push({ args: args, result: result });
});
exports.DIALECTS = {
"DEFAULT": exports.MODERN_BASIC,
"DARTMOUTH": exports.DARTMOUTH_4TH_EDITION,
"DARTMOUTH4": exports.DARTMOUTH_4TH_EDITION,
"ALTAIR": exports.ALTAIR_BASIC41,
"ALTAIR4": exports.ALTAIR_BASIC41,
"ALTAIR41": exports.ALTAIR_BASIC41,
"TINY": exports.TINY_BASIC,
"ECMA55": exports.ECMA55_MINIMAL,
"MINIMAL": exports.ECMA55_MINIMAL,
"HP": exports.HP_TIMESHARED_BASIC,
"HPB": exports.HP_TIMESHARED_BASIC,
"HPTSB": exports.HP_TIMESHARED_BASIC,
"HP2000": exports.HP_TIMESHARED_BASIC,
"HPBASIC": exports.HP_TIMESHARED_BASIC,
"HPACCESS": exports.HP_TIMESHARED_BASIC,
"DEC11": exports.DEC_BASIC_11,
"DEC": exports.DEC_BASIC_PLUS,
"DECPLUS": exports.DEC_BASIC_PLUS,
"BASICPLUS": exports.DEC_BASIC_PLUS,
"BASICODE": exports.BASICODE,
"APPLESOFT": exports.APPLESOFT_BASIC,
"BASIC80": exports.BASIC80,
"MODERN": exports.MODERN_BASIC,
};
//# sourceMappingURL=compiler.js.map