import * as basic from "./compiler"; import { EmuHalt } from "../emu"; function isLiteral(arg: basic.Expr): arg is basic.Literal { return (arg as any).value != null; } function isLookup(arg: basic.Expr): arg is basic.IndOp { return (arg as any).name != null; } function isBinOp(arg: basic.Expr): arg is basic.BinOp { return (arg as any).op != null && (arg as any).left != null && (arg as any).right != null; } function isUnOp(arg: basic.Expr): arg is basic.UnOp { return (arg as any).op != null && (arg as any).expr != null; } class ExprOptions { check: boolean; isconst?: boolean; locals?: string[]; } interface CompiledStatement { $run?: () => void; } export class BASICRuntime { program : basic.BASICProgram; allstmts : basic.Statement[]; line2pc : number[]; pc2line : Map; label2lineidx : {[label : string] : number}; label2pc : {[label : string] : number}; datums : basic.Literal[]; curpc : number; dataptr : number; vars : {}; arrays : {}; forLoops : {}; returnStack : number[]; column : number; abase : number; // array base running : boolean = false; exited : boolean = false; trace : boolean = false; load(program: basic.BASICProgram) { this.program = program; this.label2lineidx = {}; this.label2pc = {}; this.allstmts = []; this.line2pc = []; this.pc2line = new Map(); this.datums = []; // TODO: lines start @ 1? program.lines.forEach((line, idx) => { // make lookup tables if (line.label != null) this.label2lineidx[line.label] = idx; if (line.label != null) this.label2pc[line.label] = this.allstmts.length; this.line2pc.push(this.allstmts.length); this.pc2line.set(this.allstmts.length, idx); // combine all statements into single list line.stmts.forEach((stmt) => this.allstmts.push(stmt)); // parse DATA literals line.stmts.filter((stmt) => stmt.command == 'DATA').forEach((datastmt) => { (datastmt as basic.DATA_Statement).datums.forEach(d => { var functext = this.expr2js(d, {check:true, isconst:true}); // TODO: catch exceptions // TODO: any value doing this ahead of time? var value = new Function(`return ${functext};`).bind(this)(); this.datums.push({value:value}); }); }); // TODO: compile statements? //line.stmts.forEach((stmt) => this.compileStatement(stmt)); }); } reset() { this.curpc = 0; this.dataptr = 0; this.vars = {}; this.arrays = {}; this.forLoops = {}; this.returnStack = []; this.column = 0; this.abase = 1; this.running = true; this.exited = false; } runtimeError(msg : string) { this.curpc--; // we did curpc++ before executing statement // TODO: pass source location to error throw new EmuHalt(`${msg} (line ${this.getLabelForPC(this.curpc)})`); } // TODO: sometimes on next line getLineForPC(pc:number) { var line; do { line = this.pc2line.get(pc); if (line != null) break; } while (--pc >= 0); return line; } getLabelForPC(pc:number) { var lineno = this.getLineForPC(pc); var pgmline = this.program.lines[lineno]; return pgmline ? pgmline.label : '?'; } getStatement() { return this.allstmts[this.curpc]; } step() : boolean { if (!this.running) return false; var stmt = this.getStatement(); // end of program? if (!stmt) { this.running = false; this.exited = true; return false; } if (this.trace) console.log(this.curpc, stmt, this.vars, Object.keys(this.arrays)); // skip to next statment this.curpc++; // compile statement to JS? this.compileStatement(stmt); this.executeStatement(stmt); return this.running; } compileStatement(stmt: basic.Statement & CompiledStatement) { if (stmt.$run == null) { var stmtfn = this['do__' + stmt.command]; if (stmtfn == null) this.runtimeError(`I don't know how to "${stmt.command}".`); var functext = stmtfn.bind(this)(stmt); if (this.trace) console.log(functext); stmt.$run = new Function(functext).bind(this); } } executeStatement(stmt: basic.Statement & CompiledStatement) { // run compiled statement stmt.$run(); } skipToEOL() { do { this.curpc++; } while (this.curpc < this.allstmts.length && !this.pc2line.get(this.curpc)); } skipToEOF() { this.curpc = this.allstmts.length; } gotoLabel(label) { var pc = this.label2pc[label]; if (pc >= 0) { this.curpc = pc; } else { this.runtimeError(`I tried to go to the label "${label}" but couldn't find it.`); } } gosubLabel(label) { this.returnStack.push(this.curpc + 1); this.gotoLabel(label); } returnFromGosub() { if (this.returnStack.length == 0) this.runtimeError("I tried to RETURN, but there wasn't a corresponding GOSUB."); // RETURN BEFORE GOSUB var pc = this.returnStack.pop(); this.curpc = pc; } valueToString(obj) : string { var str; if (typeof obj === 'number') { var numstr = obj.toString().toUpperCase(); var prec = 11; while (numstr.length > 11) { numstr = obj.toPrecision(prec--); } if (numstr.startsWith('0.')) numstr = numstr.substr(1); else if (numstr.startsWith('-0.')) numstr = '-'+numstr.substr(2); if (numstr.startsWith('-')) { str = `${numstr} `; } else { str = ` ${numstr} `; } } else if (obj == '\n') { this.column = 0; str = obj; } else if (obj == '\t') { var curgroup = Math.floor(this.column / 15); var nextcol = (curgroup + 1) * 15; str = this.TAB(nextcol); } else { str = `${obj}`; } return str; } printExpr(obj) { var str = this.valueToString(obj); this.column += str.length; this.print(str); } // override this print(str: string) { console.log(str); } // override this async input(prompt: string, nargs: number) : Promise { return []; } // override this resume() { } expr2js(expr: basic.Expr, opts: ExprOptions) : string { if (isLiteral(expr)) { return JSON.stringify(expr.value); } else if (isLookup(expr)) { if (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 = ''; if (expr.args && this[expr.name]) { // is it a function? s += `this.${expr.name}(`; s += expr.args.map((arg) => this.expr2js(arg, opts)).join(', '); s += ')'; } else if (expr.args) { // is it a subscript? s += `this.getArray(${JSON.stringify(expr.name)}, ${expr.args.length})`; s += expr.args.map((arg) => '[this.ROUND('+this.expr2js(arg, opts)+')]').join(''); } else { // just a variable s = `this.vars.${expr.name}`; } if (opts.check) return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`; // TODO: better error else return s; } } else if (isBinOp(expr)) { var left = this.expr2js(expr.left, opts); var right = this.expr2js(expr.right, opts); return `this.${expr.op}(${left}, ${right})`; } else if (isUnOp(expr) && expr.op == 'neg') { var e = this.expr2js(expr.expr, opts); return `-(${e})`; // TODO: other ops? } } startForLoop(name, init, targ, step) { // TODO: check for loop params var pc = this.curpc; if (!step) step = 1; this.vars[name] = init; if (this.trace) console.log(`FOR ${name} = ${this.vars[name]} TO ${targ} STEP ${step}`); this.forLoops[name] = { next: () => { var done = step >= 0 ? this.vars[name] >= targ : this.vars[name] <= targ; if (done) { delete this.forLoops[name]; } else { this.vars[name] += step; this.curpc = pc; } if (this.trace) console.log(`NEXT ${name}: ${this.vars[name]} TO ${targ} STEP ${step} DONE=${done}`); } }; } nextForLoop(name) { // TODO: check for for loop var fl = this.forLoops[name]; if (!fl) this.runtimeError(`I couldn't find a matching FOR for this NEXT.`) this.forLoops[name].next(); } // converts a variable to string/number based on var name convert(name: string, right: number|string) : number|string { // TODO: error check? if (name.endsWith("$")) return right+""; else if (typeof right === 'string') return parseFloat(right); else if (typeof right === 'number') return right; else return this.checkValue(right, name); } // dimension array dimArray(name: string, ...dims:number[]) { var isstring = name.endsWith('$'); // TODO: option for undefined float array elements? var arrcons = isstring ? Array : Float64Array; var ab = this.abase; if (dims.length == 1) { this.arrays[name] = new arrcons(dims[0]+ab); } else if (dims.length == 2) { this.arrays[name] = new Array(dims[0]+ab); for (var i=ab; i labels.length) this.runtimeError(`I needed a number between 1 and ${labels.length}, but I got ${value}.`); this.gotoLabel(labels[value-1]); } nextDatum() : string|number { if (this.dataptr >= this.datums.length) this.runtimeError("I tried to READ, but ran out of data."); return this.datums[this.dataptr++].value; } //// STATEMENTS do__PRINT(stmt : basic.PRINT_Statement) { var s = ''; for (var arg of stmt.args) { var expr = this.expr2js(arg, {check:true}); s += `this.printExpr(${expr});`; } return s; } do__INPUT(stmt : basic.INPUT_Statement) { var prompt = this.expr2js(stmt.prompt, {check:true}); var setvals = ''; stmt.args.forEach((arg, index) => { var lexpr = this.expr2js(arg, {check:false}); setvals += `${lexpr} = this.convert(${JSON.stringify(arg.name)}, vals[${index}]);` }); return `this.running=false; this.input(${prompt}, ${stmt.args.length}).then((vals) => {${setvals}; this.running=true; this.resume();})`; } do__LET(stmt : basic.LET_Statement) { var lexpr = this.expr2js(stmt.lexpr, {check:false}); var right = this.expr2js(stmt.right, {check:true}); return `${lexpr} = this.convert(${JSON.stringify(stmt.lexpr.name)}, ${right});`; } do__FOR(stmt : basic.FOR_Statement) { var name = JSON.stringify(stmt.lexpr.name); // TODO: args? var init = this.expr2js(stmt.initial, {check:true}); var targ = this.expr2js(stmt.target, {check:true}); var step = stmt.step ? this.expr2js(stmt.step, {check:true}) : 'null'; return `this.startForLoop(${name}, ${init}, ${targ}, ${step})`; } do__NEXT(stmt : basic.NEXT_Statement) { var name = JSON.stringify(stmt.lexpr.name); // TODO: args? lexpr == null? return `this.nextForLoop(${name})`; } do__IF(stmt : basic.IF_Statement) { var cond = this.expr2js(stmt.cond, {check:true}); return `if (!(${cond})) { this.skipToEOL(); }` } do__DEF(stmt : basic.DEF_Statement) { var lexpr = `this.${stmt.lexpr.name}`; var args = []; for (var arg of stmt.lexpr.args) { if (isLookup(arg)) { args.push(arg.name); } else { this.runtimeError("I found a DEF statement with arguments other than variable names."); } } var functext = this.expr2js(stmt.def, {check:true, locals:args}); // TODO: use stmt.args to add function params return `${lexpr} = function(${args.join(',')}) { return ${functext}; }`; } _DIM(dim : basic.IndOp) { var argsstr = ''; for (var arg of dim.args) { // TODO: check for float argsstr += ', ' + this.expr2js(arg, {check:true}); } return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`; } do__DIM(stmt : basic.DIM_Statement) { var s = ''; stmt.args.forEach((dim) => s += this._DIM(dim)); return s; } do__GOTO(stmt : basic.GOTO_Statement) { var label = this.expr2js(stmt.label, {check:true}); return `this.gotoLabel(${label})`; } do__GOSUB(stmt : basic.GOSUB_Statement) { var label = this.expr2js(stmt.label, {check:true}); return `this.gosubLabel(${label})`; } do__RETURN(stmt : basic.RETURN_Statement) { return `this.returnFromGosub()`; } do__ONGOTO(stmt : basic.ONGOTO_Statement) { var expr = this.expr2js(stmt.expr, {check:true}); var labels = stmt.labels.map((arg) => this.expr2js(arg, {check:true})).join(', '); return `this.onGotoLabel(${expr}, ${labels})`; } do__DATA() { // data is preprocessed } do__READ(stmt : basic.READ_Statement) { var s = ''; stmt.args.forEach((arg) => { s += `${this.expr2js(arg, {check:false})} = this.convert(${JSON.stringify(arg.name)}, this.nextDatum());`; }); return s; } do__RESTORE() { this.dataptr = 0; // TODO: line number? } do__END() { return `this.skipToEOF()`; } do__STOP() { return `this.skipToEOF()`; } do__OPTION(stmt: basic.OPTION_Statement) { switch (stmt.optname) { case 'BASE': let base = parseInt(stmt.optargs[0]); if (base == 0 || base == 1) this.abase = base; else this.runtimeError("OPTION BASE can only be 0 or 1."); break; default: this.runtimeError(`OPTION ${stmt.optname} is not supported by this compiler.`); break; } } // TODO: "SUBSCRIPT ERROR" // TODO: gosubs nested too deeply // TODO: memory quota // FUNCTIONS checkValue(obj:number|string, exprname:string) { if (typeof obj !== 'number' && typeof obj !== 'string') { if (exprname != null && obj == null) { this.runtimeError(`I didn't find a value for ${exprname}`); } else if (exprname != null) { this.runtimeError(`I got an invalid value for ${exprname}: ${obj}`); } else { this.runtimeError(`I got an invalid value: ${obj}`); } } return obj; } checkNum(n:number) : number { 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 { if (typeof s !== 'string') this.runtimeError(`I expected a string here.`); return s; } add(a, b) : number|string { // TODO: if string-concat if (typeof a === 'number' && typeof b === 'number') return this.checkNum(a + b); else return a + b; } sub(a:number, b:number) : number { return this.checkNum(a - b); } mul(a:number, b:number) : number { return this.checkNum(a * b); } div(a:number, b:number) : number { if (b == 0) this.runtimeError(`I can't divide by zero.`); return this.checkNum(a / b); } pow(a:number, b:number) : number { if (a == 0 && b < 0) this.runtimeError(`I can't raise zero to a negative power.`); return this.checkNum(Math.pow(a, b)); } land(a:number, b:number) : number { return a && b; } lor(a:number, b:number) : number { return a || b; } eq(a:number, b:number) : boolean { return a == b; } ne(a:number, b:number) : boolean { return a != b; } lt(a:number, b:number) : boolean { return a < b; } gt(a:number, b:number) : boolean { return a > b; } le(a:number, b:number) : boolean { return a <= b; } ge(a:number, b:number) : boolean { return a >= b; } // FUNCTIONS (uppercase) ABS(arg : number) : number { return this.checkNum(Math.abs(arg)); } ASC(arg : string) : number { return arg.charCodeAt(0); } ATN(arg : number) : number { return this.checkNum(Math.atan(arg)); } CHR$(arg : number) : string { return String.fromCharCode(this.checkNum(arg)); } COS(arg : number) : number { return this.checkNum(Math.cos(arg)); } EXP(arg : number) : number { return this.checkNum(Math.exp(arg)); } FIX(arg : number) : number { return this.checkNum(arg - Math.floor(arg)); } HEX$(arg : number) : string { return arg.toString(16); } INSTR(a, b, c) : number { if (c != null) { return this.checkString(c).indexOf(this.checkString(b), a) + 1; } else { return this.checkString(b).indexOf(this.checkString(a)) + 1; } } INT(arg : number) : number { return this.checkNum(Math.floor(arg)); } LEFT$(arg : string, count : number) : string { return arg.substr(0, count); } LEN(arg : string) : number { return arg.length; } LOG(arg : number) : number { if (arg == 0) this.runtimeError(`I can't take the logarithm of zero (${arg}).`) if (arg < 0) this.runtimeError(`I can't take the logarithm of a negative number (${arg}).`) return this.checkNum(Math.log(arg)); } MID$(arg : string, start : number, count : number) : string { if (start < 1) this.runtimeError(`The second parameter to MID$ must be between 1 and the length of the string in the first parameter.`) return arg.substr(start-1, count); } RIGHT$(arg : string, count : number) : string { return arg.substr(arg.length - count, count); } RND(arg : number) : number { return Math.random(); // argument ignored } ROUND(arg : number) : number { return this.checkNum(Math.round(arg)); // TODO? } SGN(arg : number) : number { return (arg < 0) ? -1 : (arg > 0) ? 1 : 0; } SIN(arg : number) : number { return this.checkNum(Math.sin(arg)); } SPACE$(arg : number) : string { return ' '.repeat(this.checkNum(arg)); } SQR(arg : number) : number { if (arg < 0) this.runtimeError(`I can't take the square root of a negative number (${arg}).`) return this.checkNum(Math.sqrt(arg)); } STR$(arg) : string { return this.valueToString(arg); } TAB(arg : number) : string { if (arg < 0) this.runtimeError(`I got a negative value for the TAB() function.`); var spaces = this.ROUND(arg) - this.column; return (spaces > 0) ? ' '.repeat(spaces) : ''; } TAN(arg : number) : number { return this.checkNum(Math.tan(arg)); } VAL(arg) : number { return parseFloat(arg+""); } }