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)

This commit is contained in:
Steven Hugg 2020-08-08 11:22:56 -05:00
parent 3a4b39b01c
commit 14524726e6
9 changed files with 328 additions and 158 deletions

View File

@ -510,3 +510,19 @@ in devices:
Should call trap() every cycle or insn of frame, or exit when returns true?
BETTER DEBUGGING
Need to mark start/end columns, not just line number
Know if we are actively debugging or trap occurred
isRunning() = stopped, running, waiting, debugging...
Showing running PC may be distracting, maybe lines visited?
Don't grab cursor focus when trap occurs (how do we know?)
Use tick() and refresh(), not callbacks
Show current datum when using READ
Use https://codemirror.net/doc/manual.html#markText

View File

@ -0,0 +1 @@
10 PRINT "EXAMPLE BASIC PROGRAM"

View File

@ -26,14 +26,15 @@ CodeMirror.defineMode("basic", function(conf, parserConf) {
var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
var openingKeywords = ['if','for'];
var middleKeywords = ['to'];
var middleKeywords = ['to','then'];
var endKeywords = ['next','end'];
var operatorKeywords = ['and', 'or', 'not', 'xor', 'in'];
var operatorKeywords = ['and', 'or', 'not', 'xor', 'eqv', 'imp'];
var wordOperators = wordRegexp(operatorKeywords);
var commonKeywords = [
'let','print','go','goto','gosub','next','dim','input','data',
'read','restore','return','stop','on','def','option','then','step',
'BASE','DATA','DEF','DIM',
'GO','GOSUB','GOTO','INPUT','LET','ON','OPTION','PRINT',
'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB'
];
var commontypes = ['xxxxbyte','xxxxword'];

View File

@ -13,7 +13,7 @@ class CompileError extends Error {
// Lexer regular expression -- each (capture group) handles a different token type
const re_toks = /([0-9.]+[E][+-]?\d+)|(\d*[.]\d*[E0-9]*)|(\d+)|(['].*)|(\bAND\b)|(\bOR\b)|(\w+[$]?)|(".*?")|([<>]?[=<>])|([-+*/^,;:()])|(\S+)/gi;
const re_toks = /([0-9.]+[E][+-]?\d+)|(\d*[.]\d*[E0-9]*)|(\d+)|(['].*)|(\w+[$]?)|(".*?")|([<>]?[=<>])|([-+*/^,;:()])|(\S+)/gi;
export enum TokenType {
EOL = 0,
@ -21,8 +21,6 @@ export enum TokenType {
Float2,
Int,
Remark,
And,
Or,
Ident,
String,
Relational,
@ -35,7 +33,7 @@ export type ExprTypes = BinOp | UnOp | IndOp | Literal;
export type Expr = ExprTypes & SourceLocated;
export type Opcode = 'add' | 'sub' | 'mul' | 'div' | 'pow' | 'eq' | 'ne' | 'lt' | 'gt' | 'le' | 'ge' | 'land' | 'lor';
export type Opcode = string;
export type Value = string | number;
@ -50,7 +48,7 @@ export interface BinOp {
}
export interface UnOp {
op: 'neg';
op: 'neg' | 'lnot';
expr: Expr;
}
@ -152,6 +150,7 @@ export interface BASICLine {
}
export interface BASICProgram {
opts: BASICOptions;
lines: BASICLine[];
}
@ -162,23 +161,25 @@ class Token {
}
const OPERATORS = {
'AND': {f:'land',p:5},
'OR': {f:'lor',p:5},
'=': {f:'eq',p:10},
'<>': {f:'ne',p:10},
'<': {f:'lt',p:10},
'>': {f:'gt',p:10},
'<=': {f:'le',p:10},
'>=': {f:'ge',p:10},
'OR': {f:'bor',p:7},
'AND': {f:'band',p:8},
'=': {f:'eq',p:50},
'<>': {f:'ne',p:50},
'<': {f:'lt',p:50},
'>': {f:'gt',p:50},
'<=': {f:'le',p:50},
'>=': {f:'ge',p:50},
'+': {f:'add',p:100},
'-': {f:'sub',p:100},
'%': {f:'mod',p:140},
'\\': {f:'idiv',p:150},
'*': {f:'mul',p:200},
'/': {f:'div',p:200},
'^': {f:'pow',p:300}
};
function getOpcodeForOperator(op: string): Opcode {
return OPERATORS[op].f as Opcode;
function getOperator(op: string) {
return OPERATORS[op];
}
function getPrecedence(tok: Token): number {
@ -186,7 +187,7 @@ function getPrecedence(tok: Token): number {
case TokenType.Operator:
case TokenType.Relational:
case TokenType.Ident:
let op = OPERATORS[tok.str]
let op = getOperator(tok.str);
if (op) return op.p;
}
return -1;
@ -202,7 +203,7 @@ function stripQuotes(s: string) {
return s.substr(1, s.length-2);
}
// TODO
// TODO: implement these
export interface BASICOptions {
uppercaseOnly : boolean; // convert everything to uppercase?
strictVarNames : boolean; // only allow A0-9 for numerics, single letter for arrays/strings
@ -222,21 +223,25 @@ export interface BASICOptions {
printPrecision : number; // print precision # of digits
checkOverflow : boolean; // check for overflow of numerics?
defaultValues : boolean; // initialize unset variables to default value? (0 or "")
multipleNextVars : boolean; // NEXT Y,X
}
///// BASIC PARSER
export class BASICParser {
tokens: Token[];
opts : BASICOptions = ALTAIR_BASIC40;
errors: WorkerError[];
listings: CodeListingMap;
labels: { [label: string]: BASICLine };
targets: { [targetlabel: string]: SourceLocation };
eol: Token;
decls: { [name: string]: SourceLocation }; // declared/set vars
refs: { [name: string]: SourceLocation }; // references
lineno : number;
tokens: Token[];
eol: Token;
curlabel: string;
listings: CodeListingMap;
lasttoken: Token;
opts : BASICOptions = ALTAIR_BASIC40;
constructor() {
this.labels = {};
@ -245,6 +250,8 @@ export class BASICParser {
this.lineno = 0;
this.curlabel = null;
this.listings = {};
this.decls = {};
this.refs = {};
}
compileError(msg: string, loc?: SourceLocation) {
if (!loc) loc = this.peekToken().$loc;
@ -253,7 +260,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 doesn't support ${what}.`, loc); // TODO
}
consumeToken(): Token {
var tok = this.lasttoken = (this.tokens.shift() || this.eol);
@ -261,9 +268,9 @@ export class BASICParser {
}
expectToken(str: string) : Token {
var tok = this.consumeToken();
var tokstr = tok.str.toUpperCase();
var tokstr = tok.str;
if (str != tokstr) {
this.compileError(`I expected "${str}" here, but I saw "${tokstr}".`);
this.compileError(`There should be a "${str}" here.`);
}
return tok;
}
@ -278,7 +285,7 @@ export class BASICParser {
let tok = this.consumeToken();
switch (tok.type) {
case TokenType.Int:
if (this.labels[tok.str] != null) this.compileError(`I saw a duplicated label "${tok.str}".`);
if (this.labels[tok.str] != null) this.compileError(`There's a duplicated label "${tok.str}".`);
this.labels[tok.str] = line;
line.label = tok.str;
this.curlabel = tok.str;
@ -291,8 +298,8 @@ export class BASICParser {
}
parseFile(file: string, path: string) : BASICProgram {
var pgmlines = file.split("\n").map((line) => this.parseLine(line));
this.checkLabels();
var program = { lines: pgmlines };
var program = { opts: this.opts, lines: pgmlines };
this.checkAll(program);
this.listings[path] = this.generateListing(file, program);
return program;
}
@ -308,11 +315,15 @@ export class BASICParser {
tokenize(line: string) : void {
this.lineno++;
this.tokens = [];
var m;
var m : RegExpMatchArray;
while (m = re_toks.exec(line)) {
for (var i = 1; i < TokenType._LAST; i++) {
let s = m[i];
let s : string = m[i];
if (s != null) {
// uppercase all identifiers, and maybe more
if (i == TokenType.Ident || this.opts.uppercaseOnly)
s = s.toUpperCase();
// add token to list
this.tokens.push({
str: s,
type: i,
@ -341,23 +352,27 @@ export class BASICParser {
}
parseStatement(): Statement | null {
var cmdtok = this.consumeToken();
var cmd = cmdtok.str;
var stmt;
switch (cmdtok.type) {
case TokenType.Remark:
if (!this.opts.tickComments) this.dialectError(`tick remarks`);
return null;
case TokenType.Ident:
var cmd = cmdtok.str.toUpperCase();
// remark? ignore to eol
// remark? ignore all tokens to eol
if (cmd == 'REM') {
while (this.consumeToken().type != TokenType.EOL) { }
return null;
}
// look for "GO TO"
// look for "GO TO" and "GO SUB"
if (cmd == 'GO' && this.peekToken().str == 'TO') {
this.consumeToken();
cmd = 'GOTO';
} else if (cmd == 'GO' && this.peekToken().str == 'SUB') {
this.consumeToken();
cmd = 'GOSUB';
}
// lookup JS function for command
var fn = this['stmt__' + cmd];
if (fn) {
if (this.opts.validKeywords && this.opts.validKeywords.indexOf(cmd) < 0)
@ -371,10 +386,8 @@ export class BASICParser {
break;
}
case TokenType.EOL:
this.compileError(`I expected a command here`);
return null;
default:
this.compileError(`Unknown command "${cmdtok.str}"`);
this.compileError(`There should be a command here.`);
return null;
}
if (stmt) stmt.$loc = { line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: this.peekToken().$loc.start };
@ -387,6 +400,7 @@ export class BASICParser {
var tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
this.refs[tok.str] = tok.$loc;
let args = null;
if (this.peekToken().str == '(') {
this.expectToken('(');
@ -410,8 +424,10 @@ export class BASICParser {
this.pushbackToken(sep);
return list;
}
parseVarOrIndexedList(): IndOp[] {
return this.parseList(this.parseVarOrIndexed, ',');
parseLexprList(): IndOp[] {
var list = this.parseList(this.parseVarOrIndexed, ',');
list.forEach((lexpr) => this.decls[lexpr.name] = this.lasttoken.$loc);
return list;
}
parseExprList(): Expr[] {
return this.parseList(this.parseExpr, ',');
@ -427,7 +443,7 @@ export class BASICParser {
this.targets[label] = tok.$loc;
return {value:label};
default:
this.compileError(`I expected a line number here`);
this.compileError(`There should be a line number here.`);
return;
}
}
@ -437,12 +453,17 @@ export class BASICParser {
case TokenType.Int:
case TokenType.Float1:
case TokenType.Float2:
return { value: parseFloat(tok.str), $loc: tok.$loc };
return { value: this.parseNumber(tok.str), $loc: tok.$loc };
case TokenType.String:
return { value: stripQuotes(tok.str), $loc: tok.$loc };
case TokenType.Ident:
this.pushbackToken(tok);
return this.parseVarOrIndexedOrFunc();
if (tok.str == 'NOT') {
let expr = this.parsePrimary();
return { op: 'lnot', expr: expr };
} else {
this.pushbackToken(tok);
return this.parseVarOrIndexedOrFunc();
}
case TokenType.Operator:
if (tok.str == '(') {
let expr = this.parseExpr();
@ -454,21 +475,33 @@ export class BASICParser {
} else if (tok.str == '+') {
return this.parsePrimary(); // TODO?
}
default:
this.compileError(`Unexpected "${tok.str}"`);
case TokenType.EOL:
this.compileError(`The expression is incomplete.`);
return;
}
this.compileError(`There was an unexpected "${tok.str}" in this expression.`);
}
parseNumber(str: string) : number {
var n = parseFloat(str);
if (isNaN(n))
this.compileError(`The number ${str} is not a valid floating-point number.`);
if (this.opts.checkOverflow && !isFinite(n))
this.compileError(`The number ${str} is too big to fit into a floating-point value.`);
return n;
}
parseExpr1(left: Expr, minPred: number): Expr {
let look = this.peekToken();
while (getPrecedence(look) >= minPred) {
let op = this.consumeToken();
if (this.opts.validOperators && this.opts.validOperators.indexOf(op.str) < 0)
this.dialectError(`the "${op.str}" operator`);
let right: Expr = this.parsePrimary();
look = this.peekToken();
while (getPrecedence(look) > getPrecedence(op)) {
right = this.parseExpr1(right, getPrecedence(look));
look = this.peekToken();
}
left = { op: getOpcodeForOperator(op.str), left: left, right: right };
left = { op: getOperator(op.str).f, left: left, right: right };
}
return left;
}
@ -481,6 +514,7 @@ export class BASICParser {
stmt__LET(): LET_Statement {
var lexpr = this.parseVarOrIndexed();
this.expectToken("=");
this.decls[lexpr.name] = this.lasttoken.$loc;
var right = this.parseExpr();
return { command: "LET", lexpr: lexpr, right: right };
}
@ -547,7 +581,7 @@ export class BASICParser {
return { command:'NEXT', lexpr:lexpr };
}
stmt__DIM() : DIM_Statement {
return { command:'DIM', args:this.parseVarOrIndexedList() };
return { command:'DIM', args:this.parseLexprList() };
}
stmt__INPUT() : INPUT_Statement {
var prompt = this.consumeToken();
@ -559,13 +593,13 @@ export class BASICParser {
this.pushbackToken(prompt);
promptstr = "";
}
return { command:'INPUT', prompt:{ value: promptstr, $loc: prompt.$loc }, args:this.parseVarOrIndexedList() };
return { command:'INPUT', prompt:{ value: promptstr, $loc: prompt.$loc }, args:this.parseLexprList() };
}
stmt__DATA() : DATA_Statement {
return { command:'DATA', datums:this.parseExprList() };
}
stmt__READ() {
return { command:'READ', args:this.parseVarOrIndexedList() };
return { command:'READ', args:this.parseLexprList() };
}
stmt__RESTORE() {
return { command:'RESTORE' };
@ -587,22 +621,43 @@ export class BASICParser {
}
stmt__DEF() : DEF_Statement {
var lexpr = this.parseVarOrIndexed();
if (!lexpr.name.toUpperCase().startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`)
if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`)
this.expectToken("=");
this.decls[lexpr.name] = this.lasttoken.$loc;
var func = this.parseExpr();
return { command:'DEF', lexpr:lexpr, def:func };
}
stmt__OPTION() : OPTION_Statement {
var tokname = this.consumeToken();
if (tokname.type != TokenType.Ident) this.compileError(`I expected a name after the OPTION statement.`)
if (tokname.type != TokenType.Ident) this.compileError(`There should be a name after the OPTION statement.`)
var list : string[] = [];
var tok;
do {
tok = this.consumeToken();
if (isEOS(tok)) break;
list.push(tok.str.toUpperCase());
list.push(tok.str);
} while (true);
return { command:'OPTION', optname:tokname.str.toUpperCase(), optargs:list };
var stmt : OPTION_Statement = { command:'OPTION', optname:tokname.str, optargs:list };
this.parseOptions(stmt);
return stmt;
}
parseOptions(stmt: OPTION_Statement) {
switch (stmt.optname) {
case 'BASE':
let base = parseInt(stmt.optargs[0]);
if (base == 0 || base == 1) this.opts.defaultArrayBase = base;
else this.compileError("OPTION BASE can only be 0 or 1.");
break;
case 'DIALECT':
let dname = stmt.optargs[0] || "";
let dialect = DIALECTS[dname];
if (dialect) this.opts = dialect;
else this.compileError(`The dialect named "${dname}" is not supported by this compiler.`);
break;
default:
this.compileError(`OPTION ${stmt.optname} is not supported by this compiler.`);
break;
}
}
// for workermain
@ -620,13 +675,23 @@ export class BASICParser {
}
// LINT STUFF
checkAll(program : BASICProgram) {
this.checkLabels();
//this.checkUnsetVars();
}
checkLabels() {
for (let targ in this.targets) {
if (this.labels[targ] == null) {
this.compileError(`I couldn't find line number ${targ}`, this.targets[targ]);
this.compileError(`There isn't a line number ${targ}.`, this.targets[targ]);
}
}
}
checkUnsetVars() {
for (var ref in this.refs) {
if (this.decls[ref] == null)
this.compileError(`The variable "${ref}" was used but not set with a LET, DIM, READ, or INPUT statement.`);
}
}
}
///// BASIC DIALECTS
@ -643,18 +708,19 @@ export const ECMA55_MINIMAL : BASICOptions = {
stringConcat : false,
typeConvert : false,
maxDimensions : 2,
maxArguments : Infinity,
maxArguments : 255,
sparseArrays : false,
tickComments : false,
validKeywords : ['BASE','DATA','DEF','DIM','END',
'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT',
'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB','THEN','TO'
],
validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAN'],
validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN'],
validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'],
printZoneLength : 15,
printPrecision : 6,
checkOverflow : true,
multipleNextVars : false,
}
export const ALTAIR_BASIC40 : BASICOptions = {
@ -667,7 +733,7 @@ export const ALTAIR_BASIC40 : BASICOptions = {
stringConcat : false,
typeConvert : false,
maxDimensions : 2,
maxArguments : Infinity,
maxArguments : 255,
sparseArrays : false,
tickComments : false,
validKeywords : null, // all
@ -676,4 +742,13 @@ export const ALTAIR_BASIC40 : BASICOptions = {
printZoneLength : 15,
printPrecision : 6,
checkOverflow : true,
multipleNextVars : true,
}
export const DIALECTS = {
"DEFAULT": ALTAIR_BASIC40,
"ALTAIR": ALTAIR_BASIC40,
"ALTAIR40": ALTAIR_BASIC40,
"ECMA55": ECMA55_MINIMAL,
"MINIMAL": ECMA55_MINIMAL,
};

View File

@ -17,9 +17,13 @@ var data = fs.readFileSync(filename, 'utf-8');
try {
var pgm = parser.parseFile(data, filename);
} catch (e) {
console.log("@@@ " + e.message);
throw e;
if (parser.errors.length == 0)
console.log("@@@ " + e.msg);
else
console.log(e);
}
parser.errors.forEach((err) => console.log("@@@ " + err.msg));
if (parser.errors.length) process.exit(2);
var runtime = new BASICRuntime();
runtime.trace = process.argv[3] == '-v';
@ -47,7 +51,7 @@ runtime.resume = function() {
}
} catch (e) {
console.log("### " + e.message);
throw e;
process.exit(1);
}
});
}

View File

@ -39,16 +39,17 @@ export class BASICRuntime {
dataptr : number;
vars : {};
arrays : {};
forLoops : {};
defs : {};
forLoops : {next:(name:string) => void}[];
returnStack : number[];
column : number;
abase : number; // array base
running : boolean = false;
exited : boolean = false;
exited : boolean = true;
trace : boolean = false;
load(program: basic.BASICProgram) {
let prevlabel = this.label2pc && this.getLabelForPC(this.curpc);
this.program = program;
this.label2lineidx = {};
this.label2pc = {};
@ -59,6 +60,7 @@ export class BASICRuntime {
// TODO: lines start @ 1?
program.lines.forEach((line, idx) => {
// make lookup tables
this.curpc = this.allstmts.length + 1; // set for error reporting
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);
@ -69,8 +71,6 @@ export class BASICRuntime {
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});
});
@ -78,6 +78,9 @@ export class BASICRuntime {
// TODO: compile statements?
//line.stmts.forEach((stmt) => this.compileStatement(stmt));
});
// try to resume where we left off after loading
this.curpc = this.label2pc[prevlabel] || 0;
this.dataptr = Math.min(this.dataptr, this.datums.length);
}
reset() {
@ -85,21 +88,29 @@ export class BASICRuntime {
this.dataptr = 0;
this.vars = {};
this.arrays = {};
this.forLoops = {};
this.defs = this.getBuiltinFunctions();
this.forLoops = [];
this.returnStack = [];
this.column = 0;
this.abase = 1;
this.running = true;
this.exited = false;
}
getBuiltinFunctions() {
var fnames = this.program && this.program.opts.validFunctions;
// if no valid function list, look for ABC...() functions in prototype
if (!fnames) fnames = Object.keys(BASICRuntime.prototype).filter((name) => /^[A-Z]{3,}[$]?$/.test(name));
var dict = {};
for (var fn of fnames) dict[fn] = this[fn].bind(this);
return dict;
}
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 {
@ -131,7 +142,7 @@ export class BASICRuntime {
if (this.trace) console.log(this.curpc, stmt, this.vars, Object.keys(this.arrays));
// skip to next statment
this.curpc++;
// compile statement to JS?
// compile (unless cached) and execute statement
this.compileStatement(stmt);
this.executeStatement(stmt);
return this.running;
@ -171,7 +182,7 @@ export class BASICRuntime {
}
gosubLabel(label) {
this.returnStack.push(this.curpc + 1);
this.returnStack.push(this.curpc);
this.gotoLabel(label);
}
@ -240,9 +251,9 @@ export class BASICRuntime {
} 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(', ');
if (this.defs[expr.name]) { // is it a function?
s += `this.defs.${expr.name}(`;
if (expr.args) 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})`;
@ -252,7 +263,7 @@ export class BASICRuntime {
s = `this.vars.${expr.name}`;
}
if (opts.check)
return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`; // TODO: better error
return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`;
else
return s;
}
@ -260,64 +271,85 @@ export class BASICRuntime {
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') {
} else if (isUnOp(expr)) {
var e = this.expr2js(expr.expr, opts);
return `-(${e})`; // TODO: other ops?
return `this.${expr.op}(${e})`;
}
}
startForLoop(name, init, targ, step) {
// TODO: check for loop params
startForLoop(forname, init, targ, step) {
// TODO: support 0-iteration loops
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;
this.vars[forname] = init;
if (this.trace) console.log(`FOR ${forname} = ${init} TO ${targ} STEP ${step}`);
this.forLoops.unshift({
next: (nextname:string) => {
if (nextname && forname != nextname)
this.runtimeError(`I executed NEXT "${nextname}", but the last FOR was for "${forname}".`)
this.vars[forname] += step;
var done = step >= 0 ? this.vars[forname] > targ : this.vars[forname] < targ;
if (done) {
delete this.forLoops[name];
this.forLoops.shift(); // pop FOR off the stack and continue
} else {
this.vars[name] += step;
this.curpc = pc;
this.curpc = pc; // go back to FOR location
}
if (this.trace) console.log(`NEXT ${name}: ${this.vars[name]} TO ${targ} STEP ${step} DONE=${done}`);
if (this.trace) console.log(`NEXT ${forname}: ${this.vars[forname]} 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();
var fl = this.forLoops[0];
if (!fl) this.runtimeError(`I couldn't find a FOR for this NEXT.`)
else fl.next(name);
}
// converts a variable to string/number based on var name
assign(name: string, right: number|string) : number|string {
if (this.program.opts.typeConvert)
return this.convert(name, right);
// TODO: use options
if (name.endsWith("$")) {
return this.convertToString(right, name);
} else {
return this.convertToNumber(right, name);
}
}
convert(name: string, right: number|string) : number|string {
// TODO: error check?
if (name.endsWith("$"))
if (name.endsWith("$")) {
return right+"";
else if (typeof right === 'string')
return parseFloat(right);
else if (typeof right === 'number')
} else if (typeof right === 'number') {
return right;
else
return this.checkValue(right, name);
} 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);
}
// dimension array
dimArray(name: string, ...dims:number[]) {
if (this.arrays[name]) this.runtimeError(`I already dimensioned this array (${name}) earlier.`)
var isstring = name.endsWith('$');
// TODO: option for undefined float array elements?
var arrcons = isstring ? Array : Float64Array;
var ab = this.abase;
// if defaultValues is true, we use Float64Array which inits to 0
var arrcons = isstring || !this.program.opts.defaultValues ? Array : Float64Array;
// TODO? var ab = this.program.opts.defaultArrayBase;
if (dims.length == 1) {
this.arrays[name] = new arrcons(dims[0]+ab);
this.arrays[name] = new arrcons(dims[0]+1);
} else if (dims.length == 2) {
this.arrays[name] = new Array(dims[0]+ab);
for (var i=ab; i<dims[0]+ab; i++)
this.arrays[name][i] = new arrcons(dims[1]+ab);
this.arrays[name] = new Array(dims[0]+1);
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.`)
}
@ -364,15 +396,22 @@ export class BASICRuntime {
var setvals = '';
stmt.args.forEach((arg, index) => {
var lexpr = this.expr2js(arg, {check:false});
setvals += `${lexpr} = this.convert(${JSON.stringify(arg.name)}, vals[${index}]);`
setvals += `valid &= this.isValid(${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();})`;
return `this.running=false;
this.input(${prompt}, ${stmt.args.length}).then((vals) => {
let valid = 1;
${setvals}
if (!valid) this.curpc--;
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});`;
return `${lexpr} = this.assign(${JSON.stringify(stmt.lexpr.name)}, ${right});`;
}
do__FOR(stmt : basic.FOR_Statement) {
@ -384,7 +423,7 @@ export class BASICRuntime {
}
do__NEXT(stmt : basic.NEXT_Statement) {
var name = JSON.stringify(stmt.lexpr.name); // TODO: args? lexpr == null?
var name = stmt.lexpr && JSON.stringify(stmt.lexpr.name); // TODO: args? lexpr == null?
return `this.nextForLoop(${name})`;
}
@ -394,9 +433,9 @@ export class BASICRuntime {
}
do__DEF(stmt : basic.DEF_Statement) {
var lexpr = `this.${stmt.lexpr.name}`;
var lexpr = `this.defs.${stmt.lexpr.name}`;
var args = [];
for (var arg of stmt.lexpr.args) {
for (var arg of stmt.lexpr.args || []) {
if (isLookup(arg)) {
args.push(arg.name);
} else {
@ -404,14 +443,13 @@ export class BASICRuntime {
}
}
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}; }`;
return `${lexpr} = function(${args.join(',')}) { return ${functext}; }.bind(this)`;
}
_DIM(dim : basic.IndOp) {
var argsstr = '';
for (var arg of dim.args) {
// TODO: check for float
// TODO: check for float (or at compile time)
argsstr += ', ' + this.expr2js(arg, {check:true});
}
return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`;
@ -450,13 +488,13 @@ export class BASICRuntime {
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());`;
s += `${this.expr2js(arg, {check:false})} = this.assign(${JSON.stringify(arg.name)}, this.nextDatum());`;
});
return s;
}
do__RESTORE() {
this.dataptr = 0; // TODO: line number?
this.dataptr = 0;
}
do__END() {
@ -468,26 +506,30 @@ export class BASICRuntime {
}
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;
}
// already parsed in compiler
}
// TODO: "SUBSCRIPT ERROR"
// TODO: "SUBSCRIPT ERROR" (range check)
// TODO: gosubs nested too deeply
// TODO: memory quota
// FUNCTIONS
checkValue(obj:number|string, exprname:string) {
isValid(obj:number|string) : boolean {
if (typeof obj === 'number' && !isNaN(obj))
return true;
else if (typeof obj === 'string')
return true;
else
return false;
}
checkValue(obj:number|string, exprname:string) : number|string {
// check for unreferenced value
if (typeof obj !== 'number' && typeof obj !== 'string') {
// assign default value?
if (obj == null && this.program.opts.defaultValues) {
return exprname.endsWith("$") ? "" : 0;
}
if (exprname != null && obj == null) {
this.runtimeError(`I didn't find a value for ${exprname}`);
} else if (exprname != null) {
@ -527,6 +569,12 @@ export class BASICRuntime {
if (b == 0) this.runtimeError(`I can't divide by zero.`);
return this.checkNum(a / b);
}
idiv(a:number, b:number) : number {
return this.div(Math.floor(a), Math.floor(b));
}
mod(a:number, b:number) : number {
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));
@ -537,6 +585,18 @@ export class BASICRuntime {
lor(a:number, b:number) : number {
return a || b;
}
lnot(a:number) : number {
return a ? 0 : 1;
}
neg(a:number) : number {
return -a;
}
band(a:number, b:number) : number {
return a & b;
}
bor(a:number, b:number) : number {
return a | b;
}
eq(a:number, b:number) : boolean {
return a == b;
}
@ -604,7 +664,7 @@ export class BASICRuntime {
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.`)
if (start < 1) this.runtimeError(`I tried to compute MID$ but the second parameter is less than zero (${start}).`)
return arg.substr(start-1, count);
}
RIGHT$(arg : string, count : number) : string {
@ -614,7 +674,7 @@ export class BASICRuntime {
return Math.random(); // argument ignored
}
ROUND(arg : number) : number {
return this.checkNum(Math.round(arg)); // TODO?
return this.checkNum(Math.round(arg));
}
SGN(arg : number) : number {
return (arg < 0) ? -1 : (arg > 0) ? 1 : 0;
@ -623,24 +683,25 @@ export class BASICRuntime {
return this.checkNum(Math.sin(arg));
}
SPACE$(arg : number) : string {
return ' '.repeat(this.checkNum(arg));
return (arg > 0) ? ' '.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);
STR$(arg : number) : string {
return this.valueToString(this.checkNum(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;
if (arg < 1) { arg = 1; } // TODO: SYSTEM MESSAGE IDENTIFYING THE EXCEPTION
var spaces = this.ROUND(arg) - 1 - this.column;
return (spaces > 0) ? ' '.repeat(spaces) : '';
}
TAN(arg : number) : number {
return this.checkNum(Math.tan(arg));
}
VAL(arg) : number {
return parseFloat(arg+"");
VAL(arg : string) : number {
var n = parseFloat(this.checkString(arg));
return isNaN(n) ? 0 : n; // TODO? altair works this way
}
}

View File

@ -225,6 +225,10 @@ export class RAM {
}
export class EmuHalt extends Error {
constructor(msg:string) {
super(msg);
Object.setPrototypeOf(this, EmuHalt.prototype);
}
}
export class AnimationTimer {

View File

@ -98,7 +98,7 @@ export class SourceEditor implements ProjectView {
this.newEditor(div, asmOverride);
if (text) {
this.setText(text); // TODO: this calls setCode() and builds... it shouldn't
this.editor.setSelection({line:0,ch:0}, {line:0,ch:0}, {scroll:true});
this.editor.setSelection({line:0,ch:0}, {line:0,ch:0}, {scroll:true}); // move cursor to start
}
this.setupEditor();
return div;

View File

@ -1,6 +1,6 @@
import { Platform, BreakpointCallback } from "../common/baseplatform";
import { PLATFORMS, AnimationTimer } from "../common/emu";
import { PLATFORMS, AnimationTimer, EmuHalt } from "../common/emu";
import { loadScript } from "../ide/ui";
import { BASICRuntime } from "../common/basic/runtime";
import { BASICProgram } from "../common/basic/compiler";
@ -278,7 +278,7 @@ class BASICPlatform implements Platform {
//var printhead = $('<div id="printhead" class="transcript-print-head"/>').appendTo(parent);
this.tty = new TeleTypeWithKeyboard(windowport[0], true, inputline[0] as HTMLInputElement, this);
this.tty.scrolldiv = parent;
this.timer = new AnimationTimer(60, this.advance1_60.bind(this));
this.timer = new AnimationTimer(60, this.animate.bind(this));
this.resize = () => {
// set font size proportional to window width
var charwidth = $(gameport).width() * 1.6 / 80;
@ -286,11 +286,15 @@ class BASICPlatform implements Platform {
this.tty.scrollToBottom();
}
this.resize();
this.runtime.print = this.tty.print.bind(this.tty);
this.runtime.print = (s:string) => {
// TODO: why null sometimes?
this.clock = 0; // exit advance loop when printing
this.tty.print(s);
}
this.runtime.resume = this.resume.bind(this);
}
advance1_60() {
animate() {
if (this.tty.isBusy()) return;
this.clock += this.ips/60;
while (!this.runtime.exited && this.clock-- > 0) {
@ -298,33 +302,36 @@ class BASICPlatform implements Platform {
}
}
// should not depend on tty state
advance(novideo?: boolean) : number {
if (this.runtime.running) {
try {
var more = this.runtime.step();
if (!more) {
this.pause();
if (this.runtime.exited) {
this.tty.print("\n\n");
this.tty.addtext("*** END OF PROGRAM ***", 1);
}
var more = this.runtime.step();
if (!more) {
this.pause();
if (this.runtime.exited) {
this.exitmsg();
}
} catch (e) {
this.break();
throw e;
}
// TODO: break() when EmuHalt at location?
return 1;
} else {
return 0;
}
}
exitmsg() {
this.tty.print("\n\n");
this.tty.addtext("*** END OF PROGRAM ***", 1);
}
resize: () => void;
loadROM(title, data) {
this.reset();
var didExit = this.runtime.exited;
this.program = data;
this.runtime.load(data);
// only reset if we exited, otherwise we try to resume
if (didExit) this.reset();
}
getROMExtension() {
@ -403,8 +410,9 @@ class BASICPlatform implements Platform {
this.break();
}
break() {
// TODO: don't highlight text in editor
if (this.onBreakpointHit) {
this.onBreakpointHit(this.saveState());
//TODO: this.onBreakpointHit(this.saveState());
}
}
}