From 9cedb1af08a75a80bdc19efa84e31715cbaaf692 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Sun, 16 Aug 2020 12:16:29 -0500 Subject: [PATCH] basic: fixes for non-line-number mode (24, 62, 81), handle unhandledrejection, MODERN default dialect, DARTMOUTH --- css/ui.css | 6 ++ package.json | 3 +- presets/basic/23match.bas | 2 + presets/basic/craps.bas | 2 +- presets/basic/hamurabi.bas | 1 + presets/basic/haunted.bas | 2 +- presets/basic/hello.bas | 3 +- presets/basic/lander.bas | 2 +- presets/basic/mortgage.bas | 2 +- presets/basic/sieve.bas | 4 +- presets/basic/startrader.bas | 4 +- presets/basic/wumpus.bas | 2 +- src/codemirror/basic.js | 3 - src/common/basic/compiler.ts | 153 ++++++++++++++++++++++++++--------- src/common/basic/fuzz.ts | 44 ++++++++++ src/common/basic/main.ts | 24 ------ src/common/basic/run.ts | 18 +++-- src/common/basic/runtime.ts | 35 +++++--- src/ide/ui.ts | 5 +- src/ide/views.ts | 9 ++- src/platform/basic.ts | 6 +- 21 files changed, 228 insertions(+), 102 deletions(-) create mode 100644 src/common/basic/fuzz.ts delete mode 100644 src/common/basic/main.ts diff --git a/css/ui.css b/css/ui.css index c49443ec..3012b5d0 100644 --- a/css/ui.css +++ b/css/ui.css @@ -22,6 +22,12 @@ .currentpc-marker { color: #ff66ee; } +.currentpc-span-blocked { + background-color: #7e2a70; +} +.currentpc-marker-blocked { + color: #ffee33; +} .tooltipbox { position: relative; display: inline-block; diff --git a/package.json b/package.json index 625d6511..8ee4b593 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "test-worker": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000 test/cli/testworker.js", "test-platforms": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000 test/cli/testplatforms.js", "test-profile": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000 --prof test/cli", - "start": "electron ." + "start": "electron .", + "fuzzbasic": "jsfuzz gen/common/basic/fuzz.js ~/basic/corpus/ --versifier false" }, "main": "electron.js", "keywords": [ diff --git a/presets/basic/23match.bas b/presets/basic/23match.bas index 1f240479..d15c2d98 100644 --- a/presets/basic/23match.bas +++ b/presets/basic/23match.bas @@ -1,3 +1,5 @@ +OPTION DIALECT DEC +0 REM from the DEC EduSystem Handbook 100 REM***23 MATCHES 110 LET M=23 115 PRINT diff --git a/presets/basic/craps.bas b/presets/basic/craps.bas index 1e123474..ef6ec29e 100644 --- a/presets/basic/craps.bas +++ b/presets/basic/craps.bas @@ -1,5 +1,5 @@ +OPTION DIALECT HP2000 0 REM http://www.bitsavers.org/bits/HP/paperTapes/JeffM/CRAPS.BAS -1 OPTION DIALECT HP2000 10 REM THIS IS A SPECIAL VERSION OF CRAPS 20 REM 1) MAXIMUM BANK IS $1000 30 REM 2) THE PLAYER GETS 10 CHANCES TO PLACE A BET diff --git a/presets/basic/hamurabi.bas b/presets/basic/hamurabi.bas index 5e740067..302b3805 100644 --- a/presets/basic/hamurabi.bas +++ b/presets/basic/hamurabi.bas @@ -1,3 +1,4 @@ +OPTION DIALECT ALTAIR 10 REM *** CONVERTED FROM THE ORIGINAL FOCAL PROGRAM AND MODIFIED 20 REM *** FOR EDUSYSTEM 70 BY DAVID AHL, DIGITAL 30 REM *** MODIFIED FOR 8K MICROSOFT BASIC BY PETER TURNBULL diff --git a/presets/basic/haunted.bas b/presets/basic/haunted.bas index 2eefde08..afb601a7 100644 --- a/presets/basic/haunted.bas +++ b/presets/basic/haunted.bas @@ -1,4 +1,4 @@ -0 OPTION DIALECT DEC:REM http://www.bitsavers.org/bits/DEC/pdp10/games/ +OPTION DIALECT DEC:REM http://www.bitsavers.org/bits/DEC/pdp10/games/ 00001 PRINT " This is the latest version as of 2-Jun-82." 00005 DIM B$(30) 00010 F=0 diff --git a/presets/basic/hello.bas b/presets/basic/hello.bas index de4c8fb8..aed21745 100644 --- a/presets/basic/hello.bas +++ b/presets/basic/hello.bas @@ -1,8 +1,9 @@ +OPTION DIALECT DARTMOUTH 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!";CHR$(7) +30 PRINT "THANKS, ";A$;"! THIS WILL BE FUN!" 35 PRINT 40 INPUT "NOW TELL ME YOUR FAVORITE NUMBER";N 45 PRINT diff --git a/presets/basic/lander.bas b/presets/basic/lander.bas index b24b413b..5e2b6e2b 100644 --- a/presets/basic/lander.bas +++ b/presets/basic/lander.bas @@ -1,4 +1,4 @@ -0 OPTION DIALECT HP +OPTION DIALECT HP 1 REM **** HP BASIC PROGRAM LIBRARY ************************** 2 REM 3 REM LANDER: ROCKET LANDING VEHICLE diff --git a/presets/basic/mortgage.bas b/presets/basic/mortgage.bas index 38c0eff7..f2add7c3 100644 --- a/presets/basic/mortgage.bas +++ b/presets/basic/mortgage.bas @@ -1,4 +1,4 @@ -1 OPTION DIALECT HP +OPTION DIALECT HP 10 PRINT "DANIEL O'ROURKE FEB. 23, 1977" 20 PRINT "MINI-COMPUTOR 102" 30 PRINT "SUBJECT: MORTGAGE PAYMENT" diff --git a/presets/basic/sieve.bas b/presets/basic/sieve.bas index ff2408e1..cbb1dacc 100644 --- a/presets/basic/sieve.bas +++ b/presets/basic/sieve.bas @@ -1,5 +1,5 @@ -001 OPTION DIALECT MODERN -002 OPTION CPUSPEED MAX +OPTION DIALECT MODERN +OPTION CPUSPEED MAX 010 REM***A PRIME NUMBER SIEVE BENCHMARK 020 T = TIMER 022 C = 0 diff --git a/presets/basic/startrader.bas b/presets/basic/startrader.bas index 847416cd..735d09d3 100644 --- a/presets/basic/startrader.bas +++ b/presets/basic/startrader.bas @@ -1,8 +1,8 @@ +OPTION DIALECT HP +OPTION BASE 0:REM I GUESS HP HAS ZERO BASE??? 1 REM***STAR TRADER FROM 2 REM***http://www.dunnington.info/public/basicgames/ 3 REM***2 chain files merged and ported to 8bitworkshop -5 OPTION DIALECT HP -6 OPTION BASE 0:REM I GUESS HP HAS ZERO BASE??? 10010 DIM S[12,15],T[12,12],T$[72],B[3,12] 10020 REM COM W,D9,K9,X9,D1,X1,P9,T9,S9,Y9,H 10030 DIM M[6,3],C[6,3]:REM COM Y1,R9,G9,Q diff --git a/presets/basic/wumpus.bas b/presets/basic/wumpus.bas index 73b0012c..4989cf19 100644 --- a/presets/basic/wumpus.bas +++ b/presets/basic/wumpus.bas @@ -1,6 +1,6 @@ +OPTION DIALECT HP 1 REM from: http://www.dunnington.info/public/basicgames/WUMPUS.hp 2 REM extracted from HP library tape -5 OPTION DIALECT HP 10 REM- HUNT THE WUMPUS 20 PRINT "INSTRUCTIONS (Y-N)"; 30 INPUT I$ diff --git a/src/codemirror/basic.js b/src/codemirror/basic.js index 6afb0445..d384f471 100644 --- a/src/codemirror/basic.js +++ b/src/codemirror/basic.js @@ -238,7 +238,6 @@ CodeMirror.defineMode("basic", function(conf, parserConf) { } var external = { - electricChars:"dDpPtTfFeE ", startState: function() { return { tokenize: tokenBase, @@ -246,8 +245,6 @@ CodeMirror.defineMode("basic", function(conf, parserConf) { currentIndent: 0, nextLineIndent: 0, doInCurrentLine: false - - }; }, diff --git a/src/common/basic/compiler.ts b/src/common/basic/compiler.ts index 38c5f15d..77b1f78c 100644 --- a/src/common/basic/compiler.ts +++ b/src/common/basic/compiler.ts @@ -11,6 +11,7 @@ export interface BASICOptions { squareBrackets : boolean; // "[" and "]" interchangable with "(" and ")"? tickComments : boolean; // support 'comments? hexOctalConsts : boolean; // support &H and &O integer constants? + optionalLet : boolean; // LET is optional chainAssignments : boolean; // support A = B = value (HP2000) validKeywords : string[]; // valid keywords (or null for accept all) validFunctions : string[]; // valid functions (or null for accept all) @@ -18,7 +19,7 @@ export interface BASICOptions { // VALUES AND OPERATORS defaultValues : boolean; // initialize unset variables to default value? (0 or "") stringConcat : boolean; // can concat strings with "+" operator? - typeConvert : boolean; // type convert strings <-> numbers? + typeConvert : boolean; // type convert strings <-> numbers? (NOT USED) checkOverflow : boolean; // check for overflow of numerics? bitwiseLogic : boolean; // -1 = TRUE, 0 = FALSE, AND/OR/NOT done with bitwise ops maxStringLength : number; // maximum string length in chars @@ -390,15 +391,17 @@ export class BASICParser { let tok = this.consumeToken(); switch (tok.type) { case TokenType.Ident: - if (this.opts.optionalLabels) { - if (this.peekToken().str == ':') { // is it a label: + if (this.opts.optionalLabels || tok.str == 'OPTION') { + // is it a "label :" and not a keyword like "PRINT : " + if (this.peekToken().str == ':' && !this.supportsCommand(tok.str)) { this.consumeToken(); // eat the ":" // fall through to the next case } else { this.pushbackToken(tok); // nope break; } - } else this.dialectError(`optional line numbers`); + } else + this.dialectError(`optional line numbers`); case TokenType.Int: this.setCurrentLabel(line, tok.str); break; @@ -406,9 +409,16 @@ export class BASICParser { case TokenType.Float: this.compileError(`Line numbers must be positive integers.`); break; + case TokenType.Operator: + if (this.supportsCommand(tok.str) && this.validKeyword(tok.str)) { + this.pushbackToken(tok); + break; // "?" is allowed + } default: - if (this.opts.optionalLabels) this.compileError(`A line must start with a line number, command, or label.`); - else this.compileError(`A line must start with a line number.`); + if (this.opts.optionalLabels) + this.compileError(`A line must start with a line number, command, or label.`); + else + this.compileError(`A line must start with a line number.`); case TokenType.Remark: break; } @@ -525,9 +535,9 @@ export class BASICParser { validKeyword(keyword: string) : string { return (this.opts.validKeywords && this.opts.validKeywords.indexOf(keyword) < 0) ? null : keyword; } - supportsKeyword(keyword: string) { - if (this['stmt__' + keyword] != null) return true; - return false; + supportsCommand(cmd: string) : () => Statement { + if (cmd == '?') return this.stmt__PRINT; + else return this['stmt__' + cmd]; } parseStatement(): Statement | null { // eat extra ":" (should have separate property for this) @@ -555,13 +565,15 @@ export class BASICParser { cmd = 'GOSUB'; } // lookup JS function for command - var fn = this['stmt__' + cmd]; + var fn = this.supportsCommand(cmd); if (fn) { if (this.validKeyword(cmd) == null) - this.dialectError(`the ${cmd} keyword`); - stmt = fn.bind(this)() as Statement; + this.dialectError(`the ${cmd} statement`); + stmt = fn.bind(this)(); break; } else if (this.peekToken().str == '=' || this.peekToken().str == '(') { + if (!this.opts.optionalLet) + this.dialectError(`assignments without a preceding LET`); // 'A = expr' or 'A(X) = expr' this.pushbackToken(cmdtok); stmt = this.stmt__LET(); @@ -631,23 +643,22 @@ export class BASICParser { if (this.opts.computedGoto) { // parse expression, but still add to list of label targets if constant var expr = this.parseExpr(); - if ((expr as Literal).value != null) { - this.targets[(expr as Literal).value] = this.lasttoken.$loc; - } + if (isLiteral(expr)) this.targets[expr.value] = this.lasttoken.$loc; return expr; - } - // parse a single number or ident label - var tok = this.consumeToken(); - switch (tok.type) { - case TokenType.Ident: - if (!this.opts.optionalLabels) this.dialectError(`labels other than line numbers`) - case TokenType.Int: - var label = tok.str; - this.targets[label] = tok.$loc; - return {value:label}; - default: - if (this.opts.optionalLabels) this.compileError(`There should be a line number or label here.`); - else this.compileError(`There should be a line number here.`); + } else { + // parse a single number or ident label + var tok = this.consumeToken(); + switch (tok.type) { + case TokenType.Ident: + if (!this.opts.optionalLabels) this.dialectError(`labels other than line numbers`) + case TokenType.Int: + var label = tok.str; + this.targets[label] = tok.$loc; + return {value:label}; + default: + var what = this.opts.optionalLabels ? "label or line number" : "line number"; + this.compileError(`There should be a ${what} here.`); + } } } parseDatumList(): Literal[] { @@ -841,13 +852,14 @@ export class BASICParser { __GO(cmd: "GOTO"|"GOSUB"): GOTO_Statement | GOSUB_Statement | ONGO_Statement { var expr = this.parseLabel(); // GOTO (expr) OF (labels...) - if (this.opts.computedGoto && this.peekToken().str == 'OF') { + if (this.peekToken().str == this.validKeyword('OF')) { this.expectToken('OF'); let newcmd : 'ONGOTO'|'ONGOSUB' = (cmd == 'GOTO') ? 'ONGOTO' : 'ONGOSUB'; - return { command:newcmd, expr:expr, labels:this.parseLabelList() }; + return { command: newcmd, expr: expr, labels: this.parseLabelList() }; + } else { + // regular GOTO or GOSUB + return { command: cmd, label: expr }; } - // regular GOTO or GOSUB - return { command: cmd, label: expr }; } stmt__IF(): IF_Statement { var cond = this.parseExpr(); @@ -1100,7 +1112,8 @@ export class BASICParser { checkLabels() { for (let targ in this.targets) { if (this.labels[targ] == null) { - this.addError(`There isn't a line number ${targ}.`, this.targets[targ]); + var what = this.opts.optionalLabels && isNaN(parseInt(targ)) ? "label named" : "line number"; + this.addError(`There isn't a ${what} ${targ}.`, this.targets[targ]); } } } @@ -1152,6 +1165,56 @@ export const ECMA55_MINIMAL : BASICOptions = { arraysContainChars : false, endStmtRequired : true, chainAssignments : false, + optionalLet : false, +} + +export const DARTMOUTH_4TH_EDITION : BASICOptions = { + dialectName: "DARTMOUTH4", + asciiOnly : true, + uppercaseOnly : true, + optionalLabels : false, + optionalWhitespace : false, + varNaming : "A1", + staticArrays : true, + sharedArrayNamespace : false, + defaultArrayBase : 0, + defaultArraySize : 11, + defaultValues : false, + stringConcat : false, + typeConvert : false, + maxDimensions : 2, + maxDefArgs : 255, + maxStringLength : 255, + tickComments : true, + hexOctalConsts : 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', + 'CHANGE','MAT','RANDOM','RESTORE$','RESTORE*', + ], + validFunctions : [ + 'ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN', + 'TRN','INV','DET','NUM','ZER', // NUM = # of strings input for MAT INPUT + ], + validOperators : [ + '=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^' + ], + printZoneLength : 15, + numericPadding : true, + checkOverflow : true, + testInitialFor : true, + optionalNextVar : false, + multipleNextVars : false, + bitwiseLogic : false, + checkOnGotoIndex : true, + computedGoto : false, + restoreWithLabel : false, + squareBrackets : false, + arraysContainChars : false, + endStmtRequired : true, + chainAssignments : true, + optionalLet : false, } // TODO: only integers supported @@ -1191,15 +1254,15 @@ export const TINY_BASIC : BASICOptions = { multipleNextVars : false, bitwiseLogic : false, checkOnGotoIndex : false, - computedGoto : true, // TODO: is it really though? + computedGoto : true, restoreWithLabel : false, squareBrackets : false, arraysContainChars : false, endStmtRequired : false, chainAssignments : false, + optionalLet : false, } - export const HP_TIMESHARED_BASIC : BASICOptions = { dialectName: "HP2000", asciiOnly : true, @@ -1242,12 +1305,13 @@ export const HP_TIMESHARED_BASIC : BASICOptions = { multipleNextVars : false, bitwiseLogic : false, checkOnGotoIndex : false, - computedGoto : true, + computedGoto : true, // not really, but we do parse expressions for GOTO ... OF restoreWithLabel : true, squareBrackets : true, arraysContainChars : true, endStmtRequired : true, chainAssignments : true, + optionalLet : true, // TODO: max line number, array index 9999 } @@ -1298,6 +1362,7 @@ export const DEC_BASIC_11 : BASICOptions = { arraysContainChars : false, endStmtRequired : false, chainAssignments : false, + optionalLet : true, // TODO: max line number 32767 // TODO: \ separator, % int vars and constants, 'single' quoted // TODO: can't compare strings and numbers @@ -1356,6 +1421,7 @@ export const DEC_BASIC_PLUS : BASICOptions = { arraysContainChars : false, endStmtRequired : false, chainAssignments : false, // TODO: can chain with "," not "=" + optionalLet : true, // TODO: max line number 32767 // TODO: \ separator, % int vars and constants, 'single' quoted // TODO: can't compare strings and numbers @@ -1408,6 +1474,7 @@ export const BASICODE : BASICOptions = { arraysContainChars : false, endStmtRequired : false, chainAssignments : false, + optionalLet : true, } export const ALTAIR_BASIC41 : BASICOptions = { @@ -1464,6 +1531,7 @@ export const ALTAIR_BASIC41 : BASICOptions = { arraysContainChars : false, endStmtRequired : false, chainAssignments : false, + optionalLet : true, } export const APPLESOFT_BASIC : BASICOptions = { @@ -1521,6 +1589,7 @@ export const APPLESOFT_BASIC : BASICOptions = { arraysContainChars : false, endStmtRequired : false, chainAssignments : false, + optionalLet : true, } export const BASIC80 : BASICOptions = { @@ -1579,6 +1648,7 @@ export const BASIC80 : BASICOptions = { arraysContainChars : false, endStmtRequired : false, chainAssignments : false, + optionalLet : true, } export const MODERN_BASIC : BASICOptions = { @@ -1594,7 +1664,7 @@ export const MODERN_BASIC : BASICOptions = { defaultArraySize : 0, // DIM required defaultValues : false, stringConcat : true, - typeConvert : true, + typeConvert : false, maxDimensions : 255, maxDefArgs : 255, maxStringLength : 2048, // TODO? @@ -1611,12 +1681,13 @@ export const MODERN_BASIC : BASICOptions = { multipleNextVars : true, bitwiseLogic : true, checkOnGotoIndex : true, - computedGoto : true, + computedGoto : false, restoreWithLabel : true, squareBrackets : true, arraysContainChars : false, endStmtRequired : false, - chainAssignments : false, + chainAssignments : true, + optionalLet : true, } // TODO: integer vars @@ -1624,7 +1695,9 @@ export const MODERN_BASIC : BASICOptions = { // TODO: excess INPUT ignored, error msg export const DIALECTS = { - "DEFAULT": ALTAIR_BASIC41, + "DEFAULT": MODERN_BASIC, + "DARTMOUTH": DARTMOUTH_4TH_EDITION, + "DARTMOUTH4": DARTMOUTH_4TH_EDITION, "ALTAIR": ALTAIR_BASIC41, "ALTAIR4": ALTAIR_BASIC41, "ALTAIR41": ALTAIR_BASIC41, diff --git a/src/common/basic/fuzz.ts b/src/common/basic/fuzz.ts new file mode 100644 index 00000000..fbb32806 --- /dev/null +++ b/src/common/basic/fuzz.ts @@ -0,0 +1,44 @@ + +import { BASICParser, DIALECTS, BASICOptions, CompileError } from "./compiler"; +import { BASICRuntime } from "./runtime"; +import { EmuHalt } from "../emu"; + +process.on('unhandledRejection', (reason, promise) => { + if (!(reason instanceof EmuHalt)) + console.log('Unhandled Rejection at:', promise, 'reason:', reason); +// Application specific logging, throwing an error, or other logic here +}); + +export function fuzz(buf) { + var parser = new BASICParser(); + var str = buf.toString(); + try { + var pgm = parser.parseFile(str, "test.bas"); + var runtime = new BASICRuntime(); + runtime.load(pgm); + runtime.reset(); + runtime.print = (s) => { + if (s == null) throw new Error("PRINT null string"); + } + runtime.input = function(prompt: string, nargs: number) : Promise { + var p = new Promise( (resolve, reject) => { + var arr = []; + for (var i=0; i console.log(`@@@ ${err.msg} (line ${err.label})`)); -if (parser.errors.length) process.exit(2); +if (parser.errors.length && !force) process.exit(2); // run program try { runtime.load(pgm); } catch (e) { - console.log(`### ${e.message} (line ${runtime.getCurrentSourceLocation().label})`); + console.log(`### ${e.message} (line ${getCurrentLabel()})`); process.exit(1); } runtime.reset(); @@ -86,7 +94,7 @@ runtime.resume = function() { process.exit(0); } } catch (e) { - console.log(`### ${e.message} (line ${runtime.getCurrentSourceLocation().label})`); + console.log(`### ${e.message} (line ${getCurrentLabel()})`); process.exit(1); } }); @@ -98,7 +106,7 @@ runtime.resume(); function dumpDialectInfo() { var dialects = new Set(); var array = {}; - var SELECTED_DIALECTS = ['TINY','ECMA55','HP','DEC','ALTAIR','BASIC80','MODERN']; + var SELECTED_DIALECTS = ['TINY','ECMA55','DARTMOUTH','HP','DEC','ALTAIR','BASIC80','MODERN']; SELECTED_DIALECTS.forEach((dkey) => { dialects.add(DIALECTS[dkey]); }); @@ -121,7 +129,7 @@ function dumpDialectInfo() { }); dialects.forEach((dialect) => { ALL_KEYWORDS.forEach((keyword) => { - if (parser.supportsKeyword(keyword)) { + if (parser.supportsCommand(keyword)) { var has = dialect.validKeywords == null || dialect.validKeywords.indexOf(keyword) >= 0; keyword = '`'+keyword+'`' if (!array[keyword]) array[keyword] = []; diff --git a/src/common/basic/runtime.ts b/src/common/basic/runtime.ts index 2c4f1608..1d42846b 100644 --- a/src/common/basic/runtime.ts +++ b/src/common/basic/runtime.ts @@ -473,7 +473,7 @@ export class BASICRuntime { this.runtimeError(`I can't call a function here.`); // is it a subscript? if (expr.args) { - // set array slice (HP BASIC) + // TODO: set array slice (HP BASIC) if (this.opts.arraysContainChars && expr.name.endsWith('$')) { this.runtimeError(`I can't set array slices via this command yet.`); } else { @@ -566,8 +566,8 @@ export class BASICRuntime { // converts a variable to string/number based on var name assign(name: string, right: number|string, isRead?:boolean) : number|string { // convert data? READ always converts if read into string - if (this.opts.typeConvert || (isRead && name.endsWith("$"))) - return this.convert(name, right); + if (isRead && name.endsWith("$")) + return this.checkValue(this.convert(name, right), name); // TODO: use options if (name.endsWith("$")) { return this.convertToString(right, name); @@ -604,9 +604,9 @@ export class BASICRuntime { if (this.opts.staticArrays) return; else this.runtimeError(`I already dimensioned this array (${name}) earlier.`) } - if (this.getTotalArrayLength(dims) > this.opts.maxArrayElements) { + var total = this.getTotalArrayLength(dims); + if (total > this.opts.maxArrayElements) this.runtimeError(`I can't create an array with this many elements.`); - } var isstring = name.endsWith('$'); // if numeric value, we use Float64Array which inits to 0 var arrcons = isstring ? Array : Float64Array; @@ -635,7 +635,7 @@ export class BASICRuntime { getArray(name: string, order: number) : [] { if (!this.arrays[name]) { if (this.opts.defaultArraySize == 0) - this.dialectError(`automatically declare arrays without a DIM statement`); + this.dialectError(`automatically declare arrays without a DIM statement (or did you mean to call a function?)`); if (order == 1) this.dimArray(name, this.opts.defaultArraySize-1); else if (order == 2) @@ -648,7 +648,7 @@ export class BASICRuntime { arrayGet(name: string, ...indices: number[]) : basic.Value { var arr = this.getArray(name, indices.length); - indices = indices.map(Math.round); + indices = indices.map(this.ROUND.bind(this)); var v = arr; for (var i=0; i this.expr2js(arg)).join(', '); - s += ')'; + s += ');'; } else { var ljs = this.assign2js(lexpr); s += `${ljs} = this.assign(${JSON.stringify(lexpr.name)}, _right);`; @@ -956,7 +967,7 @@ export class BASICRuntime { return exprname.endsWith("$") ? "" : 0; } if (exprname != null && obj == null) { - this.runtimeError(`I haven't set a value for ${exprname}.`); + this.runtimeError(`I haven't assigned a value to ${exprname}.`); } else if (exprname != null) { this.runtimeError(`I got an invalid value for ${exprname}: ${obj}`); } else { @@ -1193,7 +1204,7 @@ export class BASICRuntime { len = this.ROUND(len); if (len <= 0) return ''; if (len > this.opts.maxStringLength) - this.runtimeError(`I can't create a string longer than ${this.opts.maxStringLength} characters.`); + this.dialectError(`create a string longer than ${this.opts.maxStringLength} characters`); if (typeof chr === 'string') return chr.substr(0,1).repeat(len); else diff --git a/src/ide/ui.ts b/src/ide/ui.ts index b43be052..e3828240 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -1901,7 +1901,8 @@ function globalErrorHandler(msgevent) { if (msg.indexOf("QuotaExceededError") >= 0) { requestPersistPermission(false, false); } else { - var err = msgevent.error; + var err = msgevent.error || msgevent.reason; + msg = err.message || msg; showExceptionAsError(err, msg); } } @@ -1909,10 +1910,12 @@ function globalErrorHandler(msgevent) { // catch errors function installErrorHandler() { window.addEventListener('error', globalErrorHandler); + window.addEventListener('unhandledrejection', globalErrorHandler); } function uninstallErrorHandler() { window.removeEventListener('error', globalErrorHandler); + window.removeEventListener('unhandledrejection', globalErrorHandler); } function gotoNewLocation(replaceHistory? : boolean) { diff --git a/src/ide/views.ts b/src/ide/views.ts index bcf8d429..1b2a38ec 100644 --- a/src/ide/views.ts +++ b/src/ide/views.ts @@ -329,10 +329,12 @@ export class SourceEditor implements ProjectView { } setCurrentLine(line:SourceLocation, moveCursor:boolean) { + var blocked = platform.isBlocked && platform.isBlocked(); var addCurrentMarker = (line:SourceLocation) => { var div = document.createElement("div"); - div.classList.add('currentpc-marker'); + var cls = blocked ? 'currentpc-marker-blocked' : 'currentpc-marker'; + div.classList.add(cls); div.appendChild(document.createTextNode("\u25b6")); this.editor.setGutterMarker(line.line-1, "gutter-info", div); } @@ -343,7 +345,8 @@ export class SourceEditor implements ProjectView { if (moveCursor) { this.editor.setCursor({line:line.line-1,ch:line.start||0}, {scroll:true}); } - var markOpts = {className:'currentpc-span', inclusiveLeft:true}; + var cls = blocked ? 'currentpc-span-blocked' : 'currentpc-span'; + var markOpts = {className:cls, inclusiveLeft:true}; if (line.start || line.end) this.markCurrentPC = this.editor.markText({line:line.line-1,ch:line.start}, {line:line.line-1,ch:line.end||line.start+1}, markOpts); else @@ -379,7 +382,7 @@ export class SourceEditor implements ProjectView { refreshDebugState(moveCursor:boolean) { // TODO: only if line changed - // TODO: remove after compilation restarts platform + // TODO: remove after compilation this.clearCurrentLine(moveCursor); var line = this.getActiveLine(); if (line) { diff --git a/src/platform/basic.ts b/src/platform/basic.ts index 4d5cd00c..6b5f4b76 100644 --- a/src/platform/basic.ts +++ b/src/platform/basic.ts @@ -125,14 +125,14 @@ class BASICPlatform implements Platform { // TODO: only hot reload when we hit a label? var didExit = this.runtime.exited; this.program = data; - this.runtime.load(data); + var resumePC = this.runtime.load(data); this.tty.uppercaseOnly = true; // this.program.opts.uppercaseOnly; //TODO? // map editor to uppercase-only if need be views.textMapFunctions.input = this.program.opts.uppercaseOnly ? (s) => s.toUpperCase() : null; // HP 2000 has cute lil small caps (TODO: messes up grid alignment tho) //this.tty.page.style.fontVariant = (this.program.opts.dialectName == 'HP2000') ? 'small-caps' : 'normal'; // only reset if we exited, or couldn't restart at label (PC reset to 0) - if (!this.hotReload || didExit || this.runtime.curpc == 0) + if (!this.hotReload || didExit || !resumePC) this.reset(); } @@ -156,7 +156,7 @@ class BASICPlatform implements Platform { this.timer.start(); } - isBlocked() { return this.tty.waitingfor != null; } // is blocked for input? + isBlocked() { return this.tty.waitingfor != null || this.runtime.exited; } // is blocked for input? isRunning() { return this.timer.isRunning(); } getDefaultExtension() { return ".bas"; } getToolForFilename() { return "basic"; }