diff --git a/packages/vm/src/compiler/compile.js b/packages/vm/src/compiler/compile.js new file mode 100644 index 000000000..dcdb06d80 --- /dev/null +++ b/packages/vm/src/compiler/compile.js @@ -0,0 +1,31 @@ +const IRGenerator = require('./ir-generator'); +const JSGenerator = require('./js-generator'); +const Cast = require('../util/cast'); + +/** @import Thread from '../engine/thread.js' */ + +/** + * Compile an existing thread to executable object. + * @param {Thread} thread thread to compile + * @return {?Generator} compiled result, null if failed + */ +const compile = function (thread) { + try { + const irGenerator = new IRGenerator(thread); + const ir = irGenerator.generateScript(thread.topBlock); + console.log(ir); + + const jsGenerator = new JSGenerator(); + const fn = jsGenerator.compileForThread(ir).call(globalThis); + + console.log(fn.toString()); + + thread.compileResult = fn.call({Cast}, thread)(); + } catch (e) { + console.error(e); + thread.compileResult = null; + } + return thread.compileResult; +}; + +module.exports = compile; diff --git a/packages/vm/src/compiler/constants.js b/packages/vm/src/compiler/constants.js new file mode 100644 index 000000000..8477522b1 --- /dev/null +++ b/packages/vm/src/compiler/constants.js @@ -0,0 +1,33 @@ +const TYPE_NUMBER = 1; +const TYPE_STRING = 2; +const TYPE_STATEMENT = 8; +const TYPE_UNKNOWN = 99; + +const IR_CONSTANT = 'constant'; +const IR_IDENTIFIER = 'identifier'; +const IR_WHILE = 'control.while'; +const IR_REPEAT = 'control.repeat'; +const IR_IFELSE = 'control.ifelse'; +const IR_WAIT = 'control.wait'; +const IR_ADD = 'op.add'; +const IR_SUB = 'op.sub'; +const IR_LOAD = 'var.load'; +const IR_STORE = 'var.store'; + +module.exports = { + TYPE_NUMBER, + TYPE_STRING, + TYPE_STATEMENT, + TYPE_UNKNOWN, + + IR_CONSTANT, + IR_IDENTIFIER, + IR_WHILE, + IR_REPEAT, + IR_IFELSE, + IR_WAIT, + IR_ADD, + IR_SUB, + IR_LOAD, + IR_STORE +}; diff --git a/packages/vm/src/compiler/ir-generator.js b/packages/vm/src/compiler/ir-generator.js new file mode 100644 index 000000000..4bdb96f82 --- /dev/null +++ b/packages/vm/src/compiler/ir-generator.js @@ -0,0 +1,205 @@ +const { IR_IDENTIFIER } = require('./constants'); +const ScratchIRGenerator = require('./ir-scratch'); + +/** @import Blocks from '../engine/blocks' */ +/** @import Target from '../engine/target' */ +/** @import Thread from '../engine/thread' */ + +class IRGenerator { + /** + * @param {Thread} thread + */ + constructor (thread) { + this.thread = thread; + this.blockContainer = /** @type {Blocks} */ (thread.blockContainer); + this.generator = ScratchIRGenerator; + + this.clear(); + } + + /** + * Clear compiler context. + */ + clear () { + this.variables = {}; + } + + /** + * Generate IR for a list of blocks. + * @param {?string} blockId top block id + * @return {IRBaseInst[]} list of ir instructions + */ + generateScript (blockId) { + const result = []; + + while (blockId) { + const block = this.blockContainer.getBlock(blockId); + if (!block) { + break; + } + + result.push(this.generateBlock(blockId)); + blockId = block.next; // next block + } + + return result; + } + + /** + * Generate IR for a block. + * @param {?string} blockId block id + * @return {IRBaseInst} ir instruction + */ + generateBlock (blockId) { + const block = blockId && this.blockContainer.getBlock(blockId); + if (!block) { + return null; + } + + const opcode = block.opcode; + + const generate = this.generator[opcode]; + if (generate) { + return generate(block, this); + } else { + console.warn(`no ir generator for opcode ${opcode}`); + return IRGenerator.defaultGenerator(block, this); + } + } + + static defaultGenerator (block, generator) { + // TODO: compatible mode + return null; + } + + /** + * Generate IR from field. + * @param {Object} block block to generate ir + * @param {string} name field name + * @return {IRBaseInst} + */ + fromField (block, name) { + if (block.fields.hasOwnProperty(name)) { + return block.fields[name].value; + } else { + throw `no field ${name} in block ${block.opcode}`; + } + } + + /** + * Generate IR from field. + * @param {Object} block block to generate ir + * @param {string} name value name + * @return {IRBaseInst} + */ + fromValue (block, name) { + if (block.inputs.hasOwnProperty(name)) { + const blockId = block.inputs[name].block; + return this.generateBlock(blockId); + } else { + throw `no input ${name} in block ${block.opcode}`; + } + } + + /** + * Generate IR from statement. + * @param {Object} block block to generate ir + * @param {string} name statement name + * @return {IRBaseInst[]} + */ + fromStatement (block, name) { + if (block.inputs.hasOwnProperty(name)) { + const blockId = block.inputs[name].block; + return this.generateScript(blockId); + } else { + // input undefined when no block is connected to substack + return []; + } + } + + /** + * Generate IR from variable field. + * @param {Object} block block to generate ir + * @param {string} name field name + * @return {IRBaseInst[]} + */ + fromVariable (block, name) { + if (block.fields.hasOwnProperty(name)) { + const field = block.fields[name]; + return this.lookupVariable(field.name, field.id); + } else { + throw `no variable field ${name} in block ${block.opcode}`; + } + } + + /** + * Generate IR from variable. + * @param {string} name name of data + * @param {string} id id of data + * @return {IRBaseInst} + */ + lookupVariable (name, id) { + const target = /** @type {Target} */ (this.thread.target); + const stage = /** @type {?Target} */ (target.runtime.getTargetForStage()); + + // lookup by id + if (target.variables.hasOwnProperty(id)) { + return { + opcode: IR_IDENTIFIER, + name: name, + id: id, + scope: 'target' + }; + } + + // lookup in stage by id + if (!target.isStage && stage) { + if (stage.variables.hasOwnProperty(id)) { + return { + opcode: IR_IDENTIFIER, + name: name, + id: id, + scope: 'stage' + }; + } + } + + // lookup by name + for (const varId in this.variables) { + const currVar = this.variables[varId]; + if (currVar.name === name && currVar.type === '') { + return { + opcode: IR_IDENTIFIER, + name: name, + id: varId, + scope: 'target' + }; + } + } + + // lookup in stage by name + if (!target.isStage && stage) { + for (const varId in stage.variables) { + const currVar = stage.variables[varId]; + if (currVar.name === name && currVar.type === '') { + return { + opcode: IR_IDENTIFIER, + name: name, + id: varId, + scope: 'stage' + }; + } + } + } + + // create a new variable in current target + return { + opcode: IR_IDENTIFIER, + name: name, + id: varId, + scope: 'target' + }; + } +} + +module.exports = IRGenerator; diff --git a/packages/vm/src/compiler/ir-scratch.js b/packages/vm/src/compiler/ir-scratch.js new file mode 100644 index 000000000..d8af631ad --- /dev/null +++ b/packages/vm/src/compiler/ir-scratch.js @@ -0,0 +1,129 @@ +const constants = require('./constants'); + +/** @import IRGenerator from './ir-generator.js' */ + +// common blocks +const colour_picker = function (block, /** @type {IRGenerator} */ generator) { + return { + opcode: constants.IR_CONSTANT, + value: generator.fromField(block, 'COLOUR') + }; +}; + +const math_number = function (block, /** @type {IRGenerator} */ generator) { + return { + opcode: constants.IR_CONSTANT, + value: generator.fromField(block, 'NUM') + }; +}; + +const math_integer = math_number; + +const math_whole_number = math_number; + +const math_positive_number = math_number; + +const math_angle = math_number; + +const text = function (block, /** @type {IRGenerator} */ generator) { + return { + opcode: constants.IR_CONSTANT, + value: generator.fromField(block, 'TEXT') + }; +}; + +// other test blocks +const control_forever = function (block, /** @type {IRGenerator} */ generator) { + return { + opcode: constants.IR_WHILE, + test: { + opcode: constants.IR_CONSTANT, + value: true + }, + body: generator.fromStatement(block, 'SUBSTACK') + }; +}; + +const control_repeat = function (block, /** @type {IRGenerator} */ generator) { + return { + opcode: constants.IR_REPEAT, + times: generator.fromValue(block, 'TIMES'), + body: generator.fromStatement(block, 'SUBSTACK') + }; +}; + +const control_if = function (block, /** @type {IRGenerator} */ generator) { + return { + opcode: constants.IR_IFELSE, + test: generator.fromValue(block, 'CONDITION'), + consequent: generator.fromStatement(block, 'SUBSTACK'), + alternate: [] + }; +}; + +const control_if_else = function (block, /** @type {IRGenerator} */ generator) { + return { + opcode: constants.IR_IFELSE, + test: generator.fromValue(block, 'CONDITION'), + consequent: generator.fromStatement(block, 'SUBSTACK'), + alternate: generator.fromStatement(block, 'SUBSTACK2') + }; +}; + +const control_wait = function (block, /** @type {IRGenerator} */ generator) { + return { + opcode: constants.IR_WAIT, + duration: generator.fromValue(block, 'DURATION') + }; +}; + +const operator_add = function (block, /** @type {IRGenerator} */ generator) { + return { + opcode: constants.IR_ADD, + left: generator.fromValue(block, 'NUM1'), + right: generator.fromValue(block, 'NUM2') + }; +}; + +const operator_sub = function (block, /** @type {IRGenerator} */ generator) { + return { + opcode: constants.IR_SUB, + left: generator.fromValue(block, 'NUM1'), + right: generator.fromValue(block, 'NUM2') + }; +}; + +const data_variable = function (block, /** @type {IRGenerator} */ generator) { + return { + opcode: constants.IR_LOAD, + variable: generator.fromVariable(block, 'VARIABLE') + }; +}; + +const data_setvariableto = function (block, /** @type {IRGenerator} */ generator) { + return { + opcode: constants.IR_STORE, + variable: generator.fromVariable(block, 'VARIABLE'), + value: generator.fromValue(block, 'VALUE') + }; +}; + +module.exports = { + colour_picker, + math_number, + math_integer, + math_whole_number, + math_positive_number, + math_angle, + text, + + control_forever, + control_repeat, + control_if, + control_if_else, + control_wait, + operator_add, + operator_sub, + data_variable, + data_setvariableto +}; diff --git a/packages/vm/src/compiler/js-generator.js b/packages/vm/src/compiler/js-generator.js new file mode 100644 index 000000000..bce981aea --- /dev/null +++ b/packages/vm/src/compiler/js-generator.js @@ -0,0 +1,161 @@ +const constants = require('./constants'); + +/** + * Get JS string literal. + * @param {string} str string + * @return {string} quoted string + */ +const quote = function (str) { + return JSON.stringify(str); +} + +class TypedExpression { + /** + * @param {string} code + * @param {number} type + */ + constructor (code, type) { + this.code = code; + this.type = type; + } + + asNumber () { + if (this.type === constants.TYPE_NUMBER) { + return this.code; + } else { + //return `+(${this.code}) || 0` + return `Cast.toNumber(${this.code})` + } + } + + asBoolean () { + if (this.type === constants.TYPE_NUMBER) { + return this.code; + } else { + return `Cast.toBoolean(${this.code})` + } + } +} + +class JSGenerator { + /** + * @param {Blocks} blockContainer + */ + constructor () {} + + /** + * Generate JavaScript for single thread. + * @param {IRInst[]} irList list of IR instruction + * @return {Function} generated function + */ + compileForThread (irList) { + return new Function([ + 'return (function (thread) {', + 'const target = thread.target;', + 'const stage = target.runtime.getTargetForStage();', + 'const {Cast} = this;', + 'return (function* () {', + this.generateInstructionList(irList).code + ';', + '});})' + ].join('')); + } + + /** + * Generate JavaScript code for IR instructions. + * @param {IRInst[]} irList list of IR instruction + * @return {TypedExpression} generated code + */ + generateInstructionList (irList) { + const codes = []; + + for (const ir of irList) { + const code = this.generateInstruction(ir); + codes.push(code.code); + } + + return new TypedExpression(codes.join(';'), constants.TYPE_STATEMENT); + } + + /** + * Generate JavaScript code for IR instruction. + * @param {IRInst} ir IR instruction + * @return {TypedExpression} generated code + */ + generateInstruction (ir) { + switch (ir.opcode) { + case constants.IR_CONSTANT: { + return new TypedExpression(ir.value, constants.TYPE_UNKNOWN); + } + case constants.IR_IDENTIFIER: { + if (ir.scope === 'stage') { + return new TypedExpression(`stage.variables[${quote(ir.id)}].value`, constants.TYPE_UNKNOWN); + } else { + return new TypedExpression(`target.variables[${quote(ir.id)}].value`, constants.TYPE_UNKNOWN); + } + } + case constants.IR_WHILE: { + const test = this.generateInstruction(ir.test); + const body = this.generateInstructionList(ir.body); + return new TypedExpression( + `while (${test.asNumber()}) {${body.code}}`, + constants.TYPE_STATEMENT + ); + } + case constants.IR_REPEAT: { + const times = this.generateInstruction(ir.times); + const body = this.generateInstructionList(ir.body); + return new TypedExpression( + `for (let i = Math.round(${times.asNumber()}); i >= 0; --i) {${body.code}}`, + constants.TYPE_STATEMENT + ); + } + case constants.IR_IFELSE: { + const test = this.generateInstruction(ir.test); + const consequent = this.generateInstructionList(ir.consequent); + const alternate = this.generateInstructionList(ir.alternate); + return new TypedExpression( + `if (${test}) {${consequent.code}} else {${alternate.code}}`, + constants.TYPE_STATEMENT + ); + } + case constants.IR_WAIT: { + return new TypedExpression( + 'console.log(wait)', + constants.TYPE_STATEMENT + ); + } + case constants.IR_ADD: { + const left = this.generateInstruction(ir.left); + const right = this.generateInstruction(ir.right); + return new TypedExpression( + `(${left.asNumber()}) + (${right.asNumber()})`, + constants.TYPE_NUMBER + ); + } + case constants.IR_SUB: { + const left = this.generateInstruction(ir.left); + const right = this.generateInstruction(ir.right); + return new TypedExpression( + `(${left.asNumber()}) - (${right.asNumber()})`, + constants.TYPE_NUMBER + ); + } + case constants.IR_LOAD: { + return this.generateInstruction(ir.variable); + } + case constants.IR_STORE: { + const variable = this.generateInstruction(ir.variable); + const value = this.generateInstruction(ir.value); + return new TypedExpression( + `(${variable.code}) = (${value.code})`, + constants.TYPE_STATEMENT + ); + } + default: { + throw 'unknown ir instruction'; + } + } + } +} + +module.exports = JSGenerator; diff --git a/packages/vm/src/compiler/type.d.ts b/packages/vm/src/compiler/type.d.ts new file mode 100644 index 000000000..29d982ca7 --- /dev/null +++ b/packages/vm/src/compiler/type.d.ts @@ -0,0 +1,67 @@ +interface IRBaseInst { + opcode: string; +} + +interface IRBinaryInst extends IRBaseInst { + left: IRBaseInst; + right: IRBaseInst; +} + +interface IRConstant extends IRBaseInst { + opcode: 'constant'; + value: any; +} + +interface IRIdentifier extends IRBaseInst { + opcode: 'identifier'; + name: string, + id: string; + scope: 'target' | 'stage'; +} + +interface IRWhileInst extends IRBaseInst { + opcode: 'control.while'; + test: IRBaseInst; + body: IRBaseInst[]; +} + +interface IRRepeatInst extends IRBaseInst { + opcode: 'control.repeat'; + times: IRBaseInst; + body: IRBaseInst[]; +} + +interface IRIfElseInst extends IRBaseInst { + opcode: 'control.ifelse'; + test: IRBaseInst; + consequent: IRBaseInst[]; + alternate: IRBaseInst[]; +} + +interface IRWaitInst extends IRBaseInst { + opcode: 'control.wait'; + duration: IRBaseInst; +} + +interface IRAddInst extends IRBinaryInst { + opcode: 'op.add'; +} + +interface IRSubInst extends IRBinaryInst { + opcode: 'op.sub'; +} + +interface IRLoadInst extends IRBaseInst { + opcode: 'var.load'; + variable: IRIdentifier; +} + +interface IRStoreInst extends IRBaseInst { + opcode: 'var.store'; + variable: IRIdentifier; + value: IRBaseInst; +} + +type IRInst = IRConstant | IRIdentifier | + IRWhileInst | IRRepeatInst | IRIfElseInst | IRWaitInst | + IRAddInst | IRSubInst | IRLoadInst | IRStoreInst; diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index 3d5fedd57..01cfde18d 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -17,6 +17,7 @@ const StageLayering = require('./stage-layering'); const Variable = require('./variable'); const xmlEscape = require('../util/xml-escape'); const ScratchLinkWebSocket = require('../util/scratch-link-websocket'); +const compile = require('../compiler/compile'); // Virtual I/O devices. const Clock = require('../io/clock'); @@ -1717,6 +1718,10 @@ class Runtime extends EventEmitter { thread.blockContainer = thread.updateMonitor ? this.monitorBlocks : target.blocks; + + if (!thread.updateMonitor) { + compile(thread); + } thread.pushStack(id); this.threads.push(thread); diff --git a/packages/vm/src/engine/sequencer.js b/packages/vm/src/engine/sequencer.js index f76fa8316..b0ca08fec 100644 --- a/packages/vm/src/engine/sequencer.js +++ b/packages/vm/src/engine/sequencer.js @@ -177,6 +177,15 @@ class Sequencer { * @param {!Thread} thread Thread object to step. */ stepThread (thread) { + // check if thread is compiled + if (thread.compileResult) { + const result = thread.compileResult.next(); + if (result.done) { + thread.status = Thread.STATUS_DONE; + } + return; + } + let currentBlockId = thread.peekStack(); if (!currentBlockId) { // A "null block" - empty branch. diff --git a/packages/vm/src/engine/thread.js b/packages/vm/src/engine/thread.js index 96b1829e6..d15b2a3ab 100644 --- a/packages/vm/src/engine/thread.js +++ b/packages/vm/src/engine/thread.js @@ -199,6 +199,12 @@ class Thread { * @type {boolean} */ this.controlFlowed = false; + + /** + * The compiled JavaScript executable object. + * @type {?Generator} + */ + this.compileResult = null; } /**