diff --git a/src/common/basic/compiler.ts b/src/common/basic/compiler.ts index 4b1eacb6..b6936c05 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]*)|[0]*(\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, @@ -48,7 +48,7 @@ export interface BinOp { } export interface UnOp { - op: 'neg' | 'lnot'; + op: 'neg' | 'lnot' | 'bnot'; expr: Expr; } @@ -176,8 +176,11 @@ class Token { } const OPERATORS = { - 'OR': {f:'bor',p:7}, - 'AND': {f:'band',p:8}, + '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" '=': {f:'eq',p:50}, '<>': {f:'ne',p:50}, '<': {f:'lt',p:50}, @@ -226,31 +229,32 @@ export interface BASICOptions { 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) + defaultArrayBase : number; // arrays start at this number (0 or 1) (TODO: check) 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? (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? + 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 - 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 - 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 (TODO) - ifElse : boolean; // IF...ELSE construct } ///// BASIC PARSER export class BASICParser { - opts : BASICOptions = MAX8_BASIC; + opts : BASICOptions = ALTAIR_BASIC40; errors: WorkerError[]; listings: CodeListingMap; labels: { [label: string]: BASICLine }; @@ -287,11 +291,11 @@ export class BASICParser { var tok = this.lasttoken = (this.tokens.shift() || this.eol); return tok; } - expectToken(str: string) : Token { + expectToken(str: string, msg?: string) : Token { var tok = this.consumeToken(); var tokstr = tok.str; if (str != tokstr) { - this.compileError(`There should be a "${str}" here.`); + this.compileError(msg || `There should be a "${str}" here.`); } return tok; } @@ -452,7 +456,7 @@ export class BASICParser { if (this.peekToken().str == '(') { this.expectToken('('); args = this.parseExprList(); - this.expectToken(')'); + this.expectToken(')', `There should be another expression or a ")" here.`); } return { name: tok.str, args: args, $loc: tok.$loc }; default: @@ -513,7 +517,7 @@ export class BASICParser { case TokenType.Ident: if (tok.str == 'NOT') { let expr = this.parsePrimary(); - return { op: 'lnot', expr: expr }; + return { op: this.opts.bitwiseLogic ? 'bnot' : 'lnot', expr: expr }; } else { this.pushbackToken(tok); return this.parseVarSubscriptOrFunc(); @@ -521,13 +525,13 @@ export class BASICParser { case TokenType.Operator: if (tok.str == '(') { let expr = this.parseExpr(); - this.expectToken(')'); + this.expectToken(')', `There should be another expression or a ")" here.`); return expr; } else if (tok.str == '-') { let expr = this.parsePrimary(); // TODO: -2^2=-4 and -2-2=-4 return { op: 'neg', expr: expr }; } else if (tok.str == '+') { - return this.parsePrimary(); // TODO? + return this.parsePrimary(); // ignore unary + } case TokenType.EOL: this.compileError(`The expression is incomplete.`); @@ -555,7 +559,10 @@ export class BASICParser { right = this.parseExpr1(right, getPrecedence(look)); look = this.peekToken(); } - left = { op: getOperator(op.str).f, left: left, right: right }; + var opfn = getOperator(op.str).f; + if (this.opts.bitwiseLogic && opfn == 'land') opfn = 'band'; + if (this.opts.bitwiseLogic && opfn == 'lor') opfn = 'bor'; + left = { op:opfn, left: left, right: right }; } return left; } @@ -818,6 +825,7 @@ export const ECMA55_MINIMAL : BASICOptions = { checkOverflow : true, multipleNextVars : false, ifElse : false, + bitwiseLogic : false, } export const ALTAIR_BASIC40 : BASICOptions = { @@ -845,6 +853,7 @@ export const ALTAIR_BASIC40 : BASICOptions = { checkOverflow : true, multipleNextVars : true, // TODO: not supported ifElse : true, + bitwiseLogic : true, } export const APPLESOFT_BASIC : BASICOptions = { @@ -874,6 +883,7 @@ export const APPLESOFT_BASIC : BASICOptions = { checkOverflow : true, multipleNextVars : false, ifElse : false, + bitwiseLogic : false, } export const MAX8_BASIC : BASICOptions = { @@ -885,22 +895,23 @@ export const MAX8_BASIC : BASICOptions = { sharedArrayNamespace : false, defaultArrayBase : 0, defaultArraySize : 11, - defaultValues : true, + defaultValues : false, stringConcat : true, typeConvert : true, maxDimensions : 255, maxDefArgs : 255, // TODO: no string FNs - maxStringLength : 1024*1024, + maxStringLength : 1024, // TODO? sparseArrays : false, tickComments : true, validKeywords : null, // all - validFunctions : null, - validOperators : null, + validFunctions : null, // all + validOperators : null, // all printZoneLength : 15, numericPadding : false, checkOverflow : true, multipleNextVars : true, ifElse : true, + bitwiseLogic : true, } // TODO: integer vars diff --git a/src/common/basic/run.ts b/src/common/basic/run.ts index 5e0267f4..fbc8e46a 100644 --- a/src/common/basic/run.ts +++ b/src/common/basic/run.ts @@ -1,5 +1,5 @@ -import { BASICParser, ECMA55_MINIMAL } from "./compiler"; +import { BASICParser, DIALECTS } from "./compiler"; import { BASICRuntime } from "./runtime"; var readline = require('readline'); @@ -8,11 +8,24 @@ var rl = readline.createInterface({ output: process.stdout, //terminal: false }); +var fs = require('fs'); var parser = new BASICParser(); -parser.opts = ECMA55_MINIMAL; -var fs = require('fs'); -var filename = process.argv[2]; +var runtime = new BASICRuntime(); + +// parse args +var filename = '/dev/stdin'; +var args = process.argv.slice(2); +for (var i=0; i console.log("@@@ " + err.msg)); if (parser.errors.length) process.exit(2); -var runtime = new BASICRuntime(); -runtime.trace = process.argv[3] == '-v'; +// run program runtime.load(pgm); runtime.reset(); runtime.print = (s:string) => { diff --git a/src/common/basic/runtime.ts b/src/common/basic/runtime.ts index fcfb9bab..4e09968b 100644 --- a/src/common/basic/runtime.ts +++ b/src/common/basic/runtime.ts @@ -16,7 +16,6 @@ function isUnOp(arg: basic.Expr): arg is basic.UnOp { } class ExprOptions { - check: boolean; isconst?: boolean; locals?: string[]; } @@ -35,6 +34,7 @@ export class BASICRuntime { label2pc : {[label : string] : number}; datums : basic.Literal[]; builtins : {}; + opts : basic.BASICOptions; curpc : number; dataptr : number; @@ -52,6 +52,7 @@ export class BASICRuntime { load(program: basic.BASICProgram) { let prevlabel = this.label2pc && this.getLabelForPC(this.curpc); this.program = program; + this.opts = program.opts; this.label2lineidx = {}; this.label2pc = {}; this.allstmts = []; @@ -72,7 +73,7 @@ export class BASICRuntime { // parse DATA literals line.stmts.filter((stmt) => stmt.command == 'DATA').forEach((datastmt) => { (datastmt as basic.DATA_Statement).datums.forEach(d => { - var functext = this.expr2js(d, {check:true, isconst:true}); + var functext = this.expr2js(d, {isconst:true}); var value = new Function(`return ${functext};`).bind(this)(); this.datums.push({value:value}); }); @@ -99,7 +100,7 @@ export class BASICRuntime { } getBuiltinFunctions() { - var fnames = this.program && this.program.opts.validFunctions; + var fnames = this.program && this.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 = {}; @@ -182,7 +183,7 @@ export class BASICRuntime { 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) { + if (this.opts.ifElse) { var cmd = this.allstmts[this.curpc].command; if (cmd == 'ELSE') { this.curpc++; break; } else if (cmd == 'IF') return this.skipToEOL(); @@ -226,7 +227,7 @@ export class BASICRuntime { var str; if (typeof obj === 'number') { var numstr = obj.toString().toUpperCase(); - var numlen = this.program.opts.printZoneLength - 4; + var numlen = this.opts.printZoneLength - 4; var prec = numlen; while (numstr.length > numlen) { numstr = obj.toPrecision(prec--); @@ -235,7 +236,7 @@ export class BASICRuntime { numstr = numstr.substr(1); else if (numstr.startsWith('-0.')) numstr = '-'+numstr.substr(2); - if (!this.program.opts.numericPadding) + if (!this.opts.numericPadding) str = numstr; else if (numstr.startsWith('-')) str = `${numstr} `; @@ -245,8 +246,8 @@ export class BASICRuntime { this.column = 0; str = obj; } else if (obj == '\t') { - var curgroup = Math.floor(this.column / this.program.opts.printZoneLength); - var nextcol = (curgroup + 1) * this.program.opts.printZoneLength; + var curgroup = Math.floor(this.column / this.opts.printZoneLength); + var nextcol = (curgroup + 1) * this.opts.printZoneLength; str = this.TAB(nextcol); } else { str = `${obj}`; @@ -273,31 +274,17 @@ export class BASICRuntime { // override this resume() { } - expr2js(expr: basic.Expr, opts: ExprOptions) : string { + expr2js(expr: basic.Expr, opts?: ExprOptions) : string { + if (!opts) opts = {}; if (isLiteral(expr)) { return JSON.stringify(expr.value); } else if (isLookup(expr)) { - if (opts.locals && opts.locals.indexOf(expr.name) >= 0) { + 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`); - var 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 - s = `this.vars.${expr.name}`; - } - if (opts.check) - return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`; - else - return s; + if (opts.isconst) this.runtimeError(`I expected a constant value here.`); + var s = this.assign2js(expr, opts); + return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`; } } else if (isBinOp(expr)) { var left = this.expr2js(expr.left, opts); @@ -309,6 +296,38 @@ export class BASICRuntime { } } + assign2js(expr: basic.IndOp, opts?: ExprOptions) { + if (!opts) opts = {}; + var s = ''; + var qname = JSON.stringify(expr.name); + if (expr.name.startsWith("FN")) { // is it a user-defined function? + // TODO: check argument count? + let jsargs = expr.args && expr.args.map((arg) => this.expr2js(arg, opts)).join(', '); + s += `this.getDef(${qname})(${jsargs})`; + // TODO: detect recursion? + } else if (this.builtins[expr.name]) { // is it a built-in function? + this.checkFuncArgs(expr, this.builtins[expr.name]); + 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? + // TODO: check array bounds? + s += `this.getArray(${qname}, ${expr.args.length})`; + s += expr.args.map((arg) => '[this.ROUND('+this.expr2js(arg, opts)+')]').join(''); + } else { // just a variable + s = `this.vars.${expr.name}`; + } + return s; + } + + checkFuncArgs(expr: basic.IndOp, fn: Function) { + // TODO: check types? + var nargs = expr.args ? expr.args.length : 0; + if (expr.name == 'MID$' && nargs == 2) return; + if (expr.name == 'INSTR' && nargs == 2) return; + if (fn.length != nargs) + this.runtimeError(`I expected ${fn.length} arguments for the ${expr.name} function, but I got ${nargs}.`); + } + startForLoop(forname, init, targ, step) { // TODO: support 0-iteration loops var pc = this.curpc; @@ -339,7 +358,7 @@ export class BASICRuntime { // converts a variable to string/number based on var name assign(name: string, right: number|string) : number|string { - if (this.program.opts.typeConvert) + if (this.opts.typeConvert) return this.convert(name, right); // TODO: use options if (name.endsWith("$")) { @@ -371,11 +390,12 @@ 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.`) var isstring = name.endsWith('$'); // 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; + var arrcons = isstring || !this.opts.defaultValues ? Array : Float64Array; + // TODO? var ab = this.opts.defaultArrayBase; if (dims.length == 1) { this.arrays[name] = new arrcons(dims[0]+1); } else if (dims.length == 2) { @@ -390,9 +410,9 @@ export class BASICRuntime { getArray(name: string, order: number) : [] { if (!this.arrays[name]) { if (order == 1) - this.dimArray(name, this.program.opts.defaultArraySize); + this.dimArray(name, this.opts.defaultArraySize); else if (order == 2) - this.dimArray(name, this.program.opts.defaultArraySize, this.program.opts.defaultArraySize); + this.dimArray(name, this.opts.defaultArraySize, this.opts.defaultArraySize); else this.runtimeError(`I only support arrays of one or two dimensions.`); // TODO } @@ -417,17 +437,17 @@ export class BASICRuntime { do__PRINT(stmt : basic.PRINT_Statement) { var s = ''; for (var arg of stmt.args) { - var expr = this.expr2js(arg, {check:true}); + var expr = this.expr2js(arg); s += `this.printExpr(${expr});`; } return s; } do__INPUT(stmt : basic.INPUT_Statement) { - var prompt = this.expr2js(stmt.prompt, {check:true}); + var prompt = this.expr2js(stmt.prompt); var setvals = ''; stmt.args.forEach((arg, index) => { - var lexpr = this.expr2js(arg, {check:false}); + var lexpr = this.assign2js(arg); setvals += `valid &= this.isValid(${lexpr} = this.convert(${JSON.stringify(arg.name)}, vals[${index}]));` }); return `this.running=false; @@ -442,16 +462,16 @@ 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}); + var lexpr = this.assign2js(stmt.lexpr); + var right = this.expr2js(stmt.right); return `${lexpr} = this.assign(${JSON.stringify(stmt.lexpr.name)}, ${right});`; } do__FOR(stmt : basic.FOR_Statement) { var name = JSON.stringify(stmt.lexpr.name); // TODO: args? - var init = this.expr2js(stmt.initial, {check:true}); - var targ = this.expr2js(stmt.target, {check:true}); - var step = stmt.step ? this.expr2js(stmt.step, {check:true}) : 'null'; + var init = this.expr2js(stmt.initial); + var targ = this.expr2js(stmt.target); + var step = stmt.step ? this.expr2js(stmt.step) : 'null'; return `this.startForLoop(${name}, ${init}, ${targ}, ${step})`; } @@ -461,7 +481,7 @@ export class BASICRuntime { } do__IF(stmt : basic.IF_Statement) { - var cond = this.expr2js(stmt.cond, {check:true}); + var cond = this.expr2js(stmt.cond); return `if (!(${cond})) { this.skipToElse(); }` } @@ -478,17 +498,16 @@ export class BASICRuntime { this.runtimeError("I found a DEF statement with arguments other than variable names."); } } - var functext = this.expr2js(stmt.def, {check:true, locals:args}); + var functext = this.expr2js(stmt.def, {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)`; + return `this.defs.${stmt.lexpr.name} = function(${args.join(',')}) { return ${functext}; }.bind(this)`; } _DIM(dim : basic.IndOp) { var argsstr = ''; for (var arg of dim.args) { // TODO: check for float (or at compile time) - argsstr += ', ' + this.expr2js(arg, {check:true}); + argsstr += ', ' + this.expr2js(arg); } return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`; } @@ -500,12 +519,12 @@ export class BASICRuntime { } do__GOTO(stmt : basic.GOTO_Statement) { - var label = this.expr2js(stmt.label, {check:true}); + var label = this.expr2js(stmt.label, {isconst:true}); return `this.gotoLabel(${label})`; } do__GOSUB(stmt : basic.GOSUB_Statement) { - var label = this.expr2js(stmt.label, {check:true}); + var label = this.expr2js(stmt.label, {isconst:true}); return `this.gosubLabel(${label})`; } @@ -514,8 +533,8 @@ export class BASICRuntime { } do__ONGOTO(stmt : basic.ONGOTO_Statement) { - var expr = this.expr2js(stmt.expr, {check:true}); - var labels = stmt.labels.map((arg) => this.expr2js(arg, {check:true})).join(', '); + var expr = this.expr2js(stmt.expr); + var labels = stmt.labels.map((arg) => this.expr2js(arg, {isconst:true})).join(', '); return `this.onGotoLabel(${expr}, ${labels})`; } @@ -526,7 +545,7 @@ export class BASICRuntime { do__READ(stmt : basic.READ_Statement) { var s = ''; stmt.args.forEach((arg) => { - s += `${this.expr2js(arg, {check:false})} = this.assign(${JSON.stringify(arg.name)}, this.nextDatum());`; + s += `${this.assign2js(arg)} = this.assign(${JSON.stringify(arg.name)}, this.nextDatum());`; }); return s; } @@ -552,7 +571,7 @@ export class BASICRuntime { } do__GET(stmt : basic.GET_Statement) { - var lexpr = this.expr2js(stmt.lexpr, {check:false}); + var lexpr = this.assign2js(stmt.lexpr); // TODO: single key input return `this.running=false; this.input().then((vals) => { @@ -584,11 +603,11 @@ export class BASICRuntime { // check for unreferenced value if (typeof obj !== 'number' && typeof obj !== 'string') { // assign default value? - if (obj == null && this.program.opts.defaultValues) { + if (obj == null && this.opts.defaultValues) { return exprname.endsWith("$") ? "" : 0; } if (exprname != null && obj == null) { - this.runtimeError(`I didn't find a value for ${exprname}`); + this.runtimeError(`I haven't set a value for ${exprname}.`); } else if (exprname != null) { this.runtimeError(`I got an invalid value for ${exprname}: ${obj}`); } else { @@ -597,18 +616,21 @@ export class BASICRuntime { } return obj; } - + getDef(exprname: string) { + var fn = this.defs[exprname]; + if (!fn) this.runtimeError(`I haven't run a DEF statement for ${exprname}.`); + return fn; + } checkNum(n:number) : number { if (n === Infinity) this.runtimeError(`I computed a number too big to store.`); if (isNaN(n)) this.runtimeError(`I computed an invalid number.`); return n; } - checkString(s:string) : string { if (typeof s !== 'string') this.runtimeError(`I expected a string here.`); - else if (s.length > this.program.opts.maxStringLength) - this.dialectError(`create strings longer than ${this.program.opts.maxStringLength} characters`); + else if (s.length > this.opts.maxStringLength) + this.dialectError(`create strings longer than ${this.opts.maxStringLength} characters`); return s; } @@ -616,7 +638,7 @@ 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) + else if (this.opts.stringConcat) return this.checkString(a + b); else this.dialectError(`use the "+" operator to concatenate strings`) @@ -632,7 +654,7 @@ export class BASICRuntime { return this.checkNum(a / b); } idiv(a:number, b:number) : number { - return this.div(Math.floor(a), Math.floor(b)); + return this.FIX(this.INT(a) / this.INT(b)); } mod(a:number, b:number) : number { return this.checkNum(a % b); @@ -641,41 +663,53 @@ export class BASICRuntime { if (a == 0 && b < 0) this.runtimeError(`I can't raise zero to a negative power.`); return this.checkNum(Math.pow(a, b)); } - land(a:number, b:number) : number { - return a && b; - } - lor(a:number, b:number) : number { - return a || b; - } - 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; } + bnot(a:number) : number { + return ~a; + } + bxor(a:number, b:number) : number { + return a ^ b; + } + bimp(a:number, b:number) : number { + return this.bor(this.bnot(a), b); + } + beqv(a:number, b:number) : number { + return this.bnot(this.bxor(a, b)); + } + land(a:number, b:number) : number { + return a && b ? (this.opts.bitwiseLogic ? -1 : 1) : 0; + } + lor(a:number, b:number) : number { + return a || b ? (this.opts.bitwiseLogic ? -1 : 1) : 0; + } + lnot(a:number) : number { + return a ? 0 : (this.opts.bitwiseLogic ? -1 : 1); + } + neg(a:number) : number { + return -a; + } eq(a:number, b:number) : number { - return a == b ? 1 : 0; + return a == b ? (this.opts.bitwiseLogic ? -1 : 1) : 0; } ne(a:number, b:number) : number { - return a != b ? 1 : 0; + return a != b ? (this.opts.bitwiseLogic ? -1 : 1) : 0; } lt(a:number, b:number) : number { - return a < b ? 1 : 0; + return a < b ? (this.opts.bitwiseLogic ? -1 : 1) : 0; } gt(a:number, b:number) : number { - return a > b ? 1 : 0; + return a > b ? (this.opts.bitwiseLogic ? -1 : 1) : 0; } le(a:number, b:number) : number { - return a <= b ? 1 : 0; + return a <= b ? (this.opts.bitwiseLogic ? -1 : 1) : 0; } ge(a:number, b:number) : number { - return a >= b ? 1 : 0; + return a >= b ? (this.opts.bitwiseLogic ? -1 : 1) : 0; } // FUNCTIONS (uppercase) @@ -702,7 +736,7 @@ export class BASICRuntime { return this.checkNum(Math.exp(arg)); } FIX(arg : number) : number { - return this.checkNum(arg - Math.floor(arg)); + return this.checkNum(arg < 0 ? Math.ceil(arg) : Math.floor(arg)); } HEX$(arg : number) : string { return arg.toString(16); @@ -729,7 +763,8 @@ export class BASICRuntime { return this.checkNum(Math.log(arg)); } MID$(arg : string, start : number, count : number) : string { - if (start < 1) this.runtimeError(`I tried to compute MID$ but the second parameter is less than zero (${start}).`) + if (start < 1) this.runtimeError(`I can't compute MID$ if the starting index is less than 1.`) + if (count == 0) count = arg.length; return arg.substr(start-1, count); } RIGHT$(arg : string, count : number) : string { diff --git a/src/platform/basic.ts b/src/platform/basic.ts index a8cc9ec0..d1f6a182 100644 --- a/src/platform/basic.ts +++ b/src/platform/basic.ts @@ -357,6 +357,7 @@ class BASICPlatform implements Platform { resize: () => void; loadROM(title, data) { + // TODO: only hot reload when we hit a label? var didExit = this.runtime.exited; this.program = data; this.runtime.load(data);