diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index 7a059a5b..fcf31b88 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -1145,7 +1145,7 @@ class ScriptTreeGenerator { const blockInfo = this.getBlockInfo(block.opcode); if (blockInfo) { const type = blockInfo.info.blockType; - if (type === BlockType.COMMAND) { + if (type === BlockType.COMMAND || type === BlockType.CONDITIONAL) { return this.descendCompatLayer(block); } } @@ -1386,19 +1386,37 @@ class ScriptTreeGenerator { */ descendCompatLayer (block) { this.script.yields = true; + const inputs = {}; - const fields = {}; for (const name of Object.keys(block.inputs)) { - inputs[name] = this.descendInputOfBlock(block, name); + if (!name.startsWith('SUBSTACK')) { + inputs[name] = this.descendInputOfBlock(block, name); + } } + + const fields = {}; for (const name of Object.keys(block.fields)) { fields[name] = block.fields[name].value; } + + const blockInfo = this.getBlockInfo(block.opcode); + const blockType = (blockInfo && blockInfo.info && blockInfo.info.blockType) || BlockType.COMMAND; + const substacks = []; + if (blockType === BlockType.LOOP || blockType === BlockType.CONDITIONAL) { + const branchCount = blockInfo.info.branchCount; + for (let i = 0; i < branchCount; i++) { + const inputName = i === 0 ? 'SUBSTACK' : `SUBSTACK${i + 1}`; + substacks.push(this.descendSubstack(block, inputName)); + } + } + return { kind: 'compat', opcode: block.opcode, + blockType, inputs, - fields + fields, + substacks }; } diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 32a0d1d7..ce8df777 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -1,5 +1,6 @@ const log = require('../util/log'); const Cast = require('../util/cast'); +const BlockType = require('../extension-support/block-type'); const VariablePool = require('./variable-pool'); const jsexecute = require('./jsexecute'); const environment = require('./environment'); @@ -760,7 +761,21 @@ class JSGenerator { // If the last command in a loop returns a promise, immediately continue to the next iteration. // If you don't do this, the loop effectively yields twice per iteration and will run at half-speed. const isLastInLoop = this.isLastBlockInLoop(); - this.source += `${this.generateCompatibilityLayerCall(node, isLastInLoop)};\n`; + + const blockType = node.blockType; + if (blockType === BlockType.COMMAND || blockType === BlockType.HAT) { + this.source += `${this.generateCompatibilityLayerCall(node, isLastInLoop)};\n`; + } else if (blockType === BlockType.CONDITIONAL) { + this.source += `switch (Math.round(${this.generateCompatibilityLayerCall(node, isLastInLoop)})) {\n`; + for (let i = 0; i < node.substacks.length; i++) { + this.source += `case ${i + 1}: {\n`; + this.descendStack(node.substacks[i], new Frame(false)); + this.source += `break;\n`; + this.source += `}\n`; + } + this.source += `}\n`; + } + if (isLastInLoop) { this.source += 'if (hasResumedFromPromise) {hasResumedFromPromise = false;continue;}\n'; } diff --git a/test/fixtures/tw-conditional.sb3 b/test/fixtures/tw-conditional.sb3 new file mode 100644 index 00000000..fc72114c Binary files /dev/null and b/test/fixtures/tw-conditional.sb3 differ diff --git a/test/integration/tw_custom_conditional_and_loop.js b/test/integration/tw_custom_conditional_and_loop.js new file mode 100644 index 00000000..6d90c245 --- /dev/null +++ b/test/integration/tw_custom_conditional_and_loop.js @@ -0,0 +1,90 @@ +const fs = require('fs'); +const path = require('path'); +const {test} = require('tap'); +const VirtualMachine = require('../../src/virtual-machine'); +const Scratch = require('../../src/extension-support/tw-extension-api-common'); + +class LoopsAndThings { + getInfo () { + return { + id: 'loopsAndThings', + name: 'Loops and things test', + blocks: [ + { + opcode: 'conditional', + blockType: Scratch.BlockType.CONDITIONAL, + text: 'run branch [BRANCH] of', + arguments: { + BRANCH: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1 + } + }, + branchCount: 3 + }, + { + opcode: 'loop', + blockType: Scratch.BlockType.LOOP, + text: 'my repeat [TIMES]', + arguments: { + TIMES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 10 + } + } + } + ] + }; + } + + conditional ({BRANCH}, util) { + return Scratch.Cast.toNumber(BRANCH); + } + + loop ({TIMES}, util) { + const times = Math.round(Scratch.Cast.toNumber(TIMES)); + if (typeof util.stackFrame.loopCounter === 'undefined') { + util.stackFrame.loopCounter = times; + } + util.stackFrame.loopCounter--; + if (util.stackFrame.loopCounter >= 0) { + return true; + } + } +} + +const compilerAndInterpreter = (name, callback) => { + test(`${name} - interpreted`, t => { + callback(t, { + enabled: false + }); + }); + test(`${name} - compiled`, t => { + callback(t, { + enabled: true + }); + }); +}; + +compilerAndInterpreter('CONDITIONAL', (t, co) => { + t.plan(1); + + const vm = new VirtualMachine(); + vm.setCompilerOptions(co); + vm.extensionManager.addBuiltinExtension('loopsAndThings', LoopsAndThings); + + vm.loadProject(fs.readFileSync(path.join(__dirname, '../fixtures/tw-conditional.sb3'))).then(() => { + let okayCount = 0; + vm.runtime.on('SAY', (target, type, text) => { + if (text === 'OK!') { + okayCount++; + } else if (text === 'end') { + t.equal(okayCount, 5); + t.end(); + } + }); + + vm.greenFlag(); + vm.runtime._step(); + }); +});