AST create / change / move / delete from blockly

This commit is contained in:
Andrew Sliwinski 2016-04-26 09:49:52 -04:00
parent 53779220b7
commit 981b85e78f
22 changed files with 11858 additions and 381 deletions

View file

@ -32,6 +32,44 @@ make build
</script>
```
## Abstract Syntax Tree
#### Overview
#### Anatomy of a Block
```json
{
"id": "^1r~63Gdl7;Dh?I*OP3_",
"opcode": "wedo_motorclockwise",
"next": null,
"fields": {
"DURATION": {
"name": "DURATION",
"value": null,
"blocks": {
"1?P=eV(OiDY3vMk!24Ip": {
"id": "1?P=eV(OiDY3vMk!24Ip",
"opcode": "math_number",
"next": null,
"fields": {
"NUM": {
"name": "NUM",
"value": "10",
"blocks": null
}
}
}
}
},
"SUBSTACK": {
"name": "SUBSTACK",
"value": "@1ln(HsUO4!]*2*%BrE|",
"blocks": null
}
}
}
```
## Testing
```bash
make test

View file

@ -13,10 +13,14 @@
"scripts": {
"test": "make test"
},
"dependencies": {},
"dependencies": {
"htmlparser2": "3.9.0",
"memoizee": "0.3.10"
},
"devDependencies": {
"benchmark": "2.1.0",
"eslint": "2.7.0",
"json-loader": "0.5.4",
"tap": "5.7.1",
"webpack": "1.13.0"
}

87
src/engine/adapter.js Normal file
View file

@ -0,0 +1,87 @@
var html = require('htmlparser2');
var memoize = require('memoizee');
var parseDOM = memoize(html.parseDOM, {
length: 1,
resolvers: [String],
max: 200
});
/**
* Adapter between block creation events and block representation which can be
* used by the Scratch runtime.
*
* @param {Object} `Blockly.events.create`
*
* @return {Object}
*/
module.exports = function (e) {
// Validate input
if (typeof e !== 'object') return;
if (typeof e.blockId !== 'string') return;
if (typeof e.xml !== 'object') return;
// Storage object
var obj = {
id: e.blockId,
opcode: null,
next: null,
fields: {}
};
// Set opcode
if (typeof e.xml.attributes === 'object') {
obj.opcode = e.xml.attributes.type.value;
}
// Extract fields from event's `innerHTML`
if (typeof e.xml.innerHTML !== 'string') return obj;
if (e.xml.innerHTML === '') return obj;
obj.fields = extract(parseDOM(e.xml.innerHTML));
return obj;
}
/**
* Extracts fields from a block's innerHTML.
* @todo Extend this to support vertical grammar / nested blocks.
*
* @param {Object} DOM representation of block's innerHTML
*
* @return {Object}
*/
function extract (dom) {
// Storage object
var fields = {};
// Field
var field = dom[0];
var fieldName = field.attribs.name;
fields[fieldName] = {
name: fieldName,
value: null,
blocks: {}
};
// Shadow block
var shadow = field.children[0];
var shadowId = shadow.attribs.id;
var shadowOpcode = shadow.attribs.type;
fields[fieldName].blocks[shadowId] = {
id: shadowId,
opcode: shadowOpcode,
next: null,
fields: {}
};
// Primitive
var primitive = shadow.children[0];
var primitiveName = primitive.attribs.name;
var primitiveValue = primitive.children[0].data;
fields[fieldName].blocks[shadowId].fields[primitiveName] = {
name: primitiveName,
value: primitiveValue,
blocks: null
};
return fields;
}

View file

@ -1,41 +1,5 @@
function Primitives () {
// @todo
}
Primitives.prototype.event_whenflagclicked = function (thread, runtime) {
// No-op: flags are started by the interpreter but don't do any action
// Take 1/3 second to show running state
if (Date.now() - thread.blockFirstTime < 300) {
thread.yield = true;
return;
}
};
Primitives.prototype.control_repeat = function (thread, runtime) {
// Take 1/3 second to show running state
if (Date.now() - thread.blockFirstTime < 300) {
thread.yield = true;
return;
}
if (thread.repeatCounter == -1) {
thread.repeatCounter = 10; // @todo from the arg
}
if (thread.repeatCounter > 0) {
thread.repeatCounter -= 1;
runtime.interpreter.startSubstack(thread);
} else {
thread.repeatCounter = -1;
thread.nextBlock = runtime.getNextBlock(thread.blockPointer);
}
};
Primitives.prototype.control_forever = function (thread, runtime) {
// Take 1/3 second to show running state
if (Date.now() - thread.blockFirstTime < 300) {
thread.yield = true;
return;
}
runtime.interpreter.startSubstack(thread);
};
module.exports = Primitives;

View file

@ -1,26 +1,19 @@
var EventEmitter = require('events');
var util = require('util');
var Primitives = require('./primatives');
var Sequencer = require('./sequencer');
var Thread = require('./thread');
var STEP_THREADS_INTERVAL = 1000 / 30;
/**
* A simple runtime for blocks.
*/
function Runtime () {
// Bind event emitter
EventEmitter.call(instance);
// Instantiate sequencer and primitives
this.sequencer = new Sequencer(this);
this.primitives = new Primitives();
EventEmitter.call(this);
// State
this.blocks = {};
this.stacks = [];
window._BLOCKS = this.blocks;
window._STACKS = this.stacks;
}
/**
@ -28,19 +21,34 @@ function Runtime () {
*/
util.inherits(Runtime, EventEmitter);
Runtime.prototype.createBlock = function (e) {
Runtime.prototype.createBlock = function (block) {
// Create new block
this.blocks[e.id] = {
id: e.id,
opcode: e.opcode,
next: null,
inputs: {}
};
this.blocks[block.id] = block;
// Walk each field and add any shadow blocks
// @todo Expand this to cover vertical / nested blocks
for (var i in block.fields) {
var shadows = block.fields[i].blocks;
for (var y in shadows) {
var shadow = shadows[y];
this.blocks[shadow.id] = shadow;
};
}
// Push block id to stacks array. New blocks are always a stack even if only
// momentary. If the new block is added to an existing stack this stack will
// be removed by the `moveBlock` method below.
this.stacks.push(e.id);
this.stacks.push(block.id);
};
Runtime.prototype.changeBlock = function (args) {
// Validate
if (args.element !== 'field') 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;
};
Runtime.prototype.moveBlock = function (e) {
@ -52,10 +60,14 @@ Runtime.prototype.moveBlock = function (e) {
_this._deleteStack(e.id);
// Update new parent
if (e.newInput === undefined) {
if (e.newField === undefined) {
_this.blocks[e.newParent].next = e.id;
} else {
_this.blocks[e.newParent].inputs[e.newInput] = e.id;
_this.blocks[e.newParent].fields[e.newField] = {
name: e.newField,
value: e.id,
blocks: {}
};
}
}
@ -65,30 +77,34 @@ Runtime.prototype.moveBlock = function (e) {
_this.stacks.push(e.id);
// Update old parent
if (e.oldInput === undefined) {
if (e.oldField === undefined) {
_this.blocks[e.oldParent].next = null;
} else {
delete _this.blocks[e.oldParent].inputs[e.oldInput];
delete _this.blocks[e.oldParent].fields[e.oldField];
}
}
};
Runtime.prototype.changeBlock = function (e) {
// @todo
};
Runtime.prototype.deleteBlock = function (e) {
// @todo Stop threads running on this stack
// Delete children
// Get block
var block = this.blocks[e.id];
// Delete children
if (block.next !== null) {
this.deleteBlock({id: block.next});
}
// Delete inputs
for (var i in block.inputs) {
this.deleteBlock({id: block.inputs[i]});
// Delete substacks and fields
for (var field in block.fields) {
if (field === 'SUBSTACK') {
this.deleteBlock({id: block.fields[field].value});
} else {
for (var shadow in block.fields[field].blocks) {
this.deleteBlock({id: shadow});
}
}
}
// Delete stack
@ -98,19 +114,6 @@ Runtime.prototype.deleteBlock = function (e) {
delete this.blocks[e.id];
};
Runtime.prototype.runAllStacks = function () {
// @todo
};
Runtime.prototype.runStack = function (e) {
// @todo
console.dir(e);
};
Runtime.prototype.stopAllStacks = function () {
// @todo
};
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
@ -126,7 +129,7 @@ Runtime.prototype._getNextBlock = function (id) {
Runtime.prototype._getSubstack = function (id) {
if (typeof this.blocks[id] === 'undefined') return null;
return this.blocks[id].inputs['SUBSTACK'];
return this.blocks[id].fields['SUBSTACK'];
};
module.exports = Runtime;

View file

@ -1,29 +1,5 @@
var Timer = require('../util/timer');
/**
* Constructor
*/
function Sequencer (runtime) {
// Bi-directional binding for runtime
this.runtime = runtime;
// State
this.runningThreads = [];
this.workTime = 30;
this.timer = new Timer();
this.currentTime = 0;
function Sequencer () {
// @todo
}
Sequencer.prototype.stepAllThreads = function () {
};
Sequencer.prototype.stepThread = function (thread) {
};
Sequencer.prototype.startSubstack = function (thread) {
};
module.exports = Sequencer;

View file

@ -1,19 +1,5 @@
/**
* Thread is an internal data structure used by the interpreter. It holds the
* state of a thread so it can continue from where it left off, and it has
* a stack to support nested control structures and procedure calls.
*
* @param {String} Unique block identifier
*/
function Thread (id) {
this.topBlockId = id;
this.blockPointer = id;
this.blockFirstTime = -1;
this.nextBlock = null;
this.waiting = null;
this.runningDeviceBlock = false;
this.stack = [];
this.repeatCounter = -1;
function Thread () {
// @todo
}
module.exports = Thread;

View file

@ -2,6 +2,7 @@ var EventEmitter = require('events');
var util = require('util');
var Runtime = require('./engine/runtime');
var adapter = require('./engine/adapter');
/**
* Handles connections between blocks, stage, and extensions.
@ -30,23 +31,23 @@ function VirtualMachine () {
// Blocks
switch (e.type) {
case 'create':
instance.runtime.createBlock({
instance.runtime.createBlock(adapter(e));
break;
case 'change':
instance.runtime.changeBlock({
id: e.blockId,
opcode: e.xml.attributes.type.value
element: e.element,
name: e.name,
value: e.newValue
});
break;
case 'move':
instance.runtime.moveBlock({
id: e.blockId,
oldParent: e.oldParentId,
oldInput: e.oldInputName,
oldField: e.oldInputName,
newParent: e.newParentId,
newInput: e.newInputName
});
break;
case 'change':
instance.runtime.changeBlock({
id: e.blockId
newField: e.newInputName
});
break;
case 'delete':
@ -55,19 +56,7 @@ function VirtualMachine () {
});
break;
}
// UI
if (typeof e.element === 'undefined') return;
switch (e.element) {
case 'click':
instance.runtime.runStack({
id: e.blockId
});
break;
}
};
// @todo Forward runtime events
}
/**
@ -75,22 +64,6 @@ function VirtualMachine () {
*/
util.inherits(VirtualMachine, EventEmitter);
VirtualMachine.prototype.start = function () {
// @todo Run all green flags
};
VirtualMachine.prototype.stop = function () {
// @todo Stop all threads
};
VirtualMachine.prototype.save = function () {
// @todo Serialize runtime state
};
VirtualMachine.prototype.load = function () {
// @todo Deserialize and apply runtime state
};
/**
* Export and bind to `window`
*/

View file

@ -1,6 +1,5 @@
/**
* Constructor
* @todo Swap out Date.now() with microtime module that works in node & browsers
*/
function Timer () {
this.startTime = 0;

0
test/benchmark/ast.js Normal file
View file

View file

@ -1,26 +0,0 @@
var events = require('events');
var util = require('util');
/**
* Simulates event emitter / listener patterns from Scratch Blocks.
*
* @author Andrew Sliwinski <ascii@media.mit.edu>
*/
function Blocks () {
}
/**
* Inherit from EventEmitter to enable messaging.
*/
util.inherits(VirtualMachine, events.EventEmitter);
Blocks.prototype.spaghetti = function () {
this.emit('');
};
Blocks.prototype.spam = function () {
this.emit('');
};
module.exports = Blocks;

20
test/fixtures/events.json vendored Normal file
View file

@ -0,0 +1,20 @@
{
"create": {
"blockId": "z!+#Nqr,_(V=xz0y7a@d",
"workspaceId": "7Luws3lyb*Z98~Kk+IG|",
"group": ";OswyM#@%`%,xOrhOXC=",
"recordUndo": true,
"xml": {
"attributes": {
"type": {
"value": "wedo_motorclockwise"
}
},
"innerHTML": "<value name=\"DURATION\"><shadow type=\"math_number\" id=\"!6Ahqg4f}Ljl}X5Hws?Z\"><field name=\"NUM\">10</field></shadow></value>"
},
"ids": [
"z!+#Nqr,_(V=xz0y7a@d",
"!6Ahqg4f}Ljl}X5Hws?Z"
]
}
}

22
test/integration/index.js Normal file
View file

@ -0,0 +1,22 @@
var test = require('tap').test;
var VirtualMachine = require('../../src/index');
test('spec', function (t) {
t.end();
});
test('create', function (t) {
t.end();
});
test('move', function (t) {
t.end();
});
test('change', function (t) {
t.end();
});
test('delete', function (t) {
t.end();
});

20
test/unit/adapter.js Normal file
View file

@ -0,0 +1,20 @@
var test = require('tap').test;
var adapter = require('../../src/engine/adapter');
var events = require('../fixtures/events.json');
test('spec', function (t) {
t.type(adapter, 'function');
t.end();
});
test('create event', function (t) {
var result = adapter(events.create);
t.type(result, 'object');
t.type(result.id, 'string');
t.type(result.opcode, 'string');
t.type(result.fields, 'object');
t.type(result.fields['DURATION'], 'object');
t.end();
});

8
test/unit/primatives.js Normal file
View file

@ -0,0 +1,8 @@
var test = require('tap').test;
var Primatives = require('../../src/engine/primatives');
test('spec', function (t) {
t.type(Primatives, 'function');
// @todo
t.end();
});

View file

@ -24,7 +24,9 @@ test('create', function (t) {
var r = new Runtime();
r.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK'
opcode: 'TEST_BLOCK',
next: null,
fields: {}
});
t.type(r.blocks['foo'], 'object');
@ -37,11 +39,15 @@ test('move', function (t) {
var r = new Runtime();
r.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK'
opcode: 'TEST_BLOCK',
next: null,
fields: {}
});
r.createBlock({
id: 'bar',
opcode: 'TEST_BLOCK'
opcode: 'TEST_BLOCK',
next: null,
fields: {}
});
// Attach 'bar' to the end of 'foo'
@ -69,7 +75,9 @@ test('delete', function (t) {
var r = new Runtime();
r.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK'
opcode: 'TEST_BLOCK',
next: null,
fields: {}
});
r.deleteBlock({
id: 'foo'

8
test/unit/sequencer.js Normal file
View file

@ -0,0 +1,8 @@
var test = require('tap').test;
var Sequencer = require('../../src/engine/sequencer');
test('spec', function (t) {
t.type(Sequencer, 'function');
// @todo
t.end();
});

View file

@ -2,17 +2,10 @@ var test = require('tap').test;
var VirtualMachine = require('../../src/index');
test('spec', function (t) {
var vm = new VirtualMachine('foo');
var vm = new VirtualMachine();
t.type(VirtualMachine, 'function');
t.type(vm, 'object');
t.type(vm.blockListener, 'function');
// t.type(vm.uiListener, 'function');
// t.type(vm.start, 'function');
// t.type(vm.stop, 'function');
// t.type(vm.save, 'function');
// t.type(vm.load, 'function');
t.end();
});

View file

@ -2,9 +2,7 @@ var test = require('tap').test;
var Thread = require('../../src/engine/thread');
test('spec', function (t) {
var thread = new Thread('foo');
t.type(Thread, 'function');
t.type(thread, 'object');
// @todo
t.end();
});

11725
vm.js

File diff suppressed because it is too large Load diff

12
vm.min.js vendored

File diff suppressed because one or more lines are too long

View file

@ -9,10 +9,21 @@ module.exports = {
path: __dirname,
filename: '[name].js'
},
module: {
loaders: [
{
test: /\.json$/,
loader: 'json-loader'
}
]
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
include: /\.min\.js$/,
minimize: true
minimize: true,
compress: {
warnings: false
}
})
]
};