/* 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) 2022 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, BLACK, TableFrameMorph, ColorSlotMorph, isSnapObject, newCanvas, Symbol, SVG_Costume, SnapExtensions, AlignmentMorph, TextMorph, Cloud, HatBlockMorph*/ /*jshint esversion: 6*/ modules.threads = '2022-January-30'; var ThreadManager; var Process; var Context; var Variable; var VariableFrame; var JSCompiler; const NONNUMBERS = [true, false, '']; (function () { // "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 (var i = 9; i <= 13; i += 1) { NONNUMBERS.push(String.fromCharCode(i)); } NONNUMBERS.push(String.fromCharCode(160)); })(); function snapEquals(a, b) { // nil if (isNil(a) || isNil(b)) { return a === b; } // lists, functions and blocks if (a.equalTo || b.equalTo) { if (a.constructor.name === b.constructor.name) { return a.equalTo(b); } return false; } var x = +a, y = +b; // check for special values before coercing to numbers if (isNaN(x) || isNaN(y) || [a, b].some(any => contains(NONNUMBERS, 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.itemsArray().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.disableClickToRun = false; ThreadManager.prototype.toggleProcess = function (block, receiver) { if (this.disableClickToRun) { return; } var active = this.findProcess(block, receiver); if (active) { active.stop(); } else { return this.startProcess( block, receiver, null, null, null, true, // isClicked null, null, this.clickFrameFor(block) // for upvars declared inside hat blocks ); } }; 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 || proc.exportResult) { 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(); } }); } }; ThreadManager.prototype.clickFrameFor = function (block) { // private - answer a variable frame or null containing upvar declarations // in certain hat blocks if the user manually clicks on them var name, frame; if (block instanceof HatBlockMorph) { if (block.selector === 'receiveKey' || block.selector === 'receiveMessage') { name = block.inputs()[1].evaluate()[0]; if (name) { frame = new VariableFrame(); frame.addVar(name, ''); return frame; } } } return null; }; // 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; Process.prototype.enableLiveCoding = false; // experimental Process.prototype.enableSingleStepping = false; Process.prototype.enableCompiling = false; // experimental Process.prototype.flashTime = 0; 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.currentTime = Date.now(); // keeping track of time between yields this.frameCount = 0; // only used for profiling and debugging this.stepFrameCount = 0; // keeping track of when to keep time this.yieldCount = 0; // only used for profiling and debugging this.exportResult = false; this.onComplete = onComplete || null; this.procedureCount = 0; this.flashingContext = null; // for single-stepping this.isInterrupted = false; // 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.readyToTerminate && (this.context || this.isPaused); }; // 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; // repeatedly evaluate the next context (stack frame) until // it's time to yield. In case of WARP or infinite recursive // reporters (or long HOFs) emergency-yield every 500 ms. // Since looking up the current time at every stack frame puts // an amazing strain on performance, only check the system time // every n (=100) contexts. // This is happens over at evaluateContext(). while (!this.readyToYield && !this.isInterrupted && this.context && (this.currentTime - this.lastYield < this.timeout) ) { // also allow pausing inside atomic steps - for PAUSE block primitive: if (this.isPaused) { return this.pauseStep(); } if (deadline && (this.currentTime > deadline)) { if (this.isAtomic && this.homeContext.receiver && this.homeContext.receiver.endWarp) { this.homeContext.receiver.endWarp(); } return; } this.evaluateContext(); } this.stepFrameCount = 0; this.yieldCount += 1; 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; // keep track of overall frames for profiling purposes. // also keep track of frames inside the current atomic step. // In order to let Snap! behave similarly on a wide range of // differently performant hardware decide when to yield inside // a WARPed script or an infinitely recursive reporter // by how much time has elapsed since the last yield, but since // looking up the system time is surprisingly costly only look it // up every 100 frames. this.frameCount += 1; this.stepFrameCount += 1; if (this.stepFrameCount > 100) { this.currentTime = Date.now(); this.stepFrameCount = 0; } 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); this.evaluateNextInputSet(block); // frame-optimized version } 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: Primitive Extensions (for libraries etc.) Process.prototype.doApplyExtension = function (prim, args) { this.reportApplyExtension(prim, args); }; Process.prototype.reportApplyExtension = function (prim, args) { var ext = SnapExtensions.primitives.get(prim); if (isNil(ext)) { throw new Error(localize('missing / unspecified extension') + ': ' + prim); } return ext.apply( this.blockReceiver(), args.itemsArray().concat([this]) ); }; // 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); this.evaluateNextInputSet(multiSlot); // frame-optimized version } 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.evaluateNextInputSet = function (element) { // Optimization to use instead of evaluateNextInput(), bums out a few // frames and function calls to save a some milliseconds. // the idea behind this optimization is to keep evaluating the inputs // while we know for sure that we aren't boing to yield anyway var args = element.inputs(), sel = this.context.expression.selector, outer = this.context.outerContext, // for tail call elimination exp, ans; while (args.length > this.context.inputs.length) { exp = args[this.context.inputs.length]; if (exp.isUnevaluated) { if (exp.isUnevaluated === true || exp.isUnevaluated()) { if (sel === 'reify' || sel === 'reportScript') { this.context.addInput(exp); } else { this.context.addInput(this.reify(exp, new List())); } } else { this.pushContext(exp, outer); break; } } else { if (exp instanceof MultiArgMorph || exp instanceof ArgLabelMorph || exp instanceof BlockMorph) { this.pushContext(exp, outer); break; } else { // asuming an ArgMorph if (this.flashContext()) {return; } // yield to flash if (exp.bindingID) { if (this.isCatchingErrors) { try { ans = this.context.variables.getVar(exp.bindingID); } catch (error) { this.handleError(error, exp); } } else { ans = this.context.variables.getVar(exp.bindingID); } } else { ans = exp.evaluate(); if (ans) { if (exp.constructor === CommandSlotMorph || exp.constructor === ReporterSlotMorph || (exp instanceof CSlotMorph && (!exp.isStatic || exp.isLambda))) { ans = this.reify(ans, new List()); } } } this.context.addInput(ans); } } } }; 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.throwError = function (error, element) { var m = element, ide = this.homeContext.receiver.parentThatIsA(IDE_Morph); this.stop(); this.errorFlag = true; this.topBlock.addErrorHighlight(); if (ide.isAppMode) { ide.showMessage(localize(error.name) + '\n' + error.message); } else { if (isNil(m) || isNil(m.world())) {m = this.topBlock; } m.showBubble( this.errorBubble(error, element), this.exportResult, this.receiver ); } }; Process.prototype.tryCatch = function (action, exception, errVarName) { var next = this.context.continuation(); this.handleError = function(error) { this.resetErrorHandling(); if (exception.expression instanceof CommandBlockMorph) { exception.expression = exception.expression.blockSequence(); } exception.pc = 0; exception.outerContext.variables.addVar(errVarName); exception.outerContext.variables.setVar(errVarName, error.message); this.context = exception; this.evaluate(next, new List(), true); }; this.evaluate(action, new List(), true); }; Process.prototype.resetErrorHandling = function () { this.handleError = this.throwError; }; Process.prototype.resetErrorHandling(); Process.prototype.errorObsolete = function () { throw new Error('a custom block definition is missing'); }; Process.prototype.errorBubble = function (error, element) { // Return a morph containing an image of the elment causing the error // above the text of error. var errorMorph = new AlignmentMorph('column', 5), errorIsNested = !!element && isNil(element.world()), errorPrefix = errorIsNested ? `${localize('Inside a custom block')}\n` : '', errorMessage = new TextMorph( `${errorPrefix}${localize(error.name)}\n${localize(error.message)}`, SyntaxElementMorph.prototype.fontSize ), blockToShow = element; errorMorph.add(errorMessage); if (errorIsNested && error.cause !== 'user') { if (blockToShow.selector === 'reportGetVar') { // if I am a single variable, show my caller in the output. blockToShow = blockToShow.parent; } errorMorph.children[0].text += `\n${localize('The question came up at')}`; errorMorph.children[0].fixLayout(); errorMorph.add(blockToShow.fullCopy()); } errorMorph.fixLayout(); return errorMorph; }; // 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.itemsArray(); 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) { if (!this.enableJS) { throw new Error('JavaScript extensions for Snap!\nare turned off'); } return Function.apply( null, parmNames.itemsArray().concat([body]) ); }; Process.prototype.doRun = function (context, args) { return this.evaluate(context, args, true); }; Process.prototype.evaluate = function ( context, args, isCommand ) { if (!context) { return this.returnValueToParentContext(null); } if (context instanceof Function) { return context.apply( this.blockReceiver(), args.itemsArray().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.itemsArray(), 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 = (this.currentTime - 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( localize('expecting a') + ' ' + localize('ring') + ' ' + localize('but getting a') + ' ' + localize(this.reportTypeOf(context)) ); } var outer = new Context(null, null, context.outerContext), runnable = new Context(null, context.expression, outer ), parms = args.itemsArray(), 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)]), !isReporter ); }; Process.prototype.reportCallCC = function (aContext) { this.doCallCC(aContext, true); }; Process.prototype.runContinuation = function (aContext, args) { var parms = args.itemsArray(); // 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.itemsArray(), 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: // 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 = (this.currentTime - 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.itemsArray().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; this.assertType(value, 'number'); 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, context) { // context is an optional start-context to be used by extensions var varFrame = (context || (this.context || this.homeContext)).variables, stage, watcher, target, label, others, isGlobal, name = varName; if (name instanceof Context) { if (name.expression.selector === 'reportGetVar') { name = name.expression.blockSpec; } else { this.blockReceiver().changeBlockVisibility(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(); watcher.rerender(); } } }; Process.prototype.doHideVar = function (varName, context) { // if no varName is specified delete all watchers on temporaries // context is an optional start-context to be used by extensions var varFrame = (context || this.context).variables, stage, watcher, target, name = varName; if (name instanceof Context) { if (name.expression.selector === 'reportGetVar') { name = name.expression.blockSpec; } else { this.blockReceiver().changeBlockVisibility(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 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); } }; // 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) { 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()); } if (index instanceof List && this.enableHyperOps) { return list.query(index); } return list.at(index); }; // Process - tabular list ops Process.prototype.reportTranspose = function (list) { this.assertType(list, 'list'); return list.transpose(); }; Process.prototype.reportCrossproduct = function (lists) { this.assertType(lists, 'list'); if (lists.isEmpty()) { return lists; } this.assertType(lists.at(1), 'list'); return lists.crossproduct(); }; Process.prototype.reportReshape = function (list, shape) { this.assertType(shape, 'list'); list = list instanceof List ? list : new List([list]); return list.reshape(shape); }; Process.prototype.reportSlice = function (list, indices) { // currently not in use this.assertType(list, 'list'); this.assertType(indices, 'list'); return list.slice(indices); }; // Process - other basic list accessors Process.prototype.reportListAttribute = function (choice, list) { var option = this.inputOption(choice); switch (option) { case 'length': this.assertType(list, 'list'); return list.length(); case 'size': this.assertType(list, 'list'); return list.size(); case 'rank': return list instanceof List ? list.rank() : 0; case 'dimensions': return list instanceof List ? list.shape() : new List(); case 'flatten': return list instanceof List ? list.ravel() : new List([list]); case 'columns': this.assertType(list, 'list'); return list.columns(); case 'transpose': this.assertType(list, 'list'); return list.transpose(); case 'reverse': this.assertType(list, 'list'); return list.reversed(); case 'lines': this.assertType(list, 'list'); if (list.canBeTXT()) { return list.asTXT(); } throw new Error( localize('unable to convert to') + ' ' + localize('lines') ); case 'csv': this.assertType(list, 'list'); if (list.canBeCSV()) { return list.asCSV(); } throw new Error( localize('unable to convert to') + ' ' + localize('CSV') ); case 'json': this.assertType(list, 'list'); if (list.canBeJSON()) { return list.asJSON(); } throw new Error( localize('unable to convert to') + ' ' + localize('JSON') ); default: return 0; } }; 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.reportListCombination = function (choice, lists) { // experimental, currently not in use var option = this.inputOption(choice); switch (option) { case 'append': return this.reportConcatenatedLists(lists); case 'cross product': return this.reportCrossproduct(lists); default: return 0; } }; 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 - hyperized reporter-if, experimental, commented out for now 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; } if (inputs[0] instanceof List && this.enableHyperOps) { if (inputs.length < 3) { this.evaluateNextInput(block); } else { this.returnValueToParentContext( this.hyperIf.apply(this, inputs) ); this.popContext(); } } else { this.returnValueToParentContext(inputs.pop()); this.popContext(); } } else { if (inputs[0] instanceof List && this.enableHyperOps) { this.evaluateNextInput(block); } else { // this.assertType(inputs[0], ['Boolean']); if (inputs[0]) { this.evaluateNextInput(block); } else { inputs.push(null); this.evaluateNextInput(block); } } } }; Process.prototype.hyperIf = function (test, trueValue, falseValue) { if (test instanceof List) { return test.map(each => this.hyperIf(each, trueValue, falseValue)); } return test ? trueValue : falseValue; }; */ // 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.context.outerContext.receiver, // 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); } // 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); } }; 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 (isNaN(counter) || 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 = this.context.accumulator.source.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.isLinked) { if (this.context.accumulator === null) { // check for special cases to speed up if (this.canRunOptimizedForCombine(reporter)) { return this.reportListAggregation( list, reporter.expression.selector ); } // test for base cases if (list.length() < 2) { this.returnValueToParentContext(list.length() ? list.at(1) : 0); return; } // initialize the accumulator 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) { // check for special cases to speed up if (this.canRunOptimizedForCombine(reporter)) { return this.reportListAggregation( list, reporter.expression.selector ); } // test for base cases if (list.length() < 2) { this.returnValueToParentContext(list.length() ? list.at(1) : 0); return; } // initialize the accumulator 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.prototype.reportListAggregation = function (list, selector) { // private - used by reportCombine to optimize certain commutative // operations such as sum, product, min, max hyperized all at once var len = list.length(), result, i; if (len === 0) { switch (selector) { case 'reportProduct': return 1; case 'reportMin': return Infinity; case 'reportMax': return -Infinity; default: // reportSum return 0; } } result = list.at(1); if (len > 1) { for (i = 2; i <= len; i += 1) { result = this[selector](result, list.at(i)); } } return result; }; Process.prototype.canRunOptimizedForCombine = function (aContext) { // private - used by reportCombine to check for optimizable // special cases var op = aContext.expression.selector, eligible; if (!op) { return false; } eligible = ['reportSum', 'reportProduct', 'reportMin', 'reportMax']; if (!contains(eligible, op)) { return false; } // scan the expression's inputs, we can assume there are exactly two, // because we're only looking at eligible selectors. Make sure none is // a non-empty input slot or a variable getter whose name doesn't // correspond to an input of the context. // make sure the context has either no or exactly two inputs. if (aContext.inputs.length === 0) { return aContext.expression.inputs().every(each => each.bindingID); } if (aContext.inputs.length !== 2) { return false; } return aContext.expression.inputs().every(each => each.selector === 'reportGetVar' && contains(aContext.inputs, each.blockSpec) ); }; // 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.itemsArray()), 0, 0 ); } else { for (i = 0; i < channels; i += 1) { arrayBuffer.copyToChannel( Float32Array.from(samples.at(i + 1).itemsArray()), 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; this.checkURLAllowed(url); 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.prototype.checkURLAllowed = function (url) { if ([ 'users', 'logout', 'projects', 'collections' ].some( pathPart => { // Check out whether we're targeting one of the remote domains return Object.values(Cloud.prototype.knownDomains).filter( each => each.includes('snap') ).some( domain => url.match( // Check only against the host -not the protocol, path or // port- of the domain new RegExp(`${(new URL(domain)).host}.*${pathPart}`, 'i')) ); } )) { throw new Error('Request blocked'); } }; // Process event messages primitives Process.prototype.doBroadcast = function (message, receivers) { var stage = this.homeContext.receiver.parentThatIsA(StageMorph), target = this.inputOption(receivers.at(1) || ['all']), thisObj, msg = this.inputOption(message), rcvrs, procs = []; if (!this.canBroadcast) { return []; } // remove all clones when the green flag event is broadcast to all if (msg === '__shout__go__' && target === 'all') { stage.removeAllClones(); } // determine the receivers thisObj = this.blockReceiver(); if (target === 'all') { rcvrs = stage.children.concat(stage); } else if (isSnapObject(target)) { rcvrs = [target]; } else if (isString(target)) { // assume the string to be the name of a sprite or the stage if (target === stage.name) { rcvrs = [stage]; } else { rcvrs = [this.getOtherObject(target, thisObj, stage)]; } } else if (target instanceof List) { // assume all elements to be sprites or sprite names rcvrs = target.itemsArray().map(each => this.getOtherObject(each, thisObj, stage) ); } else { return; // abort } // transmit the message if (msg !== '') { stage.lastMessage = message; // retained for backwards compatibility rcvrs.forEach(morph => { if (isSnapObject(morph)) { morph.allHatBlocksFor(msg).forEach(block => { var varName, varFrame; if (block.selector === 'receiveMessage') { varName = block.inputs()[1].evaluate()[0]; if (varName) { varFrame = new VariableFrame(); varFrame.addVar(varName, message); } procs.push(stage.threads.startProcess( block, morph, stage.isThreadSafe, // commented out for now to enable tail recursion: // || // make "any msg" threadsafe // block.inputs()[0].evaluate() instanceof Array, null, // exportResult (bool) null, // callback null, // isClicked null, // rightAway null, // atomic varFrame )); } else { 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, target) { if (!this.context.activeSends) { this.context.activeSends = this.doBroadcast(message, target); 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 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( localize('expecting a') + ' ' + (typeString instanceof Array ? typeString.reduce((a, b) => localize(a) + ' / ' + localize(b)) // +++ : localize(typeString)) + ' ' + localize('but getting a') + ' ' + localize(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, i, result; if (this.enableHyperOps) { if (this.isMatrix(a)) { if (this.isMatrix(b)) { // zip both arguments ignoring out-of-bounds indices a = a.itemsArray(); b = b.itemsArray(); 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); } return a.map(each => this.hyperDyadic(baseOp, each, b)); } if (this.isMatrix(b)) { 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; if (a instanceof List) { if (b instanceof List) { // zip both arguments ignoring out-of-bounds indices a = a.itemsArray(); b = b.itemsArray(); 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.isMatrix = function (data) { return data instanceof List && data.at(1) instanceof List; }; // 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.reportRandom = function (a, b) { return this.hyperDyadic(this.reportBasicRandom, a, b); }; Process.prototype.reportBasicRandom = function (min, max) { var floor = Math.min(+min, +max), ceil = Math.max(+min, +max); if ((floor % 1 !== 0) || (ceil % 1 !== 0)) { return Math.random() * (ceil - floor) + floor; } return Math.floor(Math.random() * (ceil - floor + 1)) + floor; }; // Process math primtives - arithmetic hyperdyadic 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.reportAtan2 = function (a, b) { return this.hyperDyadic(this.reportBasicAtan2, a, b); }; Process.prototype.reportBasicAtan2 = function (a, b) { return degrees(Math.atan2(+a, +b)); }; Process.prototype.reportMin = function (a, b) { return this.hyperDyadic(this.reportBasicMin, a, b); }; Process.prototype.reportBasicMin = function (a, b) { // return Math.min(+a, +b); // enhanced to also work with text var x = +a, y = +b; if (isNaN(x) || isNaN(y)) { x = a; y = b; } return x < y ? x : y; }; Process.prototype.reportMax = function (a, b) { return this.hyperDyadic(this.reportBasicMax, a, b); }; Process.prototype.reportBasicMax = function (a, b) { // return Math.max(+a, +b); // enhanced to also work with text var x = +a, y = +b; if (isNaN(x) || isNaN(y)) { x = a; y = b; } return x > y ? x : y; }; // Process logic primitives - hyper-diadic / monadic where applicable Process.prototype.reportLessThan = function (a, b) { return this.hyperDyadic(this.reportBasicLessThan, a, b); }; Process.prototype.reportLessThanOrEquals = function (a, b) { return this.hyperDyadic( (a, b) => !this.reportBasicGreaterThan(a, b), 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.reportGreaterThan = function (a, b) { return this.hyperDyadic(this.reportBasicGreaterThan, a, b); }; Process.prototype.reportGreaterThanOrEquals = function (a, b) { return this.hyperDyadic( (a, b) => !this.reportBasicLessThan(a, b), 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.reportNotEquals = function (a, b) { return !snapEquals(a, b); }; 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.reportIsIdentical = function (a, b) { var tag = 'idTag'; if (isString(a) && isString(b)) { // compare texts case-sentitive return a === b; } 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 'sign': result = Math.sign(x); 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; case 'id': return n; 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) { if (this.isAST(aList)) { return this.assemble(aList); } return aList.asText(); } return (aList || '').toString(); }; Process.prototype.isAST = function (aList) { var first = aList.at(1); if (first instanceof Context) { return true; } if (first instanceof List) { return first.at(1) instanceof Context; } return false; }; // 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 : Array.from(data.toString()).length; }; Process.prototype.reportUnicode = function (string) { var str, unicodeList; if (this.enableHyperOps) { if (string instanceof List) { return string.map(each => this.reportUnicode(each)); } str = isNil(string) ? '\u0000' : string.toString(); unicodeList = Array.from(str); if (unicodeList.length > 1) { return this.reportUnicode(new List(unicodeList)); } } 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) { if (this.inputOption(delimiter) === 'blocks') { this.assertType(string, ['command', 'reporter', 'predicate']); return string.components(); } 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( localize('expecting a') + ' ' + localize('text') + ' ' + localize('but getting a') + ' ' + localize(strType) ); } if (!contains(types, delType)) { throw new Error( localize('expecting a') + ' ' + localize('text') + ' ' + localize('but getting a') + ' ' + localize(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 '': case 'letter': return new List(Array.from(str)); 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 = 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 syntax analysis Process.prototype.assemble = function (blocks) { var first; if (!(blocks instanceof List)) { return blocks; } first = blocks.at(1); if (first instanceof Context) { return first.copyWithInputs( blocks.cdr().map(each => this.assemble(each)) ); } if (blocks.isEmpty()) { return blocks; } if (this.reportIsA(blocks.at(1), 'number')) { return blocks.map(each => this.assemble(each)); } return blocks.map(each => this.assemble(each)).itemsArray().reduce( (a, b) => a.copyWithNext(b) ); }; // 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.itemsArray()); } } }; 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.itemsArray()); } } }; // 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 scene primitives Process.prototype.doSwitchToScene = function (id, transmission) { var rcvr = this.blockReceiver(), idx = 0, message = this.inputOption(transmission.at(1)), ide, scenes, num, scene; this.assertAlive(rcvr); this.assertType(message, ['text', 'number', 'Boolean', 'list']); if (message instanceof List) { // make sure only atomic leafs are inside the list // don't actually encode the list as json, though if (message.canBeJSON()) { message = message.deepMap(leaf => leaf); // deep copy the list } else { throw new Error(localize( 'cannot send media,\nsprites or procedures\nto another scene' )); } } if (this.readyToTerminate) { // let the user press "stop" or "esc", // prevent "when this scene starts" hat blocks from directly // switching to another return; } ide = rcvr.parentThatIsA(IDE_Morph); scenes = ide.scenes; if (id instanceof Array) { // special named indices switch (this.inputOption(id)) { case 'next': idx = scenes.indexOf(ide.scene) + 1; if (idx > scenes.length()) { idx = 1; } break; case 'previous': idx = scenes.indexOf(ide.scene) - 1; if (idx < 1) { idx = scenes.length(); } break; /* case 'last': idx = scenes.length(); break; */ case 'random': idx = this.reportBasicRandom(1, scenes.length()); break; } this.stop(); // ide.onNextStep = () => // slow down scene switching, disabled for now ide.switchToScene(scenes.at(idx), null, message); return; } scene = detect(scenes.itemsArray(), scn => scn.name === id); if (scene === null) { num = parseFloat(id); if (isNaN(num)) { return; } scene = scenes.at(num); } this.stop(); ide.switchToScene(scene, null, message); }; // Process color primitives Process.prototype.setColorDimension = function (name, num) { var options = ['hue', 'saturation', 'brightness', 'transparency'], choice = this.inputOption(name); if (choice === 'r-g-b(-a)') { this.blockReceiver().setColorRGBA(num); return; } this.blockReceiver().setColorDimension( options.indexOf(choice), +num ); }; Process.prototype.changeColorDimension = function (name, num) { var options = ['hue', 'saturation', 'brightness', 'transparency'], choice = this.inputOption(name); if (choice === 'r-g-b(-a)') { this.blockReceiver().changeColorRGBA(num); return; } this.blockReceiver().changeColorDimension( options.indexOf(choice), +num ); }; Process.prototype.setPenColorDimension = Process.prototype.setColorDimension; Process.prototype.changePenColorDimension = Process.prototype.changeColorDimension; Process.prototype.setBackgroundColorDimension = Process.prototype.setColorDimension; Process.prototype.changeBackgroundColorDimension = Process.prototype.changeColorDimension; // Process cutting & pasting primitives Process.prototype.doPasteOn = function (name) { this.blitOn(name, 'source-atop'); }; Process.prototype.doCutFrom = function (name) { this.blitOn(name, 'destination-out'); }; Process.prototype.blitOn = function (name, mask, 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.blitOn(name, mask); } if (name instanceof List) { // assume all elements to be sprites those = name.itemsArray(); } else { those = this.getObjectsNamed(name, thisObj, stage); // clones } those.forEach(each => { // only draw on same-named clones that don't dynamically the costume if (!each.inheritsAttribute('costume #')) { this.blitOn(each, mask, 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' - hsl HUE on a scale of 0 - 100 // 'saturation' - hsl SATURATION on a scale of 0 - 100 // 'brightness' - hsl BRIGHTNESS 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. if (this.enableHyperOps) { if (location instanceof List && !this.isCoordinate(location)) { return location.map(each => this.reportAspect(aspect, each)); } } 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[SpriteMorph.prototype.penColorModel]()[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 BLACK; } 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 BLACK; }; 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 BLACK; } 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 BLACK; }; 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) { if (this.enableHyperOps) { if (name instanceof List && !this.isCoordinate(name)) { return name.map(each => this.reportRelationTo(relation, each)); } } var rel = this.inputOption(relation); if (rel === 'distance') { return this.reportDistanceTo(name); } if (rel === 'ray length') { return this.reportRayLengthTo(name); } if (rel === 'direction') { return this.reportDirectionTo(name); } return 0; }; Process.prototype.isCoordinate = function (data) { return data instanceof List && (data.length() === 2) && this.reportTypeOf(data.at(1)) === 'number' && this.reportTypeOf(data.at(2)) === 'number'; }; 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.reportRayLengthTo = function (name) { // raycasting edge detection - answer the distance between the asking // sprite's rotation center to the target sprite's outer edge (the first // opaque pixel) in the asking sprite's current direction var thisObj = this.blockReceiver(), thatObj, stage, rc, targetBounds, intersections = [], dir, a, b, x, y, top, bottom, left, right, circa, hSect, vSect, point, hit, temp, width, imageData; circa = (num) => Math.round(num * 10000000) / 10000000; // good enough hSect = (yLevel) => { var theta = radians(dir); b = rc.y - yLevel; a = b * Math.tan(theta); x = rc.x + a; if ( (circa(x) === circa(rc.x) && ((dir === 180 && rc.y < yLevel) || dir === 0 && rc.y > yLevel) ) || (x > rc.x && dir >= 0 && dir < 180) || (circa(x) < circa(rc.x) && dir >= 180 && dir < 360) ) { if (x >= left && x <= right) { intersections.push(new Point(x, yLevel)); } } }; vSect = (xLevel) => { var theta = radians(360 - dir - 90); b = rc.x - xLevel; a = b * Math.tan(theta); y = rc.y + a; if ( (circa(y) === circa(rc.y) && ((dir === 90 && rc.x < xLevel) || dir === 270 && rc.x > xLevel) ) || (y > rc.y && dir >= 90 && dir < 270) || (y < rc.y && (dir >= 270 || dir < 90)) ) { if (y >= top && y <= bottom) { intersections.push(new Point(xLevel, y)); } } }; if (!thisObj) {return -1; } rc = thisObj.rotationCenter(); point = rc; stage = thisObj.parentThatIsA(StageMorph); thatObj = this.getOtherObject(name, thisObj, stage); if (!(thatObj instanceof SpriteMorph)) {return -1; } // determine intersections with the target's bounding box dir = thisObj.heading; targetBounds = thatObj.bounds; top = targetBounds.top(); bottom = targetBounds.bottom(); left = targetBounds.left(); right = targetBounds.right(); // test if already inside the target if (targetBounds.containsPoint(rc)) { intersections.push(rc); hSect(top); hSect(bottom); vSect(left); vSect(right); if (intersections.length < 2) { return -1; } } else { hSect(top); hSect(bottom); vSect(left); vSect(right); if (intersections.length < 2) { return -1; } // sort if (dir !== 90) { if (Math.sign(rc.x - intersections[0].x) !== Math.sign(intersections[0].x - intersections[1].x) || Math.sign(rc.y - intersections[0].y) !== Math.sign(intersections[0].y - intersections[1].y) ) { temp = intersections[0]; intersections[0] = intersections[1]; intersections[1] = temp; } } } // for debugging: /* return new List(intersections) .map(point => thisObj.snapPoint(point)) .map(point => new List([point.x, point.y])); */ // convert intersections to local bitmap coordinates of the target intersections = intersections.map(point => point.subtract(targetBounds.origin).floorDivideBy(stage.scale) ); // get image data width = Math.floor(targetBounds.width() / stage.scale); imageData = thatObj.getImageData(); // scan the ray along the coordinates of a Bresenham line // for the first opaque pixel function alphaAt(imageData, width, x, y) { var idx = y * width + x; return imageData[idx] && 0x000000FF; // alpha } function isOpaque(x, y) { return alphaAt(imageData, width, x, y) > 0; } function scan(testFunc, x0, y0, x1, y1) { // Bresenham's algorithm var dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1, dy = -Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1, err = dx + dy, e2; while (true) { if (testFunc(x0, y0)) { return new Point(x0 * stage.scale, y0 * stage.scale); } if (x0 === x1 && y0 === y1) { return -1; // not found } e2 = 2 * err; if (e2 > dy) { err += dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } hit = scan( isOpaque, intersections[0].x, intersections[0].y, intersections[1].x, intersections[1].y ); if (hit === -1) {return hit; } return rc.distanceTo(hit.add(targetBounds.origin)) / stage.scale; }; 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.reportBlockAttribute = function (attribute, block) { // hyper-dyadic // note: attributes in the left slot // can only be queried via the dropdown menu and are, therefore, not // reachable as dyadic inputs return this.hyperDyadic( (att, obj) => this.reportBasicBlockAttribute(att, obj), attribute, block ); }; Process.prototype.reportBasicBlockAttribute = function (attribute, block) { var choice = this.inputOption(attribute), expr; this.assertType(block, ['command', 'reporter', 'predicate']); expr = block.expression; switch (choice) { case 'definition': if (expr.isCustomBlock) { if (expr.isGlobal) { return expr.definition.body || new Context(); } return this.blockReceiver().getMethod(expr.semanticSpec).body || new Context(); } return new Context(); case 'custom?': return expr ? !!expr.isCustomBlock : false; case 'global?': return (expr && expr.isCustomBlock) ? !!expr.isGlobal : true; } return ''; }; Process.prototype.reportAttributeOf = function (attribute, name) { // hyper-dyadic // note: specifying strings in the left input only accesses // sprite-local variables. Attributes such as "width", "direction" etc. // can only be queried via the dropdown menu and are, therefore, not // reachable as dyadic inputs return this.hyperDyadic( (att, obj) => this.reportBasicAttributeOf(att, obj), attribute, name ); }; Process.prototype.reportBasicAttributeOf = 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(), 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': // old rectangular, bounding-box-based algorithm // deprecated in favor of a circular perimeter based newer one /* 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.isVisible && (each !== thisObj) && each.bounds.intersects(neighborhood) ) ); */ return thisObj.neighbors(); 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 'scripts': return new List( thisObj.scripts.children.filter( each => each instanceof BlockMorph ).map( each => each.fullCopy().reify() ) ); case 'blocks': // palette unoordered without inherited methods return new List( thisObj.parentThatIsA(StageMorph).globalBlocks.concat( thisObj.allBlocks(true) ).filter( def => !def.isHelper ).map( def => def.blockInstance().reify() ).concat( SpriteMorph.prototype.categories.reduce( (blocks, category) => blocks.concat( thisObj.getPrimitiveTemplates( category ).filter( each => each instanceof BlockMorph && !(each instanceof HatBlockMorph) ).map(block => { let instance = block.fullCopy(); instance.isTemplate = false; return instance.reify(); }) ), [] ) ) ); 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) { // hyper-monadic if (this.enableHyperOps) { if (name instanceof List) { return name.map(each => this.reportObject(each)); } } 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) { // 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': case 'my 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': case 'my 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?': case 'my temporary?': this.assertType(rcvr, 'sprite'); this.assertType(value, 'Boolean'); if (value) { rcvr.release(); } else { rcvr.perpetuate(); } break; case 'name': case 'my 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?': case 'my dangling?': this.assertType(rcvr, 'sprite'); this.assertType(value, 'Boolean'); rcvr.rotatesWithAnchor = !value; rcvr.version = Date.now(); break; case 'draggable?': case 'my 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': case 'my 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': case 'my rotation x': this.assertType(rcvr, 'sprite'); this.assertType(value, 'number'); rcvr.setRotationX(value); break; case 'rotation y': case 'my 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), receiverVars, rootVars; result.receiver = otherObj; if (!result.outerContext) { result.outerContext = new Context(); result.variables.parentFrame = result.outerContext.variables; } result.outerContext = copy(result.outerContext); result.outerContext.variables = copy(result.outerContext.variables); result.outerContext.receiver = otherObj; if (result.outerContext.variables.parentFrame) { rootVars = result.outerContext.variables.parentFrame; receiverVars = copy(otherObj.variables); receiverVars.parentFrame = rootVars; result.outerContext.variables.parentFrame = receiverVars; } else { result.outerContext.variables.parentFrame = otherObj.variables; } return result; }; Process.prototype.reportMouseX = function () { var world; if (this.homeContext.receiver) { world = this.homeContext.receiver.world(); if (world) { return this.homeContext.receiver.snapPoint(world.hand.position()).x; } } return 0; }; Process.prototype.reportMouseY = function () { var world; if (this.homeContext.receiver) { world = this.homeContext.receiver.world(); if (world) { return this.homeContext.receiver.snapPoint(world.hand.position()).y; } } 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) { // hyper-monadic 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; } if (keyString instanceof List && this.enableHyperOps) { return keyString.map( each => stage.keysPressed[each] !== undefined ); } 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) { // hyper-monadic var thisObj = this.blockReceiver(), stage = thisObj.parentThatIsA(StageMorph), thatObj; 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; } if (this.enableHyperOps) { if (name instanceof List) { return name.map(each => this.reportVideo(attribute, each)); } } thatObj = this.getOtherObject(name, thisObj, stage); 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 + '\'\n' + localize('is 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) { if (this.enableHyperOps) { if (name instanceof List) { return name.map(each => this.reportGetImageAttribute(choice, each)); } } 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.itemsArray(); dta = ctx.createImageData(width, height); for (i = 0; i < src.length; i += 1) { px = src[i] instanceof List ? src[i].itemsArray() : [src[i]]; for (k = 0; k < 3; k += 1) { dta.data[(i * 4) + k] = px[k] === undefined ? +px[0] : +px[k]; } dta.data[i * 4 + 3] = (px[3] === undefined ? 255 : +px[3]); } 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.prototype.reportYieldCount = function () { return this.yieldCount; }; // 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.itemsArray(), 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.itemsArray(), 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.itemsArray(), 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 ''; }; 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 var result, src, len, formalParameterCount, parms, func, i; this.assertType(list, 'list'); // check for special cases to speed up if (this.canRunOptimizedForCombine(reporter)) { return this.reportListAggregation( list, reporter.expression.selector ); } result = ''; src = list.itemsArray(); len = src.length; formalParameterCount = reporter.inputs.length; 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.itemsArray().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.itemsArray(), 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 = this.toBlock(); return ring.doWithAlpha(1, () => ring.fullImage()); }; Context.prototype.toBlock = 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); ring.clearAlpha(); return ring; } if (this.expression instanceof Array) { block = this.expression[this.pc].fullCopy(); if (block instanceof RingMorph && !block.contents()) { // empty ring return block; } ring.embed(block, this.isContinuation ? [] : this.inputs); return ring; } // otherwise show an empty ring ring.color = SpriteMorph.prototype.blockColor.other; ring.setSpec('%rr %ringparms'); // also show my inputs, unless I'm a continuation if (!this.isContinuation) { this.inputs.forEach(inp => ring.parts()[1].addInput(inp) ); } return ring; }; // Context continuations: Context.prototype.continuation = function (isReporter) { var cont; if (this.expression instanceof Array) { cont = this; } else if (this.parentContext) { cont = this.parentContext; } else { cont = new Context( null, isReporter ? 'expectReport' : 'popContext' ); 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 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(); }; Context.prototype.isInCustomBlock = function () { if (this.isCustomBlock) { return true; } if (this.parentContext) { return this.parentContext.isInCustomBlock(); } return false; }; // Context syntax analysis Context.prototype.components = function () { var expr = this.expression; if (expr && expr.components) { expr = expr.components(this.inputs.slice()); } else { expr = new Context(); expr.inputs = this.inputs.slice(); } return expr instanceof Context ? new List([expr]) : expr; }; Context.prototype.equalTo = function (other) { var c1 = this.components(), c2 = other.components(); if (this.emptyOrEqual(c1.cdr(), c2.cdr())) { if (this.expression && this.expression.length === 1 && other.expression && other.expression.length === 1) { return snapEquals(this.expression[0], other.expression[0]); } return snapEquals(this.expression, other.expression); } return false; }; Context.prototype.emptyOrEqual = function (list1, list2) { // private - return TRUE if both lists are either equal // or only contain empty items return list1.equalTo(list2) || ( list1.itemsArray().every(item => !item) && list2.itemsArray().every(item => !item) ); }; Context.prototype.copyWithInputs = function (inputs) { return this.expression ? this.expression.copyWithInputs(inputs) : this; }; Context.prototype.copyWithNext = function (next) { return this.expression.copyWithNext(next.expression, this.inputs.slice()); }; Context.prototype.updateEmptySlots = function () { this.emptySlots = this.expression.markEmptySlots(); }; // Variable ///////////////////////////////////////////////////////////////// function Variable(value, isTransient, isHidden) { this.value = value; this.isTransient = isTransient || false; // prevent value serialization this.isHidden = isHidden || false; // not shown in the blocks palette } Variable.prototype.toString = function () { return 'a ' + (this.isTransient ? 'transient ' : '') + (this.isHidden ? 'hidden ' : '') + 'Variable [' + this.value + ']'; }; Variable.prototype.copy = function () { return new Variable(this.value, this.isTransient, this.isHidden); }; // 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 (includeHidden) { var each, names = []; for (each in this.vars) { if (Object.prototype.hasOwnProperty.call(this.vars, each)) { if (!this.vars[each].isHidden || includeHidden) { names.push(each); } } } return names; }; VariableFrame.prototype.allNamesDict = function (upTo, includeHidden) { // "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)) { if (!srcDict[eachKey].isHidden || includeHidden) { trgtDict[eachKey] = eachKey; } } } } while (current && (current !== upTo)) { addKeysToDict(current.vars, dict); current = current.parentFrame; } return dict; }; VariableFrame.prototype.allNames = function (upTo, includeHidden) { /* 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, includeHidden); 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 evaluation primitives case 'doRun': case 'evaluate': return 'invoke(' + this.compileInput(inputs[0]) + ',' + this.compileInput(inputs[1]) + ')'; // 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 ); } };