From 8c654bbe607c88b2d74aeef95fd08324927598be Mon Sep 17 00:00:00 2001 From: Tim Mickel Date: Thu, 13 Oct 2016 13:11:26 -0400 Subject: [PATCH] Procedure blocks (#264) --- src/blocks/scratch3_procedures.js | 19 +++++++- src/engine/blocks.js | 23 +++++++++- src/engine/execute.js | 9 ++++ src/engine/sequencer.js | 5 +++ src/engine/thread.js | 16 +++++++ src/import/sb2import.js | 74 ++++++++++++++++++++++++++++++- src/import/sb2specmap.js | 14 ++---- 7 files changed, 144 insertions(+), 16 deletions(-) diff --git a/src/blocks/scratch3_procedures.js b/src/blocks/scratch3_procedures.js index 8d43c4c92..eb33e322b 100644 --- a/src/blocks/scratch3_procedures.js +++ b/src/blocks/scratch3_procedures.js @@ -13,7 +13,8 @@ function Scratch3ProcedureBlocks(runtime) { Scratch3ProcedureBlocks.prototype.getPrimitives = function() { return { 'procedures_defnoreturn': this.defNoReturn, - 'procedures_callnoreturn': this.callNoReturn + 'procedures_callnoreturn': this.callNoReturn, + 'procedures_param': this.param }; }; @@ -23,10 +24,24 @@ Scratch3ProcedureBlocks.prototype.defNoReturn = function () { Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) { if (!util.stackFrame.executed) { - var procedureName = args.mutation.name; + var procedureName = args.mutation.proccode; + var paramNames = util.getProcedureParamNames(procedureName); + for (var i = 0; i < paramNames.length; i++) { + if (args.hasOwnProperty('input' + i)) { + util.pushParam(paramNames[i], args['input' + i]); + } + } util.stackFrame.executed = true; util.startProcedure(procedureName); } }; +Scratch3ProcedureBlocks.prototype.param = function (args, util) { + var value = util.getParam(args.mutation.paramname); + if (!value) { + return 0; + } + return value; +}; + module.exports = Scratch3ProcedureBlocks; diff --git a/src/engine/blocks.js b/src/engine/blocks.js index f96c916ad..e3384d46c 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -151,13 +151,30 @@ Blocks.prototype.getProcedureDefinition = function (name) { var block = this._blocks[id]; if ((block.opcode == 'procedures_defnoreturn' || block.opcode == 'procedures_defreturn') && - block.fields['NAME'].value == name) { + block.mutation.proccode == name) { return id; } } return null; }; +/** + * Get the procedure definition for a given name. + * @param {?string} name Name of procedure to query. + * @return {?string} ID of procedure definition. + */ +Blocks.prototype.getProcedureParamNames = function (name) { + for (var id in this._blocks) { + var block = this._blocks[id]; + if ((block.opcode == 'procedures_defnoreturn' || + block.opcode == 'procedures_defreturn') && + block.mutation.proccode == name) { + return JSON.parse(block.mutation.argumentnames); + } + } + return null; +}; + // --------------------------------------------------------------------- /** @@ -434,7 +451,9 @@ Blocks.prototype.mutationToXML = function (mutation) { var mutationString = '<' + mutation.tagName; for (var prop in mutation) { if (prop == 'children' || prop == 'tagName') continue; - mutationString += ' ' + prop + '="' + mutation[prop] + '"'; + var mutationValue = (typeof mutation[prop] === 'string') ? + xmlEscape(mutation[prop]) : mutation[prop]; + mutationString += ' ' + prop + '="' + mutationValue + '"'; } mutationString += '>'; for (var i = 0; i < mutation.children.length; i++) { diff --git a/src/engine/execute.js b/src/engine/execute.js index 59b4a3cbf..241f238a3 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -164,6 +164,15 @@ var execute = function (sequencer, thread) { startProcedure: function (procedureName) { sequencer.stepToProcedure(thread, procedureName); }, + getProcedureParamNames: function (procedureName) { + return thread.target.blocks.getProcedureParamNames(procedureName); + }, + pushParam: function (paramName, paramValue) { + thread.pushParam(paramName, paramValue); + }, + getParam: function (paramName) { + return thread.getParam(paramName); + }, startHats: function(requestedHat, opt_matchFields, opt_target) { return ( runtime.startHats(requestedHat, opt_matchFields, opt_target) diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 85575c804..0e54ca4ab 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -132,6 +132,11 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) { Sequencer.prototype.stepToProcedure = function (thread, procedureName) { var definition = thread.target.blocks.getProcedureDefinition(procedureName); thread.pushStack(definition); + // Check if the call is recursive. If so, yield. + // @todo: Have behavior match Scratch 2.0. + if (thread.stack.indexOf(definition) > -1) { + thread.setStatus(Thread.STATUS_YIELD_FRAME); + } }; /** diff --git a/src/engine/thread.js b/src/engine/thread.js index 3bb53596b..6a08361fd 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -83,6 +83,7 @@ Thread.prototype.pushStack = function (blockId) { this.stackFrames.push({ reported: {}, // Collects reported input values. waitingReporter: null, // Name of waiting reporter. + params: {}, // Procedure parameters. executionContext: {} // A context passed to block implementations. }); } @@ -135,6 +136,21 @@ Thread.prototype.pushReportedValue = function (value) { } }; +Thread.prototype.pushParam = function (paramName, value) { + var stackFrame = this.peekStackFrame(); + stackFrame.params[paramName] = value; +}; + +Thread.prototype.getParam = function (paramName) { + for (var i = this.stackFrames.length - 1; i >= 0; i--) { + var frame = this.stackFrames[i]; + if (frame.params.hasOwnProperty(paramName)) { + return frame.params[paramName]; + } + } + return null; +}; + /** * Whether the current execution of a thread is at the top of the stack. * @return {Boolean} True if execution is at top of the stack. diff --git a/src/import/sb2import.js b/src/import/sb2import.js index f633de9c0..752e39298 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.js @@ -203,6 +203,40 @@ function flatten (blocks) { return finalBlocks; } +/** + * Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n") + * into an argument map. This allows us to provide the expected inputs + * to a mutated procedure call. + * @param {string} procCode Scratch 2.0 procedure string. + * @return {Object} Argument map compatible with those in sb2specmap. + */ +function parseProcedureArgMap (procCode) { + var argMap = [ + {} // First item in list is op string. + ]; + var INPUT_PREFIX = 'input'; + var inputCount = 0; + // Split by %n, %b, %s. + var parts = procCode.split(/(?=[^\\]\%[nbs])/); + for (var i = 0; i < parts.length; i++) { + var part = parts[i].trim(); + if (part.substring(0, 1) == '%') { + var argType = part.substring(1, 2); + var arg = { + type: 'input', + inputName: INPUT_PREFIX + (inputCount++) + }; + if (argType == 'n') { + arg.inputOp = 'math_number'; + } else if (argType == 's') { + arg.inputOp = 'text'; + } + argMap.push(arg); + } + } + return argMap; +} + /** * Parse a single SB2 JSON-formatted block and its children. * @param {!Object} sb2block SB2 JSON-formatted block. @@ -227,6 +261,10 @@ function parseBlock (sb2block) { shadow: false, // No shadow blocks in an SB2 by default. children: [] // Store any generated children, flattened in `flatten`. }; + // For a procedure call, generate argument map from proc string. + if (oldOpcode == 'call') { + blockMetadata.argMap = parseProcedureArgMap(sb2block[1]); + } // Look at the expected arguments in `blockMetadata.argMap.` // The basic problem here is to turn positional SB2 arguments into // non-positional named Scratch VM arguments. @@ -328,11 +366,43 @@ function parseBlock (sb2block) { } } // Special cases to generate mutations. - if (oldOpcode == 'call') { + if (oldOpcode == 'stopScripts') { + // Mutation for stop block: if the argument is 'other scripts', + // the block needs a next connection. + if (sb2block[1] == 'other scripts in sprite') { + activeBlock.mutation = { + tagName: 'mutation', + hasnext: 'true', + children: [] + }; + } + } else if (oldOpcode == 'procDef') { + // Mutation for procedure definition: + // store all 2.0 proc data. + var procData = sb2block.slice(1); + activeBlock.mutation = { + tagName: 'mutation', + proccode: procData[0], // e.g., "abc %n %b %s" + argumentnames: JSON.stringify(procData[1]), // e.g. ['arg1', 'arg2'] + argumentdefaults: JSON.stringify(procData[2]), // e.g., [1, 'abc'] + warp: procData[3], // Warp mode, e.g., true/false. + children: [] + }; + } else if (oldOpcode == 'call') { + // Mutation for procedure call: + // string for proc code (e.g., "abc %n %b %s"). activeBlock.mutation = { tagName: 'mutation', children: [], - name: sb2block[1] + proccode: sb2block[1] + }; + } else if (oldOpcode == 'getParam') { + // Mutation for procedure parameter. + activeBlock.mutation = { + tagName: 'mutation', + children: [], + paramname: sb2block[1], // Name of parameter. + shape: sb2block[2] // Shape - in 2.0, 'r' or 'b'. }; } return activeBlock; diff --git a/src/import/sb2specmap.js b/src/import/sb2specmap.js index 76e9683e1..1cf0620fc 100644 --- a/src/import/sb2specmap.js +++ b/src/import/sb2specmap.js @@ -752,9 +752,8 @@ var specMap = { 'opcode':'control_stop', 'argMap':[ { - 'type':'input', - 'inputOp':'control_stop_menu', - 'inputName':'STOP_OPTION' + 'type':'field', + 'fieldName':'STOP_OPTION' } ] }, @@ -1375,15 +1374,10 @@ var specMap = { }, 'procDef':{ 'opcode':'procedures_defnoreturn', - 'argMap':[ - { - 'type':'field', - 'fieldName':'NAME' - } - ] + 'argMap':[] }, 'getParam':{ - 'opcode':'proc_param', + 'opcode':'procedures_param', 'argMap':[] }, 'call':{