Mutations in block representation; an unfeatured procedure call (#212)

* Add scratch3_procedures and no-op for defnoreturn

* Add mutation adapter to parse mutations in CREATE/CHANGE events

* Add mutation-to-XML

* Update spec map for Blockly procedure names

* Placeholder for procedure special cases

* Basic stepping to procedures

* Remove extra case

* Validation for changeBlock
This commit is contained in:
Tim Mickel 2016-10-03 17:43:24 -04:00 committed by GitHub
parent dd624aea06
commit 0a66c62f6a
9 changed files with 171 additions and 8 deletions

View file

@ -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.<string, Function>} 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;

View file

@ -1,3 +1,4 @@
var mutationAdapter = require('./mutation-adapter');
var html = require('htmlparser2'); var html = require('htmlparser2');
/** /**
@ -138,6 +139,9 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
// Link next block to this block. // Link next block to this block.
block.next = childBlockNode.attribs.id; block.next = childBlockNode.attribs.id;
break; break;
case 'mutation':
block.mutation = mutationAdapter(xmlChild);
break;
} }
} }
} }

View file

@ -1,4 +1,5 @@
var adapter = require('./adapter'); var adapter = require('./adapter');
var mutationAdapter = require('./mutation-adapter');
var xmlEscape = require('../util/xml-escape'); var xmlEscape = require('../util/xml-escape');
/** /**
@ -116,6 +117,16 @@ Blocks.prototype.getInputs = function (id) {
return inputs; 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. * Get the top-level script for a given block.
* @param {?string} id ID of block to query. * @param {?string} id ID of block to query.
@ -130,6 +141,23 @@ Blocks.prototype.getTopLevelScript = function (id) {
return block.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) { Blocks.prototype.changeBlock = function (args) {
// Validate // 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] === 'undefined') return;
if (typeof this._blocks[args.id].fields[args.name] === 'undefined') return;
if (args.element == 'field') {
// Update block value // Update block value
if (!this._blocks[args.id].fields[args.name]) return;
this._blocks[args.id].fields[args.name].value = args.value; 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 + '"' + ' type="' + block.opcode + '"' +
xy + xy +
'>'; '>';
// Add any mutation. Must come before inputs.
if (block.mutation) {
xmlString += this.mutationToXML(block.mutation);
}
// Add any inputs on this block. // Add any inputs on this block.
for (var input in block.inputs) { for (var input in block.inputs) {
var blockInput = block.inputs[input]; var blockInput = block.inputs[input];
@ -389,6 +425,25 @@ Blocks.prototype.blockToXML = function (blockId) {
return xmlString; 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 += '</' + mutation.tagName + '>';
return mutationString;
};
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
/** /**

View file

@ -132,6 +132,12 @@ var execute = function (sequencer, thread) {
argValues[inputName] = currentStackFrame.reported[inputName]; 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, // If we've gotten this far, all of the input blocks are evaluated,
// and `argValues` is fully populated. So, execute the block primitive. // and `argValues` is fully populated. So, execute the block primitive.
// First, clear `currentStackFrame.reported`, so any subsequent execution // First, clear `currentStackFrame.reported`, so any subsequent execution
@ -155,6 +161,9 @@ var execute = function (sequencer, thread) {
startBranch: function (branchNum) { startBranch: function (branchNum) {
sequencer.stepToBranch(thread, branchNum); sequencer.stepToBranch(thread, branchNum);
}, },
startProcedure: function (procedureName) {
sequencer.stepToProcedure(thread, procedureName);
},
startHats: function(requestedHat, opt_matchFields, opt_target) { startHats: function(requestedHat, opt_matchFields, opt_target) {
return ( return (
runtime.startHats(requestedHat, opt_matchFields, opt_target) runtime.startHats(requestedHat, opt_matchFields, opt_target)

View file

@ -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;
}

View file

@ -15,7 +15,8 @@ var defaultBlockPackages = {
'scratch3_motion': require('../blocks/scratch3_motion'), 'scratch3_motion': require('../blocks/scratch3_motion'),
'scratch3_operators': require('../blocks/scratch3_operators'), 'scratch3_operators': require('../blocks/scratch3_operators'),
'scratch3_sensing': require('../blocks/scratch3_sensing'), 'scratch3_sensing': require('../blocks/scratch3_sensing'),
'scratch3_data': require('../blocks/scratch3_data') 'scratch3_data': require('../blocks/scratch3_data'),
'scratch3_procedures': require('../blocks/scratch3_procedures')
}; };
/** /**

View file

@ -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. * Step a thread into an input reporter, and manage its status appropriately.
* @param {!Thread} thread Thread object to step to reporter. * @param {!Thread} thread Thread object to step to reporter.

View file

@ -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; return activeBlock;
} }

View file

@ -1374,15 +1374,20 @@ var specMap = {
] ]
}, },
'procDef':{ 'procDef':{
'opcode':'proc_def', 'opcode':'procedures_defnoreturn',
'argMap':[] 'argMap':[
{
'type':'field',
'fieldName':'NAME'
}
]
}, },
'getParam':{ 'getParam':{
'opcode':'proc_param', 'opcode':'proc_param',
'argMap':[] 'argMap':[]
}, },
'call':{ 'call':{
'opcode':'proc_call', 'opcode':'procedures_callnoreturn',
'argMap':[] 'argMap':[]
} }
}; };