diff --git a/src/compiler/astgen.js b/src/compiler/astgen.js index edbb3a97..eddafc27 100644 --- a/src/compiler/astgen.js +++ b/src/compiler/astgen.js @@ -27,6 +27,14 @@ const compatBlocks = require('./compat-blocks'); * @property {string} kind */ +const createVariableData = (scope, varObj) => ({ + scope, + // todo: maybe just return varObj + id: varObj.id, + name: varObj.name, + isCloud: varObj.isCloud +}); + class ScriptTreeGenerator { constructor (thread) { this.thread = thread; @@ -1029,29 +1037,32 @@ class ScriptTreeGenerator { return result; } - descendVariable (block, name, type) { - // todo: cache variables by ID - const variable = block.fields[name]; + descendVariable (block, fieldName, type) { + const variable = block.fields[fieldName]; const id = variable.id; + if (this.variableCache.hasOwnProperty(id)) { + return this.variableCache[id]; + } + + const data = this.variableCache[id] = this._descendVariable(id, variable.value, type); + this.variableCache[id] = data; + return data; + } + + _descendVariable (id, name, type) { const target = this.target; const stage = this.stage; // Look for by ID in target... if (target.variables.hasOwnProperty(id)) { - return { - scope: 'target', - id - }; + return createVariableData('target', target.variables[id]); } // Look for by ID in stage... if (!target.isStage) { if (stage && stage.variables.hasOwnProperty(id)) { - return { - scope: 'stage', - id - }; + return createVariableData('stage', stage.variables[id]); } } @@ -1060,10 +1071,7 @@ class ScriptTreeGenerator { if (target.variables.hasOwnProperty(varId)) { const currVar = target.variables[varId]; if (currVar.name === name && currVar.type === type) { - return { - scope: 'target', - id: varId - }; + return createVariableData('target', currVar); } } } @@ -1074,10 +1082,7 @@ class ScriptTreeGenerator { if (stage.variables.hasOwnProperty(varId)) { const currVar = stage.variables[varId]; if (currVar.name === name && currVar.type === type) { - return { - sccope: 'stage', - id: varId - }; + return createVariableData('stage', currVar); } } } @@ -1086,10 +1091,7 @@ class ScriptTreeGenerator { // Create it locally... const newVariable = new Variable(id, name, type, false); target.variables[id] = newVariable; - return { - scope: 'target', - id: id - }; + return createVariableData('target', newVariable); } descendCompatLayer (block) { diff --git a/src/compiler/compat-blocks.js b/src/compiler/compat-blocks.js index 1d1cd230..1f9ff095 100644 --- a/src/compiler/compat-blocks.js +++ b/src/compiler/compat-blocks.js @@ -1,42 +1,42 @@ const stacked = [ + 'control_clear_counter', 'control_get_counter', 'control_incr_counter', - 'control_clear_counter', - 'motion_glideto', - 'motion_glidesecstoxy', - 'motion_goto', - 'sound_play', - 'sound_playuntildone', - 'sound_stopallsounds', - 'sound_seteffectto', - 'sound_changeeffectby', - 'sound_cleareffects', - 'sound_setvolumeto', - 'sensing_askandwait', - 'sound_seteffectto', - 'sound_changeeffectby', - 'sound_changevolumeby', - 'looks_nextcostume', + 'looks_changeeffectby', 'looks_nextbackdrop', + 'looks_nextcostume', 'looks_say', 'looks_sayforsecs', - 'looks_think', - 'looks_changeeffectby', 'looks_seteffectto', + 'looks_think', 'looks_thinkforsecs', + 'motion_glidesecstoxy', + 'motion_glideto', + 'motion_goto', + 'motion_pointtowards', + 'sensing_askandwait', 'sensing_setdragmode', - 'motion_pointtowards' + 'sound_changeeffectby', + 'sound_changeeffectby', + 'sound_changevolumeby', + 'sound_cleareffects', + 'sound_play', + 'sound_playuntildone', + 'sound_seteffectto', + 'sound_seteffectto', + 'sound_setvolumeto', + 'sound_stopallsounds' ]; const inputs = [ - 'sound_volume', - 'sensing_of', - 'sensing_distanceto', + 'sensing_answer', 'sensing_current', 'sensing_dayssince2000', - 'sensing_loudness', + 'sensing_distanceto', 'sensing_loud', - 'sensing_answer' + 'sensing_loudness', + 'sensing_of', + 'sound_volume' ]; module.exports = { diff --git a/src/compiler/compiler.js b/src/compiler/compiler.js index 24dee543..76a6b3f3 100644 --- a/src/compiler/compiler.js +++ b/src/compiler/compiler.js @@ -5,7 +5,7 @@ const compile = thread => { const astGenerator = new ASTGenerator(thread); const ast = astGenerator.generate(); - const jsGenerator = new JSGenerator(ast); + const jsGenerator = new JSGenerator(ast, thread.target); const js = jsGenerator.compile(); return js; diff --git a/src/compiler/execute.js b/src/compiler/execute.js index e2faacf7..66b6afb4 100644 --- a/src/compiler/execute.js +++ b/src/compiler/execute.js @@ -3,8 +3,17 @@ const Timer = require('../util/timer'); const Cast = require('../util/cast'); const log = require('../util/log'); +/* eslint-disable no-unused-vars */ + const compatibilityLayerBlockUtility = require('./compat-block-utility'); + +/** + * The currently running thread. + * @type {Thread} + */ +var thread; + // All the functions defined here will be available to compiled scripts. // The JSDoc annotations define the function's contract. // Most of these functions are only used at runtime by generated scripts. Despite what your editor may say, they are not unused. @@ -443,12 +452,6 @@ const mod = (n, modulus) => { return result; }; -/** - * The currently running thread. - * @type {Thread} - */ -var thread; - /** * Step a compiled thread. * @param {Thread} _thread diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 29dcb9ef..d6d0001c 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -120,9 +120,10 @@ class ConstantInput { } class ScriptCompiler { - constructor (script, ast) { + constructor (script, ast, target) { this.script = script; this.ast = ast; + this.target = target; this.source = ''; this.localVariables = new VariablePool('a'); this._setupVariablesPool = new VariablePool('b'); @@ -131,7 +132,7 @@ class ScriptCompiler { /** * @param {object} node Input node to compile. - * @returns {Input} + * @returns {Input} Compiled input. */ descendInput (node) { switch (node.kind) { @@ -144,8 +145,7 @@ class ScriptCompiler { return new TypedInput(`(${this.generateCompatCall(node)})`, TYPE_UNKNOWN); case 'constant': - // todo: converting to number sometimes break things, need to check for those conditions - return new ConstantInput(node.value); + return this.safeConstantInput(node.value); case 'list.contains': return new TypedInput(`listContains(${this.referenceVariable(node.list)}, ${this.descendInput(node.item).asUnknown()})`, TYPE_BOOLEAN); @@ -287,7 +287,6 @@ class ScriptCompiler { this.source += `runtime.ext_scratch3_control._createClone(${this.descendInput(node.target).asString()}, target);\n`; break; case 'control.deleteClone': - // TODO: actually stop thread this.source += 'if (!target.isOriginal) {\n'; this.source += ' runtime.disposeTarget(target);\n'; this.source += ' runtime.stopForTarget(target);\n'; @@ -373,16 +372,16 @@ class ScriptCompiler { this.source += `yield* waitThreads(startHats("event_whenbroadcastreceived", { BROADCAST_OPTION: ${this.descendInput(node.broadcast).asString()} }));\n`; break; - case 'list.add': - this.source += `${this.referenceVariable(node.list)}.value.push(${this.descendInput(node.item).asUnknown()});\n`; - // todo _monitorUpToDate + case 'list.add': { + const list = this.referenceVariable(node.list); + this.source += `${list}._monitorUpToDate = false;\n`; break; + } case 'list.delete': this.source += `listDelete(${this.referenceVariable(node.list)}, ${this.descendInput(node.index).asUnknown()});\n`; break; case 'list.deleteAll': this.source += `${this.referenceVariable(node.list)}.value = [];\n`; - // todo _monitorUpToDate break; case 'list.hide': this.source += `runtime.monitorBlocks.changeBlock({ id: "${sanitize(node.list.id)}", element: "checkbox", value: false }, runtime);\n`; @@ -424,7 +423,7 @@ class ScriptCompiler { this.source += 'runtime.ext_scratch3_looks._renderBubble(target);\n'; break; case 'looks.switchBackdrop': - this.source += `runtime.ext_scratch3_looks._setBackdrop(stage, ${this.descendInput(node.backdrop)});\n`; + this.source += `runtime.ext_scratch3_looks._setBackdrop(stage, ${this.descendInput(node.backdrop).asUnknown()});\n`; break; case 'looks.switchCostume': this.source += `runtime.ext_scratch3_looks._setCostume(target, ${this.descendInput(node.costume).asUnknown()});\n`; @@ -512,18 +511,23 @@ class ScriptCompiler { case 'var.change': { const variable = this.referenceVariable(node.variable); - // todo: cloud this.source += `${variable}.value = (+${variable}.value || 0) + ${this.descendInput(node.value).asNumber()};\n`; + if (node.variable.isCloud) { + this.source += `ioQuery("cloud", "requestUpdateVariable", ["${sanitize(node.variable.name)}", ${variable}.value]);\n`; + } break; } - case 'var.hide': this.source += `runtime.monitorBlocks.changeBlock({ id: "${sanitize(node.variable.id)}", element: "checkbox", value: false }, runtime);\n`; break; - case 'var.set': - // todo: cloud - this.source += `${this.referenceVariable(node.variable)}.value = ${this.descendInput(node.value).asUnknown()};\n`; + case 'var.set': { + const variable = this.referenceVariable(node.variable); + this.source += `${variable}.value = ${this.descendInput(node.value).asUnknown()};\n`; + if (node.variable.isCloud) { + this.source += `ioQuery("cloud", "requestUpdateVariable", ["${sanitize(node.variable.name)}", ${variable}.value]);\n`; + } break; + } case 'var.show': this.source += `runtime.monitorBlocks.changeBlock({ id: "${sanitize(node.variable.id)}", element: "checkbox", value: true }, runtime);\n`; break; @@ -541,7 +545,6 @@ class ScriptCompiler { } referenceVariable (variable) { - // todo: factoryVariables if (variable.scope === 'target') { return this.evaluateOnce(`target.variables["${sanitize(variable.id)}"]`); } @@ -567,6 +570,20 @@ class ScriptCompiler { } } + safeConstantInput (value) { + if (typeof value === 'string') { + if (this.isNameOfCostume(value)) { + return new TypedInput(`"${sanitize(value)}"`, TYPE_STRING); + } + } + return new ConstantInput(value); + } + + isNameOfCostume (stringValue) { + // todo: also check backdrop + return this.target.getCostumeIndexByName(stringValue) !== -1; + } + generateCompatCall (node) { const opcode = node.opcode; @@ -650,8 +667,9 @@ disableToString(TypedInput.prototype.asBoolean); disableToString(TypedInput.prototype.asUnknown); class JSCompiler { - constructor (ast) { + constructor (ast, target) { this.ast = ast; + this.target = target; this.compilingProcedures = []; this.compiledProcedures = {}; } @@ -676,7 +694,7 @@ class JSCompiler { this.compilingProcedures.pop(); } - const compiler = new ScriptCompiler(script, this.ast); + const compiler = new ScriptCompiler(script, this.ast, this.target); return compiler.compile(); } diff --git a/src/compiler/variable-pool.js b/src/compiler/variable-pool.js index f30b7cda..11f50594 100644 --- a/src/compiler/variable-pool.js +++ b/src/compiler/variable-pool.js @@ -2,7 +2,7 @@ class VariablePool { /** * @param {string} prefix The prefix at the start of the variable name. */ - constructor(prefix) { + constructor (prefix) { if (prefix.trim().length === 0) { throw new Error('prefix cannot be empty'); } @@ -13,7 +13,7 @@ class VariablePool { this.count = 0; } - next() { + next () { return `${this.prefix}${this.count++}`; } } diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 4cd0fec5..3a14de59 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -362,7 +362,7 @@ class Sequencer { thread.requestScriptGlowInFrame = false; thread.status = Thread.STATUS_DONE; if (thread.isCompiled) { - thread.procedures = {}; + thread.procedures = null; thread.generator = null; } } diff --git a/src/engine/thread.js b/src/engine/thread.js index 79e2e618..bdecb547 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -209,7 +209,7 @@ class Thread { /** * @type {Object.} */ - this.procedures = {}; + this.procedures = null; } /**