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",
|
||||
"file-saver": "^2.0.2",
|
||||
"jsdom": "^12.2.0",
|
||||
"jsfuzz": "^1.0.14",
|
||||
"jszip": "^3.5.0",
|
||||
"localforage": "^1.9.0",
|
||||
"lzg": "^1.0.x",
|
||||
|
@ -132,6 +132,8 @@ export interface Platform {
|
||||
|
||||
startProbing?() : ProbeRecorder;
|
||||
stopProbing?() : void;
|
||||
|
||||
isBlocked?() : boolean; // is blocked, halted, or waiting for input?
|
||||
}
|
||||
|
||||
export interface Preset {
|
||||
|
@ -6,8 +6,8 @@ export interface BASICOptions {
|
||||
asciiOnly : boolean; // reject non-ASCII chars?
|
||||
uppercaseOnly : boolean; // convert everything to uppercase?
|
||||
optionalLabels : boolean; // can omit line numbers and use labels?
|
||||
optionalWhitespace : boolean; // can "crunch" keywords?
|
||||
varNaming : 'A'|'A1'|'AA'|'*'; // only allow A0-9 for numerics, single letter for arrays/strings
|
||||
optionalWhitespace : boolean; // can "crunch" keywords? also, eat extra ":" delims
|
||||
varNaming : 'A'|'A1'|'AA'|'*'; // only allow A0-9 for numerics, single letter for arrays/strings
|
||||
squareBrackets : boolean; // "[" and "]" interchangable with "(" and ")"?
|
||||
tickComments : boolean; // support 'comments?
|
||||
hexOctalConsts : boolean; // support &H and &O integer constants?
|
||||
@ -43,6 +43,8 @@ export interface BASICOptions {
|
||||
endStmtRequired : boolean; // need END at end?
|
||||
// MISC
|
||||
commandsPerSec? : number; // how many commands per second?
|
||||
maxLinesPerFile? : number; // limit on # of lines
|
||||
maxArrayElements? : number; // max array elements (all dimensions)
|
||||
}
|
||||
|
||||
export interface SourceLocated {
|
||||
@ -59,8 +61,8 @@ export class CompileError extends Error {
|
||||
}
|
||||
|
||||
// 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]+)|(['].*)|(\w+[$]?)|(".*?")|([<>]?[=<>#])|(\*\*)|([-+*/^,;:()\[\]\?\\])|(\S+)|(\s+)/gi;
|
||||
// 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;
|
||||
|
||||
export enum TokenType {
|
||||
EOL = 0,
|
||||
@ -302,6 +304,19 @@ function stripQuotes(s: string) {
|
||||
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
|
||||
|
||||
export class BASICParser {
|
||||
@ -311,7 +326,9 @@ export class BASICParser {
|
||||
listings: CodeListingMap;
|
||||
labels: { [label: string]: BASICLine };
|
||||
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;
|
||||
lineno : number;
|
||||
@ -327,7 +344,8 @@ export class BASICParser {
|
||||
this.lineno = 0;
|
||||
this.curlabel = null;
|
||||
this.listings = {};
|
||||
this.refs = {};
|
||||
this.varrefs = {};
|
||||
this.fnrefs = {};
|
||||
this.optionCount = 0;
|
||||
}
|
||||
addError(msg: string, loc?: SourceLocation) {
|
||||
@ -382,10 +400,7 @@ export class BASICParser {
|
||||
}
|
||||
} else this.dialectError(`optional line numbers`);
|
||||
case TokenType.Int:
|
||||
if (this.labels[tok.str] != null) this.compileError(`There's a duplicated label "${tok.str}".`);
|
||||
this.labels[tok.str] = line;
|
||||
line.label = tok.str;
|
||||
this.curlabel = tok.str;
|
||||
this.setCurrentLabel(line, tok.str);
|
||||
break;
|
||||
case TokenType.HexOctalInt:
|
||||
case TokenType.Float:
|
||||
@ -398,9 +413,16 @@ export class BASICParser {
|
||||
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 {
|
||||
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 program = { opts: this.opts, lines: pgmlines };
|
||||
this.checkAll(program);
|
||||
@ -416,9 +438,7 @@ export class BASICParser {
|
||||
return {label:null, stmts:[]};
|
||||
}
|
||||
}
|
||||
tokenize(line: string) : void {
|
||||
this.lineno++;
|
||||
this.tokens = [];
|
||||
_tokenize(line: string) : void {
|
||||
// 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
|
||||
@ -428,7 +448,7 @@ export class BASICParser {
|
||||
for (var i = 1; i <= lastTokType; i++) {
|
||||
let s : string = m[i];
|
||||
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?
|
||||
if (this.opts.asciiOnly && !/^[\x00-\x7F]*$/.test(s))
|
||||
this.dialectError(`non-ASCII characters`);
|
||||
@ -437,6 +457,9 @@ export class BASICParser {
|
||||
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';
|
||||
@ -451,26 +474,31 @@ export class BASICParser {
|
||||
}
|
||||
// un-crunch tokens?
|
||||
if (splitre && i == TokenType.Ident) {
|
||||
var splittoks = s.split(splitre);
|
||||
splittoks.forEach((ss) => {
|
||||
if (ss != '') {
|
||||
// leftover might be integer
|
||||
i = /^[0-9]+$/.test(ss) ? TokenType.Int : TokenType.Ident;
|
||||
// disable crunching after this token?
|
||||
if (ss == 'DATA' || ss == 'OPTION')
|
||||
splitre = null;
|
||||
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});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// add token to list
|
||||
this.tokens.push({str: s, type: i, $loc:loc});
|
||||
});
|
||||
s = null;
|
||||
}
|
||||
}
|
||||
// add token to list
|
||||
if (s) this.tokens.push({str: s, type: i, $loc:loc});
|
||||
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 {
|
||||
var line = {label: null, stmts: []};
|
||||
@ -502,6 +530,9 @@ export class BASICParser {
|
||||
return false;
|
||||
}
|
||||
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 cmd = cmdtok.str;
|
||||
var stmt;
|
||||
@ -510,6 +541,7 @@ export class BASICParser {
|
||||
if (cmdtok.str.startsWith("'") && !this.opts.tickComments) this.dialectError(`tick remarks`);
|
||||
return null;
|
||||
case TokenType.Operator:
|
||||
// "?" is alias for "PRINT" on some platforms
|
||||
if (cmd == this.validKeyword('?')) cmd = 'PRINT';
|
||||
case TokenType.Ident:
|
||||
// ignore remarks
|
||||
@ -534,8 +566,11 @@ export class BASICParser {
|
||||
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;
|
||||
@ -547,7 +582,7 @@ export class BASICParser {
|
||||
var tok = this.consumeToken();
|
||||
switch (tok.type) {
|
||||
case TokenType.Ident:
|
||||
this.refs[tok.str] = tok.$loc;
|
||||
this.varrefs[tok.str] = tok.$loc;
|
||||
let args = null;
|
||||
if (this.peekToken().str == '(') {
|
||||
this.expectToken('(');
|
||||
@ -745,6 +780,20 @@ export class BASICParser {
|
||||
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
|
||||
|
||||
@ -865,6 +914,10 @@ export class BASICParser {
|
||||
this.compileError(`An array defined by DIM must have at least one dimension.`)
|
||||
else if (arr.args.length > this.opts.maxDimensions)
|
||||
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 };
|
||||
}
|
||||
@ -925,8 +978,26 @@ export class BASICParser {
|
||||
if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`)
|
||||
this.expectToken("=");
|
||||
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 };
|
||||
}
|
||||
// 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 {
|
||||
return { command:'POP' };
|
||||
}
|
||||
@ -1550,9 +1621,7 @@ export const MODERN_BASIC : BASICOptions = {
|
||||
|
||||
// TODO: integer vars
|
||||
// TODO: DEFINT/DEFSTR
|
||||
// TODO: superfluous ":" ignored on MS basics only?
|
||||
// TODO: excess INPUT ignored, error msg
|
||||
// TODO: max line len?
|
||||
|
||||
export const DIALECTS = {
|
||||
"DEFAULT": ALTAIR_BASIC41,
|
||||
|
@ -3,43 +3,6 @@ import { BASICParser, DIALECTS, BASICOptions } from "./compiler";
|
||||
import { BASICRuntime } from "./runtime";
|
||||
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 rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
@ -77,16 +40,20 @@ var data = fs.readFileSync(filename, 'utf-8');
|
||||
try {
|
||||
var pgm = parser.parseFile(data, filename);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (parser.errors.length == 0)
|
||||
console.log(`@@@ ${e}`);
|
||||
else
|
||||
console.log(e);
|
||||
}
|
||||
parser.errors.forEach((err) => console.log(`@@@ ${err.msg} (line ${err.label})`));
|
||||
if (parser.errors.length) process.exit(2);
|
||||
|
||||
// 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.print = (s:string) => {
|
||||
fs.writeSync(1, s+"");
|
||||
@ -125,3 +92,63 @@ runtime.resume = function() {
|
||||
});
|
||||
}
|
||||
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 {
|
||||
|
||||
program : basic.BASICProgram;
|
||||
@ -111,6 +113,7 @@ export class BASICRuntime {
|
||||
// initialize program
|
||||
this.program = program;
|
||||
this.opts = program.opts;
|
||||
if (!this.opts.maxArrayElements) this.opts.maxArrayElements = DEFAULT_MAX_ARRAY_ELEMENTS;
|
||||
this.label2pc = {};
|
||||
this.label2dataptr = {};
|
||||
this.allstmts = [];
|
||||
@ -194,9 +197,12 @@ export class BASICRuntime {
|
||||
// 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));
|
||||
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;
|
||||
}
|
||||
supportsFunction(fnname: string) {
|
||||
return typeof this[fnname] === 'function';
|
||||
}
|
||||
|
||||
runtimeError(msg : string) {
|
||||
this.curpc--; // we did curpc++ before executing statement
|
||||
@ -257,7 +263,7 @@ export class BASICRuntime {
|
||||
if (this.trace) console.log(functext);
|
||||
stmt.$run = this.compileJS(functext);
|
||||
} catch (e) {
|
||||
console.log(functext);
|
||||
if (functext) console.log(functext);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@ -279,14 +285,16 @@ export class BASICRuntime {
|
||||
}
|
||||
|
||||
skipToElse() {
|
||||
do {
|
||||
while (this.curpc < this.allstmts.length) {
|
||||
// in Altair BASIC, ELSE is bound to the right-most IF
|
||||
// TODO: this is complicated, we should just have nested expressions
|
||||
var cmd = this.allstmts[this.curpc].command;
|
||||
if (cmd == 'ELSE') { this.curpc++; break; }
|
||||
else if (cmd == 'IF') return this.skipToEOL();
|
||||
this.curpc++;
|
||||
} while (this.curpc < this.allstmts.length && !this.pc2line.get(this.curpc));
|
||||
if (this.pc2line.get(this.curpc))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
skipToEOF() {
|
||||
@ -314,7 +322,7 @@ export class BASICRuntime {
|
||||
var nesting = 0;
|
||||
while (pc < this.allstmts.length) {
|
||||
var stmt = this.allstmts[pc];
|
||||
console.log(nesting, pc, stmt);
|
||||
//console.log(nesting, pc, stmt);
|
||||
if (stmt.command == 'WHILE') {
|
||||
nesting++;
|
||||
} else if (stmt.command == 'WEND') {
|
||||
@ -461,7 +469,8 @@ export class BASICRuntime {
|
||||
if (!opts) opts = {};
|
||||
var s = '';
|
||||
// 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?
|
||||
if (expr.args) {
|
||||
// set array slice (HP BASIC)
|
||||
@ -590,10 +599,14 @@ export class BASICRuntime {
|
||||
// dimension array
|
||||
dimArray(name: string, ...dims:number[]) {
|
||||
// TODO: maybe do this check at compile-time?
|
||||
dims = dims.map(Math.round);
|
||||
if (this.arrays[name] != null) {
|
||||
if (this.opts.staticArrays) return;
|
||||
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('$');
|
||||
// if numeric value, we use Float64Array which inits to 0
|
||||
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) : [] {
|
||||
if (!this.arrays[name]) {
|
||||
if (this.opts.defaultArraySize == 0)
|
||||
@ -648,6 +671,7 @@ export class BASICRuntime {
|
||||
return (orig + ' '.repeat(start)).substr(0, start-1) + add + orig.substr(end);
|
||||
}
|
||||
getStringSlice(s: string, start: number, end: number) {
|
||||
s = this.checkString(s);
|
||||
return s.substr(start-1, end+1-start);
|
||||
}
|
||||
|
||||
@ -947,11 +971,13 @@ export class BASICRuntime {
|
||||
return fn;
|
||||
}
|
||||
checkNum(n:number) : number {
|
||||
this.checkValue(n, 'this');
|
||||
if (n === Infinity) this.runtimeError(`I computed a number too big to store.`);
|
||||
if (isNaN(n)) this.runtimeError(`I computed an invalid number.`);
|
||||
return n;
|
||||
}
|
||||
checkString(s:string) : string {
|
||||
this.checkValue(s, 'this');
|
||||
if (typeof s !== 'string')
|
||||
this.runtimeError(`I expected a string here.`);
|
||||
else if (s.length > this.opts.maxStringLength)
|
||||
@ -1044,11 +1070,14 @@ export class BASICRuntime {
|
||||
}
|
||||
|
||||
// FUNCTIONS (uppercase)
|
||||
// TODO: swizzle names for type-checking
|
||||
|
||||
ABS(arg : number) : number {
|
||||
return this.checkNum(Math.abs(arg));
|
||||
}
|
||||
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);
|
||||
}
|
||||
ATN(arg : number) : number {
|
||||
@ -1089,6 +1118,8 @@ export class BASICRuntime {
|
||||
return this.checkNum(Math.floor(arg));
|
||||
}
|
||||
LEFT$(arg : string, count : number) : string {
|
||||
arg = this.checkString(arg);
|
||||
count = this.ROUND(count);
|
||||
return arg.substr(0, count);
|
||||
}
|
||||
LEN(arg : string) : number {
|
||||
@ -1108,6 +1139,9 @@ export class BASICRuntime {
|
||||
return this.checkNum(Math.log10(arg));
|
||||
}
|
||||
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 (count == 0) count = arg.length;
|
||||
return arg.substr(start-1, count);
|
||||
@ -1123,6 +1157,8 @@ export class BASICRuntime {
|
||||
return this.column + 1;
|
||||
}
|
||||
RIGHT$(arg : string, count : number) : string {
|
||||
arg = this.checkString(arg);
|
||||
count = this.ROUND(count);
|
||||
return arg.substr(arg.length - count, count);
|
||||
}
|
||||
RND(arg : number) : number {
|
||||
@ -1134,14 +1170,14 @@ export class BASICRuntime {
|
||||
return this.checkNum(Math.round(arg));
|
||||
}
|
||||
SGN(arg : number) : number {
|
||||
this.checkNum(arg);
|
||||
return (arg < 0) ? -1 : (arg > 0) ? 1 : 0;
|
||||
}
|
||||
SIN(arg : number) : number {
|
||||
return this.checkNum(Math.sin(arg));
|
||||
}
|
||||
SPACE$(arg : number) : string {
|
||||
arg = this.ROUND(arg);
|
||||
return (arg > 0) ? ' '.repeat(arg) : '';
|
||||
return this.STRING$(arg, ' ');
|
||||
}
|
||||
SPC(arg : number) : string {
|
||||
return this.SPACE$(arg);
|
||||
@ -1156,13 +1192,17 @@ export class BASICRuntime {
|
||||
STRING$(len : number, chr : number|string) : string {
|
||||
len = this.ROUND(len);
|
||||
if (len <= 0) return '';
|
||||
if (typeof chr === 'string') return chr.substr(0,1).repeat(len);
|
||||
else return String.fromCharCode(chr).repeat(len);
|
||||
if (len > this.opts.maxStringLength)
|
||||
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 {
|
||||
if (arg < 1) { arg = 1; } // TODO: SYSTEM MESSAGE IDENTIFYING THE EXCEPTION
|
||||
var spaces = this.ROUND(arg) - 1 - this.column;
|
||||
return (spaces > 0) ? ' '.repeat(spaces) : '';
|
||||
return this.SPACE$(spaces);
|
||||
}
|
||||
TAN(arg : number) : number {
|
||||
return this.checkNum(Math.tan(arg));
|
||||
@ -1196,10 +1236,12 @@ export class BASICRuntime {
|
||||
return isNaN(n) ? 0 : n; // TODO? altair works this way
|
||||
}
|
||||
LPAD$(arg : string, len : number) : string {
|
||||
arg = this.checkString(arg);
|
||||
while (arg.length < len) arg = " " + arg;
|
||||
return arg;
|
||||
}
|
||||
RPAD$(arg : string, len : number) : string {
|
||||
arg = this.checkString(arg);
|
||||
while (arg.length < len) arg = arg + " ";
|
||||
return arg;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user