This commit is contained in:
Andrew Sliwinski 2016-04-18 17:20:30 -04:00
parent 655556273a
commit f9f47ed103
20 changed files with 2113 additions and 13 deletions

17
.eslintrc Normal file
View file

@ -0,0 +1,17 @@
{
"rules": {
"curly": [2, "multi-line"],
"eol-last": [2],
"indent": [2, 4],
"quotes": [2, "single"],
"linebreak-style": [2, "unix"],
"max-len": [2, 80, 4],
"semi": [2, "always"],
"strict": [2, "never"]
},
"env": {
"node": true,
"browser": true
},
"extends": "eslint:recommended"
}

5
.npmrc
View file

@ -1,5 +0,0 @@
engine-strict=true
save-exact=true
save-prefix=~
init-license=BSD-3-Clause
init-author-name=Massachusetts Institute of Technology

View file

@ -1,12 +1,21 @@
ESLINT=./node_modules/.bin/eslint
NODE=node
TAP=./node_modules/.bin/tap
WEBPACK=./node_modules/.bin/webpack --progress --colors
# ------------------------------------------------------------------------------
build:
$(WEBPACK)
watch:
$(WEBPACK) --watch
# ------------------------------------------------------------------------------
lint:
$(ESLINT) ./*.js
$(ESLINT) ./lib/*.js
$(ESLINT) ./src/*.js
$(ESLINT) ./src/**/*.js
$(ESLINT) ./test/**/*.js
test:
@ -21,4 +30,4 @@ benchmark:
# ------------------------------------------------------------------------------
.PHONY: lint test coverage benchmark
.PHONY: build lint test coverage benchmark

View file

@ -5,9 +5,29 @@
npm install scratch-vm
```
## Integration
## Setup
```js
var VirtualMachine = require('scratch-vm');
var vm = new VirtualMachine();
// Block events
// UI events
// Listen for events
```
## Standalone Build
```bash
make build
```
```html
<script src="/path/to/vm.js"></script>
<script>
var vm = new window.VirtualMachine();
// do things
</script>
```
## Testing

View file

View file

@ -1,7 +1,7 @@
{
"name": "scratch-vm",
"version": "1.0.0",
"description": "",
"description": "Virtual Machine for Scratch 3.0",
"author": "Massachusetts Institute of Technology",
"license": "BSD-3-Clause",
"homepage": "https://github.com/LLK/scratch-vm#readme",
@ -9,13 +9,15 @@
"type": "git",
"url": "git+ssh://git@github.com/LLK/scratch-vm.git"
},
"main": "index.js",
"main": "./src/index.js",
"scripts": {
"test": "make test"
},
"dependencies": {},
"devDependencies": {
"benchmark": "2.1.0",
"eslint": "2.7.0",
"tap": "5.7.1"
"tap": "5.7.1",
"webpack": "1.13.0"
}
}

41
src/engine/primatives.js Normal file
View file

@ -0,0 +1,41 @@
function Primitives () {
}
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;

119
src/engine/runtime.js Normal file
View file

@ -0,0 +1,119 @@
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 () {
this.sequencer = new Sequencer(this);
this.primitives = new Primitives();
// State
this.blocks = {};
this.stacks = [];
}
Runtime.prototype.createBlock = function (e) {
// Create new block
this.blocks[e.id] = {
id: e.id,
opcode: e.opcode,
next: null,
inputs: {}
};
// 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);
};
Runtime.prototype.moveBlock = function (e) {
var _this = this;
// Block has a new parent
if (e.oldParent === undefined && e.newParent !== undefined) {
// Remove stack
_this._deleteStack(e.id);
// Update new parent
if (e.newInput === undefined) {
_this.blocks[e.newParent].next = e.id;
} else {
_this.blocks[e.newParent].inputs[e.newInput] = e.id;
}
}
// Block was removed from parent
if (e.newParentId === undefined && e.oldParent !== undefined) {
// Add stack
_this.stacks.push(e.id);
// Update old parent
if (e.oldInput === undefined) {
_this.blocks[e.oldParent].next = null;
} else {
delete _this.blocks[e.oldParent].inputs[e.oldInput];
}
}
};
Runtime.prototype.changeBlock = function (e) {
// @todo
};
Runtime.prototype.deleteBlock = function (e) {
// @todo Stop threads running on this stack
// Delete children
var block = this.blocks[e.id];
if (block.next !== null) {
this.deleteBlock({id: block.next});
}
// Delete inputs
for (var i in block.inputs) {
this.deleteBlock({id: block.inputs[i]});
}
// Delete stack
this._deleteStack(e.id);
// Delete block
delete this.blocks[e.id];
};
Runtime.prototype.runAllStacks = function () {
// @todo
};
Runtime.prototype.runStack = function () {
// @todo
};
Runtime.prototype.stopAllStacks = function () {
// @todo
};
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
Runtime.prototype._deleteStack = function (id) {
var i = this.stacks.indexOf(id);
if (i > -1) this.stacks.splice(i, 1);
};
Runtime.prototype._getNextBlock = function (id) {
if (typeof this.blocks[id] === 'undefined') return null;
return this.blocks[id].next;
};
Runtime.prototype._getSubstack = function (id) {
if (typeof this.blocks[id] === 'undefined') return null;
return this.blocks[id].inputs['SUBSTACK'];
};
module.exports = Runtime;

29
src/engine/sequencer.js Normal file
View file

@ -0,0 +1,29 @@
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;
}
Sequencer.prototype.stepAllThreads = function () {
};
Sequencer.prototype.stepThread = function (thread) {
};
Sequencer.prototype.startSubstack = function (thread) {
};
module.exports = Sequencer;

19
src/engine/thread.js Normal file
View file

@ -0,0 +1,19 @@
/**
* 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;
}
module.exports = Thread;

148
src/index.js Normal file
View file

@ -0,0 +1,148 @@
var EventEmitter = require('events');
var util = require('util');
var Runtime = require('./engine/runtime');
/**
* Handles connections between blocks, stage, and extensions.
*
* @author Andrew Sliwinski <ascii@media.mit.edu>
*/
function VirtualMachine () {
var instance = this;
// Bind event emitter and runtime to VM instance
EventEmitter.call(instance);
instance.runtime = new Runtime();
/**
* Event listener for blockly. Handles validation and serves as a generic
* adapter between the blocks and the runtime interface.
*
* @param {Object} Blockly "block" event
*/
instance.blockListener = function (e) {
// Validate event
if (typeof e !== 'object') return;
if (typeof e.blockId !== 'string') return;
// Blocks
switch (e.type) {
case 'create':
instance.runtime.createBlock({
id: e.blockId,
opcode: e.xml.attributes.type.value
});
break;
case 'move':
instance.runtime.moveBlock({
id: e.blockId,
oldParent: e.oldParentId,
oldInput: e.oldInputName,
newParent: e.newParentId,
newInput: e.newInputName
});
break;
case 'change':
instance.runtime.changeBlock({
id: e.blockId
});
break;
case 'delete':
instance.runtime.deleteBlock({
id: e.blockId
});
break;
}
};
// @todo UI listener
// @todo Forward runtime events
// Event dispatcher
// this.types = keymirror({
// // Messages to runtime
// CREATE_BLOCK: null,
// MOVE_BLOCK: null,
// CHANGE_BLOCK: null,
// DELETE_BLOCK: null,
//
// ADD_DEVICE: null,
// REMOVE_DEVICE: null,
//
// RUN_STRIP: null,
// RUN_ALL_STRIPS: null,
// STOP_ALL_STRIPS: null,
// RUN_PALETTE_BLOCK: null,
//
// // Messages from runtime - subscribe to these
// FEEDBACK_EXECUTING_BLOCK: null,
// FEEDBACK_STOPPED_EXECUTING_BLOCK: null,
// DEVICE_RUN_OP: null,
// DEVICE_STOP_OP: null,
//
// // Tell back the interpreter device has finished an op
// DEVICE_FINISHED_OP: null
// });
// Bind block event stream
// setTimeout(function () {
// _this.emit('foo', 'bar');
// }, 1000);
}
/**
* Inherit from EventEmitter
*/
util.inherits(VirtualMachine, EventEmitter);
// VirtualMachine.prototype.changeListener = function (e) {
// var _this = this;
// console.dir(this);
//
// switch (e.type) {
// case 'create':
// console.dir(e);
// _this.runtime.createBlock(
// e.blockId,
// event.xml.attributes.type.value
// );
// break;
// case 'change':
// // @todo
// break;
// case 'move':
// // @todo
// break;
// case 'delete':
// // @todo
// break;
// }
// };
//
// VirtualMachine.prototype.tapListener = function (e) {
// // @todo
// };
VirtualMachine.prototype.start = function () {
this.runtime.runAllGreenFlags();
};
VirtualMachine.prototype.stop = function () {
this.runtime.stop();
};
VirtualMachine.prototype.save = function () {
// @todo Serialize runtime state
};
VirtualMachine.prototype.load = function () {
// @todo Deserialize and apply runtime state
};
/**
* Export and bind to `window`
*/
module.exports = VirtualMachine;
if (typeof window !== 'undefined') window.VirtualMachine = module.exports;

21
src/util/timer.js Normal file
View file

@ -0,0 +1,21 @@
/**
* Constructor
* @todo Swap out Date.now() with microtime module that works in node & browsers
*/
function Timer () {
this.startTime = 0;
}
Timer.prototype.time = function () {
return Date.now();
};
Timer.prototype.start = function () {
this.startTime = this.time();
};
Timer.prototype.stop = function () {
return this.startTime - this.time();
};
module.exports = Timer;

26
test/fixtures/blocks.js vendored Normal file
View file

@ -0,0 +1,26 @@
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;

81
test/unit/runtime.js Normal file
View file

@ -0,0 +1,81 @@
var test = require('tap').test;
var Runtime = require('../../src/engine/runtime');
test('spec', function (t) {
var r = new Runtime();
t.type(Runtime, 'function');
t.type(r, 'object');
t.ok(r instanceof Runtime);
t.type(r.blocks, 'object');
t.type(r.stacks, 'object');
t.ok(Array.isArray(r.stacks));
t.type(r.createBlock, 'function');
t.type(r.moveBlock, 'function');
t.type(r.changeBlock, 'function');
t.type(r.deleteBlock, 'function');
t.end();
});
test('create', function (t) {
var r = new Runtime();
r.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK'
});
t.type(r.blocks['foo'], 'object');
t.equal(r.blocks['foo'].opcode, 'TEST_BLOCK');
t.notEqual(r.stacks.indexOf('foo'), -1);
t.end();
});
test('move', function (t) {
var r = new Runtime();
r.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK'
});
r.createBlock({
id: 'bar',
opcode: 'TEST_BLOCK'
});
// Attach 'bar' to the end of 'foo'
r.moveBlock({
id: 'bar',
newParent: 'foo'
});
t.equal(r.stacks.length, 1);
t.equal(Object.keys(r.blocks).length, 2);
t.equal(r.blocks['foo'].next, 'bar');
// Detach 'bar' from 'foo'
r.moveBlock({
id: 'bar',
oldParent: 'foo'
});
t.equal(r.stacks.length, 2);
t.equal(Object.keys(r.blocks).length, 2);
t.equal(r.blocks['foo'].next, null);
t.end();
});
test('delete', function (t) {
var r = new Runtime();
r.createBlock({
id: 'foo',
opcode: 'TEST_BLOCK'
});
r.deleteBlock({
id: 'foo'
});
t.type(r.blocks['foo'], 'undefined');
t.equal(r.stacks.indexOf('foo'), -1);
t.end();
});

View file

@ -1,6 +1,18 @@
var test = require('tap').test;
var vm = require('../../index');
var VirtualMachine = require('../../src/index');
test('spec', function (t) {
var vm = new VirtualMachine('foo');
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();
});

10
test/unit/thread.js Normal file
View file

@ -0,0 +1,10 @@
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');
t.end();
});

41
test/unit/timer.js Normal file
View file

@ -0,0 +1,41 @@
var test = require('tap').test;
var Timer = require('../../src/util/timer');
test('spec', function (t) {
var timer = new Timer();
t.type(Timer, 'function');
t.type(timer, 'object');
t.type(timer.startTime, 'number');
t.type(timer.time, 'function');
t.type(timer.start, 'function');
t.type(timer.stop, 'function');
t.end();
});
test('time', function (t) {
var timer = new Timer();
var time = timer.time();
t.ok(Date.now() >= time);
t.end();
});
test('start / stop', function (t) {
var timer = new Timer();
var start = timer.time();
var delay = 100;
var threshold = 1000 / 60; // 60 hz
// Start timer
timer.start();
// Wait and stop timer
setTimeout(function () {
var stop = timer.stop();
t.ok(stop >= -(delay + threshold) && stop <= -(delay - threshold));
t.end();
}, delay);
});

1491
vm.js Normal file

File diff suppressed because it is too large Load diff

1
vm.min.js vendored Normal file

File diff suppressed because one or more lines are too long

18
webpack.config.js Normal file
View file

@ -0,0 +1,18 @@
var webpack = require('webpack');
module.exports = {
entry: {
'vm': './src/index.js',
'vm.min': './src/index.js'
},
output: {
path: __dirname,
filename: '[name].js'
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
include: /\.min\.js$/,
minimize: true
})
]
};