1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2026-03-14 17:16:35 +00:00

basic: fixes for non-line-number mode (24, 62, 81), handle unhandledrejection, MODERN default dialect, DARTMOUTH

This commit is contained in:
Steven Hugg
2020-08-16 12:16:29 -05:00
parent 88fa924507
commit 9cedb1af08
21 changed files with 228 additions and 102 deletions

View File

@@ -11,6 +11,7 @@ export interface BASICOptions {
squareBrackets : boolean; // "[" and "]" interchangable with "(" and ")"?
tickComments : boolean; // support 'comments?
hexOctalConsts : boolean; // support &H and &O integer constants?
optionalLet : boolean; // LET is optional
chainAssignments : boolean; // support A = B = value (HP2000)
validKeywords : string[]; // valid keywords (or null for accept all)
validFunctions : string[]; // valid functions (or null for accept all)
@@ -18,7 +19,7 @@ export interface BASICOptions {
// VALUES AND OPERATORS
defaultValues : boolean; // initialize unset variables to default value? (0 or "")
stringConcat : boolean; // can concat strings with "+" operator?
typeConvert : boolean; // type convert strings <-> numbers?
typeConvert : boolean; // type convert strings <-> numbers? (NOT USED)
checkOverflow : boolean; // check for overflow of numerics?
bitwiseLogic : boolean; // -1 = TRUE, 0 = FALSE, AND/OR/NOT done with bitwise ops
maxStringLength : number; // maximum string length in chars
@@ -390,15 +391,17 @@ export class BASICParser {
let tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
if (this.opts.optionalLabels) {
if (this.peekToken().str == ':') { // is it a label:
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(`optional line numbers`);
} else
this.dialectError(`optional line numbers`);
case TokenType.Int:
this.setCurrentLabel(line, tok.str);
break;
@@ -406,9 +409,16 @@ export class BASICParser {
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.`);
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;
}
@@ -525,9 +535,9 @@ export class BASICParser {
validKeyword(keyword: string) : string {
return (this.opts.validKeywords && this.opts.validKeywords.indexOf(keyword) < 0) ? null : keyword;
}
supportsKeyword(keyword: string) {
if (this['stmt__' + keyword] != null) return true;
return false;
supportsCommand(cmd: string) : () => Statement {
if (cmd == '?') return this.stmt__PRINT;
else return this['stmt__' + cmd];
}
parseStatement(): Statement | null {
// eat extra ":" (should have separate property for this)
@@ -555,13 +565,15 @@ export class BASICParser {
cmd = 'GOSUB';
}
// lookup JS function for command
var fn = this['stmt__' + cmd];
var fn = this.supportsCommand(cmd);
if (fn) {
if (this.validKeyword(cmd) == null)
this.dialectError(`the ${cmd} keyword`);
stmt = fn.bind(this)() as Statement;
this.dialectError(`the ${cmd} statement`);
stmt = fn.bind(this)();
break;
} else if (this.peekToken().str == '=' || this.peekToken().str == '(') {
if (!this.opts.optionalLet)
this.dialectError(`assignments without a preceding LET`);
// 'A = expr' or 'A(X) = expr'
this.pushbackToken(cmdtok);
stmt = this.stmt__LET();
@@ -631,23 +643,22 @@ export class BASICParser {
if (this.opts.computedGoto) {
// parse expression, but still add to list of label targets if constant
var expr = this.parseExpr();
if ((expr as Literal).value != null) {
this.targets[(expr as Literal).value] = this.lasttoken.$loc;
}
if (isLiteral(expr)) this.targets[expr.value] = this.lasttoken.$loc;
return expr;
}
// parse a single number or ident label
var tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
if (!this.opts.optionalLabels) this.dialectError(`labels other than line numbers`)
case TokenType.Int:
var label = tok.str;
this.targets[label] = tok.$loc;
return {value:label};
default:
if (this.opts.optionalLabels) this.compileError(`There should be a line number or label here.`);
else this.compileError(`There should be a line number here.`);
} else {
// parse a single number or ident label
var tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
if (!this.opts.optionalLabels) this.dialectError(`labels other than line numbers`)
case TokenType.Int:
var label = tok.str;
this.targets[label] = tok.$loc;
return {value:label};
default:
var what = this.opts.optionalLabels ? "label or line number" : "line number";
this.compileError(`There should be a ${what} here.`);
}
}
}
parseDatumList(): Literal[] {
@@ -841,13 +852,14 @@ export class BASICParser {
__GO(cmd: "GOTO"|"GOSUB"): GOTO_Statement | GOSUB_Statement | ONGO_Statement {
var expr = this.parseLabel();
// GOTO (expr) OF (labels...)
if (this.opts.computedGoto && this.peekToken().str == 'OF') {
if (this.peekToken().str == this.validKeyword('OF')) {
this.expectToken('OF');
let newcmd : 'ONGOTO'|'ONGOSUB' = (cmd == 'GOTO') ? 'ONGOTO' : 'ONGOSUB';
return { command:newcmd, expr:expr, labels:this.parseLabelList() };
return { command: newcmd, expr: expr, labels: this.parseLabelList() };
} else {
// regular GOTO or GOSUB
return { command: cmd, label: expr };
}
// regular GOTO or GOSUB
return { command: cmd, label: expr };
}
stmt__IF(): IF_Statement {
var cond = this.parseExpr();
@@ -1100,7 +1112,8 @@ export class BASICParser {
checkLabels() {
for (let targ in this.targets) {
if (this.labels[targ] == null) {
this.addError(`There isn't a line number ${targ}.`, this.targets[targ]);
var what = this.opts.optionalLabels && isNaN(parseInt(targ)) ? "label named" : "line number";
this.addError(`There isn't a ${what} ${targ}.`, this.targets[targ]);
}
}
}
@@ -1152,6 +1165,56 @@ export const ECMA55_MINIMAL : BASICOptions = {
arraysContainChars : false,
endStmtRequired : true,
chainAssignments : false,
optionalLet : false,
}
export const DARTMOUTH_4TH_EDITION : BASICOptions = {
dialectName: "DARTMOUTH4",
asciiOnly : true,
uppercaseOnly : true,
optionalLabels : false,
optionalWhitespace : false,
varNaming : "A1",
staticArrays : true,
sharedArrayNamespace : false,
defaultArrayBase : 0,
defaultArraySize : 11,
defaultValues : false,
stringConcat : false,
typeConvert : 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','SUB','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,
}
// TODO: only integers supported
@@ -1191,15 +1254,15 @@ export const TINY_BASIC : BASICOptions = {
multipleNextVars : false,
bitwiseLogic : false,
checkOnGotoIndex : false,
computedGoto : true, // TODO: is it really though?
computedGoto : true,
restoreWithLabel : false,
squareBrackets : false,
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
optionalLet : false,
}
export const HP_TIMESHARED_BASIC : BASICOptions = {
dialectName: "HP2000",
asciiOnly : true,
@@ -1242,12 +1305,13 @@ export const HP_TIMESHARED_BASIC : BASICOptions = {
multipleNextVars : false,
bitwiseLogic : false,
checkOnGotoIndex : false,
computedGoto : true,
computedGoto : true, // not really, but we do parse expressions for GOTO ... OF
restoreWithLabel : true,
squareBrackets : true,
arraysContainChars : true,
endStmtRequired : true,
chainAssignments : true,
optionalLet : true,
// TODO: max line number, array index 9999
}
@@ -1298,6 +1362,7 @@ export const DEC_BASIC_11 : BASICOptions = {
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
optionalLet : true,
// TODO: max line number 32767
// TODO: \ separator, % int vars and constants, 'single' quoted
// TODO: can't compare strings and numbers
@@ -1356,6 +1421,7 @@ export const DEC_BASIC_PLUS : BASICOptions = {
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false, // TODO: can chain with "," not "="
optionalLet : true,
// TODO: max line number 32767
// TODO: \ separator, % int vars and constants, 'single' quoted
// TODO: can't compare strings and numbers
@@ -1408,6 +1474,7 @@ export const BASICODE : BASICOptions = {
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
optionalLet : true,
}
export const ALTAIR_BASIC41 : BASICOptions = {
@@ -1464,6 +1531,7 @@ export const ALTAIR_BASIC41 : BASICOptions = {
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
optionalLet : true,
}
export const APPLESOFT_BASIC : BASICOptions = {
@@ -1521,6 +1589,7 @@ export const APPLESOFT_BASIC : BASICOptions = {
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
optionalLet : true,
}
export const BASIC80 : BASICOptions = {
@@ -1579,6 +1648,7 @@ export const BASIC80 : BASICOptions = {
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
optionalLet : true,
}
export const MODERN_BASIC : BASICOptions = {
@@ -1594,7 +1664,7 @@ export const MODERN_BASIC : BASICOptions = {
defaultArraySize : 0, // DIM required
defaultValues : false,
stringConcat : true,
typeConvert : true,
typeConvert : false,
maxDimensions : 255,
maxDefArgs : 255,
maxStringLength : 2048, // TODO?
@@ -1611,12 +1681,13 @@ export const MODERN_BASIC : BASICOptions = {
multipleNextVars : true,
bitwiseLogic : true,
checkOnGotoIndex : true,
computedGoto : true,
computedGoto : false,
restoreWithLabel : true,
squareBrackets : true,
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
chainAssignments : true,
optionalLet : true,
}
// TODO: integer vars
@@ -1624,7 +1695,9 @@ export const MODERN_BASIC : BASICOptions = {
// TODO: excess INPUT ignored, error msg
export const DIALECTS = {
"DEFAULT": ALTAIR_BASIC41,
"DEFAULT": MODERN_BASIC,
"DARTMOUTH": DARTMOUTH_4TH_EDITION,
"DARTMOUTH4": DARTMOUTH_4TH_EDITION,
"ALTAIR": ALTAIR_BASIC41,
"ALTAIR4": ALTAIR_BASIC41,
"ALTAIR41": ALTAIR_BASIC41,

44
src/common/basic/fuzz.ts Normal file
View File

@@ -0,0 +1,44 @@
import { BASICParser, DIALECTS, BASICOptions, CompileError } from "./compiler";
import { BASICRuntime } from "./runtime";
import { EmuHalt } from "../emu";
process.on('unhandledRejection', (reason, promise) => {
if (!(reason instanceof EmuHalt))
console.log('Unhandled Rejection at:', promise, 'reason:', reason);
// Application specific logging, throwing an error, or other logic here
});
export function fuzz(buf) {
var parser = new BASICParser();
var str = buf.toString();
try {
var pgm = parser.parseFile(str, "test.bas");
var runtime = new BASICRuntime();
runtime.load(pgm);
runtime.reset();
runtime.print = (s) => {
if (s == null) throw new Error("PRINT null string");
}
runtime.input = function(prompt: string, nargs: number) : Promise<string[]> {
var p = new Promise<string[]>( (resolve, reject) => {
var arr = [];
for (var i=0; i<Math.random()*10; i++)
arr.push(i+"");
resolve(arr);
});
return p;
}
for (var i=0; i<50000; i++) {
if (!runtime.step()) break;
}
if (Math.random() < 0.001) runtime.load(pgm);
for (var i=0; i<50000; i++) {
if (!runtime.step()) break;
}
} catch (e) {
if (e instanceof EmuHalt) return;
if (e instanceof CompileError) return;
throw e;
}
}

View File

@@ -1,24 +0,0 @@
import { BASICParser } from "./compiler";
var parser = new BASICParser();
var readline = require('readline');
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
rl.on('line', function (line) {
parser.tokenize(line);
console.log(parser.tokens);
try {
var ast = parser.parse();
console.log(JSON.stringify(ast, null, 4));
} catch (e) {
console.log(e);
}
if (parser.errors.length) {
console.log(parser.errors);
parser.errors = [];
}
})

View File

@@ -21,14 +21,22 @@ var fs = require('fs');
var parser = new BASICParser();
var runtime = new BASICRuntime();
function getCurrentLabel() {
var loc = runtime.getCurrentSourceLocation();
return loc ? loc.label : "?";
}
// parse args
var filename = '/dev/stdin';
var args = process.argv.slice(2);
var force = false;
for (var i=0; i<args.length; i++) {
if (args[i] == '-v')
runtime.trace = true;
else if (args[i] == '-d')
parser.opts = DIALECTS[args[++i]] || Error('no such dialect');
else if (args[i] == '-f')
force = true;
else if (args[i] == '--dialects')
dumpDialectInfo();
else
@@ -45,13 +53,13 @@ try {
console.log(`@@@ ${e}`);
}
parser.errors.forEach((err) => console.log(`@@@ ${err.msg} (line ${err.label})`));
if (parser.errors.length) process.exit(2);
if (parser.errors.length && !force) process.exit(2);
// run program
try {
runtime.load(pgm);
} catch (e) {
console.log(`### ${e.message} (line ${runtime.getCurrentSourceLocation().label})`);
console.log(`### ${e.message} (line ${getCurrentLabel()})`);
process.exit(1);
}
runtime.reset();
@@ -86,7 +94,7 @@ runtime.resume = function() {
process.exit(0);
}
} catch (e) {
console.log(`### ${e.message} (line ${runtime.getCurrentSourceLocation().label})`);
console.log(`### ${e.message} (line ${getCurrentLabel()})`);
process.exit(1);
}
});
@@ -98,7 +106,7 @@ runtime.resume();
function dumpDialectInfo() {
var dialects = new Set<BASICOptions>();
var array = {};
var SELECTED_DIALECTS = ['TINY','ECMA55','HP','DEC','ALTAIR','BASIC80','MODERN'];
var SELECTED_DIALECTS = ['TINY','ECMA55','DARTMOUTH','HP','DEC','ALTAIR','BASIC80','MODERN'];
SELECTED_DIALECTS.forEach((dkey) => {
dialects.add(DIALECTS[dkey]);
});
@@ -121,7 +129,7 @@ function dumpDialectInfo() {
});
dialects.forEach((dialect) => {
ALL_KEYWORDS.forEach((keyword) => {
if (parser.supportsKeyword(keyword)) {
if (parser.supportsCommand(keyword)) {
var has = dialect.validKeywords == null || dialect.validKeywords.indexOf(keyword) >= 0;
keyword = '`'+keyword+'`'
if (!array[keyword]) array[keyword] = [];

View File

@@ -473,7 +473,7 @@ export class BASICRuntime {
this.runtimeError(`I can't call a function here.`);
// is it a subscript?
if (expr.args) {
// set array slice (HP BASIC)
// TODO: set array slice (HP BASIC)
if (this.opts.arraysContainChars && expr.name.endsWith('$')) {
this.runtimeError(`I can't set array slices via this command yet.`);
} else {
@@ -566,8 +566,8 @@ export class BASICRuntime {
// converts a variable to string/number based on var name
assign(name: string, right: number|string, isRead?:boolean) : number|string {
// convert data? READ always converts if read into string
if (this.opts.typeConvert || (isRead && name.endsWith("$")))
return this.convert(name, right);
if (isRead && name.endsWith("$"))
return this.checkValue(this.convert(name, right), name);
// TODO: use options
if (name.endsWith("$")) {
return this.convertToString(right, name);
@@ -604,9 +604,9 @@ export class BASICRuntime {
if (this.opts.staticArrays) return;
else this.runtimeError(`I already dimensioned this array (${name}) earlier.`)
}
if (this.getTotalArrayLength(dims) > this.opts.maxArrayElements) {
var total = this.getTotalArrayLength(dims);
if (total > 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;
@@ -635,7 +635,7 @@ export class BASICRuntime {
getArray(name: string, order: number) : [] {
if (!this.arrays[name]) {
if (this.opts.defaultArraySize == 0)
this.dialectError(`automatically declare arrays without a DIM statement`);
this.dialectError(`automatically declare arrays without a DIM statement (or did you mean to call a function?)`);
if (order == 1)
this.dimArray(name, this.opts.defaultArraySize-1);
else if (order == 2)
@@ -648,7 +648,7 @@ export class BASICRuntime {
arrayGet(name: string, ...indices: number[]) : basic.Value {
var arr = this.getArray(name, indices.length);
indices = indices.map(Math.round);
indices = indices.map(this.ROUND.bind(this));
var v = arr;
for (var i=0; i<indices.length; i++) {
var idx = indices[i];
@@ -668,10 +668,20 @@ export class BASICRuntime {
// for HP BASIC string slicing (TODO?)
modifyStringSlice(orig: string, add: string, start: number, end: number) : string {
orig = orig || "";
return (orig + ' '.repeat(start)).substr(0, start-1) + add + orig.substr(end);
this.checkString(orig);
this.checkString(add);
if (!end) end = start;
start = this.ROUND(start);
end = this.ROUND(end);
if (start < 1 || end < 1) this.dialectError(`accept a string slice index less than 1`);
return (orig + ' '.repeat(start)).substr(0, start-1) + add.substr(0, end+1-start) + orig.substr(end);
}
getStringSlice(s: string, start: number, end: number) {
s = this.checkString(s);
start = this.ROUND(start);
end = this.ROUND(end);
if (start < 1 || end < 1) this.dialectError(`accept a string slice index less than 1`);
return s.substr(start-1, end+1-start);
}
@@ -707,7 +717,8 @@ export class BASICRuntime {
var s = '';
for (var arg of stmt.args) {
var expr = this.expr2js(arg);
s += `this.printExpr(${expr});`;
var name = (expr as any).name;
s += `this.printExpr(this.checkValue(${expr}, ${JSON.stringify(name)}));`;
}
return s;
}
@@ -751,7 +762,7 @@ export class BASICRuntime {
if (this.opts.arraysContainChars && lexpr.args && lexpr.name.endsWith('$')) {
s += `this.vars.${lexpr.name} = this.modifyStringSlice(this.vars.${lexpr.name}, _right, `
s += lexpr.args.map((arg) => this.expr2js(arg)).join(', ');
s += ')';
s += ');';
} else {
var ljs = this.assign2js(lexpr);
s += `${ljs} = this.assign(${JSON.stringify(lexpr.name)}, _right);`;
@@ -956,7 +967,7 @@ export class BASICRuntime {
return exprname.endsWith("$") ? "" : 0;
}
if (exprname != null && obj == null) {
this.runtimeError(`I haven't set a value for ${exprname}.`);
this.runtimeError(`I haven't assigned a value to ${exprname}.`);
} else if (exprname != null) {
this.runtimeError(`I got an invalid value for ${exprname}: ${obj}`);
} else {
@@ -1193,7 +1204,7 @@ export class BASICRuntime {
len = this.ROUND(len);
if (len <= 0) return '';
if (len > this.opts.maxStringLength)
this.runtimeError(`I can't create a string longer than ${this.opts.maxStringLength} characters.`);
this.dialectError(`create a string longer than ${this.opts.maxStringLength} characters`);
if (typeof chr === 'string')
return chr.substr(0,1).repeat(len);
else