diff --git a/src/blocks/scratch3_procedures.js b/src/blocks/scratch3_procedures.js new file mode 100644 index 000000000..8d43c4c92 --- /dev/null +++ b/src/blocks/scratch3_procedures.js @@ -0,0 +1,32 @@ +function Scratch3ProcedureBlocks(runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; +} + +/** + * Retrieve the block primitives implemented by this package. + * @return {Object.} Mapping of opcode to Function. + */ +Scratch3ProcedureBlocks.prototype.getPrimitives = function() { + return { + 'procedures_defnoreturn': this.defNoReturn, + 'procedures_callnoreturn': this.callNoReturn + }; +}; + +Scratch3ProcedureBlocks.prototype.defNoReturn = function () { + // No-op: execute the blocks. +}; + +Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) { + if (!util.stackFrame.executed) { + var procedureName = args.mutation.name; + util.stackFrame.executed = true; + util.startProcedure(procedureName); + } +}; + +module.exports = Scratch3ProcedureBlocks; diff --git a/src/engine/adapter.js b/src/engine/adapter.js index 26f52be90..327aae5e3 100644 --- a/src/engine/adapter.js +++ b/src/engine/adapter.js @@ -1,3 +1,4 @@ +var mutationAdapter = require('./mutation-adapter'); var html = require('htmlparser2'); /** @@ -138,6 +139,9 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) { // Link next block to this block. block.next = childBlockNode.attribs.id; break; + case 'mutation': + block.mutation = mutationAdapter(xmlChild); + break; } } } diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 99f52477d..f96c916ad 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -1,4 +1,5 @@ var adapter = require('./adapter'); +var mutationAdapter = require('./mutation-adapter'); var xmlEscape = require('../util/xml-escape'); /** @@ -116,6 +117,16 @@ Blocks.prototype.getInputs = function (id) { return inputs; }; +/** + * Get mutation data for a block. + * @param {?string} id ID of block to query. + * @return {!Object} Mutation for the block. + */ +Blocks.prototype.getMutation = function (id) { + if (typeof this._blocks[id] === 'undefined') return null; + return this._blocks[id].mutation; +}; + /** * Get the top-level script for a given block. * @param {?string} id ID of block to query. @@ -130,6 +141,23 @@ Blocks.prototype.getTopLevelScript = function (id) { return block.id; }; +/** + * Get the procedure definition for a given name. + * @param {?string} name Name of procedure to query. + * @return {?string} ID of procedure definition. + */ +Blocks.prototype.getProcedureDefinition = function (name) { + for (var id in this._blocks) { + var block = this._blocks[id]; + if ((block.opcode == 'procedures_defnoreturn' || + block.opcode == 'procedures_defreturn') && + block.fields['NAME'].value == name) { + return id; + } + } + return null; +}; + // --------------------------------------------------------------------- /** @@ -226,12 +254,16 @@ Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) { */ Blocks.prototype.changeBlock = function (args) { // Validate - if (args.element !== 'field') return; + if (args.element !== 'field' && args.element !== 'mutation') return; if (typeof this._blocks[args.id] === 'undefined') return; - if (typeof this._blocks[args.id].fields[args.name] === 'undefined') return; - // Update block value - this._blocks[args.id].fields[args.name].value = args.value; + if (args.element == 'field') { + // Update block value + if (!this._blocks[args.id].fields[args.name]) return; + this._blocks[args.id].fields[args.name].value = args.value; + } else if (args.element == 'mutation') { + this._blocks[args.id].mutation = mutationAdapter(args.value); + } }; /** @@ -355,6 +387,10 @@ Blocks.prototype.blockToXML = function (blockId) { ' type="' + block.opcode + '"' + xy + '>'; + // Add any mutation. Must come before inputs. + if (block.mutation) { + xmlString += this.mutationToXML(block.mutation); + } // Add any inputs on this block. for (var input in block.inputs) { var blockInput = block.inputs[input]; @@ -389,6 +425,25 @@ Blocks.prototype.blockToXML = function (blockId) { return xmlString; }; +/** + * Recursively encode a mutation object to XML. + * @param {!Object} mutation Object representing a mutation. + * @return {string} XML string representing a mutation. + */ +Blocks.prototype.mutationToXML = function (mutation) { + var mutationString = '<' + mutation.tagName; + for (var prop in mutation) { + if (prop == 'children' || prop == 'tagName') continue; + mutationString += ' ' + prop + '="' + mutation[prop] + '"'; + } + mutationString += '>'; + for (var i = 0; i < mutation.children.length; i++) { + mutationString += this.mutationToXML(mutation.children[i]); + } + mutationString += ''; + return mutationString; +}; + // --------------------------------------------------------------------- /** diff --git a/src/engine/execute.js b/src/engine/execute.js index 29116d5e5..59b4a3cbf 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -132,6 +132,12 @@ var execute = function (sequencer, thread) { argValues[inputName] = currentStackFrame.reported[inputName]; } + // Add any mutation to args (e.g., for procedures). + var mutation = target.blocks.getMutation(currentBlockId); + if (mutation) { + argValues.mutation = mutation; + } + // If we've gotten this far, all of the input blocks are evaluated, // and `argValues` is fully populated. So, execute the block primitive. // First, clear `currentStackFrame.reported`, so any subsequent execution @@ -155,6 +161,9 @@ var execute = function (sequencer, thread) { startBranch: function (branchNum) { sequencer.stepToBranch(thread, branchNum); }, + startProcedure: function (procedureName) { + sequencer.stepToProcedure(thread, procedureName); + }, startHats: function(requestedHat, opt_matchFields, opt_target) { return ( runtime.startHats(requestedHat, opt_matchFields, opt_target) diff --git a/src/engine/mutation-adapter.js b/src/engine/mutation-adapter.js new file mode 100644 index 000000000..12dc123e1 --- /dev/null +++ b/src/engine/mutation-adapter.js @@ -0,0 +1,39 @@ +var html = require('htmlparser2'); + +/** + * Adapter between mutator XML or DOM and block representation which can be + * used by the Scratch runtime. + * @param {(Object|string)} mutation Mutation XML string or DOM. + * @return {Object} Object representing the mutation. + */ +module.exports = function (mutation) { + var mutationParsed; + // Check if the mutation is already parsed; if not, parse it. + if (typeof mutation === 'object') { + mutationParsed = mutation; + } else { + mutationParsed = html.parseDOM(mutation)[0]; + } + return mutatorTagToObject(mutationParsed); +}; + +/** + * Convert a part of a mutation DOM to a mutation VM object, recursively. + * @param {Object} dom DOM object for mutation tag. + * @return {Object} Object representing useful parts of this mutation. + */ +function mutatorTagToObject (dom) { + var obj = Object.create(null); + obj.tagName = dom.name; + obj.children = []; + for (var prop in dom.attribs) { + if (prop == 'xmlns') continue; + obj[prop] = dom.attribs[prop]; + } + for (var i = 0; i < dom.children.length; i++) { + obj.children.push( + mutatorTagToObject(dom.children[i]) + ); + } + return obj; +} diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 75067be3d..18cffd8ec 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -15,7 +15,8 @@ var defaultBlockPackages = { 'scratch3_motion': require('../blocks/scratch3_motion'), 'scratch3_operators': require('../blocks/scratch3_operators'), 'scratch3_sensing': require('../blocks/scratch3_sensing'), - 'scratch3_data': require('../blocks/scratch3_data') + 'scratch3_data': require('../blocks/scratch3_data'), + 'scratch3_procedures': require('../blocks/scratch3_procedures') }; /** diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 690ab61a7..85575c804 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -124,6 +124,16 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) { } }; +/** + * Step a procedure. + * @param {!Thread} thread Thread object to step to procedure. + * @param {!string} procedureName Name of procedure defined in this target. + */ +Sequencer.prototype.stepToProcedure = function (thread, procedureName) { + var definition = thread.target.blocks.getProcedureDefinition(procedureName); + thread.pushStack(definition); +}; + /** * Step a thread into an input reporter, and manage its status appropriately. * @param {!Thread} thread Thread object to step to reporter. diff --git a/src/import/sb2import.js b/src/import/sb2import.js index 79e135a7b..f633de9c0 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.js @@ -327,6 +327,14 @@ function parseBlock (sb2block) { }; } } + // Special cases to generate mutations. + if (oldOpcode == 'call') { + activeBlock.mutation = { + tagName: 'mutation', + children: [], + name: sb2block[1] + }; + } return activeBlock; } diff --git a/src/import/sb2specmap.js b/src/import/sb2specmap.js index 693d82f81..76e9683e1 100644 --- a/src/import/sb2specmap.js +++ b/src/import/sb2specmap.js @@ -1374,15 +1374,20 @@ var specMap = { ] }, 'procDef':{ - 'opcode':'proc_def', - 'argMap':[] + 'opcode':'procedures_defnoreturn', + 'argMap':[ + { + 'type':'field', + 'fieldName':'NAME' + } + ] }, 'getParam':{ 'opcode':'proc_param', 'argMap':[] }, 'call':{ - 'opcode':'proc_call', + 'opcode':'procedures_callnoreturn', 'argMap':[] } };