diff --git a/css/ui.css b/css/ui.css index 131f06e9..ab59397a 100644 --- a/css/ui.css +++ b/css/ui.css @@ -353,7 +353,7 @@ div.replaydiv { background-repeat: no-repeat; background-position: 50%; pointer-events:auto; - z-index:2; + z-index:4; } .gutter.gutter-horizontal { background-image: url('grips/vertical.png'); @@ -424,7 +424,7 @@ div.markdown th { user-select: auto; } .alert { - z-index:2; + z-index:8; } .segment { border: 2px solid rgba(0,0,0,0.2); @@ -637,7 +637,20 @@ div.asset_toolbar { bottom: 0; height: 3em; width: 100%; - z-index: 1; + z-index: 6; + pointer-events: none; +} +.transcript-print-head.printing { + height: 5em; +} +.transcript-print-shield { + background: '#ffffff'; + background: linear-gradient(0deg, rgba(0,0,0,0.5) 0%, rgba(255,255,255,0) 56%, rgba(0,0,0,0) 100%); + position: absolute; + bottom: 0; + height: 3em; + width: 100%; + z-index: 5; pointer-events: none; } .tree-header { diff --git a/src/common/basic/compiler.ts b/src/common/basic/compiler.ts index 382850eb..4b1eacb6 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+)|(['].*)|(\w+[$]?)|(".*?")|([<>]?[=<>])|([-+*/^,;:()])|(\S+)/gi; +const re_toks = /([0-9.]+[E][+-]?\d+)|(\d*[.]\d*[E0-9]*)|[0]*(\d+)|(['].*)|(\w+[$]?)|(".*?")|([<>]?[=<>])|([-+*/^,;:()?])|(\S+)/gi; export enum TokenType { EOL = 0, @@ -68,6 +68,11 @@ export interface LET_Statement { right: Expr; } +export interface DIM_Statement { + command: "DIM"; + args: IndOp[]; +} + export interface GOTO_Statement { command: "GOTO"; label: Expr; @@ -82,11 +87,21 @@ export interface RETURN_Statement { command: "RETURN"; } +export interface ONGOTO_Statement { + command: "ONGOTO"; + expr: Expr; + labels: Expr[]; +} + export interface IF_Statement { command: "IF"; cond: Expr; } +export interface ELSE_Statement { + command: "ELSE"; +} + export interface FOR_Statement { command: "FOR"; lexpr: IndOp; @@ -100,19 +115,19 @@ export interface NEXT_Statement { lexpr?: IndOp; } -export interface DIM_Statement { - command: "DIM"; - args: IndOp[]; -} - export interface INPUT_Statement { command: "INPUT"; prompt: Expr; args: IndOp[]; } +export interface DATA_Statement { + command: "DATA"; + datums: Expr[]; +} + export interface READ_Statement { - command: "INPUT"; + command: "READ"; args: IndOp[]; } @@ -122,25 +137,25 @@ export interface DEF_Statement { def: Expr; } -export interface ONGOTO_Statement { - command: "ONGOTO"; - expr: Expr; - labels: Expr[]; -} - -export interface DATA_Statement { - command: "DATA"; - datums: Expr[]; -} - export interface OPTION_Statement { command: "OPTION"; optname: string; optargs: string[]; } +export interface GET_Statement { + command: "GET"; + lexpr: IndOp; +} + +export interface NoArgStatement { + command: string; +} + export type StatementTypes = PRINT_Statement | LET_Statement | GOTO_Statement | GOSUB_Statement - | IF_Statement | FOR_Statement | DATA_Statement; + | IF_Statement | FOR_Statement | NEXT_Statement | DIM_Statement + | INPUT_Statement | READ_Statement | DEF_Statement | ONGOTO_Statement + | DATA_Statement | OPTION_Statement | NoArgStatement; export type Statement = StatementTypes & SourceLocated; @@ -195,7 +210,8 @@ function getPrecedence(tok: Token): number { // is token an end of statement marker? (":" or end of line) function isEOS(tok: Token) { - return (tok.type == TokenType.EOL) || (tok.type == TokenType.Operator && tok.str == ':'); + return tok.type == TokenType.EOL || tok.type == TokenType.Remark + || tok.str == ':' || tok.str == 'ELSE'; // TODO: only ELSE if ifElse==true } function stripQuotes(s: string) { @@ -205,32 +221,36 @@ function stripQuotes(s: string) { // 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? + optionalLabels : boolean; // can omit line numbers and use labels? strictVarNames : boolean; // only allow A0-9 for numerics, single letter for arrays/strings sharedArrayNamespace : boolean; // arrays and variables have same namespace? (conflict) defaultArrayBase : number; // arrays start at this number (0 or 1) defaultArraySize : number; // arrays are allocated w/ this size (starting @ 0) maxDimensions : number; // max number of dimensions for arrays stringConcat : boolean; // can concat strings with "+" operator? - typeConvert : boolean; // type convert strings <-> numbers? - maxArguments : number; // maximum # of arguments for user-defined functions - sparseArrays : boolean; // true == don't require DIM for arrays + typeConvert : boolean; // type convert strings <-> numbers? (TODO) + maxDefArgs : number; // maximum # of arguments for user-defined functions + maxStringLength : number; // maximum string length in chars + sparseArrays : boolean; // true == don't require DIM for arrays (TODO) tickComments : boolean; // support 'comments? validKeywords : string[]; // valid keywords (or null for accept all) validFunctions : string[]; // valid functions (or null for accept all) validOperators : string[]; // valid operators (or null for accept all) printZoneLength : number; // print zone length - printPrecision : number; // print precision # of digits + numericPadding : boolean; // " " or "-" before and " " after numbers? checkOverflow : boolean; // check for overflow of numerics? defaultValues : boolean; // initialize unset variables to default value? (0 or "") - multipleNextVars : boolean; // NEXT Y,X + multipleNextVars : boolean; // NEXT Y,X (TODO) + ifElse : boolean; // IF...ELSE construct } ///// BASIC PARSER export class BASICParser { - opts : BASICOptions = ALTAIR_BASIC40; + opts : BASICOptions = MAX8_BASIC; errors: WorkerError[]; listings: CodeListingMap; labels: { [label: string]: BASICLine }; @@ -285,6 +305,16 @@ export class BASICParser { parseOptLabel(line: BASICLine) { let tok = this.consumeToken(); switch (tok.type) { + case TokenType.Ident: + if (this.opts.optionalLabels) { + if (this.peekToken().str == ':') { // is it a label: + this.consumeToken(); // eat the ":" + // fall through to the next case + } else { + this.pushbackToken(tok); // nope + break; + } + } else this.dialectError(`optional line numbers`); case TokenType.Int: if (this.labels[tok.str] != null) this.compileError(`There's a duplicated label "${tok.str}".`); this.labels[tok.str] = line; @@ -296,8 +326,8 @@ export class BASICParser { this.compileError(`Line numbers must be positive integers.`); break; default: - if (this.opts.optionalLabels) this.pushbackToken(tok); - else this.dialectError(`optional line numbers`); + 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.`); break; } } @@ -325,6 +355,9 @@ export class BASICParser { for (var i = 1; i < TokenType._LAST; i++) { let s : string = m[i]; if (s != null) { + // 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(); @@ -354,8 +387,16 @@ export class BASICParser { } parseCompoundStatement(): Statement[] { var list = this.parseList(this.parseStatement, ':'); - if (!isEOS(this.peekToken())) this.compileError(`Expected end of line or ':'`, this.peekToken().$loc); - return list; + var next = this.peekToken(); + if (!isEOS(next)) + this.compileError(`Expected end of line or ':'`, next.$loc); + if (next.str == 'ELSE') + return list.concat(this.parseCompoundStatement()); + else + return list; + } + validKeyword(keyword: string) : string { + return (this.opts.validKeywords && this.opts.validKeywords.indexOf(keyword) < 0) ? null : keyword; } parseStatement(): Statement | null { var cmdtok = this.consumeToken(); @@ -365,6 +406,8 @@ export class BASICParser { case TokenType.Remark: if (!this.opts.tickComments) this.dialectError(`tick remarks`); return null; + case TokenType.Operator: + if (cmd == this.validKeyword('?')) cmd = 'PRINT'; case TokenType.Ident: // remark? ignore all tokens to eol if (cmd == 'REM') { @@ -382,7 +425,7 @@ export class BASICParser { // lookup JS function for command var fn = this['stmt__' + cmd]; if (fn) { - if (this.opts.validKeywords && this.opts.validKeywords.indexOf(cmd) < 0) + if (this.validKeyword(cmd) == null) this.dialectError(`the ${cmd} keyword`); stmt = fn.bind(this)() as Statement; break; @@ -409,8 +452,6 @@ export class BASICParser { if (this.peekToken().str == '(') { this.expectToken('('); args = this.parseExprList(); - if (args && args.length > this.opts.maxArguments) - this.compileError(`There can be no more than ${this.opts.maxArguments} arguments to a function or subscript.`); this.expectToken(')'); } return { name: tok.str, args: args, $loc: tok.$loc }; @@ -449,13 +490,15 @@ export class BASICParser { parseLabel() : Expr { 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 = parseInt(tok.str).toString(); + var label = tok.str; this.targets[label] = tok.$loc; return {value:label}; default: - this.compileError(`There should be a line number here.`); - return; + 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.`); } } parsePrimary(): Expr { @@ -580,6 +623,17 @@ export class BASICParser { this.pushbackToken({type:TokenType.Operator, str:':', $loc:lineno.$loc}); return { command: "IF", cond: cond }; } + stmt__ELSE(): ELSE_Statement { + if (!this.opts.ifElse) this.dialectError(`IF...ELSE statements`); + var lineno = this.peekToken(); + // assume GOTO if number given after ELSE + if (lineno.type == TokenType.Int) { + this.pushbackToken({type:TokenType.Ident, str:'GOTO', $loc:lineno.$loc}); + } + // add fake ":" + this.pushbackToken({type:TokenType.Operator, str:':', $loc:lineno.$loc}); + return { command: "ELSE" }; + } stmt__FOR() : FOR_Statement { var lexpr = this.parseLexpr(); // TODO: parseNumVar() this.expectToken('='); @@ -624,7 +678,7 @@ export class BASICParser { stmt__DATA() : DATA_Statement { return { command:'DATA', datums:this.parseExprList() }; } - stmt__READ() { + stmt__READ() : READ_Statement { return { command:'READ', args:this.parseLexprList() }; } stmt__RESTORE() { @@ -647,15 +701,26 @@ export class BASICParser { } stmt__DEF() : DEF_Statement { var lexpr = this.parseVarSubscriptOrFunc(); + if (lexpr.args && lexpr.args.length > this.opts.maxDefArgs) + this.compileError(`There can be no more than ${this.opts.maxDefArgs} arguments to a function or subscript.`); 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__POP() : NoArgStatement { + return { command:'POP' }; + } + stmt__GET() : GET_Statement { + var lexpr = this.parseLexpr(); + this.decls[lexpr.name] = this.lasttoken.$loc; + return { command:'GET', lexpr:lexpr }; + } + // TODO: CHANGE A TO A$ (4th edition, A(0) is len and A(1..) are chars) stmt__OPTION() : OPTION_Statement { var tokname = this.consumeToken(); - if (tokname.type != TokenType.Ident) this.compileError(`There should be a name after the OPTION statement.`) + if (tokname.type != TokenType.Ident) this.compileError(`There must be a name after the OPTION statement.`) var list : string[] = []; var tok; do { @@ -677,7 +742,7 @@ export class BASICParser { break; case 'DIALECT': let dname = stmt.optargs[0] || ""; - let dialect = DIALECTS[dname]; + let dialect = DIALECTS[dname.toUpperCase()]; if (dialect) this.opts = dialect; else this.compileError(`The dialect named "${dname}" is not supported by this compiler.`); break; @@ -726,6 +791,8 @@ export class BASICParser { // TODO export const ECMA55_MINIMAL : BASICOptions = { + dialectName: "ECMA55", + asciiOnly : true, uppercaseOnly : true, optionalLabels : false, strictVarNames : true, @@ -736,7 +803,8 @@ export const ECMA55_MINIMAL : BASICOptions = { stringConcat : false, typeConvert : false, maxDimensions : 2, - maxArguments : 255, + maxDefArgs : 255, + maxStringLength : 255, sparseArrays : false, tickComments : false, validKeywords : ['BASE','DATA','DEF','DIM','END', @@ -746,38 +814,102 @@ export const ECMA55_MINIMAL : BASICOptions = { validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN'], validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'], printZoneLength : 15, - printPrecision : 6, + numericPadding : true, checkOverflow : true, multipleNextVars : false, + ifElse : false, } export const ALTAIR_BASIC40 : BASICOptions = { + dialectName: "ALTAIR40", + asciiOnly : true, uppercaseOnly : true, optionalLabels : false, - strictVarNames : true, + strictVarNames : false, sharedArrayNamespace : true, defaultArrayBase : 0, defaultArraySize : 11, - defaultValues : false, - stringConcat : false, + defaultValues : true, + stringConcat : true, typeConvert : false, - maxDimensions : 2, - maxArguments : 255, + maxDimensions : 128, // "as many as will fit on a single line" ... ? + maxDefArgs : 255, + maxStringLength : 255, sparseArrays : false, tickComments : false, validKeywords : null, // all validFunctions : null, // all validOperators : null, // all ['\\','MOD','NOT','AND','OR','XOR','EQV','IMP'], printZoneLength : 15, - printPrecision : 6, + numericPadding : true, + checkOverflow : true, + multipleNextVars : true, // TODO: not supported + ifElse : true, +} + +export const APPLESOFT_BASIC : BASICOptions = { + dialectName: "APPLESOFT", + asciiOnly : true, + uppercaseOnly : false, + optionalLabels : false, + strictVarNames : false, // TODO: first two alphanum chars + sharedArrayNamespace : false, + defaultArrayBase : 0, + defaultArraySize : 9, // A(0) to A(8) + defaultValues : true, + stringConcat : true, + typeConvert : false, + maxDimensions : 88, + maxDefArgs : 1, // TODO: no string FNs + maxStringLength : 255, + sparseArrays : false, + tickComments : false, + validKeywords : null, // all + validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAN', + 'LEN','LEFT$','MID$','RIGHT$','STR$','VAL','CHR$','ASC', + 'FRE','SCRN','PDL','PEEK'], // TODO + validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^', 'AND', 'NOT', 'OR'], + printZoneLength : 16, + numericPadding : false, + checkOverflow : true, + multipleNextVars : false, + ifElse : false, +} + +export const MAX8_BASIC : BASICOptions = { + dialectName: "MAX8", + asciiOnly : false, + uppercaseOnly : false, + optionalLabels : true, + strictVarNames : false, // TODO: first two alphanum chars + sharedArrayNamespace : false, + defaultArrayBase : 0, + defaultArraySize : 11, + defaultValues : true, + stringConcat : true, + typeConvert : true, + maxDimensions : 255, + maxDefArgs : 255, // TODO: no string FNs + maxStringLength : 1024*1024, + sparseArrays : false, + tickComments : true, + validKeywords : null, // all + validFunctions : null, + validOperators : null, + printZoneLength : 15, + numericPadding : false, checkOverflow : true, multipleNextVars : true, + ifElse : true, } +// TODO: integer vars + export const DIALECTS = { "DEFAULT": ALTAIR_BASIC40, "ALTAIR": ALTAIR_BASIC40, "ALTAIR40": ALTAIR_BASIC40, "ECMA55": ECMA55_MINIMAL, "MINIMAL": ECMA55_MINIMAL, + "APPLESOFT": APPLESOFT_BASIC, }; diff --git a/src/common/basic/runtime.ts b/src/common/basic/runtime.ts index 1bc9f290..fcfb9bab 100644 --- a/src/common/basic/runtime.ts +++ b/src/common/basic/runtime.ts @@ -34,6 +34,7 @@ export class BASICRuntime { label2lineidx : {[label : string] : number}; label2pc : {[label : string] : number}; datums : basic.Literal[]; + builtins : {}; curpc : number; dataptr : number; @@ -57,6 +58,7 @@ export class BASICRuntime { this.line2pc = []; this.pc2line = new Map(); this.datums = []; + this.builtins = this.getBuiltinFunctions(); // TODO: lines start @ 1? program.lines.forEach((line, idx) => { // make lookup tables @@ -76,7 +78,7 @@ export class BASICRuntime { }); }); // TODO: compile statements? - //line.stmts.forEach((stmt) => this.compileStatement(stmt)); + line.stmts.forEach((stmt) => this.compileStatement(stmt)); }); // try to resume where we left off after loading this.curpc = this.label2pc[prevlabel] || 0; @@ -88,7 +90,7 @@ export class BASICRuntime { this.dataptr = 0; this.vars = {}; this.arrays = {}; - this.defs = this.getBuiltinFunctions(); + this.defs = {}; this.forLoops = []; this.returnStack = []; this.column = 0; @@ -110,6 +112,9 @@ export class BASICRuntime { // TODO: pass source location to error throw new EmuHalt(`${msg} (line ${this.getLabelForPC(this.curpc)})`); } + dialectError(what : string) { + this.runtimeError(`I can't ${what} in this dialect of BASIC.`); + } getLineForPC(pc:number) { var line; @@ -150,11 +155,16 @@ export class BASICRuntime { compileStatement(stmt: basic.Statement & CompiledStatement) { if (stmt.$run == null) { - var stmtfn = this['do__' + stmt.command]; - if (stmtfn == null) this.runtimeError(`I don't know how to "${stmt.command}".`); - var functext = stmtfn.bind(this)(stmt); - if (this.trace) console.log(functext); - stmt.$run = new Function(functext).bind(this); + try { + var stmtfn = this['do__' + stmt.command]; + if (stmtfn == null) this.runtimeError(`I don't know how to "${stmt.command}".`); + var functext = stmtfn.bind(this)(stmt); + if (this.trace) console.log(functext); + stmt.$run = new Function(functext).bind(this); + } catch (e) { + console.log(functext); + throw e; + } } } executeStatement(stmt: basic.Statement & CompiledStatement) { @@ -168,6 +178,19 @@ export class BASICRuntime { } while (this.curpc < this.allstmts.length && !this.pc2line.get(this.curpc)); } + skipToElse() { + do { + // in Altair BASIC, ELSE is bound to the right-most IF + // TODO: this is complicated, we should just have nested expressions + if (this.program.opts.ifElse) { + var cmd = this.allstmts[this.curpc].command; + if (cmd == 'ELSE') { this.curpc++; break; } + else if (cmd == 'IF') return this.skipToEOL(); + } + this.curpc++; + } while (this.curpc < this.allstmts.length && !this.pc2line.get(this.curpc)); + } + skipToEOF() { this.curpc = this.allstmts.length; } @@ -192,30 +215,38 @@ export class BASICRuntime { var pc = this.returnStack.pop(); this.curpc = pc; } + + popReturnStack() { + if (this.returnStack.length == 0) + this.runtimeError("I tried to POP, but there wasn't a corresponding GOSUB."); + this.returnStack.pop(); + } valueToString(obj) : string { var str; if (typeof obj === 'number') { var numstr = obj.toString().toUpperCase(); - var prec = 11; - while (numstr.length > 11) { + var numlen = this.program.opts.printZoneLength - 4; + var prec = numlen; + while (numstr.length > numlen) { numstr = obj.toPrecision(prec--); } if (numstr.startsWith('0.')) numstr = numstr.substr(1); else if (numstr.startsWith('-0.')) numstr = '-'+numstr.substr(2); - if (numstr.startsWith('-')) { + if (!this.program.opts.numericPadding) + str = numstr; + else if (numstr.startsWith('-')) str = `${numstr} `; - } else { + else str = ` ${numstr} `; - } } else if (obj == '\n') { this.column = 0; str = obj; } else if (obj == '\t') { - var curgroup = Math.floor(this.column / 15); - var nextcol = (curgroup + 1) * 15; + var curgroup = Math.floor(this.column / this.program.opts.printZoneLength); + var nextcol = (curgroup + 1) * this.program.opts.printZoneLength; str = this.TAB(nextcol); } else { str = `${obj}`; @@ -251,15 +282,16 @@ export class BASICRuntime { } else { if (opts.isconst) this.runtimeError(`I expected a constant value here`); var s = ''; - 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 += ')'; + if (expr.name.startsWith("FN")) { // is it a user-defined function? + let jsargs = expr.args && expr.args.map((arg) => this.expr2js(arg, opts)).join(', '); + s += `this.defs.${expr.name}(${jsargs})`; // TODO: what if no exist? + } else if (this.builtins[expr.name]) { // is it a built-in function? + let jsargs = expr.args && expr.args.map((arg) => this.expr2js(arg, opts)).join(', '); + s += `this.builtins.${expr.name}(${jsargs})`; } else if (expr.args) { // is it a subscript? s += `this.getArray(${JSON.stringify(expr.name)}, ${expr.args.length})`; s += expr.args.map((arg) => '[this.ROUND('+this.expr2js(arg, opts)+')]').join(''); - } else { - // just a variable + } else { // just a variable s = `this.vars.${expr.name}`; } if (opts.check) @@ -409,6 +441,7 @@ export class BASICRuntime { } do__LET(stmt : basic.LET_Statement) { + // TODO: range-checking for subscripts (get and set) var lexpr = this.expr2js(stmt.lexpr, {check:false}); var right = this.expr2js(stmt.right, {check:true}); return `${lexpr} = this.assign(${JSON.stringify(stmt.lexpr.name)}, ${right});`; @@ -429,11 +462,14 @@ export class BASICRuntime { do__IF(stmt : basic.IF_Statement) { var cond = this.expr2js(stmt.cond, {check:true}); - return `if (!(${cond})) { this.skipToEOL(); }` + return `if (!(${cond})) { this.skipToElse(); }` + } + + do__ELSE() { + return `this.skipToEOL()` } do__DEF(stmt : basic.DEF_Statement) { - var lexpr = `this.defs.${stmt.lexpr.name}`; var args = []; for (var arg of stmt.lexpr.args || []) { if (isLookup(arg)) { @@ -443,6 +479,8 @@ export class BASICRuntime { } } var functext = this.expr2js(stmt.def, {check:true, locals:args}); + //this.defs[stmt.lexpr.name] = new Function(args.join(','), functext).bind(this); + var lexpr = `this.defs.${stmt.lexpr.name}`; return `${lexpr} = function(${args.join(',')}) { return ${functext}; }.bind(this)`; } @@ -509,9 +547,28 @@ export class BASICRuntime { // already parsed in compiler } + do__POP() { + return `this.popReturnStack()`; + } + + do__GET(stmt : basic.GET_Statement) { + var lexpr = this.expr2js(stmt.lexpr, {check:false}); + // TODO: single key input + return `this.running=false; + this.input().then((vals) => { + ${lexpr} = this.convert(${JSON.stringify(stmt.lexpr.name)}, vals[0]); + this.running=true; + this.resume(); + })`; + } + + // TODO: ONERR, ON ERROR GOTO // TODO: "SUBSCRIPT ERROR" (range check) // TODO: gosubs nested too deeply // TODO: memory quota + // TODO: useless loop (! 4th edition) + // TODO: other 4th edition errors + // FUNCTIONS @@ -548,7 +605,10 @@ export class BASICRuntime { } checkString(s:string) : string { - if (typeof s !== 'string') this.runtimeError(`I expected a string here.`); + if (typeof s !== 'string') + this.runtimeError(`I expected a string here.`); + else if (s.length > this.program.opts.maxStringLength) + this.dialectError(`create strings longer than ${this.program.opts.maxStringLength} characters`); return s; } @@ -556,8 +616,10 @@ export class BASICRuntime { // TODO: if string-concat if (typeof a === 'number' && typeof b === 'number') return this.checkNum(a + b); + else if (this.program.opts.stringConcat) + return this.checkString(a + b); else - return a + b; + this.dialectError(`use the "+" operator to concatenate strings`) } sub(a:number, b:number) : number { return this.checkNum(a - b); @@ -597,23 +659,23 @@ export class BASICRuntime { bor(a:number, b:number) : number { return a | b; } - eq(a:number, b:number) : boolean { - return a == b; + eq(a:number, b:number) : number { + return a == b ? 1 : 0; } - ne(a:number, b:number) : boolean { - return a != b; + ne(a:number, b:number) : number { + return a != b ? 1 : 0; } - lt(a:number, b:number) : boolean { - return a < b; + lt(a:number, b:number) : number { + return a < b ? 1 : 0; } - gt(a:number, b:number) : boolean { - return a > b; + gt(a:number, b:number) : number { + return a > b ? 1 : 0; } - le(a:number, b:number) : boolean { - return a <= b; + le(a:number, b:number) : number { + return a <= b ? 1 : 0; } - ge(a:number, b:number) : boolean { - return a >= b; + ge(a:number, b:number) : number { + return a >= b ? 1 : 0; } // FUNCTIONS (uppercase) @@ -633,6 +695,9 @@ export class BASICRuntime { COS(arg : number) : number { return this.checkNum(Math.cos(arg)); } + COT(arg : number) : number { + return this.checkNum(1.0 / Math.tan(arg)); // 4th edition only + } EXP(arg : number) : number { return this.checkNum(Math.exp(arg)); } diff --git a/src/platform/basic.ts b/src/platform/basic.ts index fa45b4f7..a8cc9ec0 100644 --- a/src/platform/basic.ts +++ b/src/platform/basic.ts @@ -36,6 +36,7 @@ class TeleType { this.lines = []; this.ncharsout = 0; $(this.page).empty(); + this.showPrintHead(true); } ensureline() { if (this.curline == null) { @@ -75,13 +76,15 @@ class TeleType { span.appendTo(this.curline); } this.col += line.length; + // TODO: wrap @ 80 columns this.ncharsout += line.length; - //this.movePrintHead(); + this.movePrintHead(true); } } newline() { this.flushline(); this.col = 0; + this.movePrintHead(false); } // TODO: bug in interpreter where it tracks cursor position but maybe doesn't do newlines? print(val: string) { @@ -129,9 +132,21 @@ class TeleType { scrollToBottom() { this.curline.scrollIntoView(); } - movePrintHead() { - var x = $(this.page).position().left + this.col * ($(this.page).width() / 80); - $("#printhead").offset({left: x}); + movePrintHead(printing: boolean) { + /* + var ph = $("#printhead"); // TODO: speed? + var x = $(this.page).position().left + this.col * ($(this.page).width() / 80) - 200; + ph.stop().animate({left: x}, {duration:20}); + //ph.offset({left: x}); + if (printing) ph.addClass("printing"); + else ph.removeClass("printing"); + */ + } + showPrintHead(show: boolean) { + /* + var ph = $("#printhead"); // TODO: speed? + if (show) ph.show(); else ph.hide(); + */ } } @@ -151,11 +166,15 @@ class TeleTypeWithKeyboard extends TeleType { this.input = input; this.platform = platform; this.runtime = platform.runtime; - this.runtime.input = async (prompt:string) => { + this.runtime.input = async (prompt:string, nargs:number) => { return new Promise( (resolve, reject) => { - this.addtext(prompt, 0); - this.addtext('? ', 0); - this.waitingfor = 'line'; + if (prompt != null) { + this.addtext(prompt, 0); + this.addtext('? ', 0); + this.waitingfor = 'line'; + } else { + this.waitingfor = 'char'; + } this.focusinput(); this.resolveInput = resolve; }); @@ -176,8 +195,13 @@ class TeleTypeWithKeyboard extends TeleType { }; this.hideinput(); } + clear() { + super.clear(); + this.hideinput(); + } focusinput() { this.ensureline(); + this.showPrintHead(false); // don't steal focus while editing if (this.keepinput) $(this.input).css('visibility', 'visible'); @@ -194,6 +218,7 @@ class TeleTypeWithKeyboard extends TeleType { $(this.input).removeClass('transcript-input-char') } hideinput() { + this.showPrintHead(true); if (this.keepinput) $(this.input).css('visibility','hidden'); else @@ -215,16 +240,18 @@ class TeleTypeWithKeyboard extends TeleType { } sendinput(s: string) { if (this.resolveInput) { - s = s.toUpperCase(); + if (this.platform.program.opts.uppercaseOnly) + s = s.toUpperCase(); this.addtext(s, 4); this.flushline(); - this.resolveInput(s.split(',')); + this.resolveInput(s.split(',')); // TODO: should parse quotes, etc this.resolveInput = null; } this.clearinput(); this.hideinput(); // keep from losing input handlers } sendchar(code: number) { + this.sendinput(String.fromCharCode(code)); } ensureline() { if (!this.keepinput) $(this.input).hide(); @@ -277,6 +304,7 @@ class BASICPlatform implements Platform { var windowport = $('
').appendTo(gameport); var inputline = $('').appendTo(parent); //var printhead = $('').appendTo(parent); + //var printshield = $('').appendTo(parent); this.tty = new TeleTypeWithKeyboard(windowport[0], true, inputline[0] as HTMLInputElement, this); this.tty.scrolldiv = parent; this.timer = new AnimationTimer(60, this.animate.bind(this)); @@ -323,6 +351,7 @@ class BASICPlatform implements Platform { exitmsg() { this.tty.print("\n\n"); this.tty.addtext("*** END OF PROGRAM ***", 1); + this.tty.showPrintHead(false); } resize: () => void;