From 079641ef1eb05c67ed8e3ea28aabc4294488a9ba Mon Sep 17 00:00:00 2001 From: minjaesong Date: Thu, 1 Sep 2022 18:13:13 +0900 Subject: [PATCH] renaming system.maxmem to sys.maxmem --- assets/JS_INIT.js | 6 - assets/bios/basic.js | 4262 +++++++++++++++++ assets/bios/basicbios.js | 2 +- assets/bios/bios_source.js | 2 +- assets/bios/openbios.js | 2 +- assets/bios/pip_basic_loader_source.js | 2 +- assets/bios/pip_basic_loader_source2.js | 4131 ++++++++++++++++ assets/bios/tandemport.js | 4 +- assets/bios/tsvmbios.js | 2 +- assets/bios/wp.js | 2 +- assets/disk0/dmatest.js | 2 +- assets/disk0/home/bf.js | 2 +- assets/disk0/home/logotest.js | 2 +- assets/disk0/tbas/basic.js | 4 +- assets/disk0/tvdos/TVDOS.SYS | 2 +- .../src/net/torvald/tsvm/VMJSR223Delegate.kt | 3 + 16 files changed, 8410 insertions(+), 20 deletions(-) create mode 100644 assets/bios/basic.js create mode 100644 assets/bios/pip_basic_loader_source2.js diff --git a/assets/JS_INIT.js b/assets/JS_INIT.js index f8c3317..ba93ca8 100644 --- a/assets/JS_INIT.js +++ b/assets/JS_INIT.js @@ -515,12 +515,6 @@ con.poll_keys = function() { return [-41,-42,-43,-44,-45,-46,-47,-48].map(it => sys.peek(it)); }; Object.freeze(con); -// system management function -var system = {}; -system.maxmem = function() { - return sys.peek(-65) | (sys.peek(-66) << 8) | (sys.peek(-67) << 16) | (sys.peek(-68) << 24); -}; -Object.freeze(system); // some utilities functions // TypedArray re-implementation diff --git a/assets/bios/basic.js b/assets/bios/basic.js new file mode 100644 index 0000000..6357e05 --- /dev/null +++ b/assets/bios/basic.js @@ -0,0 +1,4262 @@ +// Created by CuriousTorvald on 2020-05-19 +// Version 1.0 Release Date 2020-12-28 +// Version 1.1 Release Date 2021-01-28 +// Version 1.2 Release Date 2021-05-05 + +/* +Copyright (c) 2020-2021 CuriousTorvald + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +if (exec_args !== undefined && exec_args[1] !== undefined && exec_args[1].startsWith("-?")) { + println("Usage: basic ") + println("When the optional basic program is set, the interpreter will run the program and then quit if successful, remain open if the program had an error.") + return 0 +} + +const THEVERSION = "1.2" + +const PROD = true +let INDEX_BASE = 0 +let TRACEON = (!PROD) && true +let DBGON = (!PROD) && true +let DATA_CURSOR = 0 +let DATA_CONSTS = [] +const BASIC_HOME_PATH = "/home/basic/" + +if (sys.maxmem() < 8192) { + println("Out of memory. BASIC requires 8K or more User RAM") + throw Error("Out of memory") +} + +let vmemsize = sys.maxmem() + +let cmdbuf = [] // index: line number +let gotoLabels = {} +let cmdbufMemFootPrint = 0 +let prompt = "Ok" +let prescan = false +let replCmdBuf = [] // used to store "load filename" and issues it when user confirmed potential data loss +let replUsrConfirmed = false + +// lambdaBoundVars is used in two different mode: +// - PARSER will just store a symbol as a string literal +// - EXECUTOR will store the actual info of the bound vars in this format: [astType, astValue] +let lambdaBoundVars = [] // format: [[a,b],[c]] for "[c]~>[a,b]~>expr" + +/* if an object can be FOR REAL cast to number */ +function isNumable(s) { + // array? + if (Array.isArray(s)) return false + // undefined? + if (s === undefined) return false + // null string? + if (typeof s.trim == "function" && s.trim().length == 0) return false + // else? + return !isNaN(s) // NOTE: isNaN('') == false +} +let tonum = (t) => t*1.0 +function cloneObject(o) { return JSON.parse(JSON.stringify(o)) } + +class ParserError extends Error { + constructor(...args) { + super(...args) + Error.captureStackTrace(this, ParserError) + } +} + +class BASICerror extends Error { + constructor(...args) { + super(...args) + Error.captureStackTrace(this, ParserError) + } +} + +let lang = {} +lang.badNumberFormat = Error("Illegal number format") +lang.badOperatorFormat = Error("Illegal operator format") +lang.divByZero = Error("Division by zero") +lang.badFunctionCallFormat = function(line, reason) { + return Error("Illegal function call" + ((line) ? " in "+line : "") + ((reason) ? ": "+reason : "")) +} +lang.unmatchedBrackets = Error("Unmatched brackets") +lang.missingOperand = Error("Missing operand") +lang.noSuchFile = Error("No such file") +lang.outOfData = function(line) { + return Error("Out of DATA"+(line !== undefined ? (" in "+line) : "")) +} +lang.nextWithoutFor = function(line, varname) { + return Error("NEXT "+((varname !== undefined) ? ("'"+varname+"'") : "")+"without FOR in "+line) +} +lang.syntaxfehler = function(line, reason) { + return Error("Syntax error" + ((line !== undefined) ? (" in "+line) : "") + ((reason !== undefined) ? (": "+reason) : "")) +} +lang.illegalType = function(line, obj) { + return Error("Type mismatch" + ((obj !== undefined) ? ` "${obj} (typeof ${typeof obj})"` : "") + ((line !== undefined) ? (" in "+line) : "")) + } +lang.refError = function(line, obj) { + serial.printerr(`${line} Unresolved reference:`) + serial.printerr(` object: ${obj}, typeof: ${typeof obj}`) + if (obj !== null && obj !== undefined) serial.printerr(` entries: ${Object.entries(obj)}`) + return Error("Unresolved reference" + ((obj !== undefined) ? ` "${obj}"` : "") + ((line !== undefined) ? (" in "+line) : "")) +} +lang.nowhereToReturn = function(line) { return "RETURN without GOSUB in " + line } +lang.errorinline = function(line, stmt, errobj) { + return Error('Error'+((line !== undefined) ? (" in "+line) : "")+' on "'+stmt+'": '+errobj) +} +lang.parserError = function(line, errorobj) { + return Error("Parser error in " + line + ": " + errorobj) +} +lang.outOfMem = function(line) { + return Error("Out of memory in " + line) +} +lang.dupDef = function(line, varname) { + return Error("Duplicate definition"+((varname !== undefined) ? (" on "+varname) : "")+" in "+line) +} +lang.asgnOnConst = function(line, constname) { + return Error('Trying to modify constant "'+constname+'" in '+line) +} +lang.subscrOutOfRng = function(line, object, index, maxlen) { + return Error("Subscript out of range"+(object !== undefined ? (' for "'+object+'"') : '')+(index !== undefined ? (` (index: ${index}, len: ${maxlen})`) : "")+(line !== undefined ? (" in "+line) : "")) +} +lang.aG = " arguments were given" +lang.ord = function(n) { + if (n % 10 == 1 && n % 100 != 11) return n+"st" + if (n % 10 == 2 && n % 100 != 12) return n+"nd" + if (n % 10 == 3 && n % 100 != 13) return n+"rd" + return n+"th" +} +Object.freeze(lang) + +let fs = {} +fs._close = function(portNo) { + com.sendMessage(portNo, "CLOSE") +} +fs._flush = function(portNo) { + com.sendMessage(portNo, "FLUSH") +} +// @return true if operation committed successfully, false if: +// - opening file with R-mode and target file does not exists +// throws if: +// - java.lang.NullPointerException if path is null +// - Error if operation mode is not "R", "W" nor "A" +fs.open = function(path, operationMode) { + var port = _BIOS.FIRST_BOOTABLE_PORT + + fs._flush(port[0]); fs._close(port[0]) + + var mode = operationMode.toUpperCase() + if (mode != "R" && mode != "W" && mode != "A") { + throw Error("Unknown file opening mode: " + mode) + } + + com.sendMessage(port[0], "OPEN"+mode+'"'+BASIC_HOME_PATH+path+'",'+port[1]) + let response = com.getStatusCode(port[0]) + return (response == 0) +} +// @return the entire contents of the file in String +fs.readAll = function() { + var port = _BIOS.FIRST_BOOTABLE_PORT + com.sendMessage(port[0], "READ") + var response = com.getStatusCode(port[0]) + if (135 == response) { + throw Error("File not opened") + } + if (response < 0 || response >= 128) { + throw Error("Reading a file failed with "+response) + } + return com.pullMessage(port[0]) +} +fs.write = function(string) { + var port = _BIOS.FIRST_BOOTABLE_PORT + com.sendMessage(port[0], "WRITE"+string.length) + var response = com.getStatusCode(port[0]) + if (135 == response) { + throw Error("File not opened") + } + if (response < 0 || response >= 128) { + throw Error("Writing a file failed with "+response) + } + com.sendMessage(port[0], string) + fs._flush(port[0]); fs._close(port[0]) +} +Object.freeze(fs) + +// implement your own con object here +// requirements: reset_graphics(), getch(), curs_set(int), hitterminate(), resetkeybuf(), addch(int) + +let getUsedMemSize = function() { + var varsMemSize = 0 + + Object.entries(bS.vars).forEach((pair, i) => { + var object = pair[1] + + if (Array.isArray(object)) { + // TODO test 1-D array + varsMemSize += object.length * 8 + } + else if (!isNaN(object)) varsMemSize += 8 + else if (typeof object === "string" || object instanceof String) varsMemSize += object.length + else varsMemSize += 1 + }) + return varsMemSize + cmdbufMemFootPrint // + array's dimsize * 8 + variables' sizeof literal + functions' expression length +} + +let reLineNum = /^[0-9]+ / +//var reFloat = /^([\-+]?[0-9]*[.][0-9]+[eE]*[\-+0-9]*[fF]*|[\-+]?[0-9]+[.eEfF][0-9+\-]*[fF]?)$/ +//var reDec = /^([\-+]?[0-9_]+)$/ +//var reHex = /^(0[Xx][0-9A-Fa-f_]+)$/ +//var reBin = /^(0[Bb][01_]+)$/ + +// must match partial +let reNumber = /([0-9]*[.][0-9]+[eE]*[\-+0-9]*[fF]*|[0-9]+[.eEfF][0-9+\-]*[fF]?)|([0-9]+(\_[0-9])*)|(0[Xx][0-9A-Fa-f_]+)|(0[Bb][01_]+)/ +let reNum = /[0-9]+/ +let tbasexit = false +const termWidth = con.getmaxyx()[1] +const termHeight = con.getmaxyx()[0] +const greetText = (termWidth >= 70) ? `Terran BASIC ${THEVERSION} `+String.fromCharCode(179)+" Scratchpad Memory: "+vmemsize+" bytes" : `Terran BASIC ${THEVERSION}` +const greetLeftPad = (termWidth - greetText.length - 6) >> 1 +const greetRightPad = termWidth - greetLeftPad - greetText.length - 6 + +con.clear() +con.color_pair(253,255) +print(' ');con.addch(17) +con.color_pair(0,253) +con.move(1,4) +print(" ".repeat(greetLeftPad)+greetText+" ".repeat(greetRightPad)) +con.color_pair(253,255) +con.addch(16);con.curs_right();print(' ') +con.move(3,1) + +con.color_pair(253,255) +println(prompt) + +// variable object constructor +/** variable object constructor + * @param literal Javascript object or primitive + * @type derived from JStoBASICtype + "usrdefun" + "internal_arrindexing_lazy" + "internal_assignment_object" + * @see bS.builtin["="] + */ +let BasicVar = function(literal, type) { + this.bvLiteral = literal + this.bvType = type +} +// Abstract Syntax Tree +// creates empty tree node +let astToString = function(ast, depth, isFinalLeaf) { + let l__ = "| " + + let recDepth = depth || 0 + if (!isAST(ast)) return "" + + let hastStr = ast.astHash + let sb = "" + let marker = ("lit" == ast.astType) ? "i" : + ("op" == ast.astType) ? "+" : + ("string" == ast.astType) ? "@" : + ("num" == ast.astType) ? "$" : + ("array" == ast.astType) ? "[" : + ("defun_args" === ast.astType) ? "d" : "f" + sb += l__.repeat(recDepth)+`${marker} ${ast.astLnum}: "${ast.astValue}" (astType:${ast.astType}); leaves: ${ast.astLeaves.length}; hash:"${hastStr}"\n` + for (var k = 0; k < ast.astLeaves.length; k++) { + sb += astToString(ast.astLeaves[k], recDepth + 1, k == ast.astLeaves.length - 1) + if (ast.astSeps[k] !== undefined) + sb += l__.repeat(recDepth)+` sep:${ast.astSeps[k]}\n` + } + sb += l__.repeat(recDepth)+"`"+"-".repeat(22)+'\n' + return sb +} +let monadToString = function(monad, depth) { + let recDepth = depth || 0 + let l__ = " " + let sb = ` M"${monad.mHash}"(${monad.mType}): ` + //if (monad.mType == "value") { + sb += (monad.mVal === undefined) ? "(undefined)" : (isAST(monad.mVal)) ? `f"${monad.mVal.astHash}"` : (isMonad(monad.mVal)) ? `M"${monad.mVal.mHash}"` : monad.mVal + /*} + else if (monad.mType == "list") { + let elemToStr = function(e) { + return (e === undefined) ? "(undefined)" : + (isAST(e)) ? `f"${e.astHash}"` : + (isMonad(e)) ? `M"${e.mHash}"` : + e + } + + let m = monad.mVal + while (1) { + sb += elemToStr(m.p) + if (m.n === undefined) break + else { + sb += "," + m = m.n + } + } + } + else { + throw new BASICerror("unknown monad subtype: "+m.mType) + }*/ + return sb +} +let arrayToString = function(a) { + let acc = "" + for (let k = 0; k < a.length; k++) { + if (k > 0) acc += "," + acc += (Array.isArray(a[k])) ? arrayToString(a[k]) : a[k] + } + return "{"+acc+"}" +} +let theLambdaBoundVars = function() { + let sb = "" + lambdaBoundVars.forEach((it,i) => { + if (i > 0) sb += ' |' + sb += ` ${i} [` + it.forEach((it,i) => { + if (i > 0) sb += ',' + sb += `${it[0]}:${it[1]}` // type and value pair + }) + sb += ']' + }) + return sb +} +let makeBase32Hash = ()=>[1,2,3,4,5].map(i=>"YBNDRFG8EJKMCPQXOTLVWIS2A345H769"[Math.random()*32|0]).join('') +let BasicAST = function() { + this.astLnum = 0 + this.astLeaves = [] + this.astSeps = [] + this.astValue = undefined + this.astType = "null" // lit, op, string, num, array, function, null, defun_args (! NOT usrdefun !) + this.astHash = makeBase32Hash() +} +// I'm basically duck-typing here... +let isAST = (object) => (object === undefined) ? false : object.astLeaves !== undefined && object.astHash !== undefined +let isRunnable = (object) => isAST(object) || object.mType == "funseq" +let BasicFunSeq = function(f) { + if (!Array.isArray(f) || !isAST(f[0])) throw new BASICerror("Not an array of functions") + this.mHash = makeBase32Hash() + this.mType = "funseq" + this.mVal = f +} +/** A List Monad (a special case of Value-monad) + * This monad MUST follow the monad law! + * @param m a monadic value (Javascript array) + */ +let BasicListMonad = function(m) { + this.mHash = makeBase32Hash() + this.mType = "list" + this.mVal = [m] +} +/** A Memoisation Monad, aka the most generic monad + * This monad MUST follow the monad law! + * @param m a monadic value + */ +/* Test this monad with following program + * This program requires (>>=) to "play by the rules" +10 LSORT=[XS]~>IF LEN(XS)<1 THEN NIL ELSE LSORT(FILTER([K]~>KK>=HEAD XS,TAIL XS)) +20 LREV=[XS]~>MAP([I]~>XS(I),LEN(XS)-1 TO 0 STEP -1) +30 LINC=[XS]~>MAP([I]~>I+1,XS) +40 L=7!9!4!5!2!3!1!8!6!NIL +100 MAGICKER=[XS]~>MRET(LSORT(XS))>>=([X]~>MRET(LREV(X))>>=([X]~>MRET(LINC(X)))) +110 MAGICK_L = MAGICKER(L) +120 PRINT MAGICK_L() + */ +/* Value-monad satisfies monad laws, test with following program +10 F=[X]~>X*2 : G=[X]~>X^3 : RETN=[X]~>MRET(X) + +100 PRINT:PRINT "First law: 'return a >>= k' equals to 'k a'" +110 K=[X]~>RETN(F(X)) : REM K is monad-returning function +120 A=42 +130 KM=RETN(A)>>=K +140 KO=K(A) +150 PRINT("KM is ";TYPEOF(KM);", ";MJOIN(KM)) +160 PRINT("KO is ";TYPEOF(KO);", ";MJOIN(KO)) + +200 PRINT:PRINT "Second law: 'm >>= return' equals to 'm'" +210 M=MRET(G(42)) +220 MM=M>>=RETN +230 MO=M +240 PRINT("MM is ";TYPEOF(MM);", ";MJOIN(MM)) +250 PRINT("MO is ";TYPEOF(MO);", ";MJOIN(MO)) + +300 PRINT:PRINT "Third law: 'm >>= (\x -> k x >>= h)' equals to '(m >>= k) >>= h'" +310 REM see line 110 for the definition of K +320 H=[X]~>RETN(G(X)) : REM H is monad-returning function +330 M=MRET(69) +340 M1=M>>=([X]~>K(X)>>=H) +350 M2=(M>>=K)>>=H +360 PRINT("M1 is ";TYPEOF(M1);", ";MJOIN(M1)) +370 PRINT("M2 is ";TYPEOF(M2);", ";MJOIN(M2)) + */ +let BasicMemoMonad = function(m) { + this.mHash = makeBase32Hash() + this.mType = "value" + this.mVal = m // a monadic value + this.seq = undefined // unused +} +// I'm basically duck-typing here... +let isMonad = (o) => (o === undefined) ? false : (o.mType !== undefined) + +let literalTypes = ["string", "num", "bool", "array", "generator", "usrdefun", "monad"] +/* +@param variable SyntaxTreeReturnObj, of which the 'troType' is defined in BasicAST. +@return a value, if the input type if string or number, its literal value will be returned. Otherwise will search the + BASIC variable table and return the literal value of the BasicVar; undefined will be returned if no such var exists. +*/ +let resolve = function(variable) { + if (variable === undefined) return undefined + // head error checking + if (variable.troType === undefined) { + // primitves types somehow injected from elsewhere (main culprit: MAP) + //throw Error(`BasicIntpError: trying to resolve unknown object '${variable}' with entries ${Object.entries(variable)}`) + + if (isNumable(variable)) return tonum(variable) + if (Array.isArray(variable)) return variable + if (isGenerator(variable) || isAST(variable) || isMonad(variable)) return variable + if (typeof variable == "object") + throw Error(`BasicIntpError: trying to resolve unknown object '${variable}' with entries ${Object.entries(variable)}`) + return variable + } + else if (variable.troType === "internal_arrindexing_lazy") + return eval("variable.troValue.arrFull"+variable.troValue.arrKey) + else if (literalTypes.includes(variable.troType) || variable.troType.startsWith("internal_")) + return variable.troValue + else if (variable.troType == "lit") { + if (isNumable(variable.troValue)) { // rarely we get a number as a variable name, notably on (&) + return tonum(variable.troValue) + } + // when program tries to call builtin function (e.g. SIN), return usrdefun-wrapped version + else if (bS.builtin[variable.troValue] !== undefined) { + return bS.wrapBuiltinToUsrdefun(variable.troValue) + } + // else, it's just a plain-old variable :p + else { + let basicVar = bS.vars[variable.troValue] + if (basicVar === undefined) throw lang.refError(undefined, variable.troValue) + if (basicVar.bvLiteral === "") return "" + return (basicVar !== undefined) ? basicVar.bvLiteral : undefined + } + } + else if (variable.troType == "null") + return undefined + // tail error checking + else + throw Error("BasicIntpError: unknown variable/object with type "+variable.troType+", with value "+variable.troValue) +} +let findHighestIndex = function(exprTree) { + let highestIndex = [-1,-1] + // look for the highest index of [a,b] + let rec = function(exprTree) { + bF._recurseApplyAST(exprTree, it => { + if (it.astType == "defun_args") { + let recIndex = it.astValue[0] + let ordIndex = it.astValue[1] + + if (recIndex > highestIndex[0]) { + highestIndex = [recIndex, 0] + } + + if (recIndex == highestIndex[0] && ordIndex > highestIndex[1]) { + highestIndex[1] = ordIndex + } + } + else if (isAST(it.astValue)) { + rec(it.astValue) + } + }) + };rec(exprTree) + return highestIndex +} +let indexDec = function(node, recIndex) { + if (node.astType == "defun_args" && node.astValue[0] === recIndex) { + let newNode = cloneObject(node) + newNode.astValue[1] -= 1 + return newNode + } + else return node +} +let curryDefun = function(inputTree, inputValue) { + let exprTree = cloneObject(inputTree) + let value = cloneObject(inputValue) + let highestIndex = findHighestIndex(exprTree)[0] + + if (DBGON) { + serial.println("[curryDefun] highest index to curry: "+highestIndex) + } + + let substitution = new BasicAST() + if (isAST(value)) { + substitution = value + } + else { + substitution.astLnum = "??" + substitution.astType = JStoBASICtype(value) + substitution.astValue = value + } + + // substitute the highest index with given value + /*bF._recurseApplyAST(exprTree, it => { + return (it.astType == "defun_args" && it.astValue[0] === highestIndex[0] && it.astValue[1] === highestIndex[1]) ? substitution : it + });*/ + + // substitute the highest index [max recIndex, 0] with given value + // and if recIndex is same as the highestIndex and ordIndex is greater than zero, + // decrement the ordIndex + bF._recurseApplyAST(exprTree, it => { + return (it.astType == "defun_args" && it.astValue[0] === highestIndex && it.astValue[1] === 0) ? substitution : indexDec(it, highestIndex) + }) + + return exprTree +} +let getMonadEvalFun = (monad) => function(lnum, stmtnum, args, sep) { + if (!isMonad(monad)) throw lang.badFunctionCallFormat(lnum, "not a monad") + + if (DBGON) { + serial.println("[BASIC.MONADEVAL] monad:") + serial.println(monadToString(monad)) + } + + if (monad.mType == "funseq") { + let arg = args[0] + monad.mVal.forEach(f => { + arg = bS.getDefunThunk(f)(lnum, stmtnum, [arg]) + }) + return arg + } + else { + // args are futile + return monad.mVal + } +} +let listMonConcat = function(parentm, childm) { + parentm.mVal = parentm.mVal.concat(childm.mVal) + return parentm +} +let countArgs = function(defunTree) { + let cnt = -1 + bF._recurseApplyAST(defunTree, it => { + if (it.astType == "defun_args" && it.astValue > cnt) + cnt = it.astValue + }) + + return cnt+1 +} +let argCheckErr = function(lnum, o) { + if (o === undefined) throw lang.refError(lnum, "(variable is undefined)") + if (o.troType == "null") throw lang.refError(lnum, o) + if (o.troType == "lit" && bS.builtin[o.troValue] !== undefined) return + if (o.troType == "lit" && bS.vars[o.troValue] === undefined) throw lang.refError(lnum, o.troValue) +} +let oneArg = function(lnum, stmtnum, args, action) { + if (args.length != 1) throw lang.syntaxfehler(lnum, args.length+lang.aG) + argCheckErr(lnum, args[0]) + var rsvArg0 = resolve(args[0]) + return action(rsvArg0) +} +let oneArgNul = function(lnum, stmtnum, args, action) { + if (args.length != 1) throw lang.syntaxfehler(lnum, args.length+lang.aG) + var rsvArg0 = resolve(args[0]) + return action(rsvArg0) +} +let oneArgNum = function(lnum, stmtnum, args, action) { + if (args.length != 1) throw lang.syntaxfehler(lnum, args.length+lang.aG) + argCheckErr(lnum, args[0]) + var rsvArg0 = resolve(args[0], 1) + if (!isNumable(rsvArg0)) throw lang.illegalType(lnum, args[0]) + return action(rsvArg0) +} +let twoArg = function(lnum, stmtnum, args, action) { + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + argCheckErr(lnum, args[0]) + var rsvArg0 = resolve(args[0]) + argCheckErr(lnum, args[1]) + var rsvArg1 = resolve(args[1]) + return action(rsvArg0, rsvArg1) +} +let twoArgNul = function(lnum, stmtnum, args, action) { + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + var rsvArg0 = resolve(args[0]) + var rsvArg1 = resolve(args[1]) + return action(rsvArg0, rsvArg1) +} +let twoArgNum = function(lnum, stmtnum, args, action) { + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + argCheckErr(lnum, args[0]) + var rsvArg0 = resolve(args[0], 1) + if (!isNumable(rsvArg0)) throw lang.illegalType(lnum, "LH:"+Object.entries(args[0])) + argCheckErr(lnum, args[1]) + var rsvArg1 = resolve(args[1], 1) + if (!isNumable(rsvArg1)) throw lang.illegalType(lnum, "RH:"+Object.entries(args[1])) + return action(rsvArg0, rsvArg1) +} +let threeArg = function(lnum, stmtnum, args, action) { + if (args.length != 3) throw lang.syntaxfehler(lnum, args.length+lang.aG) + argCheckErr(lnum, args[0]) + var rsvArg0 = resolve(args[0]) + argCheckErr(lnum, args[1]) + var rsvArg1 = resolve(args[1]) + argCheckErr(lnum, args[2]) + var rsvArg2 = resolve(args[2]) + return action(rsvArg0, rsvArg1, rsvArg2) +} +let threeArgNum = function(lnum, stmtnum, args, action) { + if (args.length != 3) throw lang.syntaxfehler(lnum, args.length+lang.aG) + argCheckErr(lnum, args[0]) + var rsvArg0 = resolve(args[0], 1) + if (!isNumable(rsvArg0)) throw lang.illegalType(lnum, "1H:"+Object.entries(args[0])) + argCheckErr(lnum, args[1]) + var rsvArg1 = resolve(args[1], 1) + if (!isNumable(rsvArg1)) throw lang.illegalType(lnum, "2H:"+Object.entries(args[1])) + argCheckErr(lnum, args[2]) + var rsvArg2 = resolve(args[2], 1) + if (!isNumable(rsvArg2)) throw lang.illegalType(lnum, "3H:"+Object.entries(args[2])) + return action(rsvArg0, rsvArg1, rsvArg2) +} +let varArg = function(lnum, stmtnum, args, action) { + var rsvArg = args.map((it) => { + argCheckErr(lnum, it) + var r = resolve(it) + return r + }) + return action(rsvArg) +} +let varArgNum = function(lnum, stmtnum, args, action) { + var rsvArg = args.map((it) => { + argCheckErr(lnum, it) + var r = resolve(it) + if (isNaN(r)) throw lang.illegalType(lnum, r) + return r + }) + return action(rsvArg) +} +let makeIdFun = () => { + return JSON.parse(`{"astLnum":"**","astLeaves":[],"astSeps":[],"astValue":[0,0],"astType":"defun_args","astHash":"IDFUN"}`) +} +let _basicConsts = { + "NIL": new BasicVar([], "array"), + "PI": new BasicVar(Math.PI, "num"), + "TAU": new BasicVar(Math.PI * 2.0, "num"), + "EULER": new BasicVar(Math.E, "num"), + "ID": new BasicVar(makeIdFun(), "usrdefun"), + "UNDEFINED": new BasicVar(undefined, "null"), + "TRUE": new BasicVar(true, "bool"), + "FALSE": new BasicVar(false, "bool") +} +Object.freeze(_basicConsts) +let initBvars = function() { + return cloneObject(_basicConsts) +} +let ForGen = function(s,e,t) { + this.start = s + this.end = e + this.step = t || 1 + + this.current = this.start + this.stepsgn = (this.step > 0) ? 1 : -1 +} +// I'm basically duck-typing here... +let isGenerator = (o) => o.start !== undefined && o.end !== undefined && o.step !== undefined && o.stepsgn !== undefined +let genToArray = (gen) => { + let a = [] + let cur = gen.start + while (cur*gen.stepsgn + gen.step*gen.stepsgn <= (gen.end + gen.step)*gen.stepsgn) { + a.push(cur) + cur += gen.step + } + return a +} +let genHasHext = (o) => o.current*o.stepsgn + o.step*o.stepsgn <= (o.end + o.step)*o.stepsgn +let genGetNext = (gen, mutated) => { + if (mutated !== undefined) gen.current = tonum(mutated) + gen.current += gen.step + return genHasHext(gen) ? gen.current : undefined +} +let genToString = (gen) => `Generator: ${gen.start} to ${gen.end}`+((gen.step !== 1) ? ` step ${gen.step}` : '') +let genReset = (gen) => { gen.current = gen.start } +let bS = {} // BASIC status +bS.gosubStack = [] +bS.forLnums = {} // key: forVar, value: [lnum, stmtnum] +bS.forStack = [] // forVars only +bS.vars = initBvars() // contains instances of BasicVars +bS.rnd = 0 // stores mantissa (23 bits long) of single precision floating point number +bS.getDimSize = function(array, dim) { + var dims = [] + while (true) { + dims.push(array.length) + + if (Array.isArray(array[0])) + array = array[0] + else + break + } + return dims[dim] +} +bS.getArrayIndexFun = function(lnum, stmtnum, arrayName, array) { + if (lnum === undefined || stmtnum === undefined) throw Error(`Line or statement number is undefined: (${lnum},${stmtnum})`) + + return function(lnum, stmtnum, args, seps) { + if (lnum === undefined || stmtnum === undefined) throw Error(`Line or statement number is undefined: (${lnum},${stmtnum})`) + + return varArgNum(lnum, stmtnum, args, (dims) => { + if (TRACEON) serial.println("ar dims: "+dims) + + let dimcnt = 1 + let oldIndexingStr = "" + let indexingstr = "" + + dims.forEach(d => { + oldIndexingStr = indexingstr + indexingstr += `[${d-INDEX_BASE}]` + + var testingArr = eval(`array${indexingstr}`) + if (testingArr === undefined) + throw lang.subscrOutOfRng(lnum, `${arrayName}${oldIndexingStr} (${lang.ord(dimcnt)} dim)`, d-INDEX_BASE, bS.getDimSize(array, dimcnt-1)) + + dimcnt += 1 + }) + + if (TRACEON) + serial.println("ar indexedValue = "+`/*ar1*/array${indexingstr}`) + + return {arrFull: array, arrName: arrayName, arrKey: indexingstr} + }) + } +} +/** + * @return a Javascript function that when called, evaluates the exprTree + */ +bS.getDefunThunk = function(exprTree, norename) { + if (!isRunnable(exprTree)) throw new BASICerror("not a syntax tree") + + // turns funseq-monad into runnable function + if (isMonad(exprTree)) return getMonadEvalFun(exprTree) + + let tree = cloneObject(exprTree) // ALWAYS create new tree instance! + + return function(lnum, stmtnum, args, seps) { + if (lnum === undefined || stmtnum === undefined) throw Error(`Line or statement number is undefined: (${lnum},${stmtnum})`) + + if (!norename) { + let argsMap = args.map(it => { + //argCheckErr(lnum, it) + let rit = resolve(it) + return [JStoBASICtype(rit), rit] // substitute for [astType, astValue] + }) + + // bind arguments + lambdaBoundVars.unshift(argsMap) + + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] unthunking: ") + serial.println(astToString(tree)) + serial.println("[BASIC.getDefunThunk.invoke] thunk args:") + serial.println(argsMap) + serial.println("[BASIC.getDefunThunk.invoke] lambda bound vars:") + serial.println(theLambdaBoundVars()) + } + + // perform renaming + bF._recurseApplyAST(tree, (it) => { + if ("defun_args" == it.astType) { + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] thunk renaming arg-tree branch:") + serial.println(astToString(it)) + } + + let recIndex = it.astValue[0] + let argIndex = it.astValue[1] + + let theArg = lambdaBoundVars[recIndex][argIndex] // instanceof theArg == resolved version of SyntaxTreeReturnObj + + if (theArg !== undefined) { // this "forgiveness" is required to implement currying + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] thunk renaming-theArg: "+theArg) + serial.println(`${Object.entries(theArg)}`) + } + + if (theArg[0] === "null") { + throw new BASICerror(`Bound variable is ${theArg}; lambdaBoundVars: ${theLambdaBoundVars()}`) + } + + it.astValue = theArg[1] + it.astType = theArg[0] + } + + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] thunk successfully renamed arg-tree branch:") + serial.println(astToString(it)) + } + } + }) + + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] resulting thunk tree:") + serial.println(astToString(tree)) + } + } + else { + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] no rename, resulting thunk tree:") + serial.println(astToString(tree)) + } + } + + // evaluate new tree + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] evaluating tree:") + } + let ret = resolve(bF._executeSyntaxTree(lnum, stmtnum, tree, 0)) + + // unbind previously bound arguments + if (!norename) { + lambdaBoundVars.shift() + } + + return ret + } +} +bS.wrapBuiltinToUsrdefun = function(funcname) { + let argCount = bS.builtin[funcname].argc + + if (argCount === undefined) throw new BASICerror(`${funcname} cannot be wrapped into usrdefun`) + + let leaves = [] + for (let k = 0; k < argCount; k++) { + let l = new BasicAST() + l.astLnum = "**" + l.astValue = [0,k] + l.astType = "defun_args" + + leaves.push(l) + } + + let tree = new BasicAST() + tree.astLnum = "**" + tree.astValue = funcname + tree.astType = "function" + tree.astLeaves = leaves + + return tree +} +/* Accepts troValue, assignes to BASIC variable, and returns internal_assign_object + * @params troValue Variable to assign into + * @params rh the value, resolved + */ +bS.addAsBasicVar = function(lnum, troValue, rh) { + if (troValue.arrFull !== undefined) { // assign to existing array + let arr = eval("troValue.arrFull"+troValue.arrKey) + if (Array.isArray(arr)) throw lang.subscrOutOfRng(lnum, arr) + eval("troValue.arrFull"+troValue.arrKey+"=rh") + return {asgnVarName: troValue.arrName, asgnValue: rh} + } + else { + let varname = troValue.toUpperCase() + //println("input varname: "+varname) + if (_basicConsts[varname]) throw lang.asgnOnConst(lnum, varname) + let type = JStoBASICtype(rh) + bS.vars[varname] = new BasicVar(rh, type) + return {asgnVarName: varname, asgnValue: rh} + } +} +bS.builtin = { +/* +@param lnum line number +@param args instance of the SyntaxTreeReturnObj + +if no args were given (e.g. "10 NEXT()"), args[0] will be: {troType: null, troValue: , troNextLine: 11} +if no arg text were given (e.g. "10 NEXT"), args will have zero length +*/ +"=" : {argc:2, f:function(lnum, stmtnum, args) { + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + var troValue = args[0].troValue + + var rh = resolve(args[1]) + if (rh === undefined) throw lang.refError(lnum, "RH:"+args[1].troValue) + + if (isNumable(rh)) rh = tonum(rh) // if string we got can be cast to number, do it + + //println(lnum+" = lh: "+Object.entries(args[0])) + //println(lnum+" = rh raw: "+Object.entries(args[1])) + //println(lnum+" = rh resolved: "+rh) + //try { println(lnum+" = rh resolved entries: "+Object.entries(rh)) } + //catch (_) {} + + return bS.addAsBasicVar(lnum, troValue, rh) +}}, +"IN" : {argc:2, f:function(lnum, stmtnum, args) { // almost same as =, but don't actually make new variable. Used by FOR statement + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + var troValue = args[0].troValue + + var rh = resolve(args[1]) + if (rh === undefined) throw lang.refError(lnum, "RH:"+args[1].troValue) + + if (troValue.arrFull !== undefined) { + throw lang.syntaxfehler(lnum) + } + else { + var varname = troValue.toUpperCase() + var type = JStoBASICtype(rh) + if (_basicConsts[varname]) throw lang.asgnOnConst(lnum, varname) + return {asgnVarName: varname, asgnValue: rh} + } +}}, +"==" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNul(lnum, stmtnum, args, (lh,rh) => lh == rh) +}}, +"<>" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (lh,rh) => lh != rh) +}}, +"><" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (lh,rh) => lh != rh) +}}, +"<=" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh <= rh) +}}, +"=<" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh <= rh) +}}, +">=" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh >= rh) +}}, +"=>" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh >= rh) +}}, +"<" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh < rh) +}}, +">" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh > rh) +}}, +"<<" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh << rh) +}}, +">>" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh >>> rh) +}}, +"UNARYMINUS" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => -lh) +}}, +"UNARYPLUS" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => +lh) +}}, +"UNARYLOGICNOT" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => !(lh)) +}}, +"UNARYBNOT" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => ~(lh)) +}}, +"BAND" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh & rh) +}}, +"BOR" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh | rh) +}}, +"BXOR" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh ^ rh) +}}, +"!" : {argc:2, f:function(lnum, stmtnum, args) { // Haskell-style CONS + return twoArg(lnum, stmtnum, args, (lh,rh) => { + if (Array.isArray(rh)) { + return [lh].concat(rh) + } + else if (rh.mType === "list") { + rh.mVal = [lh].concat(rh.mVal) + return rh + } + else throw lang.illegalType(lnum, rh) + }) +}}, +"~" : {argc:2, f:function(lnum, stmtnum, args) { // array PUSH + return twoArg(lnum, stmtnum, args, (lh,rh) => { + if (Array.isArray(lh)) { + return lh.concat([rh]) + } + else if (lh.mType === "list") { + lh.mVal = [lh.mVal].concat([rh]) + return lh + } + else throw lang.illegalType(lnum, lh) + }) +}}, +"#" : {argc:2, f:function(lnum, stmtnum, args) { // array CONCAT + return twoArg(lnum, stmtnum, args, (lh,rh) => { + if (Array.isArray(lh) && Array.isArray(rh)) { + return lh.concat(rh) + } + else if (lh.mType == "list" && rh.mType == "list") { + let newMval = lh.mVal.concat(rh.mVal) + return new BasicListMonad(newMval) + } + else + throw lang.illegalType(lnum) + }) +}}, +"+" : {argc:2, f:function(lnum, stmtnum, args) { // addition, string concat + return twoArg(lnum, stmtnum, args, (lh,rh) => (!isNaN(lh) && !isNaN(rh)) ? (tonum(lh) + tonum(rh)) : (lh + rh)) +}}, +"-" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh - rh) +}}, +"*" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh * rh) +}}, +"/" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => { + if (rh == 0) throw lang.divByZero + return lh / rh + }) +}}, +"\\" : {argc:2, f:function(lnum, stmtnum, args) { // integer division, rounded towards zero + return twoArgNum(lnum, stmtnum, args, (lh,rh) => { + if (rh == 0) throw lang.divByZero + return (lh / rh)|0 + }) +}}, +"MOD" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => { + if (rh == 0) throw lang.divByZero + return lh % rh + }) +}}, +"^" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => { + let r = Math.pow(lh, rh) + if (isNaN(r)) throw lang.badFunctionCallFormat(lnum) + if (!isFinite(r)) throw lang.divByZero + return r + }) +}}, +"TO" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (from, to) => new ForGen(from, to, 1)) +}}, +"STEP" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (gen, step) => { + if (!isGenerator(gen)) throw lang.illegalType(lnum, gen) + return new ForGen(gen.start, gen.end, step) + }) +}}, +"DIM" : {f:function(lnum, stmtnum, args) { + return varArgNum(lnum, stmtnum, args, (revdims) => { + let dims = revdims.reverse() + let arraydec = "Array(dims[0]).fill(0)" + for (let k = 1; k < dims.length; k++) { + arraydec = `Array(dims[${k}]).fill().map(_=>${arraydec})` + } + return eval(arraydec) + }) +}}, +"ARRAY CONSTRUCTOR" : {f:function(lnum, stmtnum, args) { + return args.map(v => resolve(v)) +}}, +"PRINT" : {argc:1, f:function(lnum, stmtnum, args, seps) { + if (args.length == 0) + println() + else { + for (var llll = 0; llll < args.length; llll++) { + // parse separators. + // ; - concat + // , - tab + if (llll >= 1) { + if (seps[llll - 1] == ",") print("\t") + } + + var rsvArg = resolve(args[llll]) + //if (rsvArg === undefined && args[llll] !== undefined && args[llll].troType != "null") throw lang.refError(lnum, args[llll].troValue) + + //serial.println(`${lnum} PRINT ${lang.ord(llll)} arg: ${Object.entries(args[llll])}, resolved: ${rsvArg}`) + + let printstr = "" + if (Array.isArray(rsvArg)) + printstr = arrayToString(rsvArg) + else if (rsvArg === undefined || rsvArg === "") + printstr = "" + else if (rsvArg.toString !== undefined) + printstr = rsvArg.toString() + else + printstr = rsvArg + + print(printstr) + if (TRACEON) serial.println("[BASIC.PRINT] "+printstr) + } + } + + if (args[args.length - 1] !== undefined && args[args.length - 1].troType != "null") println() +}}, +"EMIT" : {argc:1, f:function(lnum, stmtnum, args, seps) { + if (args.length == 0) + println() + else { + for (var llll = 0; llll < args.length; llll++) { + // parse separators. + // ; - concat + // , - tab + if (llll >= 1) { + if (seps[llll - 1] == ",") print("\t") + } + + var rsvArg = resolve(args[llll]) + if (rsvArg === undefined && args[llll] !== undefined && args[llll].troType != "null") throw lang.refError(lnum, args[llll].troValue) + + let printstr = "" + if (rsvArg === undefined) + print("") + else if (isNumable(rsvArg)) { + let c = con.getyx() + con.addch(tonum(rsvArg)) + con.move(c[0],c[1]+1) + } + else if (rsvArg.toString !== undefined) + print(rsvArg.toString()) + else + printstr = (rsvArg) + + if (TRACEON) serial.println("[BASIC.EMIT] "+printstr) + } + } + + if (args[args.length - 1] !== undefined && args[args.length - 1].troType != "null") println() +}}, +"POKE" : {argc:2, f:function(lnum, stmtnum, args) { + twoArgNum(lnum, stmtnum, args, (lh,rh) => sys.poke(lh, rh)) +}}, +"PEEK" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => sys.peek(lh)) +}}, +"GOTO" : {argc:1, f:function(lnum, stmtnum, args) { + // search from gotoLabels first + let line = gotoLabels[args[0].troValue] + // if not found, use resolved variable + if (line === undefined) line = resolve(args[0]) + if (line < 0) throw lang.syntaxfehler(lnum, line) + + return new JumpObj(line, 0, lnum, line) +}}, +"GOSUB" : {argc:1, f:function(lnum, stmtnum, args) { + // search from gotoLabels first + let line = gotoLabels[args[0].troValue] + // if not found, use resolved variable + if (line === undefined) line = resolve(args[0]) + if (line < 0) throw lang.syntaxfehler(lnum, line) + + bS.gosubStack.push([lnum, stmtnum + 1]) + //println(lnum+" GOSUB into "+lh) + return new JumpObj(line, 0, lnum, line) +}}, +"RETURN" : {f:function(lnum, stmtnum, args) { + var r = bS.gosubStack.pop() + if (r === undefined) throw lang.nowhereToReturn(lnum) + //println(lnum+" RETURN to "+r) + return new JumpObj(r[0], r[1], lnum, r) +}}, +"CLEAR" : {argc:0, f:function(lnum, stmtnum, args) { + bS.vars = initBvars() +}}, +"PLOT" : {argc:3, f:function(lnum, stmtnum, args) { + threeArgNum(lnum, stmtnum, args, (xpos, ypos, color) => graphics.plotPixel(xpos, ypos, color)) +}}, +"AND" : {argc:2, f:function(lnum, stmtnum, args) { + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + var rsvArg = args.map((it) => resolve(it)) + rsvArg.forEach((v) => { + if (v === undefined) throw lang.refError(lnum, v) + if (typeof v !== "boolean") throw lang.illegalType(lnum, v) + }) + var argum = rsvArg.map((it) => { + if (it === undefined) throw lang.refError(lnum, it) + return it + }) + return argum[0] && argum[1] +}}, +"OR" : {argc:2, f:function(lnum, stmtnum, args) { + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + var rsvArg = args.map((it) => resolve(it)) + rsvArg.forEach((v) => { + if (v === undefined) throw lang.refError(lnum, v.value) + if (typeof v !== "boolean") throw lang.illegalType(lnum, v) + }) + var argum = rsvArg.map((it) => { + if (it === undefined) throw lang.refError(lnum, it) + return it + }) + return argum[0] || argum[1] +}}, +"RND" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => { + if (!(args.length > 0 && args[0].troValue === 0)) + bS.rnd = Math.random()//(bS.rnd * 214013 + 2531011) % 16777216 // GW-BASIC does this + return bS.rnd + }) +}}, +"ROUND" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => Math.round(lh)) +}}, +"FLOOR" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => Math.floor(lh)) +}}, +"INT" : {argc:1, f:function(lnum, stmtnum, args) { // synonymous to FLOOR + return oneArgNum(lnum, stmtnum, args, (lh) => Math.floor(lh)) +}}, +"CEIL" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => Math.ceil(lh)) +}}, +"FIX" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => (lh|0)) +}}, +"CHR" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => String.fromCharCode(lh)) +}}, +"TEST" : {argc:1, f:function(lnum, stmtnum, args) { + if (args.length != 1) throw lang.syntaxfehler(lnum, args.length+lang.aG) + return !!resolve(args[0]) // string 'false' will be interpreted as truthy; this is completely intentional +}}, +"FOREACH" : {f:function(lnum, stmtnum, args) { // list comprehension model + var asgnObj = resolve(args[0]) + // type check + if (asgnObj === undefined) throw lang.syntaxfehler(lnum) + if (!Array.isArray(asgnObj.asgnValue)) throw lang.illegalType(lnum, asgnObj) + + var varname = asgnObj.asgnVarName + + // assign new variable + // the var itself will have head of the array, and the head itself will be removed from the array + bS.vars[varname] = new BasicVar(asgnObj.asgnValue[0], JStoBASICtype(asgnObj.asgnValue.shift())) + // stores entire array (sans head) into temporary storage + bS.vars["for var "+varname] = new BasicVar(asgnObj.asgnValue, "array") + // put the varname to forstack + bS.forLnums[varname] = [lnum, stmtnum] + bS.forStack.push(varname) +}}, +"FOR" : {f:function(lnum, stmtnum, args) { // generator model + var asgnObj = resolve(args[0]) + // type check + if (asgnObj === undefined) throw lang.syntaxfehler(lnum) + if (!isGenerator(asgnObj.asgnValue)) throw lang.illegalType(lnum, typeof asgnObj) + + var varname = asgnObj.asgnVarName + var generator = asgnObj.asgnValue + + // assign new variable + // the var itself will have head of the array, and the head itself will be removed from the array + bS.vars[varname] = new BasicVar(generator.start, "num") + // stores entire array (sans head) into temporary storage + bS.vars["for var "+varname] = new BasicVar(generator, "generator") + // put the varname to forstack + bS.forLnums[varname] = [lnum, stmtnum] + bS.forStack.push(varname) +}}, +"NEXT" : {f:function(lnum, stmtnum, args) { + // if no args were given + if (args.length == 0 || (args.length == 1 && args.troType == "null")) { + // go to most recent FOR + var forVarname = bS.forStack.pop() + //serial.println(lnum+" NEXT > forVarname = "+forVarname) + if (forVarname === undefined) { + throw lang.nextWithoutFor(lnum) + } + + if (TRACEON) serial.println("[BASIC.FOR] looping "+forVarname) + + var forVar = bS.vars["for var "+forVarname].bvLiteral + + if (isGenerator(forVar)) + bS.vars[forVarname].bvLiteral = genGetNext(forVar, bS.vars[forVarname].bvLiteral) + else + bS.vars[forVarname].bvLiteral = forVar.shift() + + if ((bS.vars[forVarname].bvLiteral !== undefined)) { + // feed popped value back, we're not done yet + bS.forStack.push(forVarname) + let forLnum = bS.forLnums[forVarname] + return new JumpObj(forLnum[0], forLnum[1]+1, lnum, [forLnum[0], forLnum[1]+1]) // goto the statement RIGHT AFTER the FOR-declaration + } + else { + if (isGenerator(forVar)) + bS.vars[forVarname].bvLiteral = forVar.current // true BASIC compatibility for generator + else + bS.vars[forVarname] === undefined // unregister the variable + + return new JumpObj(lnum, stmtnum + 1, lnum, [lnum, stmtnum + 1]) + } + } + + throw lang.syntaxfehler(lnum, "extra arguments for NEXT") +}}, +/* +10 input;"what is your name";a$ + +£ Line 10 (function) +| leaves: 3 +| value: input (type: string) +£ Line 0 (null) +| leaves: 0 +| value: undefined (type: undefined) +`----------------- +| +| ¶ Line 10 (string) +| | leaves: 0 +| | value: what is your name (type: string) +| `----------------- +| +| i Line 10 (literal) +| | leaves: 0 +| | value: A$ (type: string) +| `----------------- +`----------------- +10 input "what is your name";a$ + +£ Line 10 (function) +| leaves: 2 +| value: input (type: string) +| ¶ Line 10 (string) +| | leaves: 0 +| | value: what is your name (type: string) +| `----------------- +| +| i Line 10 (literal) +| | leaves: 0 +| | value: A$ (type: string) +| `----------------- +`----------------- +*/ +"INPUT" : {argc:1, f:function(lnum, stmtnum, args) { + if (args.length != 1) throw lang.syntaxfehler(lnum, args.length+lang.aG) + let troValue = args[0].troValue + + // print out prompt text + print("? "); var rh = sys.read().trim() + + // if string we got can be cast to number, do it + // NOTE: empty string will be cast to 0, which corresponds to GW-BASIC + if (!isNaN(rh)) rh = tonum(rh) + + return bS.addAsBasicVar(lnum, troValue, rh) +}}, +"CIN" : {argc:0, f:function(lnum, stmtnum, args) { + return sys.read().trim() +}}, +"END" : {argc:0, f:function(lnum, stmtnum, args) { + serial.println("Program terminated in "+lnum) + return new JumpObj(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER - 1, lnum, undefined) // GOTO far-far-away +}}, +"SPC" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => " ".repeat(lh)) +}}, +"LEFT" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (str, len) => str.substring(0, len)) +}}, +"MID" : {argc:3, f:function(lnum, stmtnum, args) { + return threeArg(lnum, stmtnum, args, (str, start, len) => str.substring(start-INDEX_BASE, start-INDEX_BASE+len)) +}}, +"RIGHT" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (str, len) => str.substring(str.length - len, str.length)) +}}, +"SGN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => (it > 0) ? 1 : (it < 0) ? -1 : 0) +}}, +"ABS" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.abs(it)) +}}, +"SIN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.sin(it)) +}}, +"COS" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.cos(it)) +}}, +"TAN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.tan(it)) +}}, +"EXP" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.exp(it)) +}}, +"ASN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.asin(it)) +}}, +"ACO" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.acos(it)) +}}, +"ATN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.atan(it)) +}}, +"SQR" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.sqrt(it)) +}}, +"CBR" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.cbrt(it)) +}}, +"SINH" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.sinh(it)) +}}, +"COSH" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.cosh(it)) +}}, +"TANH" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.tanh(it)) +}}, +"LOG" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.log(it)) +}}, +"RESTORE" : {argc:0, f:function(lnum, stmtnum, args) { + DATA_CURSOR = 0 +}}, +"READ" : {argc:1, f:function(lnum, stmtnum, args) { + if (args.length != 1) throw lang.syntaxfehler(lnum, args.length+lang.aG) + let troValue = args[0].troValue + + let rh = DATA_CONSTS[DATA_CURSOR++] + if (rh === undefined) throw lang.outOfData(lnum) + + return bS.addAsBasicVar(lnum, troValue, rh) +}}, +"DGET" : {argc:0, f:function(lnum, stmtnum, args) { + let r = DATA_CONSTS[DATA_CURSOR++] + if (r === undefined) throw lang.outOfData(lnum) + return r +}}, +"OPTIONBASE" : {f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => { + if (lh != 0 && lh != 1) throw lang.syntaxfehler(line) + INDEX_BASE = lh|0 + }) +}}, +"DATA" : {f:function(lnum, stmtnum, args) { + if (prescan) { + args.forEach(it => DATA_CONSTS.push(resolve(it))) + } +}}, +/* Syopsis: MAP function, functor + */ +"MAP" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (fn, functor) => { + if (!isRunnable(fn)) throw lang.badFunctionCallFormat(lnum, "first argument is not a function: got "+JStoBASICtype(fn)) + if (!isGenerator(functor) && !Array.isArray(functor)) throw lang.syntaxfehler(lnum, "not a mappable type: "+functor+((typeof functor == "object") ? Object.entries(functor) : "")) + // generator? + if (isGenerator(functor)) functor = genToArray(functor) + + return functor.map(it => bS.getDefunThunk(fn)(lnum, stmtnum, [it])) + }) +}}, +/* Synopsis: FOLD function, init_value, functor + * a function must accept two arguments, of which first argument will be an accumulator + */ +"FOLD" : {argc:3, f:function(lnum, stmtnum, args) { + return threeArg(lnum, stmtnum, args, (fn, init, functor) => { + if (!isRunnable(fn)) throw lang.badFunctionCallFormat(lnum, "first argument is not a function: got "+JStoBASICtype(fn)) + if (!isGenerator(functor) && !Array.isArray(functor)) throw lang.syntaxfehler(lnum, `not a mappable type '${Object.entries(args[2])}': `+functor+((typeof functor == "object") ? Object.entries(functor) : "")) + // generator? + if (isGenerator(functor)) functor = genToArray(functor) + + let akku = init + functor.forEach(it => { + akku = bS.getDefunThunk(fn)(lnum, stmtnum, [akku, it]) + }) + return akku + }) +}}, +/* Syopsis: FILTER function, functor + */ +"FILTER" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (fn, functor) => { + if (!isRunnable(fn)) throw lang.badFunctionCallFormat(lnum, "first argument is not a function: got "+JStoBASICtype(fn)) + if (!isGenerator(functor) && !Array.isArray(functor)) throw lang.syntaxfehler(lnum, `not a mappable type '${Object.entries(args[1])}': `+functor+((typeof functor == "object") ? Object.entries(functor) : (typeof functor))) + // generator? + if (isGenerator(functor)) functor = genToArray(functor) + + return functor.filter(it => bS.getDefunThunk(fn)(lnum, stmtnum, [it])) + }) +}}, +/* GOTO and GOSUB won't work but that's probably the best...? */ +"DO" : {f:function(lnum, stmtnum, args) { + return args[args.length - 1] +}}, +"LABEL" : {f:function(lnum, stmtnum, args) { + if (prescan) { + let labelname = args[0].troValue + if (labelname === undefined) throw lang.syntaxfehler(lnum, "empty LABEL") + gotoLabels[labelname] = lnum + } +}}, +"ON" : {f:function(lnum, stmtnum, args) { + //args: functionName (string), testvalue (SyntaxTreeReturnObj), arg0 (SyntaxTreeReturnObj), arg1 (SyntaxTreeReturnObj), ... + if (args[2] === undefined) throw lang.syntaxfehler(lnum) + + let jmpFun = args.shift() + let testvalue = resolve(args.shift())-INDEX_BASE + + // args must be resolved lazily because jump label is not resolvable + let jmpTarget = args[testvalue] + + if (jmpFun !== "GOTO" && jmpFun !== "GOSUB") + throw lang.badFunctionCallFormat(lnum, `Not a jump statement: ${jmpFun}`) + + if (jmpTarget === undefined) + return undefined + + return bS.builtin[jmpFun].f(lnum, stmtnum, [jmpTarget]) +}}, +"MIN" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (lh,rh) => (lh > rh) ? rh : lh) +}}, +"MAX" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (lh,rh) => (lh < rh) ? rh : lh) +}}, +"GETKEYSDOWN" : {argc:0, f:function(lnum, stmtnum, args) { + let keys = [] + sys.poke(-40, 255) + for (let k = -41; k >= -48; k--) { + keys.push(sys.peek(k)) + } + return keys +}}, +"~<" : {argc:2, f:function(lnum, stmtnum, args) { // CURRY operator + return twoArg(lnum, stmtnum, args, (fn, value) => { + if (!isAST(fn)) throw lang.badFunctionCallFormat(lnum, "left-hand is not a function: got "+JStoBASICtype(fn)) + + if (DBGON) { + serial.println("[BASIC.BUILTIN.CURRY] currying this function tree...") + serial.println(astToString(fn)) + serial.println("[BASIC.BUILTIN.CURRY] with this value: "+value) + serial.println(Object.entries(value)) + } + + let curriedTree = curryDefun(fn, value) + + if (DBGON) { + serial.println("[BASIC.BUILTIN.CURRY] Here's your curried tree:") + serial.println(astToString(curriedTree)) + } + + return curriedTree + }) +}}, +"TYPEOF" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNul(lnum, stmtnum, args, bv => { + if (bv === undefined) return "undefined" + if (bv.bvType === undefined || !(bv instanceof BasicVar)) { + let typestr = JStoBASICtype(bv) + if (typestr == "monad") + return bv.mType+"-"+typestr + else return typestr + } + return bv.bvType + }) +}}, +"LEN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, lh => { + if (lh.length === undefined) throw lang.illegalType() + return lh.length + }) +}}, +"HEAD" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, lh => { + if (lh.length === undefined || lh.length < 1) throw lang.illegalType() + return lh[0] + }) +}}, +"TAIL" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, lh => { + if (lh.length === undefined || lh.length < 1) throw lang.illegalType() + return lh.slice(1, lh.length) + }) +}}, +"INIT" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, lh => { + if (lh.length === undefined || lh.length < 1) throw lang.illegalType() + return lh.slice(0, lh.length - 1) + }) +}}, +"LAST" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, lh => { + if (lh.length === undefined || lh.length < 1) throw lang.illegalType() + return lh[lh.length - 1] + }) +}}, +"CLS" : {argc:0, f:function(lnum, stmtnum, args) { + con.clear() +}}, +"CLPX" : {argc:0, f:function(lnum, stmtnum, args) { + graphics.clearPixels(255) +}}, +"$" : {argc:2, f:function(lnum, stmtnum, args) { + let fn = resolve(args[0]) + let value = resolve(args[1]) // FIXME undefined must be allowed as we cannot distinguish between tree-with-value-of-undefined and just undefined + + if (DBGON) { + serial.println("[BASIC.BUILTIN.APPLY] applying this function tree... "+fn) + serial.println(astToString(fn)) + serial.println("[BASIC.BUILTIN.APPLY] with this value: "+value) + if (value !== undefined) + serial.println(Object.entries(value)) + } + + if (fn.mType == "funseq") { + return getMonadEvalFun(fn)(lnum, stmtnum, [value]) + } + else { + let valueTree = new BasicAST() + valueTree.astLnum = lnum + valueTree.astType = JStoBASICtype(value) + valueTree.astValue = value + + + let newTree = new BasicAST() + newTree.astLnum = lnum + newTree.astValue = fn + newTree.astType = "usrdefun" + newTree.astLeaves = [valueTree] + + if (DBGON) { + serial.println("[BASIC.BUILTIN.APPLY] Here's your applied tree:") + serial.println(astToString(newTree)) + } + + return bF._executeSyntaxTree(lnum, stmtnum, newTree, 0) + } +}}, +"&" : {argc:2, f:function(lnum, stmtnum, args) { + return bS.builtin["$"].f(lnum, stmtnum, [args[1], args[0]].concat(args.slice(2))) +}}, +"REDUCE" : {noprod:1, argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, bv => { + if (isAST(bv)) { + if (DBGON) { + serial.println("[BASIC.BUILTIN.REDUCE] reducing:") + serial.println(astToString(bv)) + /*if (tree.astType == "usrdefun") { + serial.println("[BASIC.BUILTIN.REDUCE] usrdefun unpack:") + serial.println(astToString(tree.astValue)) + }*/ + } + + let reduced = bF._uncapAST(bv, it => { + // TODO beta-eta reduction + return it + }) + + if (DBGON) { + serial.println("[BASIC.BUILTIN.REDUCE] reduced: "+reduced) + serial.println(astToString(reduced)) + } + + // re-wrap because tree-executor wants encapsulated function + let newTree = new BasicAST() + newTree.astLnum = lnum + newTree.astType = JStoBASICtype(reduced) + newTree.astValue = reduced + + return newTree + } + else { + return bv + } + }) +}}, +/** type: m a -> (a -> m b) -> m b + * @param m a monad + * @param fnb a function that takes a monadic value from m and returns a new monad. IT'S ENTIRELY YOUR RESPONSIBILITY TO MAKE SURE THIS FUNCTION TO RETURN RIGHT KIND OF MONAD! + * @return another monad + */ +">>=" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (ma, a_to_mb) => { + if (!isMonad(ma)) throw lang.badFunctionCallFormat(lnum, "left-hand is not a monad: got "+JStoBASICtype(ma)) + if (!isRunnable(a_to_mb)) throw lang.badFunctionCallFormat(lnum, "right-hand is not a usrdefun: got "+JStoBASICtype(a_to_mb)) + + if (DBGON) { + serial.println("[BASIC.BIND] binder:") + serial.println(monadToString(ma)) + serial.println("[BASIC.BIND] bindee:") + serial.println(astToString(a_to_mb)) + } + + let a = ma.mVal + let mb = bS.getDefunThunk(a_to_mb)(lnum, stmtnum, [a]) + + if (!isMonad(mb)) throw lang.badFunctionCallFormat(lnum, "right-hand function did not return a monad") + + if (DBGON) { + serial.println("[BASIC.BIND] bound monad:") + serial.println(monadToString(mb)) + } + + return mb + }) +}}, +/** type: m a -> m b -> m b + * @param m a monad + * @param fnb a function that returns a new monad. IT'S ENTIRELY YOUR RESPONSIBILITY TO MAKE SURE THIS FUNCTION TO RETURN RIGHT KIND OF MONAD! + * @return another monad + */ +">>~" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (ma, mb) => { + if (!isMonad(ma)) throw lang.badFunctionCallFormat(lnum, "left-hand is not a monad: got "+JStoBASICtype(ma)) + if (!isMonad(mb)) throw lang.badFunctionCallFormat(lnum, "right-hand is not a monad: got "+JStoBASICtype(mb)) + + if (DBGON) { + serial.println("[BASIC.BIND] binder:") + serial.println(monadToString(ma)) + serial.println("[BASIC.BIND] bindee:") + serial.println(monadToString(mb)) + } + + let a = ma.mVal + let b = mb.mVal + + return mb + }) +}}, +/** type: (b -> c) -> (a -> b) -> (a -> c) + * @param fa a function or a funseq-monad + * @param fb a function or a funseq-monad + * @return another monad + */ +"." : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (fa, fb) => { + if (!isRunnable(fa)) throw lang.badFunctionCallFormat(lnum, "left-hand is not a function/funseq: got"+JStoBASICtype(fa)) + if (!isRunnable(fb)) throw lang.badFunctionCallFormat(lnum, "left-hand is not a function/funseq: got"+JStoBASICtype(fb)) + + let ma = (isAST(fa)) ? [fa] : fa.mVal + let mb = (isAST(fb)) ? [fb] : fb.mVal + + let mc = mb.concat(ma) + return new BasicFunSeq(mc) + }) +}}, +"MLIST" : {noprod:1, argc:1, f:function(lnum, stmtnum, args) { + return oneArgNul(lnum, stmtnum, args, fn => { + return new BasicListMonad([fn]) + }) +}}, +"MRET" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNul(lnum, stmtnum, args, fn => { + return new BasicMemoMonad(fn) + }) +}}, +"MJOIN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, m => { + if (!isMonad(m)) throw lang.illegalType(lnum, m) + return m.mVal + }) +}}, +/*"MEVAL" : {argc:1, f:function(lnum, stmtnum, args) { + return varArg(lnum, stmtnum, args, rgs => { + let m = rgs[0] + let args = rgs.slice(1, rgs.length) + return getMonadEvalFun(m)(lnum, stmtnum, args) + }) +}},*/ +"GOTOYX" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (y, x) => { + con.move(y + (1-INDEX_BASE),x + (1-INDEX_BASE)) + }) +}}, +"TEXTFORE" : {argc:2, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, col => { + print(String.fromCharCode(27,91)+"38;5;"+(col|0)+"m") + }) +}}, +"TEXTBACK" : {argc:2, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, col => { + print(String.fromCharCode(27,91)+"48;5;"+(col|0)+"m") + }) +}}, +/** type: (list of function) <*> (a functor) + * Sequnetial application + * + * Can be implemented on pure TerranBASIC using: + * APL = [FS, XS, ARR] ~> IF (LEN(FS) == 0) THEN ARR ELSE APL(TAIL(FS), XS, (IF (ARR == UNDEFINED) THEN {} ELSE ARR) # MAP(HEAD(FS), XS)) + */ +"<*>" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (fns, functor) => { + if (!Array.isArray(fns) || !isRunnable(fns[0])) throw lang.badFunctionCallFormat(lnum, "first argument is not a function: got "+JStoBASICtype(fns)) + if (!isGenerator(functor) && !Array.isArray(functor)) throw lang.syntaxfehler(lnum, "not a mappable type: "+functor+((typeof functor == "object") ? Object.entries(functor) : "")) + // generator? + if (isGenerator(functor)) functor = genToArray(functor) + + let ret = [] + fns.forEach(fn => ret = ret.concat(functor.map(it => bS.getDefunThunk(fn)(lnum, stmtnum, [it])))) + return ret + }) +}}, +/** type: (a function) <*> (a functor) + * Infix MAP + */ +"<$>" : {argc:2, f:function(lnum, stmtnum, args) { + return bS.builtin.MAP.f(lnum, stmtnum, args) +}}, +/** type: (a function/list of functions) <~> (a functor) + * SEQUENTIAL CURRY-MAP + * + * returns a list of functions curried with each element of the functor + */ +"<~>" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (fns, functor) => { + if (!isRunnable(fns) && !(Array.isArray(fns) && isRunnable(fns[0]))) throw lang.badFunctionCallFormat(lnum, "first argument is not a function: got "+JStoBASICtype(fns)) + if (!isGenerator(functor) && !Array.isArray(functor)) throw lang.syntaxfehler(lnum, "not a mappable type: "+functor+((typeof functor == "object") ? Object.entries(functor) : "")) + // generator? + if (isGenerator(functor)) functor = genToArray(functor) + // single function? + if (!Array.isArray(fns)) fns = [fns] + + let ret = [] + fns.forEach(fn => ret = ret.concat(functor.map(it => bS.builtin["~<"].f(lnum, stmtnum, [fn, it])))) + return ret + }) +}}, +"OPTIONDEBUG" : {f:function(lnum, stmtnum, args) { + oneArgNum(lnum, stmtnum, args, (lh) => { + if (lh != 0 && lh != 1) throw lang.syntaxfehler(line) + DBGON = (1 == lh|0) + }) +}}, +"OPTIONTRACE" : {f:function(lnum, stmtnum, args) { + oneArgNum(lnum, stmtnum, args, (lh) => { + if (lh != 0 && lh != 1) throw lang.syntaxfehler(line) + TRACEON = (1 == lh|0) + }) +}}, +"PRINTMONAD" : {debugonly:1, argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, (it) => { + println(monadToString(it)) + }) +}}, +"RESOLVE" : {debugonly:1, argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, (it) => { + if (isAST(it)) { + println(lnum+" RESOLVE PRINTTREE") + println(astToString(it)) + if (typeof it.astValue == "object") { + if (isAST(it.astValue)) { + println(lnum+" RESOLVE PRINTTREE ASTVALUE PRINTTREE") + println(astToString(it.astValue)) + } + else { + println(lnum+" RESOLVE PRINTTREE ASTVALUE") + println(it.astValue) + } + } + } + else + println(it) + }) +}}, +"RESOLVEVAR" : {debugonly:1, argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, (it) => { + let v = bS.vars[args[0].troValue] + if (v === undefined) println("Undefined variable: "+args[0].troValue) + else println(`type: ${v.bvType}, value: ${v.bvLiteral}`) + }) +}}, +"UNRESOLVE" : {debugonly:1, argc:1, f:function(lnum, stmtnum, args) { + println(args[0]) +}}, +"UNRESOLVE0" : {debugonly:1, argc:1, f:function(lnum, stmtnum, args) { + println(Object.entries(args[0])) +}}, +"TOJSON" : {debugonly:1, argc:1, f:function(lnum, stmtnum, args) { + println(JSON.stringify(resolve(args[0]))) +}}, +} +Object.freeze(bS.builtin) +let bF = {} // BASIC functions +bF._1os = {"!":1,"~":1,"#":1,"<":1,"=":1,">":1,"*":1,"+":1,"-":1,"/":1,"^":1,":":1,"$":1,".":1,"@":1,"\\":1,"%":1,"|":1,"`":1} +//bF._2os = {"<":1,"=":1,">":1,"~":1,"-":1,"$":1,"*":1,"!":1,"#":1} +//bF._3os = {"<":1,"=":1,">":1,"~":1,"-":1,"$":1,"*":1} +bF._uos = {"+":1,"-":1,"NOT":1,"BNOT":1,"^":1,"@":1,"`":1} +bF._isNum = function(code) { + return (code >= 0x30 && code <= 0x39) || code == 0x5F +} +bF._isNum2 = function(code) { + return (code >= 0x30 && code <= 0x39) || code == 0x5F || (code >= 0x41 && code <= 0x46) || (code >= 0x61 && code <= 0x66) +} +bF._isNumSep = function(code) { + return code == 0x2E || code == 0x42 || code == 0x58 || code == 0x62 || code == 0x78 +} +bF._is1o = function(code) { + return bF._1os[String.fromCharCode(code)] +} +/*bF._is2o = function(code) { + return bF._2os[String.fromCharCode(code)] +} +bF._is3o = function(code) { + return bF._3os[String.fromCharCode(code)] +}*/ +bF._isUnary = function(code) { + return bF._uos[String.fromCharCode(code)] +} +bF._isParenOpen = function(code) { + return (code == 0x28 || code == 0x5B || code == 0x7B) || (code == '(' || code == '[' || code == '{') +} +bF._isParenClose = function(code) { + return (code == 0x29 || code == 0x5D || code == 0x7D) || (code == ')' || code == ']' || code == '}') +} +bF._isMatchingParen = function(open, close) { + return (open == '(' && close == ')' || open == '[' && close == ']' || open == '{' && close == '}') +} +bF._isParen = function(code) { + return bF._isParenOpen(code) || bF._isParenClose(code) +} +bF._isSep = function(code) { + return code == 0x2C || code == 0x3B +} +// define operator precedence here... +// NOTE: do NOT put falsy value (e.g. 0) here!! +bF._opPrc = { + // function call in itself has highest precedence + "`":10, // MJOIN + "^":20, + "*":30,"/":30,"\\":20, + "MOD":40, + "+":50,"-":50, + "NOT":60,"BNOT":60, + "<<":70,">>":70, + "<":80,">":80,"<=":80,"=<":80,">=":80,"=>":80, + "==":90,"<>":90,"><":90, + "MIN":100,"MAX":100, + "BAND":200, + "BXOR":201, + "BOR":202, + "AND":300, + "OR":301, + "TO":400,"STEP":401, + "!":500, + "~":501, // array CONS and PUSH + "#":502, // array concat + ".": 600, // compo operator + "$": 600, // apply operator + "&": 600, // pipe operator + "~<": 601, // curry operator + "<$>": 602, // infix map operator + "<*>": 602, // sequential application operator + "<~>": 602, // infix curry-map operator + "@":700, // MRET + "~>": 1000, // closure operator + ">>~": 1000, // monad sequnce operator + ">>=": 1000, // monad bind operator + "=":9999,"IN":9999 +} // when to ops have same index of prc but different in associativity, right associative op gets higher priority (at least for the current parser implementation) +bF._opRh = {"^":1,"=":1,"!":1,"IN":1,"~>":1,"$":1,".":1,">>=":1,">>~":1,">!>":1,"@":1,"`":1,"<$>":1} // ~< and ~> cannot have same associativity +// these names appear on executeSyntaxTree as "exceptional terms" on parsing (regular function calls are not "exceptional terms") +bF._tokenise = function(lnum, cmd) { + var _debugprintStateTransition = false + var k + var tokens = [] + var states = [] + var sb = "" + var mode = "lit" // lit, qot, paren, sep, op, num; operator2, numbersep, number2, limbo, escape, quote_end + + // NOTE: malformed numbers (e.g. "_b3", "_", "__") must be re-marked as literal or syntax error in the second pass + + if (_debugprintStateTransition) println("@@ TOKENISE @@") + if (_debugprintStateTransition) println("Ln "+lnum+" cmd "+cmd) + + // TOKENISE + for (k = 0; k < cmd.length; k++) { + var char = cmd[k] + var charCode = cmd.charCodeAt(k) + + if (_debugprintStateTransition) print("Char: "+char+"("+charCode+"), state: "+mode) + + if ("lit" == mode) { + if (0x22 == charCode) { // " + tokens.push(sb); sb = ""; states.push(mode) + mode = "qot" + } + else if (bF._isParen(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "paren" + } + else if (" " == char) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "limbo" + } + else if (bF._isSep(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "sep" + } + /*else if (bF._isNum(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "num" + }*/ + else if (bF._is1o(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "op" + } + else { + sb += char + } + } + else if ("num" == mode) { + if (bF._isNum(charCode)) { + sb += char + } + else if (bF._isNumSep(charCode)) { + sb += char + mode = "nsep" + } + else if (0x22 == charCode) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "qot" + } + else if (" " == char) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "limbo" + } + else if (bF._isParen(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "paren" + } + else if (bF._isSep(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "sep" + } + else if (bF._is1o(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "op" + } + else { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "lit" + } + } + else if ("nsep" == mode) { + if (bF._isNum2(charCode)) { + sb += char + mode = "n2" + } + else { + throw lang.syntaxfehler(lnum, lang.badNumberFormat) + } + } + else if ("n2" == mode) { + if (bF._isNum2(charCode)) { + sb += char + } + else if (0x22 == charCode) { + tokens.push(sb); sb = ""; states.push("num") + mode = "qot" + } + else if (" " == char) { + tokens.push(sb); sb = ""; states.push("num") + mode = "limbo" + } + else if (bF._isParen(charCode)) { + tokens.push(sb); sb = "" + char; states.push("num") + mode = "paren" + } + else if (bF._isSep(charCode)) { + tokens.push(sb); sb = "" + char; states.push("num") + mode = "sep" + } + else if (bF._is1o(charCode)) { + tokens.push(sb); sb = "" + char; states.push("num") + mode = "op" + } + else { + tokens.push(sb); sb = "" + char; states.push("num") + mode = "lit" + } + } + else if ("op" == mode) { + if (bF._is1o(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "op" + } + else if (bF._isUnary(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + } + else if (bF._isNum(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "num" + } + else if (0x22 == charCode) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "qot" + } + else if (" " == char) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "limbo" + } + else if (bF._isParen(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "paren" + } + else if (bF._isSep(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "sep" + } + else { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "lit" + } + } + else if ("qot" == mode) { + if (0x22 == charCode) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "quote_end" + } + /*else if (charCode == 0x5C) { // reverse solidus + tokens.push(sb); sb = "" + mode = "escape" + }*/ + else { + sb += char + } + } + /*else if ("escape" == mode) { + if (0x5C == charCode) // reverse solidus + sb += String.fromCharCode(0x5C) + else if ("n" == char) + sb += String.fromCharCode(0x0A) + else if ("t" == char) + sb += String.fromCharCode(0x09) + else if (0x22 == charCode) // " + sb += String.fromCharCode(0x22) + else if (0x27 == charCode) + sb += String.fromCharCode(0x27) + else if ("e" == char) + sb += String.fromCharCode(0x1B) + else if ("a" == char) + sb += String.fromCharCode(0x07) + else if ("b" == char) + sb += String.fromCharCode(0x08) + mode = "qot" // ESCAPE is only legal when used inside of quote + }*/ + else if ("quote_end" == mode) { + if (" " == char) { + sb = "" + mode = "limbo" + } + else if (0x22 == charCode) { + sb = "" + char + mode = "qot" + } + else if (bF._isParen(charCode)) { + sb = "" + char + mode = "paren" + } + else if (bF._isSep(charCode)) { + sb = "" + char + mode = "sep" + } + else if (bF._isNum(charCode)) { + sb = "" + char + mode = "num" + } + else if (bF._is1o(charCode)) { + sb = "" + char + mode = "op" + } + else { + sb = "" + char + mode = "lit" + } + } + else if ("limbo" == mode) { + if (char == " ") { + /* do nothing */ + } + else if (0x22 == charCode) { + mode = "qot" + } + else if (bF._isParen(charCode)) { + sb = "" + char + mode = "paren" + } + else if (bF._isSep(charCode)) { + sb = "" + char + mode = "sep" + } + else if (bF._isNum(charCode)) { + sb = "" + char + mode = "num" + } + else if (bF._is1o(charCode)) { + sb = "" + char + mode = "op" + } + else { + sb = "" + char + mode = "lit" + } + } + else if ("paren" == mode) { + if (char == " ") { + tokens.push(sb); sb = ""; states.push(mode) + mode = "limbo" + } + else if (0x22 == charCode) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "qot" + } + else if (bF._isParen(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "paren" + } + else if (bF._isSep(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "sep" + } + else if (bF._isNum(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "num" + } + else if (bF._is1o(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "op" + } + else { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "lit" + } + } + else if ("sep" == mode) { + if (char == " ") { + tokens.push(sb); sb = ""; states.push(mode) + mode = "limbo" + } + else if (0x22 == charCode) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "qot" + } + else if (bF._isParen(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "paren" + } + else if (bF._isSep(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "sep" + } + else if (bF._isNum(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "num" + } + else if (bF._is1o(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "op" + } + else { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "lit" + } + } + else { + throw Error("Unknown parser state: " + mode) + } + + if (_debugprintStateTransition) println("->"+mode) + } + + if (sb.length > 0) { + tokens.push(sb); states.push(mode) + } + + // filter off initial empty token if the statement does NOT start with literal (e.g. "-3+5") + if (tokens[0].length == 0) { + tokens = tokens.slice(1, tokens.length) + states = states.slice(1, states.length) + } + // clean up operator2 and number2 + for (k = 0; k < states.length; k++) { + if (states[k] == "o2" || states[k] == "o3") states[k] = "op" + else if (states[k] == "n2" || states[k] == "nsep") states[k] = "num" + } + + if (tokens.length != states.length) { + throw new BASICerror("size of tokens and states does not match (line: "+lnum+")\n"+ + tokens+"\n"+states) + } + + return { "tokens": tokens, "states": states } +} +bF._parserElaboration = function(lnum, ltokens, lstates) { + let _debugprintElaboration = (!PROD) && true + if (_debugprintElaboration) serial.println("@@ ELABORATION @@") + + let tokens = cloneObject(ltokens) + let states = cloneObject(lstates) + + let k = 0 + + // NOTE: malformed numbers (e.g. "_b3", "_", "__") must be re-marked as literal or syntax error + + while (k < states.length) { // using while loop because array size will change during the execution + // turn errenously checked as number back into a literal + if (states[k] == "num" && !reNumber.test(tokens[k])) + states[k] = "lit" + // turn back into an op if operator is errenously checked as a literal + else if (states[k] == "lit" && bF._opPrc[tokens[k].toUpperCase()] !== undefined) + states[k] = "op" + // turn TRUE and FALSE into boolean + else if ((tokens[k].toUpperCase() == "TRUE" || tokens[k].toUpperCase() == "FALSE") && states[k] == "paren") + states[k] = "bool" + + // decimalise hex/bin numbers (because Nashorn does not support binary literal) + if (states[k] == "num") { + if (tokens[k].toUpperCase().startsWith("0B")) { + tokens[k] = parseInt(tokens[k].substring(2, tokens[k].length), 2) + "" + } + } + + k += 1 + } + + k = 0; let l = states.length + while (k < l) { + let lookahead012 = tokens[k]+tokens[k+1]+tokens[k+2] + let lookahead01 = tokens[k]+tokens[k+1] + + // turn three consecutive ops into a trigraph + if (k < states.length - 3 && states[k] == "op" && states[k+1] == "op" && states[k+2] == "op" && bF._opPrc[lookahead012]) { + if (_debugprintElaboration) serial.println(`[ParserElaboration] Line ${lnum}: Trigraph (${lookahead012}) found starting from the ${lang.ord(k+1)} token of [${tokens}]`) + + tokens[k] = lookahead012 + + // remove two future elements by splicing them + let oldtkn = cloneObject(tokens) + let oldsts = cloneObject(states) + tokens = oldtkn.slice(0, k+1).concat(oldtkn.slice(k+3, oldtkn.length)) + states = oldsts.slice(0, k+1).concat(oldsts.slice(k+3, oldsts.length)) + l -= 2 + } + // turn two consecutive ops into a digraph + else if (k < states.length - 2 && states[k] == "op" && states[k+1] == "op" && bF._opPrc[lookahead01]) { + if (_debugprintElaboration) serial.println(`[ParserElaboration] Line ${lnum}: Digraph (${lookahead01}) found starting from the ${lang.ord(k+1)} token of [${tokens}]`) + + tokens[k] = lookahead01 + + // remove two future elements by splicing them + let oldtkn = cloneObject(tokens) + let oldsts = cloneObject(states) + tokens = oldtkn.slice(0, k+1).concat(oldtkn.slice(k+2, oldtkn.length)) + states = oldsts.slice(0, k+1).concat(oldsts.slice(k+2, oldsts.length)) + l -= 1 + } + // turn (:) into a seq + else if (tokens[k] == ":" && states[k] == "op") + states[k] = "seq" + + k += 1 + } + + return {"tokens":tokens, "states":states} +} +/** + * Destructively transforms an AST (won't unpack capsulated trees by default) + * + * To NOT modify the tree, make sure you're not modifying any properties of the object */ +bF._recurseApplyAST = function(tree, action) { + if (!isAST(tree)) throw new BASICerror(`tree is not a AST (${tree})`) + + if (tree.astLeaves !== undefined && tree.astLeaves[0] === undefined) { + /*if (DBGON) { + serial.println(`RECURSE astleaf`) + serial.println(astToString(tree)) + }*/ + + return action(tree) || tree + } + else { + let newLeaves = tree.astLeaves.map(it => bF._recurseApplyAST(it, action)) + + /*if (DBGON) { + serial.println(`RECURSE ast`) + serial.println(astToString(tree)) + }*/ + + let newTree = action(tree) + + if (newTree !== undefined) { + tree.astLnum = newTree.astLnum + tree.astValue = newTree.astValue + tree.astSeps = newTree.astSeps + tree.astType = newTree.astType + // weave astLeaves + for (let k = 0; k < tree.astLeaves.length; k++) { + if (newLeaves[k] !== undefined) tree.astLeaves[k] = newLeaves[k] + } + } + } +} +/** + * Returns a copy of BasicAST where its 'capsulated' trees are fully uncapsulated. + */ +bF._uncapAST = function(tree, action) { + let expr = cloneObject(tree) + bF._recurseApplyAST(expr, it => { + if (isAST(it.astValue)) { + let capTree = bF._uncapAST(it.astValue, action) + it.astLnum = capTree.astLnum + it.astValue = capTree.astValue + it.astSeps = capTree.astSeps + it.astType = capTree.astType + it.astLeaves = capTree.astLeaves + } + + return action(it) + }) + action(expr) + return expr +} +/** EBNF notation: +(* quick reference to EBNF *) +(* { word } = word is repeated 0 or more times *) +(* [ word ] = word is optional (repeated 0 or 1 times) *) + +line = + linenumber , stmt , {":" , stmt} + | linenumber , "REM" , ? basically anything ? +linenumber = digits + +stmt = + "REM" , ? anything ? + | "IF" , expr_sans_asgn , "THEN" , stmt , ["ELSE" , stmt] + | "DEFUN" , [ident] , "(" , [ident , {" , " , ident}] , ")" , "=" , expr + | "ON" , expr_sans_asgn , ("GOTO" | "GOSUB") , expr_sans_asgn , {"," , expr_sans_asgn} + | "(" , stmt , ")" + | expr ; (* if the statement is 'lit' and contains only one word, treat it as function_call + e.g. NEXT for FOR loop *) + +expr = (* this basically blocks some funny attemps such as using DEFUN as anon function + because everything is global in BASIC *) + ? empty string ? + | lit + | "{" , [expr , {"," , expr}] , "}" + | "(" , expr , ")" + | ident_tuple + | "IF" , expr_sans_asgn , "THEN" , expr , ["ELSE" , expr] + | ("FOR"|"FOREACH") , expr + | expr , op , expr + | op_uni , expr + | kywd , expr - "(" + | function_call + +expr_sans_asgn = ? identical to expr except errors out whenever "=" is found ? + +ident_tuple = "[" , ident , {"," , ident} , "]" + +function_call = + ident , "(" , [expr , {argsep , expr} , [argsep]] , ")" + | ident , expr , {argsep , expr} , [argsep] +kywd = ? words that exists on the list of predefined function that are not operators ? + +(* don't bother looking at these, because you already know the stuff *) + +argsep = "," | ";" +ident = alph , [digits] ; (* variable and function names *) +lit = alph , [digits] | num | string ; (* ident + numbers and string literals *) +op = "^" | "*" | "/" | "\" | "MOD" | "+" | "-" | "<<" | ">>" | "<" | ">" + | "<=" | "=<" | ">=" | "=>" | "==" | "<>" | "><" | "MIN" | "MAX" | "BAND" | "BXOR" | "BOR" + | "AND" | "OR" | "TO" | "STEP" | "!" | "~" | "#" | "." | "$" | "&" | "~<" | "<$>" | "<*>" + | "<~>" | "~>" | ">>~" | ">>=" | "=" +op_uni = "-" | "+" | "NOT" | "BNOT" | "`" | "@" + +alph = letter | letter , alph +digits = digit | digit , digits +hexdigits = hexdigit | hexdigit , hexdigits +bindigits = bindigit | bindigit , bindigits +num = digits | digits , "." , [digits] | "." , digits + | ("0x"|"0X") , hexdigits + | ("0b"|"0B") , bindigits ; (* sorry, no e-notation! *) +visible = ? ASCII 0x20 to 0x7E ? +string = '"' , {visible} , '"' + +letter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" + | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "a" | "b" + | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" + | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "_" +digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" +hexdigit = "A" | "B" | "C" | "D" | "E" | "F" | "a" | "b" | "c" | "d" | "e" | "f" | "0" | "1" + | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" +bindigit = "0" | "1" + +(* all possible token states: lit num op bool qot paren sep *) +(* below are schematic of trees generated after parsing the statements *) + +IF (type: function, value: IF) +1. cond +2. true +[3. false] + +FOR (type: function, value: FOR) +1. expr (normally (=) but not necessarily) + +DEFUN (type: function, value: DEFUN) +1. funcname (type: lit) + 1. arg0 (type: lit) + [2. arg1] + [3. argN...] +2. stmt + +ON (type: function, value: ON) +1. testvalue +2. functionname (type: lit) +3. arg0 +[4. arg1] +[5. argN...] + +FUNCTION_CALL (type: function, value: PRINT or something) +1. arg0 +2. arg1 +[3. argN...] + +LAMBDA (type: op, value: "~>") +1. undefined (type: closure_args, value: undefined) + 1. arg0 (type: lit) + [2. arg1] + [3. argN...] +2. stmt + +ARRAY CONSTRUCTOR (type: function, value: undefined) +1. 0th element of the array +2. 1st element of the array +[3. Nth element of the array...] + */ +// @returns BasicAST +bF._EquationIllegalTokens = ["IF","THEN","ELSE","DEFUN","ON"] +bF.isSemanticLiteral = function(token, state) { + return undefined == token || "]" == token || ")" == token || "}" == token || + "qot" == state || "num" == state || "bool" == state || "lit" == state +} +bF.parserDoDebugPrint = (!PROD) && true +bF.parserPrintdbg = any => { if (bF.parserDoDebugPrint) serial.println(any) } +bF.parserPrintdbg2 = function(icon, lnum, tokens, states, recDepth) { + if (bF.parserDoDebugPrint) { + let treeHead = "| ".repeat(recDepth) + bF.parserPrintdbg(`${icon}${lnum} ${treeHead}${tokens.join(' ')}`) + bF.parserPrintdbg(`${icon}${lnum} ${treeHead}${states.join(' ')}`) + } +} +bF.parserPrintdbgline = function(icon, msg, lnum, recDepth) { + if (bF.parserDoDebugPrint) { + let treeHead = "| ".repeat(recDepth) + bF.parserPrintdbg(`${icon}${lnum} ${treeHead}${msg}`) + } +} + +// ## USAGE OF lambdaBoundVars IN PARSEMODE STARTS HERE ## + +/** + * @return ARRAY of BasicAST + */ +bF._parseTokens = function(lnum, tokens, states) { + if (tokens.length !== states.length) throw Error("unmatched tokens and states length") + + bF.parserPrintdbg2('Line ', lnum, tokens, states, 0) + + if (tokens.length !== states.length) throw lang.syntaxfehler(lnum) + if (tokens[0].toUpperCase() == "REM" && states[0] != "qot") return + + /*************************************************************************/ + + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + let seps = [] + + // scan for parens and (:)s + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (tokens[k] == "(" && states[k] == "paren") { + parenDepth += 1 + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // decrease paren depth + else if (tokens[k] == ")" && states[k] == "paren") { + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + parenDepth -= 1 + } + + if (parenDepth == 0 && tokens[k] == ":" && states[k] == "seq") + seps.push(k) + } + + let startPos = [0].concat(seps.map(k => k+1)) + let stmtPos = startPos.map((s,i) => {return{start:s, end:(seps[i] || tokens.length)}}) // use end of token position as separator position + + return stmtPos.map((x,i) => { + if (stmtPos.length > 1) + bF.parserPrintdbgline('Line ', 'Statement #'+(i+1), lnum, 0) + + // check for empty tokens + if (x.end - x.start <= 0) throw new ParserError("Malformed Line") + + let tree = bF._parseStmt(lnum, + tokens.slice(x.start, x.end), + states.slice(x.start, x.end), + 1 + ) + + bF.parserPrintdbgline('Tree in ', '\n'+astToString(tree), lnum, 0) + + return tree + }) +} +/** Parses following EBNF rule: +stmt = + "IF" , expr_sans_asgn , "THEN" , stmt , ["ELSE" , stmt] + | "DEFUN" , [ident] , "(" , [ident , {" , " , ident}] , ")" , "=" , expr + | "ON" , expr_sans_asgn , ident , expr_sans_asgn , {"," , expr_sans_asgn} + | "(" , stmt , ")" + | expr ; (* if the statement is 'lit' and contains only one word, treat it as function_call e.g. NEXT for FOR loop *) + * @return: BasicAST + */ +bF._parseStmt = function(lnum, tokens, states, recDepth) { + bF.parserPrintdbg2('$', lnum, tokens, states, recDepth) + + /*************************************************************************/ + + // case for: single word (e.g. NEXT for FOR loop) + if (tokens.length == 1 && states.length == 1) { + bF.parserPrintdbgline('$', "Single Word Function Call", lnum, recDepth) + return bF._parseLit(lnum, tokens, states, recDepth + 1, true) + } + + /*************************************************************************/ + + let headTkn = tokens[0].toUpperCase() + let headSta = states[0] + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + + /*************************************************************************/ + + // case for: "REM" , ? anything ? + if (headTkn == "REM" && headSta != "qot") return + + /*************************************************************************/ + + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + let onGoPos = -1 + let sepsZero = [] + let sepsOne = [] + + // scan for parens that will be used for several rules + // also find nearest THEN and ELSE but also take parens into account + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (tokens[k] == "(" && states[k] == "paren") { + parenDepth += 1 + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // decrease paren depth + else if (tokens[k] == ")" && states[k] == "paren") { + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + parenDepth -= 1 + } + + if (parenDepth == 0 && states[k] == "sep") + sepsZero.push(k) + if (parenDepth == 1 && states[k] == "sep") + sepsOne.push(k) + + if (parenDepth == 0) { + let tok = tokens[k].toUpperCase() + if (-1 == onGoPos && ("GOTO" == tok || "GOSUB" == tok) && "lit" == states[k]) + onGoPos = k + } + } + + // unmatched brackets, duh! + if (parenDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + + /*************************************************************************/ + + // ## case for: + // "IF" , expr_sans_asgn , "THEN" , stmt , ["ELSE" , stmt] + try { + bF.parserPrintdbgline('$', "Trying IF Statement...", lnum, recDepth) + return bF._parseIfMode(lnum, tokens, states, recDepth + 1, false) + } + // if ParserError is raised, continue applying other rules + catch (e) { + if (!(e instanceof ParserError)) throw e + bF.parserPrintdbgline('$', 'It was NOT!', lnum, recDepth) + } + + /*************************************************************************/ + + // ## case for: + // | "DEFUN" , [ident] , "(" , [ident , {" , " , ident}] , ")" , "=" , expr + if ("DEFUN" == headTkn && "lit" == headSta && + parenStart == 2 && tokens[parenEnd + 1] == "=" && states[parenEnd + 1] == "op" + ) { + bF.parserPrintdbgline('$', 'DEFUN Stmt', lnum, recDepth) + + treeHead.astValue = "DEFUN" + treeHead.astType = "function" + + // parse function name + if (tokens[1] == "(") { + // anonymous function + treeHead.astLeaves[0] = new BasicAST() + treeHead.astLeaves[0].astLnum = lnum + treeHead.astLeaves[0].astType = "lit" + } + else { + bF.parserPrintdbgline('$', 'DEFUN Stmt Function Name:', lnum, recDepth) + treeHead.astLeaves[0] = bF._parseIdent(lnum, [tokens[1]], [states[1]], recDepth + 1) + } + + // parse function arguments + bF.parserPrintdbgline('$', 'DEFUN Stmt Function Arguments -- ', lnum, recDepth) + let defunArgDeclSeps = sepsOne.filter((i) => i < parenEnd + 1).map(i => i-1).concat([parenEnd - 1]) + bF.parserPrintdbgline('$', 'DEFUN Stmt Function Arguments comma position: '+defunArgDeclSeps, lnum, recDepth) + + treeHead.astLeaves[0].astLeaves = defunArgDeclSeps.map(i=>bF._parseIdent(lnum, [tokens[i]], [states[i]], recDepth + 1)) + + // parse function body + let parseFunction = bF._parseExpr + treeHead.astLeaves[1] = parseFunction(lnum, + tokens.slice(parenEnd + 2, tokens.length), + states.slice(parenEnd + 2, states.length), + recDepth + 1 + ) + + return treeHead + } + + /*************************************************************************/ + + // ## case for: + // | "ON" , if_equation , ident , if_equation , {"," , if_equation} + if ("ON" == headTkn && "lit" == headSta) { + bF.parserPrintdbgline('$', 'ON Stmt', lnum, recDepth) + + if (onGoPos == -1) throw ParserError("Malformed ON Statement") + + treeHead.astValue = "ON" + treeHead.astType = "function" + + // parse testvalue + let testvalue = bF._parseExpr(lnum, + tokens.slice(1, onGoPos), + states.slice(1, onGoPos), + recDepth + 1, + true + ) + + // parse functionname + let functionname = bF._parseExpr(lnum, + [tokens[onGoPos]], + [states[onGoPos]], + recDepth + 1, + true + ) + + // parse arguments + // get list of comma but filter ones appear before GOTO/GOSUB + let onArgSeps = sepsZero.filter(i => (i > onGoPos)) + let onArgStartPos = [onGoPos + 1].concat(onArgSeps.map(k => k + 1)) + let onArgPos = onArgStartPos.map((s,i) => {return{start:s, end: (onArgSeps[i] || tokens.length)}}) // use end of token position as separator position + + // recursively parse expressions + treeHead.astLeaves = [testvalue, functionname].concat(onArgPos.map((x,i) => { + bF.parserPrintdbgline('$', 'ON GOTO/GOSUB Arguments #'+(i+1), lnum, recDepth) + + // check for empty tokens + if (x.end - x.start <= 0) throw new ParserError("Malformed ON arguments") + + return bF._parseExpr(lnum, + tokens.slice(x.start, x.end), + states.slice(x.start, x.end), + recDepth + 1, + true + ) + })) + + return treeHead + } + + /*************************************************************************/ + + // ## case for: + // | "(" , stmt , ")" + if (parenStart == 0 && parenEnd == tokens.length - 1) { + bF.parserPrintdbgline('$', '( Stmt )', lnum, recDepth) + return bF._parseStmt(lnum, + tokens.slice(parenStart + 1, parenEnd), + states.slice(parenStart + 1, parenEnd), + recDepth + 1 + ) + } + + /*************************************************************************/ + + // ## case for: + // | expr + try { + bF.parserPrintdbgline('$', 'Trying Expression Call...', lnum, recDepth) + return bF._parseExpr(lnum, tokens, states, recDepth + 1) + } + catch (e) { + bF.parserPrintdbgline('$', 'Error!', lnum, recDepth) + throw new ParserError("Statement cannot be parsed in "+lnum+": "+e.stack) + } + + /*************************************************************************/ + + throw new ParserError("Statement cannot be parsed in "+lnum) +} // END of STMT +/** Parses following EBNF rule: +expr = (* this basically blocks some funny attemps such as using DEFUN as anon function because everything is global in BASIC *) + ? empty string ? + | lit + | "{" , [expr , {"," , expr}] , "}" + | "(" , expr , ")" + | ident_tuple + | "IF" , expr_sans_asgn , "THEN" , expr , ["ELSE" , expr] + | ("FOR"|"FOREACH") , expr + | expr , op , expr + | op_uni , expr + | kywd , expr - "(" + | function_call + + * @return: BasicAST + */ +bF._parseExpr = function(lnum, tokens, states, recDepth, ifMode) { + bF.parserPrintdbg2('e', lnum, tokens, states, recDepth) + + /*************************************************************************/ + + // ## special case for virtual dummy element (e.g. phantom element on "PRINT SPC(20);") + if (tokens[0] === undefined && states[0] === undefined) { + let treeHead = new BasicAST() + treeHead.astLnum = lnum + treeHead.astValue = undefined + treeHead.astType = "null" + + return treeHead + } + + /*************************************************************************/ + + let headTkn = tokens[0].toUpperCase() + let headSta = states[0] + + /*************************************************************************/ + + // ## case for: + // lit + if (!bF._EquationIllegalTokens.includes(headTkn) && tokens.length == 1) { + bF.parserPrintdbgline('e', 'Literal Call', lnum, recDepth) + return bF._parseLit(lnum, tokens, states, recDepth + 1) + } + + /*************************************************************************/ + + // scan for operators with highest precedence, use rightmost one if multiple were found + let topmostOp + let topmostOpPrc = 0 + let operatorPos = -1 + + // find and mark position of parentheses + // properly deal with the nested function calls + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + let curlyDepth = 0 + let curlyStart = -1 + let curlyEnd = -1 + let uptkn = "" + + // Scan for unmatched parens and mark off the right operator we must deal with + // every function_call need to re-scan because it is recursively called + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (tokens[k] == "(" && states[k] == "paren") { + parenDepth += 1 + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // increase curly depth and mark curly start position + else if (tokens[k] == "{" && states[k] == "paren") { + curlyDepth += 1 + if (curlyStart == -1 && curlyDepth == 1) curlyStart = k + } + // decrease paren depth + else if (tokens[k] == ")" && states[k] == "paren") { + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + parenDepth -= 1 + } + // decrease curly depth + else if (tokens[k] == "}" && states[k] == "paren") { + if (curlyEnd == -1 && curlyDepth == 1) curlyEnd = k + curlyDepth -= 1 + } + + // determine the right operator to deal with + if (parenDepth == 0 && curlyDepth == 0) { + let uptkn = tokens[k].toUpperCase() + + if (states[k] == "op" && bF.isSemanticLiteral(tokens[k-1], states[k-1]) && + ((bF._opPrc[uptkn] > topmostOpPrc) || + (!bF._opRh[uptkn] && bF._opPrc[uptkn] == topmostOpPrc)) + ) { + topmostOp = uptkn + topmostOpPrc = bF._opPrc[uptkn] + operatorPos = k + } + } + } + + // unmatched brackets, duh! + if (parenDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + if (curlyDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + + /*************************************************************************/ + + // ## case for: + // | ident_tuple + try { + bF.parserPrintdbgline('e', "Trying Tuple...", lnum, recDepth) + return bF._parseTuple(lnum, tokens, states, recDepth + 1, false) + } + // if ParserError is raised, continue applying other rules + catch (e) { + if (!(e instanceof ParserError)) throw e + bF.parserPrintdbgline('e', 'It was NOT!', lnum, recDepth) + } + + /*************************************************************************/ + + // ## case for: + // | "{" , [expr , {"," , expr}] , "}" + if (curlyStart == 0 && curlyEnd == tokens.length - 1) { + bF.parserPrintdbgline('e', "Array", lnum, recDepth) + return bF._parseArrayLiteral(lnum, tokens, states, recDepth + 1) + } + + /*************************************************************************/ + + + // ## case for: + // | "(" , [expr] , ")" + if (parenStart == 0 && parenEnd == tokens.length - 1) { + bF.parserPrintdbgline('e', '( [Expr] )', lnum, recDepth) + + return bF._parseExpr(lnum, + tokens.slice(parenStart + 1, parenEnd), + states.slice(parenStart + 1, parenEnd), + recDepth + 1 + ) + } + + /*************************************************************************/ + + // ## case for: + // | "IF" , expr_sans_asgn , "THEN" , expr , ["ELSE" , expr] + try { + bF.parserPrintdbgline('e', "Trying IF Expression...", lnum, recDepth) + return bF._parseIfMode(lnum, tokens, states, recDepth + 1, false) + } + // if ParserError is raised, continue applying other rules + catch (e) { + if (!(e instanceof ParserError)) throw e + bF.parserPrintdbgline('e', 'It was NOT!', lnum, recDepth) + } + + /*************************************************************************/ + + // ## case for: + // | ("FOR"|"FOREACH") , expr + try { + bF.parserPrintdbgline('e', "Trying FOR Expression...", lnum, recDepth) + return bF._parseForLoop(lnum, tokens, states, recDepth + 1) + } + // if ParserError is raised, continue applying other rules + catch (e) { + if (!(e instanceof ParserError)) throw e + bF.parserPrintdbgline('e', 'It was NOT!', lnum, recDepth) + } + + /*************************************************************************/ + + // ## case for: + // | expr , op, expr + // | op_uni , expr + // if operator is found, split by the operator and recursively parse the LH and RH + if (topmostOp !== undefined) { + bF.parserPrintdbgline('e', 'Operators', lnum, recDepth) + + if (ifMode && topmostOp == "=") throw lang.syntaxfehler(lnum, "'=' used on IF, did you mean '=='?") + if (ifMode && topmostOp == ":") throw lang.syntaxfehler(lnum, "':' used on IF") + + + // this is the AST we're going to build up and return + // (other IF clauses don't use this) + let treeHead = new BasicAST() + treeHead.astLnum = lnum + treeHead.astValue = topmostOp + treeHead.astType = "op" + + // BINARY_OP? + if (operatorPos > 0) { + let subtknL = tokens.slice(0, operatorPos) + let substaL = states.slice(0, operatorPos) + let subtknR = tokens.slice(operatorPos + 1, tokens.length) + let substaR = states.slice(operatorPos + 1, tokens.length) + + treeHead.astLeaves[0] = bF._parseExpr(lnum, subtknL, substaL, recDepth + 1) + treeHead.astLeaves[1] = bF._parseExpr(lnum, subtknR, substaR, recDepth + 1) + } + else { + if (topmostOp === "-") treeHead.astValue = "UNARYMINUS" + else if (topmostOp === "+") treeHead.astValue = "UNARYPLUS" + else if (topmostOp === "NOT") treeHead.astValue = "UNARYLOGICNOT" + else if (topmostOp === "BNOT") treeHead.astValue = "UNARYBNOT" + else if (topmostOp === "@") treeHead.astValue = "MRET" + else if (topmostOp === "`") treeHead.astValue = "MJOIN" + else throw new ParserError(`Unknown unary op '${topmostOp}'`) + + treeHead.astLeaves[0] = bF._parseExpr(lnum, + tokens.slice(operatorPos + 1, tokens.length), + states.slice(operatorPos + 1, states.length), + recDepth + 1 + ) + } + + return treeHead + } + + /*************************************************************************/ + + // ## case for: + // | kywd , expr - "(" + if (bS.builtin[headTkn] && headSta == "lit" && !bF._opPrc[headTkn] && + states[1] != "paren" && tokens[1] != "(" + ) { + bF.parserPrintdbgline('e', 'Builtin Function Call w/o Paren', lnum, recDepth) + + return bF._parseFunctionCall(lnum, tokens, states, recDepth + 1) + } + + /*************************************************************************/ + + // ## case for: + // | function_call + if (topmostOp === undefined) { // don't remove this IF statement! + try { + bF.parserPrintdbgline('e', "Trying Function Call...", lnum, recDepth) + return bF._parseFunctionCall(lnum, tokens, states, recDepth + 1) + } + // if ParserError is raised, continue applying other rules + catch (e) { + if (!(e instanceof ParserError)) throw e + bF.parserPrintdbgline('e', 'It was NOT!', lnum, recDepth) + } + } + + /*************************************************************************/ + + throw new ParserError(`Expression "${tokens.join(" ")}" cannot be parsed in ${lnum}`) +} // END of EXPR +/** Parses following EBNF rule: + "{" , [expr , {"," , expr}] , "}" + * @return: BasicAST + */ +bF._parseArrayLiteral = function(lnum, tokens, states, recDepth) { + bF.parserPrintdbg2('{', lnum, tokens, states, recDepth) + + /*************************************************************************/ + + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + let curlyDepth = 0 + let curlyStart = -1 + let curlyEnd = -1 + let argSeps = [] + + // scan for parens that will be used for several rules + // also find nearest THEN and ELSE but also take parens into account + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (tokens[k] == "(" && states[k] == "paren") { + parenDepth += 1 + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // increase curly depth and mark curly start position + else if (tokens[k] == "{" && states[k] == "paren") { + curlyDepth += 1 + if (curlyStart == -1 && curlyDepth == 1) curlyStart = k + } + // decrease paren depth + else if (tokens[k] == ")" && states[k] == "paren") { + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + parenDepth -= 1 + } + // decrease curly depth + else if (tokens[k] == "}" && states[k] == "paren") { + if (curlyEnd == -1 && curlyDepth == 1) curlyEnd = k + curlyDepth -= 1 + } + + // commas + if (parenDepth == 0 && curlyDepth == 1 && tokens[k] == "," && states[k] == "sep") { + argSeps.push(k) + } + } + + // unmatched brackets, duh! + if (curlyDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + if (curlyStart == -1) throw new ParserError("not an array") + + /*************************************************************************/ + + bF.parserPrintdbgline('{', `curlyStart=${curlyStart}, curlyEnd=${curlyEnd}, argSeps=${argSeps}`, lnum, recDepth) + + let argStartPos = [1].concat(argSeps.map(k => k+1)) + let argPos = argStartPos.map((s,i) => {return{start:s, end:(argSeps[i] || curlyEnd)}}) // use end of token position as separator position + + bF.parserPrintdbgline("{", "argPos = "+argPos.map(it=>`${it.start}/${it.end}`), lnum, recDepth) + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + treeHead.astValue = "ARRAY CONSTRUCTOR" + treeHead.astType = "function" + + if (curlyStart == 0 && curlyEnd == 1) { + treeHead.astLeaves = [] + } + else { + treeHead.astLeaves = argPos.map((x,i) => { + bF.parserPrintdbgline("{", 'Array Element #'+(i+1), lnum, recDepth) + + // check for empty tokens + if (x.end - x.start < 0) throw new lang.syntaxfehler(lnum) + + return bF._parseExpr(lnum, + tokens.slice(x.start, x.end), + states.slice(x.start, x.end), + recDepth + 1 + )} + ) + } + + return treeHead + +} +/** Parses following EBNF rule: + "IF" , expr_sans_asgn , "THEN" , stmt , ["ELSE" , stmt] + | "IF" , expr_sans_asgn , "THEN" , expr , ["ELSE" , expr] + if exprMode is true, only the latter will be used; former otherwise + * @return: BasicAST + */ +bF._parseIfMode = function(lnum, tokens, states, recDepth, exprMode) { + bF.parserPrintdbg2('/', lnum, tokens, states, recDepth) + + /*************************************************************************/ + + let headTkn = tokens[0].toUpperCase() + let headSta = states[0] + + let parseFunction = (exprMode) ? bF._parseExpr : bF._parseStmt + + let thenPos = -1 + let elsePos = -1 + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + + // scan for parens that will be used for several rules + // also find nearest THEN and ELSE but also take parens into account + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (tokens[k] == "(" && states[k] == "paren") { + parenDepth += 1 + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // decrease paren depth + else if (tokens[k] == ")" && states[k] == "paren") { + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + parenDepth -= 1 + } + + if (parenDepth == 0) { + if (-1 == thenPos && "THEN" == tokens[k].toUpperCase() && "lit" == states[k]) + thenPos = k + else if (-1 == elsePos && "ELSE" == tokens[k].toUpperCase() && "lit" == states[k]) + elsePos = k + } + } + + // unmatched brackets, duh! + if (parenDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + + // ## case for: + // "IF" , expr_sans_asgn , "THEN" , stmt , ["ELSE" , stmt] + if ("IF" == headTkn && "lit" == headSta) { + + // "THEN" not found, raise error! + if (thenPos == -1) throw lang.syntaxfehler(lnum, "IF without THEN") + + treeHead.astValue = "IF" + treeHead.astType = "function" + + treeHead.astLeaves[0] = bF._parseExpr(lnum, + tokens.slice(1, thenPos), + states.slice(1, thenPos), + recDepth + 1, + true // if_equation mode + ) + treeHead.astLeaves[1] = parseFunction(lnum, + tokens.slice(thenPos + 1, (elsePos != -1) ? elsePos : tokens.length), + states.slice(thenPos + 1, (elsePos != -1) ? elsePos : tokens.length), + recDepth + 1 + ) + if (elsePos != -1) + treeHead.astLeaves[2] = parseFunction(lnum, + tokens.slice(elsePos + 1, tokens.length), + states.slice(elsePos + 1, tokens.length), + recDepth + 1 + ) + + return treeHead + } + + throw new ParserError("not an IF "+(exprMode) ? "expression" : "statement") +} // END of IF +/** Parses following EBNF rule: + ("FOR"|"FOREACH") , expr + * @return: BasicAST + */ +bF._parseForLoop = function(lnum, tokens, states, recDepth) { + bF.parserPrintdbg2('\\', lnum, tokens, states, recDepth) + + /*************************************************************************/ + + let headTkn = tokens[0].toUpperCase() + let headSta = states[0] + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + + // ## case for: + // ("FOR"|"FOREACH") , expr + if (("FOR" == headTkn || "FOREACH" == headTkn) && "lit" == headSta) { + + treeHead.astValue = headTkn + treeHead.astType = "function" + + treeHead.astLeaves[0] = bF._parseExpr(lnum, + tokens.slice(1), + states.slice(1), + recDepth + 1 + ) + + return treeHead + } + + throw new ParserError("not an FOR/FOREACH expression") + +} // END of FOR +/** Parses following EBNF rule: +ident_tuple = "[" , ident , ["," , ident] , "]" + * @return: BasicAST + */ +bF._parseTuple = function(lnum, tokens, states, recDepth) { + bF.parserPrintdbg2(']', lnum, tokens, states, recDepth) + + /*************************************************************************/ + + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + let argSeps = [] // argseps collected when parenDepth == 0 + + // Scan for unmatched parens and mark off the right operator we must deal with + // every function_call need to re-scan because it is recursively called + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (tokens[k] == "[" && states[k] == "paren") { + parenDepth += 1 + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // decrease paren depth + else if (tokens[k] == "]" && states[k] == "paren") { + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + parenDepth -= 1 + } + + // where are the argument separators + if (parenDepth == 1 && parenEnd == -1 && states[k] == "sep") + argSeps.push(k) + // break if we've got all the values we nedd + if (parenStart != -1 && parenEnd != -1) + break + } + + // unmatched brackets, duh! + if (parenDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + + if (parenStart != 0 || parenEnd != tokens.length - 1) + throw new ParserError("not a Tuple expression") + + /*************************************************************************/ + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + treeHead.astValue = undefined + treeHead.astType = "closure_args" + + // parse function arguments + bF.parserPrintdbgline(']', 'Tuple arguments -- ', lnum, recDepth) + let defunArgDeclSeps = argSeps.map(i => i-1).concat([parenEnd - 1]) + bF.parserPrintdbgline(']', 'Tuple comma position: '+defunArgDeclSeps, lnum, recDepth) + + treeHead.astLeaves = defunArgDeclSeps.map(i=>bF._parseIdent(lnum, [tokens[i]], [states[i]], recDepth + 1)) + + return treeHead +} +/** Parses following EBNF rule: +function_call = + ident , "(" , [expr , {argsep , expr} , [argsep]] , ")" + | ident , expr , {argsep , expr} , [argsep] + * @return: BasicAST + */ +bF._parseFunctionCall = function(lnum, tokens, states, recDepth) { + bF.parserPrintdbg2("F", lnum, tokens, states, recDepth) + + /*************************************************************************/ + + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + let _argsepsOnLevelZero = [] // argseps collected when parenDepth == 0 + let _argsepsOnLevelOne = [] // argseps collected when parenDepth == 1 + let currentParenMode = [] // a stack; must be able to distinguish different kinds of parens as closures use [ this paren ] + let depthsOfRoundParen = [] + + // Scan for unmatched parens and mark off the right operator we must deal with + // every function_call need to re-scan because it is recursively called + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (bF._isParenOpen(tokens[k]) && states[k] == "paren") { + parenDepth += 1; currentParenMode.unshift(tokens[k]) + if (currentParenMode[0] == '(') depthsOfRoundParen.push(parenDepth) + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // decrease paren depth + else if (bF._isParenClose(tokens[k]) && states[k] == "paren") { + if (!bF._isMatchingParen(currentParenMode[0], tokens[k])) + throw lang.syntaxfehler(lnum, `Opening paren: ${currentParenMode[0]}, closing paren: ${tokens[k]}`) + + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + if (currentParenMode[0] == '(') depthsOfRoundParen.pop() + parenDepth -= 1; currentParenMode.shift() + } + + if (parenDepth == 0 && states[k] == "sep" && currentParenMode[0] === undefined) + _argsepsOnLevelZero.push(k) + if (parenDepth == depthsOfRoundParen[0] && states[k] == "sep" && currentParenMode[0] == "(") + _argsepsOnLevelOne.push(k) + } + + // unmatched brackets, duh! + if (parenDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + let parenUsed = (parenStart == 1) + // && parenEnd == tokens.length - 1) + // if starting paren is found, just use it + // this prevents "RND(~~)*K" to be parsed as [RND, (~~)*K] + + bF.parserPrintdbgline("F", `parenStart: ${parenStart}, parenEnd: ${parenEnd}`, lnum, recDepth) + + /*************************************************************************/ + + // ## case for: + // ident , "(" , [expr , {argsep , expr} , [argsep]] , ")" + // | ident , expr , {argsep , expr} , [argsep] + bF.parserPrintdbgline("F", `Function Call (parenUsed: ${parenUsed})`, lnum, recDepth) + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + + // set function name and also check for syntax by deliberately parsing the word + treeHead.astValue = bF._parseIdent(lnum, [tokens[0]], [states[0]], recDepth + 1).astValue // always UPPERCASE + + // 5 8 11 [end] + let argSeps = parenUsed ? _argsepsOnLevelOne : _argsepsOnLevelZero // choose which "sep tray" to use + bF.parserPrintdbgline("F", "argSeps = "+argSeps, lnum, recDepth) + // 1 6 9 12 + let argStartPos = [1 + (parenUsed)].concat(argSeps.map(k => k+1)) + bF.parserPrintdbgline("F", "argStartPos = "+argStartPos, lnum, recDepth) + // [1,5) [6,8) [9,11) [12,end) + let argPos = argStartPos.map((s,i) => {return{start:s, end:(argSeps[i] || (parenUsed ? parenEnd : tokens.length) )}}) // use end of token position as separator position + bF.parserPrintdbgline("F", "argPos = "+argPos.map(it=>`${it.start}/${it.end}`), lnum, recDepth) + + // check for trailing separator + + + // recursively parse function arguments + treeHead.astLeaves = argPos.map((x,i) => { + bF.parserPrintdbgline("F", `Function Arguments #${i+1} of ${argPos.length}`, lnum, recDepth) + + // check for empty tokens + if (x.end - x.start < 0) throw new ParserError("not a function call because it's malformed") + + return bF._parseExpr(lnum, + tokens.slice(x.start, x.end), + states.slice(x.start, x.end), + recDepth + 1 + )} + ) + treeHead.astType = "function" + treeHead.astSeps = argSeps.map(i => tokens[i]) + bF.parserPrintdbgline("F", "astSeps = "+treeHead.astSeps, lnum, recDepth) + + return treeHead +} +bF._parseIdent = function(lnum, tokens, states, recDepth) { + bF.parserPrintdbg2('i', lnum, tokens, states, recDepth) + + if (!Array.isArray(tokens) && !Array.isArray(states)) throw new ParserError("Tokens and states are not array") + if (tokens.length != 1 || states[0] != "lit") throw new ParserError(`illegal tokens '${tokens}' with states '${states}' in ${lnum}`) + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + treeHead.astValue = tokens[0].toUpperCase() + treeHead.astType = "lit" + + return treeHead +} +/** + * @return: BasicAST + */ +bF._parseLit = function(lnum, tokens, states, recDepth, functionMode) { + bF.parserPrintdbg2('i', lnum, tokens, states, recDepth) + + if (!Array.isArray(tokens) && !Array.isArray(states)) throw new ParserError("Tokens and states are not array") + if (tokens.length != 1) throw new ParserError("parseLit 1") + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + treeHead.astValue = ("qot" == states[0]) ? tokens[0] : tokens[0].toUpperCase() + treeHead.astType = ("qot" == states[0]) ? "string" : + ("num" == states[0]) ? "num" : + (functionMode) ? "function" : "lit" + + return treeHead +} +/** + * @return: Array of [recurseIndex, orderlyIndex], where recurseIndex is in reverse and orderlyIndex is not + */ +bF._findDeBruijnIndex = function(varname, offset) { + let recurseIndex = -1 + let orderlyIndex = -1 + for (recurseIndex = 0; recurseIndex < lambdaBoundVars.length; recurseIndex++) { + orderlyIndex = lambdaBoundVars[recurseIndex].findIndex(it => it == varname) + if (orderlyIndex != -1) + return [recurseIndex + (offset || 0), orderlyIndex] + } + throw new ParserError("Unbound variable: "+varname) +} +/** + * @return: BasicAST + */ +bF._pruneTree = function(lnum, tree, recDepth) { + if (tree === undefined) return + + if (DBGON) { + serial.println("[Parser.PRUNE] pruning following subtree, lambdaBoundVars = "+Object.entries(lambdaBoundVars)) // theLambdaBoundVars() were not formatted for this use case! + serial.println(astToString(tree)) + if (isAST(tree) && isAST(tree.astValue)) { + serial.println("[Parser.PRUNE] unpacking astValue:") + serial.println(astToString(tree.astValue)) + } + } + + let defunName = undefined + + // catch all the bound variables for function definition + if (tree.astType == "op" && tree.astValue == "~>" || tree.astType == "function" && tree.astValue == "DEFUN") { + + let nameTree = tree.astLeaves[0] + if (tree.astValue == "DEFUN") { + defunName = nameTree.astValue + + if (DBGON) { + serial.println("[Parser.PRUNE.~>] met DEFUN, function name: "+defunName) + } + } + let vars = nameTree.astLeaves.map((it, i) => { + if (it.astType !== "lit") throw new ParserError("Malformed bound variable for function definition; tree:\n"+astToString(nameTree)) + return it.astValue + }) + + lambdaBoundVars.unshift(vars) + + if (DBGON) { + serial.println("[Parser.PRUNE.~>] added new bound variables: "+Object.entries(lambdaBoundVars)) + } + } + // simplify UNARYMINUS(num) to -num + else if (tree.astValue == "UNARYMINUS" && tree.astType == "op" && + tree.astLeaves[1] === undefined && tree.astLeaves[0] !== undefined && tree.astLeaves[0].astType == "num" + ) { + tree.astValue = -(tree.astLeaves[0].astValue) + tree.astType = "num" + tree.astLeaves = [] + } + else if (tree.astValue == "UNARYPLUS" && tree.astType == "op" && + tree.astLeaves[1] === undefined && tree.astLeaves[0] !== undefined && tree.astLeaves[0].astType == "num" + ) { + tree.astValue = +(tree.astLeaves[0].astValue) + tree.astType = "num" + tree.astLeaves = [] + } + + + // depth-first run + if (tree.astLeaves[0] != undefined) { + tree.astLeaves.forEach(it => bF._pruneTree(lnum, it, recDepth + 1)) + } + + + if (tree.astType == "op" && tree.astValue == "~>" || tree.astType == "function" && tree.astValue == "DEFUN") { + + if (tree.astLeaves.length !== 2) throw lang.syntaxfehler(lnum, tree.astLeaves.length+lang.aG) + + let nameTree = tree.astLeaves[0] + let exprTree = tree.astLeaves[1] + + // test print new tree + if (DBGON) { + serial.println("[Parser.PRUNE.~>] closure bound variables: "+Object.entries(lambdaBoundVars)) + } + + // rename the parameters + bF._recurseApplyAST(exprTree, (it) => { + if (it.astType == "lit" || it.astType == "function") { + // check if parameter name is valid + // if the name is invalid, regard it as a global variable (i.e. do nothing) + try { + let dbi = bF._findDeBruijnIndex(it.astValue) + + if (DBGON) { + serial.println(`index for ${it.astValue}: ${dbi}`) + } + + + it.astValue = dbi + it.astType = "defun_args" + } + catch (_) {} + } + }) + + tree.astType = "usrdefun" + tree.astValue = exprTree + tree.astLeaves = [] + + lambdaBoundVars.shift() + } + + // for DEFUNs, build assign tree such that: + // DEFUN F lambda + // turns into: + // F=(lambda) + if (defunName) { + let nameTree = new BasicAST() + nameTree.astLnum = tree.astLnum + nameTree.astType = "lit" + nameTree.astValue = defunName + + let newTree = new BasicAST() + newTree.astLnum = tree.astLnum + newTree.astType = "op" + newTree.astValue = "=" + newTree.astLeaves = [nameTree, tree] + + tree = newTree + + if (DBGON) { + serial.println(`[Parser.PRUNE] has DEFUN, function name: ${defunName}`) + } + } + + if (DBGON) { + serial.println("[Parser.PRUNE] pruned subtree:") + serial.println(astToString(tree)) + if (isAST(tree) && isAST(tree.astValue)) { + serial.println("[Parser.PRUNE] unpacking astValue:") + serial.println(astToString(tree.astValue)) + } + + serial.println("======================================================\n") + } + + return tree +} + +// ## USAGE OF lambdaBoundVars IN PARSEMODE ENDS HERE ## + +// @return is defined in BasicAST +let JStoBASICtype = function(object) { + if (typeof object === "boolean") return "bool" + else if (object === undefined) return "null" + else if (object.arrName !== undefined) return "internal_arrindexing_lazy" + else if (object.asgnVarName !== undefined) return "internal_assignment_object" + else if (isGenerator(object)) return "generator" + else if (isAST(object)) return "usrdefun" + else if (isMonad(object)) return "monad" + else if (Array.isArray(object)) return "array" + else if (isNumable(object)) return "num" + else if (typeof object === "string" || object instanceof String) return "string" + // buncha error msgs + else throw Error("BasicIntpError: un-translatable object with typeof "+(typeof object)+",\ntoString = "+object+",\nentries = "+Object.entries(object)) +} +let SyntaxTreeReturnObj = function(type, value, nextLine) { + if (nextLine === undefined || !Array.isArray(nextLine)) + throw Error("TODO change format of troNextLine to [linenumber, stmtnumber]") + + this.troType = type + this.troValue = value + this.troNextLine = nextLine +} +let JumpObj = function(targetLnum, targetStmtNum, fromLnum, rawValue) { + this.jmpNext = [targetLnum, targetStmtNum] + this.jmpFrom = fromLnum + this.jmpReturningValue = rawValue +} +bF._makeRunnableFunctionFromExprTree = function(lnum, stmtnum, expression, args, recDepth, _debugExec, recWedge) { + // register variables + let defunArgs = args.map(it => { + let rit = resolve(it) + return [JStoBASICtype(rit), rit] + }) + lambdaBoundVars.unshift(defunArgs) + + if (_debugExec) { + serial.println(recWedge+"usrdefun dereference") + serial.println(recWedge+"usrdefun dereference function: ") + serial.println(astToString(expression)) + serial.println(recWedge+"usrdefun dereference bound vars: "+theLambdaBoundVars()) + } + + // insert bound variables to its places + let bindVar = function(tree, recDepth) { + bF._recurseApplyAST(tree, it => { + + if (_debugExec) { + serial.println(recWedge+`usrdefun${recDepth} trying to bind some variables to:`) + serial.println(astToString(it)) + } + + if (it.astType == "defun_args") { + let recIndex = it.astValue[0] - recDepth + let varIndex = it.astValue[1] + + if (_debugExec) { + serial.println(recWedge+`usrdefun${recDepth} bindvar d(${recIndex},${varIndex})`) + } + + let theVariable = undefined + try { + theVariable = lambdaBoundVars[recIndex][varIndex] + } + catch (e0) {} + + // this will make partial applying work, but completely remove the ability of catching errors... + if (theVariable !== undefined) { + it.astValue = theVariable[1] + it.astType = theVariable[0] + } + + if (_debugExec) { + serial.println(recWedge+`usrdefun${recDepth} the bindvar: ${theVariable}`) + serial.println(recWedge+`usrdefun${recDepth} modified tree:`) + serial.println(astToString(it)) + } + } + // function in a function + else if (it.astType == "usrdefun") { + bindVar(it.astValue, recDepth + 1) + } + }) + };bindVar(expression, 0) + + + if (_debugExec) { + serial.println(recWedge+"usrdefun dereference final tree:") + serial.println(astToString(expression)) + } + + return bS.getDefunThunk(expression, true) +} +/** + * @param lnum line number of BASIC + * @param syntaxTree BasicAST + * @param recDepth recursion depth used internally + * + * @return syntaxTreeReturnObject if recursion is escaped + */ +bF._troNOP = function(lnum, stmtnum) { return new SyntaxTreeReturnObj("null", undefined, [lnum, stmtnum+1]) } +bF._executeSyntaxTree = function(lnum, stmtnum, syntaxTree, recDepth) { + if (syntaxTree == undefined) return bF._troNOP(lnum, stmtnum) + if (syntaxTree.astLeaves === undefined && syntaxTree.astValue === undefined) { + throw new BASICerror("not a syntax tree") + } + + if (lnum === undefined || stmtnum === undefined) throw Error(`Line or statement number is undefined: (${lnum},${stmtnum})`) + + let _debugExec = (!PROD) && true + let _debugPrintCurrentLine = (!PROD) && true + let recWedge = ">".repeat(recDepth+1) + " " + let tearLine = "\n =====ExecSyntaxTree===== "+("<".repeat(recDepth+1))+"\n" + + if (_debugExec || _debugPrintCurrentLine) serial.println(recWedge+`@@ EXECUTE ${lnum}:${stmtnum} @@`) + if (_debugPrintCurrentLine) { + serial.println("Syntax Tree in "+lnum+":") + serial.println(astToString(syntaxTree)) + } + + let callingUsrdefun = (syntaxTree.astType == "usrdefun" && syntaxTree.astLeaves[0] !== undefined) + // do NOT substitute (syntaxTree.astType == "usrdefun") with isAST; doing so will break (=) operator + // calling usrdefun without any args will make leaves[0] to be null-node but not undefined + // funseq-monad will be dealt with on (func === undefined) branch + + if (syntaxTree.astValue == undefined && syntaxTree.mVal == undefined) { // empty meaningless parens + if (syntaxTree.astLeaves.length > 1) throw Error("WTF") + return bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[0], recDepth) + } + // array indexing in the tree (caused by indexing array within recursive DEFUN) + else if (syntaxTree.astType == "array" && syntaxTree.astLeaves[0] !== undefined) { + let indexer = bS.getArrayIndexFun(lnum, stmtnum, "substituted array", syntaxTree.astValue) + let args = syntaxTree.astLeaves.map(it => bF._executeSyntaxTree(lnum, stmtnum, it, recDepth + 1)) + let retVal = indexer(lnum, stmtnum, args) + if (_debugExec) serial.println(recWedge+`indexing substituted array(${Object.entries(args)}) = ${Object.entries(retVal)}`) + return new SyntaxTreeReturnObj( + JStoBASICtype(retVal), + retVal, + [lnum, stmtnum + 1] + ) + } + // closure + // type: closure_args ~> (expr) + else if (syntaxTree.astType == "op" && syntaxTree.astValue == "~>") { + throw new BASICerror("Untended closure") // closure definition must be 'pruned' by the parser + } + else if (syntaxTree.astType == "function" && syntaxTree.astValue == "DEFUN") { + throw new BASICerror("Untended DEFUN") // DEFUN must be 'pruned' by the parser + } + else if (syntaxTree.astType == "function" || syntaxTree.astType == "op" || callingUsrdefun) { + if (_debugExec) serial.println(recWedge+"function|operator") + if (_debugExec) serial.println(recWedge+astToString(syntaxTree)) + let callerHash = syntaxTree.astHash + let funcName = (typeof syntaxTree.astValue.toUpperCase == "function") ? syntaxTree.astValue.toUpperCase() : "(usrdefun)" + let lambdaBoundVarsAppended = (callingUsrdefun) + let func = (callingUsrdefun) + ? bF._makeRunnableFunctionFromExprTree( + lnum, stmtnum, + cloneObject(syntaxTree.astValue), + syntaxTree.astLeaves.map(it => bF._executeSyntaxTree(lnum, stmtnum, it, recDepth + 1)), // the args + recDepth, _debugExec, recWedge + ) + : (bS.builtin[funcName] === undefined) + ? undefined + : (!DBGON && bS.builtin[funcName].debugonly) ? "NO_DBG4U" : (PROD && bS.builtin[funcName].noprod) ? "NO_PRODREADY" : bS.builtin[funcName].f + + if (func === "NO_DBG4U") throw lang.syntaxfehler(lnum) + if (func === "NO_PRODREADY") throw lang.syntaxfehler(lnum) + + if ("IF" == funcName) { + if (syntaxTree.astLeaves.length != 2 && syntaxTree.astLeaves.length != 3) throw lang.syntaxfehler(lnum) + var testedval = bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[0], recDepth + 1) + + if (_debugExec) { + serial.println(recWedge+"testedval:") + serial.println(recWedge+"type="+testedval.troValue.astType) + serial.println(recWedge+"value="+testedval.troValue.astValue) + serial.println(recWedge+"nextLine="+testedval.troValue.astNextLine) + } + + try { + var iftest = bS.builtin["TEST"].f(lnum, stmtnum, [testedval]) + + let r = (!iftest && syntaxTree.astLeaves[2] !== undefined) ? + bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[2], recDepth + 1) + : (iftest) ? + bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[1], recDepth + 1) + : bF._troNOP(lnum, stmtnum) + + if (_debugExec) serial.println(tearLine) + return r + } + catch (e) { + serial.printerr(`${e}\n${e.stack || "Stack trace undefined"}`) + throw lang.errorinline(lnum, "TEST", e) + } + } + else if ("ON" == funcName) { + if (syntaxTree.astLeaves.length < 3) throw lang.badFunctionCallFormat(lnum) + + let testValue = bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[0], recDepth + 1) + let functionName = syntaxTree.astLeaves[1].astValue + let arrays = [] + for (let k = 2; k < syntaxTree.astLeaves.length; k++) + arrays.push(bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[k], recDepth + 1)) + + try { + let r = bS.builtin["ON"].f(lnum, stmtnum, [functionName, testValue].concat(arrays)) + let r2 = new SyntaxTreeReturnObj(JStoBASICtype(r.jmpReturningValue), r.jmpReturningValue, r.jmpNext) + if (_debugExec) serial.println(tearLine) + return r2 + } + catch (e) { + serial.printerr(`${e}\n${e.stack || "Stack trace undefined"}`) + throw lang.errorinline(lnum, "ON error", e) + } + } + else { + let args = syntaxTree.astLeaves.map(it => bF._executeSyntaxTree(lnum, stmtnum, it, recDepth + 1)) + + if (_debugExec) { + serial.println(recWedge+`fn caller: "${callerHash}"`) + serial.println(recWedge+`fn call name: "${funcName}"`) + serial.println(recWedge+"fn call args: "+(args.map(it => (it == undefined) ? it : (it.troType+" "+it.troValue)).join(", "))) + } + + // func not in builtins (e.g. array access, user-defined function defuns) + if (func === undefined) { + var someVar = bS.vars[funcName] + + if (someVar !== undefined && DBGON) { + serial.println(recWedge+`variable dereference of '${funcName}' : ${someVar.bvLiteral} (bvType: ${someVar.bvType})`) + if (typeof someVar.bvLiteral == "object") + serial.println(recWedge+"variable as an object : "+Object.entries(someVar.bvLiteral)) + } + + if (someVar === undefined) { + throw lang.syntaxfehler(lnum, funcName + " is undefined") + } + else if ("array" == someVar.bvType) { + func = bS.getArrayIndexFun(lnum, stmtnum, funcName, someVar.bvLiteral) + } + else if ("usrdefun" == someVar.bvType) { + // dereference usrdefun + let expression = cloneObject(someVar.bvLiteral) + lambdaBoundVarsAppended = true + func = bF._makeRunnableFunctionFromExprTree(lnum, stmtnum, expression, args, recDepth, _debugExec, recWedge) + } + else if ("monad" == someVar.bvType) { + func = getMonadEvalFun(someVar.bvLiteral) + } + else { + throw lang.syntaxfehler(lnum, funcName + " is not a function or an array") + } + } + + // call whatever the 'func' has whether it's builtin or we just made shit up right above + if (func === undefined) { + serial.printerr(lnum+` ${funcName} is undefined`) + throw lang.syntaxfehler(lnum, funcName + " is undefined") + } + + let funcCallResult = func(lnum, stmtnum, args, syntaxTree.astSeps) + + if (funcCallResult instanceof SyntaxTreeReturnObj) return funcCallResult + + let retVal = (funcCallResult instanceof JumpObj) ? funcCallResult.jmpReturningValue : funcCallResult + + let theRealRet = new SyntaxTreeReturnObj( + JStoBASICtype(retVal), + retVal, + (funcCallResult instanceof JumpObj) ? funcCallResult.jmpNext : [lnum, stmtnum + 1] + ) + + // unregister variables + if (lambdaBoundVarsAppended) lambdaBoundVars.shift() + + if (_debugExec) serial.println(tearLine) + return theRealRet + } + } + else if (syntaxTree.astType == "defun_args") { + if (_debugExec) { + serial.println(recWedge+"defun_args lambda bound vars: "+(lambdaBoundVars === undefined) ? undefined : theLambdaBoundVars()) + serial.println(recWedge+"defun_args defun args: "+syntaxTree.astValue) + } + let recIndex = syntaxTree.astValue[0] + let varIndex = syntaxTree.astValue[1] + let theVar = lambdaBoundVars[recIndex, varIndex] + if (_debugExec) { + serial.println(recWedge+"defun_args thevar: "+(theVar === undefined) ? undefined : Object.entries(theVar)) + serial.println(tearLine) + } + return theVar + } + else if (syntaxTree.astType == "num") { + if (_debugExec) serial.println(recWedge+"num "+(tonum(syntaxTree.astValue))) + let r = new SyntaxTreeReturnObj(syntaxTree.astType, tonum(syntaxTree.astValue), [lnum, stmtnum + 1]) + if (_debugExec) serial.println(tearLine) + return r + } + else if (syntaxTree.astType == "lit" || literalTypes.includes(syntaxTree.astType)) { + if (_debugExec) { + serial.println(recWedge+"literal with astType: "+syntaxTree.astType+", astValue: "+syntaxTree.astValue) + if (isAST(syntaxTree.astValue)) { + serial.println(recWedge+"astValue is a tree, unpacking: \n"+astToString(syntaxTree.astValue)) + } + } + let r = new SyntaxTreeReturnObj(syntaxTree.astType, syntaxTree.astValue, [lnum, stmtnum + 1]) + if (_debugExec) serial.println(tearLine) + return r + } + else if (syntaxTree.astType == "null") { + if (_debugExec) serial.println(recWedge+"null") + let r = bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[0], recDepth + 1) + if (_debugExec) serial.println(tearLine) + return r + } + else { + serial.println(recWedge+"Parsing error in "+lnum) + serial.println(recWedge+astToString(syntaxTree)) + throw Error("Parsing error") + } +} // END OF bF._executeSyntaxTree +// @return ARRAY of BasicAST +bF._interpretLine = function(lnum, cmd) { + let _debugprintHighestLevel = false + + if (cmd.toUpperCase().startsWith("REM")) { + if (_debugprintHighestLevel) serial.println(lnum+" "+cmd) + return undefined + } + + // TOKENISE + let tokenisedObject = bF._tokenise(lnum, cmd) + let tokens = tokenisedObject.tokens + let states = tokenisedObject.states + + + // ELABORATION : distinguish numbers and operators from literals + let newtoks = bF._parserElaboration(lnum, tokens, states) + tokens = newtoks.tokens + states = newtoks.states + + // PARSING (SYNTAX ANALYSIS) + let syntaxTrees = bF._parseTokens(lnum, tokens, states).map(it => { + if (lambdaBoundVars.length != 0) + throw new BASICerror("lambdaBoundVars not empty") + return bF._pruneTree(lnum, it, 0) + }) + + if (_debugprintHighestLevel) { + syntaxTrees.forEach((t,i) => { + serial.println("\nParsed Statement #"+(i+1)) + serial.println(astToString(t)) + }) + } + + return syntaxTrees +} // end INTERPRETLINE +// @return [next line number, next statement number] +bF._executeAndGet = function(lnum, stmtnum, syntaxTree) { + if (lnum === undefined || stmtnum === undefined) throw Error(`Line or statement number is undefined: (${lnum},${stmtnum})`) + + // EXECUTE + try { + if (lambdaBoundVars.length != 0) throw new BASICerror() + var execResult = bF._executeSyntaxTree(lnum, stmtnum, syntaxTree, 0) + + if (DBGON) serial.println(`Line ${lnum} TRO: ${Object.entries(execResult)}`) + + return execResult.troNextLine + } + catch (e) { + serial.printerr(`ERROR on ${lnum}:${stmtnum} -- PARSE TREE:\n${astToString(syntaxTree)}\nERROR CONTENTS:\n${e}\n${e.stack || "Stack trace undefined"}`) + throw e + } +} +bF._basicList = function(v, i, arr) { + if (i < 10) print(" ") + if (i < 100) print(" ") + print(i) + print(" ") + println(v) +} +bF.list = function(args) { // LIST function + if (args.length == 1) { + cmdbuf.forEach(bF._basicList) + } + else if (args.length == 2) { + if (cmdbuf[args[1]] !== undefined) + bF._basicList(cmdbuf[args[1]], args[1], undefined) + } + else { + var lastIndex = (args[2] === ".") ? cmdbuf.length - 1 : (args[2] | 0) + var i = 0 + for (i = args[1]; i <= lastIndex; i++) { + var cmd = cmdbuf[i] + if (cmd !== undefined) { + bF._basicList(cmd, i, cmdbuf) + } + } + } +} +bF.system = function(args) { // SYSTEM function + tbasexit = true +} +bF.new = function(args) { // NEW function + if (args) cmdbuf = [] + bS.vars = initBvars() + gotoLabels = {} + lambdaBoundVars = [] + DATA_CONSTS = [] + DATA_CURSOR = 0 + INDEX_BASE = 0 +} +bF.renum = function(args) { // RENUM function + var newcmdbuf = [] + var linenumRelation = [[]] + var cnt = 10 + for (var k = 0; k < cmdbuf.length; k++) { + if (cmdbuf[k] !== undefined) { + newcmdbuf[cnt] = cmdbuf[k].trim() + linenumRelation[k] = cnt + cnt += 10 + } + } + // deal with goto/gosub line numbers + for (k = 0; k < newcmdbuf.length; k++) { + if (newcmdbuf[k] !== undefined && newcmdbuf[k].toLowerCase().startsWith("goto ")) { + newcmdbuf[k] = "GOTO " + linenumRelation[newcmdbuf[k].match(reNum)[0]] + } + else if (newcmdbuf[k] !== undefined && newcmdbuf[k].toLowerCase().startsWith("gosub ")) { + newcmdbuf[k] = "GOSUB " + linenumRelation[newcmdbuf[k].match(reNum)[0]] + } + else if (newcmdbuf[k] !== undefined && newcmdbuf[k].toLowerCase().startsWith("breakto ")) { + newcmdbuf[k] = "BREAKTO " + linenumRelation[newcmdbuf[k].match(reNum)[0]] + } + } + cmdbuf = newcmdbuf.slice() // make shallow copy + + // recalculate memory footprint + cmdbufMemFootPrint = 0 + cmdbuf.forEach((v, i, arr) => + cmdbufMemFootPrint += ("" + i).length + 1 + v.length + ) +} +bF.fre = function(args) { + println(vmemsize - getUsedMemSize()) +} +bF.tron = function(args) { + TRACEON = true +} +bF.troff = function(args) { + TRACEON = false +} +bF.delete = function(args) { + if (args.length != 2 && args.length != 3) throw lang.syntaxfehler() + + // stupid Javascript can't even Array.prototype.remove(int) + let start = 0; let end = 0 + if (args.length == 2) { + if (!isNumable(args[1])) throw lang.badFunctionCallFormat() + start = args[1]|0 + end = args[1]|0 + } + else { + if (!isNumable(args[1]) && !isNumable(args[2])) throw lang.badFunctionCallFormat() + start = args[1]|0 + end = args[2]|0 + } + + let newcmdbuf = [] + cmdbuf.forEach((v,i) => {if (i < start || i > end) newcmdbuf[i]=v}) + cmdbuf = newcmdbuf +} +bF.cls = function(args) { + con.clear() +} +bF.prescanStmts = ["DATA","LABEL"] +bF.run = function(args) { // RUN function + bF.new(false) + + let programTrees = [] + // pre-build the trees + prescan = true + cmdbuf.forEach((linestr, linenum) => { + let trees = bF._interpretLine(linenum, linestr.trim()) + programTrees[linenum] = trees + // do prescan job (data, label, etc) + if (trees !== undefined) { + trees.forEach((t, i) => { + if (t !== undefined && bF.prescanStmts.includes(t.astValue)) { + bF._executeAndGet(linenum, i, t) + } + }) + } + }) + prescan = false + + if (!PROD && DBGON) { + serial.println("[BASIC] final DATA: "+DATA_CONSTS) + } + + // actually execute the program + let lnum = 1 + let stmtnum = 0 + let oldnum = 1 + let tree = undefined + do { + if (programTrees[lnum] !== undefined) { + if (TRACEON) { + //print(`[${lnum}]`) + serial.println("[BASIC] Line "+lnum) + } + + oldnum = lnum + tree = (programTrees[lnum] !== undefined) ? programTrees[lnum][stmtnum] : undefined + + if (tree !== undefined) { + let nextObj = bF._executeAndGet(lnum, stmtnum, tree) + lnum = nextObj[0] + stmtnum = nextObj[1] + } + else { + lnum += 1 + stmtnum = 0 + } + } + else { + lnum += 1 + } + if (lnum < 0) throw lang.badNumberFormat + if (con.hitterminate()) { + println("Break in "+oldnum) + break + } + } while (lnum < cmdbuf.length) + con.resetkeybuf() +} +bF.save = function(args) { // SAVE function + if (args[1] === undefined) throw lang.missingOperand + if (!args[1].toUpperCase().endsWith(".BAS")) + args[1] += ".bas" + fs.open(args[1], "W") + var sb = "" + cmdbuf.forEach((v, i) => sb += i+" "+v+"\n") + fs.write(sb) +} +bF.load = function(args) { // LOAD function + if (args[1] === undefined) throw lang.missingOperand + var fileOpened = fs.open(args[1], "R") + + + if (replUsrConfirmed || cmdbuf.length == 0) { + if (!fileOpened) { + fileOpened = fs.open(args[1]+".BAS", "R") + } + if (!fileOpened) { + fileOpened = fs.open(args[1]+".bas", "R") + } + if (!fileOpened) { + throw lang.noSuchFile + return + } + var prg = fs.readAll() + + // reset the environment + bF.new(true) + + // read the source + prg.split('\n').forEach((line) => { + var i = line.indexOf(" ") + var lnum = line.slice(0, i) + if (isNaN(lnum)) throw lang.illegalType() + cmdbuf[lnum] = line.slice(i + 1, line.length) + }) + } + else { + replCmdBuf = ["load"].concat(args) + println("Unsaved program will be lost, are you sure? (type 'yes' to confirm)") + } +} +bF.yes = function() { + if (replCmdBuf.length > 0) { + replUsrConfirmed = true + + bF[replCmdBuf[0].toLowerCase()](replCmdBuf.slice(1, replCmdBuf.length)) + + replCmdBuf = [] + replUsrConfirmed = false + } + else { + throw lang.syntaxfehler("interactive", "nothing to confirm!") + } +} +bF.catalog = function(args) { // CATALOG function + if (args[1] === undefined) args[1] = "\\" + var pathOpened = fs.open(args[1], 'R') + if (!pathOpened) { + throw lang.noSuchFile + return + } + var port = _BIOS.FIRST_BOOTABLE_PORT[0] + com.sendMessage(port, "LIST") + println(com.pullMessage(port)) +} +Object.freeze(bF) + +if (exec_args !== undefined && exec_args[1] !== undefined) { + bF.load(["load", exec_args[1]]) + try { + bF.run() + return 0 + } + catch (e) { + serial.printerr(`${e}\n${e.stack || "Stack trace undefined"}`) + println(`${e}`) + } +} + +while (!tbasexit) { + var line = sys.read().trim() + + cmdbufMemFootPrint += line.length + + if (reLineNum.test(line)) { + var i = line.indexOf(" ") + cmdbuf[line.slice(0, i)] = line.slice(i + 1, line.length) + } + else if (line.length > 0) { + cmdbufMemFootPrint -= line.length + var cmd = line.split(" ") + if (bF[cmd[0].toLowerCase()] === undefined) { + serial.printerr("Unknown command: "+cmd[0].toLowerCase()) + println(lang.syntaxfehler()) + } + else { + try { + bF[cmd[0].toLowerCase()](cmd) + } + catch (e) { + serial.printerr(`${e}\n${e.stack || "Stack trace undefined"}`) + println(`${e}`) + } + } + + println(prompt) + } +} + +0 diff --git a/assets/bios/basicbios.js b/assets/bios/basicbios.js index f5fbae3..a222d41 100644 --- a/assets/bios/basicbios.js +++ b/assets/bios/basicbios.js @@ -1,4 +1,4 @@ -println("Main RAM:"+(system.maxmem() >> 10)+" KBytes"); +println("Main RAM:"+(sys.maxmem() >> 10)+" KBytes"); /////////////////////////////////////////////////////////////////////////////// diff --git a/assets/bios/bios_source.js b/assets/bios/bios_source.js index 381a39a..82339aa 100644 --- a/assets/bios/bios_source.js +++ b/assets/bios/bios_source.js @@ -1,5 +1,5 @@ println("TERRAN Megatrends inc."); -//println("Main RAM:"+(system.maxmem() >> 10)+" KBytes"); +//println("Main RAM:"+(sys.maxmem() >> 10)+" KBytes"); /////////////////////////////////////////////////////////////////////////////// diff --git a/assets/bios/openbios.js b/assets/bios/openbios.js index 2bc1271..4756fb9 100644 --- a/assets/bios/openbios.js +++ b/assets/bios/openbios.js @@ -113,7 +113,7 @@ function printSysInfo() { print(`System uptime ${uh}h${um}m${us}s`) con.move(6,configContentsX) - print(` User RAM ${system.maxmem()>>>10} Kbytes`) + print(` User RAM ${sys.maxmem()>>>10} Kbytes`) con.move(7,configContentsX) print(`Video RAM ${256*sys.peek(-131084)} Kbytes`) } diff --git a/assets/bios/pip_basic_loader_source.js b/assets/bios/pip_basic_loader_source.js index 419a3e0..3facfca 100644 --- a/assets/bios/pip_basic_loader_source.js +++ b/assets/bios/pip_basic_loader_source.js @@ -35,7 +35,7 @@ let DBGON = (!PROD) && true let DATA_CURSOR = 0 let DATA_CONSTS = [] -let vmemsize = system.maxmem() +let vmemsize = sys.maxmem() let cmdbuf = [] // index: line number let gotoLabels = {} diff --git a/assets/bios/pip_basic_loader_source2.js b/assets/bios/pip_basic_loader_source2.js new file mode 100644 index 0000000..acfb702 --- /dev/null +++ b/assets/bios/pip_basic_loader_source2.js @@ -0,0 +1,4131 @@ +// Created by CuriousTorvald on 2020-05-19 +// Version 1.0 Release Date 2020-12-28 +// Version 1.1 Release Date 2021-01-28 +// Version 1.2 Release Date 2021-05-05 +// Version 1.2.1 Release Date 2021-12-01 + +/* +Copyright (c) 2020-2021 CuriousTorvald + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +const THEVERSION = "1.2.1" + +const PROD = true +let INDEX_BASE = 0 +let TRACEON = (!PROD) && true +let DBGON = (!PROD) && true +let DATA_CURSOR = 0 +let DATA_CONSTS = [] + +let vmemsize = sys.maxmem() + +let cmdbuf = [] // index: line number +let gotoLabels = {} +let cmdbufMemFootPrint = 0 +let prompt = "Ok" +let prescan = false +let replCmdBuf = [] // used to store "load filename" and issues it when user confirmed potential data loss +let replUsrConfirmed = false + +// lambdaBoundVars is used in two different mode: +// - PARSER will just store a symbol as a string literal +// - EXECUTOR will store the actual info of the bound vars in this format: [astType, astValue] +let lambdaBoundVars = [] // format: [[a,b],[c]] for "[c]~>[a,b]~>expr" + +/* if an object can be FOR REAL cast to number */ +function isNumable(s) { + // array? + if (Array.isArray(s)) return false + // undefined? + if (s === undefined) return false + // null string? + if (typeof s.trim == "function" && s.trim().length == 0) return false + // else? + return !isNaN(s) // NOTE: isNaN('') == false +} +let tonum = (t) => t*1.0 +function cloneObject(o) { return JSON.parse(JSON.stringify(o)) } + +class ParserError extends Error { + constructor(...args) { + super(...args) + Error.captureStackTrace(this, ParserError) + } +} + +class BASICerror extends Error { + constructor(...args) { + super(...args) + Error.captureStackTrace(this, ParserError) + } +} + +let lang = {} +lang.badNumberFormat = Error("Illegal number format") +lang.badOperatorFormat = Error("Illegal operator format") +lang.divByZero = Error("Division by zero") +lang.badFunctionCallFormat = function(line, reason) { + return Error("Illegal function call" + ((line) ? " in "+line : "") + ((reason) ? ": "+reason : "")) +} +lang.unmatchedBrackets = Error("Unmatched brackets") +lang.missingOperand = Error("Missing operand") +lang.noSuchFile = Error("No such file") +lang.outOfData = function(line) { + return Error("Out of DATA"+(line !== undefined ? (" in "+line) : "")) +} +lang.nextWithoutFor = function(line, varname) { + return Error("NEXT "+((varname !== undefined) ? ("'"+varname+"'") : "")+"without FOR in "+line) +} +lang.syntaxfehler = function(line, reason) { + return Error("Syntax error" + ((line !== undefined) ? (" in "+line) : "") + ((reason !== undefined) ? (": "+reason) : "")) +} +lang.illegalType = function(line, obj) { + return Error("Type mismatch" + ((obj !== undefined) ? ` "${obj} (typeof ${typeof obj})"` : "") + ((line !== undefined) ? (" in "+line) : "")) + } +lang.refError = function(line, obj) { + serial.printerr(`${line} Unresolved reference:`) + serial.printerr(` object: ${obj}, typeof: ${typeof obj}`) + if (obj !== null && obj !== undefined) serial.printerr(` entries: ${Object.entries(obj)}`) + return Error("Unresolved reference" + ((obj !== undefined) ? ` "${obj}"` : "") + ((line !== undefined) ? (" in "+line) : "")) +} +lang.nowhereToReturn = function(line) { return "RETURN without GOSUB in " + line } +lang.errorinline = function(line, stmt, errobj) { + return Error('Error'+((line !== undefined) ? (" in "+line) : "")+' on "'+stmt+'": '+errobj) +} +lang.parserError = function(line, errorobj) { + return Error("Parser error in " + line + ": " + errorobj) +} +lang.outOfMem = function(line) { + return Error("Out of memory in " + line) +} +lang.dupDef = function(line, varname) { + return Error("Duplicate definition"+((varname !== undefined) ? (" on "+varname) : "")+" in "+line) +} +lang.asgnOnConst = function(line, constname) { + return Error('Trying to modify constant "'+constname+'" in '+line) +} +lang.subscrOutOfRng = function(line, object, index, maxlen) { + return Error("Subscript out of range"+(object !== undefined ? (' for "'+object+'"') : '')+(index !== undefined ? (` (index: ${index}, len: ${maxlen})`) : "")+(line !== undefined ? (" in "+line) : "")) +} +lang.aG = " arguments were given" +lang.ord = function(n) { + if (n % 10 == 1 && n % 100 != 11) return n+"st" + if (n % 10 == 2 && n % 100 != 12) return n+"nd" + if (n % 10 == 3 && n % 100 != 13) return n+"rd" + return n+"th" +} +Object.freeze(lang) + +// implement your own con object here +// requirements: reset_graphics(), getch(), curs_set(int), hitterminate(), resetkeybuf(), addch(int) + +let getUsedMemSize = function() { + var varsMemSize = 0 + + Object.entries(bS.vars).forEach((pair, i) => { + var object = pair[1] + + if (Array.isArray(object)) { + // TODO test 1-D array + varsMemSize += object.length * 8 + } + else if (!isNaN(object)) varsMemSize += 8 + else if (typeof object === "string" || object instanceof String) varsMemSize += object.length + else varsMemSize += 1 + }) + return varsMemSize + cmdbufMemFootPrint // + array's dimsize * 8 + variables' sizeof literal + functions' expression length +} + +let reLineNum = /^[0-9]+ / +//var reFloat = /^([\-+]?[0-9]*[.][0-9]+[eE]*[\-+0-9]*[fF]*|[\-+]?[0-9]+[.eEfF][0-9+\-]*[fF]?)$/ +//var reDec = /^([\-+]?[0-9_]+)$/ +//var reHex = /^(0[Xx][0-9A-Fa-f_]+)$/ +//var reBin = /^(0[Bb][01_]+)$/ + +// must match partial +let reNumber = /([0-9]*[.][0-9]+[eE]*[\-+0-9]*[fF]*|[0-9]+[.eEfF][0-9+\-]*[fF]?)|([0-9]+(\_[0-9])*)|(0[Xx][0-9A-Fa-f_]+)|(0[Bb][01_]+)/ +let reNum = /[0-9]+/ +let tbasexit = false + +// variable object constructor +/** variable object constructor + * @param literal Javascript object or primitive + * @type derived from JStoBASICtype + "usrdefun" + "internal_arrindexing_lazy" + "internal_assignment_object" + * @see bS.builtin["="] + */ +let BasicVar = function(literal, type) { + this.bvLiteral = literal + this.bvType = type +} +// Abstract Syntax Tree +// creates empty tree node +let astToString = function(ast, depth, isFinalLeaf) { + let l__ = "| " + + let recDepth = depth || 0 + if (!isAST(ast)) return "" + + let hastStr = ast.astHash + let sb = "" + let marker = ("lit" == ast.astType) ? "i" : + ("op" == ast.astType) ? "+" : + ("string" == ast.astType) ? "@" : + ("num" == ast.astType) ? "$" : + ("array" == ast.astType) ? "[" : + ("defun_args" === ast.astType) ? "d" : "f" + sb += l__.repeat(recDepth)+`${marker} ${ast.astLnum}: "${ast.astValue}" (astType:${ast.astType}); leaves: ${ast.astLeaves.length}; hash:"${hastStr}"\n` + for (var k = 0; k < ast.astLeaves.length; k++) { + sb += astToString(ast.astLeaves[k], recDepth + 1, k == ast.astLeaves.length - 1) + if (ast.astSeps[k] !== undefined) + sb += l__.repeat(recDepth)+` sep:${ast.astSeps[k]}\n` + } + sb += l__.repeat(recDepth)+"`"+"-".repeat(22)+'\n' + return sb +} +let monadToString = function(monad, depth) { + let recDepth = depth || 0 + let l__ = " " + let sb = ` M"${monad.mHash}"(${monad.mType}): ` + //if (monad.mType == "value") { + sb += (monad.mVal === undefined) ? "(undefined)" : (isAST(monad.mVal)) ? `f"${monad.mVal.astHash}"` : (isMonad(monad.mVal)) ? `M"${monad.mVal.mHash}"` : monad.mVal + /*} + else if (monad.mType == "list") { + let elemToStr = function(e) { + return (e === undefined) ? "(undefined)" : + (isAST(e)) ? `f"${e.astHash}"` : + (isMonad(e)) ? `M"${e.mHash}"` : + e + } + + let m = monad.mVal + while (1) { + sb += elemToStr(m.p) + if (m.n === undefined) break + else { + sb += "," + m = m.n + } + } + } + else { + throw new BASICerror("unknown monad subtype: "+m.mType) + }*/ + return sb +} +let arrayToString = function(a) { + let acc = "" + for (let k = 0; k < a.length; k++) { + if (k > 0) acc += "," + acc += (Array.isArray(a[k])) ? arrayToString(a[k]) : a[k] + } + return "{"+acc+"}" +} +let theLambdaBoundVars = function() { + let sb = "" + lambdaBoundVars.forEach((it,i) => { + if (i > 0) sb += ' |' + sb += ` ${i} [` + it.forEach((it,i) => { + if (i > 0) sb += ',' + sb += `${it[0]}:${it[1]}` // type and value pair + }) + sb += ']' + }) + return sb +} +let makeBase32Hash = ()=>[1,2,3,4,5].map(i=>"YBNDRFG8EJKMCPQXOTLVWIS2A345H769"[Math.random()*32|0]).join('') +let BasicAST = function() { + this.astLnum = 0 + this.astLeaves = [] + this.astSeps = [] + this.astValue = undefined + this.astType = "null" // lit, op, string, num, array, function, null, defun_args (! NOT usrdefun !) + this.astHash = makeBase32Hash() +} +// I'm basically duck-typing here... +let isAST = (object) => (object === undefined) ? false : object.astLeaves !== undefined && object.astHash !== undefined +let isRunnable = (object) => isAST(object) || object.mType == "funseq" +let BasicFunSeq = function(f) { + if (!Array.isArray(f) || !isAST(f[0])) throw new BASICerror("Not an array of functions") + this.mHash = makeBase32Hash() + this.mType = "funseq" + this.mVal = f +} +/** A List Monad (a special case of Value-monad) + * This monad MUST follow the monad law! + * @param m a monadic value (Javascript array) + */ +let BasicListMonad = function(m) { + this.mHash = makeBase32Hash() + this.mType = "list" + this.mVal = [m] +} +/** A Memoisation Monad, aka the most generic monad + * This monad MUST follow the monad law! + * @param m a monadic value + */ +/* Test this monad with following program + * This program requires (>>=) to "play by the rules" +10 LSORT=[XS]~>IF LEN(XS)<1 THEN NIL ELSE LSORT(FILTER([K]~>KK>=HEAD XS,TAIL XS)) +20 LREV=[XS]~>MAP([I]~>XS(I),LEN(XS)-1 TO 0 STEP -1) +30 LINC=[XS]~>MAP([I]~>I+1,XS) +40 L=7!9!4!5!2!3!1!8!6!NIL +100 MAGICKER=[XS]~>MRET(LSORT(XS))>>=([X]~>MRET(LREV(X))>>=([X]~>MRET(LINC(X)))) +110 MAGICK_L = MAGICKER(L) +120 PRINT MAGICK_L() + */ +/* Value-monad satisfies monad laws, test with following program +10 F=[X]~>X*2 : G=[X]~>X^3 : RETN=[X]~>MRET(X) + +100 PRINT:PRINT "First law: 'return a >>= k' equals to 'k a'" +110 K=[X]~>RETN(F(X)) : REM K is monad-returning function +120 A=42 +130 KM=RETN(A)>>=K +140 KO=K(A) +150 PRINT("KM is ";TYPEOF(KM);", ";MJOIN(KM)) +160 PRINT("KO is ";TYPEOF(KO);", ";MJOIN(KO)) + +200 PRINT:PRINT "Second law: 'm >>= return' equals to 'm'" +210 M=MRET(G(42)) +220 MM=M>>=RETN +230 MO=M +240 PRINT("MM is ";TYPEOF(MM);", ";MJOIN(MM)) +250 PRINT("MO is ";TYPEOF(MO);", ";MJOIN(MO)) + +300 PRINT:PRINT "Third law: 'm >>= (\x -> k x >>= h)' equals to '(m >>= k) >>= h'" +310 REM see line 110 for the definition of K +320 H=[X]~>RETN(G(X)) : REM H is monad-returning function +330 M=MRET(69) +340 M1=M>>=([X]~>K(X)>>=H) +350 M2=(M>>=K)>>=H +360 PRINT("M1 is ";TYPEOF(M1);", ";MJOIN(M1)) +370 PRINT("M2 is ";TYPEOF(M2);", ";MJOIN(M2)) + */ +let BasicMemoMonad = function(m) { + this.mHash = makeBase32Hash() + this.mType = "value" + this.mVal = m // a monadic value + this.seq = undefined // unused +} +// I'm basically duck-typing here... +let isMonad = (o) => (o === undefined) ? false : (o.mType !== undefined) + +let literalTypes = ["string", "num", "bool", "array", "generator", "usrdefun", "monad"] +/* +@param variable SyntaxTreeReturnObj, of which the 'troType' is defined in BasicAST. +@return a value, if the input type if string or number, its literal value will be returned. Otherwise will search the + BASIC variable table and return the literal value of the BasicVar; undefined will be returned if no such var exists. +*/ +let resolve = function(variable) { + if (variable === undefined) return undefined + // head error checking + if (variable.troType === undefined) { + // primitves types somehow injected from elsewhere (main culprit: MAP) + //throw Error(`BasicIntpError: trying to resolve unknown object '${variable}' with entries ${Object.entries(variable)}`) + + if (isNumable(variable)) return tonum(variable) + if (Array.isArray(variable)) return variable + if (isGenerator(variable) || isAST(variable) || isMonad(variable)) return variable + if (typeof variable == "object") + throw Error(`BasicIntpError: trying to resolve unknown object '${variable}' with entries ${Object.entries(variable)}`) + return variable + } + else if (variable.troType === "internal_arrindexing_lazy") + return eval("variable.troValue.arrFull"+variable.troValue.arrKey) + else if (literalTypes.includes(variable.troType) || variable.troType.startsWith("internal_")) + return variable.troValue + else if (variable.troType == "lit") { + if (isNumable(variable.troValue)) { // rarely we get a number as a variable name, notably on (&) + return tonum(variable.troValue) + } + // when program tries to call builtin function (e.g. SIN), return usrdefun-wrapped version + else if (bS.builtin[variable.troValue] !== undefined) { + return bS.wrapBuiltinToUsrdefun(variable.troValue) + } + // else, it's just a plain-old variable :p + else { + let basicVar = bS.vars[variable.troValue] + if (basicVar === undefined) throw lang.refError(undefined, variable.troValue) + if (basicVar.bvLiteral === "") return "" + return (basicVar !== undefined) ? basicVar.bvLiteral : undefined + } + } + else if (variable.troType == "null") + return undefined + // tail error checking + else + throw Error("BasicIntpError: unknown variable/object with type "+variable.troType+", with value "+variable.troValue) +} +let findHighestIndex = function(exprTree) { + let highestIndex = [-1,-1] + // look for the highest index of [a,b] + let rec = function(exprTree) { + bF._recurseApplyAST(exprTree, it => { + if (it.astType == "defun_args") { + let recIndex = it.astValue[0] + let ordIndex = it.astValue[1] + + if (recIndex > highestIndex[0]) { + highestIndex = [recIndex, 0] + } + + if (recIndex == highestIndex[0] && ordIndex > highestIndex[1]) { + highestIndex[1] = ordIndex + } + } + else if (isAST(it.astValue)) { + rec(it.astValue) + } + }) + };rec(exprTree) + return highestIndex +} +let indexDec = function(node, recIndex) { + if (node.astType == "defun_args" && node.astValue[0] === recIndex) { + let newNode = cloneObject(node) + newNode.astValue[1] -= 1 + return newNode + } + else return node +} +let curryDefun = function(inputTree, inputValue) { + let exprTree = cloneObject(inputTree) + let value = cloneObject(inputValue) + let highestIndex = findHighestIndex(exprTree)[0] + + if (DBGON) { + serial.println("[curryDefun] highest index to curry: "+highestIndex) + } + + let substitution = new BasicAST() + if (isAST(value)) { + substitution = value + } + else { + substitution.astLnum = "??" + substitution.astType = JStoBASICtype(value) + substitution.astValue = value + } + + // substitute the highest index with given value + /*bF._recurseApplyAST(exprTree, it => { + return (it.astType == "defun_args" && it.astValue[0] === highestIndex[0] && it.astValue[1] === highestIndex[1]) ? substitution : it + });*/ + + // substitute the highest index [max recIndex, 0] with given value + // and if recIndex is same as the highestIndex and ordIndex is greater than zero, + // decrement the ordIndex + bF._recurseApplyAST(exprTree, it => { + return (it.astType == "defun_args" && it.astValue[0] === highestIndex && it.astValue[1] === 0) ? substitution : indexDec(it, highestIndex) + }) + + return exprTree +} +let getMonadEvalFun = (monad) => function(lnum, stmtnum, args, sep) { + if (!isMonad(monad)) throw lang.badFunctionCallFormat(lnum, "not a monad") + + if (DBGON) { + serial.println("[BASIC.MONADEVAL] monad:") + serial.println(monadToString(monad)) + } + + if (monad.mType == "funseq") { + let arg = args[0] + monad.mVal.forEach(f => { + arg = bS.getDefunThunk(f)(lnum, stmtnum, [arg]) + }) + return arg + } + else { + // args are futile + return monad.mVal + } +} +let listMonConcat = function(parentm, childm) { + parentm.mVal = parentm.mVal.concat(childm.mVal) + return parentm +} +let countArgs = function(defunTree) { + let cnt = -1 + bF._recurseApplyAST(defunTree, it => { + if (it.astType == "defun_args" && it.astValue > cnt) + cnt = it.astValue + }) + + return cnt+1 +} +let argCheckErr = function(lnum, o) { + if (o === undefined) throw lang.refError(lnum, "(variable is undefined)") + if (o.troType == "null") throw lang.refError(lnum, o) + if (o.troType == "lit" && bS.builtin[o.troValue] !== undefined) return + if (o.troType == "lit" && bS.vars[o.troValue] === undefined) throw lang.refError(lnum, o.troValue) +} +let oneArg = function(lnum, stmtnum, args, action) { + if (args.length != 1) throw lang.syntaxfehler(lnum, args.length+lang.aG) + argCheckErr(lnum, args[0]) + var rsvArg0 = resolve(args[0]) + return action(rsvArg0) +} +let oneArgNul = function(lnum, stmtnum, args, action) { + if (args.length != 1) throw lang.syntaxfehler(lnum, args.length+lang.aG) + var rsvArg0 = resolve(args[0]) + return action(rsvArg0) +} +let oneArgNum = function(lnum, stmtnum, args, action) { + if (args.length != 1) throw lang.syntaxfehler(lnum, args.length+lang.aG) + argCheckErr(lnum, args[0]) + var rsvArg0 = resolve(args[0], 1) + if (!isNumable(rsvArg0)) throw lang.illegalType(lnum, args[0]) + return action(rsvArg0) +} +let twoArg = function(lnum, stmtnum, args, action) { + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + argCheckErr(lnum, args[0]) + var rsvArg0 = resolve(args[0]) + argCheckErr(lnum, args[1]) + var rsvArg1 = resolve(args[1]) + return action(rsvArg0, rsvArg1) +} +let twoArgNul = function(lnum, stmtnum, args, action) { + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + var rsvArg0 = resolve(args[0]) + var rsvArg1 = resolve(args[1]) + return action(rsvArg0, rsvArg1) +} +let twoArgNum = function(lnum, stmtnum, args, action) { + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + argCheckErr(lnum, args[0]) + var rsvArg0 = resolve(args[0], 1) + if (!isNumable(rsvArg0)) throw lang.illegalType(lnum, "LH:"+Object.entries(args[0])) + argCheckErr(lnum, args[1]) + var rsvArg1 = resolve(args[1], 1) + if (!isNumable(rsvArg1)) throw lang.illegalType(lnum, "RH:"+Object.entries(args[1])) + return action(rsvArg0, rsvArg1) +} +let threeArg = function(lnum, stmtnum, args, action) { + if (args.length != 3) throw lang.syntaxfehler(lnum, args.length+lang.aG) + argCheckErr(lnum, args[0]) + var rsvArg0 = resolve(args[0]) + argCheckErr(lnum, args[1]) + var rsvArg1 = resolve(args[1]) + argCheckErr(lnum, args[2]) + var rsvArg2 = resolve(args[2]) + return action(rsvArg0, rsvArg1, rsvArg2) +} +let threeArgNum = function(lnum, stmtnum, args, action) { + if (args.length != 3) throw lang.syntaxfehler(lnum, args.length+lang.aG) + argCheckErr(lnum, args[0]) + var rsvArg0 = resolve(args[0], 1) + if (!isNumable(rsvArg0)) throw lang.illegalType(lnum, "1H:"+Object.entries(args[0])) + argCheckErr(lnum, args[1]) + var rsvArg1 = resolve(args[1], 1) + if (!isNumable(rsvArg1)) throw lang.illegalType(lnum, "2H:"+Object.entries(args[1])) + argCheckErr(lnum, args[2]) + var rsvArg2 = resolve(args[2], 1) + if (!isNumable(rsvArg2)) throw lang.illegalType(lnum, "3H:"+Object.entries(args[2])) + return action(rsvArg0, rsvArg1, rsvArg2) +} +let varArg = function(lnum, stmtnum, args, action) { + var rsvArg = args.map((it) => { + argCheckErr(lnum, it) + var r = resolve(it) + return r + }) + return action(rsvArg) +} +let varArgNum = function(lnum, stmtnum, args, action) { + var rsvArg = args.map((it) => { + argCheckErr(lnum, it) + var r = resolve(it) + if (isNaN(r)) throw lang.illegalType(lnum, r) + return r + }) + return action(rsvArg) +} +let makeIdFun = () => { + return JSON.parse(`{"astLnum":"**","astLeaves":[],"astSeps":[],"astValue":[0,0],"astType":"defun_args","astHash":"IDFUN"}`) +} +let _basicConsts = { + "NIL": new BasicVar([], "array"), + "PI": new BasicVar(Math.PI, "num"), + "TAU": new BasicVar(Math.PI * 2.0, "num"), + "EULER": new BasicVar(Math.E, "num"), + "ID": new BasicVar(makeIdFun(), "usrdefun"), + "UNDEFINED": new BasicVar(undefined, "null"), + "TRUE": new BasicVar(true, "bool"), + "FALSE": new BasicVar(false, "bool") +} +Object.freeze(_basicConsts) +let initBvars = function() { + return cloneObject(_basicConsts) +} +let ForGen = function(s,e,t) { + this.start = s + this.end = e + this.step = t || 1 + + this.current = this.start + this.stepsgn = (this.step > 0) ? 1 : -1 +} +// I'm basically duck-typing here... +let isGenerator = (o) => o.start !== undefined && o.end !== undefined && o.step !== undefined && o.stepsgn !== undefined +let genToArray = (gen) => { + let a = [] + let cur = gen.start + while (cur*gen.stepsgn + gen.step*gen.stepsgn <= (gen.end + gen.step)*gen.stepsgn) { + a.push(cur) + cur += gen.step + } + return a +} +let genHasHext = (o) => o.current*o.stepsgn + o.step*o.stepsgn <= (o.end + o.step)*o.stepsgn +let genGetNext = (gen, mutated) => { + if (mutated !== undefined) gen.current = tonum(mutated) + gen.current += gen.step + return genHasHext(gen) ? gen.current : undefined +} +let genToString = (gen) => `Generator: ${gen.start} to ${gen.end}`+((gen.step !== 1) ? ` step ${gen.step}` : '') +let genReset = (gen) => { gen.current = gen.start } +let bS = {} // BASIC status +bS.gosubStack = [] +bS.forLnums = {} // key: forVar, value: [lnum, stmtnum] +bS.forStack = [] // forVars only +bS.vars = initBvars() // contains instances of BasicVars +bS.rnd = 0 // stores mantissa (23 bits long) of single precision floating point number +bS.getDimSize = function(array, dim) { + var dims = [] + while (true) { + dims.push(array.length) + + if (Array.isArray(array[0])) + array = array[0] + else + break + } + return dims[dim] +} +bS.getArrayIndexFun = function(lnum, stmtnum, arrayName, array) { + if (lnum === undefined || stmtnum === undefined) throw Error(`Line or statement number is undefined: (${lnum},${stmtnum})`) + + return function(lnum, stmtnum, args, seps) { + if (lnum === undefined || stmtnum === undefined) throw Error(`Line or statement number is undefined: (${lnum},${stmtnum})`) + + return varArgNum(lnum, stmtnum, args, (dims) => { + if (TRACEON) serial.println("ar dims: "+dims) + + let dimcnt = 1 + let oldIndexingStr = "" + let indexingstr = "" + + dims.forEach(d => { + oldIndexingStr = indexingstr + indexingstr += `[${d-INDEX_BASE}]` + + var testingArr = eval(`array${indexingstr}`) + if (testingArr === undefined) + throw lang.subscrOutOfRng(lnum, `${arrayName}${oldIndexingStr} (${lang.ord(dimcnt)} dim)`, d-INDEX_BASE, bS.getDimSize(array, dimcnt-1)) + + dimcnt += 1 + }) + + if (TRACEON) + serial.println("ar indexedValue = "+`/*ar1*/array${indexingstr}`) + + return {arrFull: array, arrName: arrayName, arrKey: indexingstr} + }) + } +} +/** + * @return a Javascript function that when called, evaluates the exprTree + */ +bS.getDefunThunk = function(exprTree, norename) { + if (!isRunnable(exprTree)) throw new BASICerror("not a syntax tree") + + // turns funseq-monad into runnable function + if (isMonad(exprTree)) return getMonadEvalFun(exprTree) + + let tree = cloneObject(exprTree) // ALWAYS create new tree instance! + + return function(lnum, stmtnum, args, seps) { + if (lnum === undefined || stmtnum === undefined) throw Error(`Line or statement number is undefined: (${lnum},${stmtnum})`) + + if (!norename) { + let argsMap = args.map(it => { + //argCheckErr(lnum, it) + let rit = resolve(it) + return [JStoBASICtype(rit), rit] // substitute for [astType, astValue] + }) + + // bind arguments + lambdaBoundVars.unshift(argsMap) + + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] unthunking: ") + serial.println(astToString(tree)) + serial.println("[BASIC.getDefunThunk.invoke] thunk args:") + serial.println(argsMap) + serial.println("[BASIC.getDefunThunk.invoke] lambda bound vars:") + serial.println(theLambdaBoundVars()) + } + + // perform renaming + bF._recurseApplyAST(tree, (it) => { + if ("defun_args" == it.astType) { + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] thunk renaming arg-tree branch:") + serial.println(astToString(it)) + } + + let recIndex = it.astValue[0] + let argIndex = it.astValue[1] + + let theArg = lambdaBoundVars[recIndex][argIndex] // instanceof theArg == resolved version of SyntaxTreeReturnObj + + if (theArg !== undefined) { // this "forgiveness" is required to implement currying + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] thunk renaming-theArg: "+theArg) + serial.println(`${Object.entries(theArg)}`) + } + + if (theArg[0] === "null") { + throw new BASICerror(`Bound variable is ${theArg}; lambdaBoundVars: ${theLambdaBoundVars()}`) + } + + it.astValue = theArg[1] + it.astType = theArg[0] + } + + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] thunk successfully renamed arg-tree branch:") + serial.println(astToString(it)) + } + } + }) + + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] resulting thunk tree:") + serial.println(astToString(tree)) + } + } + else { + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] no rename, resulting thunk tree:") + serial.println(astToString(tree)) + } + } + + // evaluate new tree + if (DBGON) { + serial.println("[BASIC.getDefunThunk.invoke] evaluating tree:") + } + let ret = resolve(bF._executeSyntaxTree(lnum, stmtnum, tree, 0)) + + // unbind previously bound arguments + if (!norename) { + lambdaBoundVars.shift() + } + + return ret + } +} +bS.wrapBuiltinToUsrdefun = function(funcname) { + let argCount = bS.builtin[funcname].argc + + if (argCount === undefined) throw new BASICerror(`${funcname} cannot be wrapped into usrdefun`) + + let leaves = [] + for (let k = 0; k < argCount; k++) { + let l = new BasicAST() + l.astLnum = "**" + l.astValue = [0,k] + l.astType = "defun_args" + + leaves.push(l) + } + + let tree = new BasicAST() + tree.astLnum = "**" + tree.astValue = funcname + tree.astType = "function" + tree.astLeaves = leaves + + return tree +} +/* Accepts troValue, assignes to BASIC variable, and returns internal_assign_object + * @params troValue Variable to assign into + * @params rh the value, resolved + */ +bS.addAsBasicVar = function(lnum, troValue, rh) { + if (troValue.arrFull !== undefined) { // assign to existing array + let arr = eval("troValue.arrFull"+troValue.arrKey) + if (Array.isArray(arr)) throw lang.subscrOutOfRng(lnum, arr) + eval("troValue.arrFull"+troValue.arrKey+"=rh") + return {asgnVarName: troValue.arrName, asgnValue: rh} + } + else { + let varname = troValue.toUpperCase() + //println("input varname: "+varname) + if (_basicConsts[varname]) throw lang.asgnOnConst(lnum, varname) + if (bS.builtin[varname]) throw lang.asgnOnConst(lnum, varname) + let type = JStoBASICtype(rh) + bS.vars[varname] = new BasicVar(rh, type) + return {asgnVarName: varname, asgnValue: rh} + } +} +bS.builtin = { +/* +@param lnum line number +@param args instance of the SyntaxTreeReturnObj + +if no args were given (e.g. "10 NEXT()"), args[0] will be: {troType: null, troValue: , troNextLine: 11} +if no arg text were given (e.g. "10 NEXT"), args will have zero length +*/ +"=" : {argc:2, f:function(lnum, stmtnum, args) { + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + var troValue = args[0].troValue + + var rh = resolve(args[1]) + if (rh === undefined) throw lang.refError(lnum, "RH:"+args[1].troValue) + + if (isNumable(rh)) rh = tonum(rh) // if string we got can be cast to number, do it + + //println(lnum+" = lh: "+Object.entries(args[0])) + //println(lnum+" = rh raw: "+Object.entries(args[1])) + //println(lnum+" = rh resolved: "+rh) + //try { println(lnum+" = rh resolved entries: "+Object.entries(rh)) } + //catch (_) {} + + return bS.addAsBasicVar(lnum, troValue, rh) +}}, +"IN" : {argc:2, f:function(lnum, stmtnum, args) { // almost same as =, but don't actually make new variable. Used by FOR statement + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + var troValue = args[0].troValue + + var rh = resolve(args[1]) + if (rh === undefined) throw lang.refError(lnum, "RH:"+args[1].troValue) + + if (troValue.arrFull !== undefined) { + throw lang.syntaxfehler(lnum) + } + else { + var varname = troValue.toUpperCase() + var type = JStoBASICtype(rh) + if (_basicConsts[varname]) throw lang.asgnOnConst(lnum, varname) + return {asgnVarName: varname, asgnValue: rh} + } +}}, +"==" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNul(lnum, stmtnum, args, (lh,rh) => lh == rh) +}}, +"<>" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (lh,rh) => lh != rh) +}}, +"><" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (lh,rh) => lh != rh) +}}, +"<=" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh <= rh) +}}, +"=<" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh <= rh) +}}, +">=" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh >= rh) +}}, +"=>" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh >= rh) +}}, +"<" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh < rh) +}}, +">" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh > rh) +}}, +"<<" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh << rh) +}}, +">>" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh >>> rh) +}}, +"UNARYMINUS" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => -lh) +}}, +"UNARYPLUS" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => +lh) +}}, +"UNARYLOGICNOT" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => !(lh)) +}}, +"UNARYBNOT" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => ~(lh)) +}}, +"BAND" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh & rh) +}}, +"BOR" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh | rh) +}}, +"BXOR" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh ^ rh) +}}, +"!" : {argc:2, f:function(lnum, stmtnum, args) { // Haskell-style CONS + return twoArg(lnum, stmtnum, args, (lh,rh) => { + if (Array.isArray(rh)) { + return [lh].concat(rh) + } + else if (rh.mType === "list") { + rh.mVal = [lh].concat(rh.mVal) + return rh + } + else throw lang.illegalType(lnum, rh) + }) +}}, +"~" : {argc:2, f:function(lnum, stmtnum, args) { // array PUSH + return twoArg(lnum, stmtnum, args, (lh,rh) => { + if (Array.isArray(lh)) { + return lh.concat([rh]) + } + else if (lh.mType === "list") { + lh.mVal = [lh.mVal].concat([rh]) + return lh + } + else throw lang.illegalType(lnum, lh) + }) +}}, +"#" : {argc:2, f:function(lnum, stmtnum, args) { // array CONCAT + return twoArg(lnum, stmtnum, args, (lh,rh) => { + if (Array.isArray(lh) && Array.isArray(rh)) { + return lh.concat(rh) + } + else if (lh.mType == "list" && rh.mType == "list") { + let newMval = lh.mVal.concat(rh.mVal) + return new BasicListMonad(newMval) + } + else + throw lang.illegalType(lnum) + }) +}}, +"+" : {argc:2, f:function(lnum, stmtnum, args) { // addition, string concat + return twoArg(lnum, stmtnum, args, (lh,rh) => (!isNaN(lh) && !isNaN(rh)) ? (tonum(lh) + tonum(rh)) : (lh + rh)) +}}, +"-" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh - rh) +}}, +"*" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => lh * rh) +}}, +"/" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => { + if (rh == 0) throw lang.divByZero + return lh / rh + }) +}}, +"\\" : {argc:2, f:function(lnum, stmtnum, args) { // integer division, rounded towards zero + return twoArgNum(lnum, stmtnum, args, (lh,rh) => { + if (rh == 0) throw lang.divByZero + return (lh / rh)|0 + }) +}}, +"MOD" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => { + if (rh == 0) throw lang.divByZero + return lh % rh + }) +}}, +"^" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (lh,rh) => { + let r = Math.pow(lh, rh) + if (isNaN(r)) throw lang.badFunctionCallFormat(lnum) + if (!isFinite(r)) throw lang.divByZero + return r + }) +}}, +"TO" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (from, to) => new ForGen(from, to, 1)) +}}, +"STEP" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (gen, step) => { + if (!isGenerator(gen)) throw lang.illegalType(lnum, gen) + return new ForGen(gen.start, gen.end, step) + }) +}}, +"DIM" : {f:function(lnum, stmtnum, args) { + return varArgNum(lnum, stmtnum, args, (revdims) => { + let dims = revdims.reverse() + let arraydec = "Array(dims[0]).fill(0)" + for (let k = 1; k < dims.length; k++) { + arraydec = `Array(dims[${k}]).fill().map(_=>${arraydec})` + } + return eval(arraydec) + }) +}}, +"ARRAY CONSTRUCTOR" : {f:function(lnum, stmtnum, args) { + return args.map(v => resolve(v)) +}}, +"PRINT" : {argc:1, f:function(lnum, stmtnum, args, seps) { + if (args.length == 0) + println() + else { + for (var llll = 0; llll < args.length; llll++) { + // parse separators. + // ; - concat + // , - tab + if (llll >= 1) { + if (seps[llll - 1] == ",") print("\t") + } + + var rsvArg = resolve(args[llll]) + //if (rsvArg === undefined && args[llll] !== undefined && args[llll].troType != "null") throw lang.refError(lnum, args[llll].troValue) + + //serial.println(`${lnum} PRINT ${lang.ord(llll)} arg: ${Object.entries(args[llll])}, resolved: ${rsvArg}`) + + let printstr = "" + if (Array.isArray(rsvArg)) + printstr = arrayToString(rsvArg) + else if (rsvArg === undefined || rsvArg === "") + printstr = "" + else if (rsvArg.toString !== undefined) + printstr = rsvArg.toString() + else + printstr = rsvArg + + print(printstr) + if (TRACEON) serial.println("[BASIC.PRINT] "+printstr) + } + } + + if (args[args.length - 1] !== undefined && args[args.length - 1].troType != "null") println() +}}, +"EMIT" : {argc:1, f:function(lnum, stmtnum, args, seps) { + if (args.length == 0) + println() + else { + for (var llll = 0; llll < args.length; llll++) { + // parse separators. + // ; - concat + // , - tab + if (llll >= 1) { + if (seps[llll - 1] == ",") print("\t") + } + + var rsvArg = resolve(args[llll]) + if (rsvArg === undefined && args[llll] !== undefined && args[llll].troType != "null") throw lang.refError(lnum, args[llll].troValue) + + let printstr = "" + if (rsvArg === undefined) + print("") + else if (isNumable(rsvArg)) { + let c = con.getyx() + con.addch(tonum(rsvArg)) + con.move(c[0],c[1]+1) + } + else if (rsvArg.toString !== undefined) + print(rsvArg.toString()) + else + printstr = (rsvArg) + + if (TRACEON) serial.println("[BASIC.EMIT] "+printstr) + } + } + + if (args[args.length - 1] !== undefined && args[args.length - 1].troType != "null") println() +}}, +"POKE" : {argc:2, f:function(lnum, stmtnum, args) { + twoArgNum(lnum, stmtnum, args, (lh,rh) => sys.poke(lh, rh)) +}}, +"PEEK" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => sys.peek(lh)) +}}, +"GOTO" : {argc:1, f:function(lnum, stmtnum, args) { + // search from gotoLabels first + let line = gotoLabels[args[0].troValue] + // if not found, use resolved variable + if (line === undefined) line = resolve(args[0]) + if (line < 0) throw lang.syntaxfehler(lnum, line) + + return new JumpObj(line, 0, lnum, line) +}}, +"GOSUB" : {argc:1, f:function(lnum, stmtnum, args) { + // search from gotoLabels first + let line = gotoLabels[args[0].troValue] + // if not found, use resolved variable + if (line === undefined) line = resolve(args[0]) + if (line < 0) throw lang.syntaxfehler(lnum, line) + + bS.gosubStack.push([lnum, stmtnum + 1]) + //println(lnum+" GOSUB into "+lh) + return new JumpObj(line, 0, lnum, line) +}}, +"RETURN" : {f:function(lnum, stmtnum, args) { + var r = bS.gosubStack.pop() + if (r === undefined) throw lang.nowhereToReturn(lnum) + //println(lnum+" RETURN to "+r) + return new JumpObj(r[0], r[1], lnum, r) +}}, +"CLEAR" : {argc:0, f:function(lnum, stmtnum, args) { + bS.vars = initBvars() +}}, +"PLOT" : {argc:3, f:function(lnum, stmtnum, args) { + threeArgNum(lnum, stmtnum, args, (xpos, ypos, color) => graphics.plotPixel(xpos, ypos, color)) +}}, +"AND" : {argc:2, f:function(lnum, stmtnum, args) { + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + var rsvArg = args.map((it) => resolve(it)) + rsvArg.forEach((v) => { + if (v === undefined) throw lang.refError(lnum, v) + if (typeof v !== "boolean") throw lang.illegalType(lnum, v) + }) + var argum = rsvArg.map((it) => { + if (it === undefined) throw lang.refError(lnum, it) + return it + }) + return argum[0] && argum[1] +}}, +"OR" : {argc:2, f:function(lnum, stmtnum, args) { + if (args.length != 2) throw lang.syntaxfehler(lnum, args.length+lang.aG) + var rsvArg = args.map((it) => resolve(it)) + rsvArg.forEach((v) => { + if (v === undefined) throw lang.refError(lnum, v.value) + if (typeof v !== "boolean") throw lang.illegalType(lnum, v) + }) + var argum = rsvArg.map((it) => { + if (it === undefined) throw lang.refError(lnum, it) + return it + }) + return argum[0] || argum[1] +}}, +"RND" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => { + if (!(args.length > 0 && args[0].troValue === 0)) + bS.rnd = Math.random()//(bS.rnd * 214013 + 2531011) % 16777216 // GW-BASIC does this + return bS.rnd + }) +}}, +"ROUND" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => Math.round(lh)) +}}, +"FLOOR" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => Math.floor(lh)) +}}, +"INT" : {argc:1, f:function(lnum, stmtnum, args) { // synonymous to FLOOR + return oneArgNum(lnum, stmtnum, args, (lh) => Math.floor(lh)) +}}, +"CEIL" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => Math.ceil(lh)) +}}, +"FIX" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => (lh|0)) +}}, +"CHR" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => String.fromCharCode(lh)) +}}, +"TEST" : {argc:1, f:function(lnum, stmtnum, args) { + if (args.length != 1) throw lang.syntaxfehler(lnum, args.length+lang.aG) + return !!resolve(args[0]) // string 'false' will be interpreted as truthy; this is completely intentional +}}, +"FOREACH" : {f:function(lnum, stmtnum, args) { // list comprehension model + var asgnObj = resolve(args[0]) + // type check + if (asgnObj === undefined) throw lang.syntaxfehler(lnum) + if (!Array.isArray(asgnObj.asgnValue)) throw lang.illegalType(lnum, asgnObj) + + var varname = asgnObj.asgnVarName + + // assign new variable + // the var itself will have head of the array, and the head itself will be removed from the array + bS.vars[varname] = new BasicVar(asgnObj.asgnValue[0], JStoBASICtype(asgnObj.asgnValue.shift())) + // stores entire array (sans head) into temporary storage + bS.vars["for var "+varname] = new BasicVar(asgnObj.asgnValue, "array") + // put the varname to forstack + bS.forLnums[varname] = [lnum, stmtnum] + bS.forStack.push(varname) +}}, +"FOR" : {f:function(lnum, stmtnum, args) { // generator model + var asgnObj = resolve(args[0]) + // type check + if (asgnObj === undefined) throw lang.syntaxfehler(lnum) + if (!isGenerator(asgnObj.asgnValue)) throw lang.illegalType(lnum, typeof asgnObj) + + var varname = asgnObj.asgnVarName + var generator = asgnObj.asgnValue + + // assign new variable + // the var itself will have head of the array, and the head itself will be removed from the array + bS.vars[varname] = new BasicVar(generator.start, "num") + // stores entire array (sans head) into temporary storage + bS.vars["for var "+varname] = new BasicVar(generator, "generator") + // put the varname to forstack + bS.forLnums[varname] = [lnum, stmtnum] + bS.forStack.push(varname) +}}, +"NEXT" : {f:function(lnum, stmtnum, args) { + // if no args were given + if (args.length == 0 || (args.length == 1 && args.troType == "null")) { + // go to most recent FOR + var forVarname = bS.forStack.pop() + //serial.println(lnum+" NEXT > forVarname = "+forVarname) + if (forVarname === undefined) { + throw lang.nextWithoutFor(lnum) + } + + if (TRACEON) serial.println("[BASIC.FOR] looping "+forVarname) + + var forVar = bS.vars["for var "+forVarname].bvLiteral + + if (isGenerator(forVar)) + bS.vars[forVarname].bvLiteral = genGetNext(forVar, bS.vars[forVarname].bvLiteral) + else + bS.vars[forVarname].bvLiteral = forVar.shift() + + if ((bS.vars[forVarname].bvLiteral !== undefined)) { + // feed popped value back, we're not done yet + bS.forStack.push(forVarname) + let forLnum = bS.forLnums[forVarname] + return new JumpObj(forLnum[0], forLnum[1]+1, lnum, [forLnum[0], forLnum[1]+1]) // goto the statement RIGHT AFTER the FOR-declaration + } + else { + if (isGenerator(forVar)) + bS.vars[forVarname].bvLiteral = forVar.current // true BASIC compatibility for generator + else + bS.vars[forVarname] === undefined // unregister the variable + + return new JumpObj(lnum, stmtnum + 1, lnum, [lnum, stmtnum + 1]) + } + } + + throw lang.syntaxfehler(lnum, "extra arguments for NEXT") +}}, +/* +10 input;"what is your name";a$ + +£ Line 10 (function) +| leaves: 3 +| value: input (type: string) +£ Line 0 (null) +| leaves: 0 +| value: undefined (type: undefined) +`----------------- +| +| ¶ Line 10 (string) +| | leaves: 0 +| | value: what is your name (type: string) +| `----------------- +| +| i Line 10 (literal) +| | leaves: 0 +| | value: A$ (type: string) +| `----------------- +`----------------- +10 input "what is your name";a$ + +£ Line 10 (function) +| leaves: 2 +| value: input (type: string) +| ¶ Line 10 (string) +| | leaves: 0 +| | value: what is your name (type: string) +| `----------------- +| +| i Line 10 (literal) +| | leaves: 0 +| | value: A$ (type: string) +| `----------------- +`----------------- +*/ +"INPUT" : {argc:1, f:function(lnum, stmtnum, args) { + if (args.length != 1) throw lang.syntaxfehler(lnum, args.length+lang.aG) + let troValue = args[0].troValue + + // print out prompt text + print("? "); var rh = sys.read().trim() + + // if string we got can be cast to number, do it + // NOTE: empty string will be cast to 0, which corresponds to GW-BASIC + if (!isNaN(rh)) rh = tonum(rh) + + return bS.addAsBasicVar(lnum, troValue, rh) +}}, +"CIN" : {argc:0, f:function(lnum, stmtnum, args) { + return sys.read().trim() +}}, +"END" : {argc:0, f:function(lnum, stmtnum, args) { + serial.println("Program terminated in "+lnum) + return new JumpObj(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER - 1, lnum, undefined) // GOTO far-far-away +}}, +"SPC" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => " ".repeat(lh)) +}}, +"LEFT" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (str, len) => str.substring(0, len)) +}}, +"MID" : {argc:3, f:function(lnum, stmtnum, args) { + return threeArg(lnum, stmtnum, args, (str, start, len) => str.substring(start-INDEX_BASE, start-INDEX_BASE+len)) +}}, +"RIGHT" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (str, len) => str.substring(str.length - len, str.length)) +}}, +"SGN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => (it > 0) ? 1 : (it < 0) ? -1 : 0) +}}, +"ABS" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.abs(it)) +}}, +"SIN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.sin(it)) +}}, +"COS" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.cos(it)) +}}, +"TAN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.tan(it)) +}}, +"EXP" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.exp(it)) +}}, +"ASN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.asin(it)) +}}, +"ACO" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.acos(it)) +}}, +"ATN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.atan(it)) +}}, +"SQR" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.sqrt(it)) +}}, +"CBR" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.cbrt(it)) +}}, +"SINH" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.sinh(it)) +}}, +"COSH" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.cosh(it)) +}}, +"TANH" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.tanh(it)) +}}, +"LOG" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (it) => Math.log(it)) +}}, +"RESTORE" : {argc:0, f:function(lnum, stmtnum, args) { + DATA_CURSOR = 0 +}}, +"READ" : {argc:1, f:function(lnum, stmtnum, args) { + if (args.length != 1) throw lang.syntaxfehler(lnum, args.length+lang.aG) + let troValue = args[0].troValue + + let rh = DATA_CONSTS[DATA_CURSOR++] + if (rh === undefined) throw lang.outOfData(lnum) + + return bS.addAsBasicVar(lnum, troValue, rh) +}}, +"DGET" : {argc:0, f:function(lnum, stmtnum, args) { + let r = DATA_CONSTS[DATA_CURSOR++] + if (r === undefined) throw lang.outOfData(lnum) + return r +}}, +"OPTIONBASE" : {f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (lh) => { + if (lh != 0 && lh != 1) throw lang.syntaxfehler(line) + INDEX_BASE = lh|0 + }) +}}, +"DATA" : {f:function(lnum, stmtnum, args) { + if (prescan) { + args.forEach(it => DATA_CONSTS.push(resolve(it))) + } +}}, +/* Syopsis: MAP function, functor + */ +"MAP" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (fn, functor) => { + if (!isRunnable(fn)) throw lang.badFunctionCallFormat(lnum, "first argument is not a function: got "+JStoBASICtype(fn)) + if (!isGenerator(functor) && !Array.isArray(functor)) throw lang.syntaxfehler(lnum, "not a mappable type: "+functor+((typeof functor == "object") ? Object.entries(functor) : "")) + // generator? + if (isGenerator(functor)) functor = genToArray(functor) + + return functor.map(it => bS.getDefunThunk(fn)(lnum, stmtnum, [it])) + }) +}}, +/* Synopsis: FOLD function, init_value, functor + * a function must accept two arguments, of which first argument will be an accumulator + */ +"FOLD" : {argc:3, f:function(lnum, stmtnum, args) { + return threeArg(lnum, stmtnum, args, (fn, init, functor) => { + if (!isRunnable(fn)) throw lang.badFunctionCallFormat(lnum, "first argument is not a function: got "+JStoBASICtype(fn)) + if (!isGenerator(functor) && !Array.isArray(functor)) throw lang.syntaxfehler(lnum, `not a mappable type '${Object.entries(args[2])}': `+functor+((typeof functor == "object") ? Object.entries(functor) : "")) + // generator? + if (isGenerator(functor)) functor = genToArray(functor) + + let akku = init + functor.forEach(it => { + akku = bS.getDefunThunk(fn)(lnum, stmtnum, [akku, it]) + }) + return akku + }) +}}, +/* Syopsis: FILTER function, functor + */ +"FILTER" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (fn, functor) => { + if (!isRunnable(fn)) throw lang.badFunctionCallFormat(lnum, "first argument is not a function: got "+JStoBASICtype(fn)) + if (!isGenerator(functor) && !Array.isArray(functor)) throw lang.syntaxfehler(lnum, `not a mappable type '${Object.entries(args[1])}': `+functor+((typeof functor == "object") ? Object.entries(functor) : (typeof functor))) + // generator? + if (isGenerator(functor)) functor = genToArray(functor) + + return functor.filter(it => bS.getDefunThunk(fn)(lnum, stmtnum, [it])) + }) +}}, +/* GOTO and GOSUB won't work but that's probably the best...? */ +"DO" : {f:function(lnum, stmtnum, args) { + return args[args.length - 1] +}}, +"LABEL" : {f:function(lnum, stmtnum, args) { + if (prescan) { + let labelname = args[0].troValue + if (labelname === undefined) throw lang.syntaxfehler(lnum, "empty LABEL") + gotoLabels[labelname] = lnum + } +}}, +"ON" : {f:function(lnum, stmtnum, args) { + //args: functionName (string), testvalue (SyntaxTreeReturnObj), arg0 (SyntaxTreeReturnObj), arg1 (SyntaxTreeReturnObj), ... + if (args[2] === undefined) throw lang.syntaxfehler(lnum) + + let jmpFun = args.shift() + let testvalue = resolve(args.shift())-INDEX_BASE + + // args must be resolved lazily because jump label is not resolvable + let jmpTarget = args[testvalue] + + if (jmpFun !== "GOTO" && jmpFun !== "GOSUB") + throw lang.badFunctionCallFormat(lnum, `Not a jump statement: ${jmpFun}`) + + if (jmpTarget === undefined) + return undefined + + return bS.builtin[jmpFun].f(lnum, stmtnum, [jmpTarget]) +}}, +"MIN" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (lh,rh) => (lh > rh) ? rh : lh) +}}, +"MAX" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (lh,rh) => (lh < rh) ? rh : lh) +}}, +"GETKEYSDOWN" : {argc:0, f:function(lnum, stmtnum, args) { + let keys = [] + sys.poke(-40, 255) + for (let k = -41; k >= -48; k--) { + keys.push(sys.peek(k)) + } + return keys +}}, +"~<" : {argc:2, f:function(lnum, stmtnum, args) { // CURRY operator + return twoArg(lnum, stmtnum, args, (fn, value) => { + if (!isAST(fn)) throw lang.badFunctionCallFormat(lnum, "left-hand is not a function: got "+JStoBASICtype(fn)) + + if (DBGON) { + serial.println("[BASIC.BUILTIN.CURRY] currying this function tree...") + serial.println(astToString(fn)) + serial.println("[BASIC.BUILTIN.CURRY] with this value: "+value) + serial.println(Object.entries(value)) + } + + let curriedTree = curryDefun(fn, value) + + if (DBGON) { + serial.println("[BASIC.BUILTIN.CURRY] Here's your curried tree:") + serial.println(astToString(curriedTree)) + } + + return curriedTree + }) +}}, +"TYPEOF" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNul(lnum, stmtnum, args, bv => { + if (bv === undefined) return "undefined" + if (bv.bvType === undefined || !(bv instanceof BasicVar)) { + let typestr = JStoBASICtype(bv) + if (typestr == "monad") + return bv.mType+"-"+typestr + else return typestr + } + return bv.bvType + }) +}}, +"LEN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, lh => { + if (lh.length === undefined) throw lang.illegalType() + return lh.length + }) +}}, +"HEAD" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, lh => { + if (lh.length === undefined || lh.length < 1) throw lang.illegalType() + return lh[0] + }) +}}, +"TAIL" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, lh => { + if (lh.length === undefined || lh.length < 1) throw lang.illegalType() + return lh.slice(1, lh.length) + }) +}}, +"INIT" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, lh => { + if (lh.length === undefined || lh.length < 1) throw lang.illegalType() + return lh.slice(0, lh.length - 1) + }) +}}, +"LAST" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, lh => { + if (lh.length === undefined || lh.length < 1) throw lang.illegalType() + return lh[lh.length - 1] + }) +}}, +"CLS" : {argc:0, f:function(lnum, stmtnum, args) { + con.clear() +}}, +"CLPX" : {argc:0, f:function(lnum, stmtnum, args) { + graphics.clearPixels(255) +}}, +"$" : {argc:2, f:function(lnum, stmtnum, args) { + let fn = resolve(args[0]) + let value = resolve(args[1]) // FIXME undefined must be allowed as we cannot distinguish between tree-with-value-of-undefined and just undefined + + if (DBGON) { + serial.println("[BASIC.BUILTIN.APPLY] applying this function tree... "+fn) + serial.println(astToString(fn)) + serial.println("[BASIC.BUILTIN.APPLY] with this value: "+value) + if (value !== undefined) + serial.println(Object.entries(value)) + } + + if (fn.mType == "funseq") { + return getMonadEvalFun(fn)(lnum, stmtnum, [value]) + } + else { + let valueTree = new BasicAST() + valueTree.astLnum = lnum + valueTree.astType = JStoBASICtype(value) + valueTree.astValue = value + + + let newTree = new BasicAST() + newTree.astLnum = lnum + newTree.astValue = fn + newTree.astType = "usrdefun" + newTree.astLeaves = [valueTree] + + if (DBGON) { + serial.println("[BASIC.BUILTIN.APPLY] Here's your applied tree:") + serial.println(astToString(newTree)) + } + + return bF._executeSyntaxTree(lnum, stmtnum, newTree, 0) + } +}}, +"&" : {argc:2, f:function(lnum, stmtnum, args) { + return bS.builtin["$"].f(lnum, stmtnum, [args[1], args[0]].concat(args.slice(2))) +}}, +"REDUCE" : {noprod:1, argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, bv => { + if (isAST(bv)) { + if (DBGON) { + serial.println("[BASIC.BUILTIN.REDUCE] reducing:") + serial.println(astToString(bv)) + /*if (tree.astType == "usrdefun") { + serial.println("[BASIC.BUILTIN.REDUCE] usrdefun unpack:") + serial.println(astToString(tree.astValue)) + }*/ + } + + let reduced = bF._uncapAST(bv, it => { + // TODO beta-eta reduction + return it + }) + + if (DBGON) { + serial.println("[BASIC.BUILTIN.REDUCE] reduced: "+reduced) + serial.println(astToString(reduced)) + } + + // re-wrap because tree-executor wants encapsulated function + let newTree = new BasicAST() + newTree.astLnum = lnum + newTree.astType = JStoBASICtype(reduced) + newTree.astValue = reduced + + return newTree + } + else { + return bv + } + }) +}}, +/** type: m a -> (a -> m b) -> m b + * @param m a monad + * @param fnb a function that takes a monadic value from m and returns a new monad. IT'S ENTIRELY YOUR RESPONSIBILITY TO MAKE SURE THIS FUNCTION TO RETURN RIGHT KIND OF MONAD! + * @return another monad + */ +">>=" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (ma, a_to_mb) => { + if (!isMonad(ma)) throw lang.badFunctionCallFormat(lnum, "left-hand is not a monad: got "+JStoBASICtype(ma)) + if (!isRunnable(a_to_mb)) throw lang.badFunctionCallFormat(lnum, "right-hand is not a usrdefun: got "+JStoBASICtype(a_to_mb)) + + if (DBGON) { + serial.println("[BASIC.BIND] binder:") + serial.println(monadToString(ma)) + serial.println("[BASIC.BIND] bindee:") + serial.println(astToString(a_to_mb)) + } + + let a = ma.mVal + let mb = bS.getDefunThunk(a_to_mb)(lnum, stmtnum, [a]) + + if (!isMonad(mb)) throw lang.badFunctionCallFormat(lnum, "right-hand function did not return a monad") + + if (DBGON) { + serial.println("[BASIC.BIND] bound monad:") + serial.println(monadToString(mb)) + } + + return mb + }) +}}, +/** type: m a -> m b -> m b + * @param m a monad + * @param fnb a function that returns a new monad. IT'S ENTIRELY YOUR RESPONSIBILITY TO MAKE SURE THIS FUNCTION TO RETURN RIGHT KIND OF MONAD! + * @return another monad + */ +">>~" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (ma, mb) => { + if (!isMonad(ma)) throw lang.badFunctionCallFormat(lnum, "left-hand is not a monad: got "+JStoBASICtype(ma)) + if (!isMonad(mb)) throw lang.badFunctionCallFormat(lnum, "right-hand is not a monad: got "+JStoBASICtype(mb)) + + if (DBGON) { + serial.println("[BASIC.BIND] binder:") + serial.println(monadToString(ma)) + serial.println("[BASIC.BIND] bindee:") + serial.println(monadToString(mb)) + } + + let a = ma.mVal + let b = mb.mVal + + return mb + }) +}}, +/** type: (b -> c) -> (a -> b) -> (a -> c) + * @param fa a function or a funseq-monad + * @param fb a function or a funseq-monad + * @return another monad + */ +"." : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (fa, fb) => { + if (!isRunnable(fa)) throw lang.badFunctionCallFormat(lnum, "left-hand is not a function/funseq: got"+JStoBASICtype(fa)) + if (!isRunnable(fb)) throw lang.badFunctionCallFormat(lnum, "left-hand is not a function/funseq: got"+JStoBASICtype(fb)) + + let ma = (isAST(fa)) ? [fa] : fa.mVal + let mb = (isAST(fb)) ? [fb] : fb.mVal + + let mc = mb.concat(ma) + return new BasicFunSeq(mc) + }) +}}, +"MLIST" : {noprod:1, argc:1, f:function(lnum, stmtnum, args) { + return oneArgNul(lnum, stmtnum, args, fn => { + return new BasicListMonad([fn]) + }) +}}, +"MRET" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArgNul(lnum, stmtnum, args, fn => { + return new BasicMemoMonad(fn) + }) +}}, +"MJOIN" : {argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, m => { + if (!isMonad(m)) throw lang.illegalType(lnum, m) + return m.mVal + }) +}}, +/*"MEVAL" : {argc:1, f:function(lnum, stmtnum, args) { + return varArg(lnum, stmtnum, args, rgs => { + let m = rgs[0] + let args = rgs.slice(1, rgs.length) + return getMonadEvalFun(m)(lnum, stmtnum, args) + }) +}},*/ +"GOTOYX" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (y, x) => { + con.move(y + (1-INDEX_BASE),x + (1-INDEX_BASE)) + }) +}}, +"TEXTFORE" : {argc:2, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, col => { + print(String.fromCharCode(27,91)+"38;5;"+(col|0)+"m") + }) +}}, +"TEXTBACK" : {argc:2, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, col => { + print(String.fromCharCode(27,91)+"48;5;"+(col|0)+"m") + }) +}}, +/** type: (list of function) <*> (a functor) + * Sequnetial application + * + * Can be implemented on pure TerranBASIC using: + * APL = [FS, XS, ARR] ~> IF (LEN(FS) == 0) THEN ARR ELSE APL(TAIL(FS), XS, (IF (ARR == UNDEFINED) THEN {} ELSE ARR) # MAP(HEAD(FS), XS)) + */ +"<*>" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (fns, functor) => { + if (!Array.isArray(fns) || !isRunnable(fns[0])) throw lang.badFunctionCallFormat(lnum, "first argument is not a function: got "+JStoBASICtype(fns)) + if (!isGenerator(functor) && !Array.isArray(functor)) throw lang.syntaxfehler(lnum, "not a mappable type: "+functor+((typeof functor == "object") ? Object.entries(functor) : "")) + // generator? + if (isGenerator(functor)) functor = genToArray(functor) + + let ret = [] + fns.forEach(fn => ret = ret.concat(functor.map(it => bS.getDefunThunk(fn)(lnum, stmtnum, [it])))) + return ret + }) +}}, +/** type: (a function) <*> (a functor) + * Infix MAP + */ +"<$>" : {argc:2, f:function(lnum, stmtnum, args) { + return bS.builtin.MAP.f(lnum, stmtnum, args) +}}, +/** type: (a function/list of functions) <~> (a functor) + * SEQUENTIAL CURRY-MAP + * + * returns a list of functions curried with each element of the functor + */ +"<~>" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (fns, functor) => { + if (!isRunnable(fns) && !(Array.isArray(fns) && isRunnable(fns[0]))) throw lang.badFunctionCallFormat(lnum, "first argument is not a function: got "+JStoBASICtype(fns)) + if (!isGenerator(functor) && !Array.isArray(functor)) throw lang.syntaxfehler(lnum, "not a mappable type: "+functor+((typeof functor == "object") ? Object.entries(functor) : "")) + // generator? + if (isGenerator(functor)) functor = genToArray(functor) + // single function? + if (!Array.isArray(fns)) fns = [fns] + + let ret = [] + fns.forEach(fn => ret = ret.concat(functor.map(it => bS.builtin["~<"].f(lnum, stmtnum, [fn, it])))) + return ret + }) +}}, +/** Writes to the serial device and blocks until a status code is returned + * @param device number (int), message (str) + * @return status code from the device. + */ +"CPUT" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArg(lnum, stmtnum, args, (devnum, msg) => { + if (!isNumable(devnum)) throw lang.illegalType(lnum, "LH:"+Object.entries(devnum)) + com.sendMessage(devnum, msg) + return com.getStatusCode(devnum) + }) +}}, +/** Reads the entire message (may be larger than 4096 bytes) from the serial device to the scratchpad memory + * @param device number (int), destination pointer (int) + * @return length of the message being read; specified memory will be filled with the actual message + */ +"CGET" : {argc:2, f:function(lnum, stmtnum, args) { + return twoArgNum(lnum, stmtnum, args, (devnum, ptr) => { + let msg = com.pullMessage(devnum) + let len = msg.length|0 + for (let i = 0; i < len; i++) { + sys.poke(ptr + i, msg.charCodeAt(i)) + } + return len + }) +}}, +/** Gets the status code of the serial device + * @param device number (int) + * @return status code 0..255 + */ +"CSTA" : {argc:2, f:function(lnum, stmtnum, args) { + return oneArgNum(lnum, stmtnum, args, (devnum) => { + return com.getStatusCode(devnum) + }) +}}, +"OPTIONDEBUG" : {f:function(lnum, stmtnum, args) { + oneArgNum(lnum, stmtnum, args, (lh) => { + if (lh != 0 && lh != 1) throw lang.syntaxfehler(line) + DBGON = (1 == lh|0) + }) +}}, +"OPTIONTRACE" : {f:function(lnum, stmtnum, args) { + oneArgNum(lnum, stmtnum, args, (lh) => { + if (lh != 0 && lh != 1) throw lang.syntaxfehler(line) + TRACEON = (1 == lh|0) + }) +}}, +"PRINTMONAD" : {debugonly:1, argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, (it) => { + println(monadToString(it)) + }) +}}, +"RESOLVE" : {debugonly:1, argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, (it) => { + if (isAST(it)) { + println(lnum+" RESOLVE PRINTTREE") + println(astToString(it)) + if (typeof it.astValue == "object") { + if (isAST(it.astValue)) { + println(lnum+" RESOLVE PRINTTREE ASTVALUE PRINTTREE") + println(astToString(it.astValue)) + } + else { + println(lnum+" RESOLVE PRINTTREE ASTVALUE") + println(it.astValue) + } + } + } + else + println(it) + }) +}}, +"RESOLVEVAR" : {debugonly:1, argc:1, f:function(lnum, stmtnum, args) { + return oneArg(lnum, stmtnum, args, (it) => { + let v = bS.vars[args[0].troValue] + if (v === undefined) println("Undefined variable: "+args[0].troValue) + else println(`type: ${v.bvType}, value: ${v.bvLiteral}`) + }) +}}, +"UNRESOLVE" : {debugonly:1, argc:1, f:function(lnum, stmtnum, args) { + println(args[0]) +}}, +"UNRESOLVE0" : {debugonly:1, argc:1, f:function(lnum, stmtnum, args) { + println(Object.entries(args[0])) +}}, +"TOJSON" : {debugonly:1, argc:1, f:function(lnum, stmtnum, args) { + println(JSON.stringify(resolve(args[0]))) +}}, +} +Object.freeze(bS.builtin) +let bF = {} // BASIC functions +bF._1os = {"!":1,"~":1,"#":1,"<":1,"=":1,">":1,"*":1,"+":1,"-":1,"/":1,"^":1,":":1,"$":1,".":1,"@":1,"\\":1,"%":1,"|":1,"`":1} +//bF._2os = {"<":1,"=":1,">":1,"~":1,"-":1,"$":1,"*":1,"!":1,"#":1} +//bF._3os = {"<":1,"=":1,">":1,"~":1,"-":1,"$":1,"*":1} +bF._uos = {"+":1,"-":1,"NOT":1,"BNOT":1,"^":1,"@":1,"`":1} +bF._isNum = function(code) { + return (code >= 0x30 && code <= 0x39) || code == 0x5F +} +bF._isNum2 = function(code) { + return (code >= 0x30 && code <= 0x39) || code == 0x5F || (code >= 0x41 && code <= 0x46) || (code >= 0x61 && code <= 0x66) +} +bF._isNumSep = function(code) { + return code == 0x2E || code == 0x42 || code == 0x58 || code == 0x62 || code == 0x78 +} +bF._is1o = function(code) { + return bF._1os[String.fromCharCode(code)] +} +/*bF._is2o = function(code) { + return bF._2os[String.fromCharCode(code)] +} +bF._is3o = function(code) { + return bF._3os[String.fromCharCode(code)] +}*/ +bF._isUnary = function(code) { + return bF._uos[String.fromCharCode(code)] +} +bF._isParenOpen = function(code) { + return (code == 0x28 || code == 0x5B || code == 0x7B) || (code == '(' || code == '[' || code == '{') +} +bF._isParenClose = function(code) { + return (code == 0x29 || code == 0x5D || code == 0x7D) || (code == ')' || code == ']' || code == '}') +} +bF._isMatchingParen = function(open, close) { + return (open == '(' && close == ')' || open == '[' && close == ']' || open == '{' && close == '}') +} +bF._isParen = function(code) { + return bF._isParenOpen(code) || bF._isParenClose(code) +} +bF._isSep = function(code) { + return code == 0x2C || code == 0x3B +} +// define operator precedence here... +// NOTE: do NOT put falsy value (e.g. 0) here!! +bF._opPrc = { + // function call in itself has highest precedence + "`":10, // MJOIN + "^":20, + "*":30,"/":30,"\\":20, + "MOD":40, + "+":50,"-":50, + "NOT":60,"BNOT":60, + "<<":70,">>":70, + "<":80,">":80,"<=":80,"=<":80,">=":80,"=>":80, + "==":90,"<>":90,"><":90, + "MIN":100,"MAX":100, + "BAND":200, + "BXOR":201, + "BOR":202, + "AND":300, + "OR":301, + "TO":400,"STEP":401, + "!":500, + "~":501, // array CONS and PUSH + "#":502, // array concat + ".": 600, // compo operator + "$": 600, // apply operator + "&": 600, // pipe operator + "~<": 601, // curry operator + "<$>": 602, // infix map operator + "<*>": 602, // sequential application operator + "<~>": 602, // infix curry-map operator + "@":700, // MRET + "~>": 1000, // closure operator + ">>~": 1000, // monad sequnce operator + ">>=": 1000, // monad bind operator + "=":9999,"IN":9999 +} // when to ops have same index of prc but different in associativity, right associative op gets higher priority (at least for the current parser implementation) +bF._opRh = {"^":1,"=":1,"!":1,"IN":1,"~>":1,"$":1,".":1,">>=":1,">>~":1,">!>":1,"@":1,"`":1,"<$>":1} // ~< and ~> cannot have same associativity +// these names appear on executeSyntaxTree as "exceptional terms" on parsing (regular function calls are not "exceptional terms") +bF._tokenise = function(lnum, cmd) { + var _debugprintStateTransition = false + var k + var tokens = [] + var states = [] + var sb = "" + var mode = "lit" // lit, qot, paren, sep, op, num; operator2, numbersep, number2, limbo, escape, quote_end + + // NOTE: malformed numbers (e.g. "_b3", "_", "__") must be re-marked as literal or syntax error in the second pass + + if (_debugprintStateTransition) println("@@ TOKENISE @@") + if (_debugprintStateTransition) println("Ln "+lnum+" cmd "+cmd) + + // TOKENISE + for (k = 0; k < cmd.length; k++) { + var char = cmd[k] + var charCode = cmd.charCodeAt(k) + + if (_debugprintStateTransition) print("Char: "+char+"("+charCode+"), state: "+mode) + + if ("lit" == mode) { + if (0x22 == charCode) { // " + tokens.push(sb); sb = ""; states.push(mode) + mode = "qot" + } + else if (bF._isParen(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "paren" + } + else if (" " == char) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "limbo" + } + else if (bF._isSep(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "sep" + } + /*else if (bF._isNum(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "num" + }*/ + else if (bF._is1o(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "op" + } + else { + sb += char + } + } + else if ("num" == mode) { + if (bF._isNum(charCode)) { + sb += char + } + else if (bF._isNumSep(charCode)) { + sb += char + mode = "nsep" + } + else if (0x22 == charCode) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "qot" + } + else if (" " == char) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "limbo" + } + else if (bF._isParen(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "paren" + } + else if (bF._isSep(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "sep" + } + else if (bF._is1o(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "op" + } + else { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "lit" + } + } + else if ("nsep" == mode) { + if (bF._isNum2(charCode)) { + sb += char + mode = "n2" + } + else { + throw lang.syntaxfehler(lnum, lang.badNumberFormat) + } + } + else if ("n2" == mode) { + if (bF._isNum2(charCode)) { + sb += char + } + else if (0x22 == charCode) { + tokens.push(sb); sb = ""; states.push("num") + mode = "qot" + } + else if (" " == char) { + tokens.push(sb); sb = ""; states.push("num") + mode = "limbo" + } + else if (bF._isParen(charCode)) { + tokens.push(sb); sb = "" + char; states.push("num") + mode = "paren" + } + else if (bF._isSep(charCode)) { + tokens.push(sb); sb = "" + char; states.push("num") + mode = "sep" + } + else if (bF._is1o(charCode)) { + tokens.push(sb); sb = "" + char; states.push("num") + mode = "op" + } + else { + tokens.push(sb); sb = "" + char; states.push("num") + mode = "lit" + } + } + else if ("op" == mode) { + if (bF._is1o(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "op" + } + else if (bF._isUnary(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + } + else if (bF._isNum(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "num" + } + else if (0x22 == charCode) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "qot" + } + else if (" " == char) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "limbo" + } + else if (bF._isParen(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "paren" + } + else if (bF._isSep(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "sep" + } + else { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "lit" + } + } + else if ("qot" == mode) { + if (0x22 == charCode) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "quote_end" + } + /*else if (charCode == 0x5C) { // reverse solidus + tokens.push(sb); sb = "" + mode = "escape" + }*/ + else { + sb += char + } + } + /*else if ("escape" == mode) { + if (0x5C == charCode) // reverse solidus + sb += String.fromCharCode(0x5C) + else if ("n" == char) + sb += String.fromCharCode(0x0A) + else if ("t" == char) + sb += String.fromCharCode(0x09) + else if (0x22 == charCode) // " + sb += String.fromCharCode(0x22) + else if (0x27 == charCode) + sb += String.fromCharCode(0x27) + else if ("e" == char) + sb += String.fromCharCode(0x1B) + else if ("a" == char) + sb += String.fromCharCode(0x07) + else if ("b" == char) + sb += String.fromCharCode(0x08) + mode = "qot" // ESCAPE is only legal when used inside of quote + }*/ + else if ("quote_end" == mode) { + if (" " == char) { + sb = "" + mode = "limbo" + } + else if (0x22 == charCode) { + sb = "" + char + mode = "qot" + } + else if (bF._isParen(charCode)) { + sb = "" + char + mode = "paren" + } + else if (bF._isSep(charCode)) { + sb = "" + char + mode = "sep" + } + else if (bF._isNum(charCode)) { + sb = "" + char + mode = "num" + } + else if (bF._is1o(charCode)) { + sb = "" + char + mode = "op" + } + else { + sb = "" + char + mode = "lit" + } + } + else if ("limbo" == mode) { + if (char == " ") { + /* do nothing */ + } + else if (0x22 == charCode) { + mode = "qot" + } + else if (bF._isParen(charCode)) { + sb = "" + char + mode = "paren" + } + else if (bF._isSep(charCode)) { + sb = "" + char + mode = "sep" + } + else if (bF._isNum(charCode)) { + sb = "" + char + mode = "num" + } + else if (bF._is1o(charCode)) { + sb = "" + char + mode = "op" + } + else { + sb = "" + char + mode = "lit" + } + } + else if ("paren" == mode) { + if (char == " ") { + tokens.push(sb); sb = ""; states.push(mode) + mode = "limbo" + } + else if (0x22 == charCode) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "qot" + } + else if (bF._isParen(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "paren" + } + else if (bF._isSep(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "sep" + } + else if (bF._isNum(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "num" + } + else if (bF._is1o(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "op" + } + else { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "lit" + } + } + else if ("sep" == mode) { + if (char == " ") { + tokens.push(sb); sb = ""; states.push(mode) + mode = "limbo" + } + else if (0x22 == charCode) { + tokens.push(sb); sb = ""; states.push(mode) + mode = "qot" + } + else if (bF._isParen(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "paren" + } + else if (bF._isSep(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "sep" + } + else if (bF._isNum(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "num" + } + else if (bF._is1o(charCode)) { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "op" + } + else { + tokens.push(sb); sb = "" + char; states.push(mode) + mode = "lit" + } + } + else { + throw Error("Unknown parser state: " + mode) + } + + if (_debugprintStateTransition) println("->"+mode) + } + + if (sb.length > 0) { + tokens.push(sb); states.push(mode) + } + + // filter off initial empty token if the statement does NOT start with literal (e.g. "-3+5") + if (tokens[0].length == 0) { + tokens = tokens.slice(1, tokens.length) + states = states.slice(1, states.length) + } + // clean up operator2 and number2 + for (k = 0; k < states.length; k++) { + if (states[k] == "o2" || states[k] == "o3") states[k] = "op" + else if (states[k] == "n2" || states[k] == "nsep") states[k] = "num" + } + + if (tokens.length != states.length) { + throw new BASICerror("size of tokens and states does not match (line: "+lnum+")\n"+ + tokens+"\n"+states) + } + + return { "tokens": tokens, "states": states } +} +bF._parserElaboration = function(lnum, ltokens, lstates) { + let _debugprintElaboration = (!PROD) && true + if (_debugprintElaboration) serial.println("@@ ELABORATION @@") + + let tokens = cloneObject(ltokens) + let states = cloneObject(lstates) + + let k = 0 + + // NOTE: malformed numbers (e.g. "_b3", "_", "__") must be re-marked as literal or syntax error + + while (k < states.length) { // using while loop because array size will change during the execution + // turn errenously checked as number back into a literal + if (states[k] == "num" && !reNumber.test(tokens[k])) + states[k] = "lit" + // turn back into an op if operator is errenously checked as a literal + else if (states[k] == "lit" && bF._opPrc[tokens[k].toUpperCase()] !== undefined) + states[k] = "op" + // turn TRUE and FALSE into boolean + else if ((tokens[k].toUpperCase() == "TRUE" || tokens[k].toUpperCase() == "FALSE") && states[k] == "paren") + states[k] = "bool" + + // decimalise hex/bin numbers (because Nashorn does not support binary literal) + if (states[k] == "num") { + if (tokens[k].toUpperCase().startsWith("0B")) { + tokens[k] = parseInt(tokens[k].substring(2, tokens[k].length), 2) + "" + } + } + + k += 1 + } + + k = 0; let l = states.length + while (k < l) { + let lookahead012 = tokens[k]+tokens[k+1]+tokens[k+2] + let lookahead01 = tokens[k]+tokens[k+1] + + // turn three consecutive ops into a trigraph + if (k < states.length - 3 && states[k] == "op" && states[k+1] == "op" && states[k+2] == "op" && bF._opPrc[lookahead012]) { + if (_debugprintElaboration) serial.println(`[ParserElaboration] Line ${lnum}: Trigraph (${lookahead012}) found starting from the ${lang.ord(k+1)} token of [${tokens}]`) + + tokens[k] = lookahead012 + + // remove two future elements by splicing them + let oldtkn = cloneObject(tokens) + let oldsts = cloneObject(states) + tokens = oldtkn.slice(0, k+1).concat(oldtkn.slice(k+3, oldtkn.length)) + states = oldsts.slice(0, k+1).concat(oldsts.slice(k+3, oldsts.length)) + l -= 2 + } + // turn two consecutive ops into a digraph + else if (k < states.length - 2 && states[k] == "op" && states[k+1] == "op" && bF._opPrc[lookahead01]) { + if (_debugprintElaboration) serial.println(`[ParserElaboration] Line ${lnum}: Digraph (${lookahead01}) found starting from the ${lang.ord(k+1)} token of [${tokens}]`) + + tokens[k] = lookahead01 + + // remove two future elements by splicing them + let oldtkn = cloneObject(tokens) + let oldsts = cloneObject(states) + tokens = oldtkn.slice(0, k+1).concat(oldtkn.slice(k+2, oldtkn.length)) + states = oldsts.slice(0, k+1).concat(oldsts.slice(k+2, oldsts.length)) + l -= 1 + } + // turn (:) into a seq + else if (tokens[k] == ":" && states[k] == "op") + states[k] = "seq" + + k += 1 + } + + return {"tokens":tokens, "states":states} +} +/** + * Destructively transforms an AST (won't unpack capsulated trees by default) + * + * To NOT modify the tree, make sure you're not modifying any properties of the object */ +bF._recurseApplyAST = function(tree, action) { + if (!isAST(tree)) throw new BASICerror(`tree is not a AST (${tree})`) + + if (tree.astLeaves !== undefined && tree.astLeaves[0] === undefined) { + /*if (DBGON) { + serial.println(`RECURSE astleaf`) + serial.println(astToString(tree)) + }*/ + + return action(tree) || tree + } + else { + let newLeaves = tree.astLeaves.map(it => bF._recurseApplyAST(it, action)) + + /*if (DBGON) { + serial.println(`RECURSE ast`) + serial.println(astToString(tree)) + }*/ + + let newTree = action(tree) + + if (newTree !== undefined) { + tree.astLnum = newTree.astLnum + tree.astValue = newTree.astValue + tree.astSeps = newTree.astSeps + tree.astType = newTree.astType + // weave astLeaves + for (let k = 0; k < tree.astLeaves.length; k++) { + if (newLeaves[k] !== undefined) tree.astLeaves[k] = newLeaves[k] + } + } + } +} +/** + * Returns a copy of BasicAST where its 'capsulated' trees are fully uncapsulated. + */ +bF._uncapAST = function(tree, action) { + let expr = cloneObject(tree) + bF._recurseApplyAST(expr, it => { + if (isAST(it.astValue)) { + let capTree = bF._uncapAST(it.astValue, action) + it.astLnum = capTree.astLnum + it.astValue = capTree.astValue + it.astSeps = capTree.astSeps + it.astType = capTree.astType + it.astLeaves = capTree.astLeaves + } + + return action(it) + }) + action(expr) + return expr +} +/** EBNF notation: +(* quick reference to EBNF *) +(* { word } = word is repeated 0 or more times *) +(* [ word ] = word is optional (repeated 0 or 1 times) *) + +line = + linenumber , stmt , {":" , stmt} + | linenumber , "REM" , ? basically anything ? +linenumber = digits + +stmt = + "REM" , ? anything ? + | "IF" , expr_sans_asgn , "THEN" , stmt , ["ELSE" , stmt] + | "DEFUN" , [ident] , "(" , [ident , {" , " , ident}] , ")" , "=" , expr + | "ON" , expr_sans_asgn , ("GOTO" | "GOSUB") , expr_sans_asgn , {"," , expr_sans_asgn} + | "(" , stmt , ")" + | expr ; (* if the statement is 'lit' and contains only one word, treat it as function_call + e.g. NEXT for FOR loop *) + +expr = (* this basically blocks some funny attemps such as using DEFUN as anon function + because everything is global in BASIC *) + ? empty string ? + | lit + | "{" , [expr , {"," , expr}] , "}" + | "(" , expr , ")" + | ident_tuple + | "IF" , expr_sans_asgn , "THEN" , expr , ["ELSE" , expr] + | ("FOR"|"FOREACH") , expr + | expr , op , expr + | op_uni , expr + | kywd , expr - "(" + | function_call + +expr_sans_asgn = ? identical to expr except errors out whenever "=" is found ? + +ident_tuple = "[" , ident , {"," , ident} , "]" + +function_call = + ident , "(" , [expr , {argsep , expr} , [argsep]] , ")" + | ident , expr , {argsep , expr} , [argsep] +kywd = ? words that exists on the list of predefined function that are not operators ? + +(* don't bother looking at these, because you already know the stuff *) + +argsep = "," | ";" +ident = alph , [digits] ; (* variable and function names *) +lit = alph , [digits] | num | string ; (* ident + numbers and string literals *) +op = "^" | "*" | "/" | "\" | "MOD" | "+" | "-" | "<<" | ">>" | "<" | ">" + | "<=" | "=<" | ">=" | "=>" | "==" | "<>" | "><" | "MIN" | "MAX" | "BAND" | "BXOR" | "BOR" + | "AND" | "OR" | "TO" | "STEP" | "!" | "~" | "#" | "." | "$" | "&" | "~<" | "<$>" | "<*>" + | "<~>" | "~>" | ">>~" | ">>=" | "=" +op_uni = "-" | "+" | "NOT" | "BNOT" | "`" | "@" + +alph = letter | letter , alph +digits = digit | digit , digits +hexdigits = hexdigit | hexdigit , hexdigits +bindigits = bindigit | bindigit , bindigits +num = digits | digits , "." , [digits] | "." , digits + | ("0x"|"0X") , hexdigits + | ("0b"|"0B") , bindigits ; (* sorry, no e-notation! *) +visible = ? ASCII 0x20 to 0x7E ? +string = '"' , {visible} , '"' + +letter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" + | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "a" | "b" + | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" + | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "_" +digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" +hexdigit = "A" | "B" | "C" | "D" | "E" | "F" | "a" | "b" | "c" | "d" | "e" | "f" | "0" | "1" + | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" +bindigit = "0" | "1" + +(* all possible token states: lit num op bool qot paren sep *) +(* below are schematic of trees generated after parsing the statements *) + +IF (type: function, value: IF) +1. cond +2. true +[3. false] + +FOR (type: function, value: FOR) +1. expr (normally (=) but not necessarily) + +DEFUN (type: function, value: DEFUN) +1. funcname (type: lit) + 1. arg0 (type: lit) + [2. arg1] + [3. argN...] +2. stmt + +ON (type: function, value: ON) +1. testvalue +2. functionname (type: lit) +3. arg0 +[4. arg1] +[5. argN...] + +FUNCTION_CALL (type: function, value: PRINT or something) +1. arg0 +2. arg1 +[3. argN...] + +LAMBDA (type: op, value: "~>") +1. undefined (type: closure_args, value: undefined) + 1. arg0 (type: lit) + [2. arg1] + [3. argN...] +2. stmt + +ARRAY CONSTRUCTOR (type: function, value: undefined) +1. 0th element of the array +2. 1st element of the array +[3. Nth element of the array...] + */ +// @returns BasicAST +bF._EquationIllegalTokens = ["IF","THEN","ELSE","DEFUN","ON"] +bF.isSemanticLiteral = function(token, state) { + return undefined == token || "]" == token || ")" == token || "}" == token || + "qot" == state || "num" == state || "bool" == state || "lit" == state +} +bF.parserDoDebugPrint = (!PROD) && true +bF.parserPrintdbg = any => { if (bF.parserDoDebugPrint) serial.println(any) } +bF.parserPrintdbg2 = function(icon, lnum, tokens, states, recDepth) { + if (bF.parserDoDebugPrint) { + let treeHead = "| ".repeat(recDepth) + bF.parserPrintdbg(`${icon}${lnum} ${treeHead}${tokens.join(' ')}`) + bF.parserPrintdbg(`${icon}${lnum} ${treeHead}${states.join(' ')}`) + } +} +bF.parserPrintdbgline = function(icon, msg, lnum, recDepth) { + if (bF.parserDoDebugPrint) { + let treeHead = "| ".repeat(recDepth) + bF.parserPrintdbg(`${icon}${lnum} ${treeHead}${msg}`) + } +} + +// ## USAGE OF lambdaBoundVars IN PARSEMODE STARTS HERE ## + +/** + * @return ARRAY of BasicAST + */ +bF._parseTokens = function(lnum, tokens, states) { + if (tokens.length !== states.length) throw Error("unmatched tokens and states length") + + bF.parserPrintdbg2('Line ', lnum, tokens, states, 0) + + if (tokens.length !== states.length) throw lang.syntaxfehler(lnum) + if (tokens[0].toUpperCase() == "REM" && states[0] != "qot") return + + /*************************************************************************/ + + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + let seps = [] + + // scan for parens and (:)s + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (tokens[k] == "(" && states[k] == "paren") { + parenDepth += 1 + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // decrease paren depth + else if (tokens[k] == ")" && states[k] == "paren") { + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + parenDepth -= 1 + } + + if (parenDepth == 0 && tokens[k] == ":" && states[k] == "seq") + seps.push(k) + } + + let startPos = [0].concat(seps.map(k => k+1)) + let stmtPos = startPos.map((s,i) => {return{start:s, end:(seps[i] || tokens.length)}}) // use end of token position as separator position + + return stmtPos.map((x,i) => { + if (stmtPos.length > 1) + bF.parserPrintdbgline('Line ', 'Statement #'+(i+1), lnum, 0) + + // check for empty tokens + if (x.end - x.start <= 0) throw new ParserError("Malformed Line") + + let tree = bF._parseStmt(lnum, + tokens.slice(x.start, x.end), + states.slice(x.start, x.end), + 1 + ) + + bF.parserPrintdbgline('Tree in ', '\n'+astToString(tree), lnum, 0) + + return tree + }) +} +/** Parses following EBNF rule: +stmt = + "IF" , expr_sans_asgn , "THEN" , stmt , ["ELSE" , stmt] + | "DEFUN" , [ident] , "(" , [ident , {" , " , ident}] , ")" , "=" , expr + | "ON" , expr_sans_asgn , ident , expr_sans_asgn , {"," , expr_sans_asgn} + | "(" , stmt , ")" + | expr ; (* if the statement is 'lit' and contains only one word, treat it as function_call e.g. NEXT for FOR loop *) + * @return: BasicAST + */ +bF._parseStmt = function(lnum, tokens, states, recDepth) { + bF.parserPrintdbg2('$', lnum, tokens, states, recDepth) + + /*************************************************************************/ + + // case for: single word (e.g. NEXT for FOR loop) + if (tokens.length == 1 && states.length == 1) { + bF.parserPrintdbgline('$', "Single Word Function Call", lnum, recDepth) + return bF._parseLit(lnum, tokens, states, recDepth + 1, true) + } + + /*************************************************************************/ + + let headTkn = tokens[0].toUpperCase() + let headSta = states[0] + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + + /*************************************************************************/ + + // case for: "REM" , ? anything ? + if (headTkn == "REM" && headSta != "qot") return + + /*************************************************************************/ + + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + let onGoPos = -1 + let sepsZero = [] + let sepsOne = [] + + // scan for parens that will be used for several rules + // also find nearest THEN and ELSE but also take parens into account + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (tokens[k] == "(" && states[k] == "paren") { + parenDepth += 1 + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // decrease paren depth + else if (tokens[k] == ")" && states[k] == "paren") { + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + parenDepth -= 1 + } + + if (parenDepth == 0 && states[k] == "sep") + sepsZero.push(k) + if (parenDepth == 1 && states[k] == "sep") + sepsOne.push(k) + + if (parenDepth == 0) { + let tok = tokens[k].toUpperCase() + if (-1 == onGoPos && ("GOTO" == tok || "GOSUB" == tok) && "lit" == states[k]) + onGoPos = k + } + } + + // unmatched brackets, duh! + if (parenDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + + /*************************************************************************/ + + // ## case for: + // "IF" , expr_sans_asgn , "THEN" , stmt , ["ELSE" , stmt] + try { + bF.parserPrintdbgline('$', "Trying IF Statement...", lnum, recDepth) + return bF._parseIfMode(lnum, tokens, states, recDepth + 1, false) + } + // if ParserError is raised, continue applying other rules + catch (e) { + if (!(e instanceof ParserError)) throw e + bF.parserPrintdbgline('$', 'It was NOT!', lnum, recDepth) + } + + /*************************************************************************/ + + // ## case for: + // | "DEFUN" , [ident] , "(" , [ident , {" , " , ident}] , ")" , "=" , expr + if ("DEFUN" == headTkn && "lit" == headSta && + parenStart == 2 && tokens[parenEnd + 1] == "=" && states[parenEnd + 1] == "op" + ) { + bF.parserPrintdbgline('$', 'DEFUN Stmt', lnum, recDepth) + + treeHead.astValue = "DEFUN" + treeHead.astType = "function" + + // parse function name + if (tokens[1] == "(") { + // anonymous function + treeHead.astLeaves[0] = new BasicAST() + treeHead.astLeaves[0].astLnum = lnum + treeHead.astLeaves[0].astType = "lit" + } + else { + bF.parserPrintdbgline('$', 'DEFUN Stmt Function Name:', lnum, recDepth) + treeHead.astLeaves[0] = bF._parseIdent(lnum, [tokens[1]], [states[1]], recDepth + 1) + } + + // parse function arguments + bF.parserPrintdbgline('$', 'DEFUN Stmt Function Arguments -- ', lnum, recDepth) + let defunArgDeclSeps = sepsOne.filter((i) => i < parenEnd + 1).map(i => i-1).concat([parenEnd - 1]) + bF.parserPrintdbgline('$', 'DEFUN Stmt Function Arguments comma position: '+defunArgDeclSeps, lnum, recDepth) + + treeHead.astLeaves[0].astLeaves = defunArgDeclSeps.map(i=>bF._parseIdent(lnum, [tokens[i]], [states[i]], recDepth + 1)) + + // parse function body + let parseFunction = bF._parseExpr + treeHead.astLeaves[1] = parseFunction(lnum, + tokens.slice(parenEnd + 2, tokens.length), + states.slice(parenEnd + 2, states.length), + recDepth + 1 + ) + + return treeHead + } + + /*************************************************************************/ + + // ## case for: + // | "ON" , if_equation , ident , if_equation , {"," , if_equation} + if ("ON" == headTkn && "lit" == headSta) { + bF.parserPrintdbgline('$', 'ON Stmt', lnum, recDepth) + + if (onGoPos == -1) throw ParserError("Malformed ON Statement") + + treeHead.astValue = "ON" + treeHead.astType = "function" + + // parse testvalue + let testvalue = bF._parseExpr(lnum, + tokens.slice(1, onGoPos), + states.slice(1, onGoPos), + recDepth + 1, + true + ) + + // parse functionname + let functionname = bF._parseExpr(lnum, + [tokens[onGoPos]], + [states[onGoPos]], + recDepth + 1, + true + ) + + // parse arguments + // get list of comma but filter ones appear before GOTO/GOSUB + let onArgSeps = sepsZero.filter(i => (i > onGoPos)) + let onArgStartPos = [onGoPos + 1].concat(onArgSeps.map(k => k + 1)) + let onArgPos = onArgStartPos.map((s,i) => {return{start:s, end: (onArgSeps[i] || tokens.length)}}) // use end of token position as separator position + + // recursively parse expressions + treeHead.astLeaves = [testvalue, functionname].concat(onArgPos.map((x,i) => { + bF.parserPrintdbgline('$', 'ON GOTO/GOSUB Arguments #'+(i+1), lnum, recDepth) + + // check for empty tokens + if (x.end - x.start <= 0) throw new ParserError("Malformed ON arguments") + + return bF._parseExpr(lnum, + tokens.slice(x.start, x.end), + states.slice(x.start, x.end), + recDepth + 1, + true + ) + })) + + return treeHead + } + + /*************************************************************************/ + + // ## case for: + // | "(" , stmt , ")" + if (parenStart == 0 && parenEnd == tokens.length - 1) { + bF.parserPrintdbgline('$', '( Stmt )', lnum, recDepth) + return bF._parseStmt(lnum, + tokens.slice(parenStart + 1, parenEnd), + states.slice(parenStart + 1, parenEnd), + recDepth + 1 + ) + } + + /*************************************************************************/ + + // ## case for: + // | expr + try { + bF.parserPrintdbgline('$', 'Trying Expression Call...', lnum, recDepth) + return bF._parseExpr(lnum, tokens, states, recDepth + 1) + } + catch (e) { + bF.parserPrintdbgline('$', 'Error!', lnum, recDepth) + throw new ParserError("Statement cannot be parsed in "+lnum+": "+e.stack) + } + + /*************************************************************************/ + + throw new ParserError("Statement cannot be parsed in "+lnum) +} // END of STMT +/** Parses following EBNF rule: +expr = (* this basically blocks some funny attemps such as using DEFUN as anon function because everything is global in BASIC *) + ? empty string ? + | lit + | "{" , [expr , {"," , expr}] , "}" + | "(" , expr , ")" + | ident_tuple + | "IF" , expr_sans_asgn , "THEN" , expr , ["ELSE" , expr] + | ("FOR"|"FOREACH") , expr + | expr , op , expr + | op_uni , expr + | kywd , expr - "(" + | function_call + + * @return: BasicAST + */ +bF._parseExpr = function(lnum, tokens, states, recDepth, ifMode) { + bF.parserPrintdbg2('e', lnum, tokens, states, recDepth) + + /*************************************************************************/ + + // ## special case for virtual dummy element (e.g. phantom element on "PRINT SPC(20);") + if (tokens[0] === undefined && states[0] === undefined) { + let treeHead = new BasicAST() + treeHead.astLnum = lnum + treeHead.astValue = undefined + treeHead.astType = "null" + + return treeHead + } + + /*************************************************************************/ + + let headTkn = tokens[0].toUpperCase() + let headSta = states[0] + + /*************************************************************************/ + + // ## case for: + // lit + if (!bF._EquationIllegalTokens.includes(headTkn) && tokens.length == 1) { + bF.parserPrintdbgline('e', 'Literal Call', lnum, recDepth) + return bF._parseLit(lnum, tokens, states, recDepth + 1) + } + + /*************************************************************************/ + + // scan for operators with highest precedence, use rightmost one if multiple were found + let topmostOp + let topmostOpPrc = 0 + let operatorPos = -1 + + // find and mark position of parentheses + // properly deal with the nested function calls + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + let curlyDepth = 0 + let curlyStart = -1 + let curlyEnd = -1 + let uptkn = "" + + // Scan for unmatched parens and mark off the right operator we must deal with + // every function_call need to re-scan because it is recursively called + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (tokens[k] == "(" && states[k] == "paren") { + parenDepth += 1 + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // increase curly depth and mark curly start position + else if (tokens[k] == "{" && states[k] == "paren") { + curlyDepth += 1 + if (curlyStart == -1 && curlyDepth == 1) curlyStart = k + } + // decrease paren depth + else if (tokens[k] == ")" && states[k] == "paren") { + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + parenDepth -= 1 + } + // decrease curly depth + else if (tokens[k] == "}" && states[k] == "paren") { + if (curlyEnd == -1 && curlyDepth == 1) curlyEnd = k + curlyDepth -= 1 + } + + // determine the right operator to deal with + if (parenDepth == 0 && curlyDepth == 0) { + let uptkn = tokens[k].toUpperCase() + + if (states[k] == "op" && bF.isSemanticLiteral(tokens[k-1], states[k-1]) && + ((bF._opPrc[uptkn] > topmostOpPrc) || + (!bF._opRh[uptkn] && bF._opPrc[uptkn] == topmostOpPrc)) + ) { + topmostOp = uptkn + topmostOpPrc = bF._opPrc[uptkn] + operatorPos = k + } + } + } + + // unmatched brackets, duh! + if (parenDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + if (curlyDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + + /*************************************************************************/ + + // ## case for: + // | ident_tuple + try { + bF.parserPrintdbgline('e', "Trying Tuple...", lnum, recDepth) + return bF._parseTuple(lnum, tokens, states, recDepth + 1, false) + } + // if ParserError is raised, continue applying other rules + catch (e) { + if (!(e instanceof ParserError)) throw e + bF.parserPrintdbgline('e', 'It was NOT!', lnum, recDepth) + } + + /*************************************************************************/ + + // ## case for: + // | "{" , [expr , {"," , expr}] , "}" + if (curlyStart == 0 && curlyEnd == tokens.length - 1) { + bF.parserPrintdbgline('e', "Array", lnum, recDepth) + return bF._parseArrayLiteral(lnum, tokens, states, recDepth + 1) + } + + /*************************************************************************/ + + + // ## case for: + // | "(" , [expr] , ")" + if (parenStart == 0 && parenEnd == tokens.length - 1) { + bF.parserPrintdbgline('e', '( [Expr] )', lnum, recDepth) + + return bF._parseExpr(lnum, + tokens.slice(parenStart + 1, parenEnd), + states.slice(parenStart + 1, parenEnd), + recDepth + 1 + ) + } + + /*************************************************************************/ + + // ## case for: + // | "IF" , expr_sans_asgn , "THEN" , expr , ["ELSE" , expr] + try { + bF.parserPrintdbgline('e', "Trying IF Expression...", lnum, recDepth) + return bF._parseIfMode(lnum, tokens, states, recDepth + 1, false) + } + // if ParserError is raised, continue applying other rules + catch (e) { + if (!(e instanceof ParserError)) throw e + bF.parserPrintdbgline('e', 'It was NOT!', lnum, recDepth) + } + + /*************************************************************************/ + + // ## case for: + // | ("FOR"|"FOREACH") , expr + try { + bF.parserPrintdbgline('e', "Trying FOR Expression...", lnum, recDepth) + return bF._parseForLoop(lnum, tokens, states, recDepth + 1) + } + // if ParserError is raised, continue applying other rules + catch (e) { + if (!(e instanceof ParserError)) throw e + bF.parserPrintdbgline('e', 'It was NOT!', lnum, recDepth) + } + + /*************************************************************************/ + + // ## case for: + // | expr , op, expr + // | op_uni , expr + // if operator is found, split by the operator and recursively parse the LH and RH + if (topmostOp !== undefined) { + bF.parserPrintdbgline('e', 'Operators', lnum, recDepth) + + if (ifMode && topmostOp == "=") throw lang.syntaxfehler(lnum, "'=' used on IF, did you mean '=='?") + if (ifMode && topmostOp == ":") throw lang.syntaxfehler(lnum, "':' used on IF") + + + // this is the AST we're going to build up and return + // (other IF clauses don't use this) + let treeHead = new BasicAST() + treeHead.astLnum = lnum + treeHead.astValue = topmostOp + treeHead.astType = "op" + + // BINARY_OP? + if (operatorPos > 0) { + let subtknL = tokens.slice(0, operatorPos) + let substaL = states.slice(0, operatorPos) + let subtknR = tokens.slice(operatorPos + 1, tokens.length) + let substaR = states.slice(operatorPos + 1, tokens.length) + + treeHead.astLeaves[0] = bF._parseExpr(lnum, subtknL, substaL, recDepth + 1) + treeHead.astLeaves[1] = bF._parseExpr(lnum, subtknR, substaR, recDepth + 1) + } + else { + if (topmostOp === "-") treeHead.astValue = "UNARYMINUS" + else if (topmostOp === "+") treeHead.astValue = "UNARYPLUS" + else if (topmostOp === "NOT") treeHead.astValue = "UNARYLOGICNOT" + else if (topmostOp === "BNOT") treeHead.astValue = "UNARYBNOT" + else if (topmostOp === "@") treeHead.astValue = "MRET" + else if (topmostOp === "`") treeHead.astValue = "MJOIN" + else throw new ParserError(`Unknown unary op '${topmostOp}'`) + + treeHead.astLeaves[0] = bF._parseExpr(lnum, + tokens.slice(operatorPos + 1, tokens.length), + states.slice(operatorPos + 1, states.length), + recDepth + 1 + ) + } + + return treeHead + } + + /*************************************************************************/ + + // ## case for: + // | kywd , expr - "(" + if (bS.builtin[headTkn] && headSta == "lit" && !bF._opPrc[headTkn] && + states[1] != "paren" && tokens[1] != "(" + ) { + bF.parserPrintdbgline('e', 'Builtin Function Call w/o Paren', lnum, recDepth) + + return bF._parseFunctionCall(lnum, tokens, states, recDepth + 1) + } + + /*************************************************************************/ + + // ## case for: + // | function_call + if (topmostOp === undefined) { // don't remove this IF statement! + try { + bF.parserPrintdbgline('e', "Trying Function Call...", lnum, recDepth) + return bF._parseFunctionCall(lnum, tokens, states, recDepth + 1) + } + // if ParserError is raised, continue applying other rules + catch (e) { + if (!(e instanceof ParserError)) throw e + bF.parserPrintdbgline('e', 'It was NOT!', lnum, recDepth) + } + } + + /*************************************************************************/ + + throw new ParserError(`Expression "${tokens.join(" ")}" cannot be parsed in ${lnum}`) +} // END of EXPR +/** Parses following EBNF rule: + "{" , [expr , {"," , expr}] , "}" + * @return: BasicAST + */ +bF._parseArrayLiteral = function(lnum, tokens, states, recDepth) { + bF.parserPrintdbg2('{', lnum, tokens, states, recDepth) + + /*************************************************************************/ + + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + let curlyDepth = 0 + let curlyStart = -1 + let curlyEnd = -1 + let argSeps = [] + + // scan for parens that will be used for several rules + // also find nearest THEN and ELSE but also take parens into account + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (tokens[k] == "(" && states[k] == "paren") { + parenDepth += 1 + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // increase curly depth and mark curly start position + else if (tokens[k] == "{" && states[k] == "paren") { + curlyDepth += 1 + if (curlyStart == -1 && curlyDepth == 1) curlyStart = k + } + // decrease paren depth + else if (tokens[k] == ")" && states[k] == "paren") { + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + parenDepth -= 1 + } + // decrease curly depth + else if (tokens[k] == "}" && states[k] == "paren") { + if (curlyEnd == -1 && curlyDepth == 1) curlyEnd = k + curlyDepth -= 1 + } + + // commas + if (parenDepth == 0 && curlyDepth == 1 && tokens[k] == "," && states[k] == "sep") { + argSeps.push(k) + } + } + + // unmatched brackets, duh! + if (curlyDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + if (curlyStart == -1) throw new ParserError("not an array") + + /*************************************************************************/ + + bF.parserPrintdbgline('{', `curlyStart=${curlyStart}, curlyEnd=${curlyEnd}, argSeps=${argSeps}`, lnum, recDepth) + + let argStartPos = [1].concat(argSeps.map(k => k+1)) + let argPos = argStartPos.map((s,i) => {return{start:s, end:(argSeps[i] || curlyEnd)}}) // use end of token position as separator position + + bF.parserPrintdbgline("{", "argPos = "+argPos.map(it=>`${it.start}/${it.end}`), lnum, recDepth) + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + treeHead.astValue = "ARRAY CONSTRUCTOR" + treeHead.astType = "function" + + if (curlyStart == 0 && curlyEnd == 1) { + treeHead.astLeaves = [] + } + else { + treeHead.astLeaves = argPos.map((x,i) => { + bF.parserPrintdbgline("{", 'Array Element #'+(i+1), lnum, recDepth) + + // check for empty tokens + if (x.end - x.start < 0) throw new lang.syntaxfehler(lnum) + + return bF._parseExpr(lnum, + tokens.slice(x.start, x.end), + states.slice(x.start, x.end), + recDepth + 1 + )} + ) + } + + return treeHead + +} +/** Parses following EBNF rule: + "IF" , expr_sans_asgn , "THEN" , stmt , ["ELSE" , stmt] + | "IF" , expr_sans_asgn , "THEN" , expr , ["ELSE" , expr] + if exprMode is true, only the latter will be used; former otherwise + * @return: BasicAST + */ +bF._parseIfMode = function(lnum, tokens, states, recDepth, exprMode) { + bF.parserPrintdbg2('/', lnum, tokens, states, recDepth) + + /*************************************************************************/ + + let headTkn = tokens[0].toUpperCase() + let headSta = states[0] + + let parseFunction = (exprMode) ? bF._parseExpr : bF._parseStmt + + let thenPos = -1 + let elsePos = -1 + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + + // scan for parens that will be used for several rules + // also find nearest THEN and ELSE but also take parens into account + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (tokens[k] == "(" && states[k] == "paren") { + parenDepth += 1 + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // decrease paren depth + else if (tokens[k] == ")" && states[k] == "paren") { + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + parenDepth -= 1 + } + + if (parenDepth == 0) { + if (-1 == thenPos && "THEN" == tokens[k].toUpperCase() && "lit" == states[k]) + thenPos = k + else if (-1 == elsePos && "ELSE" == tokens[k].toUpperCase() && "lit" == states[k]) + elsePos = k + } + } + + // unmatched brackets, duh! + if (parenDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + + // ## case for: + // "IF" , expr_sans_asgn , "THEN" , stmt , ["ELSE" , stmt] + if ("IF" == headTkn && "lit" == headSta) { + + // "THEN" not found, raise error! + if (thenPos == -1) throw lang.syntaxfehler(lnum, "IF without THEN") + + treeHead.astValue = "IF" + treeHead.astType = "function" + + treeHead.astLeaves[0] = bF._parseExpr(lnum, + tokens.slice(1, thenPos), + states.slice(1, thenPos), + recDepth + 1, + true // if_equation mode + ) + treeHead.astLeaves[1] = parseFunction(lnum, + tokens.slice(thenPos + 1, (elsePos != -1) ? elsePos : tokens.length), + states.slice(thenPos + 1, (elsePos != -1) ? elsePos : tokens.length), + recDepth + 1 + ) + if (elsePos != -1) + treeHead.astLeaves[2] = parseFunction(lnum, + tokens.slice(elsePos + 1, tokens.length), + states.slice(elsePos + 1, tokens.length), + recDepth + 1 + ) + + return treeHead + } + + throw new ParserError("not an IF "+(exprMode) ? "expression" : "statement") +} // END of IF +/** Parses following EBNF rule: + ("FOR"|"FOREACH") , expr + * @return: BasicAST + */ +bF._parseForLoop = function(lnum, tokens, states, recDepth) { + bF.parserPrintdbg2('\\', lnum, tokens, states, recDepth) + + /*************************************************************************/ + + let headTkn = tokens[0].toUpperCase() + let headSta = states[0] + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + + // ## case for: + // ("FOR"|"FOREACH") , expr + if (("FOR" == headTkn || "FOREACH" == headTkn) && "lit" == headSta) { + + treeHead.astValue = headTkn + treeHead.astType = "function" + + treeHead.astLeaves[0] = bF._parseExpr(lnum, + tokens.slice(1), + states.slice(1), + recDepth + 1 + ) + + return treeHead + } + + throw new ParserError("not an FOR/FOREACH expression") + +} // END of FOR +/** Parses following EBNF rule: +ident_tuple = "[" , ident , ["," , ident] , "]" + * @return: BasicAST + */ +bF._parseTuple = function(lnum, tokens, states, recDepth) { + bF.parserPrintdbg2(']', lnum, tokens, states, recDepth) + + /*************************************************************************/ + + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + let argSeps = [] // argseps collected when parenDepth == 0 + + // Scan for unmatched parens and mark off the right operator we must deal with + // every function_call need to re-scan because it is recursively called + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (tokens[k] == "[" && states[k] == "paren") { + parenDepth += 1 + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // decrease paren depth + else if (tokens[k] == "]" && states[k] == "paren") { + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + parenDepth -= 1 + } + + // where are the argument separators + if (parenDepth == 1 && parenEnd == -1 && states[k] == "sep") + argSeps.push(k) + // break if we've got all the values we nedd + if (parenStart != -1 && parenEnd != -1) + break + } + + // unmatched brackets, duh! + if (parenDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + + if (parenStart != 0 || parenEnd != tokens.length - 1) + throw new ParserError("not a Tuple expression") + + /*************************************************************************/ + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + treeHead.astValue = undefined + treeHead.astType = "closure_args" + + // parse function arguments + bF.parserPrintdbgline(']', 'Tuple arguments -- ', lnum, recDepth) + let defunArgDeclSeps = argSeps.map(i => i-1).concat([parenEnd - 1]) + bF.parserPrintdbgline(']', 'Tuple comma position: '+defunArgDeclSeps, lnum, recDepth) + + treeHead.astLeaves = defunArgDeclSeps.map(i=>bF._parseIdent(lnum, [tokens[i]], [states[i]], recDepth + 1)) + + return treeHead +} +/** Parses following EBNF rule: +function_call = + ident , "(" , [expr , {argsep , expr} , [argsep]] , ")" + | ident , expr , {argsep , expr} , [argsep] + * @return: BasicAST + */ +bF._parseFunctionCall = function(lnum, tokens, states, recDepth) { + bF.parserPrintdbg2("F", lnum, tokens, states, recDepth) + + /*************************************************************************/ + + let parenDepth = 0 + let parenStart = -1 + let parenEnd = -1 + let _argsepsOnLevelZero = [] // argseps collected when parenDepth == 0 + let _argsepsOnLevelOne = [] // argseps collected when parenDepth == 1 + let currentParenMode = [] // a stack; must be able to distinguish different kinds of parens as closures use [ this paren ] + let depthsOfRoundParen = [] + + // Scan for unmatched parens and mark off the right operator we must deal with + // every function_call need to re-scan because it is recursively called + for (let k = 0; k < tokens.length; k++) { + // increase paren depth and mark paren start position + if (bF._isParenOpen(tokens[k]) && states[k] == "paren") { + parenDepth += 1; currentParenMode.unshift(tokens[k]) + if (currentParenMode[0] == '(') depthsOfRoundParen.push(parenDepth) + if (parenStart == -1 && parenDepth == 1) parenStart = k + } + // decrease paren depth + else if (bF._isParenClose(tokens[k]) && states[k] == "paren") { + if (!bF._isMatchingParen(currentParenMode[0], tokens[k])) + throw lang.syntaxfehler(lnum, `Opening paren: ${currentParenMode[0]}, closing paren: ${tokens[k]}`) + + if (parenEnd == -1 && parenDepth == 1) parenEnd = k + if (currentParenMode[0] == '(') depthsOfRoundParen.pop() + parenDepth -= 1; currentParenMode.shift() + } + + if (parenDepth == 0 && states[k] == "sep" && currentParenMode[0] === undefined) + _argsepsOnLevelZero.push(k) + if (parenDepth == depthsOfRoundParen[0] && states[k] == "sep" && currentParenMode[0] == "(") + _argsepsOnLevelOne.push(k) + } + + // unmatched brackets, duh! + if (parenDepth != 0) throw lang.syntaxfehler(lnum, lang.unmatchedBrackets) + let parenUsed = (parenStart == 1) + // && parenEnd == tokens.length - 1) + // if starting paren is found, just use it + // this prevents "RND(~~)*K" to be parsed as [RND, (~~)*K] + + bF.parserPrintdbgline("F", `parenStart: ${parenStart}, parenEnd: ${parenEnd}`, lnum, recDepth) + + /*************************************************************************/ + + // ## case for: + // ident , "(" , [expr , {argsep , expr} , [argsep]] , ")" + // | ident , expr , {argsep , expr} , [argsep] + bF.parserPrintdbgline("F", `Function Call (parenUsed: ${parenUsed})`, lnum, recDepth) + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + + // set function name and also check for syntax by deliberately parsing the word + treeHead.astValue = bF._parseIdent(lnum, [tokens[0]], [states[0]], recDepth + 1).astValue // always UPPERCASE + + // 5 8 11 [end] + let argSeps = parenUsed ? _argsepsOnLevelOne : _argsepsOnLevelZero // choose which "sep tray" to use + bF.parserPrintdbgline("F", "argSeps = "+argSeps, lnum, recDepth) + // 1 6 9 12 + let argStartPos = [1 + (parenUsed)].concat(argSeps.map(k => k+1)) + bF.parserPrintdbgline("F", "argStartPos = "+argStartPos, lnum, recDepth) + // [1,5) [6,8) [9,11) [12,end) + let argPos = argStartPos.map((s,i) => {return{start:s, end:(argSeps[i] || (parenUsed ? parenEnd : tokens.length) )}}) // use end of token position as separator position + bF.parserPrintdbgline("F", "argPos = "+argPos.map(it=>`${it.start}/${it.end}`), lnum, recDepth) + + // check for trailing separator + + + // recursively parse function arguments + treeHead.astLeaves = argPos.map((x,i) => { + bF.parserPrintdbgline("F", `Function Arguments #${i+1} of ${argPos.length}`, lnum, recDepth) + + // check for empty tokens + if (x.end - x.start < 0) throw new ParserError("not a function call because it's malformed") + + return bF._parseExpr(lnum, + tokens.slice(x.start, x.end), + states.slice(x.start, x.end), + recDepth + 1 + )} + ) + treeHead.astType = "function" + treeHead.astSeps = argSeps.map(i => tokens[i]) + bF.parserPrintdbgline("F", "astSeps = "+treeHead.astSeps, lnum, recDepth) + + return treeHead +} +bF._parseIdent = function(lnum, tokens, states, recDepth) { + bF.parserPrintdbg2('i', lnum, tokens, states, recDepth) + + if (!Array.isArray(tokens) && !Array.isArray(states)) throw new ParserError("Tokens and states are not array") + if (tokens.length != 1 || states[0] != "lit") throw new ParserError(`illegal tokens '${tokens}' with states '${states}' in ${lnum}`) + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + treeHead.astValue = tokens[0].toUpperCase() + treeHead.astType = "lit" + + return treeHead +} +/** + * @return: BasicAST + */ +bF._parseLit = function(lnum, tokens, states, recDepth, functionMode) { + bF.parserPrintdbg2('i', lnum, tokens, states, recDepth) + + if (!Array.isArray(tokens) && !Array.isArray(states)) throw new ParserError("Tokens and states are not array") + if (tokens.length != 1) throw new ParserError("parseLit 1") + + let treeHead = new BasicAST() + treeHead.astLnum = lnum + treeHead.astValue = ("qot" == states[0]) ? tokens[0] : tokens[0].toUpperCase() + treeHead.astType = ("qot" == states[0]) ? "string" : + ("num" == states[0]) ? "num" : + (functionMode) ? "function" : "lit" + + return treeHead +} +/** + * @return: Array of [recurseIndex, orderlyIndex], where recurseIndex is in reverse and orderlyIndex is not + */ +bF._findDeBruijnIndex = function(varname, offset) { + let recurseIndex = -1 + let orderlyIndex = -1 + for (recurseIndex = 0; recurseIndex < lambdaBoundVars.length; recurseIndex++) { + orderlyIndex = lambdaBoundVars[recurseIndex].findIndex(it => it == varname) + if (orderlyIndex != -1) + return [recurseIndex + (offset || 0), orderlyIndex] + } + throw new ParserError("Unbound variable: "+varname) +} +/** + * @return: BasicAST + */ +bF._pruneTree = function(lnum, tree, recDepth) { + if (tree === undefined) return + + if (DBGON) { + serial.println("[Parser.PRUNE] pruning following subtree, lambdaBoundVars = "+Object.entries(lambdaBoundVars)) // theLambdaBoundVars() were not formatted for this use case! + serial.println(astToString(tree)) + if (isAST(tree) && isAST(tree.astValue)) { + serial.println("[Parser.PRUNE] unpacking astValue:") + serial.println(astToString(tree.astValue)) + } + } + + let defunName = undefined + + // catch all the bound variables for function definition + if (tree.astType == "op" && tree.astValue == "~>" || tree.astType == "function" && tree.astValue == "DEFUN") { + + let nameTree = tree.astLeaves[0] + if (tree.astValue == "DEFUN") { + defunName = nameTree.astValue + + if (DBGON) { + serial.println("[Parser.PRUNE.~>] met DEFUN, function name: "+defunName) + } + } + let vars = nameTree.astLeaves.map((it, i) => { + if (it.astType !== "lit") throw new ParserError("Malformed bound variable for function definition; tree:\n"+astToString(nameTree)) + return it.astValue + }) + + lambdaBoundVars.unshift(vars) + + if (DBGON) { + serial.println("[Parser.PRUNE.~>] added new bound variables: "+Object.entries(lambdaBoundVars)) + } + } + // simplify UNARYMINUS(num) to -num + else if (tree.astValue == "UNARYMINUS" && tree.astType == "op" && + tree.astLeaves[1] === undefined && tree.astLeaves[0] !== undefined && tree.astLeaves[0].astType == "num" + ) { + tree.astValue = -(tree.astLeaves[0].astValue) + tree.astType = "num" + tree.astLeaves = [] + } + else if (tree.astValue == "UNARYPLUS" && tree.astType == "op" && + tree.astLeaves[1] === undefined && tree.astLeaves[0] !== undefined && tree.astLeaves[0].astType == "num" + ) { + tree.astValue = +(tree.astLeaves[0].astValue) + tree.astType = "num" + tree.astLeaves = [] + } + + + // depth-first run + if (tree.astLeaves[0] != undefined) { + tree.astLeaves.forEach(it => bF._pruneTree(lnum, it, recDepth + 1)) + } + + + if (tree.astType == "op" && tree.astValue == "~>" || tree.astType == "function" && tree.astValue == "DEFUN") { + + if (tree.astLeaves.length !== 2) throw lang.syntaxfehler(lnum, tree.astLeaves.length+lang.aG) + + let nameTree = tree.astLeaves[0] + let exprTree = tree.astLeaves[1] + + // test print new tree + if (DBGON) { + serial.println("[Parser.PRUNE.~>] closure bound variables: "+Object.entries(lambdaBoundVars)) + } + + // rename the parameters + bF._recurseApplyAST(exprTree, (it) => { + if (it.astType == "lit" || it.astType == "function") { + // check if parameter name is valid + // if the name is invalid, regard it as a global variable (i.e. do nothing) + try { + let dbi = bF._findDeBruijnIndex(it.astValue) + + if (DBGON) { + serial.println(`index for ${it.astValue}: ${dbi}`) + } + + + it.astValue = dbi + it.astType = "defun_args" + } + catch (_) {} + } + }) + + tree.astType = "usrdefun" + tree.astValue = exprTree + tree.astLeaves = [] + + lambdaBoundVars.shift() + } + + // for DEFUNs, build assign tree such that: + // DEFUN F lambda + // turns into: + // F=(lambda) + if (defunName) { + let nameTree = new BasicAST() + nameTree.astLnum = tree.astLnum + nameTree.astType = "lit" + nameTree.astValue = defunName + + let newTree = new BasicAST() + newTree.astLnum = tree.astLnum + newTree.astType = "op" + newTree.astValue = "=" + newTree.astLeaves = [nameTree, tree] + + tree = newTree + + if (DBGON) { + serial.println(`[Parser.PRUNE] has DEFUN, function name: ${defunName}`) + } + } + + if (DBGON) { + serial.println("[Parser.PRUNE] pruned subtree:") + serial.println(astToString(tree)) + if (isAST(tree) && isAST(tree.astValue)) { + serial.println("[Parser.PRUNE] unpacking astValue:") + serial.println(astToString(tree.astValue)) + } + + serial.println("======================================================\n") + } + + return tree +} + +// ## USAGE OF lambdaBoundVars IN PARSEMODE ENDS HERE ## + +// @return is defined in BasicAST +let JStoBASICtype = function(object) { + if (typeof object === "boolean") return "bool" + else if (object === undefined) return "null" + else if (object.arrName !== undefined) return "internal_arrindexing_lazy" + else if (object.asgnVarName !== undefined) return "internal_assignment_object" + else if (isGenerator(object)) return "generator" + else if (isAST(object)) return "usrdefun" + else if (isMonad(object)) return "monad" + else if (Array.isArray(object)) return "array" + else if (isNumable(object)) return "num" + else if (typeof object === "string" || object instanceof String) return "string" + // buncha error msgs + else throw Error("BasicIntpError: un-translatable object with typeof "+(typeof object)+",\ntoString = "+object+",\nentries = "+Object.entries(object)) +} +let SyntaxTreeReturnObj = function(type, value, nextLine) { + if (nextLine === undefined || !Array.isArray(nextLine)) + throw Error("TODO change format of troNextLine to [linenumber, stmtnumber]") + + this.troType = type + this.troValue = value + this.troNextLine = nextLine +} +let JumpObj = function(targetLnum, targetStmtNum, fromLnum, rawValue) { + this.jmpNext = [targetLnum, targetStmtNum] + this.jmpFrom = fromLnum + this.jmpReturningValue = rawValue +} +bF._makeRunnableFunctionFromExprTree = function(lnum, stmtnum, expression, args, recDepth, _debugExec, recWedge) { + // register variables + let defunArgs = args.map(it => { + let rit = resolve(it) + return [JStoBASICtype(rit), rit] + }) + lambdaBoundVars.unshift(defunArgs) + + if (_debugExec) { + serial.println(recWedge+"usrdefun dereference") + serial.println(recWedge+"usrdefun dereference function: ") + serial.println(astToString(expression)) + serial.println(recWedge+"usrdefun dereference bound vars: "+theLambdaBoundVars()) + } + + // insert bound variables to its places + let bindVar = function(tree, recDepth) { + bF._recurseApplyAST(tree, it => { + + if (_debugExec) { + serial.println(recWedge+`usrdefun${recDepth} trying to bind some variables to:`) + serial.println(astToString(it)) + } + + if (it.astType == "defun_args") { + let recIndex = it.astValue[0] - recDepth + let varIndex = it.astValue[1] + + if (_debugExec) { + serial.println(recWedge+`usrdefun${recDepth} bindvar d(${recIndex},${varIndex})`) + } + + let theVariable = undefined + try { + theVariable = lambdaBoundVars[recIndex][varIndex] + } + catch (e0) {} + + // this will make partial applying work, but completely remove the ability of catching errors... + if (theVariable !== undefined) { + it.astValue = theVariable[1] + it.astType = theVariable[0] + } + + if (_debugExec) { + serial.println(recWedge+`usrdefun${recDepth} the bindvar: ${theVariable}`) + serial.println(recWedge+`usrdefun${recDepth} modified tree:`) + serial.println(astToString(it)) + } + } + // function in a function + else if (it.astType == "usrdefun") { + bindVar(it.astValue, recDepth + 1) + } + }) + };bindVar(expression, 0) + + + if (_debugExec) { + serial.println(recWedge+"usrdefun dereference final tree:") + serial.println(astToString(expression)) + } + + return bS.getDefunThunk(expression, true) +} +/** + * @param lnum line number of BASIC + * @param syntaxTree BasicAST + * @param recDepth recursion depth used internally + * + * @return syntaxTreeReturnObject if recursion is escaped + */ +bF._troNOP = function(lnum, stmtnum) { return new SyntaxTreeReturnObj("null", undefined, [lnum, stmtnum+1]) } +bF._executeSyntaxTree = function(lnum, stmtnum, syntaxTree, recDepth) { + if (syntaxTree == undefined) return bF._troNOP(lnum, stmtnum) + if (syntaxTree.astLeaves === undefined && syntaxTree.astValue === undefined) { + throw new BASICerror("not a syntax tree") + } + + if (lnum === undefined || stmtnum === undefined) throw Error(`Line or statement number is undefined: (${lnum},${stmtnum})`) + + let _debugExec = (!PROD) && true + let _debugPrintCurrentLine = (!PROD) && true + let recWedge = ">".repeat(recDepth+1) + " " + let tearLine = "\n =====ExecSyntaxTree===== "+("<".repeat(recDepth+1))+"\n" + + if (_debugExec || _debugPrintCurrentLine) serial.println(recWedge+`@@ EXECUTE ${lnum}:${stmtnum} @@`) + if (_debugPrintCurrentLine) { + serial.println("Syntax Tree in "+lnum+":") + serial.println(astToString(syntaxTree)) + } + + let callingUsrdefun = (syntaxTree.astType == "usrdefun" && syntaxTree.astLeaves[0] !== undefined) + // do NOT substitute (syntaxTree.astType == "usrdefun") with isAST; doing so will break (=) operator + // calling usrdefun without any args will make leaves[0] to be null-node but not undefined + // funseq-monad will be dealt with on (func === undefined) branch + + if (syntaxTree.astValue == undefined && syntaxTree.mVal == undefined) { // empty meaningless parens + if (syntaxTree.astLeaves.length > 1) throw Error("WTF") + return bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[0], recDepth) + } + // array indexing in the tree (caused by indexing array within recursive DEFUN) + else if (syntaxTree.astType == "array" && syntaxTree.astLeaves[0] !== undefined) { + let indexer = bS.getArrayIndexFun(lnum, stmtnum, "substituted array", syntaxTree.astValue) + let args = syntaxTree.astLeaves.map(it => bF._executeSyntaxTree(lnum, stmtnum, it, recDepth + 1)) + let retVal = indexer(lnum, stmtnum, args) + if (_debugExec) serial.println(recWedge+`indexing substituted array(${Object.entries(args)}) = ${Object.entries(retVal)}`) + return new SyntaxTreeReturnObj( + JStoBASICtype(retVal), + retVal, + [lnum, stmtnum + 1] + ) + } + // closure + // type: closure_args ~> (expr) + else if (syntaxTree.astType == "op" && syntaxTree.astValue == "~>") { + throw new BASICerror("Untended closure") // closure definition must be 'pruned' by the parser + } + else if (syntaxTree.astType == "function" && syntaxTree.astValue == "DEFUN") { + throw new BASICerror("Untended DEFUN") // DEFUN must be 'pruned' by the parser + } + else if (syntaxTree.astType == "function" || syntaxTree.astType == "op" || callingUsrdefun) { + if (_debugExec) serial.println(recWedge+"function|operator") + if (_debugExec) serial.println(recWedge+astToString(syntaxTree)) + let callerHash = syntaxTree.astHash + let funcName = (typeof syntaxTree.astValue.toUpperCase == "function") ? syntaxTree.astValue.toUpperCase() : "(usrdefun)" + let lambdaBoundVarsAppended = (callingUsrdefun) + let func = (callingUsrdefun) + ? bF._makeRunnableFunctionFromExprTree( + lnum, stmtnum, + cloneObject(syntaxTree.astValue), + syntaxTree.astLeaves.map(it => bF._executeSyntaxTree(lnum, stmtnum, it, recDepth + 1)), // the args + recDepth, _debugExec, recWedge + ) + : (bS.builtin[funcName] === undefined) + ? undefined + : (!DBGON && bS.builtin[funcName].debugonly) ? "NO_DBG4U" : (PROD && bS.builtin[funcName].noprod) ? "NO_PRODREADY" : bS.builtin[funcName].f + + if (func === "NO_DBG4U") throw lang.syntaxfehler(lnum) + if (func === "NO_PRODREADY") throw lang.syntaxfehler(lnum) + + if ("IF" == funcName) { + if (syntaxTree.astLeaves.length != 2 && syntaxTree.astLeaves.length != 3) throw lang.syntaxfehler(lnum) + var testedval = bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[0], recDepth + 1) + + if (_debugExec) { + serial.println(recWedge+"testedval:") + serial.println(recWedge+"type="+testedval.troValue.astType) + serial.println(recWedge+"value="+testedval.troValue.astValue) + serial.println(recWedge+"nextLine="+testedval.troValue.astNextLine) + } + + try { + var iftest = bS.builtin["TEST"].f(lnum, stmtnum, [testedval]) + + let r = (!iftest && syntaxTree.astLeaves[2] !== undefined) ? + bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[2], recDepth + 1) + : (iftest) ? + bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[1], recDepth + 1) + : bF._troNOP(lnum, stmtnum) + + if (_debugExec) serial.println(tearLine) + return r + } + catch (e) { + serial.printerr(`${e}\n${e.stack || "Stack trace undefined"}`) + throw lang.errorinline(lnum, "TEST", e) + } + } + else if ("ON" == funcName) { + if (syntaxTree.astLeaves.length < 3) throw lang.badFunctionCallFormat(lnum) + + let testValue = bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[0], recDepth + 1) + let functionName = syntaxTree.astLeaves[1].astValue + let arrays = [] + for (let k = 2; k < syntaxTree.astLeaves.length; k++) + arrays.push(bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[k], recDepth + 1)) + + try { + let r = bS.builtin["ON"].f(lnum, stmtnum, [functionName, testValue].concat(arrays)) + let r2 = new SyntaxTreeReturnObj(JStoBASICtype(r.jmpReturningValue), r.jmpReturningValue, r.jmpNext) + if (_debugExec) serial.println(tearLine) + return r2 + } + catch (e) { + serial.printerr(`${e}\n${e.stack || "Stack trace undefined"}`) + throw lang.errorinline(lnum, "ON error", e) + } + } + else { + let args = syntaxTree.astLeaves.map(it => bF._executeSyntaxTree(lnum, stmtnum, it, recDepth + 1)) + + if (_debugExec) { + serial.println(recWedge+`fn caller: "${callerHash}"`) + serial.println(recWedge+`fn call name: "${funcName}"`) + serial.println(recWedge+"fn call args: "+(args.map(it => (it == undefined) ? it : (it.troType+" "+it.troValue)).join(", "))) + } + + // func not in builtins (e.g. array access, user-defined function defuns) + if (func === undefined) { + var someVar = bS.vars[funcName] + + if (someVar !== undefined && DBGON) { + serial.println(recWedge+`variable dereference of '${funcName}' : ${someVar.bvLiteral} (bvType: ${someVar.bvType})`) + if (typeof someVar.bvLiteral == "object") + serial.println(recWedge+"variable as an object : "+Object.entries(someVar.bvLiteral)) + } + + if (someVar === undefined) { + throw lang.syntaxfehler(lnum, funcName + " is undefined") + } + else if ("array" == someVar.bvType) { + func = bS.getArrayIndexFun(lnum, stmtnum, funcName, someVar.bvLiteral) + } + else if ("usrdefun" == someVar.bvType) { + // dereference usrdefun + let expression = cloneObject(someVar.bvLiteral) + lambdaBoundVarsAppended = true + func = bF._makeRunnableFunctionFromExprTree(lnum, stmtnum, expression, args, recDepth, _debugExec, recWedge) + } + else if ("monad" == someVar.bvType) { + func = getMonadEvalFun(someVar.bvLiteral) + } + else { + throw lang.syntaxfehler(lnum, funcName + " is not a function or an array") + } + } + + // call whatever the 'func' has whether it's builtin or we just made shit up right above + if (func === undefined) { + serial.printerr(lnum+` ${funcName} is undefined`) + throw lang.syntaxfehler(lnum, funcName + " is undefined") + } + + let funcCallResult = func(lnum, stmtnum, args, syntaxTree.astSeps) + + if (funcCallResult instanceof SyntaxTreeReturnObj) return funcCallResult + + let retVal = (funcCallResult instanceof JumpObj) ? funcCallResult.jmpReturningValue : funcCallResult + + let theRealRet = new SyntaxTreeReturnObj( + JStoBASICtype(retVal), + retVal, + (funcCallResult instanceof JumpObj) ? funcCallResult.jmpNext : [lnum, stmtnum + 1] + ) + + // unregister variables + if (lambdaBoundVarsAppended) lambdaBoundVars.shift() + + if (_debugExec) serial.println(tearLine) + return theRealRet + } + } + else if (syntaxTree.astType == "defun_args") { + if (_debugExec) { + serial.println(recWedge+"defun_args lambda bound vars: "+(lambdaBoundVars === undefined) ? undefined : theLambdaBoundVars()) + serial.println(recWedge+"defun_args defun args: "+syntaxTree.astValue) + } + let recIndex = syntaxTree.astValue[0] + let varIndex = syntaxTree.astValue[1] + let theVar = lambdaBoundVars[recIndex, varIndex] + if (_debugExec) { + serial.println(recWedge+"defun_args thevar: "+(theVar === undefined) ? undefined : Object.entries(theVar)) + serial.println(tearLine) + } + return theVar + } + else if (syntaxTree.astType == "num") { + if (_debugExec) serial.println(recWedge+"num "+(tonum(syntaxTree.astValue))) + let r = new SyntaxTreeReturnObj(syntaxTree.astType, tonum(syntaxTree.astValue), [lnum, stmtnum + 1]) + if (_debugExec) serial.println(tearLine) + return r + } + else if (syntaxTree.astType == "lit" || literalTypes.includes(syntaxTree.astType)) { + if (_debugExec) { + serial.println(recWedge+"literal with astType: "+syntaxTree.astType+", astValue: "+syntaxTree.astValue) + if (isAST(syntaxTree.astValue)) { + serial.println(recWedge+"astValue is a tree, unpacking: \n"+astToString(syntaxTree.astValue)) + } + } + let r = new SyntaxTreeReturnObj(syntaxTree.astType, syntaxTree.astValue, [lnum, stmtnum + 1]) + if (_debugExec) serial.println(tearLine) + return r + } + else if (syntaxTree.astType == "null") { + if (_debugExec) serial.println(recWedge+"null") + let r = bF._executeSyntaxTree(lnum, stmtnum, syntaxTree.astLeaves[0], recDepth + 1) + if (_debugExec) serial.println(tearLine) + return r + } + else { + serial.println(recWedge+"Parsing error in "+lnum) + serial.println(recWedge+astToString(syntaxTree)) + throw Error("Parsing error") + } +} // END OF bF._executeSyntaxTree +// @return ARRAY of BasicAST +bF._interpretLine = function(lnum, cmd) { + let _debugprintHighestLevel = false + + if (cmd.toUpperCase().startsWith("REM")) { + if (_debugprintHighestLevel) serial.println(lnum+" "+cmd) + return undefined + } + + // TOKENISE + let tokenisedObject = bF._tokenise(lnum, cmd) + let tokens = tokenisedObject.tokens + let states = tokenisedObject.states + + + // ELABORATION : distinguish numbers and operators from literals + let newtoks = bF._parserElaboration(lnum, tokens, states) + tokens = newtoks.tokens + states = newtoks.states + + // PARSING (SYNTAX ANALYSIS) + let syntaxTrees = bF._parseTokens(lnum, tokens, states).map(it => { + if (lambdaBoundVars.length != 0) + throw new BASICerror("lambdaBoundVars not empty") + return bF._pruneTree(lnum, it, 0) + }) + + if (_debugprintHighestLevel) { + syntaxTrees.forEach((t,i) => { + serial.println("\nParsed Statement #"+(i+1)) + serial.println(astToString(t)) + }) + } + + return syntaxTrees +} // end INTERPRETLINE +// @return [next line number, next statement number] +bF._executeAndGet = function(lnum, stmtnum, syntaxTree) { + if (lnum === undefined || stmtnum === undefined) throw Error(`Line or statement number is undefined: (${lnum},${stmtnum})`) + + // EXECUTE + try { + if (lambdaBoundVars.length != 0) throw new BASICerror() + var execResult = bF._executeSyntaxTree(lnum, stmtnum, syntaxTree, 0) + + if (DBGON) serial.println(`Line ${lnum} TRO: ${Object.entries(execResult)}`) + + return execResult.troNextLine + } + catch (e) { + serial.printerr(`ERROR on ${lnum}:${stmtnum} -- PARSE TREE:\n${astToString(syntaxTree)}\nERROR CONTENTS:\n${e}\n${e.stack || "Stack trace undefined"}`) + throw e + } +} +bF._basicList = function(v, i, arr) { + if (i < 10) print(" ") + if (i < 100) print(" ") + print(i) + print(" ") + println(v) +} +bF.list = function(args) { // LIST function + if (args.length == 1) { + cmdbuf.forEach(bF._basicList) + } + else if (args.length == 2) { + if (cmdbuf[args[1]] !== undefined) + bF._basicList(cmdbuf[args[1]], args[1], undefined) + } + else { + var lastIndex = (args[2] === ".") ? cmdbuf.length - 1 : (args[2] | 0) + var i = 0 + for (i = args[1]; i <= lastIndex; i++) { + var cmd = cmdbuf[i] + if (cmd !== undefined) { + bF._basicList(cmd, i, cmdbuf) + } + } + } +} +bF.system = function(args) { // SYSTEM function + tbasexit = true +} +bF.new = function(args) { // NEW function + if (args) cmdbuf = [] + bS.vars = initBvars() + gotoLabels = {} + lambdaBoundVars = [] + DATA_CONSTS = [] + DATA_CURSOR = 0 + INDEX_BASE = 0 +} +bF.renum = function(args) { // RENUM function + var newcmdbuf = [] + var linenumRelation = [[]] + var cnt = 10 + for (var k = 0; k < cmdbuf.length; k++) { + if (cmdbuf[k] !== undefined) { + newcmdbuf[cnt] = cmdbuf[k].trim() + linenumRelation[k] = cnt + cnt += 10 + } + } + // deal with goto/gosub line numbers + for (k = 0; k < newcmdbuf.length; k++) { + if (newcmdbuf[k] !== undefined && newcmdbuf[k].toLowerCase().startsWith("goto ")) { + newcmdbuf[k] = "GOTO " + linenumRelation[newcmdbuf[k].match(reNum)[0]] + } + else if (newcmdbuf[k] !== undefined && newcmdbuf[k].toLowerCase().startsWith("gosub ")) { + newcmdbuf[k] = "GOSUB " + linenumRelation[newcmdbuf[k].match(reNum)[0]] + } + else if (newcmdbuf[k] !== undefined && newcmdbuf[k].toLowerCase().startsWith("breakto ")) { + newcmdbuf[k] = "BREAKTO " + linenumRelation[newcmdbuf[k].match(reNum)[0]] + } + } + cmdbuf = newcmdbuf.slice() // make shallow copy + + // recalculate memory footprint + cmdbufMemFootPrint = 0 + cmdbuf.forEach((v, i, arr) => + cmdbufMemFootPrint += ("" + i).length + 1 + v.length + ) +} +bF.fre = function(args) { + println(vmemsize - getUsedMemSize()) +} +bF.tron = function(args) { + TRACEON = true +} +bF.troff = function(args) { + TRACEON = false +} +bF.delete = function(args) { + if (args.length != 2 && args.length != 3) throw lang.syntaxfehler() + + // stupid Javascript can't even Array.prototype.remove(int) + let start = 0; let end = 0 + if (args.length == 2) { + if (!isNumable(args[1])) throw lang.badFunctionCallFormat() + start = args[1]|0 + end = args[1]|0 + } + else { + if (!isNumable(args[1]) && !isNumable(args[2])) throw lang.badFunctionCallFormat() + start = args[1]|0 + end = args[2]|0 + } + + let newcmdbuf = [] + cmdbuf.forEach((v,i) => {if (i < start || i > end) newcmdbuf[i]=v}) + cmdbuf = newcmdbuf +} +bF.cls = function(args) { + con.clear() +} +bF.prescanStmts = ["DATA","LABEL"] +bF.run = function(args) { // RUN function + bF.new(false) + + let programTrees = [] + // pre-build the trees + prescan = true + cmdbuf.forEach((linestr, linenum) => { + let trees = bF._interpretLine(linenum, linestr.trim()) + programTrees[linenum] = trees + // do prescan job (data, label, etc) + if (trees !== undefined) { + trees.forEach((t, i) => { + if (t !== undefined && bF.prescanStmts.includes(t.astValue)) { + bF._executeAndGet(linenum, i, t) + } + }) + } + }) + prescan = false + + if (!PROD && DBGON) { + serial.println("[BASIC] final DATA: "+DATA_CONSTS) + } + + // actually execute the program + let lnum = 1 + let stmtnum = 0 + let oldnum = 1 + let tree = undefined + do { + if (programTrees[lnum] !== undefined) { + if (TRACEON) { + //print(`[${lnum}]`) + serial.println("[BASIC] Line "+lnum) + } + + oldnum = lnum + tree = (programTrees[lnum] !== undefined) ? programTrees[lnum][stmtnum] : undefined + + if (tree !== undefined) { + let nextObj = bF._executeAndGet(lnum, stmtnum, tree) + lnum = nextObj[0] + stmtnum = nextObj[1] + } + else { + lnum += 1 + stmtnum = 0 + } + } + else { + lnum += 1 + } + if (lnum < 0) throw lang.badNumberFormat + if (con.hitterminate()) { + println("Break in "+oldnum) + break + } + } while (lnum < cmdbuf.length) + con.resetkeybuf() +} +bF.load = function(filecontents) { // LOAD function + filecontents.split('\n').forEach((line) => { + var i = line.indexOf(" ") + var lnum = line.slice(0, i) + if (isNaN(lnum)) throw lang.illegalType() + cmdbuf[lnum] = line.slice(i + 1, line.length) + }) +} +Object.freeze(bF) + + +println("Loading program from ROM...") +sys.mapRom(1); +let ffffff = sys.romReadAll() +bF.load(ffffff) +println(ffffff) + +try { + bF.run() + // TODO turn EXE lamp on + // TODO turn ERR lamp off +} +catch (e) { + serial.printerr(`${e}\n${e.stack || "Stack trace undefined"}`) + println(`BASIC exit with error:`) + println(`${e}`) + // TODO turn EXE lamp off + // TODO turn ERR lamp on +} diff --git a/assets/bios/tandemport.js b/assets/bios/tandemport.js index 1b9186a..3784a48 100644 --- a/assets/bios/tandemport.js +++ b/assets/bios/tandemport.js @@ -1,6 +1,6 @@ con.curs_set(0) con.clear() -let t=`${system.maxmem()>>>10} Kbytes System` +let t=`${sys.maxmem()>>>10} Kbytes System` let imageBits = gzip.decomp(base64.atob( "H4sICC62h2ACA3RhbmRlbV9sb2dvXzI0MC5iaW4AhdQ/bsMgGAXwh4hEhyisHSq5R+iYISpX6REydqhkjsZRfASPDJbJ449jQuxUspDsn2XD+z6wAMSIPjiECQOgAwcoIMwQNuoAQ+2TilZlrehbdeioJqspypeTqgfttrXLqhvVljO9qypq/IPqrLLRblcZQQi8oyqqClZwiI+6cdHPVYcdlUnHVmdc5aooypVV+iaS+lYnXMUr9dQjkk6LMsEt/YkRcKL8WlQPj+BO+NtW/vFZpc06Ununcan1S9r3rHL+X+3HgwpkHaim1bPglVSFqFzTpsZeWzWncUZRd+DLTg+HOskL8Jv1+ErtiZk7PaKu4I6W6n8jph+1S+pRd85dOX/Wq6h9UmOjTqg71kAsykD2dI4qnZ5R75RVexbirmWXGSuTTlGF0wH1Dt1R02pg81BtfTIYp5L6qFh0OVWe1NUnUtIb4Dr/QbAEAAA=" )) @@ -15,7 +15,7 @@ for (let y = 0; y < 40; y++) { con.move(8,1+(40-t.length>>1)) print(t) // wait arbitrary time -for (let b=0;b>>10} Kbytes`; +let uramstr=` USER RAM : ${sys.maxmem()>>>10} Kbytes`; con.move(20,(80-vramstr.length)/2);println(vramstr); con.move(21,(80-uramstr.length)/2);println(uramstr); diff --git a/assets/bios/wp.js b/assets/bios/wp.js index 4762f87..2b3bab1 100644 --- a/assets/bios/wp.js +++ b/assets/bios/wp.js @@ -8,7 +8,7 @@ const COL_CARET_ROW = 81 const BIG_STRIDE = 999 const TAB_SIZE = 4 const PAINT_START_Y = 3 -const MEM = system.maxmem() +const MEM = sys.maxmem() const TYPESET_DEBUG_PRINT = true diff --git a/assets/disk0/dmatest.js b/assets/disk0/dmatest.js index 54f962f..975382d 100644 --- a/assets/disk0/dmatest.js +++ b/assets/disk0/dmatest.js @@ -1,5 +1,5 @@ let len = 560*448 -let pcnt = (system.maxmem() / len)|0 +let pcnt = (sys.maxmem() / len)|0 if (pcnt < 2) { printerrln("Not enought RAM on the system to test!") return diff --git a/assets/disk0/home/bf.js b/assets/disk0/home/bf.js index 738bd6a..c345c63 100644 --- a/assets/disk0/home/bf.js +++ b/assets/disk0/home/bf.js @@ -1,6 +1,6 @@ // exec_args: bf.js input_file optional_memsize let memsize = exec_args[2]|0; -if (memsize <= 0) memsize = (system.maxmem() < 30000) ? system.maxmem()-256 : 30000; +if (memsize <= 0) memsize = (sys.maxmem() < 30000) ? sys.maxmem()-256 : 30000; let nativePtr = undefined; try { nativePtr = sys.malloc(memsize); diff --git a/assets/disk0/home/logotest.js b/assets/disk0/home/logotest.js index b54f225..1f971b1 100644 --- a/assets/disk0/home/logotest.js +++ b/assets/disk0/home/logotest.js @@ -1,6 +1,6 @@ con.curs_set(0) con.clear() -let t=`${system.maxmem()>>>10} Kbytes System` +let t=`${sys.maxmem()>>>10} Kbytes System` let imageBits = gzip.decomp(base64.atob( "H4sICC62h2ACA3RhbmRlbV9sb2dvXzI0MC5iaW4AhdQ/bsMgGAXwh4hEhyisHSq5R+iYISpX6REydqhkjsZRfASPDJbJ449jQuxUspDsn2XD+z6wAMSIPjiECQOgAwcoIMwQNuoAQ+2TilZlrehbdeioJqspypeTqgfttrXLqhvVljO9qypq/IPqrLLRblcZQQi8oyqqClZwiI+6cdHPVYcdlUnHVmdc5aooypVV+iaS+lYnXMUr9dQjkk6LMsEt/YkRcKL8WlQPj+BO+NtW/vFZpc06Ununcan1S9r3rHL+X+3HgwpkHaim1bPglVSFqFzTpsZeWzWncUZRd+DLTg+HOskL8Jv1+ErtiZk7PaKu4I6W6n8jph+1S+pRd85dOX/Wq6h9UmOjTqg71kAsykD2dI4qnZ5R75RVexbirmWXGSuTTlGF0wH1Dt1R02pg81BtfTIYp5L6qFh0OVWe1NUnUtIb4Dr/QbAEAAA=" )) diff --git a/assets/disk0/tbas/basic.js b/assets/disk0/tbas/basic.js index b170208..4cb6b62 100644 --- a/assets/disk0/tbas/basic.js +++ b/assets/disk0/tbas/basic.js @@ -42,12 +42,12 @@ let DATA_CURSOR = 0 let DATA_CONSTS = [] const BASIC_HOME_PATH = "/home/basic/" -if (system.maxmem() < 8192) { +if (sys.maxmem() < 8192) { println("Out of memory. BASIC requires 8K or more User RAM") throw Error("Out of memory") } -let vmemsize = system.maxmem() +let vmemsize = sys.maxmem() let cmdbuf = [] // index: line number let gotoLabels = {} diff --git a/assets/disk0/tvdos/TVDOS.SYS b/assets/disk0/tvdos/TVDOS.SYS index f8866ba..d40ebb5 100644 --- a/assets/disk0/tvdos/TVDOS.SYS +++ b/assets/disk0/tvdos/TVDOS.SYS @@ -168,7 +168,7 @@ class TVDOSFileDescriptor { } list() { - return if (!this.isDirectory) undefined else this.driver.listFiles(this) + return (!this.isDirectory) ? undefined : this.driver.listFiles(this) } /** When the file does not exist, mkfile() will be called; if you want to make a directory, use mkdir() */ touch() { diff --git a/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt index f80c279..c07d889 100644 --- a/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt @@ -178,6 +178,9 @@ class VMJSR223Delegate(val vm: VM) { vm.sysrqDown = false } + fun maxmem(): Int { + return vm.memsize.toInt() + } fun getMallocStatus(): IntArray { return intArrayOf(vm.MALLOC_UNIT, vm.allocatedBlockCount) }