diff --git a/snap_stable.html b/snap_stable.html new file mode 100755 index 00000000..0cc006b0 --- /dev/null +++ b/snap_stable.html @@ -0,0 +1,44 @@ + + + + + Snap stable! Build Your Own Blocks 6.0.0 - beta - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/threads.js b/src/threads.js index 9d6606c5..ef28d022 100644 --- a/src/threads.js +++ b/src/threads.js @@ -3540,21 +3540,19 @@ Process.prototype.reportTypeOf = function (thing) { // Process math primtives - hyper-dyadic -Process.prototype.hyperDyadic = function (baseOp, a, b, a_info, b_info) { +Process.prototype.hyperDyadic = function (baseOp, a, b) { // enable dyadic operations to be performed on lists and tables - var len, i, result; + var len, a_info, b_info, i, result; if (this.enableHyperOps) { - a_info = a_info || this.examine(a); - b_info = b_info || this.examine(b); + a_info = this.examine(a); + b_info = this.examine(b); if (a_info.isScalar && b_info.isScalar && (a_info.rank !== b_info.rank)) { // keep the shape of the higher rank return this.hyperZip( baseOp, a_info.rank > b_info.rank ? a : a_info.scalar, - b_info.rank > a_info.rank ? b : b_info.scalar, - a_info.rank > b_info.rank ? a_info : null, - b_info.rank > a_info.rank ? b_info : null + b_info.rank > a_info.rank ? b : b_info.scalar ); } if (a_info.rank > 1) { @@ -3562,22 +3560,10 @@ Process.prototype.hyperDyadic = function (baseOp, a, b, a_info, b_info) { if (a.length() !== b.length()) { // test for special cased scalars in single-item lists if (a_info.isScalar) { - return this.hyperDyadic( - baseOp, - a_info.scalar, - b, - null, - b_info - ); + return this.hyperDyadic(baseOp, a_info.scalar, b); } if (b_info.isScalar) { - return this.hyperDyadic( - baseOp, - a, - b_info.scalar, - a_info, - null - ); + return this.hyperDyadic(baseOp, a, b_info.scalar); } } // zip both arguments ignoring out-of-bounds indices @@ -3591,71 +3577,35 @@ Process.prototype.hyperDyadic = function (baseOp, a, b, a_info, b_info) { return new List(result); } if (a_info.isScalar) { - return this.hyperZip( - baseOp, - a_info.scalar, - b, - null, - b_info - ); + return this.hyperZip(baseOp, a_info.scalar, b); } - return a.map(each => this.hyperDyadic( - baseOp, - each, - b, - null, - b_info - )); + return a.map(each => this.hyperDyadic(baseOp, each, b)); } if (b_info.rank > 1) { if (b_info.isScalar) { - return this.hyperZip( - baseOp, - a, - b_info.scalar, - a_info, - null - ); + return this.hyperZip(baseOp, a, b_info.scalar); } - return b.map(each => this.hyperDyadic( - baseOp, - a, - each, - a_info, - null - )); + return b.map(each => this.hyperDyadic(baseOp, a, each)); } - return this.hyperZip(baseOp, a, b, a_info, b_info); + return this.hyperZip(baseOp, a, b); } return baseOp(a, b); }; -Process.prototype.hyperZip = function (baseOp, a, b, a_info, b_info) { +Process.prototype.hyperZip = function (baseOp, a, b) { // enable dyadic operations to be performed on lists and tables - var len, i, result; - a_info = a_info || this.examine(a); - b_info = b_info || this.examine(b); + var len, i, result, + a_info = this.examine(a), + b_info = this.examine(b); if (a instanceof List) { if (b instanceof List) { if (a.length() !== b.length()) { // test for special cased scalars in single-item lists if (a_info.isScalar) { - return this.hyperZip( - baseOp, - a_info.scalar, - b, - null, - b_info - ); + return this.hyperZip(baseOp, a_info.scalar, b); } if (b_info.isScalar) { - return this.hyperZip( - baseOp, - a, - b_info.scalar, - a_info, - null - ); + return this.hyperZip(baseOp, a, b_info.scalar); } } // zip both arguments ignoring out-of-bounds indices @@ -3668,22 +3618,10 @@ Process.prototype.hyperZip = function (baseOp, a, b, a_info, b_info) { } return new List(result); } - return a.map(each => this.hyperZip( - baseOp, - each, - b, - null, - b_info - )); + return a.map(each => this.hyperZip(baseOp, each, b)); } if (b instanceof List) { - return b.map(each => this.hyperZip( - baseOp, - a, - each, - a_info, - null - )); + return b.map(each => this.hyperZip(baseOp, a, each)); } return baseOp(a, b); }; diff --git a/src/threads_uncached.js b/src/threads_uncached.js deleted file mode 100644 index ef28d022..00000000 --- a/src/threads_uncached.js +++ /dev/null @@ -1,6756 +0,0 @@ -/* - - threads.js - - a tail call optimized blocks-based programming language interpreter - based on morphic.js and blocks.js - inspired by Scratch, Scheme and Squeak - - written by Jens Mönig - jens@moenig.org - - Copyright (C) 2020 by Jens Mönig - - This file is part of Snap!. - - Snap! is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of - the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - - - prerequisites: - -------------- - needs blocks.js and objects.js - - - toc - --- - the following list shows the order in which all constructors are - defined. Use this list to locate code in this document: - - ThreadManager - Process - Context - Variable - VariableFrame - JSCompiler - - credits - ------- - John Maloney and Dave Feinberg designed the original Scratch evaluator - Ivan Motyashov contributed initial porting from Squeak - -*/ - -// Global stuff //////////////////////////////////////////////////////// - -/*global ArgMorph, BlockMorph, CommandBlockMorph, CommandSlotMorph, Morph, ZERO, -MultiArgMorph, Point, ReporterBlockMorph, SyntaxElementMorph, contains, Costume, -degrees, detect, nop, radians, ReporterSlotMorph, CSlotMorph, RingMorph, Sound, -IDE_Morph, ArgLabelMorph, localize, XML_Element, hex_sha512, TableDialogMorph, -StageMorph, SpriteMorph, StagePrompterMorph, Note, modules, isString, copy, Map, -isNil, WatcherMorph, List, ListWatcherMorph, alert, console, TableMorph, Color, -TableFrameMorph, ColorSlotMorph, isSnapObject, newCanvas, Symbol, SVG_Costume*/ - -modules.threads = '2020-June-22'; - -var ThreadManager; -var Process; -var Context; -var Variable; -var VariableFrame; -var JSCompiler; - -function snapEquals(a, b) { - if (a instanceof List || (b instanceof List)) { - if (a instanceof List && (b instanceof List)) { - return a.equalTo(b); - } - return false; - } - - var x = +a, - y = +b, - i, - specials = [true, false, '']; - - // "zum Schneckengang verdorben, was Adlerflug geworden wäre" - // collecting edge-cases that somebody complained about - // on Github. Folks, take it easy and keep it fun, okay? - // Shit like this is patently ugly and slows Snap down. Tnx! - for (i = 9; i <= 13; i += 1) { - specials.push(String.fromCharCode(i)); - } - specials.push(String.fromCharCode(160)); - - // check for special values before coercing to numbers - if (isNaN(x) || isNaN(y) || - [a, b].some(any => contains(specials, any) || - (isString(any) && (any.indexOf(' ') > -1))) - ) { - x = a; - y = b; - } - - // handle text comparison case-insensitive. - if (isString(x) && isString(y)) { - return x.toLowerCase() === y.toLowerCase(); - } - - return x === y; -} - -function invoke( - action, // a BlockMorph or a Context, a reified ("ringified") block - contextArgs, // optional List of arguments for the context, or null - receiver, // sprite or environment, optional for contexts - timeout, // msecs - timeoutErrorMsg, // string - suppressErrors, // bool - callerProcess, // optional for JS-functions - returnContext // bool -) { - // execute the given block or context synchronously without yielding. - // Apply context (not a block) to a list of optional arguments. - // Receiver (sprite, stage or environment), timeout etc. are optional. - // If a timeout (in milliseconds) is specified, abort execution - // after the timeout has been reached and throw an error. - // SuppressErrors (bool) if non-timeout errors occurring in the - // block are handled elsewhere. - // This is highly experimental. - // Caution: Kids, do not try this at home! - // Use ThreadManager::startProcess with a callback instead - - var proc = new Process(), - deadline = (timeout ? Date.now() + timeout : null); - - if (action instanceof Context) { - if (receiver) { // optional - action = proc.reportContextFor(receiver); - } - proc.initializeFor(action, contextArgs || new List()); - } else if (action instanceof BlockMorph) { - proc.topBlock = action; - if (receiver) { - proc.homeContext = new Context(); - proc.homeContext.receiver = receiver; - if (receiver.variables) { - proc.homeContext.variables.parentFrame = receiver.variables; - } - } else { - throw new Error('expecting a receiver but getting ' + receiver); - } - proc.context = new Context( - null, - action.blockSequence(), - proc.homeContext - ); - } else if (action.evaluate) { - return action.evaluate(); - } else if (action instanceof Function) { - return action.apply( - receiver, - contextArgs.asArray().concat(callerProcess) - ); - } else { - throw new Error('expecting a block or ring but getting ' + action); - } - if (suppressErrors) { - proc.isCatchingErrors = false; - } - while (proc.isRunning()) { - if (deadline && (Date.now() > deadline)) { - throw (new Error( - localize( - timeoutErrorMsg || - "a synchronous Snap! script has timed out") - ) - ); - } - proc.runStep(deadline); - } - return returnContext ? proc.homeContext : proc.homeContext.inputs[0]; -} - -// ThreadManager /////////////////////////////////////////////////////// - -function ThreadManager() { - this.processes = []; - this.wantsToPause = false; // single stepping support -} - -ThreadManager.prototype.pauseCustomHatBlocks = false; - -ThreadManager.prototype.toggleProcess = function (block, receiver) { - var active = this.findProcess(block, receiver); - if (active) { - active.stop(); - } else { - return this.startProcess(block, receiver, null, null, null, true); - } -}; - -ThreadManager.prototype.startProcess = function ( - block, - receiver, - isThreadSafe, - exportResult, // bool - callback, - isClicked, - rightAway, - atomic, // special option used (only) for "onStop" scripts - variables // optional variable frame, used for WHEN hats -) { - var top = block.topBlock(), - active = this.findProcess(top, receiver), - glow, - newProc; - if (active) { - if (isThreadSafe) { - return active; - } - active.stop(); - active.canBroadcast = true; // broadcasts to fire despite reentrancy - this.removeTerminatedProcesses(); - } - newProc = new Process(top, receiver, callback, isClicked); - newProc.exportResult = exportResult; - newProc.isClicked = isClicked || false; - newProc.isAtomic = atomic || false; - - // in case an optional variable frame has been passed, - // copy it into the new outer context. - // Relevance: When a predicate inside a generic WHEN hat block - // publishes an upvar, this code makes the upvar accessible - // to the script attached to the WHEN hat - if (variables instanceof VariableFrame) { - Object.keys(variables.vars).forEach(vName => - newProc.context.outerContext.variables.vars[vName] = - variables.vars[vName] - ); - } - - // show a highlight around the running stack - // if there are more than one active processes - // for a block, display the thread count - // next to it - glow = top.getHighlight(); - if (glow) { - glow.threadCount = this.processesForBlock(top).length + 1; - glow.updateReadout(); - } else { - top.addHighlight(); - } - - this.processes.push(newProc); - if (rightAway) { - newProc.runStep(); - } - return newProc; -}; - -ThreadManager.prototype.stopAll = function (excpt) { - // excpt is optional - this.processes.forEach(proc => { - if (proc !== excpt) { - proc.stop(); - } - }); -}; - -ThreadManager.prototype.stopAllForReceiver = function (rcvr, excpt) { - // excpt is optional - this.processes.forEach(proc => { - if (proc.homeContext.receiver === rcvr && proc !== excpt) { - proc.stop(); - if (rcvr.isTemporary) { - proc.isDead = true; - } - } - }); -}; - -ThreadManager.prototype.stopAllForBlock = function (aTopBlock) { - this.processesForBlock(aTopBlock, true).forEach(proc => - proc.stop() - ); -}; - -ThreadManager.prototype.stopProcess = function (block, receiver) { - var active = this.findProcess(block, receiver); - if (active) { - active.stop(); - } -}; - -ThreadManager.prototype.pauseAll = function (stage) { - this.processes.forEach(proc => proc.pause()); - if (stage) { - stage.pauseAllActiveSounds(); - } -}; - -ThreadManager.prototype.isPaused = function () { - return detect( - this.processes, - proc => proc.isPaused - ) !== null; -}; - -ThreadManager.prototype.resumeAll = function (stage) { - this.processes.forEach(proc => proc.resume()); - if (stage) { - stage.resumeAllActiveSounds(); - } -}; - -ThreadManager.prototype.step = function () { - // run each process until it gives up control, skipping processes - // for sprites that are currently picked up, then filter out any - // processes that have been terminated - - var isInterrupted; - if (Process.prototype.enableSingleStepping) { - this.processes.forEach(proc => { - if (proc.isInterrupted) { - proc.runStep(); - isInterrupted = true; - } else { - proc.lastYield = Date.now(); - } - }); - this.wantsToPause = (Process.prototype.flashTime > 0.5); - if (isInterrupted) { - if (this.wantsToPause) { - this.pauseAll(); - } - return; - } - } - - this.processes.forEach(proc => { - if (!proc.homeContext.receiver.isPickedUp() && !proc.isDead) { - proc.runStep(); - } - }); - this.removeTerminatedProcesses(); -}; - -ThreadManager.prototype.removeTerminatedProcesses = function () { - // and un-highlight their scripts - var remaining = [], - count; - this.processes.forEach(proc => { - var result, - glow; - if ((!proc.isRunning() && !proc.errorFlag) || proc.isDead) { - if (proc.topBlock instanceof BlockMorph) { - proc.unflash(); - // adjust the thread count indicator, if any - count = this.processesForBlock(proc.topBlock).length; - if (count) { - glow = proc.topBlock.getHighlight() || - proc.topBlock.addHighlight(); - glow.threadCount = count; - glow.updateReadout(); - } else { - proc.topBlock.removeHighlight(); - } - } - if (proc.prompter) { - proc.prompter.destroy(); - if (proc.homeContext.receiver.stopTalking) { - proc.homeContext.receiver.stopTalking(); - } - } - if (proc.topBlock instanceof ReporterBlockMorph || - proc.isShowingResult) { - result = proc.homeContext.inputs[0]; - if (proc.onComplete instanceof Function) { - proc.onComplete(result); - } else { - if (result instanceof List) { - proc.topBlock.showBubble( - result.isTable() ? - new TableFrameMorph( - new TableMorph(result, 10) - ) - : new ListWatcherMorph(result), - proc.exportResult, - proc.receiver - ); - } else { - proc.topBlock.showBubble( - result, - proc.exportResult, - proc.receiver - ); - } - } - } else if (proc.onComplete instanceof Function) { - proc.onComplete(); - } - } else { - remaining.push(proc); - } - }); - this.processes = remaining; -}; - -ThreadManager.prototype.findProcess = function (block, receiver) { - var top = block.topBlock(); - return detect( - this.processes, - each => each.topBlock === top && (each.receiver === receiver) - ); -}; - -ThreadManager.prototype.processesForBlock = function (block, only) { - var top = only ? block : block.topBlock(); - return this.processes.filter(each => - each.topBlock === top && - each.isRunning() && - !each.isDead - ); -}; - -ThreadManager.prototype.doWhen = function (block, receiver, stopIt) { - if (this.pauseCustomHatBlocks) {return; } - if ((!block) || this.findProcess(block, receiver)) { - return; - } - var pred = block.inputs()[0], world, test; - if (block.removeHighlight()) { - world = block.world(); - if (world) { - world.hand.destroyTemporaries(); - } - } - if (stopIt) {return; } - try { - test = invoke( - pred, - null, - receiver, - 50, // timeout in msecs - 'the predicate takes\ntoo long for a\ncustom hat block', - true, // suppress errors => handle them right here instead - null, // caller process for JS-functions - true // return the whole home context instead of just he result - ); - } catch (error) { - block.addErrorHighlight(); - block.showBubble( - error.name - + '\n' - + error.message - ); - } - // since we're asking for the whole context instead of just the result - // of the computation, we need to look at the result-context's first - // input to find out whether the condition is met - if (test === true || (test && test.inputs && test.inputs[0] === true)) { - this.startProcess( - block, - receiver, - null, // isThreadSafe - null, // exportResult - null, // callback - null, // isClicked - true, // rightAway - null, // atomic - test.variables // make the test-context's variables available - ); - } -}; - -ThreadManager.prototype.toggleSingleStepping = function () { - Process.prototype.enableSingleStepping = - !Process.prototype.enableSingleStepping; - if (!Process.prototype.enableSingleStepping) { - this.processes.forEach(proc => { - if (!proc.isPaused) { - proc.unflash(); - } - }); - } -}; - -// Process ///////////////////////////////////////////////////////////// - -/* - A Process is what brings a stack of blocks to life. The process - keeps track of which block to run next, evaluates block arguments, - handles control structures, and so forth. - - The ThreadManager is the (passive) scheduler, telling each process - when to run by calling its runStep() method. The runStep() method - will execute some number of blocks, then voluntarily yield control - so that the ThreadManager can run another process. - - The Scratch etiquette is that a process should yield control at the - end of every loop iteration, and while it is running a timed command - (e.g. "wait 5 secs") or a synchronous command (e.g. "broadcast xxx - and wait"). Since Snap also has lambda and custom blocks Snap adds - yields at the beginning of each non-atomic custom command block - execution, and - to let users escape infinite loops and recursion - - whenever the process runs into a timeout. - - a Process runs for a receiver, i.e. a sprite or the stage or any - blocks-scriptable object that we'll introduce. - - structure: - - topBlock the stack's first block, of which all others - are children - receiver object (sprite) to which the process applies, - cached from the top block - instrument musical instrument type, cached from the receiver, - so a single sprite can play several instruments - at once - context the Context describing the current state - of this process - homeContext stores information relevant to the whole process, - i.e. its receiver, result etc. - isPaused boolean indicating whether to pause - readyToYield boolean indicating whether to yield control to - another process - readyToTerminate boolean indicating whether the stop method has - been called - isDead boolean indicating a terminated clone process - timeout msecs after which to force yield - lastYield msecs when the process last yielded - isFirstStep boolean indicating whether on first step - for clones - errorFlag boolean indicating whether an error was encountered - prompter active instance of StagePrompterMorph - httpRequest active instance of an HttpRequest or null - pauseOffset msecs between the start of an interpolated operation - and when the process was paused - isClicked boolean flag indicating whether the process was - initiated by a user-click on a block - isShowingResult boolean flag indicating whether a "report" command - has been executed in a user-clicked process - exportResult boolean flag indicating whether a picture of the top - block along with the result bubble shoud be exported - onComplete an optional callback function to be executed when - the process is done - procedureCount number counting procedure call entries, - used to tag custom block calls, so "stop block" - invocations can catch them - flashingContext for single stepping - isInterrupted boolean, indicates intra-step flashing of blocks - canBroadcast boolean, used to control reentrancy & "when stopped" -*/ - -Process.prototype = {}; -Process.prototype.constructor = Process; -Process.prototype.timeout = 500; // msecs after which to force yield -Process.prototype.isCatchingErrors = true; -Process.prototype.enableHyperOps = true; // experimental hyper operations -Process.prototype.enableLiveCoding = false; // experimental -Process.prototype.enableSingleStepping = false; // experimental -Process.prototype.enableCompiling = false; // experimental -Process.prototype.flashTime = 0; // experimental -// Process.prototype.enableJS = false; - -function Process(topBlock, receiver, onComplete, yieldFirst) { - this.topBlock = topBlock || null; - this.receiver = receiver; - this.instrument = receiver ? receiver.instrument : null; - this.readyToYield = false; - this.readyToTerminate = false; - this.isDead = false; - this.isClicked = false; - this.isShowingResult = false; - this.errorFlag = false; - this.context = null; - this.homeContext = new Context(null, null, null, receiver); - this.lastYield = Date.now(); - this.isFirstStep = true; - this.isAtomic = false; - this.prompter = null; - this.httpRequest = null; - this.isPaused = false; - this.pauseOffset = null; - this.frameCount = 0; - this.exportResult = false; - this.onComplete = onComplete || null; - this.procedureCount = 0; - this.flashingContext = null; // experimental, for single-stepping - this.isInterrupted = false; // experimental, for single-stepping - this.canBroadcast = true; // used to control "when I am stopped" - - if (topBlock) { - this.homeContext.variables.parentFrame = - this.homeContext.receiver.variables; - this.context = new Context( - null, - topBlock.blockSequence(), - this.homeContext - ); - if (yieldFirst) { - this.pushContext('doYield'); // highlight top block - } - } -} - -// Process accessing - -Process.prototype.isRunning = function () { - return (this.context !== null) && (!this.readyToTerminate); -}; - -// Process entry points - -Process.prototype.runStep = function (deadline) { - // a step is an an uninterruptable 'atom', it can consist - // of several contexts, even of several blocks - - if (this.isPaused) { // allow pausing in between atomic steps: - return this.pauseStep(); - } - this.readyToYield = false; - this.isInterrupted = false; - - while (!this.readyToYield && !this.isInterrupted - && this.context - && (Date.now() - this.lastYield < this.timeout) - ) { - // also allow pausing inside atomic steps - for PAUSE block primitive: - if (this.isPaused) { - return this.pauseStep(); - } - if (deadline && (Date.now() > deadline)) { - if (this.isAtomic && - this.homeContext.receiver && - this.homeContext.receiver.endWarp) { - this.homeContext.receiver.endWarp(); - } - return; - } - this.evaluateContext(); - } - - this.lastYield = Date.now(); - this.isFirstStep = false; - - // make sure to redraw atomic things - if (this.isAtomic && - this.homeContext.receiver && - this.homeContext.receiver.endWarp) { - this.homeContext.receiver.endWarp(); - this.homeContext.receiver.startWarp(); - } - - if (this.readyToTerminate) { - while (this.context) { - this.popContext(); - } - if (this.homeContext.receiver) { - if (this.homeContext.receiver.endWarp) { - // pen optimization - this.homeContext.receiver.endWarp(); - } - } - } -}; - -Process.prototype.stop = function () { - this.readyToYield = true; - this.readyToTerminate = true; - this.errorFlag = false; - if (this.context) { - this.context.stopMusic(); - } - this.canBroadcast = false; -}; - -Process.prototype.pause = function () { - if (this.readyToTerminate) { - return; - } - this.isPaused = true; - this.flashPausedContext(); - if (this.context && this.context.startTime) { - this.pauseOffset = Date.now() - this.context.startTime; - } -}; - -Process.prototype.resume = function () { - if (!this.enableSingleStepping) { - this.unflash(); - } - this.isPaused = false; - this.pauseOffset = null; -}; - -Process.prototype.pauseStep = function () { - this.lastYield = Date.now(); - if (this.context && this.context.startTime) { - this.context.startTime = this.lastYield - this.pauseOffset; - } -}; - -// Process evaluation - -Process.prototype.evaluateContext = function () { - var exp = this.context.expression; - this.frameCount += 1; - if (this.context.tag === 'exit') { - this.expectReport(); - } - if (exp instanceof Array) { - return this.evaluateSequence(exp); - } - if (exp instanceof MultiArgMorph) { - return this.evaluateMultiSlot(exp, exp.inputs().length); - } - if (exp instanceof ArgLabelMorph) { - return this.evaluateArgLabel(exp); - } - if (exp instanceof ArgMorph || exp.bindingID) { - return this.evaluateInput(exp); - } - if (exp instanceof BlockMorph) { - return this.evaluateBlock(exp, exp.inputs().length); - } - if (isString(exp)) { - return this[exp].apply(this, this.context.inputs); - } - if (exp instanceof Variable) { // special case for empty reporter rings - this.returnValueToParentContext(exp.value); - } - this.popContext(); // default: just ignore it -}; - -Process.prototype.evaluateBlock = function (block, argCount) { - var rcvr, inputs, - selector = block.selector; - - // check for special forms - if (selector === 'reportOr' || - selector === 'reportAnd' || - selector === 'reportIfElse' || - selector === 'doReport') { - if (this.isCatchingErrors) { - try { - return this[selector](block); - } catch (error) { - this.handleError(error, block); - } - } else { - return this[selector](block); - } - } - - // first evaluate all inputs, then apply the primitive - rcvr = this.context.receiver || this.receiver; - inputs = this.context.inputs; - - if (argCount > inputs.length) { - this.evaluateNextInput(block); - } else { - if (this.flashContext()) {return; } // yield to flash the block - if (this[selector]) { - rcvr = this; - } - if (this.isCatchingErrors) { - try { - this.returnValueToParentContext( - rcvr[selector].apply(rcvr, inputs) - ); - this.popContext(); - } catch (error) { - this.handleError(error, block); - } - } else { - this.returnValueToParentContext( - rcvr[selector].apply(rcvr, inputs) - ); - this.popContext(); - } - } -}; - -// Process: Special Forms Blocks Primitives - -Process.prototype.reportOr = function (block) { - var inputs = this.context.inputs; - - if (inputs.length < 1) { - this.evaluateNextInput(block); - } else if (inputs.length === 1) { - // this.assertType(inputs[0], 'Boolean'); - if (inputs[0]) { - if (this.flashContext()) {return; } - this.returnValueToParentContext(true); - this.popContext(); - } else { - this.evaluateNextInput(block); - } - } else { - // this.assertType(inputs[1], 'Boolean'); - if (this.flashContext()) {return; } - this.returnValueToParentContext(inputs[1] === true); - this.popContext(); - } -}; - -Process.prototype.reportAnd = function (block) { - var inputs = this.context.inputs; - - if (inputs.length < 1) { - this.evaluateNextInput(block); - } else if (inputs.length === 1) { - // this.assertType(inputs[0], 'Boolean'); - if (!inputs[0]) { - if (this.flashContext()) {return; } - this.returnValueToParentContext(false); - this.popContext(); - } else { - this.evaluateNextInput(block); - } - } else { - // this.assertType(inputs[1], 'Boolean'); - if (this.flashContext()) {return; } - this.returnValueToParentContext(inputs[1] === true); - this.popContext(); - } -}; - -Process.prototype.doReport = function (block) { - var outer = this.context.outerContext; - if (this.flashContext()) {return; } // flash the block here, special form - if (this.isClicked && (block.topBlock() === this.topBlock)) { - this.isShowingResult = true; - } - if (block.partOfCustomCommand) { - this.doStopCustomBlock(); - this.popContext(); - } else { - while (this.context && this.context.tag !== 'exit') { - if (this.context.expression === 'doStopWarping') { - this.doStopWarping(); - } else { - this.popContext(); - } - } - if (this.context) { - if (this.context.expression === 'expectReport') { - // pop off inserted top-level exit context - this.popContext(); - } else { - // un-tag and preserve original caller - this.context.tag = null; - } - } - } - // in any case evaluate (and ignore) - // the input, because it could be - // an HTTP Request for a hardware extension - this.pushContext(block.inputs()[0], outer); - this.context.isCustomCommand = block.partOfCustomCommand; -}; - -// Process: Non-Block evaluation - -Process.prototype.evaluateMultiSlot = function (multiSlot, argCount) { - // first evaluate all subslots, then return a list of their values - var inputs = this.context.inputs, - ans; - if (multiSlot.bindingID) { - if (this.isCatchingErrors) { - try { - ans = this.context.variables.getVar(multiSlot.bindingID); - } catch (error) { - this.handleError(error, multiSlot); - } - } else { - ans = this.context.variables.getVar(multiSlot.bindingID); - } - this.returnValueToParentContext(ans); - this.popContext(); - } else { - if (argCount > inputs.length) { - this.evaluateNextInput(multiSlot); - } else { - this.returnValueToParentContext(new List(inputs)); - this.popContext(); - } - } -}; - -Process.prototype.evaluateArgLabel = function (argLabel) { - // perform the ID function on an ArgLabelMorph element - var inputs = this.context.inputs; - if (inputs.length < 1) { - this.evaluateNextInput(argLabel); - } else { - this.returnValueToParentContext(inputs[0]); - this.popContext(); - } -}; - -Process.prototype.evaluateInput = function (input) { - // evaluate the input unless it is bound to an implicit parameter - var ans; - if (this.flashContext()) {return; } // yield to flash the current argMorph - if (input.bindingID) { - if (this.isCatchingErrors) { - try { - ans = this.context.variables.getVar(input.bindingID); - } catch (error) { - this.handleError(error, input); - } - } else { - ans = this.context.variables.getVar(input.bindingID); - } - } else { - ans = input.evaluate(); - if (ans) { - if (input.constructor === CommandSlotMorph || - input.constructor === ReporterSlotMorph || - (input instanceof CSlotMorph && - (!input.isStatic || input.isLambda))) { - // I know, this still needs yet to be done right.... - ans = this.reify(ans, new List()); - } - } - } - this.returnValueToParentContext(ans); - this.popContext(); -}; - -Process.prototype.evaluateSequence = function (arr) { - var pc = this.context.pc, - outer = this.context.outerContext, - isCustomBlock = this.context.isCustomBlock; - if (pc === (arr.length - 1)) { // tail call elimination - this.context = new Context( - this.context.parentContext, - arr[pc], - this.context.outerContext, - this.context.receiver - ); - this.context.isCustomBlock = isCustomBlock; - } else { - if (pc >= arr.length) { - this.popContext(); - } else { - this.context.pc += 1; - this.pushContext(arr[pc], outer); - } - } -}; - -/* -// version w/o tail call optimization: --------------------------------------- -Caution: we cannot just revert to this version of the method, because to make -tail call elimination work many tweaks had to be done to various primitives. -For the most part these tweaks are about schlepping the outer context (for -the variable bindings) and the isCustomBlock flag along, and are indicated -by a short comment in the code. But to really revert would take a good measure -of trial and error as well as debugging. In the developers file archive there -is a version of threads.js dated 120119(2) which basically resembles the -last version before introducing tail call optimization on 120123. - -Process.prototype.evaluateSequence = function (arr) { - var pc = this.context.pc; - if (pc >= arr.length) { - this.popContext(); - } else { - this.context.pc += 1; - this.pushContext(arr[pc]); - } -}; -*/ - -Process.prototype.evaluateNextInput = function (element) { - var nxt = this.context.inputs.length, - args = element.inputs(), - exp = args[nxt], - sel = this.context.expression.selector, - outer = this.context.outerContext; // for tail call elimination - - if (exp.isUnevaluated) { - if (exp.isUnevaluated === true || exp.isUnevaluated()) { - // just return the input as-is - /* - Note: we only reify the input here, if it's not an - input to a reification primitive itself (THE BLOCK, - THE SCRIPT), because those allow for additional - explicit parameter bindings. - */ - if (sel === 'reify' || sel === 'reportScript') { - this.context.addInput(exp); - } else { - this.context.addInput(this.reify(exp, new List())); - } - } else { - this.pushContext(exp, outer); - } - } else { - this.pushContext(exp, outer); - } -}; - -Process.prototype.doYield = function () { - this.popContext(); - if (!this.isAtomic) { - this.readyToYield = true; - } -}; - -Process.prototype.expectReport = function () { - this.handleError(new Error("reporter didn't report")); -}; - -// Process Exception Handling - -Process.prototype.handleError = function (error, element) { - var m = element; - this.stop(); - this.errorFlag = true; - this.topBlock.addErrorHighlight(); - if (isNil(m) || isNil(m.world())) {m = this.topBlock; } - m.showBubble( - (m === element ? '' : 'Inside: ') - + error.name - + '\n' - + error.message, - this.exportResult, - this.receiver - ); -}; - -Process.prototype.errorObsolete = function () { - throw new Error('a custom block definition is missing'); -}; - -// Process Lambda primitives - -Process.prototype.reify = function (topBlock, parameterNames, isCustomBlock) { - var context = new Context( - null, - null, - this.context ? this.context.outerContext : null - ), - i = 0; - - if (topBlock) { - context.expression = this.enableLiveCoding || - this.enableSingleStepping ? - topBlock : topBlock.fullCopy(); - context.expression.show(); // be sure to make visible if in app mode - - if (!isCustomBlock && !parameterNames.length()) { - // mark all empty slots with an identifier - context.expression.allEmptySlots().forEach(slot => { - i += 1; - if (slot instanceof MultiArgMorph) { - slot.bindingID = Symbol.for('arguments'); - } else { - slot.bindingID = i; - } - }); - // and remember the number of detected empty slots - context.emptySlots = i; - } - } else { - context.expression = this.enableLiveCoding || - this.enableSingleStepping ? [this.context.expression] - : [this.context.expression.fullCopy()]; - } - - context.inputs = parameterNames.asArray(); - context.receiver - = this.context ? this.context.receiver : this.receiver; - context.origin = context.receiver; // for serialization - - return context; -}; - -Process.prototype.reportScript = function (parameterNames, topBlock) { - return this.reify(topBlock, parameterNames); -}; - -Process.prototype.reifyScript = function (topBlock, parameterNames) { - return this.reify(topBlock, parameterNames); -}; - -Process.prototype.reifyReporter = function (topBlock, parameterNames) { - return this.reify(topBlock, parameterNames); -}; - -Process.prototype.reifyPredicate = function (topBlock, parameterNames) { - return this.reify(topBlock, parameterNames); -}; - -Process.prototype.reportJSFunction = function (parmNames, body) { - return Function.apply( - null, - parmNames.asArray().concat([body]) - ); -}; - -Process.prototype.doRun = function (context, args) { - return this.evaluate(context, args, true); -}; - -Process.prototype.evaluate = function ( - context, - args, - isCommand -) { - if (!context) {return null; } - if (context instanceof Function) { - // if (!this.enableJS) { - // throw new Error('JavaScript is not enabled'); - // } - return context.apply( - this.blockReceiver(), - args.asArray().concat([this]) - ); - } - if (context.isContinuation) { - return this.runContinuation(context, args); - } - if (!(context instanceof Context)) { - throw new Error('expecting a ring but getting ' + context); - } - - var outer = new Context(null, null, context.outerContext), - caller = this.context.parentContext, - exit, - runnable, - expr, - parms = args.asArray(), - i, - value; - - if (!outer.receiver) { - outer.receiver = context.receiver; // for custom blocks - } - runnable = new Context( - this.context.parentContext, - context.expression, - outer, - context.receiver - ); - runnable.isCustomCommand = isCommand; // for short-circuiting HTTP requests - this.context.parentContext = runnable; - - if (context.expression instanceof ReporterBlockMorph) { - // auto-"warp" nested reporters - this.readyToYield = (Date.now() - this.lastYield > this.timeout); - } - - // assign arguments to parameters - - // assign the actual arguments list to the special - // parameter ID Symbol.for('arguments'), to be used for variadic inputs - outer.variables.addVar(Symbol.for('arguments'), args); - - // assign arguments that are actually passed - if (parms.length > 0) { - - // assign formal parameters - for (i = 0; i < context.inputs.length; i += 1) { - value = 0; - if (!isNil(parms[i])) { - value = parms[i]; - } - outer.variables.addVar(context.inputs[i], value); - } - - // assign implicit parameters if there are no formal ones - if (context.inputs.length === 0) { - // in case there is only one input - // assign it to all empty slots... - if (parms.length === 1) { - // ... unless it's an empty reporter ring, - // in which special case it gets treated as the ID-function; - // experimental feature jens is not at all comfortable with - if (!context.emptySlots) { - expr = context.expression; - if (expr instanceof Array && - expr.length === 1 && - expr[0].selector && - expr[0].selector === 'reifyReporter' && - !expr[0].contents()) { - runnable.expression = new Variable(parms[0]); - } - } else { - for (i = 1; i <= context.emptySlots; i += 1) { - outer.variables.addVar(i, parms[0]); - } - } - - // if the number of inputs matches the number - // of empty slots distribute them sequentially - } else if (parms.length === context.emptySlots) { - for (i = 1; i <= parms.length; i += 1) { - outer.variables.addVar(i, parms[i - 1]); - } - - } else if (context.emptySlots !== 1) { - throw new Error( - localize('expecting') + ' ' + context.emptySlots + ' ' - + localize('input(s), but getting') + ' ' - + parms.length - ); - } - } - } - - if (runnable.expression instanceof CommandBlockMorph) { - runnable.expression = runnable.expression.blockSequence(); - if (!isCommand) { - if (caller) { - // tag caller, so "report" can catch it later - caller.tag = 'exit'; - } else { - // top-level context, insert a tagged exit context - // which "report" can catch later - exit = new Context( - runnable.parentContext, - 'expectReport', - outer, - outer.receiver - ); - exit.tag = 'exit'; - runnable.parentContext = exit; - } - } - } -}; - -Process.prototype.fork = function (context, args) { - if (this.readyToTerminate) {return; } - var proc = new Process(), - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - proc.instrument = this.instrument; - proc.receiver = this.receiver; - proc.initializeFor(context, args); - // proc.pushContext('doYield'); - stage.threads.processes.push(proc); -}; - -Process.prototype.initializeFor = function (context, args) { - // used by Process.fork() and global invoke() - if (context.isContinuation) { - throw new Error( - 'continuations cannot be forked' - ); - } - if (!(context instanceof Context)) { - throw new Error('expecting a ring but getting ' + context); - } - - var outer = new Context(null, null, context.outerContext), - runnable = new Context(null, - context.expression, - outer - ), - parms = args.asArray(), - i, - value; - - // remember the receiver - this.context = context.receiver; - - // assign arguments to parameters - - // assign the actual arguments list to the special - // parameter ID Symbol.for('arguments'), to be used for variadic inputs - outer.variables.addVar(Symbol.for('arguments'), args); - - // assign arguments that are actually passed - if (parms.length > 0) { - - // assign formal parameters - for (i = 0; i < context.inputs.length; i += 1) { - value = 0; - if (!isNil(parms[i])) { - value = parms[i]; - } - outer.variables.addVar(context.inputs[i], value); - } - - // assign implicit parameters if there are no formal ones - if (context.inputs.length === 0) { - // in case there is only one input - // assign it to all empty slots - if (parms.length === 1) { - for (i = 1; i <= context.emptySlots; i += 1) { - outer.variables.addVar(i, parms[0]); - } - - // if the number of inputs matches the number - // of empty slots distribute them sequentially - } else if (parms.length === context.emptySlots) { - for (i = 1; i <= parms.length; i += 1) { - outer.variables.addVar(i, parms[i - 1]); - } - - } else if (context.emptySlots !== 1) { - throw new Error( - localize('expecting') + ' ' + context.emptySlots + ' ' - + localize('input(s), but getting') + ' ' - + parms.length - ); - } - } - } - - if (runnable.expression instanceof CommandBlockMorph) { - runnable.expression = runnable.expression.blockSequence(); - } - - this.homeContext = new Context(); // context.outerContext; - this.homeContext.receiver = context.outerContext.receiver; - this.topBlock = context.expression; - this.context = runnable; -}; - -// Process stopping blocks primitives - -Process.prototype.doStopBlock = function () { - var target = this.context.expression.exitTag; - if (isNil(target)) { - return this.doStopCustomBlock(); - } - while (this.context && - (isNil(this.context.tag) || (this.context.tag > target))) { - if (this.context.expression === 'doStopWarping') { - this.doStopWarping(); - } else { - this.popContext(); - } - } - this.pushContext(); -}; - -Process.prototype.doStopCustomBlock = function () { - // fallback solution for "report" blocks inside - // custom command definitions and untagged "stop" blocks - while (this.context && !this.context.isCustomBlock) { - if (this.context.expression === 'doStopWarping') { - this.doStopWarping(); - } else { - this.popContext(); - } - } -}; - -// Process continuations primitives - -Process.prototype.doCallCC = function (aContext, isReporter) { - this.evaluate( - aContext, - new List([this.context.continuation()]), - !isReporter - ); -}; - -Process.prototype.reportCallCC = function (aContext) { - this.doCallCC(aContext, true); -}; - -Process.prototype.runContinuation = function (aContext, args) { - var parms = args.asArray(); - - // determine whether the continuations is to show the result - // in a value-balloon becuse the user has directly clicked on a reporter - if (aContext.expression === 'expectReport' && parms.length) { - this.stop(); - this.homeContext.inputs[0] = parms[0]; - return; - } - - this.context.parentContext = aContext.copyForContinuationCall(); - // passing parameter if any was passed - if (parms.length === 1) { - this.context.parentContext.outerContext.variables.addVar( - 1, - parms[0] - ); - } -}; - -// Process custom block primitives - -Process.prototype.evaluateCustomBlock = function () { - var caller = this.context.parentContext, - block = this.context.expression, - method = block.isGlobal ? block.definition - : this.blockReceiver().getMethod(block.semanticSpec), - context = method.body, - declarations = method.declarations, - args = new List(this.context.inputs), - parms = args.asArray(), - runnable, - exit, - i, - value, - outer; - - if (!context) {return null; } - this.procedureCount += 1; - outer = new Context(); - outer.receiver = this.context.receiver; - - outer.variables.parentFrame = block.variables; - - // block (instance) var support, experimental: - // only splice in block vars if any are defined, because block vars - // can cause race conditions in global block definitions that - // access sprite-local variables at the same time. - if (method.variableNames.length) { - block.variables.parentFrame = outer.receiver ? - outer.receiver.variables : null; - } else { - // original code without block variables: - outer.variables.parentFrame = outer.receiver ? - outer.receiver.variables : null; - } - - runnable = new Context( - this.context.parentContext, - context.expression, - outer, - outer.receiver - ); - runnable.isCustomBlock = true; - this.context.parentContext = runnable; - - // passing parameters if any were passed - if (parms.length > 0) { - - // assign formal parameters - for (i = 0; i < context.inputs.length; i += 1) { - value = 0; - if (!isNil(parms[i])) { - value = parms[i]; - } - outer.variables.addVar(context.inputs[i], value); - - // if the parameter is an upvar, - // create a reference to the variable it points to - if (declarations.get(context.inputs[i])[0] === '%upvar') { - this.context.outerContext.variables.vars[value] = - outer.variables.vars[context.inputs[i]]; - } - } - } - - // tag return target - if (method.type !== 'command') { - if (caller) { - // tag caller, so "report" can catch it later - caller.tag = 'exit'; - } else { - // top-level context, insert a tagged exit context - // which "report" can catch later - exit = new Context( - runnable.parentContext, - 'expectReport', - outer, - outer.receiver - ); - exit.tag = 'exit'; - runnable.parentContext = exit; - } - // auto-"warp" nested reporters - this.readyToYield = (Date.now() - this.lastYield > this.timeout); - } else { - // tag all "stop this block" blocks with the current - // procedureCount as exitTag, and mark all "report" blocks - // as being inside a custom command definition - runnable.expression.tagExitBlocks(this.procedureCount, true); - - // tag the caller with the current procedure count, so - // "stop this block" blocks can catch it, but only - // if the caller hasn't been tagged already - if (caller && !caller.tag) { - caller.tag = this.procedureCount; - } - // yield commands unless explicitly "warped" or directly recursive - if (!this.isAtomic && method.isDirectlyRecursive()) { - this.readyToYield = true; - } - } - runnable.expression = runnable.expression.blockSequence(); -}; - -// Process variables primitives - -Process.prototype.doDeclareVariables = function (varNames) { - var varFrame = this.context.outerContext.variables; - varNames.asArray().forEach(name => - varFrame.addVar(name) - ); -}; - -Process.prototype.doSetVar = function (varName, value) { - var varFrame = this.context.variables, - name = varName; - if (name instanceof Context) { - if (name.expression.selector === 'reportGetVar') { - name.variables.setVar( - name.expression.blockSpec, - value, - this.blockReceiver() - ); - return; - } - this.doSet(name, value); - return; - } - if (name instanceof Array) { - this.doSet(name, value); - return; - } - varFrame.setVar(name, value, this.blockReceiver()); -}; - -Process.prototype.doChangeVar = function (varName, value) { - var varFrame = this.context.variables, - name = varName; - - if (name instanceof Context) { - if (name.expression.selector === 'reportGetVar') { - name.variables.changeVar( - name.expression.blockSpec, - value, - this.blockReceiver() - ); - return; - } - } - varFrame.changeVar(name, value, this.blockReceiver()); -}; - -Process.prototype.reportGetVar = function () { - // assumes a getter block whose blockSpec is a variable name - return this.context.variables.getVar( - this.context.expression.blockSpec - ); -}; - -Process.prototype.doShowVar = function (varName) { - var varFrame = this.context.variables, - stage, - watcher, - target, - label, - others, - isGlobal, - name = varName; - - if (name instanceof Context) { - if (name.expression.selector === 'reportGetVar') { - name = name.expression.blockSpec; - } else { - this.doChangePrimitiveVisibility(name.expression, false); - return; - } - } - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - target = varFrame.silentFind(name); - if (!target) {return; } - // first try to find an existing (hidden) watcher - watcher = detect( - stage.children, - morph => morph instanceof WatcherMorph && - morph.target === target && - morph.getter === name - ); - if (watcher !== null) { - watcher.show(); - watcher.fixLayout(); // re-hide hidden parts - return; - } - // if no watcher exists, create a new one - isGlobal = contains( - this.homeContext.receiver.globalVariables().names(), - varName - ); - if (isGlobal || target.owner) { - label = name; - } else { - label = name + ' ' + localize('(temporary)'); - } - watcher = new WatcherMorph( - label, - SpriteMorph.prototype.blockColor.variables, - target, - name - ); - watcher.setPosition(stage.position().add(10)); - others = stage.watchers(watcher.left()); - if (others.length > 0) { - watcher.setTop(others[others.length - 1].bottom()); - } - stage.add(watcher); - watcher.fixLayout(); - } - } -}; - -Process.prototype.doHideVar = function (varName) { - // if no varName is specified delete all watchers on temporaries - var varFrame = this.context.variables, - stage, - watcher, - target, - name = varName; - - if (name instanceof Context) { - if (name.expression.selector === 'reportGetVar') { - name = name.expression.blockSpec; - } else { - this.doChangePrimitiveVisibility(name.expression, true); - return; - } - } - if (!name) { - this.doRemoveTemporaries(); - return; - } - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - target = varFrame.find(name); - watcher = detect( - stage.children, - morph => morph instanceof WatcherMorph && - morph.target === target && - morph.getter === name - ); - if (watcher !== null) { - if (watcher.isTemporary()) { - watcher.destroy(); - } else { - watcher.hide(); - } - } - } - } -}; - -Process.prototype.doRemoveTemporaries = function () { - var stage; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - stage.watchers().forEach(watcher => { - if (watcher.isTemporary()) { - watcher.destroy(); - } - }); - } - } -}; - -// Process hiding and showing primitives primitives :-) - -Process.prototype.doChangePrimitiveVisibility = function (aBlock, hideIt) { - var ide = this.homeContext.receiver.parentThatIsA(IDE_Morph), - dict, - cat; - if (!ide || (aBlock.selector === 'evaluateCustomBlock')) { - return; - } - if (hideIt) { - StageMorph.prototype.hiddenPrimitives[aBlock.selector] = true; - } else { - delete StageMorph.prototype.hiddenPrimitives[aBlock.selector]; - } - dict = { - doWarp: 'control', - reifyScript: 'operators', - reifyReporter: 'operators', - reifyPredicate: 'operators', - doDeclareVariables: 'variables' - }; - cat = dict[this.selector] || this.category; - if (cat === 'lists') {cat = 'variables'; } - ide.flushBlocksCache(cat); - ide.refreshPalette(); -}; - -// Process sprite inheritance primitives - -Process.prototype.doDeleteAttr = function (attrName) { - var name = attrName, - rcvr = this.blockReceiver(); - if (name instanceof Context) { - if (name.expression.selector === 'reportGetVar') { - name = name.expression.blockSpec; - } else { // attribute - name = { - xPosition: 'x position', - yPosition: 'y position', - direction: 'direction', - getCostumeIdx: 'costume #', - size: 'size' - }[name.expression.selector]; - if (!isNil(name)) { - rcvr.inheritAttribute(name); - } - return; // error: cannot delete attribute... - } - } - if (name instanceof Array) { - return rcvr.inheritAttribute(this.inputOption(name)); - } - if (contains(rcvr.inheritedVariableNames(true), name)) { - rcvr.deleteVariable(name); - } -}; - -// experimental message passing primitives - -Process.prototype.doTellTo = function (sprite, context, args) { - this.doRun( - this.reportAttributeOf(context, sprite), - args - ); -}; - -Process.prototype.reportAskFor = function (sprite, context, args) { - this.evaluate( - this.reportAttributeOf(context, sprite), - args - ); -}; - -// Process lists primitives - -Process.prototype.reportNewList = function (elements) { - return elements; -}; - -Process.prototype.reportCONS = function (car, cdr) { - this.assertType(cdr, 'list'); - return new List().cons(car, cdr); -}; - -Process.prototype.reportCDR = function (list) { - this.assertType(list, 'list'); - return list.cdr(); -}; - -Process.prototype.doAddToList = function (element, list) { - this.assertType(list, 'list'); - if (list.type) { - this.assertType(element, list.type); - list = this.shadowListAttribute(list); - } - list.add(element); -}; - -Process.prototype.doDeleteFromList = function (index, list) { - var idx = index; - this.assertType(list, 'list'); - if (list.type) { - list = this.shadowListAttribute(list); - } - if (this.inputOption(index) === 'all') { - return list.clear(); - } - if (index === '') { - return null; - } - if (this.inputOption(index) === 'last') { - idx = list.length(); - } else if (isNaN(+this.inputOption(index))) { - return null; - } - list.remove(idx); -}; - -Process.prototype.doInsertInList = function (element, index, list) { - var idx = index; - this.assertType(list, 'list'); - if (list.type) { - this.assertType(element, list.type); - list = this.shadowListAttribute(list); - } - if (index === '') { - return null; - } - if (this.inputOption(index) === 'any') { - idx = this.reportBasicRandom(1, list.length() + 1); - } - if (this.inputOption(index) === 'last') { - idx = list.length() + 1; - } - list.add(element, idx); -}; - -Process.prototype.doReplaceInList = function (index, list, element) { - var idx = index; - this.assertType(list, 'list'); - if (list.type) { - this.assertType(element, list.type); - list = this.shadowListAttribute(list); - } - if (index === '') { - return null; - } - if (this.inputOption(index) === 'any') { - idx = this.reportBasicRandom(1, list.length()); - } - if (this.inputOption(index) === 'last') { - idx = list.length(); - } - list.put(element, idx); -}; - -Process.prototype.shadowListAttribute = function (list) { - // private - check whether the list is an attribute that needs to be - // shadowed. Use only on typed lists for performance. - var rcvr; - if (list.type === 'costume' || list.type === 'sound') { - rcvr = this.blockReceiver(); - if (list === rcvr.costumes) { - rcvr.shadowAttribute('costumes'); - list = rcvr.costumes; - } else if (list === rcvr.sounds) { - rcvr.shadowAttribute('sounds'); - list = rcvr.sounds; - } - } - return list; -}; - -// Process accessing list elements - hyper dyadic - -Process.prototype.reportListItem = function (index, list) { - var rank; - this.assertType(list, 'list'); - if (index === '') { - return ''; - } - if (this.inputOption(index) === 'any') { - return list.at(this.reportBasicRandom(1, list.length())); - } - if (this.inputOption(index) === 'last') { - return list.at(list.length()); - } - rank = this.rank(index); - if (rank > 0 && this.enableHyperOps) { - if (rank === 1) { - if (index.isEmpty()) { - return list.map(item => item); - } - return index.map(idx => list.at(idx)); - } - return this.reportItems(index, list); - } - return list.at(index); -}; - -Process.prototype.reportItems = function (indices, list) { - // This. This is it. The pinnacle of my programmer's life. - // After days of roaming about my house and garden, - // of taking showers and rummaging through the fridge, - // of strumming the charango and the five ukuleles - // sitting next to my laptop on my desk, - // and of letting my mind wander far and wide, - // to come up with this design, always thinking - // "What would Brian do?". - // And look, Ma, it's turned out all beautiful! -jens - - return makeSelector( - this.rank(list), - indices.cdr(), - makeLeafSelector(indices.at(1)) - )(list); - - function makeSelector(rank, indices, next) { - if (rank === 1) { - return next; - } - return makeSelector( - rank - 1, - indices.cdr(), - makeBranch( - indices.at(1) || new List(), - next - ) - ); - } - - function makeBranch(indices, next) { - return function(data) { - if (indices.isEmpty()) { - return data.map(item => next(item)); - } - return indices.map(idx => next(data.at(idx))); - }; - } - - function makeLeafSelector(indices) { - return function (data) { - if (indices.isEmpty()) { - return data.map(item => item); - } - return indices.map(idx => data.at(idx)); - }; - } -}; - -// Process - other basic list accessors - -Process.prototype.reportListLength = function (list) { - this.assertType(list, 'list'); - return list.length(); -}; - -Process.prototype.reportListIndex = function(element, list) { - this.assertType(list, 'list'); - return list.indexOf(element); -}; - -Process.prototype.reportListContainsItem = function (list, element) { - this.assertType(list, 'list'); - return list.contains(element); -}; - -Process.prototype.reportListIsEmpty = function (list) { - this.assertType(list, 'list'); - return list.isEmpty(); -}; - -Process.prototype.doShowTable = function (list) { - // experimental - this.assertType(list, 'list'); - new TableDialogMorph(list).popUp(this.blockReceiver().world()); -}; - -// Process non-HOF list primitives - -Process.prototype.reportNumbers = function (start, end) { - // hyper-dyadic - if (this.enableHyperOps) { - return this.hyperDyadic( - (strt, stp) => this.reportBasicNumbers(strt, stp), - start, - end - ); - } - return this.reportLinkedNumbers(start, end); -}; - -Process.prototype.reportBasicNumbers = function (start, end) { - // answer a new arrayed list containing an linearly ascending progression - // of integers beginning at start to end. - var result, len, i, - s = +start, - e = +end, - n = s; - - this.assertType(s, 'number'); - this.assertType(e, 'number'); - - if (e > s) { - len = Math.floor(e - s); - result = new Array(len); - for(i = 0; i <= len; i += 1) { - result[i] = n; - n += 1; - } - } else { - len = Math.floor(s - e); - result = new Array(len); - for(i = 0; i <= len; i += 1) { - result[i] = n; - n -= 1; - } - } - return new List(result); -}; - -Process.prototype.reportConcatenatedLists = function (lists) { - var first, result, rows, row, rowIdx, cols, col; - this.assertType(lists, 'list'); - if (lists.isEmpty()) { - return lists; - } - first = lists.at(1); - this.assertType(first, 'list'); - if (first.isLinked) { // link everything - return this.concatenateLinkedLists(lists); - } - - // in case the first sub-list is arrayed - result = []; - rows = lists.length(); - for (rowIdx = 1; rowIdx <= rows; rowIdx += 1) { - row = lists.at(rowIdx); - this.assertType(row, 'list'); - cols = row.length(); - for (col = 1; col <= cols; col += 1) { - result.push(row.at(col)); - } - } - return new List(result); -}; - -Process.prototype.concatenateLinkedLists = function (lists) { - var first; - if (lists.isEmpty()) { - return lists; - } - first = lists.at(1); - this.assertType(first, 'list'); - if (lists.length() === 1) { - return first; - } - if (first.isEmpty()) { - return this.concatenateLinkedLists(lists.cdr()); - } - return lists.cons( - first.at(1), - this.concatenateLinkedLists( - lists.cons( - first.cdr(), - lists.cdr() - ) - ) - ); -}; - -// Process interpolated non-HOF list primitives - -Process.prototype.reportLinkedNumbers = function (start, end) { - // answer a new linked list containing an linearly ascending progression - // of integers beginning at start to end. - // this is interpolated so it can handle big ranges of numbers - // without blocking the UI - - var dta; - if (this.context.accumulator === null) { - this.assertType(start, 'number'); - this.assertType(end, 'number'); - this.context.accumulator = { - target : new List(), - end : null, - idx : +start, - step: +end > +start ? +1 : -1 - }; - this.context.accumulator.target.isLinked = true; - this.context.accumulator.end = this.context.accumulator.target; - } - dta = this.context.accumulator; - if (dta.step === 1 ? dta.idx > +end : dta.idx < +end) { - dta.end.rest = new List(); - this.returnValueToParentContext(dta.target.cdr()); - return; - } - dta.end.rest = dta.target.cons(dta.idx); - dta.end = dta.end.rest; - dta.idx += dta.step; - this.pushContext(); -}; - -// Process conditionals primitives - -Process.prototype.doIf = function () { - var args = this.context.inputs, - outer = this.context.outerContext, // for tail call elimination - isCustomBlock = this.context.isCustomBlock; - - // this.assertType(args[0], ['Boolean']); - this.popContext(); - if (args[0]) { - if (args[1]) { - this.pushContext(args[1].blockSequence(), outer); - this.context.isCustomBlock = isCustomBlock; - } - } - this.pushContext(); -}; - -Process.prototype.doIfElse = function () { - var args = this.context.inputs, - outer = this.context.outerContext, // for tail call elimination - isCustomBlock = this.context.isCustomBlock; - - // this.assertType(args[0], ['Boolean']); - this.popContext(); - if (args[0]) { - if (args[1]) { - this.pushContext(args[1].blockSequence(), outer); - } - } else { - if (args[2]) { - this.pushContext(args[2].blockSequence(), outer); - } else { - this.pushContext('doYield'); - } - } - if (this.context) { - this.context.isCustomBlock = isCustomBlock; - } - - this.pushContext(); -}; - -Process.prototype.reportIfElse = function (block) { - var inputs = this.context.inputs; - - if (inputs.length < 1) { - this.evaluateNextInput(block); - } else if (inputs.length > 1) { - if (this.flashContext()) {return; } - this.returnValueToParentContext(inputs.pop()); - this.popContext(); - } else { - // this.assertType(inputs[0], ['Boolean']); - if (inputs[0]) { - this.evaluateNextInput(block); - } else { - inputs.push(null); - this.evaluateNextInput(block); - } - } -}; - -// Process process related primitives - -Process.prototype.doStop = function () { - this.stop(); -}; - -Process.prototype.doStopAll = function () { - var stage, ide; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - if (stage.enableCustomHatBlocks) { - stage.threads.pauseCustomHatBlocks = - !stage.threads.pauseCustomHatBlocks; - } else { - stage.threads.pauseCustomHatBlocks = false; - } - stage.stopAllActiveSounds(); - stage.threads.resumeAll(stage); - stage.keysPressed = {}; - stage.runStopScripts(); - stage.threads.stopAll(); - if (stage.projectionSource) { - stage.stopProjection(); - } - stage.children.forEach(morph => { - if (morph.stopTalking) { - morph.stopTalking(); - } - }); - stage.removeAllClones(); - } - ide = stage.parentThatIsA(IDE_Morph); - if (ide) { - ide.controlBar.pauseButton.refresh(); - ide.controlBar.stopButton.refresh(); - } - } -}; - -Process.prototype.doStopThis = function (choice) { - switch (this.inputOption(choice)) { - case 'all': - this.doStopAll(); - break; - case 'this script': - this.doStop(); - break; - case 'this block': - this.doStopBlock(); - break; - default: - this.doStopOthers(choice); - } -}; - -Process.prototype.doStopOthers = function (choice) { - var stage; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - switch (this.inputOption(choice)) { - case 'all but this script': - stage.threads.stopAll(this); - break; - case 'other scripts in sprite': - stage.threads.stopAllForReceiver( - this.homeContext.receiver, - this - ); - break; - default: - nop(); - } - } - } -}; - -Process.prototype.doWarp = function (body) { - // execute my contents block atomically (more or less) - var outer = this.context.outerContext, // for tail call elimination - isCustomBlock = this.context.isCustomBlock, - stage; - - this.popContext(); - - if (body) { - if (this.homeContext.receiver) { - if (this.homeContext.receiver.startWarp) { - // pen optimization - this.homeContext.receiver.startWarp(); - } - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - stage.fps = 0; // variable frame rate - } - } - - // this.pushContext('doYield'); // no longer needed in Morphic2 - this.pushContext('popContext'); // instead we do this... - - if (this.context) { - this.context.isCustomBlock = isCustomBlock; - } - if (!this.isAtomic) { - this.pushContext('doStopWarping'); - } - this.pushContext(body.blockSequence(), outer); - this.isAtomic = true; - } - this.pushContext(); -}; - -Process.prototype.doStopWarping = function () { - var stage; - this.popContext(); - this.isAtomic = false; - if (this.homeContext.receiver) { - if (this.homeContext.receiver.endWarp) { - // pen optimization - this.homeContext.receiver.endWarp(); - } - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - stage.fps = stage.frameRate; // back to fixed frame rate - } - } -}; - -Process.prototype.reportIsFastTracking = function () { - var ide; - if (this.homeContext.receiver) { - ide = this.homeContext.receiver.parentThatIsA(IDE_Morph); - if (ide) { - return ide.stage.isFastTracked; - } - } - return false; -}; - -Process.prototype.doSetGlobalFlag = function (name, bool) { - var stage = this.homeContext.receiver.parentThatIsA(StageMorph); - name = this.inputOption(name); - this.assertType(bool, 'Boolean'); - switch (name) { - case 'turbo mode': - this.doSetFastTracking(bool); - break; - case 'flat line ends': - SpriteMorph.prototype.useFlatLineEnds = bool; - break; - case 'log pen vectors': - StageMorph.prototype.enablePenLogging = bool; - break; - case 'video capture': - if (bool) { - this.startVideo(stage); - } else { - stage.stopProjection(); - } - break; - case 'mirror video': - stage.mirrorVideo = bool; - break; - } -}; - -Process.prototype.reportGlobalFlag = function (name) { - var stage = this.homeContext.receiver.parentThatIsA(StageMorph); - name = this.inputOption(name); - switch (name) { - case 'turbo mode': - return this.reportIsFastTracking(); - case 'flat line ends': - return SpriteMorph.prototype.useFlatLineEnds; - case 'log pen vectors': - return StageMorph.prototype.enablePenLogging; - case 'video capture': - return !isNil(stage.projectionSource) && - stage.projectionLayer() - .getContext('2d') - .getImageData(0, 0, 1, 1) - .data[3] > 0; - case 'mirror video': - return stage.mirrorVideo; - default: - return ''; - } -}; - -Process.prototype.doSetFastTracking = function (bool) { - var ide; - if (!this.reportIsA(bool, 'Boolean')) { - return; - } - if (this.homeContext.receiver) { - ide = this.homeContext.receiver.parentThatIsA(IDE_Morph); - if (ide) { - if (bool) { - ide.startFastTracking(); - } else { - ide.stopFastTracking(); - } - } - } -}; - -Process.prototype.doPauseAll = function () { - var stage, ide; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - stage.threads.pauseAll(stage); - } - ide = stage.parentThatIsA(IDE_Morph); - if (ide) {ide.controlBar.pauseButton.refresh(); } - } -}; - -// Process loop primitives - -Process.prototype.doForever = function (body) { - this.context.inputs = []; // force re-evaluation of C-slot - this.pushContext('doYield'); - if (body) { - this.pushContext(body.blockSequence()); - } - this.pushContext(); -}; - -Process.prototype.doRepeat = function (counter, body) { - var block = this.context.expression, - outer = this.context.outerContext, // for tail call elimination - isCustomBlock = this.context.isCustomBlock; - - if (counter < 1) { // was '=== 0', which caused infinite loops on non-ints - return null; - } - this.popContext(); - this.pushContext(block, outer); - this.context.isCustomBlock = isCustomBlock; - this.context.addInput(counter - 1); - this.pushContext('doYield'); - if (body) { - this.pushContext(body.blockSequence()); - } - this.pushContext(); -}; - -Process.prototype.doUntil = function (goalCondition, body) { - // this.assertType(goalCondition, ['Boolean']); - if (goalCondition) { - this.popContext(); - this.pushContext('doYield'); - return null; - } - this.context.inputs = []; - this.pushContext('doYield'); - if (body) { - this.pushContext(body.blockSequence()); - } - this.pushContext(); -}; - -Process.prototype.doWaitUntil = function (goalCondition) { - // this.assertType(goalCondition, ['Boolean']); - if (goalCondition) { - this.popContext(); - this.pushContext('doYield'); - return null; - } - this.context.inputs = []; - this.pushContext('doYield'); - this.pushContext(); -}; - -// Process interpolated iteration primitives - -Process.prototype.doForEach = function (upvar, list, script) { - // perform a script for each element of a list, assigning the - // current iteration's element to a variable with the name - // specified in the "upvar" parameter, so it can be referenced - // within the script. - // Distinguish between linked and arrayed lists. - - var next; - if (this.context.accumulator === null) { - this.assertType(list, 'list'); - this.context.accumulator = { - source : list, - remaining : list.length(), - idx : 0 - }; - } - if (this.context.accumulator.remaining === 0) { - return; - } - this.context.accumulator.remaining -= 1; - if (this.context.accumulator.source.isLinked) { - next = this.context.accumulator.source.at(1); - this.context.accumulator.source = - this.context.accumulator.source.cdr(); - } else { // arrayed - this.context.accumulator.idx += 1; - next = list.at(this.context.accumulator.idx); - } - this.pushContext('doYield'); - this.pushContext(); - this.context.outerContext.variables.addVar(upvar); - this.context.outerContext.variables.setVar(upvar, next); - this.evaluate(script, new List([next]), true); -}; - -Process.prototype.doFor = function (upvar, start, end, script) { - // perform a script for every integer step between start and stop, - // assigning the current iteration index to a variable with the - // name specified in the "upvar" parameter, so it can be referenced - // within the script. - - var vars = this.context.outerContext.variables, - dta = this.context.accumulator; - if (dta === null) { - this.assertType(start, 'number'); - this.assertType(end, 'number'); - dta = this.context.accumulator = { - test : +start < +end ? - (() => vars.getVar(upvar) > +end) - : (() => vars.getVar(upvar) < +end), - step : +start < +end ? 1 : -1, - parms : new List() // empty parameters, reusable to avoid GC - }; - vars.addVar(upvar); - vars.setVar(upvar, Math.floor(+start)); - } else { - vars.changeVar(upvar, dta.step); - } - if (dta.test()) {return; } - this.pushContext('doYield'); - this.pushContext(); - this.evaluate(script, dta.parms, true); -}; - -// Process interpolated HOF primitives - -/* - this.context.inputs: - [0] - reporter - [1] - list (original source) - ----------------------------- - [2] - last reporter evaluation result - - these primitives used to store the accumulated data in the unused parts - of the context's input-array. For reasons obscure to me this led to - JS stack overflows when used on large lists (> 150 k items). As a remedy - aggregations are now accumulated in the "accumulator" property slot - of Context. Why this speeds up execution by orders of magnitude while - "fixing" the stack-overflow issue eludes me. -Jens -*/ - -Process.prototype.reportMap = function (reporter, list) { - // answer a new list containing the results of the reporter applied - // to each value of the given list. Distinguish between linked and - // arrayed lists. - // if the reporter uses formal parameters instead of implicit empty slots - // there are two additional optional parameters: - // #1 - element - // #2 - optional | index - // #3 - optional | source list - - var next, index, parms; - if (list.isLinked) { - if (this.context.accumulator === null) { - this.assertType(list, 'list'); - this.context.accumulator = { - source : list, - idx : 1, - target : new List(), - end : null, - remaining : list.length() - }; - this.context.accumulator.target.isLinked = true; - this.context.accumulator.end = this.context.accumulator.target; - } else if (this.context.inputs.length > 2) { - this.context.accumulator.end.rest = list.cons( - this.context.inputs.pop() - ); - this.context.accumulator.end = this.context.accumulator.end.rest; - this.context.accumulator.idx += 1; - this.context.accumulator.remaining -= 1; - } - if (this.context.accumulator.remaining === 0) { - this.context.accumulator.end.rest = list.cons( - this.context.inputs[2] - ).cdr(); - this.returnValueToParentContext( - this.context.accumulator.target.cdr() - ); - return; - } - index = this.context.accumulator.idx; - next = this.context.accumulator.source.at(1); - this.context.accumulator.source = this.context.accumulator.source.cdr(); - } else { // arrayed - if (this.context.accumulator === null) { - this.assertType(list, 'list'); - this.context.accumulator = []; - } else if (this.context.inputs.length > 2) { - this.context.accumulator.push(this.context.inputs.pop()); - } - if (this.context.accumulator.length === list.length()) { - this.returnValueToParentContext( - new List(this.context.accumulator) - ); - return; - } - index = this.context.accumulator.length + 1; - next = list.at(index); - } - this.pushContext(); - parms = [next]; - if (reporter.inputs.length > 1) { - parms.push(index); - } - if (reporter.inputs.length > 2) { - parms.push(list); - } - this.evaluate(reporter, new List(parms)); -}; - -Process.prototype.reportKeep = function (predicate, list) { - // Filter - answer a new list containing the items of the list for which - // the predicate evaluates TRUE. - // Distinguish between linked and arrayed lists. - // if the predicate uses formal parameters instead of implicit empty slots - // there are two additional optional parameters: - // #1 - element - // #2 - optional | index - // #3 - optional | source list - - var next, index, parms; - if (list.isLinked) { - if (this.context.accumulator === null) { - this.assertType(list, 'list'); - this.context.accumulator = { - source : list, - idx: 1, - target : new List(), - end : null, - remaining : list.length() - }; - this.context.accumulator.target.isLinked = true; - this.context.accumulator.end = this.context.accumulator.target; - } else if (this.context.inputs.length > 2) { - if (this.context.inputs.pop() === true) { - this.context.accumulator.end.rest = list.cons( - this.context.accumulator.source.at(1) - ); - this.context.accumulator.end = - this.context.accumulator.end.rest; - } - this.context.accumulator.remaining -= 1; - this.context.accumulator.idx += 1; - this.context.accumulator.source = - this.context.accumulator.source.cdr(); - } - if (this.context.accumulator.remaining === 0) { - this.context.accumulator.end.rest = new List(); - this.returnValueToParentContext( - this.context.accumulator.target.cdr() - ); - return; - } - index = this.context.accumulator.idx; - next = this.context.accumulator.source.at(1); - } else { // arrayed - if (this.context.accumulator === null) { - this.assertType(list, 'list'); - this.context.accumulator = { - idx : 0, - target : [] - }; - } else if (this.context.inputs.length > 2) { - if (this.context.inputs.pop() === true) { - this.context.accumulator.target.push( - list.at(this.context.accumulator.idx) - ); - } - } - if (this.context.accumulator.idx === list.length()) { - this.returnValueToParentContext( - new List(this.context.accumulator.target) - ); - return; - } - this.context.accumulator.idx += 1; - index = this.context.accumulator.idx; - next = list.at(index); - } - this.pushContext(); - parms = [next]; - if (predicate.inputs.length > 1) { - parms.push(index); - } - if (predicate.inputs.length > 2) { - parms.push(list); - } - this.evaluate(predicate, new List(parms)); -}; - -Process.prototype.reportFindFirst = function (predicate, list) { - // Find - answer the first item of the list for which - // the predicate evaluates TRUE. - // Distinguish between linked and arrayed lists. - // if the predicate uses formal parameters instead of implicit empty slots - // there are two additional optional parameters: - // #1 - element - // #2 - optional | index - // #3 - optional | source list - - var next, index, parms; - if (list.isLinked) { - if (this.context.accumulator === null) { - this.assertType(list, 'list'); - this.context.accumulator = { - source : list, - idx : 1, - remaining : list.length() - }; - } else if (this.context.inputs.length > 2) { - if (this.context.inputs.pop() === true) { - this.returnValueToParentContext( - this.context.accumulator.source.at(1) - ); - return; - } - this.context.accumulator.remaining -= 1; - this.context.accumulator.idx += 1; - this.context.accumulator.source = - this.context.accumulator.source.cdr(); - } - if (this.context.accumulator.remaining === 0) { - this.returnValueToParentContext(''); - return; - } - index = this.context.accumulator.idx; - next = this.context.accumulator.source.at(1); - } else { // arrayed - if (this.context.accumulator === null) { - this.assertType(list, 'list'); - this.context.accumulator = { - idx : 0, - current : null - }; - } else if (this.context.inputs.length > 2) { - if (this.context.inputs.pop() === true) { - this.returnValueToParentContext( - this.context.accumulator.current - ); - return; - } - } - if (this.context.accumulator.idx === list.length()) { - this.returnValueToParentContext(''); - return; - } - this.context.accumulator.idx += 1; - index = this.context.accumulator.idx; - next = list.at(index); - this.context.accumulator.current = next; - } - this.pushContext(); - parms = [next]; - if (predicate.inputs.length > 1) { - parms.push(index); - } - if (predicate.inputs.length > 2) { - parms.push(list); - } - this.evaluate(predicate, new List(parms)); -}; - -Process.prototype.reportCombine = function (list, reporter) { - // Fold - answer an aggregation of all list items from "left to right" - // Distinguish between linked and arrayed lists. - // if the reporter uses formal parameters instead of implicit empty slots - // there are two additional optional parameters: - // #1 - accumulator - // #2 - element - // #3 - optional | index - // #4 - optional | source list - - var next, current, index, parms; - this.assertType(list, 'list'); - if (list.length() < 2) { - this.returnValueToParentContext(list.length() ? list.at(1) : 0); - return; - } - if (list.isLinked) { - if (this.context.accumulator === null) { - this.context.accumulator = { - source : list.cdr(), - idx : 1, - target : list.at(1), - remaining : list.length() - 1 - }; - } else if (this.context.inputs.length > 2) { - this.context.accumulator.target = this.context.inputs.pop(); - this.context.accumulator.remaining -= 1; - this.context.accumulator.idx += 1; - this.context.accumulator.source = - this.context.accumulator.source.cdr(); - } - if (this.context.accumulator.remaining === 0) { - this.returnValueToParentContext(this.context.accumulator.target); - return; - } - next = this.context.accumulator.source.at(1); - } else { // arrayed - if (this.context.accumulator === null) { - this.context.accumulator = { - idx : 1, - target : list.at(1) - }; - } else if (this.context.inputs.length > 2) { - this.context.accumulator.target = this.context.inputs.pop(); - } - if (this.context.accumulator.idx === list.length()) { - this.returnValueToParentContext(this.context.accumulator.target); - return; - } - this.context.accumulator.idx += 1; - next = list.at(this.context.accumulator.idx); - } - index = this.context.accumulator.idx; - current = this.context.accumulator.target; - this.pushContext(); - parms = [current, next]; - if (reporter.inputs.length > 2) { - parms.push(index); - } - if (reporter.inputs.length > 3) { - parms.push(list); - } - this.evaluate(reporter, new List(parms)); -}; - -// Process interpolated primitives - -Process.prototype.doWait = function (secs) { - if (!this.context.startTime) { - this.context.startTime = Date.now(); - } - if ((Date.now() - this.context.startTime) >= (secs * 1000)) { - if (!this.isAtomic && (secs === 0)) { - // "wait 0 secs" is a plain "yield" - // that can be overridden by "warp" - this.readyToYield = true; - } - return null; - } - this.pushContext('doYield'); - this.pushContext(); -}; - -Process.prototype.doGlide = function (secs, endX, endY) { - if (!this.context.startTime) { - this.context.startTime = Date.now(); - this.context.startValue = new Point( - this.blockReceiver().xPosition(), - this.blockReceiver().yPosition() - ); - } - if ((Date.now() - this.context.startTime) >= (secs * 1000)) { - this.blockReceiver().gotoXY(endX, endY); - return null; - } - this.blockReceiver().glide( - secs * 1000, - endX, - endY, - Date.now() - this.context.startTime, - this.context.startValue - ); - - this.pushContext('doYield'); - this.pushContext(); -}; - -Process.prototype.doSayFor = function (data, secs) { - if (!this.context.startTime) { - this.context.startTime = Date.now(); - this.blockReceiver().bubble(data); - } - if ((Date.now() - this.context.startTime) >= (secs * 1000)) { - this.blockReceiver().stopTalking(); - return null; - } - this.pushContext('doYield'); - this.pushContext(); -}; - -Process.prototype.doThinkFor = function (data, secs) { - if (!this.context.startTime) { - this.context.startTime = Date.now(); - this.blockReceiver().doThink(data); - } - if ((Date.now() - this.context.startTime) >= (secs * 1000)) { - this.blockReceiver().stopTalking(); - return null; - } - this.pushContext('doYield'); - this.pushContext(); -}; - -Process.prototype.blockReceiver = function () { - return this.context ? this.context.receiver || this.homeContext.receiver - : this.homeContext.receiver || this.receiver; -}; - -// Process sound primitives (interpolated) - -Process.prototype.playSound = function (name) { - if (name instanceof List) { - return this.doPlaySoundAtRate(name, 44100); - } - return this.blockReceiver().doPlaySound(name); -}; - -Process.prototype.doPlaySoundUntilDone = function (name) { - if (this.context.activeAudio === null) { - this.context.activeAudio = this.playSound(name); - } - if (name === null || this.context.activeAudio.ended - || this.context.activeAudio.terminated) { - return null; - } - this.pushContext('doYield'); - this.pushContext(); -}; - -Process.prototype.doStopAllSounds = function () { - var stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - stage.threads.processes.forEach(thread => { - if (thread.context) { - thread.context.stopMusic(); - if (thread.context.activeAudio) { - thread.popContext(); - } - } - }); - stage.stopAllActiveSounds(); - } -}; - -Process.prototype.doPlaySoundAtRate = function (name, rate) { - var sound, samples, ctx, gain, pan, source, rcvr; - - if (!(name instanceof List)) { - sound = name instanceof Sound ? name - : (typeof name === 'number' ? this.blockReceiver().sounds.at(name) - : detect( - this.blockReceiver().sounds.asArray(), - s => s.name === name.toString() - ) - ); - if (!sound.audioBuffer) { - this.decodeSound(sound); - return; - } - samples = this.reportGetSoundAttribute('samples', sound); - } else { - samples = name; - } - - rcvr = this.blockReceiver(); - ctx = rcvr.audioContext(); - gain = rcvr.getGainNode(); - pan = rcvr.getPannerNode(); - source = this.encodeSound(samples, rate); - rcvr.setVolume(rcvr.volume); - source.connect(gain); - if (pan) { - gain.connect(pan); - pan.connect(ctx.destination); - rcvr.setPan(rcvr.pan); - } else { - gain.connect(ctx.destination); - } - source.pause = source.stop; - source.ended = false; - source.onended = () => source.ended = true; - source.start(); - rcvr.parentThatIsA(StageMorph).activeSounds.push(source); - return source; -}; - -Process.prototype.reportGetSoundAttribute = function (choice, soundName) { - var sound = soundName instanceof Sound ? soundName - : (typeof soundName === 'number' ? - this.blockReceiver().sounds.at(soundName) - : (soundName instanceof List ? this.encodeSound(soundName) - : detect( - this.blockReceiver().sounds.asArray(), - s => s.name === soundName.toString() - ) - ) - ), - option = this.inputOption(choice); - - if (option === 'name') { - return sound.name; - } - - if (!sound.audioBuffer) { - this.decodeSound(sound); - return; - } - - switch (option) { - case 'samples': - if (!sound.cachedSamples) { - sound.cachedSamples = function (sound, untype) { - var buf = sound.audioBuffer, - result, i; - if (buf.numberOfChannels > 1) { - result = new List(); - for (i = 0; i < buf.numberOfChannels; i += 1) { - result.add(new List(untype(buf.getChannelData(i)))); - } - return result; - } - return new List(untype(buf.getChannelData(0))); - } (sound, this.untype); - } - return sound.cachedSamples; - case 'sample rate': - return sound.audioBuffer.sampleRate; - case 'duration': - return sound.audioBuffer.duration; - case 'length': - return sound.audioBuffer.length; - case 'number of channels': - return sound.audioBuffer.numberOfChannels; - default: - return 0; - } -}; - -Process.prototype.decodeSound = function (sound, callback) { - // private - callback is optional and invoked with sound as argument - var base64, binaryString, len, bytes, i, arrayBuffer, audioCtx; - - if (sound.audioBuffer) { - return (callback || nop)(sound); - } - if (!sound.isDecoding) { - base64 = sound.audio.src.split(',')[1]; - binaryString = window.atob(base64); - len = binaryString.length; - bytes = new Uint8Array(len); - for (i = 0; i < len; i += 1) { - bytes[i] = binaryString.charCodeAt(i); - } - arrayBuffer = bytes.buffer; - audioCtx = Note.prototype.getAudioContext(); - sound.isDecoding = true; - audioCtx.decodeAudioData( - arrayBuffer, - buffer => { - sound.audioBuffer = buffer; - sound.isDecoding = false; - }, - err => { - sound.isDecoding = false; - this.handleError(err); - } - ); - } - this.pushContext('doYield'); - this.pushContext(); -}; - -Process.prototype.encodeSound = function (samples, rate) { - // private - var rcvr = this.blockReceiver(), - ctx = rcvr.audioContext(), - channels = (samples.at(1) instanceof List) ? samples.length() : 1, - frameCount = (channels === 1) ? - samples.length() - : samples.at(1).length(), - arrayBuffer = ctx.createBuffer(channels, frameCount, +rate || 44100), - i, - source; - - if (!arrayBuffer.copyToChannel) { - arrayBuffer.copyToChannel = function (src, channel) { - var buffer = this.getChannelData(channel); - for (i = 0; i < src.length; i += 1) { - buffer[i] = src[i]; - } - }; - } - if (channels === 1) { - arrayBuffer.copyToChannel( - Float32Array.from(samples.asArray()), - 0, - 0 - ); - } else { - for (i = 0; i < channels; i += 1) { - arrayBuffer.copyToChannel( - Float32Array.from(samples.at(i + 1).asArray()), - i, - 0 - ); - } - } - source = ctx.createBufferSource(); - source.buffer = arrayBuffer; - source.audioBuffer = source.buffer; - return source; -}; - -// Process first-class sound creation from samples, interpolated - -Process.prototype.reportNewSoundFromSamples = function (samples, rate) { - // this method inspired by: https://github.com/Jam3/audiobuffer-to-wav - // https://www.russellgood.com/how-to-convert-audiobuffer-to-audio-file - - var audio, blob, reader; - - if (isNil(this.context.accumulator)) { - this.assertType(samples, 'list'); // check only the first time - this.context.accumulator = { - audio: null - }; - audio = new Audio(); - blob = new Blob( - [ - this.audioBufferToWav( - this.encodeSound(samples, rate || 44100).audioBuffer - ) - ], - {type: "audio/wav"} - ); - reader = new FileReader(); - reader.onload = () => { - audio.src = reader.result; - this.context.accumulator.audio = audio; - }; - reader.readAsDataURL(blob); - } - if (this.context.accumulator.audio) { - return new Sound( - this.context.accumulator.audio, - this.blockReceiver().newSoundName(localize('sound')) - ); - } - this.pushContext('doYield'); - this.pushContext(); -}; - -Process.prototype.audioBufferToWav = function (buffer, opt) { - var numChannels = buffer.numberOfChannels, - sampleRate = buffer.sampleRate, - format = (opt || {}).float32 ? 3 : 1, - bitDepth = format === 3 ? 32 : 16, - result; - - function interleave(inputL, inputR) { - var length = inputL.length + inputR.length, - result = new Float32Array(length), - index = 0, - inputIndex = 0; - - while (index < length) { - result[index++] = inputL[inputIndex]; - result[index++] = inputR[inputIndex]; - inputIndex += 1; - } - return result; - } - - if (numChannels === 2) { - result = interleave( - buffer.getChannelData(0), - buffer.getChannelData(1) - ); - } else { - result = buffer.getChannelData(0); - } - return this.encodeWAV(result, format, sampleRate, numChannels, bitDepth); -}; - -Process.prototype.encodeWAV = function ( - samples, - format, - sampleRate, - numChannels, - bitDepth -) { - var bytesPerSample = bitDepth / 8, - blockAlign = numChannels * bytesPerSample, - buffer = new ArrayBuffer(44 + samples.length * bytesPerSample), - view = new DataView(buffer); - - function writeFloat32(output, offset, input) { - for (var i = 0; i < input.length; i += 1, offset += 4) { - output.setFloat32(offset, input[i], true); - } - } - - function floatTo16BitPCM(output, offset, input) { - var i, s; - for (i = 0; i < input.length; i += 1, offset += 2) { - s = Math.max(-1, Math.min(1, input[i])); - output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); - } - } - - function writeString(view, offset, string) { - for (var i = 0; i < string.length; i += 1) { - view.setUint8(offset + i, string.charCodeAt(i)); - } - } - - writeString(view, 0, 'RIFF'); // RIFF identifier - // RIFF chunk length: - view.setUint32(4, 36 + samples.length * bytesPerSample, true); - writeString(view, 8, 'WAVE'); // RIFF type - writeString(view, 12, 'fmt '); // format chunk identifier - view.setUint32(16, 16, true); // format chunk length - view.setUint16(20, format, true); // sample format (raw) - view.setUint16(22, numChannels, true); // channel count - view.setUint32(24, sampleRate, true); // sample rate - // byte rate (sample rate * block align): - view.setUint32(28, sampleRate * blockAlign, true); - // block align (channel count * bytes per sample): - view.setUint16(32, blockAlign, true); - view.setUint16(34, bitDepth, true); // bits per sample - writeString(view, 36, 'data'); // data chunk identifier - // data chunk length: - view.setUint32(40, samples.length * bytesPerSample, true); - if (format === 1) { // Raw PCM - floatTo16BitPCM(view, 44, samples); - } else { - writeFloat32(view, 44, samples); - } - return buffer; -}; - -// Process audio input (interpolated) - -Process.prototype.reportAudio = function (choice) { - var stage = this.blockReceiver().parentThatIsA(StageMorph), - selection = this.inputOption(choice); - if (selection === 'resolution') { - return stage.microphone.binSize(); - } - if (selection === 'modifier') { - return stage.microphone.modifier; - } - if (stage.microphone.isOn()) { - switch (selection) { - case 'volume': - return stage.microphone.volume * 100; - case 'frequency': - return stage.microphone.pitch; - case 'note': - return stage.microphone.note; - case 'samples': - return new List(this.untype(stage.microphone.signals)); - case 'sample rate': - return stage.microphone.audioContext.sampleRate; - case 'output': - return new List(this.untype(stage.microphone.output)); - case 'spectrum': - return new List(this.untype(stage.microphone.frequencies)); - default: - return null; - } - } - this.pushContext('doYield'); - this.pushContext(); -}; - -Process.prototype.untype = function (typedArray) { - var len = typedArray.length, - arr = new Array(len), - i; - for (i = 0; i < len; i += 1) { - arr[i] = typedArray[i]; - } - return arr; -}; - -Process.prototype.setMicrophoneModifier = function (modifier) { - var stage = this.blockReceiver().parentThatIsA(StageMorph), - invalid = [ - 'sprite', - 'stage', - 'list', - 'costume', - 'sound', - 'number', - 'text', - 'Boolean' - ]; - if (!modifier || contains(invalid, this.reportTypeOf(modifier))) { - stage.microphone.modifier = null; - stage.microphone.stop(); - return; - } - stage.microphone.modifier = modifier; - stage.microphone.compiledModifier = this.reportCompiled(modifier, 1); - stage.microphone.compilerProcess = this; -}; - -// Process user prompting primitives (interpolated) - -Process.prototype.doAsk = function (data) { - var stage = this.homeContext.receiver.parentThatIsA(StageMorph), - rcvr = this.blockReceiver(), - isStage = rcvr instanceof StageMorph, - isHiddenSprite = rcvr instanceof SpriteMorph && !rcvr.isVisible, - activePrompter; - - stage.keysPressed = {}; - if (!this.prompter) { - activePrompter = detect( - stage.children, - morph => morph instanceof StagePrompterMorph - ); - if (!activePrompter) { - if (!isStage && !isHiddenSprite) { - rcvr.bubble(data, false, true); - } - this.prompter = new StagePrompterMorph( - isStage || isHiddenSprite ? data : null - ); - if (stage.scale < 1) { - this.prompter.setWidth(stage.width() - 10); - } else { - this.prompter.setWidth(stage.dimensions.x - 20); - } - this.prompter.fixLayout(); - this.prompter.setCenter(stage.center()); - this.prompter.setBottom(stage.bottom() - this.prompter.border); - stage.add(this.prompter); - this.prompter.inputField.edit(); - stage.changed(); - } - } else { - if (this.prompter.isDone) { - stage.lastAnswer = this.prompter.inputField.getValue(); - this.prompter.destroy(); - this.prompter = null; - if (!isStage) {rcvr.stopTalking(); } - return null; - } - } - this.pushContext('doYield'); - this.pushContext(); -}; - -Process.prototype.reportLastAnswer = function () { - return this.homeContext.receiver.parentThatIsA(StageMorph).lastAnswer; -}; - -// Process URI retrieval (interpolated) - -Process.prototype.reportURL = function (url) { - var response; - if (!this.httpRequest) { - // use the location protocol unless the user specifies otherwise - if (url.indexOf('//') < 0 || url.indexOf('//') > 8) { - if (location.protocol === 'file:') { - // allow requests from locally loaded sources - url = 'https://' + url; - } else { - url = location.protocol + '//' + url; - } - } - this.httpRequest = new XMLHttpRequest(); - this.httpRequest.open("GET", url, true); - // cache-control, commented out for now - // added for Snap4Arduino but has issues with local robot servers - // this.httpRequest.setRequestHeader('Cache-Control', 'max-age=0'); - this.httpRequest.send(null); - if (this.context.isCustomCommand) { - // special case when ignoring the result, e.g. when - // communicating with a robot to control its motors - this.httpRequest = null; - return null; - } - } else if (this.httpRequest.readyState === 4) { - response = this.httpRequest.responseText; - this.httpRequest = null; - return response; - } - this.pushContext('doYield'); - this.pushContext(); -}; - -// Process event messages primitives - -Process.prototype.doBroadcast = function (message) { - // messages are user-defined events, and by default global, same as in - // Scratch. An experimental feature, messages can be sent to a single - // sprite or to a list of sprites by using a 2-item list in the message - // slot, where the first slot is a message text, and the second slot - // its recipient(s), identified either by a single name or sprite, or by - // a list of names or sprites (can be a heterogeneous list). - - var stage = this.homeContext.receiver.parentThatIsA(StageMorph), - thisObj, - msg = this.inputOption(message), - trg, - rcvrs, - procs = []; - - if (!this.canBroadcast) { - return []; - } - if (message instanceof List && (message.length() === 2)) { - thisObj = this.blockReceiver(); - msg = message.at(1); - trg = message.at(2); - if (isSnapObject(trg)) { - rcvrs = [trg]; - } else if (isString(trg)) { - // assume the string to be the name of a sprite or the stage - if (trg === stage.name) { - rcvrs = [stage]; - } else { - rcvrs = [this.getOtherObject(trg, thisObj, stage)]; - } - } else if (trg instanceof List) { - // assume all elements to be sprites or sprite names - rcvrs = trg.itemsArray().map(each => - this.getOtherObject(each, thisObj, stage) - ); - } else { - return; // abort - } - } else { // global - rcvrs = stage.children.concat(stage); - } - if (msg !== '') { - stage.lastMessage = message; // the actual data structure - rcvrs.forEach(morph => { - if (isSnapObject(morph)) { - morph.allHatBlocksFor(msg).forEach(block => { - procs.push(stage.threads.startProcess( - block, - morph, - stage.isThreadSafe - )); - }); - } - }); - (stage.messageCallbacks[''] || []).forEach(callback => - callback(msg) // for "any" message, pass it along as argument - ); - (stage.messageCallbacks[msg] || []).forEach(callback => - callback() // for a particular message - ); - } - return procs; -}; - -Process.prototype.doBroadcastAndWait = function (message) { - if (!this.context.activeSends) { - this.context.activeSends = this.doBroadcast(message); - if (this.isRunning()) { - this.context.activeSends.forEach(proc => - proc.runStep() - ); - } - } - this.context.activeSends = this.context.activeSends.filter(proc => - proc.isRunning() - ); - if (this.context.activeSends.length === 0) { - return null; - } - this.pushContext('doYield'); - this.pushContext(); -}; - -Process.prototype.getLastMessage = function () { - var stage; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - return stage.getLastMessage(); - } - } - return ''; -}; - -Process.prototype.doSend = function (message, target) { - var stage = this.homeContext.receiver.parentThatIsA(StageMorph); - this.doBroadcast( - new List( - [ - message, - target instanceof List ? target : - target === stage.name ? new List([stage]) : - new List([target]) - ] - ) - ); -}; - -// Process type inference - -Process.prototype.reportIsA = function (thing, typeString) { - return this.reportTypeOf(thing) === this.inputOption(typeString); -}; - -Process.prototype.assertType = function (thing, typeString) { - // make sure "thing" is a particular type or any of a number of types - // and raise an error if not - // use responsibly wrt performance implications - var thingType = this.reportTypeOf(thing); - if (thingType === typeString) {return true; } - if (typeString instanceof Array && contains(typeString, thingType)) { - return true; - } - throw new Error('expecting ' + typeString + ' but getting ' + thingType); -}; - -Process.prototype.assertAlive = function (thing) { - if (thing && thing.isCorpse) { - throw new Error('cannot operate on a deleted sprite'); - } -}; - -Process.prototype.reportTypeOf = function (thing) { - // answer a string denoting the argument's type - var exp; - if (thing === null || (thing === undefined)) { - return 'nothing'; - } - if (thing === true || (thing === false)) { - return 'Boolean'; - } - if (thing instanceof List) { - return 'list'; - } - if (parseFloat(thing) === +thing) { // I hate this! -Jens - return 'number'; - } - if (isString(thing)) { - return 'text'; - } - if (thing instanceof SpriteMorph) { - return 'sprite'; - } - if (thing instanceof StageMorph) { - return 'stage'; - } - if (thing instanceof Costume) { - return 'costume'; - } - if (thing instanceof Sound) { - return 'sound'; - } - if (thing instanceof Context) { - if (thing.expression instanceof RingMorph) { - return thing.expression.dataType(); - } - if (thing.expression instanceof ReporterBlockMorph) { - if (thing.expression.isPredicate) { - return 'predicate'; - } - return 'reporter'; - } - - if (thing.expression instanceof Array) { - exp = thing.expression[thing.pc || 0]; - if (exp.isPredicate) { - return 'predicate'; - } - if (exp instanceof RingMorph) { - return exp.dataType(); - } - if (exp instanceof ReporterBlockMorph) { - return 'reporter'; - } - if (exp instanceof CommandBlockMorph) { - return 'command'; - } - return 'reporter'; // 'ring'; - } - - if (thing.expression instanceof CommandBlockMorph) { - return 'command'; - } - return 'reporter'; // 'ring'; - } - return 'undefined'; -}; - -// Process math primtives - hyper-dyadic - -Process.prototype.hyperDyadic = function (baseOp, a, b) { - // enable dyadic operations to be performed on lists and tables - var len, a_info, b_info, i, result; - if (this.enableHyperOps) { - a_info = this.examine(a); - b_info = this.examine(b); - if (a_info.isScalar && b_info.isScalar && - (a_info.rank !== b_info.rank)) { - // keep the shape of the higher rank - return this.hyperZip( - baseOp, - a_info.rank > b_info.rank ? a : a_info.scalar, - b_info.rank > a_info.rank ? b : b_info.scalar - ); - } - if (a_info.rank > 1) { - if (b_info.rank > 1) { - if (a.length() !== b.length()) { - // test for special cased scalars in single-item lists - if (a_info.isScalar) { - return this.hyperDyadic(baseOp, a_info.scalar, b); - } - if (b_info.isScalar) { - return this.hyperDyadic(baseOp, a, b_info.scalar); - } - } - // zip both arguments ignoring out-of-bounds indices - a = a.asArray(); - b = b.asArray(); - len = Math.min(a.length, b.length); - result = new Array(len); - for (i = 0; i < len; i += 1) { - result[i] = this.hyperDyadic(baseOp, a[i], b[i]); - } - return new List(result); - } - if (a_info.isScalar) { - return this.hyperZip(baseOp, a_info.scalar, b); - } - return a.map(each => this.hyperDyadic(baseOp, each, b)); - } - if (b_info.rank > 1) { - if (b_info.isScalar) { - return this.hyperZip(baseOp, a, b_info.scalar); - } - return b.map(each => this.hyperDyadic(baseOp, a, each)); - } - return this.hyperZip(baseOp, a, b); - } - return baseOp(a, b); -}; - -Process.prototype.hyperZip = function (baseOp, a, b) { - // enable dyadic operations to be performed on lists and tables - var len, i, result, - a_info = this.examine(a), - b_info = this.examine(b); - if (a instanceof List) { - if (b instanceof List) { - if (a.length() !== b.length()) { - // test for special cased scalars in single-item lists - if (a_info.isScalar) { - return this.hyperZip(baseOp, a_info.scalar, b); - } - if (b_info.isScalar) { - return this.hyperZip(baseOp, a, b_info.scalar); - } - } - // zip both arguments ignoring out-of-bounds indices - a = a.asArray(); - b = b.asArray(); - len = Math.min(a.length, b.length); - result = new Array(len); - for (i = 0; i < len; i += 1) { - result[i] = this.hyperZip(baseOp, a[i], b[i]); - } - return new List(result); - } - return a.map(each => this.hyperZip(baseOp, each, b)); - } - if (b instanceof List) { - return b.map(each => this.hyperZip(baseOp, a, each)); - } - return baseOp(a, b); -}; - -Process.prototype.dimensions = function (data) { - var dim = [], - cur = data; - while (cur instanceof List) { - dim.push(cur.length()); - cur = cur.at(1); - } - return dim; -}; - -Process.prototype.isMatrix = function (data) { - return this.rank(data) > 1; -}; - -Process.prototype.rank = function(data) { - return this.dimensions(data).length; -}; - -Process.prototype.isScalar = function (data) { - return this.dimensions.every(n => n === 1); -}; - -Process.prototype.scalar = function (data) { - var cur = data; - while (cur instanceof List) { - cur = cur.at(1); - } - return cur; -}; - -Process.prototype.examine = function (data) { - var cur = data, - meta = { - rank: 0, - isScalar: true, - scalar: null - }; - while (cur instanceof List) { - meta.rank += 1; - if (cur.length() !== 1) { - meta.isScalar = false; - } - cur = cur.at(1); - } - meta.scalar = cur; - return meta; -}; - -// Process math primtives - arithmetic - -Process.prototype.reportSum = function (a, b) { - return this.hyperDyadic(this.reportBasicSum, a, b); -}; - -Process.prototype.reportBasicSum = function (a, b) { - return +a + (+b); -}; - -Process.prototype.reportDifference = function (a, b) { - return this.hyperDyadic(this.reportBasicDifference, a, b); -}; - -Process.prototype.reportBasicDifference = function (a, b) { - return +a - +b; -}; - -Process.prototype.reportProduct = function (a, b) { - return this.hyperDyadic(this.reportBasicProduct, a, b); -}; - -Process.prototype.reportBasicProduct = function (a, b) { - return +a * +b; -}; - -Process.prototype.reportQuotient = function (a, b) { - return this.hyperDyadic(this.reportBasicQuotient, a, b); -}; - -Process.prototype.reportBasicQuotient = function (a, b) { - return +a / +b; -}; - -Process.prototype.reportPower = function (a, b) { - return this.hyperDyadic(this.reportBasicPower, a, b); -}; - -Process.prototype.reportBasicPower = function (a, b) { - return Math.pow(+a, +b); -}; - -Process.prototype.reportModulus = function (a, b) { - return this.hyperDyadic(this.reportBasicModulus, a, b); -}; - -Process.prototype.reportBasicModulus = function (a, b) { - var x = +a, - y = +b; - return ((x % y) + y) % y; -}; - -Process.prototype.reportRandom = function (a, b) { - return this.hyperDyadic(this.reportBasicRandom, a, b); -}; - -Process.prototype.reportBasicRandom = function (min, max) { - var floor = +min, - ceil = +max; - if ((floor % 1 !== 0) || (ceil % 1 !== 0)) { - return Math.random() * (ceil - floor) + floor; - } - return Math.floor(Math.random() * (ceil - floor + 1)) + floor; -}; - -// Process logic primitives - hyper-diadic / monadic where applicable - -Process.prototype.reportLessThan = function (a, b) { - return this.hyperDyadic(this.reportBasicLessThan, a, b); -}; - -Process.prototype.reportBasicLessThan = function (a, b) { - var x = +a, - y = +b; - if (isNaN(x) || isNaN(y)) { - x = a; - y = b; - } - return x < y; -}; - -Process.prototype.reportNot = function (bool) { - if (this.enableHyperOps) { - if (bool instanceof List) { - return bool.map(each => this.reportNot(each)); - } - } - // this.assertType(bool, 'Boolean'); - return !bool; -}; - -Process.prototype.reportGreaterThan = function (a, b) { - return this.hyperDyadic(this.reportBasicGreaterThan, a, b); -}; - -Process.prototype.reportBasicGreaterThan = function (a, b) { - var x = +a, - y = +b; - if (isNaN(x) || isNaN(y)) { - x = a; - y = b; - } - return x > y; -}; - -Process.prototype.reportEquals = function (a, b) { - return snapEquals(a, b); -}; - -Process.prototype.reportIsIdentical = function (a, b) { - var tag = 'idTag'; - if (this.isImmutable(a) || this.isImmutable(b)) { - return snapEquals(a, b); - } - - function clear() { - if (Object.prototype.hasOwnProperty.call(a, tag)) { - delete a[tag]; - } - if (Object.prototype.hasOwnProperty.call(b, tag)) { - delete b[tag]; - } - } - - clear(); - a[tag] = Date.now(); - if (b[tag] === a[tag]) { - clear(); - return true; - } - clear(); - return false; -}; - -Process.prototype.isImmutable = function (obj) { - // private - var type = this.reportTypeOf(obj); - return type === 'nothing' || - type === 'Boolean' || - type === 'text' || - type === 'number' || - type === 'undefined'; -}; - -Process.prototype.reportBoolean = function (bool) { - return bool; -}; - -// Process hyper-monadic primitives - -Process.prototype.reportRound = function (n) { - if (this.enableHyperOps) { - if (n instanceof List) { - return n.map(each => this.reportRound(each)); - } - } - return Math.round(+n); -}; - -Process.prototype.reportMonadic = function (fname, n) { - if (this.enableHyperOps) { - if (n instanceof List) { - return n.map(each => this.reportMonadic(fname, each)); - } - } - - var x = +n, - result = 0; - - switch (this.inputOption(fname)) { - case 'abs': - result = Math.abs(x); - break; - // case '\u2212': // minus-sign - case 'neg': - result = n * -1; - break; - case 'ceiling': - result = Math.ceil(x); - break; - case 'floor': - result = Math.floor(x); - break; - case 'sqrt': - result = Math.sqrt(x); - break; - case 'sin': - result = Math.sin(radians(x)); - break; - case 'cos': - result = Math.cos(radians(x)); - break; - case 'tan': - result = Math.tan(radians(x)); - break; - case 'asin': - result = degrees(Math.asin(x)); - break; - case 'acos': - result = degrees(Math.acos(x)); - break; - case 'atan': - result = degrees(Math.atan(x)); - break; - case 'ln': - result = Math.log(x); - break; - case 'log': // base 10 - result = Math.log10(x); - break; - case 'lg': // base 2 - result = Math.log2(x); - break; - case 'e^': - result = Math.exp(x); - break; - case '10^': - result = Math.pow(10, x); - break; - case '2^': - result = Math.pow(2, x); - break; - default: - nop(); - } - return result; -}; - -// Process - non hyper-monadic text primitives - -Process.prototype.reportTextFunction = function (fname, string) { - // currently in dev mode only, not hyper-monadic - var x = (isNil(string) ? '' : string).toString(), - result = ''; - - switch (this.inputOption(fname)) { - case 'encode URI': - result = encodeURI(x); - break; - case 'decode URI': - result = decodeURI(x); - break; - case 'encode URI component': - result = encodeURIComponent(x); - break; - case 'decode URI component': - result = decodeURIComponent(x); - break; - case 'XML escape': - result = new XML_Element().escape(x); - break; - case 'XML unescape': - result = new XML_Element().unescape(x); - break; - case 'hex sha512 hash': - result = hex_sha512(x); - break; - default: - nop(); - } - return result; -}; - -Process.prototype.reportJoin = function (a, b) { - var x = (isNil(a) ? '' : a).toString(), - y = (isNil(b) ? '' : b).toString(); - return x.concat(y); -}; - -Process.prototype.reportJoinWords = function (aList) { - if (aList instanceof List) { - return aList.asText(); - } - return (aList || '').toString(); -}; - -// Process string ops - hyper-monadic/dyadic - -Process.prototype.reportLetter = function (idx, string) { - return this.hyperDyadic( - (ix, str) => this.reportBasicLetter(ix, str), - idx, - string - ); -}; - -Process.prototype.reportBasicLetter = function (idx, string) { - var str, i; - - str = isNil(string) ? '' : string.toString(); - if (this.inputOption(idx) === 'any') { - idx = this.reportBasicRandom(1, str.length); - } - if (this.inputOption(idx) === 'last') { - idx = str.length; - } - i = +(idx || 0); - return str[i - 1] || ''; -}; - -Process.prototype.reportStringSize = function (data) { - if (this.enableHyperOps) { - if (data instanceof List) { - return data.map(each => this.reportStringSize(each)); - } - } - if (data instanceof List) { // catch a common user error - return data.length(); - } - return isNil(data) ? 0 : data.toString().length; -}; - -Process.prototype.reportUnicode = function (string) { - var str; - - if (this.enableHyperOps) { - if (string instanceof List) { - return string.map(each => this.reportUnicode(each)); - } - str = isNil(string) ? '\u0000' : string.toString(); - if (str.length > 1) { - return this.reportUnicode(new List(str.split(''))); - } - } else { - str = isNil(string) ? '\u0000' : string.toString(); - } - if (str.codePointAt) { // support for Unicode in newer browsers. - return str.codePointAt(0) || 0; - } - return str.charCodeAt(0) || 0; -}; - -Process.prototype.reportUnicodeAsLetter = function (num) { - if (this.enableHyperOps) { - if (num instanceof List) { - return num.map(each => this.reportUnicodeAsLetter(each)); - } - } - - var code = +(num || 0); - - if (String.fromCodePoint) { // support for Unicode in newer browsers. - return String.fromCodePoint(code); - } - return String.fromCharCode(code); -}; - -Process.prototype.reportTextSplit = function (string, delimiter) { - return this.hyperDyadic( - (str, delim) => this.reportBasicTextSplit(str, delim), - string, - delimiter - ); -}; - -Process.prototype.reportBasicTextSplit = function (string, delimiter) { - var types = ['text', 'number'], - strType = this.reportTypeOf(string), - delType = this.reportTypeOf(this.inputOption(delimiter)), - str, - del; - if (!contains(types, strType)) { - throw new Error('expecting text instead of a ' + strType); - } - if (!contains(types, delType)) { - throw new Error('expecting a text delimiter instead of a ' + delType); - } - str = isNil(string) ? '' : string.toString(); - switch (this.inputOption(delimiter)) { - case 'line': - // Unicode compliant line splitting (platform independent) - // http://www.unicode.org/reports/tr18/#Line_Boundaries - del = /\r\n|[\n\v\f\r\x85\u2028\u2029]/; - break; - case 'tab': - del = '\t'; - break; - case 'cr': - del = '\r'; - break; - case 'word': - case 'whitespace': - str = str.trim(); - del = /\s+/; - break; - case 'letter': - del = ''; - break; - case 'csv': - return this.parseCSV(string); - case 'json': - return this.parseJSON(string); - /* - case 'csv records': - return this.parseCSVrecords(string); - case 'csv fields': - return this.parseCSVfields(string); - */ - default: - del = isNil(delimiter) ? '' : delimiter.toString(); - } - return new List(str.split(del)); -}; - -// Process - parsing primitives - -Process.prototype.parseCSV = function (text) { - // try to address the kludge that Excel sometimes uses commas - // and sometimes semi-colons as delimiters, try to find out - // which makes more sense by examining the first line - return this.rawParseCSV(text, this.guessDelimiterCSV(text)); -}; - -Process.prototype.guessDelimiterCSV = function (text) { - // assumes that the first line contains the column headers. - // report the first delimiter for which parsing the header - // yields more than a single field, otherwise default to comma - var delims = [',', ';', '|', '\t'], - len = delims.length, - firstLine = text.split('\n')[0], - i; - for (i = 0; i < len; i += 1) { - if (this.rawParseCSV(firstLine, delims[i]).length() > 1) { - return delims[i]; - } - } - return delims[0]; -}; - -Process.prototype.rawParseCSV = function (text, delim) { - // RFC 4180 - // parse a csv table into a two-dimensional list. - // if the table contains just a single row return it a one-dimensional - // list of fields instead (for backwards-compatibility) - var prev = '', - fields = [''], - records = [fields], - col = 0, - r = 0, - esc = true, - len = text.length, - idx, - char; - delim = delim || ','; - for (idx = 0; idx < len; idx += 1) { - char = text[idx]; - if (char === '\"') { - if (esc && char === prev) { - fields[col] += char; - } - esc = !esc; - } else if (char === delim && esc) { - char = ''; - col += 1; - fields[col] = char; - } else if (char === '\r' && esc) { - r += 1; - records[r] = ['']; - fields = records[r]; - col = 0; - } else if (char === '\n' && esc) { - if (prev !== '\r') { - r += 1; - records[r] = ['']; - fields = records[r]; - col = 0; - } - } else { - fields[col] += char; - } - prev = char; - } - - // remove the last record, if it is empty - if (records[records.length - 1].length === 1 && - records[records.length - 1][0] === '') - { - records.pop(); - } - - // convert arrays to Snap! Lists - records = new List( - records.map(row => new List(row)) - ); - - // for backwards compatibility return the first row if it is the only one - if (records.length() === 1) { - return records.at(1); - } - return records; -}; - -Process.prototype.parseJSON = function (string) { - // Bernat's original Snapi contribution - function listify(jsonObject) { - if (jsonObject instanceof Array) { - return new List( - jsonObject.map(function(eachElement) { - return listify(eachElement); - }) - ); - } else if (jsonObject instanceof Object) { - return new List( - Object.keys(jsonObject).map(function(eachKey) { - return new List([ - eachKey, - listify(jsonObject[eachKey]) - ]); - }) - ); - } else { - return jsonObject; - } - } - - return listify(JSON.parse(string)); -}; - -// Process debugging - -Process.prototype.alert = function (data) { - // debugging primitives only work in dev mode, otherwise they're nop - var world; - if (this.homeContext.receiver) { - world = this.homeContext.receiver.world(); - if (world.isDevMode) { - alert('Snap! ' + data.asArray()); - } - } -}; - -Process.prototype.log = function (data) { - // debugging primitives only work in dev mode, otherwise they're nop - var world; - if (this.homeContext.receiver) { - world = this.homeContext.receiver.world(); - if (world.isDevMode) { - console.log('Snap! ' + data.asArray()); - } - } -}; - -// Process motion primitives - -Process.prototype.getOtherObject = function (name, thisObj, stageObj) { - // private, find the sprite indicated by the given name - // either onstage or in the World's hand - - // deal with first-class sprites - if (isSnapObject(name)) { - return name; - } - - if (this.inputOption(name) === 'myself') { - return thisObj; - } - var stage = isNil(stageObj) ? - thisObj.parentThatIsA(StageMorph) : stageObj, - thatObj = null; - if (stage) { - // find the corresponding sprite on the stage - thatObj = detect( - stage.children, - morph => morph.name === name - ); - if (!thatObj) { - // check if the sprite in question is currently being - // dragged around - thatObj = detect( - stage.world().hand.children, - morph => morph instanceof SpriteMorph && - morph.name === name - ); - } - } - return thatObj; -}; - -Process.prototype.getObjectsNamed = function (name, thisObj, stageObj) { - // private, find all sprites and their clones indicated - // by the given name either onstage or in the World's hand - - var stage = isNil(stageObj) ? - thisObj.parentThatIsA(StageMorph) : stageObj, - those = []; - - function check(obj) { - return obj instanceof SpriteMorph && obj.isTemporary ? - obj.cloneOriginName === name : obj.name === name; - } - - if (stage) { - // find the corresponding sprite on the stage - those = stage.children.filter(check); - if (!those.length) { - // check if a sprite in question is currently being - // dragged around - those = stage.world().hand.children.filter(check); - } - } - return those; -}; - -Process.prototype.setHeading = function (direction) { - var thisObj = this.blockReceiver(); - - if (thisObj) { - if (this.inputOption(direction) === 'random') { - direction = this.reportBasicRandom(1, 36000) / 100; - } - thisObj.setHeading(direction); - } -}; - -Process.prototype.doFaceTowards = function (name) { - var thisObj = this.blockReceiver(), - thatObj; - - if (thisObj) { - if (this.inputOption(name) === 'center') { - thisObj.faceToXY(0, 0); - } else if (this.inputOption(name) === 'mouse-pointer') { - thisObj.faceToXY(this.reportMouseX(), this.reportMouseY()); - } else if (this.inputOption(name) === 'random position') { - thisObj.setHeading(this.reportBasicRandom(1, 36000) / 100); - } else { - if (name instanceof List) { - thisObj.faceToXY( - name.at(1), - name.at(2) - ); - return; - } - thatObj = this.getOtherObject(name, this.homeContext.receiver); - if (thatObj) { - thisObj.faceToXY( - thatObj.xPosition(), - thatObj.yPosition() - ); - } - } - } -}; - -Process.prototype.doGotoObject = function (name) { - var thisObj = this.blockReceiver(), - thatObj, - stage; - - if (thisObj) { - if (this.inputOption(name) === 'center') { - thisObj.gotoXY(0, 0); - } else if (this.inputOption(name) === 'mouse-pointer') { - thisObj.gotoXY(this.reportMouseX(), this.reportMouseY()); - } else if (this.inputOption(name) === 'random position') { - stage = thisObj.parentThatIsA(StageMorph); - if (stage) { - thisObj.setCenter(new Point( - this.reportBasicRandom(stage.left(), stage.right()), - this.reportBasicRandom(stage.top(), stage.bottom()) - )); - } - } else { - if (name instanceof List) { - thisObj.gotoXY( - name.at(1), - name.at(2) - ); - return; - } - thatObj = this.getOtherObject(name, this.homeContext.receiver); - if (thatObj) { - thisObj.gotoXY( - thatObj.xPosition(), - thatObj.yPosition() - ); - } - } - } -}; - -// Process layering primitives - -Process.prototype.goToLayer = function (name) { - var option = this.inputOption(name), - thisObj = this.blockReceiver(); - if (thisObj instanceof SpriteMorph) { - if (option === 'front') { - thisObj.comeToFront(); - } else if (option === 'back') { - thisObj.goToBack(); - } - } -}; - -// Process color primitives - -Process.prototype.setHSVA = function (name, num) { - var options = ['hue', 'saturation', 'brightness', 'transparency']; - this.blockReceiver().setColorComponentHSVA( - options.indexOf(this.inputOption(name)), - +num - ); -}; - -Process.prototype.changeHSVA = function (name, num) { - var options = ['hue', 'saturation', 'brightness', 'transparency']; - this.blockReceiver().changeColorComponentHSVA( - options.indexOf(this.inputOption(name)), - +num - ); -}; - -Process.prototype.setPenHSVA = Process.prototype.setHSVA; -Process.prototype.changePenHSVA = Process.prototype.changeHSVA; -Process.prototype.setBackgroundHSVA = Process.prototype.setHSVA; -Process.prototype.changeBackgroundHSVA = Process.prototype.changeHSVA; - -// Process pasting primitives - -Process.prototype.doPasteOn = function (name, thisObj, stage) { - // allow for lists of sprites and also check for temparary clones, - // as in Scratch 2.0, - var those; - thisObj = thisObj || this.blockReceiver(); - stage = stage || thisObj.parentThatIsA(StageMorph); - if (stage.name === name) { - name = stage; - } - if (isSnapObject(name)) { - return thisObj.pasteOn(name); - } - if (name instanceof List) { // assume all elements to be sprites - those = name.itemsArray(); - } else { - those = this.getObjectsNamed(name, thisObj, stage); // clones - } - those.forEach(each => - this.doPasteOn(each, thisObj, stage) - ); -}; - -// Process temporary cloning (Scratch-style) - -Process.prototype.createClone = function (name) { - var thisObj = this.blockReceiver(), - thatObj; - - if (!name || this.readyToTerminate) {return; } - if (thisObj) { - if (this.inputOption(name) === 'myself') { - thisObj.createClone(!this.isFirstStep); - } else { - thatObj = this.getOtherObject(name, thisObj); - if (thatObj) { - thatObj.createClone(!this.isFirstStep); - } - } - } -}; - -Process.prototype.newClone = function (name) { - var thisObj = this.blockReceiver(), - thatObj; - - if (!name) {return; } - if (thisObj) { - if (this.inputOption(name) === 'myself') { - return thisObj.newClone(!this.isFirstStep); - } - thatObj = this.getOtherObject(name, thisObj); - if (thatObj) { - return thatObj.newClone(!this.isFirstStep); - } - } -}; - -// Process sensing primitives - -Process.prototype.reportTouchingObject = function (name) { - var thisObj = this.blockReceiver(); - - if (thisObj) { - return this.objectTouchingObject(thisObj, name); - } - return false; -}; - -Process.prototype.objectTouchingObject = function (thisObj, name) { - // helper function for reportTouchingObject() - // also check for temparary clones, as in Scratch 2.0, - // and for any parts (subsprites) - var those, - stage, - box, - mouse; - - if (this.inputOption(name) === 'mouse-pointer') { - mouse = thisObj.world().hand.position(); - if (thisObj.bounds.containsPoint(mouse) && - !thisObj.isTransparentAt(mouse)) { - return true; - } - } else { - stage = thisObj.parentThatIsA(StageMorph); - if (stage) { - if (this.inputOption(name) === 'edge') { - box = thisObj.bounds; - if (!thisObj.costume && thisObj.penBounds) { - box = thisObj.penBounds.translateBy(thisObj.position()); - } - if (!stage.bounds.containsRectangle(box)) { - return true; - } - } - if (this.inputOption(name) === 'pen trails' && - thisObj.isTouching(stage.penTrailsMorph())) { - return true; - } - if (isSnapObject(name)) { - return name.isVisible && thisObj.isTouching(name); - } - if (name instanceof List) { // assume all elements to be sprites - those = name.itemsArray(); - } else { - those = this.getObjectsNamed(name, thisObj, stage); // clones - } - if (those.some(any => any.isVisible && thisObj.isTouching(any) - // check collision with any part, performance issue - // commented out for now - /* - return any.allParts().some(function (part) { - return part.isVisible && thisObj.isTouching(part); - }) - */ - )) { - return true; - } - } - } - return thisObj.parts.some(any => - this.objectTouchingObject(any, name) - ); -}; - -Process.prototype.reportAspect = function (aspect, location) { - // sense colors and sprites anywhere, - // use sprites to read/write data encoded in colors. - // - // usage: - // ------ - // left input selects color/saturation/brightness/transparency or "sprites". - // right input selects "mouse-pointer", "myself" or name of another sprite. - // you can also embed a a reporter with a reference to a sprite itself - // or a list of two items representing x- and y- coordinates. - // - // what you'll get: - // ---------------- - // left input (aspect): - // - // 'hue' - hsv HUE on a scale of 0 - 100 - // 'saturation' - hsv SATURATION on a scale of 0 - 100 - // 'brightness' - hsv VALUE on a scale of 0 - 100 - // 'transparency' - rgba ALPHA on a reversed (!) scale of 0 - 100 - // 'r-g-b-a' - list of rgba values on a scale of 0 - 255 each - // 'sprites' - a list of sprites at the location, empty if none - // - // right input (location): - // - // 'mouse-pointer' - color/sprites at mouse-pointer anywhere in Snap - // 'myself' - sprites at or color UNDERNEATH the rotation center - // sprite-name - sprites at or color UNDERNEATH sprites's rot-ctr. - // two-item-list - color/sprites at x-/y- coordinates on the Stage - // - // what does "underneath" mean? - // ---------------------------- - // the not-fully-transparent color of the top-layered sprite at the given - // location excluding the receiver sprite's own layer and all layers above - // it gets reported. - // - // color-aspect "underneath" a sprite means that the sprite's layer is - // relevant for what gets reported. Sprites can only sense colors in layers - // below themselves, not their own color and not colors in sprites above - // their own layer. - - var choice = this.inputOption(aspect), - target = this.inputOption(location), - options = ['hue', 'saturation', 'brightness', 'transparency'], - idx = options.indexOf(choice), - thisObj = this.blockReceiver(), - thatObj, - stage = thisObj.parentThatIsA(StageMorph), - world = thisObj.world(), - point, - clr; - - if (target === 'myself') { - if (choice === 'sprites') { - if (thisObj instanceof StageMorph) { - point = thisObj.center(); - } else { - point = thisObj.rotationCenter(); - } - return this.spritesAtPoint(point, stage); - } else { - clr = this.colorAtSprite(thisObj); - } - } else if (target === 'mouse-pointer') { - if (choice === 'sprites') { - return this.spritesAtPoint(world.hand.position(), stage); - } else { - clr = world.getGlobalPixelColor(world.hand.position()); - } - } else if (target instanceof List) { - point = new Point( - target.at(1) * stage.scale + stage.center().x, - stage.center().y - (target.at(2) * stage.scale) - ); - if (choice === 'sprites') { - return this.spritesAtPoint(point, stage); - } else { - clr = world.getGlobalPixelColor(point); - } - } else { - if (!target) {return; } - thatObj = this.getOtherObject(target, thisObj, stage); - if (thatObj) { - if (choice === 'sprites') { - point = thatObj instanceof SpriteMorph ? - thatObj.rotationCenter() : thatObj.center(); - return this.spritesAtPoint(point, stage); - } else { - clr = this.colorAtSprite(thatObj); - } - } else { - return; - } - - } - - if (choice === 'r-g-b-a') { - return new List([clr.r, clr.g, clr.b, Math.round(clr.a * 255)]); - } - if (idx < 0 || idx > 3) { - return; - } - if (idx === 3) { - return (1 - clr.a) * 100; - } - return clr.hsv()[idx] * 100; -}; - -Process.prototype.colorAtSprite = function (sprite) { - // private - helper function for aspect of location - // answer the top-most color at the sprite's rotation center - // excluding the sprite itself - var point = sprite instanceof SpriteMorph ? sprite.rotationCenter() - : sprite.center(), - stage = sprite.parentThatIsA(StageMorph), - child, - i; - - if (!stage) {return new Color(); } - for (i = stage.children.length; i > 0; i -= 1) { - child = stage.children[i - 1]; - if ((child !== sprite) && - child.isVisible && - child.bounds.containsPoint(point) && - !child.isTransparentAt(point) - ) { - return child.getPixelColor(point); - } - } - if (stage.bounds.containsPoint(point)) { - return stage.getPixelColor(point); - } - return new Color(); -}; - -Process.prototype.colorBelowSprite = function (sprite) { - // private - helper function for aspect of location - // answer the color underneath the layer of the sprite's rotation center - // NOTE: layer-aware color sensing is currently unused - // in favor of top-layer detection because of user-observations - var point = sprite instanceof SpriteMorph ? sprite.rotationCenter() - : sprite.center(), - stage = sprite.parentThatIsA(StageMorph), - below = stage, - found = false, - child, - i; - - if (!stage) {return new Color(); } - for (i = 0; i < stage.children.length; i += 1) { - if (!found) { - child = stage.children[i]; - if (child === sprite) { - found = true; - } else if (child.isVisible && - child.bounds.containsPoint(point) && - !child.isTransparentAt(point) - ) { - below = child; - } - } - } - if (below.bounds.containsPoint(point)) { - return below.getPixelColor(point); - } - return new Color(); -}; - -Process.prototype.spritesAtPoint = function (point, stage) { - // private - helper function for aspect of location - // point argument is an absolute (Morphic) point - // answer a list of sprites, if any, at the given point - // ordered by their layer, i.e. top-layer is last in the list - return new List( - stage.children.filter(morph => - morph instanceof SpriteMorph && - morph.isVisible && - morph.bounds.containsPoint(point) && - !morph.isTransparentAt(point) - ) - ); -}; - -Process.prototype.reportRelationTo = function (relation, name) { - var rel = this.inputOption(relation); - if (rel === 'distance') { - return this.reportDistanceTo(name); - } - if (rel === 'direction') { - return this.reportDirectionTo(name); - } - return 0; -}; - -Process.prototype.reportDistanceTo = function (name) { - var thisObj = this.blockReceiver(), - thatObj, - stage, - rc, - point; - - if (thisObj) { - rc = thisObj.rotationCenter(); - point = rc; - if (this.inputOption(name) === 'mouse-pointer') { - point = thisObj.world().hand.position(); - } else if (this.inputOption(name) === 'center') { - return new Point(thisObj.xPosition(), thisObj.yPosition()) - .distanceTo(ZERO); - } else if (name instanceof List) { - return new Point(thisObj.xPosition(), thisObj.yPosition()) - .distanceTo(new Point(name.at(1), name.at(2))); - } - stage = thisObj.parentThatIsA(StageMorph); - thatObj = this.getOtherObject(name, thisObj, stage); - if (thatObj) { - point = thatObj.rotationCenter(); - } - return rc.distanceTo(point) / stage.scale; - } - return 0; -}; - -Process.prototype.reportDirectionTo = function (name) { - var thisObj = this.blockReceiver(), - thatObj; - - if (thisObj) { - if (this.inputOption(name) === 'mouse-pointer') { - return thisObj.angleToXY(this.reportMouseX(), this.reportMouseY()); - } - if (this.inputOption(name) === 'center') { - return thisObj.angleToXY(0, 0); - } - if (name instanceof List) { - return thisObj.angleToXY( - name.at(1), - name.at(2) - ); - } - thatObj = this.getOtherObject(name, this.homeContext.receiver); - if (thatObj) { - return thisObj.angleToXY( - thatObj.xPosition(), - thatObj.yPosition() - ); - } - return thisObj.direction(); - } - return 0; -}; - -Process.prototype.reportAttributeOf = function (attribute, name) { - var thisObj = this.blockReceiver(), - thatObj, - stage; - - if (thisObj) { - this.assertAlive(thisObj); - stage = thisObj.parentThatIsA(StageMorph); - if (stage.name === name) { - thatObj = stage; - } else { - thatObj = this.getOtherObject(name, thisObj, stage); - } - if (thatObj) { - this.assertAlive(thatObj); - if (attribute instanceof BlockMorph) { // a "wish" - return this.reportContextFor( - this.reify( - thatObj.getMethod(attribute.semanticSpec) - .blockInstance(), - new List() - ), - thatObj - ); - } - if (attribute instanceof Context) { - return this.reportContextFor(attribute, thatObj); - } - if (isString(attribute)) { - return thatObj.variables.getVar(attribute); - } - switch (this.inputOption(attribute)) { - case 'x position': - return thatObj.xPosition ? thatObj.xPosition() : ''; - case 'y position': - return thatObj.yPosition ? thatObj.yPosition() : ''; - case 'direction': - return thatObj.direction ? thatObj.direction() : ''; - case 'costume #': - return thatObj.getCostumeIdx(); - case 'costume name': - return thatObj.costume ? thatObj.costume.name - : thatObj instanceof SpriteMorph ? localize('Turtle') - : localize('Empty'); - case 'size': - return thatObj.getScale ? thatObj.getScale() : ''; - case 'volume': - return thatObj.getVolume(); - case 'balance': - return thatObj.getPan(); - case 'width': - if (thatObj instanceof StageMorph) { - return thatObj.dimensions.x; - } - this.assertType(thatObj, 'sprite'); - return thatObj.width() / stage.scale; - case 'height': - if (thatObj instanceof StageMorph) { - return thatObj.dimensions.y; - } - this.assertType(thatObj, 'sprite'); - return thatObj.height() / stage.scale; - case 'left': - return thatObj.xLeft(); - case 'right': - return thatObj.xRight(); - case 'top': - return thatObj.yTop(); - case 'bottom': - return thatObj.yBottom(); - } - } - } - return ''; -}; - -Process.prototype.reportGet = function (query) { - // answer a reference to a first-class member - // or a list of first-class members - var thisObj = this.blockReceiver(), - neighborhood, - stage, - objName; - - if (thisObj) { - switch (this.inputOption(query)) { - case 'self' : - return thisObj; - case 'other sprites': - stage = thisObj.parentThatIsA(StageMorph); - return new List( - stage.children.filter(each => - each instanceof SpriteMorph && - each !== thisObj - ) - ); - case 'parts': // shallow copy to disable side-effects - return new List((thisObj.parts || []).map(each => each)); - case 'anchor': - return thisObj.anchor || ''; - case 'parent': - return thisObj.exemplar || ''; - case 'children': - return new List(thisObj.specimens ? thisObj.specimens() : []); - case 'temporary?': - return thisObj.isTemporary || false; - case 'clones': - stage = thisObj.parentThatIsA(StageMorph); - objName = thisObj.name || thisObj.cloneOriginName; - return new List( - stage.children.filter(each => - each.isTemporary && - (each !== thisObj) && - (each.cloneOriginName === objName) - ) - ); - case 'other clones': - return thisObj.isTemporary ? - this.reportGet(['clones']) : new List(); - case 'neighbors': - stage = thisObj.parentThatIsA(StageMorph); - neighborhood = thisObj.bounds.expandBy(new Point( - thisObj.width(), - thisObj.height() - )); - return new List( - stage.children.filter(each => - each instanceof SpriteMorph && - (each !== thisObj) && - each.bounds.intersects(neighborhood) - ) - ); - case 'dangling?': - return !thisObj.rotatesWithAnchor; - case 'draggable?': - return thisObj.isDraggable; - case 'rotation style': - return thisObj.rotationStyle || 0; - case 'rotation x': - return thisObj.xPosition(); - case 'rotation y': - return thisObj.yPosition(); - case 'center x': - return thisObj.xCenter(); - case 'center y': - return thisObj.yCenter(); - case 'left': - return thisObj.xLeft(); - case 'right': - return thisObj.xRight(); - case 'top': - return thisObj.yTop(); - case 'bottom': - return thisObj.yBottom(); - case 'name': - return thisObj.name; - case 'stage': - return thisObj.parentThatIsA(StageMorph); - case 'costume': - return thisObj.costume; - case 'costumes': - return thisObj.reportCostumes(); - case 'sounds': - return thisObj.sounds; - case 'width': - if (thisObj instanceof StageMorph) { - return thisObj.dimensions.x; - } - stage = thisObj.parentThatIsA(StageMorph); - return stage ? thisObj.width() / stage.scale : 0; - case 'height': - if (thisObj instanceof StageMorph) { - return thisObj.dimensions.y; - } - stage = thisObj.parentThatIsA(StageMorph); - return stage ? thisObj.height() / stage.scale : 0; - } - } - return ''; -}; - -Process.prototype.reportObject = function (name) { - var thisObj = this.blockReceiver(), - thatObj, - stage; - - if (thisObj) { - this.assertAlive(thisObj); - stage = thisObj.parentThatIsA(StageMorph); - if (stage.name === name) { - thatObj = stage; - } else { - thatObj = this.getOtherObject(name, thisObj, stage); - } - if (thatObj) { - this.assertAlive(thatObj); - } - return thatObj; - } -}; - -Process.prototype.doSet = function (attribute, value) { - // experimental, manipulate sprites' attributes - var name, rcvr, ide; - rcvr = this.blockReceiver(); - this.assertAlive(rcvr); - if (!(attribute instanceof Context || attribute instanceof Array) || - (attribute instanceof Context && - attribute.expression.selector !== 'reportGet')) { - throw new Error(localize('unsupported attribute')); - } - name = attribute instanceof Context ? - attribute.expression.inputs()[0].evaluate() - : attribute; - if (name instanceof Array) { - name = name[0]; - } - switch (name) { - case 'anchor': - this.assertType(rcvr, 'sprite'); - if (value instanceof SpriteMorph) { - // avoid circularity here, because the GUI already checks for - // conflicts while the user drags parts over prospective targets - if (!rcvr.enableNesting || contains(rcvr.allParts(), value)) { - throw new Error( - localize('unable to nest\n(disabled or circular?)') - ); - } - value.attachPart(rcvr); - } else { - rcvr.detachFromAnchor(); - } - break; - case 'parent': - this.assertType(rcvr, 'sprite'); - value = value instanceof SpriteMorph ? value : null; - rcvr.setExemplar(value, true); // throw an error in case of circularity - break; - case 'temporary?': - this.assertType(rcvr, 'sprite'); - this.assertType(value, 'Boolean'); - if (value) { - rcvr.release(); - } else { - rcvr.perpetuate(); - } - break; - case 'name': - this.assertType(rcvr, ['sprite', 'stage']); - this.assertType(value, ['text', 'number']); - ide = rcvr.parentThatIsA(IDE_Morph); - if (ide) { - rcvr.setName( - ide.newSpriteName(value.toString(), rcvr) - ); - ide.spriteBar.nameField.setContents( - ide.currentSprite.name.toString() - ); - } - break; - case 'dangling?': - this.assertType(rcvr, 'sprite'); - this.assertType(value, 'Boolean'); - rcvr.rotatesWithAnchor = !value; - rcvr.version = Date.now(); - break; - case 'draggable?': - this.assertType(rcvr, 'sprite'); - this.assertType(value, 'Boolean'); - rcvr.isDraggable = value; - // update padlock symbol in the IDE: - ide = rcvr.parentThatIsA(IDE_Morph); - if (ide) { - ide.spriteBar.children.forEach(each => { - if (each.refresh) { - each.refresh(); - } - }); - } - rcvr.version = Date.now(); - break; - case 'rotation style': - this.assertType(rcvr, 'sprite'); - this.assertType(+value, 'number'); - if (!contains([0, 1, 2], +value)) { - return; // maybe throw an error msg - } - rcvr.changed(); - rcvr.rotationStyle = +value; - rcvr.fixLayout(); - rcvr.rerender(); - // update padlock symbol in the IDE: - ide = rcvr.parentThatIsA(IDE_Morph); - if (ide) { - ide.spriteBar.children.forEach(each => { - if (each.refresh) { - each.refresh(); - } - }); - } - rcvr.version = Date.now(); - break; - case 'rotation x': - this.assertType(rcvr, 'sprite'); - this.assertType(value, 'number'); - rcvr.setRotationX(value); - break; - case 'rotation y': - this.assertType(rcvr, 'sprite'); - this.assertType(value, 'number'); - rcvr.setRotationY(value); - break; - case 'microphone modifier': - this.setMicrophoneModifier(value); - break; - default: - throw new Error( - '"' + localize(name) + '" ' + localize('is read-only') - ); - } -}; - -Process.prototype.reportContextFor = function (context, otherObj) { - // Private - return a copy of the context - // and bind it to another receiver - var result = copy(context); - result.receiver = otherObj; - if (result.outerContext) { - result.outerContext = copy(result.outerContext); - result.outerContext.variables = copy(result.outerContext.variables); - result.outerContext.receiver = otherObj; - if (result.outerContext.variables.parentFrame) { - result.outerContext.variables.parentFrame = - copy(result.outerContext.variables.parentFrame); - result.outerContext.variables.parentFrame.parentFrame = - otherObj.variables; - } else { - result.outerContext.variables.parentFrame = otherObj.variables; - } - } - return result; -}; - -Process.prototype.reportMouseX = function () { - var stage, world; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - world = stage.world(); - if (world) { - return (world.hand.position().x - stage.center().x) - / stage.scale; - } - } - } - return 0; -}; - -Process.prototype.reportMouseY = function () { - var stage, world; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - world = stage.world(); - if (world) { - return (stage.center().y - world.hand.position().y) - / stage.scale; - } - } - } - return 0; -}; - -Process.prototype.reportMouseDown = function () { - var world; - if (this.homeContext.receiver) { - world = this.homeContext.receiver.world(); - if (world) { - return world.hand.mouseButton === 'left'; - } - } - return false; -}; - -Process.prototype.reportKeyPressed = function (keyString) { - var stage; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - if (this.inputOption(keyString) === 'any key') { - return Object.keys(stage.keysPressed).length > 0; - } - return stage.keysPressed[keyString] !== undefined; - } - } - return false; -}; - -Process.prototype.doResetTimer = function () { - var stage; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - stage.resetTimer(); - } - } -}; - -Process.prototype.reportTimer = function () { - var stage; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - return stage.getTimer(); - } - } - return 0; -}; - -// Process Dates and times in Snap -Process.prototype.reportDate = function (datefn) { - var currDate, func, result, - inputFn = this.inputOption(datefn), - // Map block options to built-in functions - dateMap = { - 'year' : 'getFullYear', - 'month' : 'getMonth', - 'date': 'getDate', - 'day of week' : 'getDay', - 'hour' : 'getHours', - 'minute' : 'getMinutes', - 'second' : 'getSeconds', - 'time in milliseconds' : 'getTime' - }; - - if (!dateMap[inputFn]) { return ''; } - currDate = new Date(); - func = dateMap[inputFn]; - result = currDate[func](); - - // Show months as 1-12 and days as 1-7 - if (inputFn === 'month' || inputFn === 'day of week') { - result += 1; - } - return result; -}; - -// Process video motion detection primitives - -Process.prototype.doSetVideoTransparency = function(factor) { - var stage; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - stage.projectionTransparency = Math.max(0, Math.min(100, factor)); - } - } -}; - -Process.prototype.reportVideo = function(attribute, name) { - var thisObj = this.blockReceiver(), - stage = thisObj.parentThatIsA(StageMorph), - thatObj = this.getOtherObject(name, thisObj, stage); - - if (!stage.projectionSource || !stage.projectionSource.stream) { - // wait until video is turned on - if (!this.context.accumulator) { - this.context.accumulator = true; // started video - stage.startVideo(); - } - this.pushContext('doYield'); - this.pushContext(); - return; - } - - switch (this.inputOption(attribute)) { - case 'motion': - if (thatObj instanceof SpriteMorph) { - stage.videoMotion.getLocalMotion(thatObj); - return thatObj.motionAmount; - } - stage.videoMotion.getStageMotion(); - return stage.videoMotion.motionAmount; - case 'direction': - if (thatObj instanceof SpriteMorph) { - stage.videoMotion.getLocalMotion(thatObj); - return thatObj.motionDirection; - } - stage.videoMotion.getStageMotion(); - return stage.videoMotion.motionDirection; - case 'snap': - if (thatObj instanceof SpriteMorph) { - return thatObj.projectionSnap(); - } - return stage.projectionSnap(); - } - return -1; -}; - -Process.prototype.startVideo = function(stage) { - // interpolated - if (this.reportGlobalFlag('video capture')) {return; } - if (!stage.projectionSource || !stage.projectionSource.stream) { - // wait until video is turned on - if (!this.context.accumulator) { - this.context.accumulator = true; // started video - stage.startVideo(); - } - } - this.pushContext('doYield'); - this.pushContext(); -}; - -// Process code mapping - -/* - for generating textual source code using - blocks - not needed to run or debug Snap -*/ - -Process.prototype.doMapCodeOrHeader = function (aContext, anOption, aString) { - if (this.inputOption(anOption) === 'code') { - return this.doMapCode(aContext, aString); - } - if (this.inputOption(anOption) === 'header') { - return this.doMapHeader(aContext, aString); - } - throw new Error( - ' \'' + anOption + '\'\nis not a valid option' - ); -}; - -Process.prototype.doMapHeader = function (aContext, aString) { - if (aContext instanceof Context) { - if (aContext.expression instanceof SyntaxElementMorph) { - return aContext.expression.mapHeader(aString || ''); - } - } -}; - -Process.prototype.doMapCode = function (aContext, aString) { - if (aContext instanceof Context) { - if (aContext.expression instanceof SyntaxElementMorph) { - return aContext.expression.mapCode(aString || ''); - } - } -}; - -Process.prototype.doMapValueCode = function (type, aString) { - var tp = this.inputOption(type); - switch (tp) { - case 'String': - StageMorph.prototype.codeMappings.string = aString || '<#1>'; - break; - case 'Number': - StageMorph.prototype.codeMappings.number = aString || '<#1>'; - break; - case 'true': - StageMorph.prototype.codeMappings.boolTrue = aString || 'true'; - break; - case 'false': - StageMorph.prototype.codeMappings.boolFalse = aString || 'true'; - break; - default: - throw new Error( - localize('unsupported data type') + ' ' + tp - ); - } - -}; - -Process.prototype.doMapListCode = function (part, kind, aString) { - var key1 = '', - key2 = 'delim'; - - if (this.inputOption(kind) === 'parameters') { - key1 = 'parms_'; - } else if (this.inputOption(kind) === 'variables') { - key1 = 'tempvars_'; - } - - if (this.inputOption(part) === 'list') { - key2 = 'list'; - } else if (this.inputOption(part) === 'item') { - key2 = 'item'; - } - - StageMorph.prototype.codeMappings[key1 + key2] = aString || ''; -}; - -Process.prototype.reportMappedCode = function (aContext) { - if (aContext instanceof Context) { - if (aContext.expression instanceof SyntaxElementMorph) { - return aContext.expression.mappedCode(); - } - } - return ''; -}; - -// Process music primitives - -Process.prototype.doRest = function (beats) { - var tempo = this.reportTempo(); - this.doWait(60 / tempo * beats); -}; - -Process.prototype.reportTempo = function () { - var stage; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - return stage.getTempo(); - } - } - return 0; -}; - -Process.prototype.doChangeTempo = function (delta) { - var stage; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - stage.changeTempo(delta); - } - } -}; - -Process.prototype.doSetTempo = function (bpm) { - var stage; - if (this.homeContext.receiver) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (stage) { - stage.setTempo(bpm); - } - } -}; - -Process.prototype.doPlayNote = function (pitch, beats) { - var tempo = this.reportTempo(); - this.doPlayNoteForSecs( - parseFloat(pitch || '0'), - 60 / tempo * parseFloat(beats || '0') - ); -}; - -Process.prototype.doPlayNoteForSecs = function (pitch, secs) { - // interpolated - var rcvr = this.blockReceiver(); - if (!this.context.startTime) { - rcvr.setVolume(rcvr.getVolume()); // b/c Chrome needs lazy init - rcvr.setPan(rcvr.getPan()); // b/c Chrome needs lazy initialization - this.context.startTime = Date.now(); - this.context.activeNote = new Note(pitch); - this.context.activeNote.play( - this.instrument, - rcvr.getGainNode(), - rcvr.getPannerNode() - ); - } - if ((Date.now() - this.context.startTime) >= (secs * 1000)) { - if (this.context.activeNote) { - this.context.activeNote.stop(); - this.context.activeNote = null; - } - return null; - } - this.pushContext('doYield'); - this.pushContext(); -}; - -Process.prototype.doPlayFrequency = function (hz, secs) { - this.doPlayFrequencyForSecs( - parseFloat(hz || '0'), - parseFloat(secs || '0') - ); -}; - -Process.prototype.doPlayFrequencyForSecs = function (hz, secs) { - // interpolated - if (!this.context.startTime) { - this.context.startTime = Date.now(); - this.context.activeNote = new Note(); - this.context.activeNote.frequency = hz; - this.context.activeNote.play(this.instrument); - } - if ((Date.now() - this.context.startTime) >= (secs * 1000)) { - if (this.context.activeNote) { - this.context.activeNote.stop(); - this.context.activeNote = null; - } - return null; - } - this.pushContext('doYield'); - this.pushContext(); -}; - -Process.prototype.doSetInstrument = function (num) { - this.instrument = +num; - this.receiver.instrument = +num; - if (this.receiver.freqPlayer) { - this.receiver.freqPlayer.setInstrument(+num); - } -}; - -// Process image processing primitives - -Process.prototype.reportGetImageAttribute = function (choice, name) { - var cst = this.costumeNamed(name) || new Costume(), - option = this.inputOption(choice); - - switch (option) { - case 'name': - return cst.name; - case 'width': - return cst.width(); - case 'height': - return cst.height(); - case 'pixels': - return cst.rasterized().pixels(); - default: - return cst; - } -}; - -Process.prototype.reportNewCostumeStretched = function (name, xP, yP) { - var cst; - if (name instanceof List) { - return this.reportNewCostume(name, xP, yP); - } - cst = this.costumeNamed(name); - if (!cst) { - return new Costume(); - } - if (!isFinite(+xP * +yP) || isNaN(+xP * +yP)) { - throw new Error( - 'expecting a finite number\nbut getting Infinity or NaN' - ); - } - return cst.stretched( - Math.round(cst.width() * +xP / 100), - Math.round(cst.height() * +yP / 100) - ); -}; - -Process.prototype.costumeNamed = function (name) { - // private - if (name instanceof Costume) { - return name; - } - if (typeof name === 'number') { - return this.blockReceiver().costumes.at(name); - } - if (this.inputOption(name) === 'current') { - return this.blockReceiver().costume; - } - return detect( - this.blockReceiver().costumes.asArray(), - c => c.name === name.toString() - ); -}; - -Process.prototype.reportNewCostume = function (pixels, width, height, name) { - var rcvr, stage, canvas, ctx, src, dta, i, k, px; - - this.assertType(pixels, 'list'); - if (this.inputOption(width) === 'current') { - rcvr = this.blockReceiver(); - stage = rcvr.parentThatIsA(StageMorph); - width = rcvr.costume ? rcvr.costume.width() : stage.dimensions.x; - } - if (this.inputOption(height) === 'current') { - rcvr = rcvr || this.blockReceiver(); - stage = stage || rcvr.parentThatIsA(StageMorph); - height = rcvr.costume ? rcvr.costume.height() : stage.dimensions.y; - } - width = Math.abs(Math.floor(+width)); - height = Math.abs(Math.floor(+height)); - if (width <= 0 || height <= 0) { - return new Costume(); - } - if (!isFinite(width * height) || isNaN(width * height)) { - throw new Error( - 'expecting a finite number\nbut getting Infinity or NaN' - ); - } - - canvas = newCanvas(new Point(width, height), true); - ctx = canvas.getContext('2d'); - src = pixels.asArray(); - dta = ctx.createImageData(width, height); - for (i = 0; i < src.length; i += 1) { - px = src[i].asArray(); - for (k = 0; k < 4; k += 1) { - dta.data[(i * 4) + k] = px[k]; - } - } - ctx.putImageData(dta, 0, 0); - return new Costume( - canvas, - name || (rcvr || this.blockReceiver()).newCostumeName( - localize('costume') - ) - ); -}; - -Process.prototype.reportPentrailsAsSVG = function () { - // interpolated - var rcvr, stage, svg, acc, offset; - - if (!this.context.accumulator) { - stage = this.homeContext.receiver.parentThatIsA(StageMorph); - if (!stage.trailsLog.length) { - throw new Error (localize( - 'there are currently no\nvectorizable pen trail segments' - )); - } - svg = stage.trailsLogAsSVG(); - this.context.accumulator = { - img : new Image(), - rot : svg.rot, - ready : false - }; - acc = this.context.accumulator; - acc.img.onload = () => acc.ready = true; - acc.img.src = 'data:image/svg+xml,' + svg.src; - acc.img.rot = svg.rotationShift; - } else if (this.context.accumulator.ready) { - offset = ZERO; - rcvr = this.blockReceiver(); - if (rcvr instanceof SpriteMorph) { - offset = new Point(rcvr.xPosition(), -rcvr.yPosition()); - } - this.returnValueToParentContext( - new SVG_Costume( - this.context.accumulator.img, - this.blockReceiver().newCostumeName(localize('Costume')), - this.context.accumulator.rot.translateBy(offset) - ) - ); - return; - } - this.pushContext(); -}; - -// Process constant input options - -Process.prototype.inputOption = function (dta) { - // private - for localization - return dta instanceof Array ? dta[0] : dta; -}; - -// Process stack - -Process.prototype.pushContext = function (expression, outerContext) { - this.context = new Context( - this.context, - expression, - outerContext || (this.context ? this.context.outerContext : null), - // for tail call elimination - this.context ? // check needed due to tail call elimination - this.context.receiver : this.homeContext.receiver - ); -}; - -Process.prototype.popContext = function () { - if (this.context) { - this.context.stopMusic(); - } - this.context = this.context ? this.context.parentContext : null; -}; - -Process.prototype.returnValueToParentContext = function (value) { - // if no parent context exists treat value as result - if (value !== undefined) { - var target = this.context ? // in case of tail call elimination - this.context.parentContext || this.homeContext - : this.homeContext; - target.addInput(value); - } -}; - -Process.prototype.reportStackSize = function () { - return this.context ? this.context.stackSize() : 0; -}; - -Process.prototype.reportFrameCount = function () { - return this.frameCount; -}; - -// Process single-stepping - -Process.prototype.flashContext = function () { - var expr = this.context.expression; - if (this.enableSingleStepping && - !this.isAtomic && - expr instanceof SyntaxElementMorph && - !(expr instanceof CommandSlotMorph) && - !this.context.isFlashing && - expr.world() && - !(expr instanceof ColorSlotMorph)) { - this.unflash(); - expr.flash(); - this.context.isFlashing = true; - this.flashingContext = this.context; - if (this.flashTime > 0 && (this.flashTime <= 0.5)) { - this.pushContext('doIdle'); - this.context.addInput(this.flashTime); - } else { - this.pushContext('doInterrupt'); - } - return true; - } - return false; -}; - -Process.prototype.flashPausedContext = function () { - var flashable = this.context ? this.context.lastFlashable() : null; - if (flashable) { - this.unflash(); - flashable.expression.flash(); - flashable.isFlashing = true; - this.flashingContext = flashable; - } -}; - -Process.prototype.doInterrupt = function () { - this.popContext(); - if (!this.isAtomic) { - this.isInterrupted = true; - } -}; - -Process.prototype.doIdle = function (secs) { - if (!this.context.startTime) { - this.context.startTime = Date.now(); - } - if ((Date.now() - this.context.startTime) < (secs * 1000)) { - this.pushContext('doInterrupt'); - return; - } - this.popContext(); -}; - -Process.prototype.unflash = function () { - if (this.flashingContext) { - this.flashingContext.expression.unflash(); - this.flashingContext.isFlashing = false; - this.flashingContext = null; - } -}; - -// Process: Compile (as of yet simple) block scripts to JS - -/* - with either only explicit formal parameters or a specified number of - implicit formal parameters mapped to empty input slots - *** highly experimental and heavily under construction *** -*/ - -Process.prototype.reportCompiled = function (context, implicitParamCount) { - // implicitParamCount is optional and indicates the number of - // expected parameters, if any. This is only used to handle - // implicit (empty slot) parameters and can otherwise be - // ignored - return new JSCompiler(this).compileFunction(context, implicitParamCount); -}; - -Process.prototype.capture = function (aContext) { - // private - answer a new process on a full copy of the given context - // while retaining the lexical variable scope - var proc = new Process(this.topBlock, this.receiver); - var clos = new Context( - aContext.parentContext, - aContext.expression, - aContext.outerContext, - aContext.receiver - ); - clos.variables = aContext.variables.fullCopy(); - clos.variables.root().parentFrame = proc.variables; - proc.context = clos; - return proc; -}; - -Process.prototype.getVarNamed = function (name) { - // private - special form for compiled expressions - // DO NOT use except in compiled methods! - // first check script vars, then global ones - var frame = this.homeContext.variables.silentFind(name) || - this.context.variables.silentFind(name), - value; - if (frame) { - value = frame.vars[name].value; - return (value === 0 ? 0 - : value === false ? false - : value === '' ? '' - : value || 0); // don't return null - } - throw new Error( - localize('a variable of name \'') - + name - + localize('\'\ndoes not exist in this context') - ); -}; - -Process.prototype.setVarNamed = function (name, value) { - // private - special form for compiled expressions - // incomplete, currently only sets named vars - // DO NOT use except in compiled methods! - // first check script vars, then global ones - var frame = this.homeContext.variables.silentFind(name) || - this.context.variables.silentFind(name); - if (isNil(frame)) { - throw new Error( - localize('a variable of name \'') - + name - + localize('\'\ndoes not exist in this context') - ); - } - frame.vars[name].value = value; -}; - -Process.prototype.incrementVarNamed = function (name, delta) { - // private - special form for compiled expressions - this.setVarNamed(name, this.getVarNamed(name) + (+delta)); -}; - -// Process: Atomic HOFs using experimental JIT-compilation - -Process.prototype.reportAtomicMap = function (reporter, list) { - // if the reporter uses formal parameters instead of implicit empty slots - // there are two additional optional parameters: - // #1 - element - // #2 - optional | index - // #3 - optional | source list - - this.assertType(list, 'list'); - var result = [], - src = list.asArray(), - len = src.length, - formalParameterCount = reporter.inputs.length, - parms, - func, - i; - - // try compiling the reporter into generic JavaScript - // fall back to the morphic reporter if unsuccessful - try { - func = this.reportCompiled(reporter, 1); // a single expected input - } catch (err) { - console.log(err.message); - func = reporter; - } - - // iterate over the data in a single frame: - // to do: Insert some kind of user escape mechanism - - for (i = 0; i < len; i += 1) { - parms = [src[i]]; - if (formalParameterCount > 1) { - parms.push(i + 1); - } - if (formalParameterCount > 2) { - parms.push(list); - } - result.push( - invoke( - func, - new List(parms), - null, - null, - null, - null, - this.capture(reporter) // process - ) - ); - } - return new List(result); -}; - -Process.prototype.reportAtomicKeep = function (reporter, list) { - // if the reporter uses formal parameters instead of implicit empty slots - // there are two additional optional parameters: - // #1 - element - // #2 - optional | index - // #3 - optional | source list - - this.assertType(list, 'list'); - var result = [], - src = list.asArray(), - len = src.length, - formalParameterCount = reporter.inputs.length, - parms, - func, - i; - - // try compiling the reporter into generic JavaScript - // fall back to the morphic reporter if unsuccessful - try { - func = this.reportCompiled(reporter, 1); // a single expected input - } catch (err) { - console.log(err.message); - func = reporter; - } - - // iterate over the data in a single frame: - // to do: Insert some kind of user escape mechanism - for (i = 0; i < len; i += 1) { - parms = [src[i]]; - if (formalParameterCount > 1) { - parms.push(i + 1); - } - if (formalParameterCount > 2) { - parms.push(list); - } - if ( - invoke( - func, - new List(parms), - null, - null, - null, - null, - this.capture(reporter) // process - ) - ) { - result.push(src[i]); - } - } - return new List(result); -}; - -Process.prototype.reportAtomicFindFirst = function (reporter, list) { - // if the reporter uses formal parameters instead of implicit empty slots - // there are two additional optional parameters: - // #1 - element - // #2 - optional | index - // #3 - optional | source list - - this.assertType(list, 'list'); - var src = list.asArray(), - len = src.length, - formalParameterCount = reporter.inputs.length, - parms, - func, - i; - - // try compiling the reporter into generic JavaScript - // fall back to the morphic reporter if unsuccessful - try { - func = this.reportCompiled(reporter, 1); // a single expected input - } catch (err) { - console.log(err.message); - func = reporter; - } - - // iterate over the data in a single frame: - // to do: Insert some kind of user escape mechanism - for (i = 0; i < len; i += 1) { - parms = [src[i]]; - if (formalParameterCount > 1) { - parms.push(i + 1); - } - if (formalParameterCount > 2) { - parms.push(list); - } - if ( - invoke( - func, - new List(parms), - null, - null, - null, - null, - this.capture(reporter) // process - ) - ) { - return src[i]; - } - } - return false; -}; - -Process.prototype.reportAtomicCombine = function (list, reporter) { - // if the reporter uses formal parameters instead of implicit empty slots - // there are two additional optional parameters: - // #1 - accumulator - // #2 - element - // #3 - optional | index - // #4 - optional | source list - - this.assertType(list, 'list'); - var result = '', - src = list.asArray(), - len = src.length, - formalParameterCount = reporter.inputs.length, - parms, - func, - i; - - if (len === 0) { - return result; - } - result = src[0]; - - // try compiling the reporter into generic JavaScript - // fall back to the morphic reporter if unsuccessful - try { - func = this.reportCompiled(reporter, 2); // a single expected input - } catch (err) { - console.log(err.message); - func = reporter; - } - - // iterate over the data in a single frame: - // to do: Insert some kind of user escape mechanism - for (i = 1; i < len; i += 1) { - parms = [result, src[i]]; - if (formalParameterCount > 2) { - parms.push(i + 1); - } - if (formalParameterCount > 3) { - parms.push(list); - } - result = invoke( - func, - new List(parms), - null, - null, - null, - null, - this.capture(reporter) // process - ); - } - return result; -}; - -Process.prototype.reportAtomicSort = function (list, reporter) { - this.assertType(list, 'list'); - var func; - - // try compiling the reporter into generic JavaScript - // fall back to the morphic reporter if unsuccessful - try { - func = this.reportCompiled(reporter, 2); // two inputs expected - } catch (err) { - console.log(err.message); - func = reporter; - } - - // iterate over the data in a single frame: - return new List( - list.asArray().slice().sort((a, b) => - invoke( - func, - new List([a, b]), - null, - null, - null, - null, - this.capture(reporter) // process - ) ? -1 : 1 - ) - ); -}; - -Process.prototype.reportAtomicGroup = function (list, reporter) { - this.assertType(list, 'list'); - var result = [], - dict = new Map(), - groupKey, - src = list.asArray(), - len = src.length, - func, - i; - - // try compiling the reporter into generic JavaScript - // fall back to the morphic reporter if unsuccessful - try { - func = this.reportCompiled(reporter, 1); // a single expected input - } catch (err) { - console.log(err.message); - func = reporter; - } - - // iterate over the data in a single frame: - // to do: Insert some kind of user escape mechanism - - for (i = 0; i < len; i += 1) { - groupKey = invoke( - func, - new List([src[i]]), - null, - null, - null, - null, - this.capture(reporter) // process - ); - if (dict.has(groupKey)) { - dict.get(groupKey).push(src[i]); - } else { - dict.set(groupKey, [src[i]]); - } - } - - dict.forEach((value, key) => - result.push(new List([key, value.length, new List(value)])) - ); - return new List(result); -}; - -// Context ///////////////////////////////////////////////////////////// - -/* - A Context describes the state of a Process. - - Each Process has a pointer to a Context containing its - state. Whenever the Process yields control, its Context - tells it exactly where it left off. - - structure: - - parentContext the Context to return to when this one has - been evaluated. - outerContext the Context holding my lexical scope - expression SyntaxElementMorph, an array of blocks to evaluate, - null or a String denoting a selector, e.g. 'doYield' - origin the object of origin, only used for serialization - receiver the object to which the expression applies, if any - variables the current VariableFrame, if any - inputs an array of input values computed so far - (if expression is a BlockMorph) - pc the index of the next block to evaluate - (if expression is an array) - isContinuation flag for marking a transient continuation context - startTime time when the context was first evaluated - startValue initial value for interpolated operations - activeAudio audio buffer for interpolated operations, don't persist - activeNote audio oscillator for interpolated ops, don't persist - activeSends forked processes waiting to be completed - isCustomBlock marker for return ops - isCustomCommand marker for interpolated blocking reporters (reportURL) - emptySlots caches the number of empty slots for reification - tag string or number to optionally identify the Context, - as a "return" target (for the "stop block" primitive) - isFlashing flag for single-stepping - accumulator slot for collecting data from reentrant visits -*/ - -function Context( - parentContext, - expression, - outerContext, - receiver -) { - this.outerContext = outerContext || null; - this.parentContext = parentContext || null; - this.expression = expression || null; - this.receiver = receiver || null; - this.origin = receiver || null; // only for serialization - this.variables = new VariableFrame(); - if (this.outerContext) { - this.variables.parentFrame = this.outerContext.variables; - this.receiver = this.outerContext.receiver; - } - this.inputs = []; - this.pc = 0; - this.isContinuation = false; - this.startTime = null; - this.activeSends = null; - this.activeAudio = null; - this.activeNote = null; - this.isCustomBlock = false; // marks the end of a custom block's stack - this.isCustomCommand = null; // used for ignoring URL reporters' results - this.emptySlots = 0; // used for block reification - this.tag = null; // lexical catch-tag for custom blocks - this.isFlashing = false; // for single-stepping - this.accumulator = null; -} - -Context.prototype.toString = function () { - var expr = this.expression; - if (expr instanceof Array) { - if (expr.length > 0) { - expr = '[' + expr[0] + ']'; - } - } - return 'Context >> ' + expr + ' ' + this.variables; -}; - -Context.prototype.image = function () { - var ring = new RingMorph(), - block, - cont; - - if (this.expression instanceof Morph) { - block = this.expression.fullCopy(); - - // replace marked call/cc block with empty slot - if (this.isContinuation) { - cont = detect( - block.allInputs(), - inp => inp.bindingID === 1 - ); - if (cont) { - block.revertToDefaultInput(cont, true); - } - } - ring.embed(block, this.inputs); - return ring.fullImage(); - } - if (this.expression instanceof Array) { - block = this.expression[this.pc].fullCopy(); - if (block instanceof RingMorph && !block.contents()) { // empty ring - return block.fullImage(); - } - ring.embed(block, this.isContinuation ? [] : this.inputs); - return ring.fullImage(); - } - - // otherwise show an empty ring - ring.color = SpriteMorph.prototype.blockColor.other; - ring.setSpec('%rc %ringparms'); - - // also show my inputs, unless I'm a continuation - if (!this.isContinuation) { - this.inputs.forEach(inp => - ring.parts()[1].addInput(inp) - ); - } - return ring.fullImage(); -}; - -// Context continuations: - -Context.prototype.continuation = function () { - var cont; - if (this.expression instanceof Array) { - cont = this; - } else if (this.parentContext) { - cont = this.parentContext; - } else { - cont = new Context(null, 'expectReport'); - cont.isContinuation = true; - return cont; - } - cont = cont.copyForContinuation(); - cont.tag = null; - cont.isContinuation = true; - return cont; -}; - -Context.prototype.copyForContinuation = function () { - var cpy = copy(this), - cur = cpy, - isReporter = !(this.expression instanceof Array || - isString(this.expression)); - if (isReporter) { - cur.prepareContinuationForBinding(); - while (cur.parentContext) { - cur.parentContext = copy(cur.parentContext); - cur = cur.parentContext; - cur.inputs = []; - } - } - return cpy; -}; - -Context.prototype.copyForContinuationCall = function () { - var cpy = copy(this), - cur = cpy, - isReporter = !(this.expression instanceof Array || - isString(this.expression)); - if (isReporter) { - this.expression = this.expression.fullCopy(); - this.inputs = []; - while (cur.parentContext) { - cur.parentContext = copy(cur.parentContext); - cur = cur.parentContext; - cur.inputs = []; - } - } - return cpy; -}; - -Context.prototype.prepareContinuationForBinding = function () { - var pos = this.inputs.length, - slot; - this.expression = this.expression.fullCopy(); - slot = this.expression.inputs()[pos]; - if (slot) { - this.inputs = []; - // mark slot containing the call/cc reporter with an identifier - slot.bindingID = 1; - // and remember the number of detected empty slots - this.emptySlots = 1; - } -}; - -// Context accessing: - -Context.prototype.addInput = function (input) { - this.inputs.push(input); -}; - -// Context music - -Context.prototype.stopMusic = function () { - if (this.activeNote) { - this.activeNote.stop(); - this.activeNote = null; - } -}; - -// Context single-stepping: - -Context.prototype.lastFlashable = function () { - // for experimental single-stepping when pausing - if (this.expression instanceof SyntaxElementMorph && - !(this.expression instanceof CommandSlotMorph)) { - return this; - } else if (this.parentContext) { - return this.parentContext.lastFlashable(); - } - return null; -}; - -// Context debugging - -Context.prototype.stackSize = function () { - if (!this.parentContext) { - return 1; - } - return 1 + this.parentContext.stackSize(); -}; - -// Variable ///////////////////////////////////////////////////////////////// - -function Variable(value, isTransient) { - this.value = value; - this.isTransient = isTransient || false; // prevent value serialization -} - -Variable.prototype.toString = function () { - return 'a ' + (this.isTransient ? 'transient ' : '') + 'Variable [' + - this.value + ']'; -}; - -Variable.prototype.copy = function () { - return new Variable(this.value, this.isTransient); -}; - -// VariableFrame /////////////////////////////////////////////////////// - -function VariableFrame(parentFrame, owner) { - this.vars = {}; - this.parentFrame = parentFrame || null; - this.owner = owner || null; -} - -VariableFrame.prototype.toString = function () { - return 'a VariableFrame {' + this.names() + '}'; -}; - -VariableFrame.prototype.copy = function () { - var frame = new VariableFrame(this.parentFrame); - this.names().forEach(vName => - frame.addVar(vName, this.getVar(vName)) - ); - return frame; -}; - -VariableFrame.prototype.fullCopy = function () { - // experimental - for compiling to JS - var frame; - if (this.parentFrame) { - frame = new VariableFrame(this.parentFrame.fullCopy()); - } else { - frame = new VariableFrame(); - } - frame.vars = copy(this.vars); - return frame; -}; - -VariableFrame.prototype.root = function () { - if (this.parentFrame) { - return this.parentFrame.root(); - } - return this; -}; - -VariableFrame.prototype.find = function (name) { - // answer the closest variable frame containing - // the specified variable. otherwise throw an exception. - var frame = this.silentFind(name); - if (frame) {return frame; } - throw new Error( - localize('a variable of name \'') - + name - + localize('\'\ndoes not exist in this context') - ); -}; - -VariableFrame.prototype.silentFind = function (name) { - // answer the closest variable frame containing - // the specified variable. Otherwise return null. - if (this.vars[name] instanceof Variable) { - return this; - } - if (this.parentFrame) { - return this.parentFrame.silentFind(name); - } - return null; -}; - -VariableFrame.prototype.setVar = function (name, value, sender) { - // change the specified variable if it exists - // else throw an error, because variables need to be - // declared explicitly (e.g. through a "script variables" block), - // before they can be accessed. - // if the found frame is inherited by the sender sprite - // shadow it (create an explicit one for the sender) - // before setting the value ("create-on-write") - - var frame = this.find(name); - if (frame) { - if (sender instanceof SpriteMorph && - (frame.owner instanceof SpriteMorph) && - (sender !== frame.owner)) { - sender.shadowVar(name, value); - } else { - frame.vars[name].value = value; - } - } -}; - -VariableFrame.prototype.changeVar = function (name, delta, sender) { - // change the specified variable if it exists - // else throw an error, because variables need to be - // declared explicitly (e.g. through a "script variables" block, - // before they can be accessed. - // if the found frame is inherited by the sender sprite - // shadow it (create an explicit one for the sender) - // before changing the value ("create-on-write") - - var frame = this.find(name), - value, - newValue; - if (frame) { - value = parseFloat(frame.vars[name].value); - newValue = isNaN(value) ? delta : value + parseFloat(delta); - if (sender instanceof SpriteMorph && - (frame.owner instanceof SpriteMorph) && - (sender !== frame.owner)) { - sender.shadowVar(name, newValue); - } else { - frame.vars[name].value = newValue; - } - - } -}; - -VariableFrame.prototype.getVar = function (name) { - var frame = this.silentFind(name), - value; - if (frame) { - value = frame.vars[name].value; - return (value === 0 ? 0 - : value === false ? false - : value === '' ? '' - : value || 0); // don't return null - } - if (typeof name === 'number') { - // empty input with a Binding-ID called without an argument - return ''; - } - throw new Error( - localize('a variable of name \'') - + name - + localize('\'\ndoes not exist in this context') - ); -}; - -VariableFrame.prototype.addVar = function (name, value) { - this.vars[name] = new Variable(value === 0 ? 0 - : value === false ? false - : value === '' ? '' : value || 0); -}; - -VariableFrame.prototype.deleteVar = function (name) { - var frame = this.find(name); - if (frame) { - delete frame.vars[name]; - } -}; - -// VariableFrame tools - -VariableFrame.prototype.names = function () { - var each, names = []; - for (each in this.vars) { - if (Object.prototype.hasOwnProperty.call(this.vars, each)) { - names.push(each); - } - } - return names; -}; - -VariableFrame.prototype.allNamesDict = function (upTo) { - // "upTo" is an optional parent frame at which to stop, e.g. globals - var dict = {}, current = this; - - function addKeysToDict(srcDict, trgtDict) { - var eachKey; - for (eachKey in srcDict) { - if (Object.prototype.hasOwnProperty.call(srcDict, eachKey)) { - trgtDict[eachKey] = eachKey; - } - } - } - - while (current && (current !== upTo)) { - addKeysToDict(current.vars, dict); - current = current.parentFrame; - } - return dict; -}; - -VariableFrame.prototype.allNames = function (upTo) { -/* - only show the names of the lexical scope, hybrid scoping is - reserved to the daring ;-) - "upTo" is an optional parent frame at which to stop, e.g. globals -*/ - var answer = [], each, dict = this.allNamesDict(upTo); - - for (each in dict) { - if (Object.prototype.hasOwnProperty.call(dict, each)) { - answer.push(each); - } - } - return answer; -}; - -// JSCompiler ///////////////////////////////////////////////////////////////// - -/* - Compile simple, side-effect free Reporters - with either only explicit formal parameters or a specified number of - implicit formal parameters mapped to empty input slots - *** highly experimental and heavily under construction *** -*/ - -function JSCompiler(aProcess) { - this.process = aProcess; - this.source = null; // a context - this.gensyms = null; // temp dictionary for parameter substitutions - this.implicitParams = null; - this.paramCount = null; -} - -JSCompiler.prototype.toString = function () { - return 'a JSCompiler'; -}; - -JSCompiler.prototype.compileFunction = function (aContext, implicitParamCount) { - var block = aContext.expression, - parameters = aContext.inputs, - parms = [], - hasEmptySlots = false, - i; - - this.source = aContext; - this.implicitParams = implicitParamCount || 1; - - // scan for empty input slots - hasEmptySlots = !isNil(detect( - block.allChildren(), - morph => morph.isEmptySlot && morph.isEmptySlot() - )); - - // translate formal parameters into gensyms - this.gensyms = {}; - this.paramCount = 0; - if (parameters.length) { - // test for conflicts - if (hasEmptySlots) { - throw new Error( - 'compiling does not yet support\n' + - 'mixing explicit formal parameters\n' + - 'with empty input slots' - ); - } - // map explicit formal parameters - parameters.forEach((pName, idx) => { - var pn = 'p' + idx; - parms.push(pn); - this.gensyms[pName] = pn; - }); - } else if (hasEmptySlots) { - if (this.implicitParams > 1) { - for (i = 0; i < this.implicitParams; i += 1) { - parms.push('p' + i); - } - } else { - // allow for a single implicit formal parameter - parms = ['p0']; - } - } - - // compile using gensyms - - if (block instanceof CommandBlockMorph) { - return Function.apply( - null, - parms.concat([this.compileSequence(block)]) - ); - } - return Function.apply( - null, - parms.concat(['return ' + this.compileExpression(block)]) - ); -}; - -JSCompiler.prototype.compileExpression = function (block) { - var selector = block.selector, - inputs = block.inputs(), - target, - rcvr, - args; - - // first check for special forms and infix operators - switch (selector) { - case 'reportOr': - return this.compileInfix('||', inputs); - case 'reportAnd': - return this.compileInfix('&&', inputs); - case 'reportIfElse': - return '(' + - this.compileInput(inputs[0]) + - ' ? ' + - this.compileInput(inputs[1]) + - ' : ' + - this.compileInput(inputs[2]) + - ')'; - case 'evaluateCustomBlock': - throw new Error( - 'compiling does not yet support\n' + - 'custom blocks' - ); - - // special command forms - case 'doSetVar': // redirect var to process - return 'arguments[arguments.length - 1].setVarNamed(' + - this.compileInput(inputs[0]) + - ',' + - this.compileInput(inputs[1]) + - ')'; - case 'doChangeVar': // redirect var to process - return 'arguments[arguments.length - 1].incrementVarNamed(' + - this.compileInput(inputs[0]) + - ',' + - this.compileInput(inputs[1]) + - ')'; - case 'doReport': - return 'return ' + this.compileInput(inputs[0]); - case 'doIf': - return 'if (' + - this.compileInput(inputs[0]) + - ') {\n' + - this.compileSequence(inputs[1].evaluate()) + - '}'; - case 'doIfElse': - return 'if (' + - this.compileInput(inputs[0]) + - ') {\n' + - this.compileSequence(inputs[1].evaluate()) + - '} else {\n' + - this.compileSequence(inputs[2].evaluate()) + - '}'; - - default: - target = this.process[selector] ? this.process - : (this.source.receiver || this.process.receiver); - rcvr = target.constructor.name + '.prototype'; - args = this.compileInputs(inputs); - if (isSnapObject(target)) { - return rcvr + '.' + selector + '.apply('+ rcvr + ', [' + args +'])'; - } else { - return 'arguments[arguments.length - 1].' + - selector + - '.apply(arguments[arguments.length - 1], [' + args +'])'; - } - } -}; - -JSCompiler.prototype.compileSequence = function (commandBlock) { - var body = ''; - commandBlock.blockSequence().forEach(block => { - body += this.compileExpression(block); - body += ';\n'; - }); - return body; -}; - -JSCompiler.prototype.compileInfix = function (operator, inputs) { - return '(' + this.compileInput(inputs[0]) + ' ' + operator + ' ' + - this.compileInput(inputs[1]) +')'; -}; - -JSCompiler.prototype.compileInputs = function (array) { - var args = ''; - array.forEach(inp => { - if (args.length) { - args += ', '; - } - args += this.compileInput(inp); - }); - return args; -}; - -JSCompiler.prototype.compileInput = function (inp) { - var value, type; - - if (inp.isEmptySlot && inp.isEmptySlot()) { - // implicit formal parameter - if (this.implicitParams > 1) { - if (this.paramCount < this.implicitParams) { - this.paramCount += 1; - return 'p' + (this.paramCount - 1); - } - throw new Error( - localize('expecting') + ' ' + this.implicitParams + ' ' - + localize('input(s), but getting') + ' ' - + this.paramCount - ); - } - return 'p0'; - } else if (inp instanceof MultiArgMorph) { - return 'new List([' + this.compileInputs(inp.inputs()) + '])'; - } else if (inp instanceof ArgLabelMorph) { - return this.compileInput(inp.argMorph()); - } else if (inp instanceof ArgMorph) { - // literal - evaluate inline - value = inp.evaluate(); - type = this.process.reportTypeOf(value); - switch (type) { - case 'number': - case 'Boolean': - return '' + value; - case 'text': - // enclose in double quotes - return '"' + value + '"'; - case 'list': - return 'new List([' + this.compileInputs(value) + '])'; - default: - if (value instanceof Array) { - return '"' + value[0] + '"'; - } - throw new Error( - 'compiling does not yet support\n' + - 'inputs of type\n' + - type - ); - } - } else if (inp instanceof BlockMorph) { - if (inp.selector === 'reportGetVar') { - if (contains(this.source.inputs, inp.blockSpec)) { - // un-quoted gensym: - return this.gensyms[inp.blockSpec]; - } - // redirect var query to process - return 'arguments[arguments.length - 1].getVarNamed("' + - inp.blockSpec + - '")'; - } - return this.compileExpression(inp); - } else { - throw new Error( - 'compiling does not yet support\n' + - 'input slots of type\n' + - inp.constructor.name - ); - } -};