diff --git a/assets/disk0/AUTOEXEC.BAT b/assets/disk0/AUTOEXEC.BAT index 7ba415e..bf9d57e 100644 --- a/assets/disk0/AUTOEXEC.BAT +++ b/assets/disk0/AUTOEXEC.BAT @@ -1,7 +1,7 @@ echo "Starting TVDOS..." rem put set-xxx commands here: -set PATH=\tvdos\installer;\tvdos\tuidev;$PATH +set PATH=\tvdos\installer;\tvdos\tuidev;\tbas;$PATH set KEYBOARD=us_colemak rem this line specifies which shell to be presented after the boot precess: diff --git a/assets/disk0/tbas/basic.js b/assets/disk0/tbas/basic.js index 4cb6b62..355a726 100644 --- a/assets/disk0/tbas/basic.js +++ b/assets/disk0/tbas/basic.js @@ -32,7 +32,7 @@ if (exec_args !== undefined && exec_args[1] !== undefined && exec_args[1].starts return 0 } -const THEVERSION = "1.2.1" +const THEVERSION = "1.2.2" const PROD = true let INDEX_BASE = 0 @@ -4197,7 +4197,7 @@ bF.load = function(args) { // LOAD function if (args[1] === undefined) throw lang.missingOperand var fileOpened = fs.open(args[1], "R") - + serial.printerr('load '+args[1]) if (replUsrConfirmed || cmdbuf.length == 0) { if (!fileOpened) { fileOpened = fs.open(args[1]+".BAS", "R") @@ -4241,7 +4241,7 @@ bF.yes = function() { } } bF.catalog = function(args) { // CATALOG function - if (args[1] === undefined) args[1] = "\\" + if (args[1] === undefined) args[1] = BASIC_HOME_PATH var pathOpened = fs.open(args[1], 'R') if (!pathOpened) { throw lang.noSuchFile @@ -4251,6 +4251,57 @@ bF.catalog = function(args) { // CATALOG function com.sendMessage(port, "LIST") println(com.pullMessage(port)) } +// Load a file by absolute disk path (bypasses BASIC_HOME_PATH). +// Used by COMPILE to fetch /tbas/compile.js. +bF._slurpAbsolute = function(path) { + var port = _BIOS.FIRST_BOOTABLE_PORT + com.sendMessage(port[0], "FLUSH") + com.sendMessage(port[0], "CLOSE") + com.sendMessage(port[0], 'OPENR"' + path + '",' + port[1]) + if (com.getStatusCode(port[0]) != 0) return undefined + com.sendMessage(port[0], "READ") + if (com.getStatusCode(port[0]) >= 128) return undefined + var s = com.pullMessage(port[0]) + com.sendMessage(port[0], "FLUSH"); com.sendMessage(port[0], "CLOSE") + return s +} +bF.compile = function(args) { // COMPILE "OUT.JS" -- transpile cmdbuf to JS + if (args[1] === undefined) { + println("Usage: COMPILE \"out.js\""); return + } + if (cmdbuf.length === 0) { + println("No program loaded"); return + } + if (bS._compileImpl === undefined) { + // Lazy-load compile.js from /tbas/compile.js + var src = bF._slurpAbsolute("/tbas/compile.js") + if (src === undefined) { + println("Cannot load /tbas/compile.js") + return + } + try { eval(src) } catch (e) { + println("Failed to load compiler: " + e); return + } + if (bS._compileImpl === undefined) { + println("compile.js loaded but did not define bS._compileImpl"); return + } + } + var outpath = args[1] + // Strip surrounding quotes if any + if ((outpath.charAt(0) === '"' || outpath.charAt(0) === "'") && + outpath.charAt(outpath.length - 1) === outpath.charAt(0)) { + outpath = outpath.substring(1, outpath.length - 1) + } + // Default to .js extension if missing + if (!/\.[A-Za-z0-9]+$/.test(outpath)) outpath += ".js" + try { + var n = bS._compileImpl(outpath) + println("Wrote " + n + " bytes to " + outpath) + } catch (e) { + serial.printerr(e + "\n" + (e.stack || "")) + println("Compile error: " + e) + } +} Object.freeze(bF) if (exec_args !== undefined && exec_args[1] !== undefined) { diff --git a/assets/disk0/tbas/compile.js b/assets/disk0/tbas/compile.js new file mode 100644 index 0000000..d144569 --- /dev/null +++ b/assets/disk0/tbas/compile.js @@ -0,0 +1,564 @@ +// Terran BASIC -> JavaScript compiler +// Loaded into basic.js's context by `bF.compile`. Re-uses bF._interpretLine +// (tokeniser + elaborator + parser + pruner) verbatim and emits a self- +// contained JS program that does its work via `let bS = require("tbas")`. +// +// On load, attaches `bS._compileImpl` to the live bS object. + +;(function() { + +// ---------- helpers ---------------------------------------------------------- + +function isValidJsId(s) { + return /^[A-Z_][A-Z0-9_]*$/i.test(s) +} +function varRef(name) { + const u = String(name).toUpperCase() + return isValidJsId(u) ? `bS.__state.vars.${u}` : `bS.__state.vars[${JSON.stringify(u)}]` +} +function jsLit(v) { return JSON.stringify(v) } + +// Resolve a literal AST node down to a raw JS value at compile time. Used +// for harvesting DATA constants. Only constant-propagatable types are +// permitted; otherwise compile-time evaluation fails. +function literalValue(node) { + if (!node) return undefined + switch (node.astType) { + case "num": return Number(node.astValue) + case "string": return String(node.astValue) + case "bool": return Boolean(node.astValue) + case "null": return undefined + case "lit": return String(node.astValue) // bare identifier in DATA: keep as string + default: + throw Error("DATA: unsupported literal node type: " + node.astType) + } +} + +// Returns the maximum varIndex used at the immediate scope of a lambda body, +// hence its arity. +function lambdaArity(body) { + let maxIdx = -1 + function walk(t, level) { + if (!t || !t.astType) return + if (t.astType === "defun_args" && t.astValue[0] === level) { + if (t.astValue[1] > maxIdx) maxIdx = t.astValue[1] + } + // descend into nested usrdefun (its body lives in astValue, not leaves) + if (t.astType === "usrdefun" && t.astValue && t.astValue.astLeaves !== undefined) { + walk(t.astValue, level + 1) + } + // generic descent + if (t.astLeaves) { + for (let i = 0; i < t.astLeaves.length; i++) walk(t.astLeaves[i], level) + } + } + walk(body, 0) + return maxIdx + 1 +} + +// ---------- expression lowering --------------------------------------------- + +// `depth` tracks the number of enclosing lambdas during emission. When we +// emit a lambda we increment it; defun_args [d, i] becomes _aN_i where +// N = depth - 1 - d (the absolute lambda index of the binding scope). +function compileExpr(tree, depth) { + if (tree === undefined || tree === null) return "undefined" + + // Empty parens / wrapper node: descend into the single child + if (tree.astType === "null") { + if (tree.astLeaves && tree.astLeaves[0] !== undefined) return compileExpr(tree.astLeaves[0], depth) + return "undefined" + } + if (tree.astValue === undefined && tree.astLeaves && tree.astLeaves.length === 1) { + return compileExpr(tree.astLeaves[0], depth) + } + + switch (tree.astType) { + case "num": return String(Number(tree.astValue)) + case "string": return jsLit(String(tree.astValue)) + case "bool": return tree.astValue ? "true" : "false" + case "lit": return compileLit(tree) + case "defun_args": { + const d = tree.astValue[0], i = tree.astValue[1] + const scope = depth - 1 - d + if (scope < 0) throw Error("defun_args refers to a scope outside the program (depth=" + depth + ", d=" + d + ")") + return "_a" + scope + "_" + i + } + case "usrdefun": return compileLambdaExpr(tree, depth) + case "array": return compileArrayRef(tree, depth) + case "function": return compileFunctionExpr(tree, depth) + case "op": return compileOpExpr(tree, depth) + default: + throw Error("Cannot compile expression node of type: " + tree.astType + " (value=" + tree.astValue + ")") + } +} + +function compileLit(tree) { + const name = String(tree.astValue).toUpperCase() + // Built-in zero-arg / pass-as-value functions: when a builtin name is + // referenced as a value (e.g. assigned to a variable for later use as a + // higher-order arg), emit a JS function reference. For a plain variable + // read, emit the vars table lookup. + // Heuristic: if the name matches a builtin we know about, prefer the + // function; otherwise, vars lookup. + if (RUNTIME_BUILTINS.has(name)) { + return "bS." + (isValidJsId(name) ? name : `[${jsLit(name)}]`) + } + return varRef(name) +} + +function compileArrayRef(tree, depth) { + // tree.astValue = array variable name; tree.astLeaves = index expressions + if (!tree.astLeaves || tree.astLeaves.length === 0) { + return varRef(tree.astValue) + } + const indices = tree.astLeaves.map(l => compileExpr(l, depth)) + return `bS.__arrGet(${varRef(tree.astValue)}, [${indices.join(",")}])` +} + +function compileFunctionExpr(tree, depth) { + const name = String(tree.astValue).toUpperCase() + + if (name === "PRINT" || name === "EMIT") { + // PRINT/EMIT used as expression — emit as IIFE returning undefined + return "(" + compilePrintLike(tree, name, depth) + ", undefined)" + } + // user function call by name: (args) — when astType is "function" + // and astValue is a string that matches a variable, the parser may have + // generated this. Treat it as: invoke the var. + if (!RUNTIME_BUILTINS.has(name)) { + // Not a known builtin: treat as a user defined function call + const args = (tree.astLeaves || []).map(l => compileExpr(l, depth)) + return `bS.__runFn(${varRef(name)}, [${args.join(",")}])` + } + + const args = (tree.astLeaves || []).map(l => compileExpr(l, depth)) + return `bS.${isValidJsId(name) ? name : `[${jsLit(name)}]`}(${args.join(",")})` +} + +const ARITH_OP = { + "+": (l,r) => `bS.__add(${l},${r})`, + "-": (l,r) => `((${l})-(${r}))`, + "*": (l,r) => `((${l})*(${r}))`, + "/": (l,r) => `bS.__div(${l},${r})`, + "\\": (l,r) => `bS.__intdiv(${l},${r})`, + "MOD":(l,r) => `bS.__mod(${l},${r})`, + "^": (l,r) => `bS.__pow(${l},${r})`, + "==": (l,r) => `((${l})==(${r}))`, + "<>": (l,r) => `((${l})!=(${r}))`, + "><": (l,r) => `((${l})!=(${r}))`, + "<": (l,r) => `((${l})<(${r}))`, + ">": (l,r) => `((${l})>(${r}))`, + "<=": (l,r) => `((${l})<=(${r}))`, + "=<": (l,r) => `((${l})<=(${r}))`, + ">=": (l,r) => `((${l})>=(${r}))`, + "=>": (l,r) => `((${l})>=(${r}))`, + "AND":(l,r) => `bS.AND(${l},${r})`, + "OR": (l,r) => `bS.OR(${l},${r})`, + "<<": (l,r) => `((${l})<<(${r}))`, + ">>": (l,r) => `((${l})>>>(${r}))`, + "BAND":(l,r) => `((${l})&(${r}))`, + "BOR": (l,r) => `((${l})|(${r}))`, + "BXOR":(l,r) => `((${l})^(${r}))`, +} +const UNARY_OP = { + "UNARYMINUS": (a) => `(-(${a}))`, + "UNARYPLUS": (a) => `(+(${a}))`, + "UNARYLOGICNOT":(a) => `(!(${a}))`, + "UNARYBNOT": (a) => `(~(${a}))`, +} + +function compileOpExpr(tree, depth) { + const op = String(tree.astValue) + const leaves = tree.astLeaves || [] + + // Unary + if (UNARY_OP[op] && (leaves.length === 1 || leaves[1] === undefined)) { + return UNARY_OP[op](compileExpr(leaves[0], depth)) + } + + // Binary arithmetic / comparison / logic + if (ARITH_OP[op] && leaves.length === 2) { + return ARITH_OP[op](compileExpr(leaves[0], depth), compileExpr(leaves[1], depth)) + } + + // Generator / range + if (op === "TO" && leaves.length === 2) { + return `new bS.__ForGen(${compileExpr(leaves[0], depth)}, ${compileExpr(leaves[1], depth)}, 1)` + } + if (op === "STEP" && leaves.length === 2) { + return `bS.STEP(${compileExpr(leaves[0], depth)}, ${compileExpr(leaves[1], depth)})` + } + + // List ops + if ((op === "!" || op === "~" || op === "#") && leaves.length === 2) { + const fn = (op === "!") ? "['!']" : (op === "~") ? "['~']" : "['#']" + return `bS${fn}(${compileExpr(leaves[0], depth)}, ${compileExpr(leaves[1], depth)})` + } + + // Assignment as expression — returns the assigned value + if (op === "=" && leaves.length === 2) { + return "(" + compileAssignExpr(tree, depth) + ")" + } + if (op === "IN" && leaves.length === 2) { + // Used inside FOR/FOREACH; compileFor unwraps these. As a value, treat + // as { asgnVarName, asgnValue } so a stray IN still works. + const name = jsLit(String(leaves[0].astValue).toUpperCase()) + const rhs = compileExpr(leaves[1], depth) + return `({asgnVarName: ${name}, asgnValue: ${rhs}})` + } + + // Functional / monad ops + if ((op === ">>=" || op === ">>~" || op === "." || op === "$" || + op === "&" || op === "~<" || op === "<*>" || op === "<$>" || + op === "<~>") && leaves.length === 2) { + return `bS[${jsLit(op)}](${compileExpr(leaves[0], depth)}, ${compileExpr(leaves[1], depth)})` + } + if (op === "@" && leaves.length === 1) { + // Monad return as prefix + return `bS.MRET(${compileExpr(leaves[0], depth)})` + } + if (op === "~>") { + throw Error("Compiler: bare ~> survived prune (should be usrdefun)") + } + + throw Error("Cannot compile op '" + op + "' with " + leaves.length + " operand(s)") +} + +function compileLambdaExpr(tree, depth) { + // tree.astType === "usrdefun"; tree.astValue holds the body AST; if + // tree.astLeaves is non-empty, this is an immediate application. + const body = tree.astValue + if (!body || !body.astType) throw Error("Malformed usrdefun") + + const arity = lambdaArity(body) + const newDepth = depth + 1 + const params = [] + for (let i = 0; i < arity; i++) params.push("_a" + (newDepth - 1) + "_" + i) + const bodyJs = compileExpr(body, newDepth) + const arrow = `((${params.join(",")}) => (${bodyJs}))` + + if (tree.astLeaves && tree.astLeaves.length > 0) { + const args = tree.astLeaves.map(l => compileExpr(l, depth)) + return `${arrow}(${args.join(",")})` + } + return arrow +} + +function compileAssignExpr(tree, depth) { + // op "=" with leaves[0] as target, leaves[1] as RHS + const lhs = tree.astLeaves[0] + const rhs = compileExpr(tree.astLeaves[1], depth) + + if (lhs.astType === "lit") { + const name = String(lhs.astValue).toUpperCase() + return `(${varRef(name)} = ${rhs})` + } + // The parser emits "function" or "array" for `A(i,j) = ...` — both mean + // "store into element of A". + if (lhs.astType === "array" || lhs.astType === "function") { + const indices = lhs.astLeaves.map(l => compileExpr(l, depth)) + return `(bS.__arrSet(${varRef(lhs.astValue)}, [${indices.join(",")}], ${rhs}), ${rhs})` + } + throw Error("Cannot assign to LHS of type " + lhs.astType) +} + +// ---------- statement lowering ---------------------------------------------- + +function compilePrintLike(tree, fname, depth) { + const leaves = (tree.astLeaves || []).slice() + const seps = (tree.astSeps || []).slice() + + let suppressNewline = false + if (leaves.length > 0 && leaves[leaves.length - 1] !== undefined && + leaves[leaves.length - 1].astType === "null") { + suppressNewline = true + leaves.pop() + } + + const valueExprs = leaves.map(l => compileExpr(l, depth)) + if (suppressNewline) valueExprs.push("bS.__PRINT_NONL") + const sepArr = seps.slice(0, leaves.length - 1) + + return `bS.${fname}([${valueExprs.join(", ")}], ${jsLit(sepArr)})` +} + +function setPc(pc) { + if (pc[0] === Infinity) return "pc=[Infinity,0];" + return "pc=[" + pc[0] + "," + pc[1] + "];" +} + +function compileStatement(tree, lnum, stmt, nextPc) { + if (!tree) return setPc(nextPc) + if (tree.astType === "null" && tree.astLeaves && tree.astLeaves[0]) { + return compileStatement(tree.astLeaves[0], lnum, stmt, nextPc) + } + + const isFn = (tree.astType === "function" || tree.astType === "op") + const fname = isFn ? String(tree.astValue).toUpperCase() : null + + switch (fname) { + case "GOTO": { + const target = compileGotoTarget(tree.astLeaves[0]) + return `pc=${target};` + } + case "GOSUB": { + const target = compileGotoTarget(tree.astLeaves[0]) + return `gosubStack.push([${nextPc[0]},${nextPc[1]}]); pc=${target};` + } + case "RETURN": + return `pc=gosubStack.pop(); if(!pc) throw new Error("RETURN without GOSUB");` + case "END": + return "pc=[Infinity,0];" + case "IF": + return compileIf(tree, lnum, stmt, nextPc) + case "ON": + return compileOn(tree, lnum, stmt, nextPc) + case "FOR": + case "FOREACH": + return compileFor(tree, lnum, stmt, nextPc, fname === "FOREACH") + case "NEXT": + return compileNext(tree, lnum, stmt, nextPc) + case "READ": { + const target = tree.astLeaves[0] + if (target.astType !== "lit") throw Error("READ: target must be a variable") + return `${varRef(target.astValue)}=bS.__readData(); ${setPc(nextPc)}` + } + case "RESTORE": + return `bS.__state.dataCursor=0; ${setPc(nextPc)}` + case "DATA": + case "LABEL": + return setPc(nextPc) // harvested at compile time + case "DIM": + return compileDim(tree, lnum, stmt, nextPc) + case "PRINT": + case "EMIT": + return `${compilePrintLike(tree, fname, 0)}; ${setPc(nextPc)}` + case "OPTIONBASE": + return `bS.OPTIONBASE(${compileExpr(tree.astLeaves[0], 0)}); ${setPc(nextPc)}` + case "OPTIONDEBUG": + return `bS.OPTIONDEBUG(${compileExpr(tree.astLeaves[0], 0)}); ${setPc(nextPc)}` + case "OPTIONTRACE": + return `bS.OPTIONTRACE(${compileExpr(tree.astLeaves[0], 0)}); ${setPc(nextPc)}` + case "INPUT": { + // INPUT -> read into var + const target = tree.astLeaves[tree.astLeaves.length - 1] + if (target.astType !== "lit") throw Error("INPUT: target must be a variable") + return `${varRef(target.astValue)}=bS.INPUT(); ${setPc(nextPc)}` + } + case "=": + return `${compileAssignExpr(tree, 0)}; ${setPc(nextPc)}` + case "IN": + // bare IN as a statement is unusual but harmless + return `${compileExpr(tree, 0)}; ${setPc(nextPc)}` + case "REM": + return setPc(nextPc) + } + + // Default: evaluate as an expression for side effect, then advance + return `${compileExpr(tree, 0)}; ${setPc(nextPc)}` +} + +function compileGotoTarget(leaf) { + // Always route through __resolveTarget so non-existent line numbers snap + // upward to the next existing line — matching basic.js's main loop, + // which increments lnum until it finds a populated cmdbuf entry. + if (leaf.astType === "num") return `bS.__resolveTarget(${Number(leaf.astValue)})` + if (leaf.astType === "string") return `bS.__resolveTarget(${jsLit(leaf.astValue)})` + if (leaf.astType === "lit") { + const name = String(leaf.astValue) + return `bS.__resolveTarget(bS.__state.gotoLabels[${jsLit(name)}]!==undefined ? ${jsLit(name)} : ${varRef(name)})` + } + return `bS.__resolveTarget(${compileExpr(leaf, 0)})` +} + +function compileIf(tree, lnum, stmt, nextPc) { + const test = compileExpr(tree.astLeaves[0], 0) + const thenStmt = compileStatement(tree.astLeaves[1], lnum, stmt, nextPc) + const elseStmt = (tree.astLeaves[2]) + ? compileStatement(tree.astLeaves[2], lnum, stmt, nextPc) + : setPc(nextPc) + return `if(bS.__test(${test})){${thenStmt}}else{${elseStmt}}` +} + +function compileOn(tree, lnum, stmt, nextPc) { + // children: testExpr, jumpFnLit, target0, target1, ... + const testExpr = compileExpr(tree.astLeaves[0], 0) + const jmpFn = String(tree.astLeaves[1].astValue).toUpperCase() + const targets = tree.astLeaves.slice(2) + + const cases = targets.map((t, i) => { + const tgt = compileGotoTarget(t) + if (jmpFn === "GOSUB") { + return `case ${i}: gosubStack.push([${nextPc[0]},${nextPc[1]}]); pc=${tgt}; break;` + } + return `case ${i}: pc=${tgt}; break;` + }) + return `{const _o=(${testExpr})-bS.__state.indexBase; switch(_o){${cases.join(" ")} default: ${setPc(nextPc)}}}` +} + +function compileFor(tree, lnum, stmt, nextPc, isForEach) { + const child = tree.astLeaves[0] + if (child.astType !== "op" || (child.astValue !== "=" && child.astValue !== "IN")) { + throw Error("FOR/FOREACH: expected = or IN, got " + child.astType + ":" + child.astValue) + } + const varname = String(child.astLeaves[0].astValue).toUpperCase() + let iter = compileExpr(child.astLeaves[1], 0) + if (isForEach) { + // ensure we coerce generators into arrays for FOREACH semantics + iter = `(function(_x){return bS.__isGenerator(_x)?bS.__genToArray(_x):_x})(${iter})` + } + // Pass nextPc — the PC of the loop body's first statement — so NEXT can + // jump straight back without relying on fall-through. + return `bS.__forSetup(${jsLit(varname)}, ${iter}, ${nextPc[0]}, ${nextPc[1]}); ${setPc(nextPc)}` +} + +function compileNext(tree, lnum, stmt, nextPc) { + let argExpr = "undefined" + const leaves = tree.astLeaves || [] + if (leaves.length === 1 && leaves[0] && leaves[0].astType === "lit") { + argExpr = jsLit(String(leaves[0].astValue).toUpperCase()) + } + return `{const _n=bS.__forNext(${argExpr}); if(_n){pc=_n;}else{${setPc(nextPc)}}}` +} + +function compileDim(tree, lnum, stmt, nextPc) { + // tree.astLeaves contains array constructor calls: each leaf is either + // an `array` node OR a `function` node (the parser doesn't distinguish + // `A(5)` from a function call until runtime). astValue is the variable + // name and astLeaves are the dimension expressions. + const stmts = [] + for (let i = 0; i < tree.astLeaves.length; i++) { + const leaf = tree.astLeaves[i] + if (leaf.astType !== "array" && leaf.astType !== "function") { + throw Error("DIM: expected array decl, got " + leaf.astType) + } + const name = String(leaf.astValue).toUpperCase() + const dims = leaf.astLeaves.map(l => compileExpr(l, 0)) + stmts.push(`${varRef(name)}=bS.__dim([${dims.join(",")}]);`) + } + return stmts.join(" ") + " " + setPc(nextPc) +} + +// ---------- top-level entry -------------------------------------------------- + +// Set of builtin names exposed by tbas.mjs. Used to decide whether a `lit` +// in expression position is a variable or a function reference. +const RUNTIME_BUILTINS = new Set([ + "PRINT","EMIT","INPUT","CIN", + "ABS","SGN","INT","FLOOR","CEIL","FIX","ROUND","SQR","CBR", + "SIN","COS","TAN","ASN","ACO","ATN","SINH","COSH","TANH", + "EXP","LOG","MIN","MAX","RND", + "SPC","LEFT","RIGHT","MID","CHR", + "LEN","HEAD","TAIL","INIT","LAST","MAP","FOLD","FILTER","ARRAY", + "CLS","CLPX","PLOT","GOTOYX","TEXTFORE","TEXTBACK", + "POKE","PEEK","GETKEYSDOWN","CPUT","CGET","CSTA", + "TYPEOF","OPTIONBASE","OPTIONDEBUG","OPTIONTRACE", + "MRET","MLIST","MJOIN", + "AND","OR","NOT", + "DO","CLEAR","END","TO","STEP", + "FOR","FOREACH","NEXT","IF","ON","GOTO","GOSUB","RETURN", + "DIM","DATA","READ","RESTORE","LABEL","REM", + "TEST", +]) + +bS._compileImpl = function(outpath) { + if (typeof cmdbuf === "undefined") throw Error("compile.js: cmdbuf not available") + if (typeof bF === "undefined") throw Error("compile.js: bF not available") + if (typeof bF._interpretLine !== "function") throw Error("compile.js: bF._interpretLine not available") + + // Reset parser-side state so we don't pollute the live interpreter + if (typeof lambdaBoundVars !== "undefined") lambdaBoundVars.length = 0 + const savedPrescan = (typeof prescan !== "undefined") ? prescan : false + if (typeof prescan !== "undefined") prescan = true // suppress execution of LABEL/DATA prescan side-effects + + // ---- pass 1: parse every line ---- + const programTrees = [] // [lnum] -> array of statements + for (let lnum = 0; lnum < cmdbuf.length; lnum++) { + const linestr = cmdbuf[lnum] + if (linestr === undefined) continue + const trees = bF._interpretLine(lnum, String(linestr).trim()) + if (trees !== undefined) programTrees[lnum] = trees + } + if (typeof prescan !== "undefined") prescan = savedPrescan + + // ---- pass 2: ordered list of populated lnums and successor table ---- + const linenums = [] + for (let lnum = 0; lnum < programTrees.length; lnum++) { + if (programTrees[lnum] !== undefined) linenums.push(lnum) + } + + function nextPcOf(idx, stmtIdx) { + const lnum = linenums[idx] + const stmts = programTrees[lnum] + if (stmtIdx + 1 < stmts.length) return [lnum, stmtIdx + 1] + if (idx + 1 < linenums.length) return [linenums[idx + 1], 0] + return [Infinity, 0] + } + + // ---- pass 3: harvest DATA constants and LABEL definitions ---- + const dataConsts = [] + const labelMap = {} + for (let i = 0; i < linenums.length; i++) { + const lnum = linenums[i] + const stmts = programTrees[lnum] + for (let s = 0; s < stmts.length; s++) { + const t = stmts[s] + if (!t) continue + if (t.astValue === "DATA") { + for (let k = 0; k < t.astLeaves.length; k++) { + dataConsts.push(literalValue(t.astLeaves[k])) + } + } else if (t.astValue === "LABEL") { + const lblNode = t.astLeaves[0] + if (!lblNode) throw Error("LABEL with no name on line " + lnum) + const lblName = String(lblNode.astValue) + labelMap[lblName] = [lnum, s] + } + } + } + + // ---- pass 4: emit case bodies ---- + const cases = [] + for (let i = 0; i < linenums.length; i++) { + const lnum = linenums[i] + const stmts = programTrees[lnum] + for (let s = 0; s < stmts.length; s++) { + const next = nextPcOf(i, s) + const body = compileStatement(stmts[s], lnum, s, next) + cases.push(` case ${lnum}*32+${s}: { ${body} break; }`) + } + } + + // ---- pass 5: assemble final output ---- + const firstPc = (linenums.length > 0) ? `[${linenums[0]},0]` : `[Infinity,0]` + const labelMapJs = "{" + Object.keys(labelMap).map(k => + `${jsLit(k)}: [${labelMap[k][0]}, ${labelMap[k][1]}]` + ).join(", ") + "}" + + const out = +`// Compiled by Terran BASIC -> JS compiler (assets/disk0/tbas/compile.js) +// Source line count: ${linenums.length} +let bS = require("tbas") +bS.__reset() +bS.__data(${jsLit(dataConsts)}) +bS.__labels(${labelMapJs}) +bS.__setLines(${jsLit(linenums)}) +let pc = ${firstPc} +const gosubStack = [] +while (pc[0] !== Infinity) { + switch (pc[0]*32 + pc[1]) { +${cases.join("\n")} + default: pc = [Infinity, 0]; break; + } +} +` + + // ---- write to disk via basic.js's fs (writes under BASIC_HOME_PATH) ---- + const opened = fs.open(outpath, "W") + if (!opened) throw Error("Cannot open " + outpath + " for writing") + fs.write(out) + return out.length +} + +})(); diff --git a/assets/disk0/tvdos/include/tbas.mjs b/assets/disk0/tvdos/include/tbas.mjs new file mode 100644 index 0000000..eb9f4e1 --- /dev/null +++ b/assets/disk0/tvdos/include/tbas.mjs @@ -0,0 +1,621 @@ +// Terran BASIC runtime helper for compiled programs +// Compiled-by: assets/disk0/tbas/compile.js +// Loaded at runtime by `let bS = require("tbas")` +// +// Contract with compiler: +// - The compiler has lowered every BASIC expression to a JS expression +// that produces the *raw* JS value (number, string, array, ForGen, +// function, BasicMemoMonad, …). Builtins take such raw values, NOT +// SyntaxTreeReturnObj wrappers. +// - Variable reads: bS.__state.vars.X (key always uppercased) +// - Variable writes: bS.__state.vars.X = v +// - Control flow (GOTO/GOSUB/RETURN/FOR/NEXT/IF/ON/END/READ/RESTORE/LABEL/DATA) +// is *not* exposed here — the compiler emits inline JS that updates the +// `pc` and `gosubStack` directly. +// +// Naming: BASIC builtins exposed under their UPPERCASE name (bS.PRINT, +// bS.PLOT, bS.SIN). Compiler-only helpers prefixed with __. + +// --------------------------------------------------------------------------- +// Types & helpers +// --------------------------------------------------------------------------- + +function isNumable(s) { + if (Array.isArray(s)) return false + if (s === undefined) return false + if (typeof s.trim == "function" && s.trim().length == 0) return false + return !isNaN(s) +} +const tonum = (t) => t * 1.0 + +function ForGen(s, e, t) { + this.start = s + this.end = e + this.step = t || 1 + this.current = this.start + this.stepsgn = (this.step > 0) ? 1 : -1 +} +const isGenerator = (o) => + o !== undefined && o !== null && + o.start !== undefined && o.end !== undefined && + o.step !== undefined && o.stepsgn !== undefined +const 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 +} +const genHasNext = (o) => o.current * o.stepsgn + o.step * o.stepsgn <= (o.end + o.step) * o.stepsgn +const genGetNext = (gen, mutated) => { + if (mutated !== undefined) gen.current = tonum(mutated) + gen.current += gen.step + return genHasNext(gen) ? gen.current : undefined +} + +function BasicMemoMonad(m) { this.mType = "value"; this.mVal = m } +function BasicListMonad(m) { this.mType = "list"; this.mVal = [m] } +function BasicFunSeq(f) { this.mType = "funseq"; this.mVal = f } +const isMonad = (o) => o !== undefined && o !== null && o.mType !== undefined + +function arrayToString(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 + "}" +} + +// --------------------------------------------------------------------------- +// State container +// --------------------------------------------------------------------------- + +const _initialConsts = () => ({ + NIL: [], + PI: Math.PI, + TAU: Math.PI * 2, + EULER: Math.E, + UNDEFINED: undefined, + TRUE: true, + FALSE: false, + // ID is identity-function: emitted as JS arrow when needed + ID: (x) => x, +}) + +const state = { + vars: _initialConsts(), + indexBase: 0, + dataConsts: [], + dataCursor: 0, + gotoLabels: {}, // labelName -> [lnum, stmt] + lineList: [], // sorted ascending list of existing source lines (for GOTO snap) + rnd: Math.random(), + forVar: {}, // varname -> generator|array (the iterable we still owe to FOR/FOREACH) + forLnums: {}, // varname -> [lnum, stmt of the FOR/FOREACH header] + forStack: [], + trace: false, + debug: false, +} + +function __reset() { + state.vars = _initialConsts() + state.indexBase = 0 + state.dataConsts = [] + state.dataCursor = 0 + state.gotoLabels = {} + state.lineList = [] + state.rnd = Math.random() + state.forVar = {} + state.forLnums = {} + state.forStack = [] +} + +function __data(values) { state.dataConsts = values.slice() } +function __labels(map) { state.gotoLabels = Object.assign({}, map) } +function __setLines(arr) { state.lineList = arr.slice() } + +// --------------------------------------------------------------------------- +// Compiler-emitted operator helpers (need behaviour not directly expressible +// in raw JS without losing semantics) +// --------------------------------------------------------------------------- + +function __add(lh, rh) { + return (!isNaN(lh) && !isNaN(rh)) ? (tonum(lh) + tonum(rh)) : (lh + rh) +} +function __div(lh, rh) { if (rh == 0) throw Error("Division by zero"); return lh / rh } +function __intdiv(lh, rh) { if (rh == 0) throw Error("Division by zero"); return (lh / rh) | 0 } +function __mod(lh, rh) { if (rh == 0) throw Error("Division by zero"); return lh % rh } +function __pow(lh, rh) { + let r = Math.pow(lh, rh) + if (isNaN(r)) throw Error("Illegal function call") + if (!isFinite(r)) throw Error("Division by zero") + return r +} + +function __test(v) { return !!v } // matches builtin TEST: string "false" is truthy + +function __dim(dims) { + let revdims = dims.slice().reverse() + let inner = new Array(revdims[0]).fill(0) + for (let k = 1; k < revdims.length; k++) { + const sz = revdims[k] + const prev = inner + inner = new Array(sz).fill(0).map(_ => JSON.parse(JSON.stringify(prev))) + } + return inner +} + +function __subscriptError(idx, dim) { + return Error("Subscript out of range (index " + idx + ", dim " + dim + ")") +} +function __arrGet(arr, idx) { + let v = arr + for (let i = 0; i < idx.length; i++) { + if (v === undefined || v === null) throw __subscriptError(idx[i], i) + v = v[idx[i] - state.indexBase] + } + return v +} +function __arrSet(arr, idx, value) { + let v = arr + for (let i = 0; i < idx.length - 1; i++) { + if (v === undefined || v === null) throw __subscriptError(idx[i], i) + v = v[idx[i] - state.indexBase] + } + if (v === undefined || v === null) throw __subscriptError(idx[idx.length - 1], idx.length - 1) + v[idx[idx.length - 1] - state.indexBase] = value +} + +// FOR / FOREACH setup. Lowered as: +// __forSetup(varname, iterable, bodyLnum, bodyStmt) +// where iterable is a ForGen (FOR…TO…STEP) OR an Array (FOREACH IN…), and +// (bodyLnum, bodyStmt) is the PC of the statement immediately following the +// FOR header — i.e. where NEXT should jump back to. The compiler supplies +// this directly so the state machine doesn't rely on fall-through. +function __forSetup(varname, iterable, bodyLnum, bodyStmt) { + const v = varname.toUpperCase() + if (isGenerator(iterable)) { + state.vars[v] = iterable.start + state.forVar[v] = iterable + } else if (Array.isArray(iterable)) { + state.vars[v] = iterable[0] + state.forVar[v] = iterable.slice(1) // remainder + } else { + throw Error("FOR: not a generator or array") + } + state.forLnums[v] = [bodyLnum, bodyStmt] + state.forStack.push(v) +} + +// NEXT [varname]. Without varname, pops the most recent. +// Returns [lnum, stmt] to jump back to (just-after the FOR header) if more +// iterations remain, or undefined if the loop is exhausted (caller falls +// through). +function __forNext(varname) { + let v + if (varname === undefined || varname === null) { + v = state.forStack.pop() + } else { + v = varname.toUpperCase() + // remove this varname from the stack + const idx = state.forStack.lastIndexOf(v) + if (idx >= 0) state.forStack.splice(idx, 1) + } + if (v === undefined) throw Error("NEXT without FOR") + + const it = state.forVar[v] + let nextVal + if (isGenerator(it)) { + nextVal = genGetNext(it, state.vars[v]) + } else { + nextVal = it.shift() + } + + if (nextVal !== undefined) { + state.vars[v] = nextVal + state.forStack.push(v) + return state.forLnums[v] // already the PC of the loop body + } else { + if (isGenerator(it)) state.vars[v] = it.current + return undefined + } +} + +function __readData() { + const r = state.dataConsts[state.dataCursor++] + if (r === undefined) throw Error("Out of DATA") + return r +} + +// Resolve a GOTO/GOSUB target — accepts numeric line, label string, or +// already-evaluated expression. For numeric targets that don't match an +// existing source line, snap upward to the next one (matches the +// interpreter's behaviour, where the main loop simply increments lnum until +// it finds a populated cmdbuf entry). +function __resolveTarget(t) { + if (typeof t === "string" && state.gotoLabels[t] !== undefined) { + return state.gotoLabels[t] + } + let target + if (typeof t === "number") target = t + else if (isNumable(t)) target = tonum(t) + else throw Error("Invalid jump target: " + t) + + const lines = state.lineList + if (lines.length === 0) return [target, 0] + // linear scan is fine for the line counts BASIC programs reach + for (let i = 0; i < lines.length; i++) { + if (lines[i] >= target) return [lines[i], 0] + } + return [Infinity, 0] +} + +// Invoke a usrdefun (compiled to a JS function), or — when the parser +// couldn't tell array-indexing apart from function-call (e.g. `A(5)` for an +// unknown identifier) — index into an array. Used by MAP/FOLD/FILTER, monad +// operators, and the compiler's default `function` lowering. +function __runFn(fn, args) { + if (typeof fn === "function") return fn.apply(null, args) + if (Array.isArray(fn)) return __arrGet(fn, args) + if (isMonad(fn) && fn.mType === "funseq") { + let arg = args[0] + for (let i = 0; i < fn.mVal.length; i++) arg = __runFn(fn.mVal[i], [arg]) + return arg + } + throw Error("Not a callable: " + fn) +} + +// --------------------------------------------------------------------------- +// Operator builtins (where JS doesn't already do the right thing) +// --------------------------------------------------------------------------- + +function _AND(a, b) { if (typeof a !== "boolean" || typeof b !== "boolean") throw Error("Type mismatch"); return a && b } +function _OR (a, b) { if (typeof a !== "boolean" || typeof b !== "boolean") throw Error("Type mismatch"); return a || b } +function _NOT(a) { return !a } + +function _CONS(lh, rh) { // ! + if (Array.isArray(rh)) return [lh].concat(rh) + if (rh && rh.mType === "list") { rh.mVal = [lh].concat(rh.mVal); return rh } + throw Error("Type mismatch") +} +function _PUSH(lh, rh) { // ~ + if (Array.isArray(lh)) return lh.concat([rh]) + if (lh && lh.mType === "list") { lh.mVal = [lh.mVal].concat([rh]); return lh } + throw Error("Type mismatch") +} +function _CONCAT(lh, rh) { // # + if (Array.isArray(lh) && Array.isArray(rh)) return lh.concat(rh) + if (lh && rh && lh.mType === "list" && rh.mType === "list") return new BasicListMonad(lh.mVal.concat(rh.mVal)) + throw Error("Type mismatch") +} + +function _TO(from, to) { return new ForGen(from, to, 1) } +function _STEP(gen, step) { + if (!isGenerator(gen)) throw Error("Type mismatch (STEP)") + return new ForGen(gen.start, gen.end, step) +} + +// --------------------------------------------------------------------------- +// I/O builtins +// --------------------------------------------------------------------------- + +// PRINT(values, seps) — values: array of resolved JS values; seps: array of +// length values.length-1 with "," | ";" between each consecutive pair. +// Trailing semicolon? The compiler signals "no newline" by passing a final +// `null` element in `values` and "noNewline" flag — we use the convention +// that the LAST entry of `values` being a marker `__noNewline` suppresses +// the newline (matches basic.js trailing-null behaviour). +const __PRINT_NONL = Symbol("PRINT_NONL") +function PRINT(values, seps) { + seps = seps || [] + if (values.length === 0) { + println() + return + } + let suppressNewline = false + let realLen = values.length + if (values[realLen - 1] === __PRINT_NONL) { + suppressNewline = true + realLen -= 1 + } + for (let i = 0; i < realLen; i++) { + if (i >= 1 && seps[i - 1] === ",") print("\t") + const v = values[i] + let s + if (Array.isArray(v)) s = arrayToString(v) + else if (v === undefined || v === "") s = "" + else if (v.toString !== undefined) s = v.toString() + else s = v + print(s) + } + if (!suppressNewline) println() +} +function EMIT(values, seps) { + seps = seps || [] + if (values.length === 0) { println(); return } + let suppressNewline = false + let realLen = values.length + if (values[realLen - 1] === __PRINT_NONL) { suppressNewline = true; realLen -= 1 } + for (let i = 0; i < realLen; i++) { + if (i >= 1 && seps[i - 1] === ",") print("\t") + const v = values[i] + if (v === undefined) print("") + else if (isNumable(v)) { + const c = con.getyx() + con.addch(tonum(v)) + con.move(c[0], c[1] + 1) + } else if (v.toString !== undefined) print(v.toString()) + else print(v) + } + if (!suppressNewline) println() +} + +function INPUT(promptOrVarname) { + print("? ") + let r = sys.read().trim() + if (!isNaN(r)) r = tonum(r) + return r +} +function CIN() { return sys.read().trim() } + +// --------------------------------------------------------------------------- +// Numeric builtins +// --------------------------------------------------------------------------- + +const _num = (f) => (x) => { if (!isNumable(x)) throw Error("Type mismatch"); return f(tonum(x)) } +const _num2 = (f) => (a, b) => { + if (!isNumable(a) || !isNumable(b)) throw Error("Type mismatch") + return f(tonum(a), tonum(b)) +} + +const ABS = _num(Math.abs) +const SGN = _num(x => x > 0 ? 1 : x < 0 ? -1 : 0) +const INT = _num(Math.floor) +const FLOOR = _num(Math.floor) +const CEIL = _num(Math.ceil) +const FIX = _num(x => x | 0) +const ROUND = _num(Math.round) +const SQR = _num(Math.sqrt) +const CBR = _num(Math.cbrt) +const SIN = _num(Math.sin) +const COS = _num(Math.cos) +const TAN = _num(Math.tan) +const ASN = _num(Math.asin) +const ACO = _num(Math.acos) +const ATN = _num(Math.atan) +const SINH = _num(Math.sinh) +const COSH = _num(Math.cosh) +const TANH = _num(Math.tanh) +const EXP = _num(Math.exp) +const LOG = _num(Math.log) +const MIN = _num2((a,b) => a > b ? b : a) +const MAX = _num2((a,b) => a < b ? b : a) + +function RND(x) { + // matches basic.js:1199 — only re-roll when arg !== 0 + if (!(x === 0)) state.rnd = Math.random() + return state.rnd +} + +// --------------------------------------------------------------------------- +// String builtins +// --------------------------------------------------------------------------- + +function SPC(n) { return " ".repeat(n) } +function LEFT(s, n) { return String(s).substring(0, n) } +function RIGHT(s, n) { return String(s).substring(String(s).length - n) } +function MID(s, start, len) { return String(s).substring(start - state.indexBase, start - state.indexBase + len) } +function CHR(n) { return String.fromCharCode(n) } + +// --------------------------------------------------------------------------- +// List builtins +// --------------------------------------------------------------------------- + +function LEN(x) { if (x === undefined || x.length === undefined) throw Error("Type mismatch"); return x.length } +function HEAD(x) { if (!x || x.length < 1) throw Error("Type mismatch"); return x[0] } +function TAIL(x) { if (!x || x.length < 1) throw Error("Type mismatch"); return x.slice(1) } +function INIT(x) { if (!x || x.length < 1) throw Error("Type mismatch"); return x.slice(0, x.length - 1) } +function LAST(x) { if (!x || x.length < 1) throw Error("Type mismatch"); return x[x.length - 1] } + +function MAP(fn, functor) { + if (typeof fn !== "function" && !(isMonad(fn) && fn.mType === "funseq")) throw Error("MAP: not a function") + if (isGenerator(functor)) functor = genToArray(functor) + if (!Array.isArray(functor)) throw Error("MAP: not iterable") + return functor.map(it => __runFn(fn, [it])) +} +function FOLD(fn, init, functor) { + if (typeof fn !== "function" && !(isMonad(fn) && fn.mType === "funseq")) throw Error("FOLD: not a function") + if (isGenerator(functor)) functor = genToArray(functor) + if (!Array.isArray(functor)) throw Error("FOLD: not iterable") + let akku = init + for (let i = 0; i < functor.length; i++) akku = __runFn(fn, [akku, functor[i]]) + return akku +} +function FILTER(fn, functor) { + if (typeof fn !== "function" && !(isMonad(fn) && fn.mType === "funseq")) throw Error("FILTER: not a function") + if (isGenerator(functor)) functor = genToArray(functor) + if (!Array.isArray(functor)) throw Error("FILTER: not iterable") + return functor.filter(it => __runFn(fn, [it])) +} + +// Array literal constructor — emitted by the compiler for `[a,b,c]` syntax +function ARRAY() { return Array.prototype.slice.call(arguments) } + +// --------------------------------------------------------------------------- +// Graphics / system +// --------------------------------------------------------------------------- + +function CLS() { con.clear() } +function CLPX() { graphics.clearPixels(255) } +function PLOT(x, y, c) { graphics.plotPixel(x, y, c) } +function GOTOYX(y, x) { con.move(y + (1 - state.indexBase), x + (1 - state.indexBase)) } +function TEXTFORE(c) { print(String.fromCharCode(27, 91) + "38;5;" + (c | 0) + "m") } +function TEXTBACK(c) { print(String.fromCharCode(27, 91) + "48;5;" + (c | 0) + "m") } +function POKE(addr, v) { sys.poke(addr, v) } +function PEEK(addr) { return sys.peek(addr) } +function GETKEYSDOWN() { + const keys = [] + sys.poke(-40, 255) + for (let k = -41; k >= -48; k--) keys.push(sys.peek(k)) + return keys +} + +function CPUT(devnum, msg) { com.sendMessage(devnum, msg); return com.getStatusCode(devnum) } +function CGET(devnum, ptr) { + const msg = com.pullMessage(devnum) + const len = msg.length | 0 + for (let i = 0; i < len; i++) sys.poke(ptr + i, msg.charCodeAt(i)) + return len +} +function CSTA(devnum) { return com.getStatusCode(devnum) } + +// --------------------------------------------------------------------------- +// Type / debug +// --------------------------------------------------------------------------- + +function TYPEOF(v) { + if (v === undefined) return "null" + if (typeof v === "boolean") return "bool" + if (Array.isArray(v)) return "array" + if (isGenerator(v)) return "generator" + if (isMonad(v)) return v.mType + "-monad" + if (typeof v === "function") return "usrdefun" + if (isNumable(v)) return "num" + if (typeof v === "string") return "string" + return typeof v +} + +function OPTIONBASE(n) { + if (n != 0 && n != 1) throw Error("Syntax error: OPTIONBASE") + state.indexBase = n | 0 +} +function OPTIONDEBUG(n) { state.debug = (n | 0) === 1 } +function OPTIONTRACE(n) { state.trace = (n | 0) === 1 } + +// --------------------------------------------------------------------------- +// Monad / functional ops (best-effort port) +// --------------------------------------------------------------------------- + +function MRET(v) { return new BasicMemoMonad(v) } +function MLIST(v) { return new BasicListMonad(v) } +function MJOIN(m) { if (!isMonad(m)) throw Error("Type mismatch"); return m.mVal } + +function _BIND(ma, fn) { // >>= + if (!isMonad(ma)) throw Error(">>=: left is not a monad") + if (typeof fn !== "function") throw Error(">>=: right is not a function") + const mb = __runFn(fn, [ma.mVal]) + if (!isMonad(mb)) throw Error(">>=: function did not return a monad") + return mb +} +function _SEQ(ma, mb) { // >>~ + if (!isMonad(ma) || !isMonad(mb)) throw Error("Type mismatch") + return mb +} +function _COMPOSE(fa, fb) { // . + const ma = (typeof fa === "function") ? [fa] : fa.mVal + const mb = (typeof fb === "function") ? [fb] : fb.mVal + return new BasicFunSeq(mb.concat(ma)) +} +function _APPLY(fn, value) { // $ + return __runFn(fn, [value]) +} +function _PIPE(value, fn) { // & + return _APPLY(fn, value) +} +function _CURRY(fn, value) { // ~< + if (typeof fn !== "function") throw Error("~<: left is not a function") + return function() { + const rest = Array.prototype.slice.call(arguments) + return fn.apply(null, [value].concat(rest)) + } +} +function _SEQAPP(fns, functor) { // <*> + if (!Array.isArray(fns)) throw Error("<*>: first arg must be an array of functions") + if (isGenerator(functor)) functor = genToArray(functor) + if (!Array.isArray(functor)) throw Error("<*>: not iterable") + let ret = [] + for (let i = 0; i < fns.length; i++) ret = ret.concat(functor.map(it => __runFn(fns[i], [it]))) + return ret +} +function _SEQCURRYMAP(fns, functor) { // <~> + if (typeof fns === "function") fns = [fns] + if (!Array.isArray(fns)) throw Error("<~>: first arg must be a function or array of functions") + if (isGenerator(functor)) functor = genToArray(functor) + if (!Array.isArray(functor)) throw Error("<~>: not iterable") + let ret = [] + for (let i = 0; i < fns.length; i++) ret = ret.concat(functor.map(it => _CURRY(fns[i], it))) + return ret +} + +// --------------------------------------------------------------------------- +// Exports +// --------------------------------------------------------------------------- + +exports = { + // state & introspection + __state: state, __reset, __data, __labels, __setLines, + __PRINT_NONL, + + // operator helpers + __add, __div, __intdiv, __mod, __pow, __test, + __dim, __arrGet, __arrSet, + __forSetup, __forNext, __readData, __resolveTarget, + __runFn, + + // type ctors + __ForGen: ForGen, __isGenerator: isGenerator, __genToArray: genToArray, + __isMonad: isMonad, + + // operators + AND: _AND, OR: _OR, NOT: _NOT, + UNARYLOGICNOT: _NOT, + UNARYBNOT: (a) => ~a, + UNARYMINUS: (a) => -a, + UNARYPLUS: (a) => +a, + BAND: (a,b)=>a&b, BOR: (a,b)=>a|b, BXOR: (a,b)=>a^b, + "<<": (a,b)=>a<>": (a,b)=>a>>>b, + "!": _CONS, "~": _PUSH, "#": _CONCAT, + TO: _TO, STEP: _STEP, + + // i/o + PRINT, EMIT, INPUT, CIN, + + // numeric + ABS, SGN, INT, FLOOR, CEIL, FIX, ROUND, SQR, CBR, + SIN, COS, TAN, ASN, ACO, ATN, SINH, COSH, TANH, + EXP, LOG, MIN, MAX, RND, + + // strings + SPC, LEFT, RIGHT, MID, CHR, + + // lists + LEN, HEAD, TAIL, INIT, LAST, MAP, FOLD, FILTER, + ARRAY, + + // graphics / system + CLS, CLPX, PLOT, GOTOYX, TEXTFORE, TEXTBACK, + POKE, PEEK, GETKEYSDOWN, CPUT, CGET, CSTA, + + // type / option + TYPEOF, OPTIONBASE, OPTIONDEBUG, OPTIONTRACE, + + // monads / functional + MRET, MLIST, MJOIN, + ">>=": _BIND, ">>~": _SEQ, + ".": _COMPOSE, "$": _APPLY, "&": _PIPE, "~<": _CURRY, + "<*>": _SEQAPP, "<$>": MAP, "<~>": _SEQCURRYMAP, + + // misc + DO: function() { return arguments[arguments.length - 1] }, + CLEAR: function() { state.vars = _initialConsts() }, + END: function() { /* compiler emits pc=[Infinity,0] */ }, + LABEL: function() { /* harvested at compile time */ }, + DATA: function() { /* harvested at compile time */ }, + // DIM as an expression (e.g. `WS = DIM(H, V)`): allocate and return a + // freshly zero-filled N-D array. The statement form `DIM A(H, V)` is + // compiled inline and never reaches this entry. + DIM: function() { return __dim(Array.prototype.slice.call(arguments)) }, +}