From 41b68edd90aab89a55bb133647eace96bc25f2eb Mon Sep 17 00:00:00 2001 From: Chad Walker Date: Mon, 8 Sep 2014 10:34:08 -0500 Subject: [PATCH] Add procedure support - when a call block is executed, the block is copied, any parameters are evaluated and stored on the copy, then the copy is pushed on the stack - when a getParam block is executed it finds the last call block on the stack, and then gets the already evaluated parameters from there. - based on PR #21 by @bobbybee, passes all of @nathan's tests there --- js/Interpreter.js | 81 +++++++++++++++++++++++++++++++++++++ js/Runtime.js | 7 ++++ js/Sprite.js | 3 ++ js/Stage.js | 2 +- js/primitives/Primitives.js | 1 - 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/js/Interpreter.js b/js/Interpreter.js index 2c1a517..ed7655e 100644 --- a/js/Interpreter.js +++ b/js/Interpreter.js @@ -29,9 +29,22 @@ var Block = function(opAndArgs, optionalSubstack) { this.subStack2 = null; this.nextBlock = null; this.tmp = -1; + // used in procedure blocks and call blocks + // in procedure blocks it's a map of parameter name to array [position, default value] + // in call blocks it's a map of parameter name to evaluated value + this.namedParams = {}; + this.asyncFlag = false; // used in procedure hats interp.fixArgs(this); }; +Block.copyBlock = function (srcBlock) { + var dstBlock = new Block([srcBlock.op]); + ['primFcn', 'args', 'isLoop', 'substack', 'subStack2', 'nextBlock', 'tmp', 'namedParams', 'asyncFlag'].forEach(function (property) { + dstBlock[property] = srcBlock[property]; + }); + return dstBlock; +}; + var Thread = function(block, target) { this.nextBlock = block; // next block to run; null when thread is finished this.firstBlock = block; @@ -306,6 +319,11 @@ Interpreter.prototype.initPrims = function() { this.primitiveTable['timerReset'] = function(b) { interp.timerBase = Date.now(); }; this.primitiveTable['timer'] = function(b) { return (Date.now() - interp.timerBase) / 1000; }; + // procedure primatives + this.primitiveTable['procDef'] = this.primProcDef; + this.primitiveTable["getParam"] = this.primGetParam; + this.primitiveTable['call'] = this.primCall; + new Primitives().addPrimsTo(this.primitiveTable); }; @@ -391,3 +409,66 @@ Interpreter.prototype.startSubstack = function(b, isLoop, secondSubstack) { this.activeThread.nextBlock = b.substack2; } }; + +Interpreter.prototype.primProcDef = function (b) { + var namedParams = b.args.slice(1); // proc name was 0, and async flag is last + b.asyncFlag = namedParams.pop(); + var i; + for (i = 0; i < namedParams.length; i += 2) { + b.namedParams[namedParams[i].op] = [i / 2, namedParams[i + 1].op]; + } +}; + +Interpreter.prototype.primGetParam = function (b) { + // the last call block on the stack is the one with the parameters + var callBlock; + for (var i = interp.activeThread.stack.length - 1; i >= 0; i--) { + if (interp.activeThread.stack[i].op === 'call') { + callBlock = interp.activeThread.stack[i]; + break; + } + } + if (!callBlock) { + console.log('failed to find a callBlock to get parameters from'); + return; + } + + var paramName = interp.arg(b, 0); + + if (!callBlock.namedParams.hasOwnProperty(paramName)) { + console.log('got getParam block missing parameter name', paramName); + return; + } + + return callBlock.namedParams[paramName]; +}; + +Interpreter.prototype.primCall = function (b) { + // first make a copy of the block (so recursive calls can evaluate + // parameters as different values) + b = Block.copyBlock(b); + + var procedure = interp.targetSprite().procedures[interp.arg(b, 0)]; + // any parameterss for the procedure will be in the block at the top of + // the stack, but if there are any blocks as parameters, we need to + // evaluate them now, and save the values in place in the call block + + var paramNames = Object.keys(procedure.namedParams); + var args = b.args.slice(1); + + paramNames.forEach(function (paramName) { + var i = procedure.namedParams[paramName][0]; + // searching for paramName + if (i < args.length) { + b.namedParams[paramName] = interp.arg(b, i + 1); + } else { + b.namedParams[paramName] = procedure.namedParams[paramName][1]; + } + }); + + // push the copy block onto the stack, so when the call returns we can continue + interp.activeThread.stack.push(b); + + // jump to the procedure, should probably check args first + interp.activeThread.nextBlock = procedure; +}; diff --git a/js/Runtime.js b/js/Runtime.js index 3952618..d112a01 100644 --- a/js/Runtime.js +++ b/js/Runtime.js @@ -70,6 +70,13 @@ Runtime.prototype.loadStart = function() { setTimeout(function(runtime) { runtime.loadStart(); }, 50, this); return; } + runtime.allStacksDo(function (stack, target) { + if (stack.op === 'procDef') { + target.procedures[stack.args[0]] = stack; + stack.primFcn(stack); // initialize parameters + } + }); + $('#preloader').css('display', 'none'); setInterval(this.step, 33); this.projectLoaded = true; diff --git a/js/Sprite.js b/js/Sprite.js index 463b9c6..e1a1180 100644 --- a/js/Sprite.js +++ b/js/Sprite.js @@ -111,6 +111,9 @@ var Sprite = function(data) { // Stacks to be pushed to the interpreter and run this.stacks = []; + + // Procedures by name + this.procedures = {}; }; // Attaches a Sprite () to a Scratch scene diff --git a/js/Stage.js b/js/Stage.js index e433955..2470524 100644 --- a/js/Stage.js +++ b/js/Stage.js @@ -51,7 +51,7 @@ Stage.prototype.attachPenLayer = function(scene) { }; Stage.prototype.isLoaded = function() { - return this.penLayerLoaded && this.costumesLoaded == this.costumes.length && this.soundsLoaded == Object.keys(this.sounds).length; + return this.penLayerLoaded && Sprite.prototype.isLoaded.call(this); }; // Pen functions diff --git a/js/primitives/Primitives.js b/js/primitives/Primitives.js index 62f9298..55257df 100644 --- a/js/primitives/Primitives.js +++ b/js/primitives/Primitives.js @@ -105,4 +105,3 @@ Primitives.prototype.primMathFunction = function(b) { } return 0; } -