2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
import * as basic from "./compiler";
|
|
|
|
import { EmuHalt } from "../emu";
|
2020-08-09 21:32:52 +00:00
|
|
|
import { SourceLocation } from "../workertypes";
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-08-21 03:20:53 +00:00
|
|
|
export interface InputResponse {
|
|
|
|
line: string;
|
|
|
|
vals: string[];
|
|
|
|
elapsed?: number;
|
|
|
|
}
|
|
|
|
|
2020-08-10 16:08:43 +00:00
|
|
|
// expr2js() options
|
2020-08-05 04:48:29 +00:00
|
|
|
class ExprOptions {
|
2020-08-10 16:08:43 +00:00
|
|
|
isconst?: boolean; // only allow constant operations
|
|
|
|
novalid?: boolean; // check for valid values when fetching
|
|
|
|
locals?: string[]; // pass local variable names when defining functions
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
interface CompiledStatement {
|
|
|
|
$run?: () => void;
|
|
|
|
}
|
|
|
|
|
2020-08-10 16:08:43 +00:00
|
|
|
function isArray(obj) {
|
|
|
|
return obj != null && (Array.isArray(obj) || obj.BYTES_PER_ELEMENT);
|
|
|
|
}
|
|
|
|
|
2020-08-12 16:57:54 +00:00
|
|
|
class RNG {
|
|
|
|
next : () => number;
|
|
|
|
seed : (aa,bb,cc,dd) => void;
|
2020-08-14 21:21:32 +00:00
|
|
|
seedfloat : (n) => void;
|
2020-08-12 16:57:54 +00:00
|
|
|
randomize() {
|
|
|
|
this.seed(Math.random()*0x7fffffff, Math.random()*0x7fffffff, Math.random()*0x7fffffff, Math.random()*0x7fffffff);
|
|
|
|
}
|
|
|
|
constructor() {
|
|
|
|
let f = () => {
|
|
|
|
var a, b, c, d : number;
|
|
|
|
this.seed = function(aa,bb,cc,dd) {
|
|
|
|
a = aa; b = bb; c = cc; d = dd;
|
|
|
|
}
|
2020-08-14 21:21:32 +00:00
|
|
|
this.seedfloat = function(n) {
|
|
|
|
this.seed(n, n*4294, n*429496, n*4294967296);
|
|
|
|
this.next(); this.next(); this.next();
|
|
|
|
}
|
2020-08-12 16:57:54 +00:00
|
|
|
this.next = function() {
|
|
|
|
// sfc32
|
|
|
|
a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
|
|
|
|
var t = (a + b) | 0;
|
|
|
|
a = b ^ b >>> 9;
|
|
|
|
b = c + (c << 3) | 0;
|
|
|
|
c = (c << 21 | c >>> 11);
|
|
|
|
d = d + 1 | 0;
|
|
|
|
t = t + d | 0;
|
|
|
|
c = c + t | 0;
|
|
|
|
return (t >>> 0) / 4294967296;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
f();
|
2020-08-14 21:21:32 +00:00
|
|
|
this.seedfloat(-1);
|
2020-08-12 16:57:54 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-08-15 20:03:56 +00:00
|
|
|
const DEFAULT_MAX_ARRAY_ELEMENTS = 1024*1024;
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
export class BASICRuntime {
|
|
|
|
|
|
|
|
program : basic.BASICProgram;
|
|
|
|
allstmts : basic.Statement[];
|
2020-08-15 11:53:13 +00:00
|
|
|
pc2label : Map<number,string>;
|
2020-08-05 04:48:29 +00:00
|
|
|
label2pc : {[label : string] : number};
|
2020-08-14 04:34:54 +00:00
|
|
|
label2dataptr : {[label : string] : number};
|
2020-08-05 04:48:29 +00:00
|
|
|
datums : basic.Literal[];
|
2020-08-09 02:36:21 +00:00
|
|
|
builtins : {};
|
2020-08-09 16:23:49 +00:00
|
|
|
opts : basic.BASICOptions;
|
2020-08-14 21:21:32 +00:00
|
|
|
margin : number = 80; // number of columns
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
curpc : number;
|
|
|
|
dataptr : number;
|
2020-08-24 16:51:47 +00:00
|
|
|
vars : {[name:string] : any}; // actually Value, but += doesn't work
|
|
|
|
globals : {[name:string] : any};
|
2020-08-05 04:48:29 +00:00
|
|
|
arrays : {};
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
defs : {};
|
2020-08-24 16:51:47 +00:00
|
|
|
subroutines : {};
|
2020-08-13 17:32:47 +00:00
|
|
|
forLoops : { [varname:string] : { $next:(name:string) => void } };
|
|
|
|
forLoopStack: string[];
|
2020-08-12 16:57:54 +00:00
|
|
|
whileLoops : number[];
|
2020-08-05 04:48:29 +00:00
|
|
|
returnStack : number[];
|
|
|
|
column : number;
|
2020-08-12 16:57:54 +00:00
|
|
|
rng : RNG;
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
running : boolean = false;
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
exited : boolean = true;
|
2020-08-05 04:48:29 +00:00
|
|
|
trace : boolean = false;
|
|
|
|
|
2020-08-15 11:53:13 +00:00
|
|
|
load(program: basic.BASICProgram) : boolean {
|
|
|
|
// get previous label and offset for hot reload
|
|
|
|
let prevlabel = null;
|
|
|
|
let prevpcofs = 0;
|
|
|
|
if (this.pc2label != null) {
|
2020-08-19 17:05:30 +00:00
|
|
|
let pc = this.curpc;
|
2020-08-15 11:53:13 +00:00
|
|
|
while (pc > 0 && (prevlabel = this.pc2label.get(pc)) == null) {
|
|
|
|
pc--;
|
|
|
|
}
|
|
|
|
prevpcofs = this.curpc - pc;
|
|
|
|
console.log('oldpc=', this.curpc, 'restart @ label', prevlabel, '+', prevpcofs);
|
|
|
|
}
|
|
|
|
// initialize program
|
2020-08-05 04:48:29 +00:00
|
|
|
this.program = program;
|
2020-08-09 16:23:49 +00:00
|
|
|
this.opts = program.opts;
|
2020-08-15 20:03:56 +00:00
|
|
|
if (!this.opts.maxArrayElements) this.opts.maxArrayElements = DEFAULT_MAX_ARRAY_ELEMENTS;
|
2020-08-19 17:05:30 +00:00
|
|
|
this.allstmts = program.stmts;
|
|
|
|
this.label2pc = program.labels;
|
2020-08-14 04:34:54 +00:00
|
|
|
this.label2dataptr = {};
|
2020-08-15 11:53:13 +00:00
|
|
|
this.pc2label = new Map();
|
2020-08-05 04:48:29 +00:00
|
|
|
this.datums = [];
|
2020-08-24 16:51:47 +00:00
|
|
|
this.subroutines = {};
|
2020-08-09 02:36:21 +00:00
|
|
|
this.builtins = this.getBuiltinFunctions();
|
2020-08-13 17:51:35 +00:00
|
|
|
// TODO: detect undeclared vars
|
2020-08-19 17:05:30 +00:00
|
|
|
// build PC -> label lookup
|
|
|
|
for (var label in program.labels) {
|
|
|
|
var targetpc = program.labels[label];
|
|
|
|
this.pc2label.set(targetpc, label);
|
|
|
|
}
|
2020-08-24 16:51:47 +00:00
|
|
|
// iterate through all the statements
|
2020-08-11 17:29:31 +00:00
|
|
|
this.allstmts.forEach((stmt, pc) => {
|
2020-08-24 16:51:47 +00:00
|
|
|
// compile statements ahead of time
|
2020-08-11 17:29:31 +00:00
|
|
|
this.curpc = pc + 1; // for error reporting
|
|
|
|
this.compileStatement(stmt);
|
2020-08-24 16:51:47 +00:00
|
|
|
// parse DATA literals
|
|
|
|
if (stmt.command == 'DATA') {
|
|
|
|
this.label2dataptr[stmt.$loc.label] = this.datums.length;
|
|
|
|
(stmt as basic.DATA_Statement).datums.forEach(datum => {
|
|
|
|
this.curpc = stmt.$loc.offset; // for error reporting
|
|
|
|
this.datums.push(datum);
|
|
|
|
});
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
});
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
// try to resume where we left off after loading
|
2020-08-15 11:53:13 +00:00
|
|
|
if (this.label2pc[prevlabel] != null) {
|
|
|
|
this.curpc = this.label2pc[prevlabel] + prevpcofs;
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
this.curpc = 0;
|
|
|
|
return false;
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
reset() {
|
|
|
|
this.curpc = 0;
|
|
|
|
this.dataptr = 0;
|
2020-08-10 13:07:09 +00:00
|
|
|
this.clearVars();
|
2020-08-05 04:48:29 +00:00
|
|
|
this.returnStack = [];
|
|
|
|
this.column = 0;
|
|
|
|
this.running = true;
|
|
|
|
this.exited = false;
|
|
|
|
}
|
2020-08-10 13:07:09 +00:00
|
|
|
clearVars() {
|
2020-08-24 16:51:47 +00:00
|
|
|
this.globals = this.vars = {};
|
2020-08-10 13:07:09 +00:00
|
|
|
this.arrays = {};
|
|
|
|
this.defs = {}; // TODO? only in interpreters
|
2020-08-11 17:29:31 +00:00
|
|
|
this.forLoops = {};
|
2020-08-13 17:32:47 +00:00
|
|
|
this.forLoopStack = [];
|
2020-08-12 16:57:54 +00:00
|
|
|
this.whileLoops = [];
|
|
|
|
this.rng = new RNG();
|
2020-08-11 17:29:31 +00:00
|
|
|
// initialize arrays?
|
|
|
|
if (this.opts && this.opts.staticArrays) {
|
|
|
|
this.allstmts.filter((stmt) => stmt.command == 'DIM').forEach((dimstmt: basic.DIM_Statement) => {
|
|
|
|
dimstmt.args.forEach( (arg) => this.compileJS(this._DIM(arg))() );
|
|
|
|
});
|
|
|
|
}
|
2020-08-10 13:07:09 +00:00
|
|
|
}
|
|
|
|
|
2020-08-12 16:57:54 +00:00
|
|
|
// TODO: saveState(), loadState()
|
2020-08-13 17:32:47 +00:00
|
|
|
saveState() {
|
|
|
|
// TODO: linked list loop?
|
|
|
|
return $.extend(true, {}, this);
|
|
|
|
}
|
|
|
|
loadState(state) {
|
|
|
|
$.extend(true, this, state);
|
|
|
|
}
|
2020-08-12 16:57:54 +00:00
|
|
|
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
getBuiltinFunctions() {
|
2020-08-09 16:23:49 +00:00
|
|
|
var fnames = this.program && this.opts.validFunctions;
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
// 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 = {};
|
2020-08-15 20:03:56 +00:00
|
|
|
for (var fn of fnames) if (this.supportsFunction(fn)) dict[fn] = this[fn].bind(this);
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
return dict;
|
|
|
|
}
|
2020-08-15 20:03:56 +00:00
|
|
|
supportsFunction(fnname: string) {
|
|
|
|
return typeof this[fnname] === 'function';
|
|
|
|
}
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
runtimeError(msg : string) {
|
|
|
|
this.curpc--; // we did curpc++ before executing statement
|
2020-08-09 21:32:52 +00:00
|
|
|
throw new EmuHalt(msg, this.getCurrentSourceLocation());
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-09 21:32:52 +00:00
|
|
|
|
2020-08-09 02:36:21 +00:00
|
|
|
dialectError(what : string) {
|
|
|
|
this.runtimeError(`I can't ${what} in this dialect of BASIC.`);
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
getLineForPC(pc:number) {
|
2020-08-10 13:07:09 +00:00
|
|
|
var stmt = this.allstmts[pc];
|
|
|
|
return stmt && stmt.$loc && stmt.$loc.line;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
getLabelForPC(pc:number) {
|
2020-08-10 13:07:09 +00:00
|
|
|
var stmt = this.allstmts[pc];
|
|
|
|
return stmt && stmt.$loc && stmt.$loc.label;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
2020-08-09 21:32:52 +00:00
|
|
|
getCurrentSourceLocation() : SourceLocation {
|
|
|
|
var stmt = this.getStatement();
|
|
|
|
return stmt && stmt.$loc;
|
|
|
|
}
|
|
|
|
|
2020-08-10 13:07:09 +00:00
|
|
|
getCurrentLabel() : string {
|
|
|
|
var loc = this.getCurrentSourceLocation();
|
|
|
|
return loc && loc.label;
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
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++;
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
// compile (unless cached) and execute statement
|
2020-08-05 04:48:29 +00:00
|
|
|
this.executeStatement(stmt);
|
|
|
|
return this.running;
|
|
|
|
}
|
|
|
|
|
|
|
|
compileStatement(stmt: basic.Statement & CompiledStatement) {
|
|
|
|
if (stmt.$run == null) {
|
2020-08-09 02:36:21 +00:00
|
|
|
try {
|
|
|
|
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);
|
2020-08-11 17:29:31 +00:00
|
|
|
stmt.$run = this.compileJS(functext);
|
2020-08-09 02:36:21 +00:00
|
|
|
} catch (e) {
|
2020-08-15 20:03:56 +00:00
|
|
|
if (functext) console.log(functext);
|
2020-08-09 02:36:21 +00:00
|
|
|
throw e;
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-11 17:29:31 +00:00
|
|
|
compileJS(functext: string) : () => void {
|
|
|
|
return new Function(functext).bind(this);
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
executeStatement(stmt: basic.Statement & CompiledStatement) {
|
2020-08-10 16:08:43 +00:00
|
|
|
// compile (unless cached)
|
|
|
|
this.compileStatement(stmt);
|
2020-08-05 04:48:29 +00:00
|
|
|
// run compiled statement
|
|
|
|
stmt.$run();
|
|
|
|
}
|
|
|
|
|
2020-08-19 17:05:30 +00:00
|
|
|
// TODO: this only works because each line has a label
|
2020-08-05 04:48:29 +00:00
|
|
|
skipToEOL() {
|
|
|
|
do {
|
|
|
|
this.curpc++;
|
2020-08-19 17:05:30 +00:00
|
|
|
} while (this.curpc < this.allstmts.length && !this.pc2label.get(this.curpc));
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
2020-08-09 02:36:21 +00:00
|
|
|
skipToElse() {
|
2020-08-15 20:03:56 +00:00
|
|
|
while (this.curpc < this.allstmts.length) {
|
2020-08-09 02:36:21 +00:00
|
|
|
// in Altair BASIC, ELSE is bound to the right-most IF
|
|
|
|
// TODO: this is complicated, we should just have nested expressions
|
2020-08-12 16:57:54 +00:00
|
|
|
var cmd = this.allstmts[this.curpc].command;
|
|
|
|
if (cmd == 'ELSE') { this.curpc++; break; }
|
|
|
|
else if (cmd == 'IF') return this.skipToEOL();
|
2020-08-09 02:36:21 +00:00
|
|
|
this.curpc++;
|
2020-08-19 17:05:30 +00:00
|
|
|
if (this.pc2label.get(this.curpc))
|
2020-08-15 20:03:56 +00:00
|
|
|
break;
|
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
skipToEOF() {
|
|
|
|
this.curpc = this.allstmts.length;
|
|
|
|
}
|
|
|
|
|
2020-08-11 17:29:31 +00:00
|
|
|
skipToAfterNext(forname: string) : void {
|
|
|
|
var pc = this.curpc;
|
|
|
|
while (pc < this.allstmts.length) {
|
|
|
|
var stmt = this.allstmts[pc];
|
|
|
|
if (stmt.command == 'NEXT') {
|
|
|
|
var nextlexpr = (stmt as basic.NEXT_Statement).lexpr;
|
|
|
|
if (nextlexpr && nextlexpr.name == forname) {
|
|
|
|
this.curpc = pc + 1;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
pc++;
|
|
|
|
}
|
|
|
|
this.runtimeError(`I couldn't find a matching NEXT ${forname} to skip this for loop.`);
|
|
|
|
}
|
|
|
|
|
2020-08-12 16:57:54 +00:00
|
|
|
skipToAfterWend() {
|
|
|
|
var pc = this.curpc - 1;
|
|
|
|
var nesting = 0;
|
|
|
|
while (pc < this.allstmts.length) {
|
|
|
|
var stmt = this.allstmts[pc];
|
2020-08-15 20:03:56 +00:00
|
|
|
//console.log(nesting, pc, stmt);
|
2020-08-12 16:57:54 +00:00
|
|
|
if (stmt.command == 'WHILE') {
|
|
|
|
nesting++;
|
|
|
|
} else if (stmt.command == 'WEND') {
|
|
|
|
nesting--;
|
|
|
|
if (nesting == 0) {
|
|
|
|
this.curpc = pc + 1;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
pc++;
|
|
|
|
}
|
|
|
|
this.runtimeError(`I couldn't find a matching WEND for this WHILE.`);
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
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.`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-24 16:51:47 +00:00
|
|
|
newLocalScope() {
|
|
|
|
this.vars = Object.create(this.vars);
|
|
|
|
}
|
|
|
|
|
|
|
|
popLocalScope() {
|
|
|
|
if (this.vars !== this.globals)
|
|
|
|
this.vars = Object.getPrototypeOf(this.vars);
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
gosubLabel(label) {
|
2020-08-12 16:57:54 +00:00
|
|
|
if (this.returnStack.length > 32767) // TODO: const?
|
2020-08-10 16:08:43 +00:00
|
|
|
this.runtimeError(`I did too many GOSUBs without a RETURN.`)
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
this.returnStack.push(this.curpc);
|
2020-08-05 04:48:29 +00:00
|
|
|
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;
|
2020-08-24 16:51:47 +00:00
|
|
|
this.popLocalScope();
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
|
|
|
|
popReturnStack() {
|
|
|
|
if (this.returnStack.length == 0)
|
|
|
|
this.runtimeError("I tried to POP, but there wasn't a corresponding GOSUB.");
|
|
|
|
this.returnStack.pop();
|
|
|
|
}
|
2020-08-10 13:07:09 +00:00
|
|
|
|
2020-08-22 16:40:58 +00:00
|
|
|
valueToString(obj:basic.Value, padding:boolean) : string {
|
2020-08-05 04:48:29 +00:00
|
|
|
var str;
|
|
|
|
if (typeof obj === 'number') {
|
2020-08-14 21:21:32 +00:00
|
|
|
var numstr = this.float2str(obj, this.opts.printZoneLength - 4);
|
2020-08-22 16:40:58 +00:00
|
|
|
if (!padding)
|
2020-08-14 21:21:32 +00:00
|
|
|
return numstr;
|
2020-08-09 02:36:21 +00:00
|
|
|
else if (numstr.startsWith('-'))
|
2020-08-14 21:21:32 +00:00
|
|
|
return `${numstr} `;
|
2020-08-09 02:36:21 +00:00
|
|
|
else
|
2020-08-14 21:21:32 +00:00
|
|
|
return ` ${numstr} `;
|
2020-08-05 04:48:29 +00:00
|
|
|
} else if (obj == '\n') {
|
|
|
|
this.column = 0;
|
|
|
|
str = obj;
|
|
|
|
} else if (obj == '\t') {
|
2020-08-19 17:05:30 +00:00
|
|
|
var l = this.opts.printZoneLength;
|
|
|
|
var curgroup = Math.floor(this.column / l);
|
2020-08-09 16:23:49 +00:00
|
|
|
var nextcol = (curgroup + 1) * this.opts.printZoneLength;
|
2020-08-19 17:05:30 +00:00
|
|
|
if (nextcol+l > this.margin) { this.column = 0; str = "\n"; } // return to left margin
|
2020-08-14 21:21:32 +00:00
|
|
|
else str = this.TAB(nextcol); // next column
|
2020-08-05 04:48:29 +00:00
|
|
|
} else {
|
|
|
|
str = `${obj}`;
|
|
|
|
}
|
|
|
|
return str;
|
|
|
|
}
|
|
|
|
|
2020-08-14 21:21:32 +00:00
|
|
|
float2str(arg: number, numlen: number) : string {
|
|
|
|
var numstr = arg.toString().toUpperCase();
|
|
|
|
if (numlen > 0) {
|
|
|
|
var prec = numlen;
|
|
|
|
while (numstr.length > numlen) {
|
|
|
|
numstr = arg.toPrecision(prec--);
|
|
|
|
}
|
|
|
|
if (numstr.startsWith('0.'))
|
|
|
|
numstr = numstr.substr(1);
|
|
|
|
else if (numstr.startsWith('-0.'))
|
|
|
|
numstr = '-'+numstr.substr(2);
|
|
|
|
}
|
|
|
|
return numstr;
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
printExpr(obj) {
|
2020-08-22 16:40:58 +00:00
|
|
|
var str = this.valueToString(obj, this.opts.numericPadding);
|
2020-08-05 04:48:29 +00:00
|
|
|
this.column += str.length;
|
|
|
|
this.print(str);
|
|
|
|
}
|
|
|
|
|
|
|
|
// override this
|
|
|
|
print(str: string) {
|
|
|
|
console.log(str);
|
|
|
|
}
|
|
|
|
|
|
|
|
// override this
|
2020-08-21 03:20:53 +00:00
|
|
|
async input(prompt: string, nargs: number) : Promise<InputResponse> {
|
|
|
|
return {line:"", vals:[]};
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// override this
|
|
|
|
resume() { }
|
|
|
|
|
2020-08-09 16:23:49 +00:00
|
|
|
expr2js(expr: basic.Expr, opts?: ExprOptions) : string {
|
|
|
|
if (!opts) opts = {};
|
2020-08-05 04:48:29 +00:00
|
|
|
if (isLiteral(expr)) {
|
|
|
|
return JSON.stringify(expr.value);
|
|
|
|
} else if (isLookup(expr)) {
|
2020-08-09 16:23:49 +00:00
|
|
|
if (!expr.args && opts.locals && opts.locals.indexOf(expr.name) >= 0) {
|
2020-08-05 04:48:29 +00:00
|
|
|
return expr.name; // local arg in DEF
|
|
|
|
} else {
|
2020-08-11 17:29:31 +00:00
|
|
|
if (opts.isconst)
|
|
|
|
this.runtimeError(`I expected a constant value here.`); // TODO: check at compile-time?
|
2020-08-10 16:08:43 +00:00
|
|
|
var s = '';
|
|
|
|
var qname = JSON.stringify(expr.name);
|
2020-08-11 17:29:31 +00:00
|
|
|
let jsargs = expr.args ? expr.args.map((arg) => this.expr2js(arg, opts)).join(', ') : [];
|
2020-08-10 16:08:43 +00:00
|
|
|
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) {
|
2020-08-14 14:26:43 +00:00
|
|
|
// get array slice (HP BASIC)
|
2020-08-13 17:51:35 +00:00
|
|
|
if (this.opts.arraysContainChars && expr.name.endsWith('$'))
|
2020-08-14 21:21:32 +00:00
|
|
|
s += `this.getStringSlice(this.vars.${expr.name}, ${jsargs})`;
|
2020-08-13 17:51:35 +00:00
|
|
|
else
|
|
|
|
s += `this.arrayGet(${qname}, ${jsargs})`;
|
2020-08-10 16:08:43 +00:00
|
|
|
} else { // just a variable
|
|
|
|
s += `this.vars.${expr.name}`;
|
|
|
|
}
|
|
|
|
return opts.novalid ? s : `this.checkValue(${s}, ${qname})`;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
} else if (isBinOp(expr)) {
|
|
|
|
var left = this.expr2js(expr.left, opts);
|
|
|
|
var right = this.expr2js(expr.right, opts);
|
|
|
|
return `this.${expr.op}(${left}, ${right})`;
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
} else if (isUnOp(expr)) {
|
2020-08-05 04:48:29 +00:00
|
|
|
var e = this.expr2js(expr.expr, opts);
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
return `this.${expr.op}(${e})`;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-15 11:53:13 +00:00
|
|
|
assign2js(expr: basic.IndOp, opts?: ExprOptions) : string {
|
2020-08-09 16:23:49 +00:00
|
|
|
if (!opts) opts = {};
|
|
|
|
var s = '';
|
2020-08-10 16:08:43 +00:00
|
|
|
// is it a function? not allowed
|
2020-08-15 20:03:56 +00:00
|
|
|
if (expr.name.startsWith("FN") || this.builtins[expr.name])
|
|
|
|
this.runtimeError(`I can't call a function here.`);
|
2020-08-10 16:08:43 +00:00
|
|
|
// is it a subscript?
|
|
|
|
if (expr.args) {
|
2020-08-16 17:16:29 +00:00
|
|
|
// TODO: set array slice (HP BASIC)
|
2020-08-14 14:26:43 +00:00
|
|
|
if (this.opts.arraysContainChars && expr.name.endsWith('$')) {
|
|
|
|
this.runtimeError(`I can't set array slices via this command yet.`);
|
|
|
|
} else {
|
2020-08-15 11:53:13 +00:00
|
|
|
s += this.array2js(expr, opts);
|
2020-08-14 14:26:43 +00:00
|
|
|
}
|
2020-08-09 16:23:49 +00:00
|
|
|
} else { // just a variable
|
2020-08-24 16:51:47 +00:00
|
|
|
s = `this.globals.${expr.name}`;
|
2020-08-09 16:23:49 +00:00
|
|
|
}
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2020-08-15 11:53:13 +00:00
|
|
|
array2js(expr: basic.IndOp, opts?: ExprOptions) : string {
|
|
|
|
var qname = JSON.stringify(expr.name);
|
|
|
|
var args = expr.args || [];
|
|
|
|
return this.expr2js(expr, {novalid:true}) // check array bounds
|
|
|
|
+ `;this.getArray(${qname}, ${args.length})`
|
|
|
|
+ args.map((arg) => '[this.ROUND('+this.expr2js(arg, opts)+')]').join('');
|
|
|
|
}
|
|
|
|
|
2020-08-09 16:23:49 +00:00
|
|
|
checkFuncArgs(expr: basic.IndOp, fn: Function) {
|
|
|
|
// TODO: check types?
|
|
|
|
var nargs = expr.args ? expr.args.length : 0;
|
2020-08-11 17:29:31 +00:00
|
|
|
// exceptions
|
|
|
|
if (expr.name == 'RND' && nargs == 0) return;
|
2020-08-09 16:23:49 +00:00
|
|
|
if (expr.name == 'MID$' && nargs == 2) return;
|
|
|
|
if (expr.name == 'INSTR' && nargs == 2) return;
|
|
|
|
if (fn.length != nargs)
|
|
|
|
this.runtimeError(`I expected ${fn.length} arguments for the ${expr.name} function, but I got ${nargs}.`);
|
|
|
|
}
|
|
|
|
|
2020-08-20 19:56:28 +00:00
|
|
|
startForLoop(forname:string, init:number, targ:number, step?:number, endpc?:number) {
|
2020-08-15 11:53:13 +00:00
|
|
|
// save start PC and label in case of hot reload (only works if FOR is first stmt in line)
|
|
|
|
var looppc = this.curpc - 1;
|
|
|
|
var looplabel = this.pc2label.get(looppc);
|
2020-08-05 04:48:29 +00:00
|
|
|
if (!step) step = 1;
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
this.vars[forname] = init;
|
|
|
|
if (this.trace) console.log(`FOR ${forname} = ${init} TO ${targ} STEP ${step}`);
|
2020-08-11 17:29:31 +00:00
|
|
|
// create done function
|
|
|
|
var loopdone = () => {
|
|
|
|
return step >= 0 ? this.vars[forname] > targ : this.vars[forname] < targ;
|
|
|
|
}
|
|
|
|
// skip entire for loop before first iteration? (Minimal BASIC)
|
2020-08-20 19:56:28 +00:00
|
|
|
if (this.opts.testInitialFor && loopdone()) {
|
|
|
|
if (endpc != null)
|
|
|
|
this.curpc = endpc+1;
|
|
|
|
else
|
|
|
|
this.skipToAfterNext(forname);
|
|
|
|
}
|
2020-08-13 17:32:47 +00:00
|
|
|
// save for var name on stack, remove existing entry
|
|
|
|
if (this.forLoopStack[forname] != null)
|
|
|
|
this.forLoopStack = this.forLoopStack.filter((n) => n == forname);
|
|
|
|
this.forLoopStack.push(forname);
|
2020-08-11 17:29:31 +00:00
|
|
|
// create for loop record
|
|
|
|
this.forLoops[forname] = {
|
|
|
|
$next: (nextname:string) => {
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
if (nextname && forname != nextname)
|
|
|
|
this.runtimeError(`I executed NEXT "${nextname}", but the last FOR was for "${forname}".`)
|
|
|
|
this.vars[forname] += step;
|
2020-08-11 17:29:31 +00:00
|
|
|
var done = loopdone();
|
2020-08-05 04:48:29 +00:00
|
|
|
if (done) {
|
2020-08-13 17:32:47 +00:00
|
|
|
// delete entry, pop FOR off the stack and continue
|
|
|
|
this.forLoopStack.pop();
|
2020-08-11 17:29:31 +00:00
|
|
|
delete this.forLoops[forname];
|
2020-08-05 04:48:29 +00:00
|
|
|
} else {
|
2020-08-15 11:53:13 +00:00
|
|
|
// go back to FOR loop, adjusting for hot reload (fetch pc by label)
|
|
|
|
this.curpc = ((looplabel != null && this.label2pc[looplabel]) || looppc) + 1;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
if (this.trace) console.log(`NEXT ${forname}: ${this.vars[forname]} TO ${targ} STEP ${step} DONE=${done}`);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-11 17:29:31 +00:00
|
|
|
};
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
nextForLoop(name) {
|
2020-08-13 17:32:47 +00:00
|
|
|
// get FOR loop entry, or get top of stack if NEXT var is optional
|
|
|
|
var fl = this.forLoops[name || (this.opts.optionalNextVar && this.forLoopStack[this.forLoopStack.length-1])];
|
2020-08-12 16:57:54 +00:00
|
|
|
if (!fl) this.runtimeError(`I couldn't find a matching FOR for this NEXT.`)
|
2020-08-11 17:29:31 +00:00
|
|
|
fl.$next(name);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
2020-08-12 16:57:54 +00:00
|
|
|
whileLoop(cond) {
|
|
|
|
if (cond) {
|
|
|
|
this.whileLoops.push(this.curpc-1);
|
|
|
|
} else {
|
|
|
|
this.skipToAfterWend();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
nextWhileLoop() {
|
|
|
|
var pc = this.whileLoops.pop();
|
|
|
|
if (pc == null) this.runtimeError(`I couldn't find a matching WHILE for this WEND.`);
|
|
|
|
else this.curpc = pc;
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
// converts a variable to string/number based on var name
|
2020-08-14 04:34:54 +00:00
|
|
|
assign(name: string, right: number|string, isRead?:boolean) : number|string {
|
|
|
|
// convert data? READ always converts if read into string
|
2020-08-16 17:16:29 +00:00
|
|
|
if (isRead && name.endsWith("$"))
|
|
|
|
return this.checkValue(this.convert(name, right), name);
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
// TODO: use options
|
|
|
|
if (name.endsWith("$")) {
|
|
|
|
return this.convertToString(right, name);
|
|
|
|
} else {
|
|
|
|
return this.convertToNumber(right, name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
convert(name: string, right: number|string) : number|string {
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
if (name.endsWith("$")) {
|
2020-08-10 16:08:43 +00:00
|
|
|
return right == null ? "" : right.toString();
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
} else if (typeof right === 'number') {
|
2020-08-05 04:48:29 +00:00
|
|
|
return right;
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
} else {
|
|
|
|
return parseFloat(right+"");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
convertToString(right: number|string, name?: string) {
|
|
|
|
if (typeof right !== 'string') this.runtimeError(`I can't convert ${right} to a string.`);
|
|
|
|
else return right;
|
|
|
|
}
|
|
|
|
|
|
|
|
convertToNumber(right: number|string, name?: string) {
|
|
|
|
if (typeof right !== 'number') this.runtimeError(`I can't convert ${right} to a number.`);
|
|
|
|
else return this.checkNum(right);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// dimension array
|
|
|
|
dimArray(name: string, ...dims:number[]) {
|
2020-08-09 16:23:49 +00:00
|
|
|
// TODO: maybe do this check at compile-time?
|
2020-08-15 20:03:56 +00:00
|
|
|
dims = dims.map(Math.round);
|
2020-08-11 17:29:31 +00:00
|
|
|
if (this.arrays[name] != null) {
|
|
|
|
if (this.opts.staticArrays) return;
|
|
|
|
else this.runtimeError(`I already dimensioned this array (${name}) earlier.`)
|
|
|
|
}
|
2020-08-16 17:16:29 +00:00
|
|
|
var total = this.getTotalArrayLength(dims);
|
|
|
|
if (total > this.opts.maxArrayElements)
|
2020-08-15 20:03:56 +00:00
|
|
|
this.runtimeError(`I can't create an array with this many elements.`);
|
2020-08-05 04:48:29 +00:00
|
|
|
var isstring = name.endsWith('$');
|
2020-08-11 17:29:31 +00:00
|
|
|
// if numeric value, we use Float64Array which inits to 0
|
|
|
|
var arrcons = isstring ? Array : Float64Array;
|
2020-08-05 04:48:29 +00:00
|
|
|
if (dims.length == 1) {
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
this.arrays[name] = new arrcons(dims[0]+1);
|
2020-08-05 04:48:29 +00:00
|
|
|
} else if (dims.length == 2) {
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
this.arrays[name] = new Array(dims[0]+1);
|
2020-08-10 16:08:43 +00:00
|
|
|
for (var i=0; i<dims[0]+1; i++) {
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
this.arrays[name][i] = new arrcons(dims[1]+1);
|
2020-08-10 16:08:43 +00:00
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
} else {
|
|
|
|
this.runtimeError(`I only support arrays of one or two dimensions.`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-15 20:03:56 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
getArray(name: string, order: number) : [] {
|
|
|
|
if (!this.arrays[name]) {
|
2020-08-11 17:29:31 +00:00
|
|
|
if (this.opts.defaultArraySize == 0)
|
2020-08-16 17:16:29 +00:00
|
|
|
this.dialectError(`automatically declare arrays without a DIM statement (or did you mean to call a function?)`);
|
2020-08-05 04:48:29 +00:00
|
|
|
if (order == 1)
|
2020-08-11 17:29:31 +00:00
|
|
|
this.dimArray(name, this.opts.defaultArraySize-1);
|
2020-08-05 04:48:29 +00:00
|
|
|
else if (order == 2)
|
2020-08-11 17:29:31 +00:00
|
|
|
this.dimArray(name, this.opts.defaultArraySize-1, this.opts.defaultArraySize-1);
|
2020-08-05 04:48:29 +00:00
|
|
|
else
|
2020-08-09 01:25:58 +00:00
|
|
|
this.runtimeError(`I only support arrays of one or two dimensions.`); // TODO
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
return this.arrays[name];
|
|
|
|
}
|
|
|
|
|
2020-08-10 16:08:43 +00:00
|
|
|
arrayGet(name: string, ...indices: number[]) : basic.Value {
|
|
|
|
var arr = this.getArray(name, indices.length);
|
2020-08-16 17:16:29 +00:00
|
|
|
indices = indices.map(this.ROUND.bind(this));
|
2020-08-10 16:08:43 +00:00
|
|
|
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}.`);
|
2020-08-11 17:29:31 +00:00
|
|
|
if (idx >= v.length) // TODO: also can happen when mispelling function name
|
2020-08-10 16:08:43 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-08-14 21:21:32 +00:00
|
|
|
// for HP BASIC string slicing (TODO?)
|
2020-08-13 17:51:35 +00:00
|
|
|
modifyStringSlice(orig: string, add: string, start: number, end: number) : string {
|
2020-08-14 21:21:32 +00:00
|
|
|
orig = orig || "";
|
2020-08-16 17:16:29 +00:00
|
|
|
this.checkString(orig);
|
|
|
|
this.checkString(add);
|
|
|
|
if (!end) end = start;
|
|
|
|
start = this.ROUND(start);
|
|
|
|
end = this.ROUND(end);
|
2020-08-22 16:40:58 +00:00
|
|
|
if (start < 1) this.dialectError(`accept a string slice index less than 1`);
|
|
|
|
if (end < start) this.dialectError(`accept a string slice index less than the starting index`);
|
2020-08-16 17:16:29 +00:00
|
|
|
return (orig + ' '.repeat(start)).substr(0, start-1) + add.substr(0, end+1-start) + orig.substr(end);
|
2020-08-14 21:21:32 +00:00
|
|
|
}
|
2020-08-16 17:16:29 +00:00
|
|
|
|
2020-08-14 21:21:32 +00:00
|
|
|
getStringSlice(s: string, start: number, end: number) {
|
2020-08-15 20:03:56 +00:00
|
|
|
s = this.checkString(s);
|
2020-08-16 17:16:29 +00:00
|
|
|
start = this.ROUND(start);
|
2020-08-22 16:40:58 +00:00
|
|
|
if (start < 1) this.dialectError(`accept a string slice index less than 1`);
|
|
|
|
if (end != null) {
|
|
|
|
end = this.ROUND(end);
|
|
|
|
if (end < start) this.dialectError(`accept a string slice index less than the starting index`);
|
|
|
|
return s.substr(start-1, end+1-start);
|
|
|
|
} else {
|
|
|
|
return s.substr(start-1);
|
|
|
|
}
|
2020-08-13 17:51:35 +00:00
|
|
|
}
|
|
|
|
|
2020-08-12 16:57:54 +00:00
|
|
|
checkOnGoto(value: number, labels: string[]) {
|
2020-08-05 04:48:29 +00:00
|
|
|
value = this.ROUND(value);
|
2020-08-12 16:57:54 +00:00
|
|
|
if (value < 0) // > 255 ?
|
|
|
|
this.runtimeError(`I needed a number between 1 and ${labels.length}, but I got ${value}.`);
|
|
|
|
if (this.opts.checkOnGotoIndex && (value < 1 || value > labels.length))
|
2020-08-05 04:48:29 +00:00
|
|
|
this.runtimeError(`I needed a number between 1 and ${labels.length}, but I got ${value}.`);
|
2020-08-13 17:51:35 +00:00
|
|
|
if (value < 1 || value > labels.length)
|
|
|
|
return 0;
|
2020-08-12 16:57:54 +00:00
|
|
|
return value;
|
|
|
|
}
|
2020-08-13 17:51:35 +00:00
|
|
|
|
2020-08-12 16:57:54 +00:00
|
|
|
onGotoLabel(value: number, ...labels: string[]) {
|
|
|
|
value = this.checkOnGoto(value, labels);
|
2020-08-13 17:51:35 +00:00
|
|
|
if (value) this.gotoLabel(labels[value-1]);
|
2020-08-10 13:07:09 +00:00
|
|
|
}
|
|
|
|
onGosubLabel(value: number, ...labels: string[]) {
|
2020-08-12 16:57:54 +00:00
|
|
|
value = this.checkOnGoto(value, labels);
|
2020-08-13 17:51:35 +00:00
|
|
|
if (value) this.gosubLabel(labels[value-1]);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
2020-08-09 19:45:39 +00:00
|
|
|
nextDatum() : basic.Value {
|
2020-08-05 04:48:29 +00:00
|
|
|
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) {
|
2020-08-09 16:23:49 +00:00
|
|
|
var expr = this.expr2js(arg);
|
2020-08-16 17:16:29 +00:00
|
|
|
var name = (expr as any).name;
|
|
|
|
s += `this.printExpr(this.checkValue(${expr}, ${JSON.stringify(name)}));`;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2020-08-15 11:53:13 +00:00
|
|
|
preInput() {
|
|
|
|
this.running=false;
|
|
|
|
this.curpc--;
|
|
|
|
}
|
|
|
|
|
|
|
|
postInput(valid : boolean) {
|
|
|
|
if (valid) this.curpc++;
|
|
|
|
this.running = true;
|
|
|
|
this.resume();
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
do__INPUT(stmt : basic.INPUT_Statement) {
|
2020-08-21 03:20:53 +00:00
|
|
|
var prompt = stmt.prompt != null ? this.expr2js(stmt.prompt) : '""';
|
|
|
|
var elapsed = stmt.elapsed != null ? this.assign2js(stmt.elapsed) : "let ___";
|
2020-08-05 04:48:29 +00:00
|
|
|
var setvals = '';
|
|
|
|
stmt.args.forEach((arg, index) => {
|
2020-08-09 16:23:49 +00:00
|
|
|
var lexpr = this.assign2js(arg);
|
2020-08-10 16:08:43 +00:00
|
|
|
setvals += `
|
2020-08-21 03:20:53 +00:00
|
|
|
var value = this.convert(${JSON.stringify(arg.name)}, response.vals[${index}]);
|
2020-08-10 16:08:43 +00:00
|
|
|
valid &= this.isValid(value);
|
|
|
|
${lexpr} = value;
|
|
|
|
`
|
2020-08-05 04:48:29 +00:00
|
|
|
});
|
2020-08-15 11:53:13 +00:00
|
|
|
return `this.preInput();
|
2020-08-21 03:20:53 +00:00
|
|
|
this.input(${prompt}, ${stmt.args.length}).then((response) => {
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
let valid = 1;
|
|
|
|
${setvals}
|
2020-08-15 11:53:13 +00:00
|
|
|
this.postInput(valid);
|
|
|
|
this.column = 0; // assume linefeed
|
2020-08-21 03:20:53 +00:00
|
|
|
${elapsed} = response.elapsed;
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
})`;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
do__LET(stmt : basic.LET_Statement) {
|
2020-08-09 16:23:49 +00:00
|
|
|
var right = this.expr2js(stmt.right);
|
2020-08-15 11:53:13 +00:00
|
|
|
var s = `let _right = ${right};`;
|
|
|
|
for (var lexpr of stmt.lexprs) {
|
|
|
|
// HP BASIC string-slice syntax?
|
|
|
|
if (this.opts.arraysContainChars && lexpr.args && lexpr.name.endsWith('$')) {
|
2020-08-24 16:51:47 +00:00
|
|
|
s += `this.globals.${lexpr.name} = this.modifyStringSlice(this.vars.${lexpr.name}, _right, `
|
2020-08-15 11:53:13 +00:00
|
|
|
s += lexpr.args.map((arg) => this.expr2js(arg)).join(', ');
|
2020-08-16 17:16:29 +00:00
|
|
|
s += ');';
|
2020-08-15 11:53:13 +00:00
|
|
|
} else {
|
|
|
|
var ljs = this.assign2js(lexpr);
|
|
|
|
s += `${ljs} = this.assign(${JSON.stringify(lexpr.name)}, _right);`;
|
|
|
|
}
|
2020-08-13 17:51:35 +00:00
|
|
|
}
|
2020-08-15 11:53:13 +00:00
|
|
|
return s;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
do__FOR(stmt : basic.FOR_Statement) {
|
2020-08-12 16:57:54 +00:00
|
|
|
var name = JSON.stringify(stmt.lexpr.name);
|
2020-08-09 16:23:49 +00:00
|
|
|
var init = this.expr2js(stmt.initial);
|
|
|
|
var targ = this.expr2js(stmt.target);
|
|
|
|
var step = stmt.step ? this.expr2js(stmt.step) : 'null';
|
2020-08-20 19:56:28 +00:00
|
|
|
return `this.startForLoop(${name}, ${init}, ${targ}, ${step}, ${stmt.endpc})`;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
do__NEXT(stmt : basic.NEXT_Statement) {
|
2020-08-12 16:57:54 +00:00
|
|
|
var name = stmt.lexpr && JSON.stringify(stmt.lexpr.name);
|
2020-08-05 04:48:29 +00:00
|
|
|
return `this.nextForLoop(${name})`;
|
|
|
|
}
|
|
|
|
|
|
|
|
do__IF(stmt : basic.IF_Statement) {
|
2020-08-09 16:23:49 +00:00
|
|
|
var cond = this.expr2js(stmt.cond);
|
2020-08-20 19:56:28 +00:00
|
|
|
if (stmt.endpc != null)
|
|
|
|
return `if (!(${cond})) { this.curpc = ${stmt.endpc}; }`
|
|
|
|
else
|
|
|
|
return `if (!(${cond})) { this.skipToElse(); }`
|
2020-08-09 02:36:21 +00:00
|
|
|
}
|
|
|
|
|
2020-08-20 19:56:28 +00:00
|
|
|
do__ELSE(stmt : basic.ELSE_Statement) {
|
|
|
|
if (stmt.endpc != null)
|
|
|
|
return `this.curpc = ${stmt.endpc}`
|
|
|
|
else
|
|
|
|
return `this.skipToEOL()`
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
2020-08-12 16:57:54 +00:00
|
|
|
do__WHILE(stmt : basic.WHILE_Statement) {
|
|
|
|
var cond = this.expr2js(stmt.cond);
|
2020-08-22 16:40:58 +00:00
|
|
|
if (stmt.endpc != null)
|
|
|
|
return `if (!(${cond})) { this.curpc = ${stmt.endpc+1}; }`;
|
|
|
|
else
|
|
|
|
return `this.whileLoop(${cond})`;
|
2020-08-12 16:57:54 +00:00
|
|
|
}
|
|
|
|
|
2020-08-22 16:40:58 +00:00
|
|
|
do__WEND(stmt : basic.WEND_Statement) {
|
|
|
|
if (stmt.startpc != null)
|
|
|
|
return `this.curpc = ${stmt.startpc}`;
|
|
|
|
else
|
|
|
|
return `this.nextWhileLoop()`
|
2020-08-12 16:57:54 +00:00
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
do__DEF(stmt : basic.DEF_Statement) {
|
|
|
|
var args = [];
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
for (var arg of stmt.lexpr.args || []) {
|
2020-08-05 04:48:29 +00:00
|
|
|
if (isLookup(arg)) {
|
|
|
|
args.push(arg.name);
|
|
|
|
} else {
|
|
|
|
this.runtimeError("I found a DEF statement with arguments other than variable names.");
|
|
|
|
}
|
|
|
|
}
|
2020-08-09 16:23:49 +00:00
|
|
|
var functext = this.expr2js(stmt.def, {locals:args});
|
2020-08-09 02:36:21 +00:00
|
|
|
//this.defs[stmt.lexpr.name] = new Function(args.join(','), functext).bind(this);
|
2020-08-09 16:23:49 +00:00
|
|
|
return `this.defs.${stmt.lexpr.name} = function(${args.join(',')}) { return ${functext}; }.bind(this)`;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_DIM(dim : basic.IndOp) {
|
2020-08-22 16:40:58 +00:00
|
|
|
// HP BASIC doesn't really have string arrays, just strings
|
2020-08-13 17:51:35 +00:00
|
|
|
if (this.opts.arraysContainChars && dim.name.endsWith('$'))
|
2020-08-22 16:40:58 +00:00
|
|
|
return '';
|
|
|
|
// dimension an array
|
2020-08-05 04:48:29 +00:00
|
|
|
var argsstr = '';
|
|
|
|
for (var arg of dim.args) {
|
2020-08-11 17:29:31 +00:00
|
|
|
argsstr += ', ' + this.expr2js(arg, {isconst: this.opts.staticArrays});
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`;
|
|
|
|
}
|
|
|
|
|
|
|
|
do__DIM(stmt : basic.DIM_Statement) {
|
2020-08-11 17:29:31 +00:00
|
|
|
if (this.opts.staticArrays) return; // DIM at reset()
|
2020-08-05 04:48:29 +00:00
|
|
|
var s = '';
|
|
|
|
stmt.args.forEach((dim) => s += this._DIM(dim));
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
|
|
|
do__GOTO(stmt : basic.GOTO_Statement) {
|
2020-08-13 17:51:35 +00:00
|
|
|
var label = this.expr2js(stmt.label);
|
2020-08-05 04:48:29 +00:00
|
|
|
return `this.gotoLabel(${label})`;
|
|
|
|
}
|
|
|
|
|
|
|
|
do__GOSUB(stmt : basic.GOSUB_Statement) {
|
2020-08-13 17:51:35 +00:00
|
|
|
var label = this.expr2js(stmt.label);
|
2020-08-05 04:48:29 +00:00
|
|
|
return `this.gosubLabel(${label})`;
|
|
|
|
}
|
|
|
|
|
|
|
|
do__RETURN(stmt : basic.RETURN_Statement) {
|
|
|
|
return `this.returnFromGosub()`;
|
|
|
|
}
|
|
|
|
|
2020-08-13 17:51:35 +00:00
|
|
|
do__ONGOTO(stmt : basic.ONGO_Statement) {
|
2020-08-09 16:23:49 +00:00
|
|
|
var expr = this.expr2js(stmt.expr);
|
|
|
|
var labels = stmt.labels.map((arg) => this.expr2js(arg, {isconst:true})).join(', ');
|
2020-08-10 13:07:09 +00:00
|
|
|
if (stmt.command == 'ONGOTO')
|
|
|
|
return `this.onGotoLabel(${expr}, ${labels})`;
|
|
|
|
else
|
|
|
|
return `this.onGosubLabel(${expr}, ${labels})`;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
2020-08-14 04:34:54 +00:00
|
|
|
do__ONGOSUB(stmt : basic.ONGO_Statement) {
|
|
|
|
return this.do__ONGOTO(stmt);
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
do__DATA() {
|
|
|
|
// data is preprocessed
|
|
|
|
}
|
|
|
|
|
|
|
|
do__READ(stmt : basic.READ_Statement) {
|
|
|
|
var s = '';
|
|
|
|
stmt.args.forEach((arg) => {
|
2020-08-14 04:34:54 +00:00
|
|
|
s += `${this.assign2js(arg)} = this.assign(${JSON.stringify(arg.name)}, this.nextDatum(), true);`;
|
2020-08-05 04:48:29 +00:00
|
|
|
});
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2020-08-12 16:57:54 +00:00
|
|
|
do__RESTORE(stmt : basic.RESTORE_Statement) {
|
|
|
|
if (stmt.label != null)
|
2020-08-14 04:34:54 +00:00
|
|
|
return `this.dataptr = this.label2dataptr[${this.expr2js(stmt.label, {isconst:true})}] || 0`;
|
2020-08-12 16:57:54 +00:00
|
|
|
else
|
|
|
|
return `this.dataptr = 0`;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
do__END() {
|
|
|
|
return `this.skipToEOF()`;
|
|
|
|
}
|
|
|
|
|
|
|
|
do__STOP() {
|
|
|
|
return `this.skipToEOF()`;
|
|
|
|
}
|
|
|
|
|
|
|
|
do__OPTION(stmt: basic.OPTION_Statement) {
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
// already parsed in compiler
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
2020-08-09 02:36:21 +00:00
|
|
|
do__POP() {
|
|
|
|
return `this.popReturnStack()`;
|
|
|
|
}
|
|
|
|
|
|
|
|
do__GET(stmt : basic.GET_Statement) {
|
2020-08-09 16:23:49 +00:00
|
|
|
var lexpr = this.assign2js(stmt.lexpr);
|
2020-08-09 02:36:21 +00:00
|
|
|
// TODO: single key input
|
2020-08-15 11:53:13 +00:00
|
|
|
return `this.preInput();
|
2020-08-09 02:36:21 +00:00
|
|
|
this.input().then((vals) => {
|
|
|
|
${lexpr} = this.convert(${JSON.stringify(stmt.lexpr.name)}, vals[0]);
|
2020-08-15 11:53:13 +00:00
|
|
|
this.postInput(true);
|
2020-08-09 02:36:21 +00:00
|
|
|
})`;
|
|
|
|
}
|
|
|
|
|
2020-08-10 13:07:09 +00:00
|
|
|
do__CLEAR() {
|
|
|
|
return 'this.clearVars()';
|
|
|
|
}
|
|
|
|
|
2020-08-12 16:57:54 +00:00
|
|
|
do__RANDOMIZE() {
|
|
|
|
return `this.rng.randomize()`;
|
|
|
|
}
|
|
|
|
|
2020-08-15 11:53:13 +00:00
|
|
|
do__CHANGE(stmt : basic.CHANGE_Statement) {
|
|
|
|
var arr2str = stmt.dest.name.endsWith('$');
|
|
|
|
if (arr2str) { // array -> string
|
|
|
|
let arrname = (stmt.src as basic.IndOp).name || this.runtimeError("I can only change to a string from an array.");
|
|
|
|
let dest = this.assign2js(stmt.dest);
|
|
|
|
return `
|
|
|
|
let arrname = ${JSON.stringify(arrname)};
|
|
|
|
let len = this.arrayGet(arrname, 0);
|
|
|
|
let s = '';
|
|
|
|
for (let i=0; i<len; i++) {
|
|
|
|
s += String.fromCharCode(this.arrayGet(arrname, i+1));
|
|
|
|
}
|
|
|
|
${dest} = s;
|
|
|
|
`;
|
|
|
|
} else { // string -> array
|
|
|
|
let src = this.expr2js(stmt.src);
|
|
|
|
let dest = this.array2js(stmt.dest);
|
|
|
|
return `
|
|
|
|
let src = ${src}+"";
|
|
|
|
${dest}[0] = src.length;
|
|
|
|
for (let i=0; i<src.length; i++) {
|
|
|
|
${dest}[i+1] = src.charCodeAt(i);
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-22 16:40:58 +00:00
|
|
|
do__CONVERT(stmt : basic.CONVERT_Statement) {
|
|
|
|
var num2str = stmt.dest.name.endsWith('$');
|
|
|
|
let src = this.expr2js(stmt.src);
|
|
|
|
let dest = this.assign2js(stmt.dest);
|
|
|
|
if (num2str) {
|
|
|
|
return `${dest} = this.valueToString(${src}, false)`;
|
|
|
|
} else {
|
|
|
|
return `${dest} = this.VAL(${src})`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-24 16:51:47 +00:00
|
|
|
do__SUB(stmt: basic.SUB_Statement) {
|
|
|
|
this.subroutines[stmt.lexpr.name] = stmt;
|
|
|
|
// skip the SUB definition
|
|
|
|
return `this.curpc = ${stmt.endpc}`
|
|
|
|
}
|
|
|
|
|
|
|
|
do__CALL(stmt: basic.CALL_Statement) {
|
|
|
|
var substmt : basic.SUB_Statement = this.subroutines[stmt.call.name];
|
|
|
|
if (substmt == null)
|
|
|
|
this.runtimeError(`I can't find a subroutine named "${stmt.call.name}".`);
|
|
|
|
var subargs = substmt.lexpr.args || [];
|
|
|
|
var callargs = stmt.call.args || [];
|
|
|
|
if (subargs.length != callargs.length)
|
|
|
|
this.runtimeError(`I tried to call ${stmt.call.name} with the wrong number of parameters.`);
|
|
|
|
var s = '';
|
|
|
|
s += `this.gosubLabel(${JSON.stringify(stmt.call.name)});`
|
|
|
|
s += `this.newLocalScope();`
|
|
|
|
for (var i=0; i<subargs.length; i++) {
|
|
|
|
var arg = subargs[i] as basic.IndOp;
|
|
|
|
s += `this.vars.${arg.name} = ${this.expr2js(callargs[i])};`
|
|
|
|
}
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2020-08-09 02:36:21 +00:00
|
|
|
// TODO: ONERR, ON ERROR GOTO
|
2020-08-05 04:48:29 +00:00
|
|
|
// TODO: memory quota
|
2020-08-09 02:36:21 +00:00
|
|
|
// TODO: useless loop (! 4th edition)
|
|
|
|
// TODO: other 4th edition errors
|
2020-08-10 16:08:43 +00:00
|
|
|
// TODO: ecma55 all-or-none input checking?
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
// FUNCTIONS
|
|
|
|
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
isValid(obj:number|string) : boolean {
|
2020-08-12 16:57:54 +00:00
|
|
|
if (typeof obj === 'number' && !isNaN(obj) && (!this.opts.checkOverflow || isFinite(obj)))
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
return true;
|
|
|
|
else if (typeof obj === 'string')
|
|
|
|
return true;
|
|
|
|
else
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
checkValue(obj:number|string, exprname:string) : number|string {
|
|
|
|
// check for unreferenced value
|
2020-08-05 04:48:29 +00:00
|
|
|
if (typeof obj !== 'number' && typeof obj !== 'string') {
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
// assign default value?
|
2020-08-09 16:23:49 +00:00
|
|
|
if (obj == null && this.opts.defaultValues) {
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
return exprname.endsWith("$") ? "" : 0;
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
if (exprname != null && obj == null) {
|
2020-08-16 17:16:29 +00:00
|
|
|
this.runtimeError(`I haven't assigned a value to ${exprname}.`);
|
2020-08-05 04:48:29 +00:00
|
|
|
} 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;
|
|
|
|
}
|
2020-08-09 16:23:49 +00:00
|
|
|
getDef(exprname: string) {
|
|
|
|
var fn = this.defs[exprname];
|
|
|
|
if (!fn) this.runtimeError(`I haven't run a DEF statement for ${exprname}.`);
|
|
|
|
return fn;
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
checkNum(n:number) : number {
|
2020-08-15 20:03:56 +00:00
|
|
|
this.checkValue(n, 'this');
|
2020-08-05 04:48:29 +00:00
|
|
|
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 {
|
2020-08-15 20:03:56 +00:00
|
|
|
this.checkValue(s, 'this');
|
2020-08-09 02:36:21 +00:00
|
|
|
if (typeof s !== 'string')
|
|
|
|
this.runtimeError(`I expected a string here.`);
|
2020-08-09 16:23:49 +00:00
|
|
|
else if (s.length > this.opts.maxStringLength)
|
|
|
|
this.dialectError(`create strings longer than ${this.opts.maxStringLength} characters`);
|
2020-08-05 04:48:29 +00:00
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
|
|
|
add(a, b) : number|string {
|
|
|
|
// TODO: if string-concat
|
|
|
|
if (typeof a === 'number' && typeof b === 'number')
|
|
|
|
return this.checkNum(a + b);
|
2020-08-09 16:23:49 +00:00
|
|
|
else if (this.opts.stringConcat)
|
2020-08-09 02:36:21 +00:00
|
|
|
return this.checkString(a + b);
|
2020-08-05 04:48:29 +00:00
|
|
|
else
|
2020-08-09 02:36:21 +00:00
|
|
|
this.dialectError(`use the "+" operator to concatenate strings`)
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
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);
|
|
|
|
}
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
idiv(a:number, b:number) : number {
|
2020-08-09 16:23:49 +00:00
|
|
|
return this.FIX(this.INT(a) / this.INT(b));
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
}
|
|
|
|
mod(a:number, b:number) : number {
|
|
|
|
return this.checkNum(a % b);
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
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));
|
|
|
|
}
|
2020-08-09 16:23:49 +00:00
|
|
|
band(a:number, b:number) : number {
|
|
|
|
return a & b;
|
|
|
|
}
|
|
|
|
bor(a:number, b:number) : number {
|
|
|
|
return a | b;
|
|
|
|
}
|
|
|
|
bnot(a:number) : number {
|
|
|
|
return ~a;
|
|
|
|
}
|
|
|
|
bxor(a:number, b:number) : number {
|
|
|
|
return a ^ b;
|
|
|
|
}
|
|
|
|
bimp(a:number, b:number) : number {
|
|
|
|
return this.bor(this.bnot(a), b);
|
|
|
|
}
|
|
|
|
beqv(a:number, b:number) : number {
|
|
|
|
return this.bnot(this.bxor(a, b));
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
land(a:number, b:number) : number {
|
2020-08-09 16:23:49 +00:00
|
|
|
return a && b ? (this.opts.bitwiseLogic ? -1 : 1) : 0;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
lor(a:number, b:number) : number {
|
2020-08-09 16:23:49 +00:00
|
|
|
return a || b ? (this.opts.bitwiseLogic ? -1 : 1) : 0;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
lnot(a:number) : number {
|
2020-08-09 16:23:49 +00:00
|
|
|
return a ? 0 : (this.opts.bitwiseLogic ? -1 : 1);
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
}
|
|
|
|
neg(a:number) : number {
|
|
|
|
return -a;
|
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
eq(a:number, b:number) : number {
|
2020-08-09 16:23:49 +00:00
|
|
|
return a == b ? (this.opts.bitwiseLogic ? -1 : 1) : 0;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
ne(a:number, b:number) : number {
|
2020-08-09 16:23:49 +00:00
|
|
|
return a != b ? (this.opts.bitwiseLogic ? -1 : 1) : 0;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
lt(a:number, b:number) : number {
|
2020-08-09 16:23:49 +00:00
|
|
|
return a < b ? (this.opts.bitwiseLogic ? -1 : 1) : 0;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
gt(a:number, b:number) : number {
|
2020-08-09 16:23:49 +00:00
|
|
|
return a > b ? (this.opts.bitwiseLogic ? -1 : 1) : 0;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
le(a:number, b:number) : number {
|
2020-08-09 16:23:49 +00:00
|
|
|
return a <= b ? (this.opts.bitwiseLogic ? -1 : 1) : 0;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
ge(a:number, b:number) : number {
|
2020-08-09 16:23:49 +00:00
|
|
|
return a >= b ? (this.opts.bitwiseLogic ? -1 : 1) : 0;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-13 17:51:35 +00:00
|
|
|
min(a:number, b:number) : number {
|
|
|
|
return a < b ? a : b;
|
|
|
|
}
|
|
|
|
max(a:number, b:number) : number {
|
|
|
|
return a > b ? a : b;
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
// FUNCTIONS (uppercase)
|
2020-08-15 20:03:56 +00:00
|
|
|
// TODO: swizzle names for type-checking
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
ABS(arg : number) : number {
|
|
|
|
return this.checkNum(Math.abs(arg));
|
|
|
|
}
|
|
|
|
ASC(arg : string) : number {
|
2020-08-15 20:03:56 +00:00
|
|
|
arg = this.checkString(arg);
|
|
|
|
if (arg == '') this.runtimeError(`I tried to call ASC() on an empty string.`);
|
2020-08-05 04:48:29 +00:00
|
|
|
return arg.charCodeAt(0);
|
|
|
|
}
|
|
|
|
ATN(arg : number) : number {
|
|
|
|
return this.checkNum(Math.atan(arg));
|
|
|
|
}
|
|
|
|
CHR$(arg : number) : string {
|
|
|
|
return String.fromCharCode(this.checkNum(arg));
|
|
|
|
}
|
2020-08-12 16:57:54 +00:00
|
|
|
CINT(arg : number) : number {
|
|
|
|
return this.ROUND(arg);
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
COS(arg : number) : number {
|
|
|
|
return this.checkNum(Math.cos(arg));
|
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
COT(arg : number) : number {
|
|
|
|
return this.checkNum(1.0 / Math.tan(arg)); // 4th edition only
|
|
|
|
}
|
2020-08-13 17:51:35 +00:00
|
|
|
CTL(arg : number) : string {
|
|
|
|
return this.CHR$(arg);
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
EXP(arg : number) : number {
|
|
|
|
return this.checkNum(Math.exp(arg));
|
|
|
|
}
|
|
|
|
FIX(arg : number) : number {
|
2020-08-09 16:23:49 +00:00
|
|
|
return this.checkNum(arg < 0 ? Math.ceil(arg) : Math.floor(arg));
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
HEX$(arg : number) : string {
|
2020-08-12 16:57:54 +00:00
|
|
|
return this.ROUND(arg).toString(16);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
INSTR(a, b, c) : number {
|
|
|
|
if (c != null) {
|
2020-08-15 11:53:13 +00:00
|
|
|
return this.checkString(b).indexOf(this.checkString(c), this.checkNum(a) - 1) + 1;
|
2020-08-05 04:48:29 +00:00
|
|
|
} else {
|
2020-08-15 11:53:13 +00:00
|
|
|
return this.checkString(a).indexOf(this.checkString(b)) + 1;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
INT(arg : number) : number {
|
|
|
|
return this.checkNum(Math.floor(arg));
|
|
|
|
}
|
|
|
|
LEFT$(arg : string, count : number) : string {
|
2020-08-15 20:03:56 +00:00
|
|
|
arg = this.checkString(arg);
|
|
|
|
count = this.ROUND(count);
|
2020-08-05 04:48:29 +00:00
|
|
|
return arg.substr(0, count);
|
|
|
|
}
|
|
|
|
LEN(arg : string) : number {
|
2020-08-13 17:51:35 +00:00
|
|
|
return this.checkString(arg).length;
|
|
|
|
}
|
|
|
|
LIN(arg : number) : string {
|
|
|
|
return this.STRING$(arg, '\n');
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
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));
|
|
|
|
}
|
2020-08-15 11:53:13 +00:00
|
|
|
LOG10(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.log10(arg));
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
MID$(arg : string, start : number, count : number) : string {
|
2020-08-15 20:03:56 +00:00
|
|
|
arg = this.checkString(arg);
|
2020-08-20 17:58:14 +00:00
|
|
|
if (!count) count = arg.length;
|
2020-08-15 20:03:56 +00:00
|
|
|
start = this.ROUND(start);
|
|
|
|
count = this.ROUND(count);
|
2020-08-09 16:23:49 +00:00
|
|
|
if (start < 1) this.runtimeError(`I can't compute MID$ if the starting index is less than 1.`)
|
2020-08-05 04:48:29 +00:00
|
|
|
return arg.substr(start-1, count);
|
|
|
|
}
|
2020-08-12 16:57:54 +00:00
|
|
|
OCT$(arg : number) : string {
|
|
|
|
return this.ROUND(arg).toString(8);
|
|
|
|
}
|
2020-08-15 11:53:13 +00:00
|
|
|
PI() : number {
|
|
|
|
return Math.PI;
|
|
|
|
}
|
|
|
|
// TODO: POS(haystack, needle, start)
|
2020-08-21 03:20:53 +00:00
|
|
|
POS(arg1, arg2) { // arg ignored
|
|
|
|
if (typeof arg1 == 'string' && typeof arg2 == 'string')
|
|
|
|
return arg1.indexOf(arg2) >= 0 + 1;
|
|
|
|
else
|
|
|
|
return this.column + 1;
|
2020-08-12 16:57:54 +00:00
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
RIGHT$(arg : string, count : number) : string {
|
2020-08-15 20:03:56 +00:00
|
|
|
arg = this.checkString(arg);
|
|
|
|
count = this.ROUND(count);
|
2020-08-05 04:48:29 +00:00
|
|
|
return arg.substr(arg.length - count, count);
|
|
|
|
}
|
|
|
|
RND(arg : number) : number {
|
2020-08-12 16:57:54 +00:00
|
|
|
// TODO: X<0 restart w/ seed, X=0 repeats
|
2020-08-14 21:21:32 +00:00
|
|
|
if (arg < 0) this.rng.seedfloat(arg);
|
2020-08-12 16:57:54 +00:00
|
|
|
return this.rng.next();
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
ROUND(arg : number) : number {
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
return this.checkNum(Math.round(arg));
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
SGN(arg : number) : number {
|
2020-08-15 20:03:56 +00:00
|
|
|
this.checkNum(arg);
|
2020-08-05 04:48:29 +00:00
|
|
|
return (arg < 0) ? -1 : (arg > 0) ? 1 : 0;
|
|
|
|
}
|
|
|
|
SIN(arg : number) : number {
|
|
|
|
return this.checkNum(Math.sin(arg));
|
|
|
|
}
|
|
|
|
SPACE$(arg : number) : string {
|
2020-08-15 20:03:56 +00:00
|
|
|
return this.STRING$(arg, ' ');
|
2020-08-12 16:57:54 +00:00
|
|
|
}
|
|
|
|
SPC(arg : number) : string {
|
|
|
|
return this.SPACE$(arg);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
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));
|
|
|
|
}
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
STR$(arg : number) : string {
|
2020-08-22 16:40:58 +00:00
|
|
|
return this.valueToString(this.checkNum(arg), false);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-12 16:57:54 +00:00
|
|
|
STRING$(len : number, chr : number|string) : string {
|
|
|
|
len = this.ROUND(len);
|
|
|
|
if (len <= 0) return '';
|
2020-08-15 20:03:56 +00:00
|
|
|
if (len > this.opts.maxStringLength)
|
2020-08-16 17:16:29 +00:00
|
|
|
this.dialectError(`create a string longer than ${this.opts.maxStringLength} characters`);
|
2020-08-15 20:03:56 +00:00
|
|
|
if (typeof chr === 'string')
|
|
|
|
return chr.substr(0,1).repeat(len);
|
|
|
|
else
|
|
|
|
return String.fromCharCode(chr).repeat(len);
|
2020-08-12 16:57:54 +00:00
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
TAB(arg : number) : string {
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
if (arg < 1) { arg = 1; } // TODO: SYSTEM MESSAGE IDENTIFYING THE EXCEPTION
|
|
|
|
var spaces = this.ROUND(arg) - 1 - this.column;
|
2020-08-15 20:03:56 +00:00
|
|
|
return this.SPACE$(spaces);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
TAN(arg : number) : number {
|
|
|
|
return this.checkNum(Math.tan(arg));
|
|
|
|
}
|
2020-08-20 17:58:14 +00:00
|
|
|
TIM(arg : number) : number { // only HP BASIC?
|
2020-08-13 17:51:35 +00:00
|
|
|
var d = new Date();
|
|
|
|
switch (this.ROUND(arg)) {
|
|
|
|
case 0: return d.getMinutes();
|
|
|
|
case 1: return d.getHours();
|
|
|
|
case 2:
|
|
|
|
var dayCount = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
|
|
|
var mn = d.getMonth();
|
|
|
|
var dn = d.getDate();
|
|
|
|
var dayOfYear = dayCount[mn] + dn;
|
|
|
|
var isLeapYear = (d.getFullYear() & 3) == 0; // TODO: wrong
|
|
|
|
if(mn > 1 && isLeapYear) dayOfYear++;
|
|
|
|
return dayOfYear;
|
|
|
|
case 3: return d.getFullYear() % 100; // Y@K!
|
|
|
|
case 4: return d.getSeconds();
|
|
|
|
default: return 0;
|
|
|
|
}
|
|
|
|
}
|
2020-08-10 03:19:27 +00:00
|
|
|
TIMER() : number {
|
|
|
|
return Date.now() / 1000;
|
|
|
|
}
|
2020-08-13 17:51:35 +00:00
|
|
|
UPS$(arg : string) : string {
|
|
|
|
return this.checkString(arg).toUpperCase();
|
|
|
|
}
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
VAL(arg : string) : number {
|
|
|
|
var n = parseFloat(this.checkString(arg));
|
|
|
|
return isNaN(n) ? 0 : n; // TODO? altair works this way
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-14 21:21:32 +00:00
|
|
|
LPAD$(arg : string, len : number) : string {
|
2020-08-15 20:03:56 +00:00
|
|
|
arg = this.checkString(arg);
|
2020-08-14 21:21:32 +00:00
|
|
|
while (arg.length < len) arg = " " + arg;
|
|
|
|
return arg;
|
|
|
|
}
|
|
|
|
RPAD$(arg : string, len : number) : string {
|
2020-08-15 20:03:56 +00:00
|
|
|
arg = this.checkString(arg);
|
2020-08-14 21:21:32 +00:00
|
|
|
while (arg.length < len) arg = arg + " ";
|
|
|
|
return arg;
|
|
|
|
}
|
|
|
|
NFORMAT$(arg : number, numlen : number) : string {
|
2020-08-15 11:53:13 +00:00
|
|
|
var s = this.float2str(arg, numlen);
|
|
|
|
return (numlen > 0) ? this.LPAD$(s, numlen) : this.RPAD$(s, -numlen);
|
2020-08-14 21:21:32 +00:00
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|