mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2025-02-26 22:29:56 +00:00
basic: fuzz test fixes, DEF cycle detector
This commit is contained in:
parent
295f1ef9de
commit
88fa924507
887
package-lock.json
generated
887
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -29,6 +29,7 @@
|
|||||||
"electron-packager": "^15.0.0",
|
"electron-packager": "^15.0.0",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"jsdom": "^12.2.0",
|
"jsdom": "^12.2.0",
|
||||||
|
"jsfuzz": "^1.0.14",
|
||||||
"jszip": "^3.5.0",
|
"jszip": "^3.5.0",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
"lzg": "^1.0.x",
|
"lzg": "^1.0.x",
|
||||||
|
@ -132,6 +132,8 @@ export interface Platform {
|
|||||||
|
|
||||||
startProbing?() : ProbeRecorder;
|
startProbing?() : ProbeRecorder;
|
||||||
stopProbing?() : void;
|
stopProbing?() : void;
|
||||||
|
|
||||||
|
isBlocked?() : boolean; // is blocked, halted, or waiting for input?
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Preset {
|
export interface Preset {
|
||||||
|
@ -6,8 +6,8 @@ export interface BASICOptions {
|
|||||||
asciiOnly : boolean; // reject non-ASCII chars?
|
asciiOnly : boolean; // reject non-ASCII chars?
|
||||||
uppercaseOnly : boolean; // convert everything to uppercase?
|
uppercaseOnly : boolean; // convert everything to uppercase?
|
||||||
optionalLabels : boolean; // can omit line numbers and use labels?
|
optionalLabels : boolean; // can omit line numbers and use labels?
|
||||||
optionalWhitespace : boolean; // can "crunch" keywords?
|
optionalWhitespace : boolean; // can "crunch" keywords? also, eat extra ":" delims
|
||||||
varNaming : 'A'|'A1'|'AA'|'*'; // only allow A0-9 for numerics, single letter for arrays/strings
|
varNaming : 'A'|'A1'|'AA'|'*'; // only allow A0-9 for numerics, single letter for arrays/strings
|
||||||
squareBrackets : boolean; // "[" and "]" interchangable with "(" and ")"?
|
squareBrackets : boolean; // "[" and "]" interchangable with "(" and ")"?
|
||||||
tickComments : boolean; // support 'comments?
|
tickComments : boolean; // support 'comments?
|
||||||
hexOctalConsts : boolean; // support &H and &O integer constants?
|
hexOctalConsts : boolean; // support &H and &O integer constants?
|
||||||
@ -43,6 +43,8 @@ export interface BASICOptions {
|
|||||||
endStmtRequired : boolean; // need END at end?
|
endStmtRequired : boolean; // need END at end?
|
||||||
// MISC
|
// MISC
|
||||||
commandsPerSec? : number; // how many commands per second?
|
commandsPerSec? : number; // how many commands per second?
|
||||||
|
maxLinesPerFile? : number; // limit on # of lines
|
||||||
|
maxArrayElements? : number; // max array elements (all dimensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SourceLocated {
|
export interface SourceLocated {
|
||||||
@ -59,8 +61,8 @@ export class CompileError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lexer regular expression -- each (capture group) handles a different token type
|
// Lexer regular expression -- each (capture group) handles a different token type
|
||||||
// FLOAT INT HEXOCTAL REMARK IDENT STRING RELOP EXP OPERATORS OTHER WS
|
// 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]+)|(['].*)|(\w+[$]?)|(".*?")|([<>]?[=<>#])|(\*\*)|([-+*/^,;:()\[\]\?\\])|(\S+)|(\s+)/gi;
|
const re_toks = /([0-9.]+[E][+-]?\d+|\d+[.][E0-9]*|[.][E0-9]+)|[0]*(\d+)|&([OH][0-9A-F]+)|(['].*)|([A-Z_]\w*[$]?)|(".*?")|([<>]?[=<>#])|(\*\*)|([-+*/^,;:()\[\]\?\\])|(\S+)|(\s+)/gi;
|
||||||
|
|
||||||
export enum TokenType {
|
export enum TokenType {
|
||||||
EOL = 0,
|
EOL = 0,
|
||||||
@ -302,6 +304,19 @@ function stripQuotes(s: string) {
|
|||||||
return s.substr(1, s.length-2);
|
return s.substr(1, s.length-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLiteral(arg: Expr): arg is Literal {
|
||||||
|
return (arg as any).value != null;
|
||||||
|
}
|
||||||
|
function isLookup(arg: Expr): arg is IndOp {
|
||||||
|
return (arg as any).name != null;
|
||||||
|
}
|
||||||
|
function isBinOp(arg: Expr): arg is BinOp {
|
||||||
|
return (arg as any).op != null && (arg as any).left != null && (arg as any).right != null;
|
||||||
|
}
|
||||||
|
function isUnOp(arg: Expr): arg is UnOp {
|
||||||
|
return (arg as any).op != null && (arg as any).expr != null;
|
||||||
|
}
|
||||||
|
|
||||||
///// BASIC PARSER
|
///// BASIC PARSER
|
||||||
|
|
||||||
export class BASICParser {
|
export class BASICParser {
|
||||||
@ -311,7 +326,9 @@ export class BASICParser {
|
|||||||
listings: CodeListingMap;
|
listings: CodeListingMap;
|
||||||
labels: { [label: string]: BASICLine };
|
labels: { [label: string]: BASICLine };
|
||||||
targets: { [targetlabel: string]: SourceLocation };
|
targets: { [targetlabel: string]: SourceLocation };
|
||||||
refs: { [name: string]: SourceLocation }; // references
|
varrefs: { [name: string]: SourceLocation }; // references
|
||||||
|
fnrefs: { [name: string]: string[] }; // DEF FN call graph
|
||||||
|
maxlinelen : number = 255; // maximum line length
|
||||||
|
|
||||||
path : string;
|
path : string;
|
||||||
lineno : number;
|
lineno : number;
|
||||||
@ -327,7 +344,8 @@ export class BASICParser {
|
|||||||
this.lineno = 0;
|
this.lineno = 0;
|
||||||
this.curlabel = null;
|
this.curlabel = null;
|
||||||
this.listings = {};
|
this.listings = {};
|
||||||
this.refs = {};
|
this.varrefs = {};
|
||||||
|
this.fnrefs = {};
|
||||||
this.optionCount = 0;
|
this.optionCount = 0;
|
||||||
}
|
}
|
||||||
addError(msg: string, loc?: SourceLocation) {
|
addError(msg: string, loc?: SourceLocation) {
|
||||||
@ -382,10 +400,7 @@ export class BASICParser {
|
|||||||
}
|
}
|
||||||
} else this.dialectError(`optional line numbers`);
|
} else this.dialectError(`optional line numbers`);
|
||||||
case TokenType.Int:
|
case TokenType.Int:
|
||||||
if (this.labels[tok.str] != null) this.compileError(`There's a duplicated label "${tok.str}".`);
|
this.setCurrentLabel(line, tok.str);
|
||||||
this.labels[tok.str] = line;
|
|
||||||
line.label = tok.str;
|
|
||||||
this.curlabel = tok.str;
|
|
||||||
break;
|
break;
|
||||||
case TokenType.HexOctalInt:
|
case TokenType.HexOctalInt:
|
||||||
case TokenType.Float:
|
case TokenType.Float:
|
||||||
@ -398,9 +413,16 @@ export class BASICParser {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setCurrentLabel(line: BASICLine, str: string) {
|
||||||
|
if (this.labels[str] != null) this.compileError(`There's a duplicated label "${str}".`);
|
||||||
|
this.labels[str] = line;
|
||||||
|
line.label = str;
|
||||||
|
this.curlabel = str;
|
||||||
|
this.tokens.forEach((tok) => tok.$loc.label = str);
|
||||||
|
}
|
||||||
parseFile(file: string, path: string) : BASICProgram {
|
parseFile(file: string, path: string) : BASICProgram {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
var txtlines = file.split(/\n|\r\n/);
|
var txtlines = file.split(/\n|\r\n?/);
|
||||||
var pgmlines = txtlines.map((line) => this.parseLine(line));
|
var pgmlines = txtlines.map((line) => this.parseLine(line));
|
||||||
var program = { opts: this.opts, lines: pgmlines };
|
var program = { opts: this.opts, lines: pgmlines };
|
||||||
this.checkAll(program);
|
this.checkAll(program);
|
||||||
@ -416,9 +438,7 @@ export class BASICParser {
|
|||||||
return {label:null, stmts:[]};
|
return {label:null, stmts:[]};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tokenize(line: string) : void {
|
_tokenize(line: string) : void {
|
||||||
this.lineno++;
|
|
||||||
this.tokens = [];
|
|
||||||
// split identifier regex (if token-crunching enabled)
|
// split identifier regex (if token-crunching enabled)
|
||||||
let splitre = this.opts.optionalWhitespace && new RegExp('('+this.opts.validKeywords.map(s => `${s}`).join('|')+')');
|
let splitre = this.opts.optionalWhitespace && new RegExp('('+this.opts.validKeywords.map(s => `${s}`).join('|')+')');
|
||||||
// iterate over each token via re_toks regex
|
// iterate over each token via re_toks regex
|
||||||
@ -428,7 +448,7 @@ export class BASICParser {
|
|||||||
for (var i = 1; i <= lastTokType; i++) {
|
for (var i = 1; i <= lastTokType; i++) {
|
||||||
let s : string = m[i];
|
let s : string = m[i];
|
||||||
if (s != null) {
|
if (s != null) {
|
||||||
let loc = { path: this.path, line: this.lineno, start: m.index, end: m.index+s.length, label: this.curlabel };
|
let loc = { path: this.path, line: this.lineno, start: m.index, end: m.index+s.length };
|
||||||
// maybe we don't support unicode in 1975?
|
// maybe we don't support unicode in 1975?
|
||||||
if (this.opts.asciiOnly && !/^[\x00-\x7F]*$/.test(s))
|
if (this.opts.asciiOnly && !/^[\x00-\x7F]*$/.test(s))
|
||||||
this.dialectError(`non-ASCII characters`);
|
this.dialectError(`non-ASCII characters`);
|
||||||
@ -437,6 +457,9 @@ export class BASICParser {
|
|||||||
s = s.toUpperCase();
|
s = s.toUpperCase();
|
||||||
// DATA statement captures whitespace too
|
// DATA statement captures whitespace too
|
||||||
if (s == 'DATA') lastTokType = TokenType.Whitespace;
|
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
|
// REM means ignore rest of statement
|
||||||
if (lastTokType == TokenType.CatchAll && s.startsWith('REM')) {
|
if (lastTokType == TokenType.CatchAll && s.startsWith('REM')) {
|
||||||
s = 'REM';
|
s = 'REM';
|
||||||
@ -451,26 +474,31 @@ export class BASICParser {
|
|||||||
}
|
}
|
||||||
// un-crunch tokens?
|
// un-crunch tokens?
|
||||||
if (splitre && i == TokenType.Ident) {
|
if (splitre && i == TokenType.Ident) {
|
||||||
var splittoks = s.split(splitre);
|
var splittoks = s.split(splitre).filter((s) => s != ''); // only non-empties
|
||||||
splittoks.forEach((ss) => {
|
if (splittoks.length > 1) {
|
||||||
if (ss != '') {
|
splittoks.forEach((ss) => {
|
||||||
// leftover might be integer
|
// check to see if leftover might be integer, or identifier
|
||||||
i = /^[0-9]+$/.test(ss) ? TokenType.Int : TokenType.Ident;
|
if (/^[0-9]+$/.test(ss)) i = TokenType.Int;
|
||||||
// disable crunching after this token?
|
else if (/^[A-Z_]\w*[$]?$/.test(ss)) i = TokenType.Ident;
|
||||||
if (ss == 'DATA' || ss == 'OPTION')
|
else this.compileError(`Try adding whitespace before "${ss}".`);
|
||||||
splitre = null;
|
|
||||||
this.tokens.push({str: ss, type: i, $loc:loc});
|
this.tokens.push({str: ss, type: i, $loc:loc});
|
||||||
}
|
});
|
||||||
});
|
s = null;
|
||||||
} else {
|
}
|
||||||
// add token to list
|
|
||||||
this.tokens.push({str: s, type: i, $loc:loc});
|
|
||||||
}
|
}
|
||||||
|
// add token to list
|
||||||
|
if (s) this.tokens.push({str: s, type: i, $loc:loc});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.eol = { type: TokenType.EOL, str: "", $loc: { path: this.path, line: this.lineno, start: line.length, label: this.curlabel } };
|
}
|
||||||
|
tokenize(line: string) : void {
|
||||||
|
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() : BASICLine {
|
parse() : BASICLine {
|
||||||
var line = {label: null, stmts: []};
|
var line = {label: null, stmts: []};
|
||||||
@ -502,6 +530,9 @@ export class BASICParser {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
parseStatement(): Statement | null {
|
parseStatement(): Statement | null {
|
||||||
|
// 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 cmdtok = this.consumeToken();
|
||||||
var cmd = cmdtok.str;
|
var cmd = cmdtok.str;
|
||||||
var stmt;
|
var stmt;
|
||||||
@ -510,6 +541,7 @@ export class BASICParser {
|
|||||||
if (cmdtok.str.startsWith("'") && !this.opts.tickComments) this.dialectError(`tick remarks`);
|
if (cmdtok.str.startsWith("'") && !this.opts.tickComments) this.dialectError(`tick remarks`);
|
||||||
return null;
|
return null;
|
||||||
case TokenType.Operator:
|
case TokenType.Operator:
|
||||||
|
// "?" is alias for "PRINT" on some platforms
|
||||||
if (cmd == this.validKeyword('?')) cmd = 'PRINT';
|
if (cmd == this.validKeyword('?')) cmd = 'PRINT';
|
||||||
case TokenType.Ident:
|
case TokenType.Ident:
|
||||||
// ignore remarks
|
// ignore remarks
|
||||||
@ -534,8 +566,11 @@ export class BASICParser {
|
|||||||
this.pushbackToken(cmdtok);
|
this.pushbackToken(cmdtok);
|
||||||
stmt = this.stmt__LET();
|
stmt = this.stmt__LET();
|
||||||
break;
|
break;
|
||||||
|
} else {
|
||||||
|
this.compileError(`I don't understand the command "${cmd}".`);
|
||||||
}
|
}
|
||||||
case TokenType.EOL:
|
case TokenType.EOL:
|
||||||
|
if (this.opts.optionalWhitespace) return null;
|
||||||
default:
|
default:
|
||||||
this.compileError(`There should be a command here.`);
|
this.compileError(`There should be a command here.`);
|
||||||
return null;
|
return null;
|
||||||
@ -547,7 +582,7 @@ export class BASICParser {
|
|||||||
var tok = this.consumeToken();
|
var tok = this.consumeToken();
|
||||||
switch (tok.type) {
|
switch (tok.type) {
|
||||||
case TokenType.Ident:
|
case TokenType.Ident:
|
||||||
this.refs[tok.str] = tok.$loc;
|
this.varrefs[tok.str] = tok.$loc;
|
||||||
let args = null;
|
let args = null;
|
||||||
if (this.peekToken().str == '(') {
|
if (this.peekToken().str == '(') {
|
||||||
this.expectToken('(');
|
this.expectToken('(');
|
||||||
@ -745,6 +780,20 @@ export class BASICParser {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
visitExpr(expr: Expr, callback: (expr:Expr) => void) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
//// STATEMENTS
|
//// STATEMENTS
|
||||||
|
|
||||||
@ -865,6 +914,10 @@ export class BASICParser {
|
|||||||
this.compileError(`An array defined by DIM must have at least one dimension.`)
|
this.compileError(`An array defined by DIM must have at least one dimension.`)
|
||||||
else if (arr.args.length > this.opts.maxDimensions)
|
else if (arr.args.length > this.opts.maxDimensions)
|
||||||
this.dialectError(`more than ${this.opts.maxDimensions} dimensional arrays`);
|
this.dialectError(`more than ${this.opts.maxDimensions} dimensional arrays`);
|
||||||
|
for (var arrdim of arr.args) {
|
||||||
|
if (isLiteral(arrdim) && arrdim.value < this.opts.defaultArrayBase)
|
||||||
|
this.compileError(`An array dimension cannot be less than ${this.opts.defaultArrayBase}.`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return { command:'DIM', args:lexprs };
|
return { command:'DIM', args:lexprs };
|
||||||
}
|
}
|
||||||
@ -925,8 +978,26 @@ export class BASICParser {
|
|||||||
if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`)
|
if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`)
|
||||||
this.expectToken("=");
|
this.expectToken("=");
|
||||||
var func = this.parseExpr();
|
var func = this.parseExpr();
|
||||||
|
// build call graph to detect cycles
|
||||||
|
this.visitExpr(func, (expr: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 };
|
return { command:'DEF', lexpr:lexpr, def:func };
|
||||||
}
|
}
|
||||||
|
// detect cycles in call graph starting at function 'name'
|
||||||
|
checkCallGraph(name: string, visited: Set<string>) {
|
||||||
|
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() : NoArgStatement {
|
stmt__POP() : NoArgStatement {
|
||||||
return { command:'POP' };
|
return { command:'POP' };
|
||||||
}
|
}
|
||||||
@ -1550,9 +1621,7 @@ export const MODERN_BASIC : BASICOptions = {
|
|||||||
|
|
||||||
// TODO: integer vars
|
// TODO: integer vars
|
||||||
// TODO: DEFINT/DEFSTR
|
// TODO: DEFINT/DEFSTR
|
||||||
// TODO: superfluous ":" ignored on MS basics only?
|
|
||||||
// TODO: excess INPUT ignored, error msg
|
// TODO: excess INPUT ignored, error msg
|
||||||
// TODO: max line len?
|
|
||||||
|
|
||||||
export const DIALECTS = {
|
export const DIALECTS = {
|
||||||
"DEFAULT": ALTAIR_BASIC41,
|
"DEFAULT": ALTAIR_BASIC41,
|
||||||
|
@ -3,43 +3,6 @@ import { BASICParser, DIALECTS, BASICOptions } from "./compiler";
|
|||||||
import { BASICRuntime } from "./runtime";
|
import { BASICRuntime } from "./runtime";
|
||||||
import { lpad, rpad } from "../util";
|
import { lpad, rpad } from "../util";
|
||||||
|
|
||||||
function dumpDialectInfo() {
|
|
||||||
var dialects = new Set<BASICOptions>();
|
|
||||||
var array = {};
|
|
||||||
var SELECTED_DIALECTS = ['TINY','ECMA55','HP','DEC','ALTAIR','BASIC80','MODERN'];
|
|
||||||
SELECTED_DIALECTS.forEach((dkey) => {
|
|
||||||
dialects.add(DIALECTS[dkey]);
|
|
||||||
});
|
|
||||||
var ALL_KEYWORDS = new Set<string>();
|
|
||||||
dialects.forEach((dialect) => {
|
|
||||||
Object.entries(dialect).forEach(([key, value]) => {
|
|
||||||
if (value === null) value = "all";
|
|
||||||
else if (value === true) value = "Y";
|
|
||||||
else if (value === false) value = "-";
|
|
||||||
else if (Array.isArray(value))
|
|
||||||
value = value.length;
|
|
||||||
if (!array[key]) array[key] = [];
|
|
||||||
array[key].push(value);
|
|
||||||
if (dialect.validKeywords) dialect.validKeywords.map(ALL_KEYWORDS.add.bind(ALL_KEYWORDS));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
dialects.forEach((dialect) => {
|
|
||||||
ALL_KEYWORDS.forEach((keyword) => {
|
|
||||||
if (parser.supportsKeyword(keyword)) {
|
|
||||||
var has = dialect.validKeywords == null || dialect.validKeywords.indexOf(keyword) >= 0;
|
|
||||||
if (!array[keyword]) array[keyword] = [];
|
|
||||||
array[keyword].push(has ? "Y" : "-");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Object.entries(array).forEach(([key, arr]) => {
|
|
||||||
var s = rpad(key, 30) + "|";
|
|
||||||
s += (arr as []).map((val) => rpad(val, 9)).join('|');
|
|
||||||
console.log(s);
|
|
||||||
});
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var readline = require('readline');
|
var readline = require('readline');
|
||||||
var rl = readline.createInterface({
|
var rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
@ -77,16 +40,20 @@ var data = fs.readFileSync(filename, 'utf-8');
|
|||||||
try {
|
try {
|
||||||
var pgm = parser.parseFile(data, filename);
|
var pgm = parser.parseFile(data, filename);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
if (parser.errors.length == 0)
|
if (parser.errors.length == 0)
|
||||||
console.log(`@@@ ${e}`);
|
console.log(`@@@ ${e}`);
|
||||||
else
|
|
||||||
console.log(e);
|
|
||||||
}
|
}
|
||||||
parser.errors.forEach((err) => console.log(`@@@ ${err.msg} (line ${err.label})`));
|
parser.errors.forEach((err) => console.log(`@@@ ${err.msg} (line ${err.label})`));
|
||||||
if (parser.errors.length) process.exit(2);
|
if (parser.errors.length) process.exit(2);
|
||||||
|
|
||||||
// run program
|
// run program
|
||||||
runtime.load(pgm);
|
try {
|
||||||
|
runtime.load(pgm);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`### ${e.message} (line ${runtime.getCurrentSourceLocation().label})`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
runtime.reset();
|
runtime.reset();
|
||||||
runtime.print = (s:string) => {
|
runtime.print = (s:string) => {
|
||||||
fs.writeSync(1, s+"");
|
fs.writeSync(1, s+"");
|
||||||
@ -125,3 +92,63 @@ runtime.resume = function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
runtime.resume();
|
runtime.resume();
|
||||||
|
|
||||||
|
/////
|
||||||
|
|
||||||
|
function dumpDialectInfo() {
|
||||||
|
var dialects = new Set<BASICOptions>();
|
||||||
|
var array = {};
|
||||||
|
var SELECTED_DIALECTS = ['TINY','ECMA55','HP','DEC','ALTAIR','BASIC80','MODERN'];
|
||||||
|
SELECTED_DIALECTS.forEach((dkey) => {
|
||||||
|
dialects.add(DIALECTS[dkey]);
|
||||||
|
});
|
||||||
|
var ALL_KEYWORDS = new Set<string>();
|
||||||
|
var ALL_FUNCTIONS = new Set<string>();
|
||||||
|
var ALL_OPERATORS = new Set<string>();
|
||||||
|
dialects.forEach((dialect) => {
|
||||||
|
Object.entries(dialect).forEach(([key, value]) => {
|
||||||
|
if (value === null) value = "all";
|
||||||
|
else if (value === true) value = "Y";
|
||||||
|
else if (value === false) value = "-";
|
||||||
|
else if (Array.isArray(value))
|
||||||
|
value = value.length;
|
||||||
|
if (!array[key]) array[key] = [];
|
||||||
|
array[key].push(value);
|
||||||
|
if (dialect.validKeywords) dialect.validKeywords.map(ALL_KEYWORDS.add.bind(ALL_KEYWORDS));
|
||||||
|
if (dialect.validFunctions) dialect.validFunctions.map(ALL_FUNCTIONS.add.bind(ALL_FUNCTIONS));
|
||||||
|
if (dialect.validOperators) dialect.validOperators.map(ALL_OPERATORS.add.bind(ALL_OPERATORS));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
dialects.forEach((dialect) => {
|
||||||
|
ALL_KEYWORDS.forEach((keyword) => {
|
||||||
|
if (parser.supportsKeyword(keyword)) {
|
||||||
|
var has = dialect.validKeywords == null || dialect.validKeywords.indexOf(keyword) >= 0;
|
||||||
|
keyword = '`'+keyword+'`'
|
||||||
|
if (!array[keyword]) array[keyword] = [];
|
||||||
|
array[keyword].push(has ? "Y" : "-");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ALL_OPERATORS.forEach((keyword) => {
|
||||||
|
var has = dialect.validOperators == null || dialect.validOperators.indexOf(keyword) >= 0;
|
||||||
|
if (keyword == '#') keyword = '*#*';
|
||||||
|
keyword = "*a* " + keyword + " *b*";
|
||||||
|
if (!array[keyword]) array[keyword] = [];
|
||||||
|
array[keyword].push(has ? "Y" : "-");
|
||||||
|
});
|
||||||
|
ALL_FUNCTIONS.forEach((keyword) => {
|
||||||
|
if (runtime.supportsFunction(keyword)) {
|
||||||
|
var has = dialect.validFunctions == null || dialect.validFunctions.indexOf(keyword) >= 0;
|
||||||
|
keyword = '`'+keyword+'()`'
|
||||||
|
if (!array[keyword]) array[keyword] = [];
|
||||||
|
array[keyword].push(has ? "Y" : "-");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Object.entries(array).forEach(([key, arr]) => {
|
||||||
|
var s = rpad(key, 30) + "|";
|
||||||
|
s += (arr as []).map((val) => rpad(val, 9)).join('|');
|
||||||
|
console.log(s);
|
||||||
|
});
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -66,6 +66,8 @@ class RNG {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_MAX_ARRAY_ELEMENTS = 1024*1024;
|
||||||
|
|
||||||
export class BASICRuntime {
|
export class BASICRuntime {
|
||||||
|
|
||||||
program : basic.BASICProgram;
|
program : basic.BASICProgram;
|
||||||
@ -111,6 +113,7 @@ export class BASICRuntime {
|
|||||||
// initialize program
|
// initialize program
|
||||||
this.program = program;
|
this.program = program;
|
||||||
this.opts = program.opts;
|
this.opts = program.opts;
|
||||||
|
if (!this.opts.maxArrayElements) this.opts.maxArrayElements = DEFAULT_MAX_ARRAY_ELEMENTS;
|
||||||
this.label2pc = {};
|
this.label2pc = {};
|
||||||
this.label2dataptr = {};
|
this.label2dataptr = {};
|
||||||
this.allstmts = [];
|
this.allstmts = [];
|
||||||
@ -194,9 +197,12 @@ export class BASICRuntime {
|
|||||||
// if no valid function list, look for ABC...() functions in prototype
|
// if no valid function list, look for ABC...() functions in prototype
|
||||||
if (!fnames) fnames = Object.keys(BASICRuntime.prototype).filter((name) => /^[A-Z]{3,}[$]?$/.test(name));
|
if (!fnames) fnames = Object.keys(BASICRuntime.prototype).filter((name) => /^[A-Z]{3,}[$]?$/.test(name));
|
||||||
var dict = {};
|
var dict = {};
|
||||||
for (var fn of fnames) if (this[fn]) dict[fn] = this[fn].bind(this);
|
for (var fn of fnames) if (this.supportsFunction(fn)) dict[fn] = this[fn].bind(this);
|
||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
supportsFunction(fnname: string) {
|
||||||
|
return typeof this[fnname] === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
runtimeError(msg : string) {
|
runtimeError(msg : string) {
|
||||||
this.curpc--; // we did curpc++ before executing statement
|
this.curpc--; // we did curpc++ before executing statement
|
||||||
@ -257,7 +263,7 @@ export class BASICRuntime {
|
|||||||
if (this.trace) console.log(functext);
|
if (this.trace) console.log(functext);
|
||||||
stmt.$run = this.compileJS(functext);
|
stmt.$run = this.compileJS(functext);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(functext);
|
if (functext) console.log(functext);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -279,14 +285,16 @@ export class BASICRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
skipToElse() {
|
skipToElse() {
|
||||||
do {
|
while (this.curpc < this.allstmts.length) {
|
||||||
// in Altair BASIC, ELSE is bound to the right-most IF
|
// in Altair BASIC, ELSE is bound to the right-most IF
|
||||||
// TODO: this is complicated, we should just have nested expressions
|
// TODO: this is complicated, we should just have nested expressions
|
||||||
var cmd = this.allstmts[this.curpc].command;
|
var cmd = this.allstmts[this.curpc].command;
|
||||||
if (cmd == 'ELSE') { this.curpc++; break; }
|
if (cmd == 'ELSE') { this.curpc++; break; }
|
||||||
else if (cmd == 'IF') return this.skipToEOL();
|
else if (cmd == 'IF') return this.skipToEOL();
|
||||||
this.curpc++;
|
this.curpc++;
|
||||||
} while (this.curpc < this.allstmts.length && !this.pc2line.get(this.curpc));
|
if (this.pc2line.get(this.curpc))
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
skipToEOF() {
|
skipToEOF() {
|
||||||
@ -314,7 +322,7 @@ export class BASICRuntime {
|
|||||||
var nesting = 0;
|
var nesting = 0;
|
||||||
while (pc < this.allstmts.length) {
|
while (pc < this.allstmts.length) {
|
||||||
var stmt = this.allstmts[pc];
|
var stmt = this.allstmts[pc];
|
||||||
console.log(nesting, pc, stmt);
|
//console.log(nesting, pc, stmt);
|
||||||
if (stmt.command == 'WHILE') {
|
if (stmt.command == 'WHILE') {
|
||||||
nesting++;
|
nesting++;
|
||||||
} else if (stmt.command == 'WEND') {
|
} else if (stmt.command == 'WEND') {
|
||||||
@ -461,7 +469,8 @@ export class BASICRuntime {
|
|||||||
if (!opts) opts = {};
|
if (!opts) opts = {};
|
||||||
var s = '';
|
var s = '';
|
||||||
// is it a function? not allowed
|
// is it a function? not allowed
|
||||||
if (expr.name.startsWith("FN") || this.builtins[expr.name]) this.runtimeError(`I can't call a function here.`);
|
if (expr.name.startsWith("FN") || this.builtins[expr.name])
|
||||||
|
this.runtimeError(`I can't call a function here.`);
|
||||||
// is it a subscript?
|
// is it a subscript?
|
||||||
if (expr.args) {
|
if (expr.args) {
|
||||||
// set array slice (HP BASIC)
|
// set array slice (HP BASIC)
|
||||||
@ -590,10 +599,14 @@ export class BASICRuntime {
|
|||||||
// dimension array
|
// dimension array
|
||||||
dimArray(name: string, ...dims:number[]) {
|
dimArray(name: string, ...dims:number[]) {
|
||||||
// TODO: maybe do this check at compile-time?
|
// TODO: maybe do this check at compile-time?
|
||||||
|
dims = dims.map(Math.round);
|
||||||
if (this.arrays[name] != null) {
|
if (this.arrays[name] != null) {
|
||||||
if (this.opts.staticArrays) return;
|
if (this.opts.staticArrays) return;
|
||||||
else this.runtimeError(`I already dimensioned this array (${name}) earlier.`)
|
else this.runtimeError(`I already dimensioned this array (${name}) earlier.`)
|
||||||
}
|
}
|
||||||
|
if (this.getTotalArrayLength(dims) > this.opts.maxArrayElements) {
|
||||||
|
this.runtimeError(`I can't create an array with this many elements.`);
|
||||||
|
}
|
||||||
var isstring = name.endsWith('$');
|
var isstring = name.endsWith('$');
|
||||||
// if numeric value, we use Float64Array which inits to 0
|
// if numeric value, we use Float64Array which inits to 0
|
||||||
var arrcons = isstring ? Array : Float64Array;
|
var arrcons = isstring ? Array : Float64Array;
|
||||||
@ -609,6 +622,16 @@ export class BASICRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTotalArrayLength(dims:number[]) {
|
||||||
|
var n = 1;
|
||||||
|
for (var i=0; i<dims.length; i++) {
|
||||||
|
if (dims[i] < this.opts.defaultArrayBase)
|
||||||
|
this.runtimeError(`I can't create an array with a dimension less than ${this.opts.defaultArrayBase}.`);
|
||||||
|
n *= dims[i];
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
getArray(name: string, order: number) : [] {
|
getArray(name: string, order: number) : [] {
|
||||||
if (!this.arrays[name]) {
|
if (!this.arrays[name]) {
|
||||||
if (this.opts.defaultArraySize == 0)
|
if (this.opts.defaultArraySize == 0)
|
||||||
@ -648,6 +671,7 @@ export class BASICRuntime {
|
|||||||
return (orig + ' '.repeat(start)).substr(0, start-1) + add + orig.substr(end);
|
return (orig + ' '.repeat(start)).substr(0, start-1) + add + orig.substr(end);
|
||||||
}
|
}
|
||||||
getStringSlice(s: string, start: number, end: number) {
|
getStringSlice(s: string, start: number, end: number) {
|
||||||
|
s = this.checkString(s);
|
||||||
return s.substr(start-1, end+1-start);
|
return s.substr(start-1, end+1-start);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -947,11 +971,13 @@ export class BASICRuntime {
|
|||||||
return fn;
|
return fn;
|
||||||
}
|
}
|
||||||
checkNum(n:number) : number {
|
checkNum(n:number) : number {
|
||||||
|
this.checkValue(n, 'this');
|
||||||
if (n === Infinity) this.runtimeError(`I computed a number too big to store.`);
|
if (n === Infinity) this.runtimeError(`I computed a number too big to store.`);
|
||||||
if (isNaN(n)) this.runtimeError(`I computed an invalid number.`);
|
if (isNaN(n)) this.runtimeError(`I computed an invalid number.`);
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
checkString(s:string) : string {
|
checkString(s:string) : string {
|
||||||
|
this.checkValue(s, 'this');
|
||||||
if (typeof s !== 'string')
|
if (typeof s !== 'string')
|
||||||
this.runtimeError(`I expected a string here.`);
|
this.runtimeError(`I expected a string here.`);
|
||||||
else if (s.length > this.opts.maxStringLength)
|
else if (s.length > this.opts.maxStringLength)
|
||||||
@ -1044,11 +1070,14 @@ export class BASICRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FUNCTIONS (uppercase)
|
// FUNCTIONS (uppercase)
|
||||||
|
// TODO: swizzle names for type-checking
|
||||||
|
|
||||||
ABS(arg : number) : number {
|
ABS(arg : number) : number {
|
||||||
return this.checkNum(Math.abs(arg));
|
return this.checkNum(Math.abs(arg));
|
||||||
}
|
}
|
||||||
ASC(arg : string) : number {
|
ASC(arg : string) : number {
|
||||||
|
arg = this.checkString(arg);
|
||||||
|
if (arg == '') this.runtimeError(`I tried to call ASC() on an empty string.`);
|
||||||
return arg.charCodeAt(0);
|
return arg.charCodeAt(0);
|
||||||
}
|
}
|
||||||
ATN(arg : number) : number {
|
ATN(arg : number) : number {
|
||||||
@ -1089,6 +1118,8 @@ export class BASICRuntime {
|
|||||||
return this.checkNum(Math.floor(arg));
|
return this.checkNum(Math.floor(arg));
|
||||||
}
|
}
|
||||||
LEFT$(arg : string, count : number) : string {
|
LEFT$(arg : string, count : number) : string {
|
||||||
|
arg = this.checkString(arg);
|
||||||
|
count = this.ROUND(count);
|
||||||
return arg.substr(0, count);
|
return arg.substr(0, count);
|
||||||
}
|
}
|
||||||
LEN(arg : string) : number {
|
LEN(arg : string) : number {
|
||||||
@ -1108,6 +1139,9 @@ export class BASICRuntime {
|
|||||||
return this.checkNum(Math.log10(arg));
|
return this.checkNum(Math.log10(arg));
|
||||||
}
|
}
|
||||||
MID$(arg : string, start : number, count : number) : string {
|
MID$(arg : string, start : number, count : number) : string {
|
||||||
|
arg = this.checkString(arg);
|
||||||
|
start = this.ROUND(start);
|
||||||
|
count = this.ROUND(count);
|
||||||
if (start < 1) this.runtimeError(`I can't compute MID$ if the starting index is less than 1.`)
|
if (start < 1) this.runtimeError(`I can't compute MID$ if the starting index is less than 1.`)
|
||||||
if (count == 0) count = arg.length;
|
if (count == 0) count = arg.length;
|
||||||
return arg.substr(start-1, count);
|
return arg.substr(start-1, count);
|
||||||
@ -1123,6 +1157,8 @@ export class BASICRuntime {
|
|||||||
return this.column + 1;
|
return this.column + 1;
|
||||||
}
|
}
|
||||||
RIGHT$(arg : string, count : number) : string {
|
RIGHT$(arg : string, count : number) : string {
|
||||||
|
arg = this.checkString(arg);
|
||||||
|
count = this.ROUND(count);
|
||||||
return arg.substr(arg.length - count, count);
|
return arg.substr(arg.length - count, count);
|
||||||
}
|
}
|
||||||
RND(arg : number) : number {
|
RND(arg : number) : number {
|
||||||
@ -1134,14 +1170,14 @@ export class BASICRuntime {
|
|||||||
return this.checkNum(Math.round(arg));
|
return this.checkNum(Math.round(arg));
|
||||||
}
|
}
|
||||||
SGN(arg : number) : number {
|
SGN(arg : number) : number {
|
||||||
|
this.checkNum(arg);
|
||||||
return (arg < 0) ? -1 : (arg > 0) ? 1 : 0;
|
return (arg < 0) ? -1 : (arg > 0) ? 1 : 0;
|
||||||
}
|
}
|
||||||
SIN(arg : number) : number {
|
SIN(arg : number) : number {
|
||||||
return this.checkNum(Math.sin(arg));
|
return this.checkNum(Math.sin(arg));
|
||||||
}
|
}
|
||||||
SPACE$(arg : number) : string {
|
SPACE$(arg : number) : string {
|
||||||
arg = this.ROUND(arg);
|
return this.STRING$(arg, ' ');
|
||||||
return (arg > 0) ? ' '.repeat(arg) : '';
|
|
||||||
}
|
}
|
||||||
SPC(arg : number) : string {
|
SPC(arg : number) : string {
|
||||||
return this.SPACE$(arg);
|
return this.SPACE$(arg);
|
||||||
@ -1156,13 +1192,17 @@ export class BASICRuntime {
|
|||||||
STRING$(len : number, chr : number|string) : string {
|
STRING$(len : number, chr : number|string) : string {
|
||||||
len = this.ROUND(len);
|
len = this.ROUND(len);
|
||||||
if (len <= 0) return '';
|
if (len <= 0) return '';
|
||||||
if (typeof chr === 'string') return chr.substr(0,1).repeat(len);
|
if (len > this.opts.maxStringLength)
|
||||||
else return String.fromCharCode(chr).repeat(len);
|
this.runtimeError(`I can't create a string longer than ${this.opts.maxStringLength} characters.`);
|
||||||
|
if (typeof chr === 'string')
|
||||||
|
return chr.substr(0,1).repeat(len);
|
||||||
|
else
|
||||||
|
return String.fromCharCode(chr).repeat(len);
|
||||||
}
|
}
|
||||||
TAB(arg : number) : string {
|
TAB(arg : number) : string {
|
||||||
if (arg < 1) { arg = 1; } // TODO: SYSTEM MESSAGE IDENTIFYING THE EXCEPTION
|
if (arg < 1) { arg = 1; } // TODO: SYSTEM MESSAGE IDENTIFYING THE EXCEPTION
|
||||||
var spaces = this.ROUND(arg) - 1 - this.column;
|
var spaces = this.ROUND(arg) - 1 - this.column;
|
||||||
return (spaces > 0) ? ' '.repeat(spaces) : '';
|
return this.SPACE$(spaces);
|
||||||
}
|
}
|
||||||
TAN(arg : number) : number {
|
TAN(arg : number) : number {
|
||||||
return this.checkNum(Math.tan(arg));
|
return this.checkNum(Math.tan(arg));
|
||||||
@ -1196,10 +1236,12 @@ export class BASICRuntime {
|
|||||||
return isNaN(n) ? 0 : n; // TODO? altair works this way
|
return isNaN(n) ? 0 : n; // TODO? altair works this way
|
||||||
}
|
}
|
||||||
LPAD$(arg : string, len : number) : string {
|
LPAD$(arg : string, len : number) : string {
|
||||||
|
arg = this.checkString(arg);
|
||||||
while (arg.length < len) arg = " " + arg;
|
while (arg.length < len) arg = " " + arg;
|
||||||
return arg;
|
return arg;
|
||||||
}
|
}
|
||||||
RPAD$(arg : string, len : number) : string {
|
RPAD$(arg : string, len : number) : string {
|
||||||
|
arg = this.checkString(arg);
|
||||||
while (arg.length < len) arg = arg + " ";
|
while (arg.length < len) arg = arg + " ";
|
||||||
return arg;
|
return arg;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user