diff --git a/doc/notes.txt b/doc/notes.txt index 761af872..854dda8d 100644 --- a/doc/notes.txt +++ b/doc/notes.txt @@ -510,3 +510,19 @@ in devices: Should call trap() every cycle or insn of frame, or exit when returns true? + +BETTER DEBUGGING + +Need to mark start/end columns, not just line number +Know if we are actively debugging or trap occurred +isRunning() = stopped, running, waiting, debugging... +Showing running PC may be distracting, maybe lines visited? +Don't grab cursor focus when trap occurs (how do we know?) +Use tick() and refresh(), not callbacks +Show current datum when using READ +Use https://codemirror.net/doc/manual.html#markText + + + + + diff --git a/presets/basic/skeleton.basic b/presets/basic/skeleton.basic new file mode 100644 index 00000000..e86e9f18 --- /dev/null +++ b/presets/basic/skeleton.basic @@ -0,0 +1 @@ +10 PRINT "EXAMPLE BASIC PROGRAM" diff --git a/src/codemirror/basic.js b/src/codemirror/basic.js index eaf34014..0039acbf 100644 --- a/src/codemirror/basic.js +++ b/src/codemirror/basic.js @@ -26,14 +26,15 @@ CodeMirror.defineMode("basic", function(conf, parserConf) { var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*"); var openingKeywords = ['if','for']; - var middleKeywords = ['to']; + var middleKeywords = ['to','then']; var endKeywords = ['next','end']; - var operatorKeywords = ['and', 'or', 'not', 'xor', 'in']; + var operatorKeywords = ['and', 'or', 'not', 'xor', 'eqv', 'imp']; var wordOperators = wordRegexp(operatorKeywords); var commonKeywords = [ - 'let','print','go','goto','gosub','next','dim','input','data', - 'read','restore','return','stop','on','def','option','then','step', + 'BASE','DATA','DEF','DIM', + 'GO','GOSUB','GOTO','INPUT','LET','ON','OPTION','PRINT', + 'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB' ]; var commontypes = ['xxxxbyte','xxxxword']; diff --git a/src/common/basic/compiler.ts b/src/common/basic/compiler.ts index 543c07f4..2d20c451 100644 --- a/src/common/basic/compiler.ts +++ b/src/common/basic/compiler.ts @@ -13,7 +13,7 @@ class CompileError extends Error { // Lexer regular expression -- each (capture group) handles a different token type -const re_toks = /([0-9.]+[E][+-]?\d+)|(\d*[.]\d*[E0-9]*)|(\d+)|(['].*)|(\bAND\b)|(\bOR\b)|(\w+[$]?)|(".*?")|([<>]?[=<>])|([-+*/^,;:()])|(\S+)/gi; +const re_toks = /([0-9.]+[E][+-]?\d+)|(\d*[.]\d*[E0-9]*)|(\d+)|(['].*)|(\w+[$]?)|(".*?")|([<>]?[=<>])|([-+*/^,;:()])|(\S+)/gi; export enum TokenType { EOL = 0, @@ -21,8 +21,6 @@ export enum TokenType { Float2, Int, Remark, - And, - Or, Ident, String, Relational, @@ -35,7 +33,7 @@ export type ExprTypes = BinOp | UnOp | IndOp | Literal; export type Expr = ExprTypes & SourceLocated; -export type Opcode = 'add' | 'sub' | 'mul' | 'div' | 'pow' | 'eq' | 'ne' | 'lt' | 'gt' | 'le' | 'ge' | 'land' | 'lor'; +export type Opcode = string; export type Value = string | number; @@ -50,7 +48,7 @@ export interface BinOp { } export interface UnOp { - op: 'neg'; + op: 'neg' | 'lnot'; expr: Expr; } @@ -152,6 +150,7 @@ export interface BASICLine { } export interface BASICProgram { + opts: BASICOptions; lines: BASICLine[]; } @@ -162,23 +161,25 @@ class Token { } const OPERATORS = { - 'AND': {f:'land',p:5}, - 'OR': {f:'lor',p:5}, - '=': {f:'eq',p:10}, - '<>': {f:'ne',p:10}, - '<': {f:'lt',p:10}, - '>': {f:'gt',p:10}, - '<=': {f:'le',p:10}, - '>=': {f:'ge',p:10}, + 'OR': {f:'bor',p:7}, + 'AND': {f:'band',p:8}, + '=': {f:'eq',p:50}, + '<>': {f:'ne',p:50}, + '<': {f:'lt',p:50}, + '>': {f:'gt',p:50}, + '<=': {f:'le',p:50}, + '>=': {f:'ge',p:50}, '+': {f:'add',p:100}, '-': {f:'sub',p:100}, + '%': {f:'mod',p:140}, + '\\': {f:'idiv',p:150}, '*': {f:'mul',p:200}, '/': {f:'div',p:200}, '^': {f:'pow',p:300} }; -function getOpcodeForOperator(op: string): Opcode { - return OPERATORS[op].f as Opcode; +function getOperator(op: string) { + return OPERATORS[op]; } function getPrecedence(tok: Token): number { @@ -186,7 +187,7 @@ function getPrecedence(tok: Token): number { case TokenType.Operator: case TokenType.Relational: case TokenType.Ident: - let op = OPERATORS[tok.str] + let op = getOperator(tok.str); if (op) return op.p; } return -1; @@ -202,7 +203,7 @@ function stripQuotes(s: string) { return s.substr(1, s.length-2); } -// TODO +// TODO: implement these export interface BASICOptions { uppercaseOnly : boolean; // convert everything to uppercase? strictVarNames : boolean; // only allow A0-9 for numerics, single letter for arrays/strings @@ -222,21 +223,25 @@ export interface BASICOptions { printPrecision : number; // print precision # of digits checkOverflow : boolean; // check for overflow of numerics? defaultValues : boolean; // initialize unset variables to default value? (0 or "") + multipleNextVars : boolean; // NEXT Y,X } ///// BASIC PARSER export class BASICParser { - tokens: Token[]; + opts : BASICOptions = ALTAIR_BASIC40; errors: WorkerError[]; + listings: CodeListingMap; labels: { [label: string]: BASICLine }; targets: { [targetlabel: string]: SourceLocation }; - eol: Token; + decls: { [name: string]: SourceLocation }; // declared/set vars + refs: { [name: string]: SourceLocation }; // references + lineno : number; + tokens: Token[]; + eol: Token; curlabel: string; - listings: CodeListingMap; lasttoken: Token; - opts : BASICOptions = ALTAIR_BASIC40; constructor() { this.labels = {}; @@ -245,6 +250,8 @@ export class BASICParser { this.lineno = 0; this.curlabel = null; this.listings = {}; + this.decls = {}; + this.refs = {}; } compileError(msg: string, loc?: SourceLocation) { if (!loc) loc = this.peekToken().$loc; @@ -253,7 +260,7 @@ export class BASICParser { throw new CompileError(`${msg} (line ${loc.line})`); // TODO: label too? } dialectError(what: string, loc?: SourceLocation) { - this.compileError(`The selected BASIC dialect doesn't support ${what}`, loc); // TODO + this.compileError(`The selected BASIC dialect doesn't support ${what}.`, loc); // TODO } consumeToken(): Token { var tok = this.lasttoken = (this.tokens.shift() || this.eol); @@ -261,9 +268,9 @@ export class BASICParser { } expectToken(str: string) : Token { var tok = this.consumeToken(); - var tokstr = tok.str.toUpperCase(); + var tokstr = tok.str; if (str != tokstr) { - this.compileError(`I expected "${str}" here, but I saw "${tokstr}".`); + this.compileError(`There should be a "${str}" here.`); } return tok; } @@ -278,7 +285,7 @@ export class BASICParser { let tok = this.consumeToken(); switch (tok.type) { case TokenType.Int: - if (this.labels[tok.str] != null) this.compileError(`I saw a duplicated label "${tok.str}".`); + if (this.labels[tok.str] != null) this.compileError(`There's a duplicated label "${tok.str}".`); this.labels[tok.str] = line; line.label = tok.str; this.curlabel = tok.str; @@ -291,8 +298,8 @@ export class BASICParser { } parseFile(file: string, path: string) : BASICProgram { var pgmlines = file.split("\n").map((line) => this.parseLine(line)); - this.checkLabels(); - var program = { lines: pgmlines }; + var program = { opts: this.opts, lines: pgmlines }; + this.checkAll(program); this.listings[path] = this.generateListing(file, program); return program; } @@ -308,11 +315,15 @@ export class BASICParser { tokenize(line: string) : void { this.lineno++; this.tokens = []; - var m; + var m : RegExpMatchArray; while (m = re_toks.exec(line)) { for (var i = 1; i < TokenType._LAST; i++) { - let s = m[i]; + let s : string = m[i]; if (s != null) { + // uppercase all identifiers, and maybe more + if (i == TokenType.Ident || this.opts.uppercaseOnly) + s = s.toUpperCase(); + // add token to list this.tokens.push({ str: s, type: i, @@ -341,23 +352,27 @@ export class BASICParser { } parseStatement(): Statement | null { var cmdtok = this.consumeToken(); + var cmd = cmdtok.str; var stmt; switch (cmdtok.type) { case TokenType.Remark: if (!this.opts.tickComments) this.dialectError(`tick remarks`); return null; case TokenType.Ident: - var cmd = cmdtok.str.toUpperCase(); - // remark? ignore to eol + // remark? ignore all tokens to eol if (cmd == 'REM') { while (this.consumeToken().type != TokenType.EOL) { } return null; } - // look for "GO TO" + // look for "GO TO" and "GO SUB" if (cmd == 'GO' && this.peekToken().str == 'TO') { this.consumeToken(); cmd = 'GOTO'; + } else if (cmd == 'GO' && this.peekToken().str == 'SUB') { + this.consumeToken(); + cmd = 'GOSUB'; } + // lookup JS function for command var fn = this['stmt__' + cmd]; if (fn) { if (this.opts.validKeywords && this.opts.validKeywords.indexOf(cmd) < 0) @@ -371,10 +386,8 @@ export class BASICParser { break; } case TokenType.EOL: - this.compileError(`I expected a command here`); - return null; default: - this.compileError(`Unknown command "${cmdtok.str}"`); + this.compileError(`There should be a command here.`); return null; } if (stmt) stmt.$loc = { line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: this.peekToken().$loc.start }; @@ -387,6 +400,7 @@ export class BASICParser { var tok = this.consumeToken(); switch (tok.type) { case TokenType.Ident: + this.refs[tok.str] = tok.$loc; let args = null; if (this.peekToken().str == '(') { this.expectToken('('); @@ -410,8 +424,10 @@ export class BASICParser { this.pushbackToken(sep); return list; } - parseVarOrIndexedList(): IndOp[] { - return this.parseList(this.parseVarOrIndexed, ','); + parseLexprList(): IndOp[] { + var list = this.parseList(this.parseVarOrIndexed, ','); + list.forEach((lexpr) => this.decls[lexpr.name] = this.lasttoken.$loc); + return list; } parseExprList(): Expr[] { return this.parseList(this.parseExpr, ','); @@ -427,7 +443,7 @@ export class BASICParser { this.targets[label] = tok.$loc; return {value:label}; default: - this.compileError(`I expected a line number here`); + this.compileError(`There should be a line number here.`); return; } } @@ -437,12 +453,17 @@ export class BASICParser { case TokenType.Int: case TokenType.Float1: case TokenType.Float2: - return { value: parseFloat(tok.str), $loc: tok.$loc }; + return { value: this.parseNumber(tok.str), $loc: tok.$loc }; case TokenType.String: return { value: stripQuotes(tok.str), $loc: tok.$loc }; case TokenType.Ident: - this.pushbackToken(tok); - return this.parseVarOrIndexedOrFunc(); + if (tok.str == 'NOT') { + let expr = this.parsePrimary(); + return { op: 'lnot', expr: expr }; + } else { + this.pushbackToken(tok); + return this.parseVarOrIndexedOrFunc(); + } case TokenType.Operator: if (tok.str == '(') { let expr = this.parseExpr(); @@ -454,21 +475,33 @@ export class BASICParser { } else if (tok.str == '+') { return this.parsePrimary(); // TODO? } - default: - this.compileError(`Unexpected "${tok.str}"`); + case TokenType.EOL: + this.compileError(`The expression is incomplete.`); + return; } + this.compileError(`There was an unexpected "${tok.str}" in this expression.`); + } + parseNumber(str: string) : number { + var n = parseFloat(str); + if (isNaN(n)) + this.compileError(`The number ${str} is not a valid floating-point number.`); + if (this.opts.checkOverflow && !isFinite(n)) + this.compileError(`The number ${str} is too big to fit into a floating-point value.`); + return n; } parseExpr1(left: Expr, minPred: number): Expr { let look = this.peekToken(); while (getPrecedence(look) >= minPred) { let op = this.consumeToken(); + if (this.opts.validOperators && this.opts.validOperators.indexOf(op.str) < 0) + this.dialectError(`the "${op.str}" operator`); let right: Expr = this.parsePrimary(); look = this.peekToken(); while (getPrecedence(look) > getPrecedence(op)) { right = this.parseExpr1(right, getPrecedence(look)); look = this.peekToken(); } - left = { op: getOpcodeForOperator(op.str), left: left, right: right }; + left = { op: getOperator(op.str).f, left: left, right: right }; } return left; } @@ -481,6 +514,7 @@ export class BASICParser { stmt__LET(): LET_Statement { var lexpr = this.parseVarOrIndexed(); this.expectToken("="); + this.decls[lexpr.name] = this.lasttoken.$loc; var right = this.parseExpr(); return { command: "LET", lexpr: lexpr, right: right }; } @@ -547,7 +581,7 @@ export class BASICParser { return { command:'NEXT', lexpr:lexpr }; } stmt__DIM() : DIM_Statement { - return { command:'DIM', args:this.parseVarOrIndexedList() }; + return { command:'DIM', args:this.parseLexprList() }; } stmt__INPUT() : INPUT_Statement { var prompt = this.consumeToken(); @@ -559,13 +593,13 @@ export class BASICParser { this.pushbackToken(prompt); promptstr = ""; } - return { command:'INPUT', prompt:{ value: promptstr, $loc: prompt.$loc }, args:this.parseVarOrIndexedList() }; + return { command:'INPUT', prompt:{ value: promptstr, $loc: prompt.$loc }, args:this.parseLexprList() }; } stmt__DATA() : DATA_Statement { return { command:'DATA', datums:this.parseExprList() }; } stmt__READ() { - return { command:'READ', args:this.parseVarOrIndexedList() }; + return { command:'READ', args:this.parseLexprList() }; } stmt__RESTORE() { return { command:'RESTORE' }; @@ -587,22 +621,43 @@ export class BASICParser { } stmt__DEF() : DEF_Statement { var lexpr = this.parseVarOrIndexed(); - if (!lexpr.name.toUpperCase().startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`) + if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`) this.expectToken("="); + this.decls[lexpr.name] = this.lasttoken.$loc; var func = this.parseExpr(); return { command:'DEF', lexpr:lexpr, def:func }; } stmt__OPTION() : OPTION_Statement { var tokname = this.consumeToken(); - if (tokname.type != TokenType.Ident) this.compileError(`I expected a name after the OPTION statement.`) + if (tokname.type != TokenType.Ident) this.compileError(`There should be a name after the OPTION statement.`) var list : string[] = []; var tok; do { tok = this.consumeToken(); if (isEOS(tok)) break; - list.push(tok.str.toUpperCase()); + list.push(tok.str); } while (true); - return { command:'OPTION', optname:tokname.str.toUpperCase(), optargs:list }; + var stmt : OPTION_Statement = { command:'OPTION', optname:tokname.str, optargs:list }; + this.parseOptions(stmt); + return stmt; + } + parseOptions(stmt: OPTION_Statement) { + switch (stmt.optname) { + case 'BASE': + let base = parseInt(stmt.optargs[0]); + if (base == 0 || base == 1) this.opts.defaultArrayBase = base; + else this.compileError("OPTION BASE can only be 0 or 1."); + break; + case 'DIALECT': + let dname = stmt.optargs[0] || ""; + let dialect = DIALECTS[dname]; + if (dialect) this.opts = dialect; + else this.compileError(`The dialect named "${dname}" is not supported by this compiler.`); + break; + default: + this.compileError(`OPTION ${stmt.optname} is not supported by this compiler.`); + break; + } } // for workermain @@ -620,13 +675,23 @@ export class BASICParser { } // LINT STUFF + checkAll(program : BASICProgram) { + this.checkLabels(); + //this.checkUnsetVars(); + } checkLabels() { for (let targ in this.targets) { if (this.labels[targ] == null) { - this.compileError(`I couldn't find line number ${targ}`, this.targets[targ]); + this.compileError(`There isn't a line number ${targ}.`, this.targets[targ]); } } } + checkUnsetVars() { + for (var ref in this.refs) { + if (this.decls[ref] == null) + this.compileError(`The variable "${ref}" was used but not set with a LET, DIM, READ, or INPUT statement.`); + } + } } ///// BASIC DIALECTS @@ -643,18 +708,19 @@ export const ECMA55_MINIMAL : BASICOptions = { stringConcat : false, typeConvert : false, maxDimensions : 2, - maxArguments : Infinity, + maxArguments : 255, sparseArrays : false, tickComments : false, validKeywords : ['BASE','DATA','DEF','DIM','END', 'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT', 'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB','THEN','TO' ], - validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAN'], + validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN'], validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'], printZoneLength : 15, printPrecision : 6, checkOverflow : true, + multipleNextVars : false, } export const ALTAIR_BASIC40 : BASICOptions = { @@ -667,7 +733,7 @@ export const ALTAIR_BASIC40 : BASICOptions = { stringConcat : false, typeConvert : false, maxDimensions : 2, - maxArguments : Infinity, + maxArguments : 255, sparseArrays : false, tickComments : false, validKeywords : null, // all @@ -676,4 +742,13 @@ export const ALTAIR_BASIC40 : BASICOptions = { printZoneLength : 15, printPrecision : 6, checkOverflow : true, + multipleNextVars : true, } + +export const DIALECTS = { + "DEFAULT": ALTAIR_BASIC40, + "ALTAIR": ALTAIR_BASIC40, + "ALTAIR40": ALTAIR_BASIC40, + "ECMA55": ECMA55_MINIMAL, + "MINIMAL": ECMA55_MINIMAL, +}; diff --git a/src/common/basic/run.ts b/src/common/basic/run.ts index 6515c6a2..5e0267f4 100644 --- a/src/common/basic/run.ts +++ b/src/common/basic/run.ts @@ -17,9 +17,13 @@ var data = fs.readFileSync(filename, 'utf-8'); try { var pgm = parser.parseFile(data, filename); } catch (e) { - console.log("@@@ " + e.message); - throw e; + if (parser.errors.length == 0) + console.log("@@@ " + e.msg); + else + console.log(e); } +parser.errors.forEach((err) => console.log("@@@ " + err.msg)); +if (parser.errors.length) process.exit(2); var runtime = new BASICRuntime(); runtime.trace = process.argv[3] == '-v'; @@ -47,7 +51,7 @@ runtime.resume = function() { } } catch (e) { console.log("### " + e.message); - throw e; + process.exit(1); } }); } diff --git a/src/common/basic/runtime.ts b/src/common/basic/runtime.ts index a6337629..ff5684c4 100644 --- a/src/common/basic/runtime.ts +++ b/src/common/basic/runtime.ts @@ -39,16 +39,17 @@ export class BASICRuntime { dataptr : number; vars : {}; arrays : {}; - forLoops : {}; + defs : {}; + forLoops : {next:(name:string) => void}[]; returnStack : number[]; column : number; - abase : number; // array base running : boolean = false; - exited : boolean = false; + exited : boolean = true; trace : boolean = false; load(program: basic.BASICProgram) { + let prevlabel = this.label2pc && this.getLabelForPC(this.curpc); this.program = program; this.label2lineidx = {}; this.label2pc = {}; @@ -59,6 +60,7 @@ export class BASICRuntime { // TODO: lines start @ 1? program.lines.forEach((line, idx) => { // make lookup tables + this.curpc = this.allstmts.length + 1; // set for error reporting if (line.label != null) this.label2lineidx[line.label] = idx; if (line.label != null) this.label2pc[line.label] = this.allstmts.length; this.line2pc.push(this.allstmts.length); @@ -69,8 +71,6 @@ export class BASICRuntime { line.stmts.filter((stmt) => stmt.command == 'DATA').forEach((datastmt) => { (datastmt as basic.DATA_Statement).datums.forEach(d => { var functext = this.expr2js(d, {check:true, isconst:true}); - // TODO: catch exceptions - // TODO: any value doing this ahead of time? var value = new Function(`return ${functext};`).bind(this)(); this.datums.push({value:value}); }); @@ -78,6 +78,9 @@ export class BASICRuntime { // TODO: compile statements? //line.stmts.forEach((stmt) => this.compileStatement(stmt)); }); + // try to resume where we left off after loading + this.curpc = this.label2pc[prevlabel] || 0; + this.dataptr = Math.min(this.dataptr, this.datums.length); } reset() { @@ -85,21 +88,29 @@ export class BASICRuntime { this.dataptr = 0; this.vars = {}; this.arrays = {}; - this.forLoops = {}; + this.defs = this.getBuiltinFunctions(); + this.forLoops = []; this.returnStack = []; this.column = 0; - this.abase = 1; this.running = true; this.exited = false; } + getBuiltinFunctions() { + var fnames = this.program && this.program.opts.validFunctions; + // if no valid function list, look for ABC...() functions in prototype + if (!fnames) fnames = Object.keys(BASICRuntime.prototype).filter((name) => /^[A-Z]{3,}[$]?$/.test(name)); + var dict = {}; + for (var fn of fnames) dict[fn] = this[fn].bind(this); + return dict; + } + runtimeError(msg : string) { this.curpc--; // we did curpc++ before executing statement // TODO: pass source location to error throw new EmuHalt(`${msg} (line ${this.getLabelForPC(this.curpc)})`); } - // TODO: sometimes on next line getLineForPC(pc:number) { var line; do { @@ -131,7 +142,7 @@ export class BASICRuntime { if (this.trace) console.log(this.curpc, stmt, this.vars, Object.keys(this.arrays)); // skip to next statment this.curpc++; - // compile statement to JS? + // compile (unless cached) and execute statement this.compileStatement(stmt); this.executeStatement(stmt); return this.running; @@ -171,7 +182,7 @@ export class BASICRuntime { } gosubLabel(label) { - this.returnStack.push(this.curpc + 1); + this.returnStack.push(this.curpc); this.gotoLabel(label); } @@ -240,9 +251,9 @@ export class BASICRuntime { } else { if (opts.isconst) this.runtimeError(`I expected a constant value here`); var s = ''; - if (expr.args && this[expr.name]) { // is it a function? - s += `this.${expr.name}(`; - s += expr.args.map((arg) => this.expr2js(arg, opts)).join(', '); + if (this.defs[expr.name]) { // is it a function? + s += `this.defs.${expr.name}(`; + if (expr.args) s += expr.args.map((arg) => this.expr2js(arg, opts)).join(', '); s += ')'; } else if (expr.args) { // is it a subscript? s += `this.getArray(${JSON.stringify(expr.name)}, ${expr.args.length})`; @@ -252,7 +263,7 @@ export class BASICRuntime { s = `this.vars.${expr.name}`; } if (opts.check) - return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`; // TODO: better error + return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`; else return s; } @@ -260,64 +271,85 @@ export class BASICRuntime { var left = this.expr2js(expr.left, opts); var right = this.expr2js(expr.right, opts); return `this.${expr.op}(${left}, ${right})`; - } else if (isUnOp(expr) && expr.op == 'neg') { + } else if (isUnOp(expr)) { var e = this.expr2js(expr.expr, opts); - return `-(${e})`; // TODO: other ops? + return `this.${expr.op}(${e})`; } } - startForLoop(name, init, targ, step) { - // TODO: check for loop params + startForLoop(forname, init, targ, step) { + // TODO: support 0-iteration loops var pc = this.curpc; if (!step) step = 1; - this.vars[name] = init; - if (this.trace) console.log(`FOR ${name} = ${this.vars[name]} TO ${targ} STEP ${step}`); - this.forLoops[name] = { - next: () => { - var done = step >= 0 ? this.vars[name] >= targ : this.vars[name] <= targ; + this.vars[forname] = init; + if (this.trace) console.log(`FOR ${forname} = ${init} TO ${targ} STEP ${step}`); + this.forLoops.unshift({ + next: (nextname:string) => { + if (nextname && forname != nextname) + this.runtimeError(`I executed NEXT "${nextname}", but the last FOR was for "${forname}".`) + this.vars[forname] += step; + var done = step >= 0 ? this.vars[forname] > targ : this.vars[forname] < targ; if (done) { - delete this.forLoops[name]; + this.forLoops.shift(); // pop FOR off the stack and continue } else { - this.vars[name] += step; - this.curpc = pc; + this.curpc = pc; // go back to FOR location } - if (this.trace) console.log(`NEXT ${name}: ${this.vars[name]} TO ${targ} STEP ${step} DONE=${done}`); + if (this.trace) console.log(`NEXT ${forname}: ${this.vars[forname]} TO ${targ} STEP ${step} DONE=${done}`); } - }; + }); } nextForLoop(name) { - // TODO: check for for loop - var fl = this.forLoops[name]; - if (!fl) this.runtimeError(`I couldn't find a matching FOR for this NEXT.`) - this.forLoops[name].next(); + var fl = this.forLoops[0]; + if (!fl) this.runtimeError(`I couldn't find a FOR for this NEXT.`) + else fl.next(name); } // converts a variable to string/number based on var name + assign(name: string, right: number|string) : number|string { + if (this.program.opts.typeConvert) + return this.convert(name, right); + // TODO: use options + if (name.endsWith("$")) { + return this.convertToString(right, name); + } else { + return this.convertToNumber(right, name); + } + } + convert(name: string, right: number|string) : number|string { - // TODO: error check? - if (name.endsWith("$")) + if (name.endsWith("$")) { return right+""; - else if (typeof right === 'string') - return parseFloat(right); - else if (typeof right === 'number') + } else if (typeof right === 'number') { return right; - else - return this.checkValue(right, name); + } else { + return parseFloat(right+""); + } + } + + convertToString(right: number|string, name?: string) { + if (typeof right !== 'string') this.runtimeError(`I can't convert ${right} to a string.`); + else return right; + } + + convertToNumber(right: number|string, name?: string) { + if (typeof right !== 'number') this.runtimeError(`I can't convert ${right} to a number.`); + else return this.checkNum(right); } // dimension array dimArray(name: string, ...dims:number[]) { + if (this.arrays[name]) this.runtimeError(`I already dimensioned this array (${name}) earlier.`) var isstring = name.endsWith('$'); - // TODO: option for undefined float array elements? - var arrcons = isstring ? Array : Float64Array; - var ab = this.abase; + // if defaultValues is true, we use Float64Array which inits to 0 + var arrcons = isstring || !this.program.opts.defaultValues ? Array : Float64Array; + // TODO? var ab = this.program.opts.defaultArrayBase; if (dims.length == 1) { - this.arrays[name] = new arrcons(dims[0]+ab); + this.arrays[name] = new arrcons(dims[0]+1); } else if (dims.length == 2) { - this.arrays[name] = new Array(dims[0]+ab); - for (var i=ab; i { var lexpr = this.expr2js(arg, {check:false}); - setvals += `${lexpr} = this.convert(${JSON.stringify(arg.name)}, vals[${index}]);` + setvals += `valid &= this.isValid(${lexpr} = this.convert(${JSON.stringify(arg.name)}, vals[${index}]));` }); - return `this.running=false; this.input(${prompt}, ${stmt.args.length}).then((vals) => {${setvals}; this.running=true; this.resume();})`; + return `this.running=false; + this.input(${prompt}, ${stmt.args.length}).then((vals) => { + let valid = 1; + ${setvals} + if (!valid) this.curpc--; + this.running=true; + this.resume(); + })`; } do__LET(stmt : basic.LET_Statement) { var lexpr = this.expr2js(stmt.lexpr, {check:false}); var right = this.expr2js(stmt.right, {check:true}); - return `${lexpr} = this.convert(${JSON.stringify(stmt.lexpr.name)}, ${right});`; + return `${lexpr} = this.assign(${JSON.stringify(stmt.lexpr.name)}, ${right});`; } do__FOR(stmt : basic.FOR_Statement) { @@ -384,7 +423,7 @@ export class BASICRuntime { } do__NEXT(stmt : basic.NEXT_Statement) { - var name = JSON.stringify(stmt.lexpr.name); // TODO: args? lexpr == null? + var name = stmt.lexpr && JSON.stringify(stmt.lexpr.name); // TODO: args? lexpr == null? return `this.nextForLoop(${name})`; } @@ -394,9 +433,9 @@ export class BASICRuntime { } do__DEF(stmt : basic.DEF_Statement) { - var lexpr = `this.${stmt.lexpr.name}`; + var lexpr = `this.defs.${stmt.lexpr.name}`; var args = []; - for (var arg of stmt.lexpr.args) { + for (var arg of stmt.lexpr.args || []) { if (isLookup(arg)) { args.push(arg.name); } else { @@ -404,14 +443,13 @@ export class BASICRuntime { } } var functext = this.expr2js(stmt.def, {check:true, locals:args}); - // TODO: use stmt.args to add function params - return `${lexpr} = function(${args.join(',')}) { return ${functext}; }`; + return `${lexpr} = function(${args.join(',')}) { return ${functext}; }.bind(this)`; } _DIM(dim : basic.IndOp) { var argsstr = ''; for (var arg of dim.args) { - // TODO: check for float + // TODO: check for float (or at compile time) argsstr += ', ' + this.expr2js(arg, {check:true}); } return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`; @@ -450,13 +488,13 @@ export class BASICRuntime { do__READ(stmt : basic.READ_Statement) { var s = ''; stmt.args.forEach((arg) => { - s += `${this.expr2js(arg, {check:false})} = this.convert(${JSON.stringify(arg.name)}, this.nextDatum());`; + s += `${this.expr2js(arg, {check:false})} = this.assign(${JSON.stringify(arg.name)}, this.nextDatum());`; }); return s; } do__RESTORE() { - this.dataptr = 0; // TODO: line number? + this.dataptr = 0; } do__END() { @@ -468,26 +506,30 @@ export class BASICRuntime { } do__OPTION(stmt: basic.OPTION_Statement) { - switch (stmt.optname) { - case 'BASE': - let base = parseInt(stmt.optargs[0]); - if (base == 0 || base == 1) this.abase = base; - else this.runtimeError("OPTION BASE can only be 0 or 1."); - break; - default: - this.runtimeError(`OPTION ${stmt.optname} is not supported by this compiler.`); - break; - } + // already parsed in compiler } - // TODO: "SUBSCRIPT ERROR" + // TODO: "SUBSCRIPT ERROR" (range check) // TODO: gosubs nested too deeply // TODO: memory quota // FUNCTIONS - checkValue(obj:number|string, exprname:string) { + isValid(obj:number|string) : boolean { + if (typeof obj === 'number' && !isNaN(obj)) + return true; + else if (typeof obj === 'string') + return true; + else + return false; + } + checkValue(obj:number|string, exprname:string) : number|string { + // check for unreferenced value if (typeof obj !== 'number' && typeof obj !== 'string') { + // assign default value? + if (obj == null && this.program.opts.defaultValues) { + return exprname.endsWith("$") ? "" : 0; + } if (exprname != null && obj == null) { this.runtimeError(`I didn't find a value for ${exprname}`); } else if (exprname != null) { @@ -527,6 +569,12 @@ export class BASICRuntime { if (b == 0) this.runtimeError(`I can't divide by zero.`); return this.checkNum(a / b); } + idiv(a:number, b:number) : number { + return this.div(Math.floor(a), Math.floor(b)); + } + mod(a:number, b:number) : number { + return this.checkNum(a % b); + } pow(a:number, b:number) : number { if (a == 0 && b < 0) this.runtimeError(`I can't raise zero to a negative power.`); return this.checkNum(Math.pow(a, b)); @@ -537,6 +585,18 @@ export class BASICRuntime { lor(a:number, b:number) : number { return a || b; } + lnot(a:number) : number { + return a ? 0 : 1; + } + neg(a:number) : number { + return -a; + } + band(a:number, b:number) : number { + return a & b; + } + bor(a:number, b:number) : number { + return a | b; + } eq(a:number, b:number) : boolean { return a == b; } @@ -604,7 +664,7 @@ export class BASICRuntime { return this.checkNum(Math.log(arg)); } MID$(arg : string, start : number, count : number) : string { - if (start < 1) this.runtimeError(`The second parameter to MID$ must be between 1 and the length of the string in the first parameter.`) + if (start < 1) this.runtimeError(`I tried to compute MID$ but the second parameter is less than zero (${start}).`) return arg.substr(start-1, count); } RIGHT$(arg : string, count : number) : string { @@ -614,7 +674,7 @@ export class BASICRuntime { return Math.random(); // argument ignored } ROUND(arg : number) : number { - return this.checkNum(Math.round(arg)); // TODO? + return this.checkNum(Math.round(arg)); } SGN(arg : number) : number { return (arg < 0) ? -1 : (arg > 0) ? 1 : 0; @@ -623,24 +683,25 @@ export class BASICRuntime { return this.checkNum(Math.sin(arg)); } SPACE$(arg : number) : string { - return ' '.repeat(this.checkNum(arg)); + return (arg > 0) ? ' '.repeat(this.checkNum(arg)) : ''; } SQR(arg : number) : number { if (arg < 0) this.runtimeError(`I can't take the square root of a negative number (${arg}).`) return this.checkNum(Math.sqrt(arg)); } - STR$(arg) : string { - return this.valueToString(arg); + STR$(arg : number) : string { + return this.valueToString(this.checkNum(arg)); } TAB(arg : number) : string { - if (arg < 0) this.runtimeError(`I got a negative value for the TAB() function.`); - var spaces = this.ROUND(arg) - this.column; + if (arg < 1) { arg = 1; } // TODO: SYSTEM MESSAGE IDENTIFYING THE EXCEPTION + var spaces = this.ROUND(arg) - 1 - this.column; return (spaces > 0) ? ' '.repeat(spaces) : ''; } TAN(arg : number) : number { return this.checkNum(Math.tan(arg)); } - VAL(arg) : number { - return parseFloat(arg+""); + VAL(arg : string) : number { + var n = parseFloat(this.checkString(arg)); + return isNaN(n) ? 0 : n; // TODO? altair works this way } } diff --git a/src/common/emu.ts b/src/common/emu.ts index ed05ca06..6869f0c1 100644 --- a/src/common/emu.ts +++ b/src/common/emu.ts @@ -225,6 +225,10 @@ export class RAM { } export class EmuHalt extends Error { + constructor(msg:string) { + super(msg); + Object.setPrototypeOf(this, EmuHalt.prototype); + } } export class AnimationTimer { diff --git a/src/ide/views.ts b/src/ide/views.ts index 924e3f6d..88d146ca 100644 --- a/src/ide/views.ts +++ b/src/ide/views.ts @@ -98,7 +98,7 @@ export class SourceEditor implements ProjectView { this.newEditor(div, asmOverride); if (text) { this.setText(text); // TODO: this calls setCode() and builds... it shouldn't - this.editor.setSelection({line:0,ch:0}, {line:0,ch:0}, {scroll:true}); + this.editor.setSelection({line:0,ch:0}, {line:0,ch:0}, {scroll:true}); // move cursor to start } this.setupEditor(); return div; diff --git a/src/platform/basic.ts b/src/platform/basic.ts index 8b024154..da609da6 100644 --- a/src/platform/basic.ts +++ b/src/platform/basic.ts @@ -1,6 +1,6 @@ import { Platform, BreakpointCallback } from "../common/baseplatform"; -import { PLATFORMS, AnimationTimer } from "../common/emu"; +import { PLATFORMS, AnimationTimer, EmuHalt } from "../common/emu"; import { loadScript } from "../ide/ui"; import { BASICRuntime } from "../common/basic/runtime"; import { BASICProgram } from "../common/basic/compiler"; @@ -278,7 +278,7 @@ class BASICPlatform implements Platform { //var printhead = $('
').appendTo(parent); this.tty = new TeleTypeWithKeyboard(windowport[0], true, inputline[0] as HTMLInputElement, this); this.tty.scrolldiv = parent; - this.timer = new AnimationTimer(60, this.advance1_60.bind(this)); + this.timer = new AnimationTimer(60, this.animate.bind(this)); this.resize = () => { // set font size proportional to window width var charwidth = $(gameport).width() * 1.6 / 80; @@ -286,11 +286,15 @@ class BASICPlatform implements Platform { this.tty.scrollToBottom(); } this.resize(); - this.runtime.print = this.tty.print.bind(this.tty); + this.runtime.print = (s:string) => { + // TODO: why null sometimes? + this.clock = 0; // exit advance loop when printing + this.tty.print(s); + } this.runtime.resume = this.resume.bind(this); } - advance1_60() { + animate() { if (this.tty.isBusy()) return; this.clock += this.ips/60; while (!this.runtime.exited && this.clock-- > 0) { @@ -298,33 +302,36 @@ class BASICPlatform implements Platform { } } + // should not depend on tty state advance(novideo?: boolean) : number { if (this.runtime.running) { - try { - var more = this.runtime.step(); - if (!more) { - this.pause(); - if (this.runtime.exited) { - this.tty.print("\n\n"); - this.tty.addtext("*** END OF PROGRAM ***", 1); - } + var more = this.runtime.step(); + if (!more) { + this.pause(); + if (this.runtime.exited) { + this.exitmsg(); } - } catch (e) { - this.break(); - throw e; } + // TODO: break() when EmuHalt at location? return 1; } else { return 0; } } + + exitmsg() { + this.tty.print("\n\n"); + this.tty.addtext("*** END OF PROGRAM ***", 1); + } resize: () => void; loadROM(title, data) { - this.reset(); + var didExit = this.runtime.exited; this.program = data; this.runtime.load(data); + // only reset if we exited, otherwise we try to resume + if (didExit) this.reset(); } getROMExtension() { @@ -403,8 +410,9 @@ class BASICPlatform implements Platform { this.break(); } break() { + // TODO: don't highlight text in editor if (this.onBreakpointHit) { - this.onBreakpointHit(this.saveState()); + //TODO: this.onBreakpointHit(this.saveState()); } } }