diff --git a/css/codemirror.css b/css/codemirror.css
index 5a4f494c..2d4b4be6 100644
--- a/css/codemirror.css
+++ b/css/codemirror.css
@@ -371,7 +371,7 @@ span.CodeMirror-selectedtext { background: none; }
.cm-s-mbo span.cm-property, .cm-s-mbo span.cm-attribute { color: #9ddfe9; }
.cm-s-mbo span.cm-keyword { color: #ffb928; }
-.cm-s-mbo span.cm-string { color: #ffcf6c; }
+.cm-s-mbo span.cm-string { color: #b4fdb7; }
.cm-s-mbo span.cm-string.cm-property { color: #ffffec; }
.cm-s-mbo span.cm-variable { color: #ffffec; }
diff --git a/css/ui.css b/css/ui.css
index 381e9094..131f06e9 100644
--- a/css/ui.css
+++ b/css/ui.css
@@ -353,7 +353,7 @@ div.replaydiv {
background-repeat: no-repeat;
background-position: 50%;
pointer-events:auto;
- z-index:1;
+ z-index:2;
}
.gutter.gutter-horizontal {
background-image: url('grips/vertical.png');
@@ -631,6 +631,15 @@ div.asset_toolbar {
.transcript-input-char {
max-width:5em;
}
+.transcript-print-head {
+ background: url('../images/print-head.png') no-repeat scroll 0 0 transparent;
+ position: absolute;
+ bottom: 0;
+ height: 3em;
+ width: 100%;
+ z-index: 1;
+ pointer-events: none;
+}
.tree-header {
border: 2px solid #555;
border-radius:8px;
diff --git a/electron.html b/electron.html
index 23591966..a96e76f1 100644
--- a/electron.html
+++ b/electron.html
@@ -361,6 +361,7 @@ body {
+
diff --git a/index.html b/index.html
index 8c74cb93..e6d8ef67 100644
--- a/index.html
+++ b/index.html
@@ -556,6 +556,7 @@ if (window.location.host.endsWith('8bitworkshop.com')) {
+
diff --git a/presets/basic/hello.bas b/presets/basic/hello.bas
new file mode 100644
index 00000000..a1327ddd
--- /dev/null
+++ b/presets/basic/hello.bas
@@ -0,0 +1,10 @@
+10 PRINT "HELLO! LET'S PROGRAM IN BASIC."
+15 PRINT
+20 INPUT "WOULD YOU MIND TYPING IN YOUR NAME";A$
+25 PRINT
+30 PRINT "THANKS, ";A$;"! THIS WILL BE FUN!"
+35 PRINT
+40 INPUT "NOW TELL ME YOUR FAVORITE NUMBER";N
+45 PRINT
+50 PRINT "THAT'S A GOOD ONE! I LIKE";N^2;"MYSELF."
+60 PRINT "NICE MEETING YOU, ";A$;"."
diff --git a/src/codemirror/basic.js b/src/codemirror/basic.js
new file mode 100644
index 00000000..eaf34014
--- /dev/null
+++ b/src/codemirror/basic.js
@@ -0,0 +1,286 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.defineMode("basic", function(conf, parserConf) {
+ var ERRORCLASS = 'error';
+
+ function wordRegexp(words) {
+ return new RegExp("^((" + words.join(")|(") + "))\\b", "i");
+ }
+
+ var singleOperators = new RegExp("^[\\+\\-\\*/%&\\\\|\\^~<>!]");
+ var singleDelimiters = new RegExp('^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]');
+ var doubleOperators = new RegExp("^((==)|(<>)|(<=)|(>=)|(<>)|(<<)|(>>)|(//)|(\\*\\*))");
+ var doubleDelimiters = new RegExp("^((\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))");
+ var tripleDelimiters = new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))");
+ var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
+
+ var openingKeywords = ['if','for'];
+ var middleKeywords = ['to'];
+ var endKeywords = ['next','end'];
+
+ var operatorKeywords = ['and', 'or', 'not', 'xor', 'in'];
+ var wordOperators = wordRegexp(operatorKeywords);
+ var commonKeywords = [
+ 'let','print','go','goto','gosub','next','dim','input','data',
+ 'read','restore','return','stop','on','def','option','then','step',
+ ];
+ var commontypes = ['xxxxbyte','xxxxword'];
+
+ var keywords = wordRegexp(commonKeywords);
+ var types = wordRegexp(commontypes);
+ var stringPrefixes = '"';
+
+ var opening = wordRegexp(openingKeywords);
+ var middle = wordRegexp(middleKeywords);
+ var closing = wordRegexp(endKeywords);
+ var doubleClosing = wordRegexp(['end']);
+ var doOpening = wordRegexp(['do']);
+
+ var indentInfo = null;
+
+ CodeMirror.registerHelper("hintWords", "vb", openingKeywords.concat(middleKeywords).concat(endKeywords)
+ .concat(operatorKeywords).concat(commonKeywords).concat(commontypes));
+
+ function indent(_stream, state) {
+ state.currentIndent++;
+ }
+
+ function dedent(_stream, state) {
+ state.currentIndent--;
+ }
+ // tokenizers
+ function tokenBase(stream, state) {
+ if (stream.eatSpace()) {
+ return null;
+ }
+
+ var ch = stream.peek();
+
+ // Handle Comments
+ if (ch === "'") {
+ stream.skipToEnd();
+ return 'comment';
+ }
+
+
+ // Handle Number Literals
+ if (stream.match(/^(\$)?[0-9\.a-f]/i, false)) {
+ var floatLiteral = false;
+ // Floats
+ if (stream.match(/^\d*\.\d+F?/i)) { floatLiteral = true; }
+ else if (stream.match(/^\d+\.\d*F?/)) { floatLiteral = true; }
+ else if (stream.match(/^\.\d+F?/)) { floatLiteral = true; }
+
+ if (floatLiteral) {
+ // Float literals may be "imaginary"
+ stream.eat(/J/i);
+ return 'number';
+ }
+ // Integers
+ var intLiteral = false;
+ // Hex
+ if (stream.match(/^\$[0-9a-f]+/i)) { intLiteral = true; }
+ // Octal
+ else if (stream.match(/^&O[0-7]+/i)) { intLiteral = true; }
+ // Decimal
+ else if (stream.match(/^[1-9]\d*F?/)) {
+ // Decimal literals may be "imaginary"
+ stream.eat(/J/i);
+ // TODO - Can you have imaginary longs?
+ intLiteral = true;
+ }
+ // Zero by itself with no other piece of number.
+ else if (stream.match(/^0(?![\dx])/i)) { intLiteral = true; }
+ if (intLiteral) {
+ // Integer literals may be "long"
+ stream.eat(/L/i);
+ return 'number';
+ }
+ }
+
+ // Handle RAM
+ if (stream.match(/\bREM/i)) {
+ stream.skipToEnd();
+ return 'comment';
+ }
+
+ // Handle Strings
+ if (stream.match(stringPrefixes)) {
+ state.tokenize = tokenStringFactory(stream.current());
+ return state.tokenize(stream, state);
+ }
+
+ // Handle operators and Delimiters
+ if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) {
+ return null;
+ }
+ if (stream.match(doubleOperators)
+ || stream.match(singleOperators)
+ || stream.match(wordOperators)) {
+ return 'operator';
+ }
+ if (stream.match(singleDelimiters)) {
+ return null;
+ }
+ if (stream.match(doOpening)) {
+ /*
+ indent(stream,state);
+ state.doInCurrentLine = true;
+ */
+ return 'keyword';
+ }
+ if (stream.match(opening)) {
+ /*
+ if (! state.doInCurrentLine)
+ indent(stream,state);
+ else
+ state.doInCurrentLine = false;
+ */
+ return 'keyword';
+ }
+ if (stream.match(middle)) {
+ return 'keyword';
+ }
+
+ if (stream.match(doubleClosing)) {
+ dedent(stream,state);
+ dedent(stream,state);
+ return 'keyword';
+ }
+ if (stream.match(closing)) {
+ dedent(stream,state);
+ return 'keyword';
+ }
+
+ if (stream.match(types)) {
+ return 'keyword';
+ }
+
+ if (stream.match(keywords)) {
+ return 'keyword';
+ }
+
+ if (stream.match(identifiers)) {
+ return 'variable';
+ }
+
+ // Handle non-detected items
+ stream.next();
+ return ERRORCLASS;
+ }
+
+ function tokenStringFactory(delimiter) {
+ var singleline = delimiter.length == 1;
+ var OUTCLASS = 'string';
+
+ return function(stream, state) {
+ while (!stream.eol()) {
+ stream.eatWhile(/[^'"]/);
+ if (stream.match(delimiter)) {
+ state.tokenize = tokenBase;
+ return OUTCLASS;
+ } else {
+ stream.eat(/['"]/);
+ }
+ }
+ if (singleline) {
+ if (parserConf.singleLineStringErrors) {
+ return ERRORCLASS;
+ } else {
+ state.tokenize = tokenBase;
+ }
+ }
+ return OUTCLASS;
+ };
+ }
+
+
+ function tokenLexer(stream, state) {
+ var style = state.tokenize(stream, state);
+ var current = stream.current();
+
+ // Handle '.' connected identifiers
+ if (current === '.') {
+ style = state.tokenize(stream, state);
+ if (style === 'variable') {
+ return 'variable';
+ } else {
+ return ERRORCLASS;
+ }
+ }
+
+
+ var delimiter_index = '[({'.indexOf(current);
+ if (delimiter_index !== -1) {
+ indent(stream, state );
+ }
+ if (indentInfo === 'dedent') {
+ if (dedent(stream, state)) {
+ return ERRORCLASS;
+ }
+ }
+ delimiter_index = '])}'.indexOf(current);
+ if (delimiter_index !== -1) {
+ if (dedent(stream, state)) {
+ return ERRORCLASS;
+ }
+ }
+
+ return style;
+ }
+
+ var external = {
+ electricChars:"dDpPtTfFeE ",
+ startState: function() {
+ return {
+ tokenize: tokenBase,
+ lastToken: null,
+ currentIndent: 0,
+ nextLineIndent: 0,
+ doInCurrentLine: false
+
+
+ };
+ },
+
+ token: function(stream, state) {
+ if (stream.sol()) {
+ state.currentIndent += state.nextLineIndent;
+ state.nextLineIndent = 0;
+ state.doInCurrentLine = 0;
+ }
+ var style = tokenLexer(stream, state);
+
+ state.lastToken = {style:style, content: stream.current()};
+
+
+
+ return style;
+ },
+
+ indent: function(state, textAfter) {
+ var trueText = textAfter.replace(/^\s+|\s+$/g, '') ;
+ if (trueText.match(closing) || trueText.match(doubleClosing) || trueText.match(middle)) return conf.indentUnit*(state.currentIndent-1);
+ if(state.currentIndent < 0) return 0;
+ return state.currentIndent * conf.indentUnit;
+ },
+
+ lineComment: "'"
+ };
+ return external;
+});
+
+CodeMirror.defineMIME("text/x-vb", "vb");
+
+});
diff --git a/src/common/baseplatform.ts b/src/common/baseplatform.ts
index 98728219..7eefd429 100644
--- a/src/common/baseplatform.ts
+++ b/src/common/baseplatform.ts
@@ -215,7 +215,6 @@ export abstract class BaseDebugPlatform extends BasePlatform {
frameCount : number = 0;
abstract getCPUState() : CpuState;
- abstract readAddress(addr:number) : number;
setBreakpoint(id : string, cond : DebugCondition) {
if (cond) {
@@ -416,6 +415,7 @@ export abstract class Base6502Platform extends BaseDebugPlatform {
getSP() { return this.getCPUState().SP };
getPC() { return this.getCPUState().PC };
isStable() { return !this.getCPUState()['T']; }
+ abstract readAddress(addr:number) : number;
newCPU(membus : MemoryBus) {
var cpu = new jt.M6502();
diff --git a/src/common/basic/compiler.ts b/src/common/basic/compiler.ts
new file mode 100644
index 00000000..543c07f4
--- /dev/null
+++ b/src/common/basic/compiler.ts
@@ -0,0 +1,679 @@
+import { WorkerError, CodeListingMap, SourceLocation } from "../workertypes";
+
+export interface SourceLocated {
+ $loc?: SourceLocation;
+}
+
+class CompileError extends Error {
+ constructor(msg: string) {
+ super(msg);
+ Object.setPrototypeOf(this, CompileError.prototype);
+ }
+}
+
+// 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;
+
+export enum TokenType {
+ EOL = 0,
+ Float1,
+ Float2,
+ Int,
+ Remark,
+ And,
+ Or,
+ Ident,
+ String,
+ Relational,
+ Operator,
+ CatchAll,
+ _LAST,
+}
+
+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 Value = string | number;
+
+export interface Literal {
+ value: Value;
+}
+
+export interface BinOp {
+ op: Opcode;
+ left: Expr;
+ right: Expr;
+}
+
+export interface UnOp {
+ op: 'neg';
+ expr: Expr;
+}
+
+export interface IndOp extends SourceLocated {
+ name: string;
+ args: Expr[];
+}
+
+export interface PRINT_Statement {
+ command: "PRINT";
+ args: Expr[];
+}
+
+export interface LET_Statement {
+ command: "LET";
+ lexpr: IndOp;
+ right: Expr;
+}
+
+export interface GOTO_Statement {
+ command: "GOTO";
+ label: Expr;
+}
+
+export interface GOSUB_Statement {
+ command: "GOSUB";
+ label: Expr;
+}
+
+export interface RETURN_Statement {
+ command: "RETURN";
+}
+
+export interface IF_Statement {
+ command: "IF";
+ cond: Expr;
+}
+
+export interface FOR_Statement {
+ command: "FOR";
+ lexpr: IndOp;
+ initial: Expr;
+ target: Expr;
+ step?: Expr;
+}
+
+export interface NEXT_Statement {
+ command: "NEXT";
+ lexpr?: IndOp;
+}
+
+export interface DIM_Statement {
+ command: "DIM";
+ args: IndOp[];
+}
+
+export interface INPUT_Statement {
+ command: "INPUT";
+ prompt: Expr;
+ args: IndOp[];
+}
+
+export interface READ_Statement {
+ command: "INPUT";
+ args: IndOp[];
+}
+
+export interface DEF_Statement {
+ command: "DEF";
+ lexpr: IndOp;
+ def: Expr;
+}
+
+export interface ONGOTO_Statement {
+ command: "ONGOTO";
+ expr: Expr;
+ labels: Expr[];
+}
+
+export interface DATA_Statement {
+ command: "DATA";
+ datums: Expr[];
+}
+
+export interface OPTION_Statement {
+ command: "OPTION";
+ optname: string;
+ optargs: string[];
+}
+
+export type StatementTypes = PRINT_Statement | LET_Statement | GOTO_Statement | GOSUB_Statement
+ | IF_Statement | FOR_Statement | DATA_Statement;
+
+export type Statement = StatementTypes & SourceLocated;
+
+export interface BASICLine {
+ label: string;
+ stmts: Statement[];
+}
+
+export interface BASICProgram {
+ lines: BASICLine[];
+}
+
+class Token {
+ str: string;
+ type: TokenType;
+ $loc: SourceLocation;
+}
+
+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},
+ '+': {f:'add',p:100},
+ '-': {f:'sub',p:100},
+ '*': {f:'mul',p:200},
+ '/': {f:'div',p:200},
+ '^': {f:'pow',p:300}
+};
+
+function getOpcodeForOperator(op: string): Opcode {
+ return OPERATORS[op].f as Opcode;
+}
+
+function getPrecedence(tok: Token): number {
+ switch (tok.type) {
+ case TokenType.Operator:
+ case TokenType.Relational:
+ case TokenType.Ident:
+ let op = OPERATORS[tok.str]
+ if (op) return op.p;
+ }
+ return -1;
+}
+
+// is token an end of statement marker? (":" or end of line)
+function isEOS(tok: Token) {
+ return (tok.type == TokenType.EOL) || (tok.type == TokenType.Operator && tok.str == ':');
+}
+
+function stripQuotes(s: string) {
+ // TODO: assert
+ return s.substr(1, s.length-2);
+}
+
+// TODO
+export interface BASICOptions {
+ uppercaseOnly : boolean; // convert everything to uppercase?
+ strictVarNames : boolean; // only allow A0-9 for numerics, single letter for arrays/strings
+ sharedArrayNamespace : boolean; // arrays and variables have same namespace? (conflict)
+ defaultArrayBase : number; // arrays start at this number (0 or 1)
+ defaultArraySize : number; // arrays are allocated w/ this size (starting @ 0)
+ maxDimensions : number; // max number of dimensions for arrays
+ stringConcat : boolean; // can concat strings with "+" operator?
+ typeConvert : boolean; // type convert strings <-> numbers?
+ maxArguments : number; // maximum # of arguments for user-defined functions
+ sparseArrays : boolean; // true == don't require DIM for arrays
+ tickComments : boolean; // support 'comments?
+ validKeywords : string[]; // valid keywords (or null for accept all)
+ validFunctions : string[]; // valid functions (or null for accept all)
+ validOperators : string[]; // valid operators (or null for accept all)
+ printZoneLength : number; // print zone length
+ printPrecision : number; // print precision # of digits
+ checkOverflow : boolean; // check for overflow of numerics?
+ defaultValues : boolean; // initialize unset variables to default value? (0 or "")
+}
+
+///// BASIC PARSER
+
+export class BASICParser {
+ tokens: Token[];
+ errors: WorkerError[];
+ labels: { [label: string]: BASICLine };
+ targets: { [targetlabel: string]: SourceLocation };
+ eol: Token;
+ lineno : number;
+ curlabel: string;
+ listings: CodeListingMap;
+ lasttoken: Token;
+ opts : BASICOptions = ALTAIR_BASIC40;
+
+ constructor() {
+ this.labels = {};
+ this.targets = {};
+ this.errors = [];
+ this.lineno = 0;
+ this.curlabel = null;
+ this.listings = {};
+ }
+ compileError(msg: string, loc?: SourceLocation) {
+ if (!loc) loc = this.peekToken().$loc;
+ // TODO: pass SourceLocation to errors
+ this.errors.push({line:loc.line, msg:msg});
+ 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
+ }
+ consumeToken(): Token {
+ var tok = this.lasttoken = (this.tokens.shift() || this.eol);
+ return tok;
+ }
+ expectToken(str: string) : Token {
+ var tok = this.consumeToken();
+ var tokstr = tok.str.toUpperCase();
+ if (str != tokstr) {
+ this.compileError(`I expected "${str}" here, but I saw "${tokstr}".`);
+ }
+ return tok;
+ }
+ peekToken(): Token {
+ var tok = this.tokens[0];
+ return tok ? tok : this.eol;
+ }
+ pushbackToken(tok: Token) {
+ this.tokens.unshift(tok);
+ }
+ parseOptLabel(line: BASICLine) {
+ 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}".`);
+ this.labels[tok.str] = line;
+ line.label = tok.str;
+ this.curlabel = tok.str;
+ break;
+ default:
+ // TODO
+ this.pushbackToken(tok);
+ break;
+ }
+ }
+ parseFile(file: string, path: string) : BASICProgram {
+ var pgmlines = file.split("\n").map((line) => this.parseLine(line));
+ this.checkLabels();
+ var program = { lines: pgmlines };
+ this.listings[path] = this.generateListing(file, program);
+ return program;
+ }
+ parseLine(line: string) : BASICLine {
+ try {
+ this.tokenize(line);
+ return this.parse();
+ } catch (e) {
+ if (!(e instanceof CompileError)) throw e; // ignore compile errors since errors[] list captures them
+ return {label:null, stmts:[]};
+ }
+ }
+ tokenize(line: string) : void {
+ this.lineno++;
+ this.tokens = [];
+ var m;
+ while (m = re_toks.exec(line)) {
+ for (var i = 1; i < TokenType._LAST; i++) {
+ let s = m[i];
+ if (s != null) {
+ this.tokens.push({
+ str: s,
+ type: i,
+ $loc: { line: this.lineno, start: m.index, end: m.index+s.length }
+ });
+ break;
+ }
+ }
+ }
+ this.eol = { type: TokenType.EOL, str: "", $loc: { line: this.lineno, start: line.length } };
+ }
+ parse() : BASICLine {
+ var line = {label: null, stmts: []};
+ // not empty line?
+ if (this.tokens.length) {
+ this.parseOptLabel(line);
+ line.stmts = this.parseCompoundStatement();
+ this.curlabel = null;
+ }
+ return line;
+ }
+ parseCompoundStatement(): Statement[] {
+ var list = this.parseList(this.parseStatement, ':');
+ if (!isEOS(this.peekToken())) this.compileError(`Expected end of line or ':'`, this.peekToken().$loc);
+ return list;
+ }
+ parseStatement(): Statement | null {
+ var cmdtok = this.consumeToken();
+ 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
+ if (cmd == 'REM') {
+ while (this.consumeToken().type != TokenType.EOL) { }
+ return null;
+ }
+ // look for "GO TO"
+ if (cmd == 'GO' && this.peekToken().str == 'TO') {
+ this.consumeToken();
+ cmd = 'GOTO';
+ }
+ var fn = this['stmt__' + cmd];
+ if (fn) {
+ if (this.opts.validKeywords && this.opts.validKeywords.indexOf(cmd) < 0)
+ this.dialectError(`the ${cmd} keyword`);
+ stmt = fn.bind(this)() as Statement;
+ break;
+ } else if (this.peekToken().str == '=' || this.peekToken().str == '(') {
+ // 'A = expr' or 'A(X) = expr'
+ this.pushbackToken(cmdtok);
+ stmt = this.stmt__LET();
+ break;
+ }
+ case TokenType.EOL:
+ this.compileError(`I expected a command here`);
+ return null;
+ default:
+ this.compileError(`Unknown command "${cmdtok.str}"`);
+ return null;
+ }
+ if (stmt) stmt.$loc = { line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: this.peekToken().$loc.start };
+ return stmt;
+ }
+ parseVarOrIndexedOrFunc(): IndOp {
+ return this.parseVarOrIndexed();
+ }
+ parseVarOrIndexed(): IndOp {
+ var tok = this.consumeToken();
+ switch (tok.type) {
+ case TokenType.Ident:
+ let args = null;
+ if (this.peekToken().str == '(') {
+ this.expectToken('(');
+ args = this.parseExprList();
+ this.expectToken(')');
+ }
+ return { name: tok.str, args: args, $loc: tok.$loc };
+ default:
+ this.compileError("Expected variable or array index");
+ break;
+ }
+ }
+ parseList(parseFunc:()=>T, delim:string): T[] {
+ var sep;
+ var list = [];
+ do {
+ var el = parseFunc.bind(this)()
+ if (el != null) list.push(el);
+ sep = this.consumeToken();
+ } while (sep.str == delim);
+ this.pushbackToken(sep);
+ return list;
+ }
+ parseVarOrIndexedList(): IndOp[] {
+ return this.parseList(this.parseVarOrIndexed, ',');
+ }
+ parseExprList(): Expr[] {
+ return this.parseList(this.parseExpr, ',');
+ }
+ parseLabelList(): Expr[] {
+ return this.parseList(this.parseLabel, ',');
+ }
+ parseLabel() : Expr {
+ var tok = this.consumeToken();
+ switch (tok.type) {
+ case TokenType.Int:
+ var label = parseInt(tok.str).toString();
+ this.targets[label] = tok.$loc;
+ return {value:label};
+ default:
+ this.compileError(`I expected a line number here`);
+ return;
+ }
+ }
+ parsePrimary(): Expr {
+ let tok = this.consumeToken();
+ switch (tok.type) {
+ case TokenType.Int:
+ case TokenType.Float1:
+ case TokenType.Float2:
+ return { value: parseFloat(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();
+ case TokenType.Operator:
+ if (tok.str == '(') {
+ let expr = this.parseExpr();
+ this.expectToken(')');
+ return expr;
+ } else if (tok.str == '-') {
+ let expr = this.parsePrimary(); // TODO: -2^2=-4 and -2-2=-4
+ return { op: 'neg', expr: expr };
+ } else if (tok.str == '+') {
+ return this.parsePrimary(); // TODO?
+ }
+ default:
+ this.compileError(`Unexpected "${tok.str}"`);
+ }
+ }
+ parseExpr1(left: Expr, minPred: number): Expr {
+ let look = this.peekToken();
+ while (getPrecedence(look) >= minPred) {
+ let op = this.consumeToken();
+ 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 };
+ }
+ return left;
+ }
+ parseExpr(): Expr {
+ return this.parseExpr1(this.parsePrimary(), 0);
+ }
+
+ //// STATEMENTS
+
+ stmt__LET(): LET_Statement {
+ var lexpr = this.parseVarOrIndexed();
+ this.expectToken("=");
+ var right = this.parseExpr();
+ return { command: "LET", lexpr: lexpr, right: right };
+ }
+ stmt__PRINT(): PRINT_Statement {
+ var sep, lastsep;
+ var list = [];
+ do {
+ sep = this.peekToken();
+ if (isEOS(sep)) {
+ break;
+ } else if (sep.str == ';') {
+ this.consumeToken();
+ lastsep = sep;
+ } else if (sep.str == ',') {
+ this.consumeToken();
+ list.push({value:'\t'});
+ lastsep = sep;
+ } else {
+ list.push(this.parseExpr());
+ lastsep = null;
+ }
+ } while (true);
+ if (!(lastsep && (lastsep.str == ';' || sep.str != ','))) {
+ list.push({value:'\n'});
+ }
+ return { command: "PRINT", args: list };
+ }
+ stmt__GOTO(): GOTO_Statement {
+ return { command: "GOTO", label: this.parseLabel() };
+ }
+ stmt__GOSUB(): GOSUB_Statement {
+ return { command: "GOSUB", label: this.parseLabel() };
+ }
+ stmt__IF(): IF_Statement {
+ var cond = this.parseExpr();
+ var iftrue: Statement[];
+ this.expectToken('THEN');
+ var lineno = this.peekToken();
+ // assume GOTO if number given after THEN
+ if (lineno.type == TokenType.Int) {
+ this.pushbackToken({type:TokenType.Ident, str:'GOTO', $loc:lineno.$loc});
+ }
+ // add fake ":"
+ this.pushbackToken({type:TokenType.Operator, str:':', $loc:lineno.$loc});
+ return { command: "IF", cond: cond };
+ }
+ stmt__FOR() : FOR_Statement {
+ var lexpr = this.parseVarOrIndexed(); // TODO: parseNumVar()
+ this.expectToken('=');
+ var init = this.parseExpr();
+ this.expectToken('TO');
+ var targ = this.parseExpr();
+ if (this.peekToken().str == 'STEP') {
+ this.consumeToken();
+ var step = this.parseExpr();
+ }
+ return { command:'FOR', lexpr:lexpr, initial:init, target:targ, step:step };
+ }
+ stmt__NEXT() : NEXT_Statement {
+ var lexpr = null;
+ if (!isEOS(this.peekToken())) {
+ lexpr = this.parseExpr();
+ }
+ return { command:'NEXT', lexpr:lexpr };
+ }
+ stmt__DIM() : DIM_Statement {
+ return { command:'DIM', args:this.parseVarOrIndexedList() };
+ }
+ stmt__INPUT() : INPUT_Statement {
+ var prompt = this.consumeToken();
+ var promptstr;
+ if (prompt.type == TokenType.String) {
+ this.expectToken(';');
+ promptstr = stripQuotes(prompt.str);
+ } else {
+ this.pushbackToken(prompt);
+ promptstr = "";
+ }
+ return { command:'INPUT', prompt:{ value: promptstr, $loc: prompt.$loc }, args:this.parseVarOrIndexedList() };
+ }
+ stmt__DATA() : DATA_Statement {
+ return { command:'DATA', datums:this.parseExprList() };
+ }
+ stmt__READ() {
+ return { command:'READ', args:this.parseVarOrIndexedList() };
+ }
+ stmt__RESTORE() {
+ return { command:'RESTORE' };
+ }
+ stmt__RETURN() {
+ return { command:'RETURN' };
+ }
+ stmt__STOP() {
+ return { command:'STOP' };
+ }
+ stmt__END() {
+ return { command:'END' };
+ }
+ stmt__ON() : ONGOTO_Statement {
+ var expr = this.parseExpr();
+ this.expectToken('GOTO');
+ var labels = this.parseLabelList();
+ return { command:'ONGOTO', expr:expr, labels:labels };
+ }
+ 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".`)
+ this.expectToken("=");
+ 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.`)
+ var list : string[] = [];
+ var tok;
+ do {
+ tok = this.consumeToken();
+ if (isEOS(tok)) break;
+ list.push(tok.str.toUpperCase());
+ } while (true);
+ return { command:'OPTION', optname:tokname.str.toUpperCase(), optargs:list };
+ }
+
+ // for workermain
+ generateListing(file: string, program: BASICProgram) {
+ var srclines = [];
+ var pc = 0;
+ program.lines.forEach((line, idx) => {
+ srclines.push({offset: pc, line: idx+1});
+ pc += line.stmts.length;
+ });
+ return { lines: srclines };
+ }
+ getListings() : CodeListingMap {
+ return this.listings;
+ }
+
+ // LINT STUFF
+ checkLabels() {
+ for (let targ in this.targets) {
+ if (this.labels[targ] == null) {
+ this.compileError(`I couldn't find line number ${targ}`, this.targets[targ]);
+ }
+ }
+ }
+}
+
+///// BASIC DIALECTS
+
+// TODO
+
+export const ECMA55_MINIMAL : BASICOptions = {
+ uppercaseOnly : true,
+ strictVarNames : true,
+ sharedArrayNamespace : true,
+ defaultArrayBase : 0,
+ defaultArraySize : 11,
+ defaultValues : false,
+ stringConcat : false,
+ typeConvert : false,
+ maxDimensions : 2,
+ maxArguments : Infinity,
+ 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'],
+ validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'],
+ printZoneLength : 15,
+ printPrecision : 6,
+ checkOverflow : true,
+}
+
+export const ALTAIR_BASIC40 : BASICOptions = {
+ uppercaseOnly : true,
+ strictVarNames : true,
+ sharedArrayNamespace : true,
+ defaultArrayBase : 0,
+ defaultArraySize : 11,
+ defaultValues : false,
+ stringConcat : false,
+ typeConvert : false,
+ maxDimensions : 2,
+ maxArguments : Infinity,
+ sparseArrays : false,
+ tickComments : false,
+ validKeywords : null, // all
+ validFunctions : null, // all
+ validOperators : null, // all ['\\','MOD','NOT','AND','OR','XOR','EQV','IMP'],
+ printZoneLength : 15,
+ printPrecision : 6,
+ checkOverflow : true,
+}
diff --git a/src/common/basic/main.ts b/src/common/basic/main.ts
new file mode 100644
index 00000000..4b6cd087
--- /dev/null
+++ b/src/common/basic/main.ts
@@ -0,0 +1,24 @@
+
+import { BASICParser } from "./compiler";
+
+var parser = new BASICParser();
+var readline = require('readline');
+var rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ terminal: false
+});
+rl.on('line', function (line) {
+ parser.tokenize(line);
+ console.log(parser.tokens);
+ try {
+ var ast = parser.parse();
+ console.log(JSON.stringify(ast, null, 4));
+ } catch (e) {
+ console.log(e);
+ }
+ if (parser.errors.length) {
+ console.log(parser.errors);
+ parser.errors = [];
+ }
+})
diff --git a/src/common/basic/run.ts b/src/common/basic/run.ts
new file mode 100644
index 00000000..6515c6a2
--- /dev/null
+++ b/src/common/basic/run.ts
@@ -0,0 +1,54 @@
+
+import { BASICParser, ECMA55_MINIMAL } from "./compiler";
+import { BASICRuntime } from "./runtime";
+
+var readline = require('readline');
+var rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ //terminal: false
+});
+
+var parser = new BASICParser();
+parser.opts = ECMA55_MINIMAL;
+var fs = require('fs');
+var filename = process.argv[2];
+var data = fs.readFileSync(filename, 'utf-8');
+try {
+ var pgm = parser.parseFile(data, filename);
+} catch (e) {
+ console.log("@@@ " + e.message);
+ throw e;
+}
+
+var runtime = new BASICRuntime();
+runtime.trace = process.argv[3] == '-v';
+runtime.load(pgm);
+runtime.reset();
+runtime.print = (s:string) => {
+ fs.writeSync(1, s+"");
+}
+runtime.input = async (prompt:string) => {
+ return new Promise( (resolve, reject) => {
+ fs.writeSync(1, "\n");
+ rl.question(prompt, (answer) => {
+ var vals = answer.toUpperCase().split(',');
+ resolve(vals);
+ });
+ });
+}
+runtime.resume = function() {
+ process.nextTick(() => {
+ try {
+ if (runtime.step()) {
+ if (runtime.running) runtime.resume();
+ } else if (runtime.exited) {
+ rl.close();
+ }
+ } catch (e) {
+ console.log("### " + e.message);
+ throw e;
+ }
+ });
+}
+runtime.resume();
diff --git a/src/common/basic/runtime.ts b/src/common/basic/runtime.ts
new file mode 100644
index 00000000..a6337629
--- /dev/null
+++ b/src/common/basic/runtime.ts
@@ -0,0 +1,646 @@
+
+import * as basic from "./compiler";
+import { EmuHalt } from "../emu";
+
+function isLiteral(arg: basic.Expr): arg is basic.Literal {
+ return (arg as any).value != null;
+}
+function isLookup(arg: basic.Expr): arg is basic.IndOp {
+ return (arg as any).name != null;
+}
+function isBinOp(arg: basic.Expr): arg is basic.BinOp {
+ return (arg as any).op != null && (arg as any).left != null && (arg as any).right != null;
+}
+function isUnOp(arg: basic.Expr): arg is basic.UnOp {
+ return (arg as any).op != null && (arg as any).expr != null;
+}
+
+class ExprOptions {
+ check: boolean;
+ isconst?: boolean;
+ locals?: string[];
+}
+
+interface CompiledStatement {
+ $run?: () => void;
+}
+
+export class BASICRuntime {
+
+ program : basic.BASICProgram;
+ allstmts : basic.Statement[];
+ line2pc : number[];
+ pc2line : Map;
+ label2lineidx : {[label : string] : number};
+ label2pc : {[label : string] : number};
+ datums : basic.Literal[];
+
+ curpc : number;
+ dataptr : number;
+ vars : {};
+ arrays : {};
+ forLoops : {};
+ returnStack : number[];
+ column : number;
+ abase : number; // array base
+
+ running : boolean = false;
+ exited : boolean = false;
+ trace : boolean = false;
+
+ load(program: basic.BASICProgram) {
+ this.program = program;
+ this.label2lineidx = {};
+ this.label2pc = {};
+ this.allstmts = [];
+ this.line2pc = [];
+ this.pc2line = new Map();
+ this.datums = [];
+ // TODO: lines start @ 1?
+ program.lines.forEach((line, idx) => {
+ // make lookup tables
+ if (line.label != null) this.label2lineidx[line.label] = idx;
+ if (line.label != null) this.label2pc[line.label] = this.allstmts.length;
+ this.line2pc.push(this.allstmts.length);
+ this.pc2line.set(this.allstmts.length, idx);
+ // combine all statements into single list
+ line.stmts.forEach((stmt) => this.allstmts.push(stmt));
+ // parse DATA literals
+ line.stmts.filter((stmt) => stmt.command == 'DATA').forEach((datastmt) => {
+ (datastmt as basic.DATA_Statement).datums.forEach(d => {
+ var functext = this.expr2js(d, {check:true, isconst:true});
+ // TODO: catch exceptions
+ // TODO: any value doing this ahead of time?
+ var value = new Function(`return ${functext};`).bind(this)();
+ this.datums.push({value:value});
+ });
+ });
+ // TODO: compile statements?
+ //line.stmts.forEach((stmt) => this.compileStatement(stmt));
+ });
+ }
+
+ reset() {
+ this.curpc = 0;
+ this.dataptr = 0;
+ this.vars = {};
+ this.arrays = {};
+ this.forLoops = {};
+ this.returnStack = [];
+ this.column = 0;
+ this.abase = 1;
+ this.running = true;
+ this.exited = false;
+ }
+
+ runtimeError(msg : string) {
+ this.curpc--; // we did curpc++ before executing statement
+ // TODO: pass source location to error
+ throw new EmuHalt(`${msg} (line ${this.getLabelForPC(this.curpc)})`);
+ }
+
+ // TODO: sometimes on next line
+ getLineForPC(pc:number) {
+ var line;
+ do {
+ line = this.pc2line.get(pc);
+ if (line != null) break;
+ } while (--pc >= 0);
+ return line;
+ }
+
+ getLabelForPC(pc:number) {
+ var lineno = this.getLineForPC(pc);
+ var pgmline = this.program.lines[lineno];
+ return pgmline ? pgmline.label : '?';
+ }
+
+ getStatement() {
+ return this.allstmts[this.curpc];
+ }
+
+ step() : boolean {
+ if (!this.running) return false;
+ var stmt = this.getStatement();
+ // end of program?
+ if (!stmt) {
+ this.running = false;
+ this.exited = true;
+ return false;
+ }
+ if (this.trace) console.log(this.curpc, stmt, this.vars, Object.keys(this.arrays));
+ // skip to next statment
+ this.curpc++;
+ // compile statement to JS?
+ this.compileStatement(stmt);
+ this.executeStatement(stmt);
+ return this.running;
+ }
+
+ compileStatement(stmt: basic.Statement & CompiledStatement) {
+ if (stmt.$run == null) {
+ var stmtfn = this['do__' + stmt.command];
+ if (stmtfn == null) this.runtimeError(`I don't know how to "${stmt.command}".`);
+ var functext = stmtfn.bind(this)(stmt);
+ if (this.trace) console.log(functext);
+ stmt.$run = new Function(functext).bind(this);
+ }
+ }
+ executeStatement(stmt: basic.Statement & CompiledStatement) {
+ // run compiled statement
+ stmt.$run();
+ }
+
+ skipToEOL() {
+ do {
+ this.curpc++;
+ } while (this.curpc < this.allstmts.length && !this.pc2line.get(this.curpc));
+ }
+
+ skipToEOF() {
+ this.curpc = this.allstmts.length;
+ }
+
+ gotoLabel(label) {
+ var pc = this.label2pc[label];
+ if (pc >= 0) {
+ this.curpc = pc;
+ } else {
+ this.runtimeError(`I tried to go to the label "${label}" but couldn't find it.`);
+ }
+ }
+
+ gosubLabel(label) {
+ this.returnStack.push(this.curpc + 1);
+ this.gotoLabel(label);
+ }
+
+ returnFromGosub() {
+ if (this.returnStack.length == 0)
+ this.runtimeError("I tried to RETURN, but there wasn't a corresponding GOSUB."); // RETURN BEFORE GOSUB
+ var pc = this.returnStack.pop();
+ this.curpc = pc;
+ }
+
+ valueToString(obj) : string {
+ var str;
+ if (typeof obj === 'number') {
+ var numstr = obj.toString().toUpperCase();
+ var prec = 11;
+ while (numstr.length > 11) {
+ numstr = obj.toPrecision(prec--);
+ }
+ if (numstr.startsWith('0.'))
+ numstr = numstr.substr(1);
+ else if (numstr.startsWith('-0.'))
+ numstr = '-'+numstr.substr(2);
+ if (numstr.startsWith('-')) {
+ str = `${numstr} `;
+ } else {
+ str = ` ${numstr} `;
+ }
+ } else if (obj == '\n') {
+ this.column = 0;
+ str = obj;
+ } else if (obj == '\t') {
+ var curgroup = Math.floor(this.column / 15);
+ var nextcol = (curgroup + 1) * 15;
+ str = this.TAB(nextcol);
+ } else {
+ str = `${obj}`;
+ }
+ return str;
+ }
+
+ printExpr(obj) {
+ var str = this.valueToString(obj);
+ this.column += str.length;
+ this.print(str);
+ }
+
+ // override this
+ print(str: string) {
+ console.log(str);
+ }
+
+ // override this
+ async input(prompt: string, nargs: number) : Promise {
+ return [];
+ }
+
+ // override this
+ resume() { }
+
+ expr2js(expr: basic.Expr, opts: ExprOptions) : string {
+ if (isLiteral(expr)) {
+ return JSON.stringify(expr.value);
+ } else if (isLookup(expr)) {
+ if (opts.locals && opts.locals.indexOf(expr.name) >= 0) {
+ return expr.name; // local arg in DEF
+ } else {
+ if (opts.isconst) this.runtimeError(`I expected a constant value here`);
+ var s = '';
+ if (expr.args && this[expr.name]) { // is it a function?
+ s += `this.${expr.name}(`;
+ s += expr.args.map((arg) => this.expr2js(arg, opts)).join(', ');
+ s += ')';
+ } else if (expr.args) { // is it a subscript?
+ s += `this.getArray(${JSON.stringify(expr.name)}, ${expr.args.length})`;
+ s += expr.args.map((arg) => '[this.ROUND('+this.expr2js(arg, opts)+')]').join('');
+ } else {
+ // just a variable
+ s = `this.vars.${expr.name}`;
+ }
+ if (opts.check)
+ return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`; // TODO: better error
+ else
+ return s;
+ }
+ } else if (isBinOp(expr)) {
+ var left = this.expr2js(expr.left, opts);
+ var right = this.expr2js(expr.right, opts);
+ return `this.${expr.op}(${left}, ${right})`;
+ } else if (isUnOp(expr) && expr.op == 'neg') {
+ var e = this.expr2js(expr.expr, opts);
+ return `-(${e})`; // TODO: other ops?
+ }
+ }
+
+ startForLoop(name, init, targ, step) {
+ // TODO: check for loop params
+ var pc = this.curpc;
+ if (!step) step = 1;
+ this.vars[name] = init;
+ if (this.trace) console.log(`FOR ${name} = ${this.vars[name]} TO ${targ} STEP ${step}`);
+ this.forLoops[name] = {
+ next: () => {
+ var done = step >= 0 ? this.vars[name] >= targ : this.vars[name] <= targ;
+ if (done) {
+ delete this.forLoops[name];
+ } else {
+ this.vars[name] += step;
+ this.curpc = pc;
+ }
+ if (this.trace) console.log(`NEXT ${name}: ${this.vars[name]} TO ${targ} STEP ${step} DONE=${done}`);
+ }
+ };
+ }
+
+ nextForLoop(name) {
+ // TODO: check for for loop
+ var fl = this.forLoops[name];
+ if (!fl) this.runtimeError(`I couldn't find a matching FOR for this NEXT.`)
+ this.forLoops[name].next();
+ }
+
+ // converts a variable to string/number based on var name
+ convert(name: string, right: number|string) : number|string {
+ // TODO: error check?
+ if (name.endsWith("$"))
+ return right+"";
+ else if (typeof right === 'string')
+ return parseFloat(right);
+ else if (typeof right === 'number')
+ return right;
+ else
+ return this.checkValue(right, name);
+ }
+
+ // dimension array
+ dimArray(name: string, ...dims:number[]) {
+ var isstring = name.endsWith('$');
+ // TODO: option for undefined float array elements?
+ var arrcons = isstring ? Array : Float64Array;
+ var ab = this.abase;
+ if (dims.length == 1) {
+ this.arrays[name] = new arrcons(dims[0]+ab);
+ } else if (dims.length == 2) {
+ this.arrays[name] = new Array(dims[0]+ab);
+ for (var i=ab; i labels.length)
+ this.runtimeError(`I needed a number between 1 and ${labels.length}, but I got ${value}.`);
+ this.gotoLabel(labels[value-1]);
+ }
+
+ nextDatum() : string|number {
+ if (this.dataptr >= this.datums.length)
+ this.runtimeError("I tried to READ, but ran out of data.");
+ return this.datums[this.dataptr++].value;
+ }
+
+ //// STATEMENTS
+
+ do__PRINT(stmt : basic.PRINT_Statement) {
+ var s = '';
+ for (var arg of stmt.args) {
+ var expr = this.expr2js(arg, {check:true});
+ s += `this.printExpr(${expr});`;
+ }
+ return s;
+ }
+
+ do__INPUT(stmt : basic.INPUT_Statement) {
+ var prompt = this.expr2js(stmt.prompt, {check:true});
+ var setvals = '';
+ stmt.args.forEach((arg, index) => {
+ var lexpr = this.expr2js(arg, {check:false});
+ setvals += `${lexpr} = this.convert(${JSON.stringify(arg.name)}, vals[${index}]);`
+ });
+ return `this.running=false; this.input(${prompt}, ${stmt.args.length}).then((vals) => {${setvals}; this.running=true; this.resume();})`;
+ }
+
+ do__LET(stmt : basic.LET_Statement) {
+ var lexpr = this.expr2js(stmt.lexpr, {check:false});
+ var right = this.expr2js(stmt.right, {check:true});
+ return `${lexpr} = this.convert(${JSON.stringify(stmt.lexpr.name)}, ${right});`;
+ }
+
+ do__FOR(stmt : basic.FOR_Statement) {
+ var name = JSON.stringify(stmt.lexpr.name); // TODO: args?
+ var init = this.expr2js(stmt.initial, {check:true});
+ var targ = this.expr2js(stmt.target, {check:true});
+ var step = stmt.step ? this.expr2js(stmt.step, {check:true}) : 'null';
+ return `this.startForLoop(${name}, ${init}, ${targ}, ${step})`;
+ }
+
+ do__NEXT(stmt : basic.NEXT_Statement) {
+ var name = JSON.stringify(stmt.lexpr.name); // TODO: args? lexpr == null?
+ return `this.nextForLoop(${name})`;
+ }
+
+ do__IF(stmt : basic.IF_Statement) {
+ var cond = this.expr2js(stmt.cond, {check:true});
+ return `if (!(${cond})) { this.skipToEOL(); }`
+ }
+
+ do__DEF(stmt : basic.DEF_Statement) {
+ var lexpr = `this.${stmt.lexpr.name}`;
+ var args = [];
+ for (var arg of stmt.lexpr.args) {
+ if (isLookup(arg)) {
+ args.push(arg.name);
+ } else {
+ this.runtimeError("I found a DEF statement with arguments other than variable names.");
+ }
+ }
+ var functext = this.expr2js(stmt.def, {check:true, locals:args});
+ // TODO: use stmt.args to add function params
+ return `${lexpr} = function(${args.join(',')}) { return ${functext}; }`;
+ }
+
+ _DIM(dim : basic.IndOp) {
+ var argsstr = '';
+ for (var arg of dim.args) {
+ // TODO: check for float
+ argsstr += ', ' + this.expr2js(arg, {check:true});
+ }
+ return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`;
+ }
+
+ do__DIM(stmt : basic.DIM_Statement) {
+ var s = '';
+ stmt.args.forEach((dim) => s += this._DIM(dim));
+ return s;
+ }
+
+ do__GOTO(stmt : basic.GOTO_Statement) {
+ var label = this.expr2js(stmt.label, {check:true});
+ return `this.gotoLabel(${label})`;
+ }
+
+ do__GOSUB(stmt : basic.GOSUB_Statement) {
+ var label = this.expr2js(stmt.label, {check:true});
+ return `this.gosubLabel(${label})`;
+ }
+
+ do__RETURN(stmt : basic.RETURN_Statement) {
+ return `this.returnFromGosub()`;
+ }
+
+ do__ONGOTO(stmt : basic.ONGOTO_Statement) {
+ var expr = this.expr2js(stmt.expr, {check:true});
+ var labels = stmt.labels.map((arg) => this.expr2js(arg, {check:true})).join(', ');
+ return `this.onGotoLabel(${expr}, ${labels})`;
+ }
+
+ do__DATA() {
+ // data is preprocessed
+ }
+
+ do__READ(stmt : basic.READ_Statement) {
+ var s = '';
+ stmt.args.forEach((arg) => {
+ s += `${this.expr2js(arg, {check:false})} = this.convert(${JSON.stringify(arg.name)}, this.nextDatum());`;
+ });
+ return s;
+ }
+
+ do__RESTORE() {
+ this.dataptr = 0; // TODO: line number?
+ }
+
+ do__END() {
+ return `this.skipToEOF()`;
+ }
+
+ do__STOP() {
+ return `this.skipToEOF()`;
+ }
+
+ do__OPTION(stmt: basic.OPTION_Statement) {
+ switch (stmt.optname) {
+ case 'BASE':
+ let base = parseInt(stmt.optargs[0]);
+ if (base == 0 || base == 1) this.abase = base;
+ else this.runtimeError("OPTION BASE can only be 0 or 1.");
+ break;
+ default:
+ this.runtimeError(`OPTION ${stmt.optname} is not supported by this compiler.`);
+ break;
+ }
+ }
+
+ // TODO: "SUBSCRIPT ERROR"
+ // TODO: gosubs nested too deeply
+ // TODO: memory quota
+
+ // FUNCTIONS
+
+ checkValue(obj:number|string, exprname:string) {
+ if (typeof obj !== 'number' && typeof obj !== 'string') {
+ if (exprname != null && obj == null) {
+ this.runtimeError(`I didn't find a value for ${exprname}`);
+ } else if (exprname != null) {
+ this.runtimeError(`I got an invalid value for ${exprname}: ${obj}`);
+ } else {
+ this.runtimeError(`I got an invalid value: ${obj}`);
+ }
+ }
+ return obj;
+ }
+
+ checkNum(n:number) : number {
+ if (n === Infinity) this.runtimeError(`I computed a number too big to store.`);
+ if (isNaN(n)) this.runtimeError(`I computed an invalid number.`);
+ return n;
+ }
+
+ checkString(s:string) : string {
+ if (typeof s !== 'string') this.runtimeError(`I expected a string here.`);
+ return s;
+ }
+
+ add(a, b) : number|string {
+ // TODO: if string-concat
+ if (typeof a === 'number' && typeof b === 'number')
+ return this.checkNum(a + b);
+ else
+ return a + b;
+ }
+ sub(a:number, b:number) : number {
+ return this.checkNum(a - b);
+ }
+ mul(a:number, b:number) : number {
+ return this.checkNum(a * b);
+ }
+ div(a:number, b:number) : number {
+ if (b == 0) this.runtimeError(`I can't divide by zero.`);
+ return this.checkNum(a / b);
+ }
+ pow(a:number, b:number) : number {
+ if (a == 0 && b < 0) this.runtimeError(`I can't raise zero to a negative power.`);
+ return this.checkNum(Math.pow(a, b));
+ }
+ land(a:number, b:number) : number {
+ return a && b;
+ }
+ lor(a:number, b:number) : number {
+ return a || b;
+ }
+ eq(a:number, b:number) : boolean {
+ return a == b;
+ }
+ ne(a:number, b:number) : boolean {
+ return a != b;
+ }
+ lt(a:number, b:number) : boolean {
+ return a < b;
+ }
+ gt(a:number, b:number) : boolean {
+ return a > b;
+ }
+ le(a:number, b:number) : boolean {
+ return a <= b;
+ }
+ ge(a:number, b:number) : boolean {
+ return a >= b;
+ }
+
+ // FUNCTIONS (uppercase)
+
+ ABS(arg : number) : number {
+ return this.checkNum(Math.abs(arg));
+ }
+ ASC(arg : string) : number {
+ return arg.charCodeAt(0);
+ }
+ ATN(arg : number) : number {
+ return this.checkNum(Math.atan(arg));
+ }
+ CHR$(arg : number) : string {
+ return String.fromCharCode(this.checkNum(arg));
+ }
+ COS(arg : number) : number {
+ return this.checkNum(Math.cos(arg));
+ }
+ EXP(arg : number) : number {
+ return this.checkNum(Math.exp(arg));
+ }
+ FIX(arg : number) : number {
+ return this.checkNum(arg - Math.floor(arg));
+ }
+ HEX$(arg : number) : string {
+ return arg.toString(16);
+ }
+ INSTR(a, b, c) : number {
+ if (c != null) {
+ return this.checkString(c).indexOf(this.checkString(b), a) + 1;
+ } else {
+ return this.checkString(b).indexOf(this.checkString(a)) + 1;
+ }
+ }
+ INT(arg : number) : number {
+ return this.checkNum(Math.floor(arg));
+ }
+ LEFT$(arg : string, count : number) : string {
+ return arg.substr(0, count);
+ }
+ LEN(arg : string) : number {
+ return arg.length;
+ }
+ LOG(arg : number) : number {
+ if (arg == 0) this.runtimeError(`I can't take the logarithm of zero (${arg}).`)
+ if (arg < 0) this.runtimeError(`I can't take the logarithm of a negative number (${arg}).`)
+ return this.checkNum(Math.log(arg));
+ }
+ MID$(arg : string, start : number, count : number) : string {
+ if (start < 1) this.runtimeError(`The second parameter to MID$ must be between 1 and the length of the string in the first parameter.`)
+ return arg.substr(start-1, count);
+ }
+ RIGHT$(arg : string, count : number) : string {
+ return arg.substr(arg.length - count, count);
+ }
+ RND(arg : number) : number {
+ return Math.random(); // argument ignored
+ }
+ ROUND(arg : number) : number {
+ return this.checkNum(Math.round(arg)); // TODO?
+ }
+ SGN(arg : number) : number {
+ return (arg < 0) ? -1 : (arg > 0) ? 1 : 0;
+ }
+ SIN(arg : number) : number {
+ return this.checkNum(Math.sin(arg));
+ }
+ SPACE$(arg : number) : string {
+ return ' '.repeat(this.checkNum(arg));
+ }
+ SQR(arg : number) : number {
+ if (arg < 0) this.runtimeError(`I can't take the square root of a negative number (${arg}).`)
+ return this.checkNum(Math.sqrt(arg));
+ }
+ STR$(arg) : string {
+ return this.valueToString(arg);
+ }
+ TAB(arg : number) : string {
+ if (arg < 0) this.runtimeError(`I got a negative value for the TAB() function.`);
+ var spaces = this.ROUND(arg) - this.column;
+ return (spaces > 0) ? ' '.repeat(spaces) : '';
+ }
+ TAN(arg : number) : number {
+ return this.checkNum(Math.tan(arg));
+ }
+ VAL(arg) : number {
+ return parseFloat(arg+"");
+ }
+}
diff --git a/src/common/workertypes.ts b/src/common/workertypes.ts
index 5848e0c4..88d201e2 100644
--- a/src/common/workertypes.ts
+++ b/src/common/workertypes.ts
@@ -1,6 +1,12 @@
export type FileData = string | Uint8Array;
+export interface SourceLocation {
+ line: number;
+ start?: number;
+ end?: number;
+}
+
export interface SourceLine {
offset:number;
line:number;
diff --git a/src/ide/ui.ts b/src/ide/ui.ts
index 8d38a955..c1118b2a 100644
--- a/src/ide/ui.ts
+++ b/src/ide/ui.ts
@@ -59,6 +59,7 @@ var lastViewClicked = null;
var lastBreakExpr = "c.PC == 0x6000";
// TODO: codemirror multiplex support?
+// TODO: move to views.ts?
var TOOL_TO_SOURCE_STYLE = {
'dasm': '6502',
'acme': '6502',
@@ -80,6 +81,7 @@ var TOOL_TO_SOURCE_STYLE = {
'smlrc': 'text/x-csrc',
'inform6': 'inform6',
'fastbasic': 'fastbasic',
+ 'basic': 'basic',
}
function gaEvent(category:string, action:string, label?:string, value?:string) {
diff --git a/src/ide/views.ts b/src/ide/views.ts
index 1a7bc191..924e3f6d 100644
--- a/src/ide/views.ts
+++ b/src/ide/views.ts
@@ -70,9 +70,10 @@ const MODEDEFS = {
gas: { isAsm: true },
inform6: { theme: 'cobalt' },
markdown: { lineWrap: true },
+ fastbasic: { noGutters: true },
+ basic: { noLineNumbers: true, noGutters: true }, // TODO: not used?
}
-
export class SourceEditor implements ProjectView {
constructor(path:string, mode:string) {
this.path = path;
@@ -106,17 +107,20 @@ export class SourceEditor implements ProjectView {
newEditor(parent:HTMLElement, isAsmOverride?:boolean) {
var modedef = MODEDEFS[this.mode] || MODEDEFS.default;
var isAsm = isAsmOverride || modedef.isAsm;
- var lineWrap = modedef.lineWrap;
+ var lineWrap = !!modedef.lineWrap;
var theme = modedef.theme || MODEDEFS.default.theme;
+ var lineNums = !modedef.noLineNumbers;
+ var gutters = ["CodeMirror-linenumbers", "gutter-offset", "gutter-info"];
+ if (isAsm) gutters = ["CodeMirror-linenumbers", "gutter-offset", "gutter-bytes", "gutter-clock", "gutter-info"];
+ if (modedef.noGutters) gutters = ["gutter-info"];
this.editor = CodeMirror(parent, {
theme: theme,
- lineNumbers: true,
+ lineNumbers: lineNums,
matchBrackets: true,
tabSize: 8,
indentAuto: true,
lineWrapping: lineWrap,
- gutters: isAsm ? ["CodeMirror-linenumbers", "gutter-offset", "gutter-bytes", "gutter-clock", "gutter-info"]
- : ["CodeMirror-linenumbers", "gutter-offset", "gutter-info"],
+ gutters: gutters
});
}
@@ -324,7 +328,7 @@ export class SourceEditor implements ProjectView {
this.editor.setGutterMarker(line, "gutter-info", div);
}
- this.clearCurrentLine();
+ this.clearCurrentLine(moveCursor);
if (line>0) {
addCurrentMarker(line-1);
if (moveCursor)
@@ -333,18 +337,18 @@ export class SourceEditor implements ProjectView {
}
}
- clearCurrentLine() {
+ clearCurrentLine(moveCursor:boolean) {
if (this.currentDebugLine) {
this.editor.clearGutter("gutter-info");
- this.editor.setSelection(this.editor.getCursor());
+ if (moveCursor) this.editor.setSelection(this.editor.getCursor());
this.currentDebugLine = 0;
}
}
getActiveLine() {
var state = lastDebugState;
- if (state && state.c && this.sourcefile) {
- var EPC = state.c.EPC || state.c.PC;
+ if (this.sourcefile && state) {
+ var EPC = (state && state.c && (state.c.EPC || state.c.PC)); // || (platform.getPC && platform.getPC());
var res = this.sourcefile.findLineForOffset(EPC, 15);
return res && res.line;
} else
@@ -352,7 +356,7 @@ export class SourceEditor implements ProjectView {
}
refreshDebugState(moveCursor:boolean) {
- this.clearCurrentLine();
+ this.clearCurrentLine(moveCursor);
var line = this.getActiveLine();
if (line >= 0) {
this.setCurrentLine(line, moveCursor);
diff --git a/src/platform/basic.ts b/src/platform/basic.ts
new file mode 100644
index 00000000..8b024154
--- /dev/null
+++ b/src/platform/basic.ts
@@ -0,0 +1,414 @@
+
+import { Platform, BreakpointCallback } from "../common/baseplatform";
+import { PLATFORMS, AnimationTimer } from "../common/emu";
+import { loadScript } from "../ide/ui";
+import { BASICRuntime } from "../common/basic/runtime";
+import { BASICProgram } from "../common/basic/compiler";
+
+const BASIC_PRESETS = [
+ { id: 'hello.bas', name: 'Hello World' }
+];
+
+class TeleType {
+ page: HTMLElement;
+ fixed: boolean;
+ scrolldiv: HTMLElement;
+
+ curline: HTMLElement;
+ curstyle: number;
+ reverse: boolean;
+ col: number;
+ row: number;
+ lines: HTMLElement[];
+ ncharsout : number;
+
+ constructor(page: HTMLElement, fixed: boolean) {
+ this.page = page;
+ this.fixed = fixed;
+ this.clear();
+ }
+ clear() {
+ this.curline = null;
+ this.curstyle = 0;
+ this.reverse = false;
+ this.col = 0;
+ this.row = -1;
+ this.lines = [];
+ this.ncharsout = 0;
+ $(this.page).empty();
+ }
+ ensureline() {
+ if (this.curline == null) {
+ this.curline = this.lines[++this.row];
+ if (this.curline == null) {
+ this.curline = $('')[0];
+ this.page.appendChild(this.curline);
+ this.lines[this.row] = this.curline;
+ this.scrollToBottom();
+ }
+ }
+ }
+ flushline() {
+ this.curline = null;
+ }
+ // TODO: support fixed-width window (use CSS grid?)
+ addtext(line: string, style: number) {
+ this.ensureline();
+ if (line.length) {
+ // in fixed mode, only do characters
+ if (this.fixed && line.length > 1) {
+ for (var i = 0; i < line.length; i++)
+ this.addtext(line[i], style);
+ return;
+ }
+ var span = $("").text(line);
+ for (var i = 0; i < 8; i++) {
+ if (style & (1 << i))
+ span.addClass("transcript-style-" + (1 << i));
+ }
+ if (this.reverse) span.addClass("transcript-reverse");
+ //span.data('vmip', this.vm.pc);
+ // in fixed mode, we can overwrite individual characters
+ if (this.fixed && line.length == 1 && this.col < this.curline.childNodes.length) {
+ this.curline.replaceChild(span[0], this.curline.childNodes[this.col]);
+ } else {
+ span.appendTo(this.curline);
+ }
+ this.col += line.length;
+ this.ncharsout += line.length;
+ //this.movePrintHead();
+ }
+ }
+ newline() {
+ this.flushline();
+ this.col = 0;
+ }
+ // TODO: bug in interpreter where it tracks cursor position but maybe doesn't do newlines?
+ print(val: string) {
+ // split by newlines
+ var lines = val.split("\n");
+ for (var i = 0; i < lines.length; i++) {
+ if (i > 0) this.newline();
+ this.addtext(lines[i], this.curstyle);
+ }
+ }
+ move_cursor(col: number, row: number) {
+ if (!this.fixed) return; // fixed windows only
+ // ensure enough row elements
+ while (this.lines.length <= row) {
+ this.flushline();
+ this.ensureline();
+ }
+ // select row element
+ this.curline = this.lines[row];
+ this.row = row;
+ // get children in row (individual text cells)
+ var children = $(this.curline).children();
+ // add whitespace to line?
+ if (children.length > col) {
+ this.col = col;
+ } else {
+ while (this.col < col)
+ this.addtext(' ', this.curstyle);
+ }
+ }
+ setrows(size: number) {
+ if (!this.fixed) return; // fixed windows only
+ // truncate rows?
+ var allrows = $(this.page).children();
+ if (allrows.length > size) {
+ this.flushline();
+ allrows.slice(size).remove();
+ this.lines = this.lines.slice(0, size);
+ //this.move_cursor(0,0);
+ }
+ }
+ formfeed() {
+ this.newline();
+ }
+ scrollToBottom() {
+ this.curline.scrollIntoView();
+ }
+ movePrintHead() {
+ var x = $(this.page).position().left + this.col * ($(this.page).width() / 80);
+ $("#printhead").offset({left: x});
+ }
+}
+
+class TeleTypeWithKeyboard extends TeleType {
+ input : HTMLInputElement;
+ runtime : BASICRuntime;
+ platform : BASICPlatform;
+ keepinput : boolean = true;
+
+ focused : boolean = true;
+ scrolling : number = 0;
+ waitingfor : string;
+ resolveInput;
+
+ constructor(page: HTMLElement, fixed: boolean, input: HTMLInputElement, platform: BASICPlatform) {
+ super(page, fixed);
+ this.input = input;
+ this.platform = platform;
+ this.runtime = platform.runtime;
+ this.runtime.input = async (prompt:string) => {
+ return new Promise( (resolve, reject) => {
+ this.addtext(prompt, 0);
+ this.addtext('? ', 0);
+ this.waitingfor = 'line';
+ this.focusinput();
+ this.resolveInput = resolve;
+ });
+ }
+ this.input.onkeypress = (e) => {
+ this.sendkey(e);
+ };
+ this.input.onfocus = (e) => {
+ this.focused = true;
+ console.log('inputline gained focus');
+ };
+ $("#workspace").on('click', (e) => {
+ this.focused = false;
+ console.log('inputline lost focus');
+ });
+ this.page.onclick = (e) => {
+ this.input.focus();
+ };
+ this.hideinput();
+ }
+ focusinput() {
+ this.ensureline();
+ // don't steal focus while editing
+ if (this.keepinput)
+ $(this.input).css('visibility', 'visible');
+ else
+ $(this.input).appendTo(this.curline).show()[0];
+ this.scrollToBottom();
+ if (this.focused) {
+ $(this.input).focus();
+ }
+ // change size
+ if (this.waitingfor == 'char')
+ $(this.input).addClass('transcript-input-char')
+ else
+ $(this.input).removeClass('transcript-input-char')
+ }
+ hideinput() {
+ if (this.keepinput)
+ $(this.input).css('visibility','hidden');
+ else
+ $(this.input).appendTo($(this.page).parent()).hide();
+ }
+ clearinput() {
+ this.input.value = '';
+ this.waitingfor = null;
+ }
+ sendkey(e: KeyboardEvent) {
+ if (this.waitingfor == 'line') {
+ if (e.key == "Enter") {
+ this.sendinput(this.input.value.toString());
+ }
+ } else if (this.waitingfor == 'char') {
+ this.sendchar(e.keyCode);
+ e.preventDefault();
+ }
+ }
+ sendinput(s: string) {
+ if (this.resolveInput) {
+ s = s.toUpperCase();
+ this.addtext(s, 4);
+ this.flushline();
+ this.resolveInput(s.split(','));
+ this.resolveInput = null;
+ }
+ this.clearinput();
+ this.hideinput(); // keep from losing input handlers
+ }
+ sendchar(code: number) {
+ }
+ ensureline() {
+ if (!this.keepinput) $(this.input).hide();
+ super.ensureline();
+ }
+ scrollToBottom() {
+ // TODO: fails when lots of lines are scrolled
+ if (this.scrolldiv) {
+ this.scrolling++;
+ $(this.scrolldiv).stop().animate({scrollTop: $(this.page).height()}, 200, 'swing', () => {
+ this.scrolling = 0;
+ this.ncharsout = 0;
+ });
+ } else {
+ this.input.scrollIntoView();
+ }
+ }
+ isBusy() {
+ // stop execution when scrolling and printing non-newlines
+ return this.scrolling > 0 && this.ncharsout > 0;
+ }
+}
+
+class BASICPlatform implements Platform {
+ mainElement: HTMLElement;
+ program: BASICProgram;
+ runtime: BASICRuntime;
+ timer: AnimationTimer;
+ tty: TeleTypeWithKeyboard;
+ ips: number = 500;
+ clock: number = 0;
+
+ constructor(mainElement: HTMLElement) {
+ //super();
+ this.mainElement = mainElement;
+ mainElement.style.overflowY = 'auto';
+ mainElement.style.backgroundColor = 'white';
+ }
+
+ async start() {
+ await loadScript('./gen/common/basic/runtime.js');
+ // create runtime
+ this.runtime = new BASICRuntime();
+ this.runtime.reset();
+ // create divs
+ var parent = this.mainElement;
+ // TODO: input line should be always flush left
+ var gameport = $('').appendTo(parent);
+ var windowport = $('').appendTo(gameport);
+ var inputline = $('').appendTo(parent);
+ //var printhead = $('').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.resize = () => {
+ // set font size proportional to window width
+ var charwidth = $(gameport).width() * 1.6 / 80;
+ $(windowport).css('font-size', charwidth + 'px');
+ this.tty.scrollToBottom();
+ }
+ this.resize();
+ this.runtime.print = this.tty.print.bind(this.tty);
+ this.runtime.resume = this.resume.bind(this);
+ }
+
+ advance1_60() {
+ if (this.tty.isBusy()) return;
+ this.clock += this.ips/60;
+ while (!this.runtime.exited && this.clock-- > 0) {
+ this.advance();
+ }
+ }
+
+ 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);
+ }
+ }
+ } catch (e) {
+ this.break();
+ throw e;
+ }
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ resize: () => void;
+
+ loadROM(title, data) {
+ this.reset();
+ this.program = data;
+ this.runtime.load(data);
+ }
+
+ getROMExtension() {
+ return ".json";
+ }
+
+ reset(): void {
+ var didExit = this.runtime.exited;
+ this.tty.clear();
+ this.runtime.reset();
+ // restart program if it's finished, otherwise reset and hold
+ if (didExit) {
+ this.resume();
+ }
+ }
+
+ pause(): void {
+ this.timer.stop();
+ }
+
+ resume(): void {
+ this.clock = 0;
+ this.timer.start();
+ }
+
+ isRunning() { return this.timer.isRunning(); }
+ getDefaultExtension() { return ".bas"; }
+ getToolForFilename() { return "basic"; }
+ getPresets() { return BASIC_PRESETS; }
+
+ getPC() {
+ return this.runtime.curpc;
+ }
+ getSP() {
+ return 0x1000 - this.runtime.returnStack.length;
+ }
+ isStable() {
+ return true;
+ }
+
+ getCPUState() {
+ return { PC: this.getPC(), SP: this.getSP() }
+ }
+ saveState() {
+ return {
+ c: this.getCPUState(),
+ rt: $.extend(true, {}, this.runtime) // TODO: don't take all
+ }
+ }
+ loadState(state) {
+ $.extend(true, this.runtime, state);
+ }
+ getDebugTree() {
+ return this.runtime;
+ }
+ inspect(sym: string) {
+ var o = this.runtime.vars[sym];
+ if (o != null) {
+ return o.toString();
+ }
+ }
+
+ // TODO: debugging (get running state, etc)
+
+ onBreakpointHit : BreakpointCallback;
+
+ setupDebug(callback : BreakpointCallback) : void {
+ this.onBreakpointHit = callback;
+ }
+ clearDebug() {
+ this.onBreakpointHit = null;
+ }
+ step() {
+ this.pause();
+ this.advance();
+ this.break();
+ }
+ break() {
+ if (this.onBreakpointHit) {
+ this.onBreakpointHit(this.saveState());
+ }
+ }
+}
+
+//
+
+PLATFORMS['basic'] = BASICPlatform;
diff --git a/src/worker/workermain.ts b/src/worker/workermain.ts
index bc9cda4f..b168ea4c 100644
--- a/src/worker/workermain.ts
+++ b/src/worker/workermain.ts
@@ -311,9 +311,9 @@ var workerseq : number = 0;
function compareData(a:FileData, b:FileData) : boolean {
if (a.length != b.length) return false;
- if (typeof a === 'string' && typeof b === 'string')
- return a==b;
- else {
+ if (typeof a === 'string' && typeof b === 'string') {
+ return a == b;
+ } else {
for (var i=0; i { return (key=='$loc'?undefined:value) });
+ putWorkFile(jsonpath, json);
+ if (anyTargetChanged(step, [jsonpath])) return {
+ output: ast,
+ listings: parser.getListings(),
+ };
+ }
+}
+
////////////////////////////
var TOOLS = {
@@ -2668,6 +2696,7 @@ var TOOLS = {
'inform6': compileInform6,
'merlin32': assembleMerlin32,
'fastbasic': compileFastBasic,
+ 'basic': compileBASIC,
}
var TOOL_PRELOADFS = {