From db0776711f22b8540d62949f975acf4bf90f866a Mon Sep 17 00:00:00 2001 From: Muffin Date: Tue, 22 Aug 2023 12:29:31 -0500 Subject: [PATCH] Implement CONDITIONAL blocks in compiler --- src/compiler/irgen.js | 26 ++++- src/compiler/jsgen.js | 17 +++- test/fixtures/tw-conditional.sb3 | Bin 0 -> 2376 bytes .../tw_custom_conditional_and_loop.js | 90 ++++++++++++++++++ 4 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 test/fixtures/tw-conditional.sb3 create mode 100644 test/integration/tw_custom_conditional_and_loop.js 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 0000000000000000000000000000000000000000..fc72114c277bcf29ef67ce17c3518a6e993ca270 GIT binary patch literal 2376 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 literal 0 HcmV?d00001 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(); + }); +});