mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-04-20 00:17:04 +00:00
basic: input validation, array bounds check
This commit is contained in:
@@ -288,7 +288,7 @@ export class BASICParser {
|
||||
throw new CompileError(`${msg} (line ${loc.line})`); // TODO: label too?
|
||||
}
|
||||
dialectError(what: string, loc?: SourceLocation) {
|
||||
this.compileError(`The selected BASIC dialect doesn't support ${what}.`, loc); // TODO
|
||||
this.compileError(`The selected BASIC dialect (${this.opts.dialectName}) doesn't support ${what}.`, loc); // TODO
|
||||
}
|
||||
consumeToken(): Token {
|
||||
var tok = this.lasttoken = (this.tokens.shift() || this.eol);
|
||||
@@ -624,7 +624,9 @@ export class BASICParser {
|
||||
stmt__IF(): IF_Statement {
|
||||
var cond = this.parseExpr();
|
||||
var iftrue: Statement[];
|
||||
this.expectToken('THEN');
|
||||
// we accept GOTO or THEN if line number provided
|
||||
if (this.peekToken().str == 'GOTO') this.consumeToken();
|
||||
else this.expectToken('THEN');
|
||||
var lineno = this.peekToken();
|
||||
// assume GOTO if number given after THEN
|
||||
if (lineno.type == TokenType.Int) {
|
||||
@@ -890,6 +892,7 @@ export const APPLESOFT_BASIC : BASICOptions = {
|
||||
sparseArrays : false,
|
||||
tickComments : false,
|
||||
validKeywords : [
|
||||
'OPTION',
|
||||
'CLEAR','LET','DIM','DEF','FN','GOTO','GOSUB','RETURN','ON','POP',
|
||||
'FOR','TO','NEXT','IF','THEN','END','STOP','ONERR','RESUME',
|
||||
'PRINT','INPUT','GET','HOME','HTAB','VTAB',
|
||||
@@ -912,12 +915,12 @@ export const APPLESOFT_BASIC : BASICOptions = {
|
||||
bitwiseLogic : false,
|
||||
}
|
||||
|
||||
export const MAX8_BASIC : BASICOptions = {
|
||||
dialectName: "MAX8",
|
||||
export const MODERN_BASIC : BASICOptions = {
|
||||
dialectName: "MODERN",
|
||||
asciiOnly : false,
|
||||
uppercaseOnly : false,
|
||||
optionalLabels : true,
|
||||
strictVarNames : false, // TODO: first two alphanum chars
|
||||
strictVarNames : false,
|
||||
sharedArrayNamespace : false,
|
||||
defaultArrayBase : 0,
|
||||
defaultArraySize : 11,
|
||||
@@ -925,7 +928,7 @@ export const MAX8_BASIC : BASICOptions = {
|
||||
stringConcat : true,
|
||||
typeConvert : true,
|
||||
maxDimensions : 255,
|
||||
maxDefArgs : 255, // TODO: no string FNs
|
||||
maxDefArgs : 255,
|
||||
maxStringLength : 1024, // TODO?
|
||||
sparseArrays : false,
|
||||
tickComments : true,
|
||||
@@ -942,6 +945,7 @@ export const MAX8_BASIC : BASICOptions = {
|
||||
}
|
||||
|
||||
// TODO: integer vars
|
||||
// TODO: short-circuit FOR loop
|
||||
|
||||
export const DIALECTS = {
|
||||
"DEFAULT": ALTAIR_BASIC40,
|
||||
@@ -950,4 +954,5 @@ export const DIALECTS = {
|
||||
"ECMA55": ECMA55_MINIMAL,
|
||||
"MINIMAL": ECMA55_MINIMAL,
|
||||
"APPLESOFT": APPLESOFT_BASIC,
|
||||
"MODERN": MODERN_BASIC,
|
||||
};
|
||||
|
||||
+18
-4
@@ -6,8 +6,15 @@ var readline = require('readline');
|
||||
var rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
//terminal: false
|
||||
terminal: false,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
var inputlines = [];
|
||||
rl.on('line', (line) => {
|
||||
//console.log(`Line from file: ${line}`);
|
||||
inputlines.push(line);
|
||||
});
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
var parser = new BASICParser();
|
||||
@@ -46,10 +53,16 @@ runtime.print = (s:string) => {
|
||||
}
|
||||
runtime.input = async (prompt:string) => {
|
||||
return new Promise( (resolve, reject) => {
|
||||
fs.writeSync(1, "\n");
|
||||
rl.question(prompt, (answer) => {
|
||||
function answered(answer) {
|
||||
var vals = answer.toUpperCase().split(',');
|
||||
console.log(">>>",vals);
|
||||
resolve(vals);
|
||||
}
|
||||
fs.writeSync(1, prompt+"?");
|
||||
if (inputlines.length) {
|
||||
answered(inputlines.shift());
|
||||
} else rl.question(prompt, (answer) => {
|
||||
answered(answer);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -59,7 +72,8 @@ runtime.resume = function() {
|
||||
if (runtime.step()) {
|
||||
if (runtime.running) runtime.resume();
|
||||
} else if (runtime.exited) {
|
||||
rl.close();
|
||||
console.log("*** PROGRAM EXITED ***");
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`### ${e.message} (line ${runtime.getCurrentSourceLocation().label})`);
|
||||
|
||||
+63
-23
@@ -16,15 +16,21 @@ function isUnOp(arg: basic.Expr): arg is basic.UnOp {
|
||||
return (arg as any).op != null && (arg as any).expr != null;
|
||||
}
|
||||
|
||||
// expr2js() options
|
||||
class ExprOptions {
|
||||
isconst?: boolean;
|
||||
locals?: string[];
|
||||
isconst?: boolean; // only allow constant operations
|
||||
novalid?: boolean; // check for valid values when fetching
|
||||
locals?: string[]; // pass local variable names when defining functions
|
||||
}
|
||||
|
||||
interface CompiledStatement {
|
||||
$run?: () => void;
|
||||
}
|
||||
|
||||
function isArray(obj) {
|
||||
return obj != null && (Array.isArray(obj) || obj.BYTES_PER_ELEMENT);
|
||||
}
|
||||
|
||||
export class BASICRuntime {
|
||||
|
||||
program : basic.BASICProgram;
|
||||
@@ -158,7 +164,6 @@ export class BASICRuntime {
|
||||
// skip to next statment
|
||||
this.curpc++;
|
||||
// compile (unless cached) and execute statement
|
||||
this.compileStatement(stmt);
|
||||
this.executeStatement(stmt);
|
||||
return this.running;
|
||||
}
|
||||
@@ -178,6 +183,8 @@ export class BASICRuntime {
|
||||
}
|
||||
}
|
||||
executeStatement(stmt: basic.Statement & CompiledStatement) {
|
||||
// compile (unless cached)
|
||||
this.compileStatement(stmt);
|
||||
// run compiled statement
|
||||
stmt.$run();
|
||||
}
|
||||
@@ -215,6 +222,8 @@ export class BASICRuntime {
|
||||
}
|
||||
|
||||
gosubLabel(label) {
|
||||
if (this.returnStack.length > 65535)
|
||||
this.runtimeError(`I did too many GOSUBs without a RETURN.`)
|
||||
this.returnStack.push(this.curpc);
|
||||
this.gotoLabel(label);
|
||||
}
|
||||
@@ -291,9 +300,22 @@ export class BASICRuntime {
|
||||
if (!expr.args && opts.locals && opts.locals.indexOf(expr.name) >= 0) {
|
||||
return expr.name; // local arg in DEF
|
||||
} else {
|
||||
if (opts.isconst) this.runtimeError(`I expected a constant value here.`);
|
||||
var s = this.assign2js(expr, opts);
|
||||
return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`;
|
||||
var s = '';
|
||||
var qname = JSON.stringify(expr.name);
|
||||
let jsargs = expr.args && expr.args.map((arg) => this.expr2js(arg, opts)).join(', ');
|
||||
if (expr.name.startsWith("FN")) { // is it a user-defined function?
|
||||
// TODO: check argument count?
|
||||
s += `this.getDef(${qname})(${jsargs})`;
|
||||
// TODO: detect recursion?
|
||||
} else if (this.builtins[expr.name]) { // is it a built-in function?
|
||||
this.checkFuncArgs(expr, this.builtins[expr.name]);
|
||||
s += `this.builtins.${expr.name}(${jsargs})`;
|
||||
} else if (expr.args) {
|
||||
s += `this.arrayGet(${qname}, ${jsargs})`;
|
||||
} else { // just a variable
|
||||
s += `this.vars.${expr.name}`;
|
||||
}
|
||||
return opts.novalid ? s : `this.checkValue(${s}, ${qname})`;
|
||||
}
|
||||
} else if (isBinOp(expr)) {
|
||||
var left = this.expr2js(expr.left, opts);
|
||||
@@ -309,18 +331,12 @@ export class BASICRuntime {
|
||||
if (!opts) opts = {};
|
||||
var s = '';
|
||||
var qname = JSON.stringify(expr.name);
|
||||
if (expr.name.startsWith("FN")) { // is it a user-defined function?
|
||||
// TODO: check argument count?
|
||||
let jsargs = expr.args && expr.args.map((arg) => this.expr2js(arg, opts)).join(', ');
|
||||
s += `this.getDef(${qname})(${jsargs})`;
|
||||
// TODO: detect recursion?
|
||||
} else if (this.builtins[expr.name]) { // is it a built-in function?
|
||||
this.checkFuncArgs(expr, this.builtins[expr.name]);
|
||||
let jsargs = expr.args && expr.args.map((arg) => this.expr2js(arg, opts)).join(', ');
|
||||
s += `this.builtins.${expr.name}(${jsargs})`;
|
||||
} else if (expr.args) { // is it a subscript?
|
||||
// TODO: check array bounds?
|
||||
s += `this.getArray(${qname}, ${expr.args.length})`;
|
||||
// is it a function? not allowed
|
||||
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) {
|
||||
s += this.expr2js(expr, {novalid:true}); // check array bounds
|
||||
s += `;this.getArray(${qname}, ${expr.args.length})`;
|
||||
s += expr.args.map((arg) => '[this.ROUND('+this.expr2js(arg, opts)+')]').join('');
|
||||
} else { // just a variable
|
||||
s = `this.vars.${expr.name}`;
|
||||
@@ -388,7 +404,7 @@ export class BASICRuntime {
|
||||
|
||||
convert(name: string, right: number|string) : number|string {
|
||||
if (name.endsWith("$")) {
|
||||
return right+"";
|
||||
return right == null ? "" : right.toString();
|
||||
} else if (typeof right === 'number') {
|
||||
return right;
|
||||
} else {
|
||||
@@ -418,8 +434,9 @@ export class BASICRuntime {
|
||||
this.arrays[name] = new arrcons(dims[0]+1);
|
||||
} else if (dims.length == 2) {
|
||||
this.arrays[name] = new Array(dims[0]+1);
|
||||
for (var i=0; i<dims[0]+1; i++)
|
||||
for (var i=0; i<dims[0]+1; i++) {
|
||||
this.arrays[name][i] = new arrcons(dims[1]+1);
|
||||
}
|
||||
} else {
|
||||
this.runtimeError(`I only support arrays of one or two dimensions.`)
|
||||
}
|
||||
@@ -437,6 +454,25 @@ export class BASICRuntime {
|
||||
return this.arrays[name];
|
||||
}
|
||||
|
||||
arrayGet(name: string, ...indices: number[]) : basic.Value {
|
||||
var arr = this.getArray(name, indices.length);
|
||||
indices = indices.map(Math.round);
|
||||
var v = arr;
|
||||
for (var i=0; i<indices.length; i++) {
|
||||
var idx = indices[i];
|
||||
if (!isArray(v))
|
||||
this.runtimeError(`I tried to lookup ${name}(${indices}) but used too many dimensions.`);
|
||||
if (idx < this.opts.defaultArrayBase)
|
||||
this.runtimeError(`I tried to lookup ${name}(${indices}) but an index was less than ${this.opts.defaultArrayBase}.`);
|
||||
if (idx >= v.length)
|
||||
this.runtimeError(`I tried to lookup ${name}(${indices}) but it exceeded the dimensions of the array.`);
|
||||
v = v[indices[i]];
|
||||
}
|
||||
if (isArray(v)) // i.e. is an array?
|
||||
this.runtimeError(`I tried to lookup ${name}(${indices}) but used too few dimensions.`);
|
||||
return (v as any) as basic.Value;
|
||||
}
|
||||
|
||||
onGotoLabel(value: number, ...labels: string[]) {
|
||||
value = this.ROUND(value);
|
||||
if (value < 1 || value > labels.length)
|
||||
@@ -473,7 +509,11 @@ export class BASICRuntime {
|
||||
var setvals = '';
|
||||
stmt.args.forEach((arg, index) => {
|
||||
var lexpr = this.assign2js(arg);
|
||||
setvals += `valid &= this.isValid(${lexpr} = this.convert(${JSON.stringify(arg.name)}, vals[${index}]));`
|
||||
setvals += `
|
||||
var value = this.convert(${JSON.stringify(arg.name)}, vals[${index}]);
|
||||
valid &= this.isValid(value);
|
||||
${lexpr} = value;
|
||||
`
|
||||
});
|
||||
return `this.running=false;
|
||||
this.input(${prompt}, ${stmt.args.length}).then((vals) => {
|
||||
@@ -619,12 +659,12 @@ export class BASICRuntime {
|
||||
// TODO: memory quota
|
||||
// TODO: useless loop (! 4th edition)
|
||||
// TODO: other 4th edition errors
|
||||
|
||||
// TODO: ecma55 all-or-none input checking?
|
||||
|
||||
// FUNCTIONS
|
||||
|
||||
isValid(obj:number|string) : boolean {
|
||||
if (typeof obj === 'number' && !isNaN(obj))
|
||||
if (typeof obj === 'number' && !isNaN(obj) && isFinite(obj))
|
||||
return true;
|
||||
else if (typeof obj === 'string')
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user