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? 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 identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
var openingKeywords = ['if','for']; var openingKeywords = ['if','for'];
var middleKeywords = ['to']; var middleKeywords = ['to','then'];
var endKeywords = ['next','end']; var endKeywords = ['next','end'];
var operatorKeywords = ['and', 'or', 'not', 'xor', 'in']; var operatorKeywords = ['and', 'or', 'not', 'xor', 'eqv', 'imp'];
var wordOperators = wordRegexp(operatorKeywords); var wordOperators = wordRegexp(operatorKeywords);
var commonKeywords = [ var commonKeywords = [
'let','print','go','goto','gosub','next','dim','input','data', 'BASE','DATA','DEF','DIM',
'read','restore','return','stop','on','def','option','then','step', 'GO','GOSUB','GOTO','INPUT','LET','ON','OPTION','PRINT',
'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB'
]; ];
var commontypes = ['xxxxbyte','xxxxword']; 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 // 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 { export enum TokenType {
EOL = 0, EOL = 0,
@ -21,8 +21,6 @@ export enum TokenType {
Float2, Float2,
Int, Int,
Remark, Remark,
And,
Or,
Ident, Ident,
String, String,
Relational, Relational,
@ -35,7 +33,7 @@ export type ExprTypes = BinOp | UnOp | IndOp | Literal;
export type Expr = ExprTypes & SourceLocated; 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; export type Value = string | number;
@ -50,7 +48,7 @@ export interface BinOp {
} }
export interface UnOp { export interface UnOp {
op: 'neg'; op: 'neg' | 'lnot';
expr: Expr; expr: Expr;
} }
@ -152,6 +150,7 @@ export interface BASICLine {
} }
export interface BASICProgram { export interface BASICProgram {
opts: BASICOptions;
lines: BASICLine[]; lines: BASICLine[];
} }
@ -162,23 +161,25 @@ class Token {
} }
const OPERATORS = { const OPERATORS = {
'AND': {f:'land',p:5}, 'OR': {f:'bor',p:7},
'OR': {f:'lor',p:5}, 'AND': {f:'band',p:8},
'=': {f:'eq',p:10}, '=': {f:'eq',p:50},
'<>': {f:'ne',p:10}, '<>': {f:'ne',p:50},
'<': {f:'lt',p:10}, '<': {f:'lt',p:50},
'>': {f:'gt',p:10}, '>': {f:'gt',p:50},
'<=': {f:'le',p:10}, '<=': {f:'le',p:50},
'>=': {f:'ge',p:10}, '>=': {f:'ge',p:50},
'+': {f:'add',p:100}, '+': {f:'add',p:100},
'-': {f:'sub',p:100}, '-': {f:'sub',p:100},
'%': {f:'mod',p:140},
'\\': {f:'idiv',p:150},
'*': {f:'mul',p:200}, '*': {f:'mul',p:200},
'/': {f:'div',p:200}, '/': {f:'div',p:200},
'^': {f:'pow',p:300} '^': {f:'pow',p:300}
}; };
function getOpcodeForOperator(op: string): Opcode { function getOperator(op: string) {
return OPERATORS[op].f as Opcode; return OPERATORS[op];
} }
function getPrecedence(tok: Token): number { function getPrecedence(tok: Token): number {
@ -186,7 +187,7 @@ function getPrecedence(tok: Token): number {
case TokenType.Operator: case TokenType.Operator:
case TokenType.Relational: case TokenType.Relational:
case TokenType.Ident: case TokenType.Ident:
let op = OPERATORS[tok.str] let op = getOperator(tok.str);
if (op) return op.p; if (op) return op.p;
} }
return -1; return -1;
@ -202,7 +203,7 @@ function stripQuotes(s: string) {
return s.substr(1, s.length-2); return s.substr(1, s.length-2);
} }
// TODO // TODO: implement these
export interface BASICOptions { export interface BASICOptions {
uppercaseOnly : boolean; // convert everything to uppercase? uppercaseOnly : boolean; // convert everything to uppercase?
strictVarNames : boolean; // only allow A0-9 for numerics, single letter for arrays/strings 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 printPrecision : number; // print precision # of digits
checkOverflow : boolean; // check for overflow of numerics? checkOverflow : boolean; // check for overflow of numerics?
defaultValues : boolean; // initialize unset variables to default value? (0 or "") defaultValues : boolean; // initialize unset variables to default value? (0 or "")
multipleNextVars : boolean; // NEXT Y,X
} }
///// BASIC PARSER ///// BASIC PARSER
export class BASICParser { export class BASICParser {
tokens: Token[]; opts : BASICOptions = ALTAIR_BASIC40;
errors: WorkerError[]; errors: WorkerError[];
listings: CodeListingMap;
labels: { [label: string]: BASICLine }; labels: { [label: string]: BASICLine };
targets: { [targetlabel: string]: SourceLocation }; targets: { [targetlabel: string]: SourceLocation };
eol: Token; decls: { [name: string]: SourceLocation }; // declared/set vars
refs: { [name: string]: SourceLocation }; // references
lineno : number; lineno : number;
tokens: Token[];
eol: Token;
curlabel: string; curlabel: string;
listings: CodeListingMap;
lasttoken: Token; lasttoken: Token;
opts : BASICOptions = ALTAIR_BASIC40;
constructor() { constructor() {
this.labels = {}; this.labels = {};
@ -245,6 +250,8 @@ export class BASICParser {
this.lineno = 0; this.lineno = 0;
this.curlabel = null; this.curlabel = null;
this.listings = {}; this.listings = {};
this.decls = {};
this.refs = {};
} }
compileError(msg: string, loc?: SourceLocation) { compileError(msg: string, loc?: SourceLocation) {
if (!loc) loc = this.peekToken().$loc; if (!loc) loc = this.peekToken().$loc;
@ -253,7 +260,7 @@ export class BASICParser {
throw new CompileError(`${msg} (line ${loc.line})`); // TODO: label too? throw new CompileError(`${msg} (line ${loc.line})`); // TODO: label too?
} }
dialectError(what: string, loc?: SourceLocation) { 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 { consumeToken(): Token {
var tok = this.lasttoken = (this.tokens.shift() || this.eol); var tok = this.lasttoken = (this.tokens.shift() || this.eol);
@ -261,9 +268,9 @@ export class BASICParser {
} }
expectToken(str: string) : Token { expectToken(str: string) : Token {
var tok = this.consumeToken(); var tok = this.consumeToken();
var tokstr = tok.str.toUpperCase(); var tokstr = tok.str;
if (str != tokstr) { if (str != tokstr) {
this.compileError(`I expected "${str}" here, but I saw "${tokstr}".`); this.compileError(`There should be a "${str}" here.`);
} }
return tok; return tok;
} }
@ -278,7 +285,7 @@ export class BASICParser {
let tok = this.consumeToken(); let tok = this.consumeToken();
switch (tok.type) { switch (tok.type) {
case TokenType.Int: 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; this.labels[tok.str] = line;
line.label = tok.str; line.label = tok.str;
this.curlabel = tok.str; this.curlabel = tok.str;
@ -291,8 +298,8 @@ export class BASICParser {
} }
parseFile(file: string, path: string) : BASICProgram { parseFile(file: string, path: string) : BASICProgram {
var pgmlines = file.split("\n").map((line) => this.parseLine(line)); var pgmlines = file.split("\n").map((line) => this.parseLine(line));
this.checkLabels(); var program = { opts: this.opts, lines: pgmlines };
var program = { lines: pgmlines }; this.checkAll(program);
this.listings[path] = this.generateListing(file, program); this.listings[path] = this.generateListing(file, program);
return program; return program;
} }
@ -308,11 +315,15 @@ export class BASICParser {
tokenize(line: string) : void { tokenize(line: string) : void {
this.lineno++; this.lineno++;
this.tokens = []; this.tokens = [];
var m; var m : RegExpMatchArray;
while (m = re_toks.exec(line)) { while (m = re_toks.exec(line)) {
for (var i = 1; i < TokenType._LAST; i++) { for (var i = 1; i < TokenType._LAST; i++) {
let s = m[i]; let s : string = m[i];
if (s != null) { 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({ this.tokens.push({
str: s, str: s,
type: i, type: i,
@ -341,23 +352,27 @@ export class BASICParser {
} }
parseStatement(): Statement | null { parseStatement(): Statement | null {
var cmdtok = this.consumeToken(); var cmdtok = this.consumeToken();
var cmd = cmdtok.str;
var stmt; var stmt;
switch (cmdtok.type) { switch (cmdtok.type) {
case TokenType.Remark: case TokenType.Remark:
if (!this.opts.tickComments) this.dialectError(`tick remarks`); if (!this.opts.tickComments) this.dialectError(`tick remarks`);
return null; return null;
case TokenType.Ident: case TokenType.Ident:
var cmd = cmdtok.str.toUpperCase(); // remark? ignore all tokens to eol
// remark? ignore to eol
if (cmd == 'REM') { if (cmd == 'REM') {
while (this.consumeToken().type != TokenType.EOL) { } while (this.consumeToken().type != TokenType.EOL) { }
return null; return null;
} }
// look for "GO TO" // look for "GO TO" and "GO SUB"
if (cmd == 'GO' && this.peekToken().str == 'TO') { if (cmd == 'GO' && this.peekToken().str == 'TO') {
this.consumeToken(); this.consumeToken();
cmd = 'GOTO'; cmd = 'GOTO';
} else if (cmd == 'GO' && this.peekToken().str == 'SUB') {
this.consumeToken();
cmd = 'GOSUB';
} }
// lookup JS function for command
var fn = this['stmt__' + cmd]; var fn = this['stmt__' + cmd];
if (fn) { if (fn) {
if (this.opts.validKeywords && this.opts.validKeywords.indexOf(cmd) < 0) if (this.opts.validKeywords && this.opts.validKeywords.indexOf(cmd) < 0)
@ -371,10 +386,8 @@ export class BASICParser {
break; break;
} }
case TokenType.EOL: case TokenType.EOL:
this.compileError(`I expected a command here`);
return null;
default: default:
this.compileError(`Unknown command "${cmdtok.str}"`); this.compileError(`There should be a command here.`);
return null; return null;
} }
if (stmt) stmt.$loc = { line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: this.peekToken().$loc.start }; 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(); var tok = this.consumeToken();
switch (tok.type) { switch (tok.type) {
case TokenType.Ident: case TokenType.Ident:
this.refs[tok.str] = tok.$loc;
let args = null; let args = null;
if (this.peekToken().str == '(') { if (this.peekToken().str == '(') {
this.expectToken('('); this.expectToken('(');
@ -410,8 +424,10 @@ export class BASICParser {
this.pushbackToken(sep); this.pushbackToken(sep);
return list; return list;
} }
parseVarOrIndexedList(): IndOp[] { parseLexprList(): IndOp[] {
return this.parseList(this.parseVarOrIndexed, ','); var list = this.parseList(this.parseVarOrIndexed, ',');
list.forEach((lexpr) => this.decls[lexpr.name] = this.lasttoken.$loc);
return list;
} }
parseExprList(): Expr[] { parseExprList(): Expr[] {
return this.parseList(this.parseExpr, ','); return this.parseList(this.parseExpr, ',');
@ -427,7 +443,7 @@ export class BASICParser {
this.targets[label] = tok.$loc; this.targets[label] = tok.$loc;
return {value:label}; return {value:label};
default: default:
this.compileError(`I expected a line number here`); this.compileError(`There should be a line number here.`);
return; return;
} }
} }
@ -437,12 +453,17 @@ export class BASICParser {
case TokenType.Int: case TokenType.Int:
case TokenType.Float1: case TokenType.Float1:
case TokenType.Float2: case TokenType.Float2:
return { value: parseFloat(tok.str), $loc: tok.$loc }; return { value: this.parseNumber(tok.str), $loc: tok.$loc };
case TokenType.String: case TokenType.String:
return { value: stripQuotes(tok.str), $loc: tok.$loc }; return { value: stripQuotes(tok.str), $loc: tok.$loc };
case TokenType.Ident: case TokenType.Ident:
this.pushbackToken(tok); if (tok.str == 'NOT') {
return this.parseVarOrIndexedOrFunc(); let expr = this.parsePrimary();
return { op: 'lnot', expr: expr };
} else {
this.pushbackToken(tok);
return this.parseVarOrIndexedOrFunc();
}
case TokenType.Operator: case TokenType.Operator:
if (tok.str == '(') { if (tok.str == '(') {
let expr = this.parseExpr(); let expr = this.parseExpr();
@ -454,21 +475,33 @@ export class BASICParser {
} else if (tok.str == '+') { } else if (tok.str == '+') {
return this.parsePrimary(); // TODO? return this.parsePrimary(); // TODO?
} }
default: case TokenType.EOL:
this.compileError(`Unexpected "${tok.str}"`); 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 { parseExpr1(left: Expr, minPred: number): Expr {
let look = this.peekToken(); let look = this.peekToken();
while (getPrecedence(look) >= minPred) { while (getPrecedence(look) >= minPred) {
let op = this.consumeToken(); 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(); let right: Expr = this.parsePrimary();
look = this.peekToken(); look = this.peekToken();
while (getPrecedence(look) > getPrecedence(op)) { while (getPrecedence(look) > getPrecedence(op)) {
right = this.parseExpr1(right, getPrecedence(look)); right = this.parseExpr1(right, getPrecedence(look));
look = this.peekToken(); look = this.peekToken();
} }
left = { op: getOpcodeForOperator(op.str), left: left, right: right }; left = { op: getOperator(op.str).f, left: left, right: right };
} }
return left; return left;
} }
@ -481,6 +514,7 @@ export class BASICParser {
stmt__LET(): LET_Statement { stmt__LET(): LET_Statement {
var lexpr = this.parseVarOrIndexed(); var lexpr = this.parseVarOrIndexed();
this.expectToken("="); this.expectToken("=");
this.decls[lexpr.name] = this.lasttoken.$loc;
var right = this.parseExpr(); var right = this.parseExpr();
return { command: "LET", lexpr: lexpr, right: right }; return { command: "LET", lexpr: lexpr, right: right };
} }
@ -547,7 +581,7 @@ export class BASICParser {
return { command:'NEXT', lexpr:lexpr }; return { command:'NEXT', lexpr:lexpr };
} }
stmt__DIM() : DIM_Statement { stmt__DIM() : DIM_Statement {
return { command:'DIM', args:this.parseVarOrIndexedList() }; return { command:'DIM', args:this.parseLexprList() };
} }
stmt__INPUT() : INPUT_Statement { stmt__INPUT() : INPUT_Statement {
var prompt = this.consumeToken(); var prompt = this.consumeToken();
@ -559,13 +593,13 @@ export class BASICParser {
this.pushbackToken(prompt); this.pushbackToken(prompt);
promptstr = ""; 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 { stmt__DATA() : DATA_Statement {
return { command:'DATA', datums:this.parseExprList() }; return { command:'DATA', datums:this.parseExprList() };
} }
stmt__READ() { stmt__READ() {
return { command:'READ', args:this.parseVarOrIndexedList() }; return { command:'READ', args:this.parseLexprList() };
} }
stmt__RESTORE() { stmt__RESTORE() {
return { command:'RESTORE' }; return { command:'RESTORE' };
@ -587,22 +621,43 @@ export class BASICParser {
} }
stmt__DEF() : DEF_Statement { stmt__DEF() : DEF_Statement {
var lexpr = this.parseVarOrIndexed(); 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.expectToken("=");
this.decls[lexpr.name] = this.lasttoken.$loc;
var func = this.parseExpr(); var func = this.parseExpr();
return { command:'DEF', lexpr:lexpr, def:func }; return { command:'DEF', lexpr:lexpr, def:func };
} }
stmt__OPTION() : OPTION_Statement { stmt__OPTION() : OPTION_Statement {
var tokname = this.consumeToken(); 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 list : string[] = [];
var tok; var tok;
do { do {
tok = this.consumeToken(); tok = this.consumeToken();
if (isEOS(tok)) break; if (isEOS(tok)) break;
list.push(tok.str.toUpperCase()); list.push(tok.str);
} while (true); } 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 // for workermain
@ -620,13 +675,23 @@ export class BASICParser {
} }
// LINT STUFF // LINT STUFF
checkAll(program : BASICProgram) {
this.checkLabels();
//this.checkUnsetVars();
}
checkLabels() { checkLabels() {
for (let targ in this.targets) { for (let targ in this.targets) {
if (this.labels[targ] == null) { 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 ///// BASIC DIALECTS
@ -643,18 +708,19 @@ export const ECMA55_MINIMAL : BASICOptions = {
stringConcat : false, stringConcat : false,
typeConvert : false, typeConvert : false,
maxDimensions : 2, maxDimensions : 2,
maxArguments : Infinity, maxArguments : 255,
sparseArrays : false, sparseArrays : false,
tickComments : false, tickComments : false,
validKeywords : ['BASE','DATA','DEF','DIM','END', validKeywords : ['BASE','DATA','DEF','DIM','END',
'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT', 'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT',
'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB','THEN','TO' '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 : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'], validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'],
printZoneLength : 15, printZoneLength : 15,
printPrecision : 6, printPrecision : 6,
checkOverflow : true, checkOverflow : true,
multipleNextVars : false,
} }
export const ALTAIR_BASIC40 : BASICOptions = { export const ALTAIR_BASIC40 : BASICOptions = {
@ -667,7 +733,7 @@ export const ALTAIR_BASIC40 : BASICOptions = {
stringConcat : false, stringConcat : false,
typeConvert : false, typeConvert : false,
maxDimensions : 2, maxDimensions : 2,
maxArguments : Infinity, maxArguments : 255,
sparseArrays : false, sparseArrays : false,
tickComments : false, tickComments : false,
validKeywords : null, // all validKeywords : null, // all
@ -676,4 +742,13 @@ export const ALTAIR_BASIC40 : BASICOptions = {
printZoneLength : 15, printZoneLength : 15,
printPrecision : 6, printPrecision : 6,
checkOverflow : true, 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 { try {
var pgm = parser.parseFile(data, filename); var pgm = parser.parseFile(data, filename);
} catch (e) { } catch (e) {
console.log("@@@ " + e.message); if (parser.errors.length == 0)
throw e; 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(); var runtime = new BASICRuntime();
runtime.trace = process.argv[3] == '-v'; runtime.trace = process.argv[3] == '-v';
@ -47,7 +51,7 @@ runtime.resume = function() {
} }
} catch (e) { } catch (e) {
console.log("### " + e.message); console.log("### " + e.message);
throw e; process.exit(1);
} }
}); });
} }

View File

@ -39,16 +39,17 @@ export class BASICRuntime {
dataptr : number; dataptr : number;
vars : {}; vars : {};
arrays : {}; arrays : {};
forLoops : {}; defs : {};
forLoops : {next:(name:string) => void}[];
returnStack : number[]; returnStack : number[];
column : number; column : number;
abase : number; // array base
running : boolean = false; running : boolean = false;
exited : boolean = false; exited : boolean = true;
trace : boolean = false; trace : boolean = false;
load(program: basic.BASICProgram) { load(program: basic.BASICProgram) {
let prevlabel = this.label2pc && this.getLabelForPC(this.curpc);
this.program = program; this.program = program;
this.label2lineidx = {}; this.label2lineidx = {};
this.label2pc = {}; this.label2pc = {};
@ -59,6 +60,7 @@ export class BASICRuntime {
// TODO: lines start @ 1? // TODO: lines start @ 1?
program.lines.forEach((line, idx) => { program.lines.forEach((line, idx) => {
// make lookup tables // 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.label2lineidx[line.label] = idx;
if (line.label != null) this.label2pc[line.label] = this.allstmts.length; if (line.label != null) this.label2pc[line.label] = this.allstmts.length;
this.line2pc.push(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) => { line.stmts.filter((stmt) => stmt.command == 'DATA').forEach((datastmt) => {
(datastmt as basic.DATA_Statement).datums.forEach(d => { (datastmt as basic.DATA_Statement).datums.forEach(d => {
var functext = this.expr2js(d, {check:true, isconst:true}); 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)(); var value = new Function(`return ${functext};`).bind(this)();
this.datums.push({value:value}); this.datums.push({value:value});
}); });
@ -78,6 +78,9 @@ export class BASICRuntime {
// TODO: compile statements? // TODO: compile statements?
//line.stmts.forEach((stmt) => this.compileStatement(stmt)); //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() { reset() {
@ -85,21 +88,29 @@ export class BASICRuntime {
this.dataptr = 0; this.dataptr = 0;
this.vars = {}; this.vars = {};
this.arrays = {}; this.arrays = {};
this.forLoops = {}; this.defs = this.getBuiltinFunctions();
this.forLoops = [];
this.returnStack = []; this.returnStack = [];
this.column = 0; this.column = 0;
this.abase = 1;
this.running = true; this.running = true;
this.exited = false; 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) { runtimeError(msg : string) {
this.curpc--; // we did curpc++ before executing statement this.curpc--; // we did curpc++ before executing statement
// TODO: pass source location to error // TODO: pass source location to error
throw new EmuHalt(`${msg} (line ${this.getLabelForPC(this.curpc)})`); throw new EmuHalt(`${msg} (line ${this.getLabelForPC(this.curpc)})`);
} }
// TODO: sometimes on next line
getLineForPC(pc:number) { getLineForPC(pc:number) {
var line; var line;
do { do {
@ -131,7 +142,7 @@ export class BASICRuntime {
if (this.trace) console.log(this.curpc, stmt, this.vars, Object.keys(this.arrays)); if (this.trace) console.log(this.curpc, stmt, this.vars, Object.keys(this.arrays));
// skip to next statment // skip to next statment
this.curpc++; this.curpc++;
// compile statement to JS? // compile (unless cached) and execute statement
this.compileStatement(stmt); this.compileStatement(stmt);
this.executeStatement(stmt); this.executeStatement(stmt);
return this.running; return this.running;
@ -171,7 +182,7 @@ export class BASICRuntime {
} }
gosubLabel(label) { gosubLabel(label) {
this.returnStack.push(this.curpc + 1); this.returnStack.push(this.curpc);
this.gotoLabel(label); this.gotoLabel(label);
} }
@ -240,9 +251,9 @@ export class BASICRuntime {
} else { } else {
if (opts.isconst) this.runtimeError(`I expected a constant value here`); if (opts.isconst) this.runtimeError(`I expected a constant value here`);
var s = ''; var s = '';
if (expr.args && this[expr.name]) { // is it a function? if (this.defs[expr.name]) { // is it a function?
s += `this.${expr.name}(`; s += `this.defs.${expr.name}(`;
s += expr.args.map((arg) => this.expr2js(arg, opts)).join(', '); if (expr.args) s += expr.args.map((arg) => this.expr2js(arg, opts)).join(', ');
s += ')'; s += ')';
} else if (expr.args) { // is it a subscript? } else if (expr.args) { // is it a subscript?
s += `this.getArray(${JSON.stringify(expr.name)}, ${expr.args.length})`; s += `this.getArray(${JSON.stringify(expr.name)}, ${expr.args.length})`;
@ -252,7 +263,7 @@ export class BASICRuntime {
s = `this.vars.${expr.name}`; s = `this.vars.${expr.name}`;
} }
if (opts.check) if (opts.check)
return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`; // TODO: better error return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`;
else else
return s; return s;
} }
@ -260,64 +271,85 @@ export class BASICRuntime {
var left = this.expr2js(expr.left, opts); var left = this.expr2js(expr.left, opts);
var right = this.expr2js(expr.right, opts); var right = this.expr2js(expr.right, opts);
return `this.${expr.op}(${left}, ${right})`; return `this.${expr.op}(${left}, ${right})`;
} else if (isUnOp(expr) && expr.op == 'neg') { } else if (isUnOp(expr)) {
var e = this.expr2js(expr.expr, opts); var e = this.expr2js(expr.expr, opts);
return `-(${e})`; // TODO: other ops? return `this.${expr.op}(${e})`;
} }
} }
startForLoop(name, init, targ, step) { startForLoop(forname, init, targ, step) {
// TODO: check for loop params // TODO: support 0-iteration loops
var pc = this.curpc; var pc = this.curpc;
if (!step) step = 1; if (!step) step = 1;
this.vars[name] = init; this.vars[forname] = init;
if (this.trace) console.log(`FOR ${name} = ${this.vars[name]} TO ${targ} STEP ${step}`); if (this.trace) console.log(`FOR ${forname} = ${init} TO ${targ} STEP ${step}`);
this.forLoops[name] = { this.forLoops.unshift({
next: () => { next: (nextname:string) => {
var done = step >= 0 ? this.vars[name] >= targ : this.vars[name] <= targ; 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) { if (done) {
delete this.forLoops[name]; this.forLoops.shift(); // pop FOR off the stack and continue
} else { } else {
this.vars[name] += step; this.curpc = pc; // go back to FOR location
this.curpc = pc;
} }
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) { nextForLoop(name) {
// TODO: check for for loop var fl = this.forLoops[0];
var fl = this.forLoops[name]; if (!fl) this.runtimeError(`I couldn't find a FOR for this NEXT.`)
if (!fl) this.runtimeError(`I couldn't find a matching FOR for this NEXT.`) else fl.next(name);
this.forLoops[name].next();
} }
// converts a variable to string/number based on var 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 { convert(name: string, right: number|string) : number|string {
// TODO: error check? if (name.endsWith("$")) {
if (name.endsWith("$"))
return right+""; return right+"";
else if (typeof right === 'string') } else if (typeof right === 'number') {
return parseFloat(right);
else if (typeof right === 'number')
return right; return right;
else } else {
return this.checkValue(right, name); 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 // dimension array
dimArray(name: string, ...dims:number[]) { dimArray(name: string, ...dims:number[]) {
if (this.arrays[name]) this.runtimeError(`I already dimensioned this array (${name}) earlier.`)
var isstring = name.endsWith('$'); var isstring = name.endsWith('$');
// TODO: option for undefined float array elements? // if defaultValues is true, we use Float64Array which inits to 0
var arrcons = isstring ? Array : Float64Array; var arrcons = isstring || !this.program.opts.defaultValues ? Array : Float64Array;
var ab = this.abase; // TODO? var ab = this.program.opts.defaultArrayBase;
if (dims.length == 1) { 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) { } else if (dims.length == 2) {
this.arrays[name] = new Array(dims[0]+ab); this.arrays[name] = new Array(dims[0]+1);
for (var i=ab; i<dims[0]+ab; i++) for (var i=0; i<dims[0]+1; i++)
this.arrays[name][i] = new arrcons(dims[1]+ab); this.arrays[name][i] = new arrcons(dims[1]+1);
} else { } else {
this.runtimeError(`I only support arrays of one or two dimensions.`) this.runtimeError(`I only support arrays of one or two dimensions.`)
} }
@ -364,15 +396,22 @@ export class BASICRuntime {
var setvals = ''; var setvals = '';
stmt.args.forEach((arg, index) => { stmt.args.forEach((arg, index) => {
var lexpr = this.expr2js(arg, {check:false}); 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) { do__LET(stmt : basic.LET_Statement) {
var lexpr = this.expr2js(stmt.lexpr, {check:false}); var lexpr = this.expr2js(stmt.lexpr, {check:false});
var right = this.expr2js(stmt.right, {check:true}); 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) { do__FOR(stmt : basic.FOR_Statement) {
@ -384,7 +423,7 @@ export class BASICRuntime {
} }
do__NEXT(stmt : basic.NEXT_Statement) { 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})`; return `this.nextForLoop(${name})`;
} }
@ -394,9 +433,9 @@ export class BASICRuntime {
} }
do__DEF(stmt : basic.DEF_Statement) { do__DEF(stmt : basic.DEF_Statement) {
var lexpr = `this.${stmt.lexpr.name}`; var lexpr = `this.defs.${stmt.lexpr.name}`;
var args = []; var args = [];
for (var arg of stmt.lexpr.args) { for (var arg of stmt.lexpr.args || []) {
if (isLookup(arg)) { if (isLookup(arg)) {
args.push(arg.name); args.push(arg.name);
} else { } else {
@ -404,14 +443,13 @@ export class BASICRuntime {
} }
} }
var functext = this.expr2js(stmt.def, {check:true, locals:args}); 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}; }.bind(this)`;
return `${lexpr} = function(${args.join(',')}) { return ${functext}; }`;
} }
_DIM(dim : basic.IndOp) { _DIM(dim : basic.IndOp) {
var argsstr = ''; var argsstr = '';
for (var arg of dim.args) { for (var arg of dim.args) {
// TODO: check for float // TODO: check for float (or at compile time)
argsstr += ', ' + this.expr2js(arg, {check:true}); argsstr += ', ' + this.expr2js(arg, {check:true});
} }
return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`; return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`;
@ -450,13 +488,13 @@ export class BASICRuntime {
do__READ(stmt : basic.READ_Statement) { do__READ(stmt : basic.READ_Statement) {
var s = ''; var s = '';
stmt.args.forEach((arg) => { 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; return s;
} }
do__RESTORE() { do__RESTORE() {
this.dataptr = 0; // TODO: line number? this.dataptr = 0;
} }
do__END() { do__END() {
@ -468,26 +506,30 @@ export class BASICRuntime {
} }
do__OPTION(stmt: basic.OPTION_Statement) { do__OPTION(stmt: basic.OPTION_Statement) {
switch (stmt.optname) { // already parsed in compiler
case 'BASE':
let base = parseInt(stmt.optargs[0]);
if (base == 0 || base == 1) this.abase = base;
else this.runtimeError("OPTION BASE can only be 0 or 1.");
break;
default:
this.runtimeError(`OPTION ${stmt.optname} is not supported by this compiler.`);
break;
}
} }
// TODO: "SUBSCRIPT ERROR" // TODO: "SUBSCRIPT ERROR" (range check)
// TODO: gosubs nested too deeply // TODO: gosubs nested too deeply
// TODO: memory quota // TODO: memory quota
// FUNCTIONS // 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') { 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) { if (exprname != null && obj == null) {
this.runtimeError(`I didn't find a value for ${exprname}`); this.runtimeError(`I didn't find a value for ${exprname}`);
} else if (exprname != null) { } else if (exprname != null) {
@ -527,6 +569,12 @@ export class BASICRuntime {
if (b == 0) this.runtimeError(`I can't divide by zero.`); if (b == 0) this.runtimeError(`I can't divide by zero.`);
return this.checkNum(a / b); 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 { pow(a:number, b:number) : number {
if (a == 0 && b < 0) this.runtimeError(`I can't raise zero to a negative power.`); if (a == 0 && b < 0) this.runtimeError(`I can't raise zero to a negative power.`);
return this.checkNum(Math.pow(a, b)); return this.checkNum(Math.pow(a, b));
@ -537,6 +585,18 @@ export class BASICRuntime {
lor(a:number, b:number) : number { lor(a:number, b:number) : number {
return a || b; 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 { eq(a:number, b:number) : boolean {
return a == b; return a == b;
} }
@ -604,7 +664,7 @@ export class BASICRuntime {
return this.checkNum(Math.log(arg)); return this.checkNum(Math.log(arg));
} }
MID$(arg : string, start : number, count : number) : string { 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); return arg.substr(start-1, count);
} }
RIGHT$(arg : string, count : number) : string { RIGHT$(arg : string, count : number) : string {
@ -614,7 +674,7 @@ export class BASICRuntime {
return Math.random(); // argument ignored return Math.random(); // argument ignored
} }
ROUND(arg : number) : number { ROUND(arg : number) : number {
return this.checkNum(Math.round(arg)); // TODO? return this.checkNum(Math.round(arg));
} }
SGN(arg : number) : number { SGN(arg : number) : number {
return (arg < 0) ? -1 : (arg > 0) ? 1 : 0; return (arg < 0) ? -1 : (arg > 0) ? 1 : 0;
@ -623,24 +683,25 @@ export class BASICRuntime {
return this.checkNum(Math.sin(arg)); return this.checkNum(Math.sin(arg));
} }
SPACE$(arg : number) : string { SPACE$(arg : number) : string {
return ' '.repeat(this.checkNum(arg)); return (arg > 0) ? ' '.repeat(this.checkNum(arg)) : '';
} }
SQR(arg : number) : number { SQR(arg : number) : number {
if (arg < 0) this.runtimeError(`I can't take the square root of a negative number (${arg}).`) if (arg < 0) this.runtimeError(`I can't take the square root of a negative number (${arg}).`)
return this.checkNum(Math.sqrt(arg)); return this.checkNum(Math.sqrt(arg));
} }
STR$(arg) : string { STR$(arg : number) : string {
return this.valueToString(arg); return this.valueToString(this.checkNum(arg));
} }
TAB(arg : number) : string { TAB(arg : number) : string {
if (arg < 0) this.runtimeError(`I got a negative value for the TAB() function.`); if (arg < 1) { arg = 1; } // TODO: SYSTEM MESSAGE IDENTIFYING THE EXCEPTION
var spaces = this.ROUND(arg) - this.column; var spaces = this.ROUND(arg) - 1 - this.column;
return (spaces > 0) ? ' '.repeat(spaces) : ''; return (spaces > 0) ? ' '.repeat(spaces) : '';
} }
TAN(arg : number) : number { TAN(arg : number) : number {
return this.checkNum(Math.tan(arg)); return this.checkNum(Math.tan(arg));
} }
VAL(arg) : number { VAL(arg : string) : number {
return parseFloat(arg+""); 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 { export class EmuHalt extends Error {
constructor(msg:string) {
super(msg);
Object.setPrototypeOf(this, EmuHalt.prototype);
}
} }
export class AnimationTimer { export class AnimationTimer {

View File

@ -98,7 +98,7 @@ export class SourceEditor implements ProjectView {
this.newEditor(div, asmOverride); this.newEditor(div, asmOverride);
if (text) { if (text) {
this.setText(text); // TODO: this calls setCode() and builds... it shouldn't 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(); this.setupEditor();
return div; return div;

View File

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