diff --git a/Makefile b/Makefile index 7745015d..013a063f 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,9 @@ dist: package: dist darwin.dist win32.dist linux.dist +meta/electron.diff: index.html electron.html + -diff -u index.html electron.html > $@ + web: (ip addr || ifconfig) | grep inet python3 scripts/serveit.py 2>> /dev/null #http.out diff --git a/electron.diff b/electron.diff new file mode 100644 index 00000000..8089334b --- /dev/null +++ b/electron.diff @@ -0,0 +1,257 @@ +--- index.html 2020-08-11 11:10:30.000000000 -0500 ++++ electron.html 2020-08-11 14:45:27.000000000 -0500 +@@ -3,16 +3,6 @@ + + + 8bitworkshop IDE +- +- +- +- +- +- +- +- +- +- + + + +- +- +- +- +- +- +- +- +- +- +- +- +- +- + + + +@@ -88,26 +38,6 @@ +
+
  • Add Include File...
  • +
  • Add Linked File...
  • +-
    +- +-
  • Request Local Storage Permissions
  • +- +- +- +
  • Break Expression...
  • + + +- +-
    +- +- + + + +@@ -258,39 +158,6 @@ + evals/clk + + +- +- +- +- +- + + + +@@ -477,73 +344,6 @@ + + + +- +- +- + + + +@@ -635,12 +435,5 @@ + startUI(); + + +- +- + + diff --git a/electron.html b/electron.html index a96e76f1..f194a955 100644 --- a/electron.html +++ b/electron.html @@ -96,6 +96,7 @@ body {
  • MSX (libCV)
  • Apple ][+
  • ZX Spectrum
  • +
  • x86 (FreeDOS)
  • Midway 8080
  • Galaxian/Scramble
  • Atari Color Vector (Z80)
  • +
  • Atari Color Vector (6502)
  • Williams (Z80)
  • Williams Sound (Z80)
  • @@ -116,26 +118,25 @@ body {
  • Verilog (VGA @ 25 Mhz)
  • - + Interpreters + + + diff --git a/meta/electron.diff b/meta/electron.diff index 80f94111..0981ebd1 100644 --- a/meta/electron.diff +++ b/meta/electron.diff @@ -1,266 +1,257 @@ -*** index.html 2020-07-30 21:15:48.000000000 -0500 ---- electron.html 2020-07-30 21:16:00.000000000 -0500 -*************** -*** 3,18 **** - - - 8bitworkshop IDE -- -- -- -- -- -- -- -- -- -- - - - -- -- -- -- -- -- -- -- -- -- -- -- -- -- - - - ---- 11,16 ---- -*************** if (window.location.host.endsWith('8bitw -*** 83,108 **** -
    -
  • Add Include File...
  • -
  • Add Linked File...
  • --
    -- --
  • Request Local Storage Permissions
  • -- -- -- -
  • Break Expression...
  • - - -- --
    -- -- - - - ---- 63,68 ---- -*************** if (window.location.host.endsWith('8bitw -*** 252,290 **** - evals/clk - - -- -- -- -- -- - - - ---- 157,162 ---- -*************** if (window.location.host.endsWith('8bitw -*** 471,543 **** - - - -- -- -- - - - ---- 343,348 ---- -*************** $( ".dropdown-submenu" ).click(function( -*** 629,640 **** - startUI(); - - -- -- - - ---- 434,438 ---- +--- index.html 2020-08-11 11:10:30.000000000 -0500 ++++ electron.html 2020-08-11 14:47:38.000000000 -0500 +@@ -3,16 +3,6 @@ + + + 8bitworkshop IDE +- +- +- +- +- +- +- +- +- +- + + + +- +- +- +- +- +- +- +- +- +- +- +- +- +- + + + +@@ -88,26 +38,6 @@ +
    +
  • Add Include File...
  • +
  • Add Linked File...
  • +-
    +- +-
  • Request Local Storage Permissions
  • +- +- +- +
  • Break Expression...
  • + + +- +-
    +- +- + + + +@@ -258,39 +158,6 @@ + evals/clk + + +- +- +- +- +- + + + +@@ -477,73 +344,6 @@ + + + +- +- +- + + + +@@ -635,12 +435,5 @@ + startUI(); + + +- +- + + diff --git a/presets/basic/hello.bas b/presets/basic/hello.bas index a1327ddd..e0787527 100644 --- a/presets/basic/hello.bas +++ b/presets/basic/hello.bas @@ -8,3 +8,4 @@ 45 PRINT 50 PRINT "THAT'S A GOOD ONE! I LIKE";N^2;"MYSELF." 60 PRINT "NICE MEETING YOU, ";A$;"." +999 END diff --git a/res/ttybell.mp3 b/res/ttybell.mp3 new file mode 100644 index 00000000..af13622e Binary files /dev/null and b/res/ttybell.mp3 differ diff --git a/src/codemirror/basic.js b/src/codemirror/basic.js index 10bedb99..0fbb55c7 100644 --- a/src/codemirror/basic.js +++ b/src/codemirror/basic.js @@ -14,8 +14,9 @@ CodeMirror.defineMode("basic", function(conf, parserConf) { var ERRORCLASS = 'error'; - function wordRegexp(words) { - return new RegExp("^((" + words.join(")|(") + "))\\b", "i"); + function wordRegexp(words, crunch) { + return new RegExp("^((" + words.join(")|(") + "))", "i"); + //return new RegExp("^((" + words.join(")|(") + "))\\b", "i"); } var singleOperators = new RegExp("^[\\+\\-\\*/%&\\\\|\\^~<>!]"); diff --git a/src/common/basic/compiler.ts b/src/common/basic/compiler.ts index 3699e435..f1da3642 100644 --- a/src/common/basic/compiler.ts +++ b/src/common/basic/compiler.ts @@ -1,10 +1,46 @@ import { WorkerError, CodeListingMap, SourceLocation, SourceLine } from "../workertypes"; +export interface BASICOptions { + dialectName : string; // use this to select the dialect + // SYNTAX AND PARSING + asciiOnly : boolean; // reject non-ASCII chars? + uppercaseOnly : boolean; // convert everything to uppercase? + optionalLabels : boolean; // can omit line numbers and use labels? + optionalWhitespace : boolean; // can "crunch" keywords? + varNaming : 'A1'|'AA'|'*'; // only allow A0-9 for numerics, single letter for arrays/strings + 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) + // 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? + 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 + maxDefArgs : number; // maximum # of arguments for user-defined functions + // ARRAYS + staticArrays : boolean; // can only DIM with constant value? (and never redim) + sharedArrayNamespace : boolean; // arrays and variables have same namespace? (TODO) + 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 + // PRINTING + printZoneLength : number; // print zone length + numericPadding : boolean; // " " or "-" before and " " after numbers? + // CONTROL FLOW + testInitialFor : boolean; // can we skip a NEXT statement? (can't interleave tho) + ifElse : boolean; // IF...ELSE construct + // MISC + commandsPerSec? : number; // how many commands per second? +} + export interface SourceLocated { $loc?: SourceLine; } -class CompileError extends Error { +export class CompileError extends Error { constructor(msg: string) { super(msg); Object.setPrototypeOf(this, CompileError.prototype); @@ -179,8 +215,10 @@ const OPERATORS = { 'IMP': {f:'bimp',p:4}, 'EQV': {f:'beqv',p:5}, 'XOR': {f:'bxor',p:6}, - 'OR': {f:'lor',p:7}, // or "bor" - 'AND': {f:'land',p:8}, // or "band" + 'OR': {f:'bor',p:7}, // or "lor" for logical + 'AND': {f:'band',p:8}, // or "land" for logical + '||': {f:'lor',p:17}, // not used + '&&': {f:'land',p:18}, // not used '=': {f:'eq',p:50}, '<>': {f:'ne',p:50}, '<': {f:'lt',p:50}, @@ -222,41 +260,10 @@ function stripQuotes(s: string) { return s.substr(1, s.length-2); } -// TODO: implement these -export interface BASICOptions { - dialectName : string; // use this to select the dialect - asciiOnly : boolean; // reject non-ASCII chars? - uppercaseOnly : boolean; // convert everything to uppercase? - optionalLabels : boolean; // can omit line numbers and use labels? - strictVarNames : boolean; // only allow A0-9 for numerics, single letter for arrays/strings - tickComments : boolean; // support 'comments? - defaultValues : boolean; // initialize unset variables to default value? (0 or "") - sharedArrayNamespace : boolean; // arrays and variables have same namespace? (conflict) - defaultArrayBase : number; // arrays start at this number (0 or 1) (TODO: check) - defaultArraySize : number; // arrays are allocated w/ this size (starting @ 0) - stringConcat : boolean; // can concat strings with "+" operator? - typeConvert : boolean; // type convert strings <-> numbers? (TODO) - checkOverflow : boolean; // check for overflow of numerics? - sparseArrays : boolean; // true == don't require DIM for arrays (TODO) - printZoneLength : number; // print zone length - numericPadding : boolean; // " " or "-" before and " " after numbers? - outOfOrderNext : boolean; // can we skip a NEXT statement? (can't interleave tho) - multipleNextVars : boolean; // NEXT Y,X (TODO) - ifElse : boolean; // IF...ELSE construct - bitwiseLogic : boolean; // -1 = TRUE, 0 = FALSE, AND/OR/NOT done with bitwise ops - maxDimensions : number; // max number of dimensions for arrays - maxDefArgs : number; // maximum # of arguments for user-defined functions - maxStringLength : number; // maximum string length in chars - 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) - commandsPerSec? : number; // how many commands per second? -} - ///// BASIC PARSER export class BASICParser { - opts : BASICOptions = ALTAIR_BASIC40; + opts : BASICOptions = ALTAIR_BASIC41; errors: WorkerError[]; listings: CodeListingMap; labels: { [label: string]: BASICLine }; @@ -283,8 +290,7 @@ export class BASICParser { } compileError(msg: string, loc?: SourceLocation) { if (!loc) loc = this.peekToken().$loc; - // TODO: pass SourceLocation to errors - this.errors.push({path:loc.path, line:loc.line, label:loc.label, msg:msg}); + this.errors.push({path:loc.path, line:loc.line, label:loc.label, start:loc.start, end:loc.end, msg:msg}); throw new CompileError(`${msg} (line ${loc.line})`); // TODO: label too? } dialectError(what: string, loc?: SourceLocation) { @@ -358,23 +364,33 @@ export class BASICParser { tokenize(line: string) : void { this.lineno++; this.tokens = []; + let splitre = this.opts.optionalWhitespace && new RegExp(this.opts.validKeywords.map(s => `^${s}`).join('|')); var m : RegExpMatchArray; while (m = re_toks.exec(line)) { for (var i = 1; i < TokenType._LAST; i++) { let s : string = m[i]; if (s != null) { + let loc = { path: this.path, line: this.lineno, start: m.index, end: m.index+s.length, label: this.curlabel }; // maybe we don't support unicode in 1975? if (this.opts.asciiOnly && !/^[\x00-\x7F]*$/.test(s)) this.dialectError(`non-ASCII characters`); // uppercase all identifiers, and maybe more if (i == TokenType.Ident || this.opts.uppercaseOnly) s = s.toUpperCase(); + // un-crunch tokens? + if (splitre) { + while (i == TokenType.Ident) { + let m2 = splitre.exec(s); + if (m2 && s.length > m2[0].length) { + this.tokens.push({str:m2[0], type:TokenType.Ident, $loc:loc}); + s = s.substring(m2[0].length); + } else + break; + } + if (/^[0-9]+$/.test(s)) i = TokenType.Int; + } // add token to list - this.tokens.push({ - str: s, - type: i, - $loc: { path: this.path, line: this.lineno, start: m.index, end: m.index+s.length, label: this.curlabel } - }); + this.tokens.push({str: s, type: i, $loc:loc}); break; } } @@ -473,6 +489,12 @@ export class BASICParser { this.validateVarName(lexpr); return lexpr; } + parseForNextLexpr() : IndOp { + var lexpr = this.parseLexpr(); + if (lexpr.args || lexpr.name.endsWith('$')) + this.compileError(`A FOR ... NEXT loop can only use numeric variables.`); + return lexpr; + } parseList(parseFunc:()=>T, delim:string): T[] { var sep; var list = []; @@ -564,8 +586,9 @@ export class BASICParser { look = this.peekToken(); } var opfn = getOperator(op.str).f; - if (this.opts.bitwiseLogic && opfn == 'land') opfn = 'band'; - if (this.opts.bitwiseLogic && opfn == 'lor') opfn = 'bor'; + // use logical operators instead of bitwise? + if (!this.opts.bitwiseLogic && op.str == 'AND') opfn = 'land'; + if (!this.opts.bitwiseLogic && op.str == 'OR') opfn = 'lor'; left = { op:opfn, left: left, right: right }; } return left; @@ -574,11 +597,17 @@ export class BASICParser { return this.parseExpr1(this.parsePrimary(), 0); } validateVarName(lexpr: IndOp) { - if (this.opts.strictVarNames) { - if (lexpr.args == null && !/^[A-Z][0-9]?[$]?$/.test(lexpr.name)) - this.dialectError(`variable names other than a letter followed by an optional digit`); - if (lexpr.args != null && !/^[A-Z]?[$]?$/.test(lexpr.name)) - this.dialectError(`array names other than a single letter`); + switch (this.opts.varNaming) { + case 'A1': + if (lexpr.args == null && !/^[A-Z][0-9]?[$]?$/i.test(lexpr.name)) + this.dialectError(`variable names other than a letter followed by an optional digit`); + if (lexpr.args != null && !/^[A-Z]?[$]?$/i.test(lexpr.name)) + this.dialectError(`array names other than a single letter`); + break; + case 'AA': + if (lexpr.args == null && !/^[A-Z][A-Z0-9]?[$]?$/i.test(lexpr.name)) + this.dialectError(`variable names other than a letter followed by an optional letter or digit`); + break; } } @@ -648,7 +677,7 @@ export class BASICParser { return { command: "ELSE" }; } stmt__FOR() : FOR_Statement { - var lexpr = this.parseLexpr(); // TODO: parseNumVar() + var lexpr = this.parseForNextLexpr(); this.expectToken('='); var init = this.parseExpr(); this.expectToken('TO'); @@ -662,7 +691,7 @@ export class BASICParser { stmt__NEXT() : NEXT_Statement { var lexpr = null; if (!isEOS(this.peekToken())) { - lexpr = this.parseExpr(); + lexpr = this.parseForNextLexpr(); } return { command:'NEXT', lexpr:lexpr }; } @@ -770,6 +799,18 @@ export class BASICParser { this.compileError(`OPTION CPUSPEED takes a positive number or MAX.`); break; default: + // maybe it's one of the options? + var name = Object.getOwnPropertyNames(this.opts).find((n) => n.toUpperCase() == stmt.optname); + if (name != null) switch (typeof this.opts[name]) { + case 'boolean' : this.opts[name] = arg ? true : false; return; + case 'number' : this.opts[name] = parseFloat(arg); return; + case 'string' : this.opts[name] = arg; return; + case 'object' : + if (Array.isArray(this.opts[name]) && arg == 'ALL') { + this.opts[name] = null; + return; + } + } this.compileError(`OPTION ${stmt.optname} is not supported by this compiler.`); break; } @@ -814,13 +855,15 @@ export class BASICParser { ///// BASIC DIALECTS -// TODO: require END statement, check FOR condition at start of loop +// TODO: require END statement export const ECMA55_MINIMAL : BASICOptions = { dialectName: "ECMA55", asciiOnly : true, uppercaseOnly : true, optionalLabels : false, - strictVarNames : true, + optionalWhitespace : false, + varNaming : "A1", + staticArrays : true, sharedArrayNamespace : true, defaultArrayBase : 0, defaultArraySize : 11, @@ -830,29 +873,62 @@ export const ECMA55_MINIMAL : BASICOptions = { maxDimensions : 2, maxDefArgs : 255, maxStringLength : 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' - ], // TODO: no ON...GOSUB + ], validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN'], validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'], printZoneLength : 15, numericPadding : true, checkOverflow : true, - outOfOrderNext : false, - multipleNextVars : false, + testInitialFor : true, ifElse : false, bitwiseLogic : false, } -export const ALTAIR_BASIC40 : BASICOptions = { - dialectName: "ALTAIR40", +export const BASICODE : BASICOptions = { + dialectName: "BASICODE", asciiOnly : true, uppercaseOnly : true, optionalLabels : false, - strictVarNames : false, + optionalWhitespace : true, + varNaming : "AA", + staticArrays : true, + sharedArrayNamespace : false, + defaultArrayBase : 0, + defaultArraySize : 11, + defaultValues : false, + stringConcat : true, + typeConvert : false, + maxDimensions : 2, + maxDefArgs : 255, + maxStringLength : 255, + tickComments : false, + validKeywords : ['BASE','DATA','DEF','DIM','END', + 'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT', + 'READ','REM','RESTORE','RETURN','STEP','STOP','SUB','THEN','TO' + ], + validFunctions : ['ABS','ASC','ATN','CHR$','COS','EXP','INT','LEFT$','LEN','LOG', + 'MID$','RIGHT$','SGN','SIN','SQR','TAB','TAN','VAL'], + validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^', 'AND', 'NOT', 'OR'], + printZoneLength : 15, + numericPadding : true, + checkOverflow : true, + testInitialFor : true, + ifElse : false, + bitwiseLogic : false, +} + +export const ALTAIR_BASIC41 : BASICOptions = { + dialectName: "ALTAIR41", + asciiOnly : true, + uppercaseOnly : true, + optionalLabels : false, + optionalWhitespace : true, + varNaming : "*", // or AA + staticArrays : false, sharedArrayNamespace : true, defaultArrayBase : 0, defaultArraySize : 11, @@ -862,16 +938,21 @@ export const ALTAIR_BASIC40 : BASICOptions = { maxDimensions : 128, // "as many as will fit on a single line" ... ? maxDefArgs : 255, maxStringLength : 255, - sparseArrays : false, tickComments : false, - validKeywords : null, // all + validKeywords : [ + 'OPTION', + 'CONSOLE','DATA','DEF','DEFUSR','DIM','END','ERASE','ERROR', + 'FOR','GOTO','GOSUB','IF','THEN','ELSE','INPUT','LET','LINE', + 'PRINT','LPRINT','USING','NEXT','ON','OUT','POKE', + 'READ','REM','RESTORE','RESUME','RETURN','STOP','SWAP', + 'TROFF','TRON','WAIT'], validFunctions : null, // all validOperators : null, // all printZoneLength : 15, numericPadding : true, checkOverflow : true, - outOfOrderNext : true, - multipleNextVars : true, // TODO: not supported + testInitialFor : false, + //multipleNextVars : true, // TODO: not supported ifElse : true, bitwiseLogic : true, } @@ -881,21 +962,22 @@ export const APPLESOFT_BASIC : BASICOptions = { asciiOnly : true, uppercaseOnly : false, optionalLabels : false, - strictVarNames : false, // TODO: first two alphanum chars + optionalWhitespace : true, + varNaming : "*", // or AA + staticArrays : false, sharedArrayNamespace : false, defaultArrayBase : 0, - defaultArraySize : 9, // A(0) to A(8) + defaultArraySize : 11, defaultValues : true, stringConcat : true, typeConvert : false, maxDimensions : 88, maxDefArgs : 1, // TODO: no string FNs maxStringLength : 255, - sparseArrays : false, tickComments : false, validKeywords : [ 'OPTION', - 'CLEAR','LET','DIM','DEF','FN','GOTO','GOSUB','RETURN','ON','POP', + 'CLEAR','LET','DIM','DEF','GOTO','GOSUB','RETURN','ON','POP', 'FOR','TO','NEXT','IF','THEN','END','STOP','ONERR','RESUME', 'PRINT','INPUT','GET','HOME','HTAB','VTAB', 'INVERSE','FLASH','NORMAL','TEXT', @@ -911,8 +993,7 @@ export const APPLESOFT_BASIC : BASICOptions = { printZoneLength : 16, numericPadding : false, checkOverflow : true, - outOfOrderNext : true, - multipleNextVars : false, + testInitialFor : false, ifElse : false, bitwiseLogic : false, } @@ -922,39 +1003,40 @@ export const MODERN_BASIC : BASICOptions = { asciiOnly : false, uppercaseOnly : false, optionalLabels : true, - strictVarNames : false, + optionalWhitespace : false, + varNaming : "*", + staticArrays : false, sharedArrayNamespace : false, defaultArrayBase : 0, - defaultArraySize : 11, + defaultArraySize : 0, // DIM required defaultValues : false, stringConcat : true, typeConvert : true, maxDimensions : 255, maxDefArgs : 255, - maxStringLength : 1024, // TODO? - sparseArrays : false, + maxStringLength : 2048, // TODO? tickComments : true, validKeywords : null, // all validFunctions : null, // all validOperators : null, // all - printZoneLength : 15, + printZoneLength : 16, numericPadding : false, checkOverflow : true, - outOfOrderNext : true, - multipleNextVars : true, + testInitialFor : true, ifElse : true, bitwiseLogic : true, } // TODO: integer vars -// TODO: short-circuit FOR loop +// TODO: DEFINT/DEFSTR export const DIALECTS = { - "DEFAULT": ALTAIR_BASIC40, - "ALTAIR": ALTAIR_BASIC40, - "ALTAIR40": ALTAIR_BASIC40, + "DEFAULT": ALTAIR_BASIC41, + "ALTAIR": ALTAIR_BASIC41, + "ALTAIR41": ALTAIR_BASIC41, "ECMA55": ECMA55_MINIMAL, "MINIMAL": ECMA55_MINIMAL, + "BASICODE": BASICODE, "APPLESOFT": APPLESOFT_BASIC, "MODERN": MODERN_BASIC, }; diff --git a/src/common/basic/runtime.ts b/src/common/basic/runtime.ts index e01bbb4f..0aa7491e 100644 --- a/src/common/basic/runtime.ts +++ b/src/common/basic/runtime.ts @@ -48,7 +48,8 @@ export class BASICRuntime { vars : {}; arrays : {}; defs : {}; - forLoops : {varname:string, next:(name:string) => void}[]; + forLoops : { [varname:string] : { $next:(name:string) => void, inner:string } }; + topForLoopName : string; returnStack : number[]; column : number; @@ -70,23 +71,26 @@ 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); 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, {isconst:true}); - var value = new Function(`return ${functext};`).bind(this)(); - this.datums.push({value:value}); - }); + }); + // compile statements ahead of time + this.allstmts.forEach((stmt, pc) => { + this.curpc = pc + 1; // for error reporting + this.compileStatement(stmt); + }); + // parse DATA literals + this.allstmts.filter((stmt) => stmt.command == 'DATA').forEach((datastmt) => { + (datastmt as basic.DATA_Statement).datums.forEach(d => { + //this.curpc = datastmt.$loc.offset; // for error reporting + var functext = this.expr2js(d, {isconst:true}); + var value = new Function(`return ${functext};`).bind(this)(); + this.datums.push({value:value}); }); - // compile statements ahead of time - line.stmts.forEach((stmt) => this.compileStatement(stmt)); }); // try to resume where we left off after loading this.curpc = this.label2pc[prevlabel] || 0; @@ -97,7 +101,6 @@ export class BASICRuntime { this.curpc = 0; this.dataptr = 0; this.clearVars(); - this.forLoops = []; this.returnStack = []; this.column = 0; this.running = true; @@ -107,6 +110,14 @@ export class BASICRuntime { this.vars = {}; this.arrays = {}; this.defs = {}; // TODO? only in interpreters + this.forLoops = {}; + this.topForLoopName = null; + // initialize arrays? + if (this.opts && this.opts.staticArrays) { + this.allstmts.filter((stmt) => stmt.command == 'DIM').forEach((dimstmt: basic.DIM_Statement) => { + dimstmt.args.forEach( (arg) => this.compileJS(this._DIM(arg))() ); + }); + } } getBuiltinFunctions() { @@ -175,13 +186,16 @@ export class BASICRuntime { 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); + stmt.$run = this.compileJS(functext); } catch (e) { console.log(functext); throw e; } } } + compileJS(functext: string) : () => void { + return new Function(functext).bind(this); + } executeStatement(stmt: basic.Statement & CompiledStatement) { // compile (unless cached) this.compileStatement(stmt); @@ -212,6 +226,22 @@ export class BASICRuntime { this.curpc = this.allstmts.length; } + skipToAfterNext(forname: string) : void { + var pc = this.curpc; + while (pc < this.allstmts.length) { + var stmt = this.allstmts[pc]; + if (stmt.command == 'NEXT') { + var nextlexpr = (stmt as basic.NEXT_Statement).lexpr; + if (nextlexpr && nextlexpr.name == forname) { + this.curpc = pc + 1; + return; + } + } + pc++; + } + this.runtimeError(`I couldn't find a matching NEXT ${forname} to skip this for loop.`); + } + gotoLabel(label) { var pc = this.label2pc[label]; if (pc >= 0) { @@ -300,9 +330,11 @@ export class BASICRuntime { if (!expr.args && 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.`); // TODO: check at compile-time? var s = ''; var qname = JSON.stringify(expr.name); - let jsargs = expr.args && expr.args.map((arg) => this.expr2js(arg, opts)).join(', '); + let jsargs = expr.args ? expr.args.map((arg) => this.expr2js(arg, opts)).join(', ') : []; if (expr.name.startsWith("FN")) { // is it a user-defined function? // TODO: check argument count? s += `this.getDef(${qname})(${jsargs})`; @@ -347,6 +379,8 @@ export class BASICRuntime { checkFuncArgs(expr: basic.IndOp, fn: Function) { // TODO: check types? var nargs = expr.args ? expr.args.length : 0; + // exceptions + if (expr.name == 'RND' && nargs == 0) return; if (expr.name == 'MID$' && nargs == 2) return; if (expr.name == 'INSTR' && nargs == 2) return; if (fn.length != nargs) @@ -354,40 +388,44 @@ export class BASICRuntime { } startForLoop(forname, init, targ, step) { - // TODO: support 0-iteration loops var pc = this.curpc; if (!step) step = 1; this.vars[forname] = init; if (this.trace) console.log(`FOR ${forname} = ${init} TO ${targ} STEP ${step}`); - this.forLoops.unshift({ - varname: forname, - next: (nextname:string) => { + // create done function + var loopdone = () => { + return step >= 0 ? this.vars[forname] > targ : this.vars[forname] < targ; + } + // skip entire for loop before first iteration? (Minimal BASIC) + if (this.opts.testInitialFor && loopdone()) + return this.skipToAfterNext(forname); + // save for var name on top of linked-list stack + var inner = this.topForLoopName; + this.topForLoopName = forname; + // create for loop record + this.forLoops[forname] = { + inner: inner, + $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; + var done = loopdone(); if (done) { - this.forLoops.shift(); // pop FOR off the stack and continue + // pop FOR off the stack and continue + this.topForLoopName = inner; + delete this.forLoops[forname]; } else { this.curpc = pc; // go back to FOR location } if (this.trace) console.log(`NEXT ${forname}: ${this.vars[forname]} TO ${targ} STEP ${step} DONE=${done}`); } - }); + }; } nextForLoop(name) { - var fl = this.forLoops[0]; - if (fl != null && name != null && fl.varname != name) { - if (!this.opts.outOfOrderNext) this.dialectError(`execute out-of-order NEXT statements`); - while (fl) { - if (fl.varname == name) break; - this.forLoops.shift(); - fl = this.forLoops[0]; - } - } + var fl = this.forLoops[name]; if (!fl) this.runtimeError(`I couldn't find a FOR for this NEXT.`) - fl.next(name); + fl.$next(name); } // converts a variable to string/number based on var name @@ -425,11 +463,13 @@ export class BASICRuntime { // dimension array dimArray(name: string, ...dims:number[]) { // TODO: maybe do this check at compile-time? - if (this.arrays[name]) this.runtimeError(`I already dimensioned this array (${name}) earlier.`) + if (this.arrays[name] != null) { + if (this.opts.staticArrays) return; + else this.runtimeError(`I already dimensioned this array (${name}) earlier.`) + } var isstring = name.endsWith('$'); - // if defaultValues is true, we use Float64Array which inits to 0 - var arrcons = isstring || !this.opts.defaultValues ? Array : Float64Array; - // TODO? var ab = this.opts.defaultArrayBase; + // if numeric value, we use Float64Array which inits to 0 + var arrcons = isstring ? Array : Float64Array; if (dims.length == 1) { this.arrays[name] = new arrcons(dims[0]+1); } else if (dims.length == 2) { @@ -444,10 +484,12 @@ 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`); if (order == 1) - this.dimArray(name, this.opts.defaultArraySize); + this.dimArray(name, this.opts.defaultArraySize-1); else if (order == 2) - this.dimArray(name, this.opts.defaultArraySize, this.opts.defaultArraySize); + this.dimArray(name, this.opts.defaultArraySize-1, this.opts.defaultArraySize-1); else this.runtimeError(`I only support arrays of one or two dimensions.`); // TODO } @@ -464,7 +506,7 @@ export class BASICRuntime { this.runtimeError(`I tried to lookup ${name}(${indices}) but used too many dimensions.`); if (idx < this.opts.defaultArrayBase) this.runtimeError(`I tried to lookup ${name}(${indices}) but an index was less than ${this.opts.defaultArrayBase}.`); - if (idx >= v.length) + if (idx >= v.length) // TODO: also can happen when mispelling function name this.runtimeError(`I tried to lookup ${name}(${indices}) but it exceeded the dimensions of the array.`); v = v[indices[i]]; } @@ -572,12 +614,13 @@ export class BASICRuntime { var argsstr = ''; for (var arg of dim.args) { // TODO: check for float (or at compile time) - argsstr += ', ' + this.expr2js(arg); + argsstr += ', ' + this.expr2js(arg, {isconst: this.opts.staticArrays}); } return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`; } do__DIM(stmt : basic.DIM_Statement) { + if (this.opts.staticArrays) return; // DIM at reset() var s = ''; stmt.args.forEach((dim) => s += this._DIM(dim)); return s; diff --git a/src/common/teletype.ts b/src/common/teletype.ts index 0512b305..5139186f 100644 --- a/src/common/teletype.ts +++ b/src/common/teletype.ts @@ -2,7 +2,9 @@ export class TeleType { page: HTMLElement; fixed: boolean; + ncols: number = 80; scrolldiv: HTMLElement; + bell; // Audio curline: HTMLElement; curstyle: number; @@ -52,6 +54,17 @@ export class TeleType { this.addtext(line[i], style); return; } + // process control codes + if (line.length == 1) { + var ch = line.charCodeAt(0); + switch (ch) { + case 7: if (this.bell) this.bell.play(); break; + case 8: if (this.col > 0) this.col--; break; + case 12: this.formfeed(); break; + case 13: this.col = 0; break; + } + if (ch < 32) return; // ignore non-printables + } var span = $("").text(line); for (var i = 0; i < 8; i++) { if (style & (1 << i)) @@ -66,7 +79,8 @@ export class TeleType { span.appendTo(this.curline); } this.col += line.length; - // TODO: wrap @ 80 columns + // wrap @ 80 columns (TODO: const) + if (this.fixed && this.col >= this.ncols) this.newline(); this.ncharsout += line.length; this.movePrintHead(true); } @@ -117,7 +131,7 @@ export class TeleType { } } formfeed() { - this.newline(); + for (var i=0; i<60; i++) { this.newline(); this.ensureline(); } } scrollToBottom() { this.curline.scrollIntoView(); diff --git a/src/ide/ui.ts b/src/ide/ui.ts index 84149b97..7657637a 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -1098,6 +1098,17 @@ function showErrorAlert(errors : WorkerError[]) { $("#error_alert").show(); } +function showExceptionAsError(err, msg:string) { + var werr : WorkerError = {msg:msg, line:0}; + if (err instanceof EmuHalt && err.$loc) { + werr = Object.create(err.$loc); + werr.msg = msg; + console.log(werr); + projectWindows.refresh(false); + } + showErrorAlert([werr]); +} + var measureTimeStart : Date = new Date(); var measureTimeLoad : Date; function measureBuildTime() { @@ -1133,7 +1144,7 @@ function setCompileOutput(data: WorkerResult) { } catch (e) { console.log(e); toolbar.addClass("has-errors"); - showErrorAlert([{msg:e+"",line:0}]); + showExceptionAsError(e, e+""); current_output = null; return; } @@ -1885,14 +1896,7 @@ function globalErrorHandler(msgevent) { requestPersistPermission(false, false); } else { var err = msgevent.error; - var werr : WorkerError = {msg:msg, line:0}; - if (err instanceof EmuHalt && err.$loc) { - werr = Object.create(err.$loc); - werr.msg = msg; - console.log(werr); - projectWindows.refresh(false); - } - showErrorAlert([werr]); + showExceptionAsError(err, msg); } } diff --git a/src/ide/views.ts b/src/ide/views.ts index 28917f8d..bcf8d429 100644 --- a/src/ide/views.ts +++ b/src/ide/views.ts @@ -379,6 +379,7 @@ export class SourceEditor implements ProjectView { refreshDebugState(moveCursor:boolean) { // TODO: only if line changed + // TODO: remove after compilation restarts platform this.clearCurrentLine(moveCursor); var line = this.getActiveLine(); if (line) { diff --git a/src/platform/basic.ts b/src/platform/basic.ts index b9c5955f..fcb661a7 100644 --- a/src/platform/basic.ts +++ b/src/platform/basic.ts @@ -48,6 +48,7 @@ class BASICPlatform implements Platform { //var printshield = $('
    ').appendTo(parent); this.tty = new TeleTypeWithKeyboard(windowport[0], true, inputline[0] as HTMLInputElement); this.tty.scrolldiv = parent; + this.tty.bell = new Audio('res/ttybell.mp3'); this.runtime.input = async (prompt:string, nargs:number) => { return new Promise( (resolve, reject) => { if (prompt != null) {