const Cast = require('../util/cast'); const StringUtil = require('../util/string-util'); const BlockType = require('../extension-support/block-type'); const Sequencer = require('../engine/sequencer'); const BlockUtility = require('../engine/block-utility'); const Variable = require('../engine/variable'); const Color = require('../util/color'); const log = require('../util/log'); const Clone = require('../util/clone'); const {IntermediateScript, IntermediateRepresentation} = require('./intermediate'); const compatBlocks = require('./compat-blocks'); /** * @fileoverview Generate intermediate representations from Scratch blocks. */ const SCALAR_TYPE = ''; const LIST_TYPE = 'list'; /** * @typedef {Object.} Node * @property {string} kind */ /** * Create a variable codegen object. * @param {'target'|'stage'} scope The scope of this variable -- which object owns it. * @param {import('../engine/variable.js')} varObj The Scratch Variable * @returns {*} A variable codegen object. */ const createVariableData = (scope, varObj) => ({ scope, id: varObj.id, name: varObj.name, isCloud: varObj.isCloud }); /** * @param {string} code * @param {boolean} warp * @returns {string} */ const generateProcedureVariant = (code, warp) => { if (warp) { return `W${code}`; } return `Z${code}`; }; /** * @param {string} variant Variant generated by generateProcedureVariant() * @returns {string} original procedure code */ const parseProcedureCode = variant => variant.substring(1); /** * @param {string} variant Variant generated by generateProcedureVariant() * @returns {boolean} true if warp enabled */ const parseIsWarp = variant => variant.charAt(0) === 'W'; class ScriptTreeGenerator { constructor (thread) { /** @private */ this.thread = thread; /** @private */ this.target = thread.target; /** @private */ this.blocks = thread.blockContainer; /** @private */ this.runtime = this.target.runtime; /** @private */ this.stage = this.runtime.getTargetForStage(); /** @private */ this.util = new BlockUtility(this.runtime.sequencer, this.thread); /** * This script's intermediate representation. */ this.script = new IntermediateScript(); this.script.warpTimer = this.target.runtime.compilerOptions.warpTimer; this.script.isOptimized = this.target.runtime.runtimeOptions.dangerousOptimizations; this.script.optimizationUtil = this.target.runtime.optimizationUtil; /** * Cache of variable ID to variable data object. * @type {Object.} * @private */ this.variableCache = {}; this.usesTimer = false; } setProcedureVariant (procedureVariant) { const procedureCode = parseProcedureCode(procedureVariant); this.script.procedureCode = procedureCode; this.script.isProcedure = true; this.script.yields = false; const paramNamesIdsAndDefaults = this.blocks.getProcedureParamNamesIdsAndDefaults(procedureCode); if (paramNamesIdsAndDefaults === null) { throw new Error(`IR: cannot find procedure: ${procedureVariant}`); } const [paramNames, _paramIds, _paramDefaults] = paramNamesIdsAndDefaults; this.script.arguments = paramNames; } enableWarp () { this.script.isWarp = true; } getBlockById (blockId) { // Flyout blocks are stored in a special container. return this.blocks.getBlock(blockId) || this.blocks.runtime.flyoutBlocks.getBlock(blockId); } getBlockInfo (fullOpcode) { const [category, opcode] = StringUtil.splitFirst(fullOpcode, '_'); if (!category || !opcode) { return null; } const categoryInfo = this.runtime._blockInfo.find(ci => ci.id === category); if (!categoryInfo) { return null; } const blockInfo = categoryInfo.blocks.find(b => b.info.opcode === opcode); if (!blockInfo) { return null; } return blockInfo; } /** * Descend into a child input of a block. (eg. the input STRING of "length of ( )") * @param {*} parentBlock The parent Scratch block that contains the input. * @param {string} inputName The name of the input to descend into. * @private * @returns {Node} Compiled input node for this input. */ descendInputOfBlock (parentBlock, inputName) { const input = parentBlock.inputs[inputName]; if (!input) { log.warn(`IR: ${parentBlock.opcode}: missing input ${inputName}`, parentBlock); return { kind: 'constant', value: 0 }; } const inputId = input.block; const block = this.getBlockById(inputId); if (!block) { log.warn(`IR: ${parentBlock.opcode}: could not find input ${inputName} with ID ${inputId}`); return { kind: 'constant', value: 0 }; } return this.descendInput(block); } /** * Descend into an input. (eg. "length of ( )") * @param {*} block The parent Scratch block input. * @private * @returns {Node} Compiled input node for this input. */ descendInput (block) { // check if we have extension ir for this opcode const extensionId = String(block.opcode).split('_')[0]; const blockId = String(block.opcode).replace(extensionId + '_', ''); if (IRGenerator.hasExtensionIr(extensionId) && IRGenerator.getExtensionIr(extensionId)[blockId]) { // this is an extension block that wants to be compiled const irFunc = IRGenerator.getExtensionIr(extensionId)[blockId]; let irData = null; // make sure irFunc isnt broken try { irData = irFunc(this, block); } catch (err) { log.warn(extensionId + '_' + blockId, 'failed to create IR data;', err); } if (irData) { // check if it is this type, we dont want to descend a stack as an input if (irData.kind === 'input') { // set proper kind irData.kind = extensionId + '.' + blockId; return irData; } } } switch (block.opcode) { case 'colour_picker': return { kind: 'constant', value: block.fields.COLOUR.value }; case 'math_angle': case 'math_integer': case 'math_number': case 'math_positive_number': case 'math_whole_number': return { kind: 'constant', value: block.fields.NUM.value }; case 'text': return { kind: 'constant', value: block.fields.TEXT.value }; case 'polygon': const points = []; for (let point = 1; point <= block.mutation.points; point++) { const xn = `x${point}`; const yn = `y${point}`; points.push({ x: this.descendInputOfBlock(block, xn), y: this.descendInputOfBlock(block, yn) }); } return { kind: 'math.polygon', points }; case 'argument_reporter_string_number': { const name = block.fields.VALUE.value; // lastIndexOf because multiple parameters with the same name will use the value of the last definition const index = this.script.arguments.lastIndexOf(name); if (index === -1) { // Legacy support if (name.toLowerCase() === 'last key pressed') { return { kind: 'tw.lastKeyPressed' }; } } if (index === -1) { return { kind: 'constant', value: 0 }; } return { kind: 'args.stringNumber', index: index }; } case 'argument_reporter_boolean': { // see argument_reporter_string_number above const name = block.fields.VALUE.value; const index = this.script.arguments.lastIndexOf(name); if (index === -1) { if (name.toLowerCase() === 'is compiled?' || name.toLowerCase() === 'is turbowarp?' || name.toLowerCase() === 'is penguinmod or turbowarp?') { return { kind: 'constant', value: true }; } return { kind: 'constant', value: 0 }; } return { kind: 'args.boolean', index: index }; } case 'control_get_counter': return { kind: 'counter.get' }; case 'control_error': return { kind: 'control.error' }; case 'control_is_clone': return { kind: 'control.isclone' }; case 'data_variable': return { kind: 'var.get', variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE) }; case 'data_itemoflist': return { kind: 'list.get', list: this.descendVariable(block, 'LIST', LIST_TYPE), index: this.descendInputOfBlock(block, 'INDEX') }; case 'data_lengthoflist': return { kind: 'list.length', list: this.descendVariable(block, 'LIST', LIST_TYPE) }; case 'data_listcontainsitem': return { kind: 'list.contains', list: this.descendVariable(block, 'LIST', LIST_TYPE), item: this.descendInputOfBlock(block, 'ITEM') }; case 'data_itemnumoflist': return { kind: 'list.indexOf', list: this.descendVariable(block, 'LIST', LIST_TYPE), item: this.descendInputOfBlock(block, 'ITEM') }; case 'data_amountinlist': return { kind: 'list.amountOf', list: this.descendVariable(block, 'LIST', LIST_TYPE), value: this.descendInputOfBlock(block, 'VALUE') }; case 'data_listcontents': return { kind: 'list.contents', list: this.descendVariable(block, 'LIST', LIST_TYPE) }; case 'data_filterlistitem': return { kind: 'list.filteritem' }; case 'data_filterlistindex': return { kind: 'list.filterindex' }; case 'event_broadcast_menu': { const broadcastOption = block.fields.BROADCAST_OPTION; const broadcastVariable = this.target.lookupBroadcastMsg(broadcastOption.id, broadcastOption.value); // TODO: empty string probably isn't the correct fallback const broadcastName = broadcastVariable ? broadcastVariable.name : ''; return { kind: 'constant', value: broadcastName }; } case 'pmEventsExpansion_broadcastFunction': this.script.yields = true; return { kind: 'pmEventsExpansion.broadcastFunction', broadcast: this.descendInputOfBlock(block, 'BROADCAST') }; case 'pmEventsExpansion_broadcastFunctionArgs': this.script.yields = true; return { kind: 'pmEventsExpansion.broadcastFunctionArgs', broadcast: this.descendInputOfBlock(block, 'BROADCAST'), args: this.descendInputOfBlock(block, 'ARGS') }; case 'control_inline_stack_output': return { kind: 'control.inlineStackOutput', code: this.descendSubstack(block, 'SUBSTACK') }; case 'looks_backdropnumbername': if (block.fields.NUMBER_NAME.value === 'number') { return { kind: 'looks.backdropNumber' }; } return { kind: 'looks.backdropName' }; case 'looks_costumenumbername': if (block.fields.NUMBER_NAME.value === 'number') { return { kind: 'looks.costumeNumber' }; } return { kind: 'looks.costumeName' }; case 'looks_size': return { kind: 'looks.size' }; case 'looks_tintColor': return { kind: 'looks.tintColor' }; case 'motion_direction': return { kind: 'motion.direction' }; case 'motion_xposition': return { kind: 'motion.x' }; case 'motion_yposition': return { kind: 'motion.y' }; case 'operator_add': return { kind: 'op.add', left: this.descendInputOfBlock(block, 'NUM1'), right: this.descendInputOfBlock(block, 'NUM2') }; case 'operator_and': return { kind: 'op.and', left: this.descendInputOfBlock(block, 'OPERAND1'), right: this.descendInputOfBlock(block, 'OPERAND2') }; case 'operator_contains': return { kind: 'op.contains', string: this.descendInputOfBlock(block, 'STRING1'), contains: this.descendInputOfBlock(block, 'STRING2') }; case 'operator_divide': return { kind: 'op.divide', left: this.descendInputOfBlock(block, 'NUM1'), right: this.descendInputOfBlock(block, 'NUM2') }; case 'operator_power': return { kind: 'op.power', left: this.descendInputOfBlock(block, 'NUM1'), right: this.descendInputOfBlock(block, 'NUM2') }; case 'operator_equals': return { kind: 'op.equals', left: this.descendInputOfBlock(block, 'OPERAND1'), right: this.descendInputOfBlock(block, 'OPERAND2') }; case 'operator_gt': return { kind: 'op.greater', left: this.descendInputOfBlock(block, 'OPERAND1'), right: this.descendInputOfBlock(block, 'OPERAND2') }; case 'operator_join': return { kind: 'op.join', left: this.descendInputOfBlock(block, 'STRING1'), right: this.descendInputOfBlock(block, 'STRING2') }; case 'operator_length': return { kind: 'op.length', string: this.descendInputOfBlock(block, 'STRING') }; case 'operator_letter_of': return { kind: 'op.letterOf', letter: this.descendInputOfBlock(block, 'LETTER'), string: this.descendInputOfBlock(block, 'STRING') }; case 'operator_lt': return { kind: 'op.less', left: this.descendInputOfBlock(block, 'OPERAND1'), right: this.descendInputOfBlock(block, 'OPERAND2') }; case 'operator_mathop': { const value = this.descendInputOfBlock(block, 'NUM'); const operator = block.fields.OPERATOR.value.toLowerCase(); switch (operator) { case 'abs': return { kind: 'op.abs', value }; case 'floor': return { kind: 'op.floor', value }; case 'ceiling': return { kind: 'op.ceiling', value }; case 'sign': return { kind: 'op.sign', value }; case 'sqrt': return { kind: 'op.sqrt', value }; case 'sin': return { kind: 'op.sin', value }; case 'cos': return { kind: 'op.cos', value }; case 'tan': return { kind: 'op.tan', value }; case 'asin': return { kind: 'op.asin', value }; case 'acos': return { kind: 'op.acos', value }; case 'atan': return { kind: 'op.atan', value }; case 'ln': return { kind: 'op.ln', value }; case 'log': return { kind: 'op.log', value }; case 'log2': return { kind: 'op.log2', value }; case 'e ^': return { kind: 'op.e^', value }; case '10 ^': return { kind: 'op.10^', value }; default: return { kind: 'constant', value: 0 }; } } case 'operator_advlog': return { kind: 'op.advlog', left: this.descendInputOfBlock(block, 'NUM1'), right: this.descendInputOfBlock(block, 'NUM2') }; case 'operator_mod': return { kind: 'op.mod', left: this.descendInputOfBlock(block, 'NUM1'), right: this.descendInputOfBlock(block, 'NUM2') }; case 'operator_multiply': return { kind: 'op.multiply', left: this.descendInputOfBlock(block, 'NUM1'), right: this.descendInputOfBlock(block, 'NUM2') }; case 'operator_not': return { kind: 'op.not', operand: this.descendInputOfBlock(block, 'OPERAND') }; case 'operator_or': return { kind: 'op.or', left: this.descendInputOfBlock(block, 'OPERAND1'), right: this.descendInputOfBlock(block, 'OPERAND2') }; case 'operator_random': { const from = this.descendInputOfBlock(block, 'FROM'); const to = this.descendInputOfBlock(block, 'TO'); // If both values are known at compile time, we can do some optimizations. // TODO: move optimizations to jsgen? if (from.kind === 'constant' && to.kind === 'constant') { const sFrom = from.value; const sTo = to.value; const nFrom = Cast.toNumber(sFrom); const nTo = Cast.toNumber(sTo); // If both numbers are the same, random is unnecessary. // todo: this probably never happens so consider removing if (nFrom === nTo) { return { kind: 'constant', value: nFrom }; } // If both are ints, hint this to the compiler if (Cast.isInt(sFrom) && Cast.isInt(sTo)) { return { kind: 'op.random', low: nFrom <= nTo ? from : to, high: nFrom <= nTo ? to : from, useInts: true, useFloats: false }; } // Otherwise hint that these are floats return { kind: 'op.random', low: nFrom <= nTo ? from : to, high: nFrom <= nTo ? to : from, useInts: false, useFloats: true }; } else if (from.kind === 'constant') { // If only one value is known at compile-time, we can still attempt some optimizations. if (!Cast.isInt(Cast.toNumber(from.value))) { return { kind: 'op.random', low: from, high: to, useInts: false, useFloats: true }; } } else if (to.kind === 'constant') { if (!Cast.isInt(Cast.toNumber(to.value))) { return { kind: 'op.random', low: from, high: to, useInts: false, useFloats: true }; } } // No optimizations possible return { kind: 'op.random', low: from, high: to, useInts: false, useFloats: false }; } case 'operator_round': return { kind: 'op.round', value: this.descendInputOfBlock(block, 'NUM') }; case 'operator_subtract': return { kind: 'op.subtract', left: this.descendInputOfBlock(block, 'NUM1'), right: this.descendInputOfBlock(block, 'NUM2') }; case 'sensing_answer': return { kind: 'sensing.answer' }; case 'sensing_coloristouchingcolor': return { kind: 'sensing.colorTouchingColor', target: this.descendInputOfBlock(block, 'COLOR2'), mask: this.descendInputOfBlock(block, 'COLOR') }; case 'sensing_current': switch (block.fields.CURRENTMENU.value.toLowerCase()) { case 'year': return { kind: 'sensing.year' }; case 'month': return { kind: 'sensing.month' }; case 'date': return { kind: 'sensing.date' }; case 'dayofweek': return { kind: 'sensing.dayofweek' }; case 'hour': return { kind: 'sensing.hour' }; case 'minute': return { kind: 'sensing.minute' }; case 'second': return { kind: 'sensing.second' }; case 'timestamp': return { kind: 'sensing.timestamp' }; } return { kind: 'constant', value: 0 }; case 'sensing_dayssince2000': return { kind: 'sensing.daysSince2000' }; case 'sensing_distanceto': return { kind: 'sensing.distance', target: this.descendInputOfBlock(block, 'DISTANCETOMENU') }; case 'sensing_keypressed': return { kind: 'keyboard.pressed', key: this.descendInputOfBlock(block, 'KEY_OPTION') }; case 'sensing_mousedown': return { kind: 'mouse.down' }; case 'sensing_mousex': return { kind: 'mouse.x' }; case 'sensing_mousey': return { kind: 'mouse.y' }; case 'sensing_of': return { kind: 'sensing.of', property: block.fields.PROPERTY.value, object: this.descendInputOfBlock(block, 'OBJECT') }; case 'sensing_timer': this.usesTimer = true; return { kind: 'timer.get' }; case 'sensing_touchingcolor': return { kind: 'sensing.touchingColor', color: this.descendInputOfBlock(block, 'COLOR') }; case 'sensing_touchingobject': return { kind: 'sensing.touching', object: this.descendInputOfBlock(block, 'TOUCHINGOBJECTMENU') }; case 'sensing_username': return { kind: 'sensing.username' }; case 'sensing_loggedin': return { kind: 'sensing.loggedin' }; case 'operator_trueBoolean': return { kind: 'op.true' }; case 'operator_falseBoolean': return { kind: 'op.false' }; case 'operator_randomBoolean': return { kind: 'op.randbool' }; case 'sound_sounds_menu': return { kind: 'constant', value: block.fields.SOUND_MENU.value }; case 'lmsTempVars2_getRuntimeVariable': return { kind: 'tempVars.get', var: this.descendInputOfBlock(block, 'VAR'), runtime: true }; case 'lmsTempVars2_getThreadVariable': return { kind: 'tempVars.get', var: this.descendInputOfBlock(block, 'VAR'), thread: true }; case 'tempVars_getVariable': return { kind: 'tempVars.get', var: this.descendInputOfBlock(block, 'name') }; case 'lmsTempVars2_runtimeVariableExists': return { kind: 'tempVars.exists', var: this.descendInputOfBlock(block, 'VAR'), runtime: true }; case 'lmsTempVars2_threadVariableExists': return { kind: 'tempVars.exists', var: this.descendInputOfBlock(block, 'VAR'), thread: true }; case 'tempVars_variableExists': // This menu is special compared to other menus -- it actually has an opcode function. return { kind: 'tempVars.exists', var: this.descendInputOfBlock(block, 'name') }; case 'lmsTempVars2_listRuntimeVariables': return { kind: 'tempVars.all', runtime: true }; case 'lmsTempVars2_listThreadVariables': return { kind: 'tempVars.all', thread: true }; case 'tempVars_allVariables': return { kind: 'tempVars.all' }; // used by the stacked version of this block to run as an input block // despite there being a stacked version case 'procedures_call_return': case 'procedures_call': { // setting of yields will be handled later in the analysis phase const procedureCode = block.mutation.proccode; if (procedureCode === 'tw:debugger;') { return { kind: 'tw.debugger' }; } const paramNamesIdsAndDefaults = this.blocks.getProcedureParamNamesIdsAndDefaults(procedureCode); if (paramNamesIdsAndDefaults === null) { return { kind: 'noop' }; } const [paramNames, paramIds, paramDefaults] = paramNamesIdsAndDefaults; const addonBlock = this.runtime.getAddonBlock(procedureCode); if (addonBlock) { this.script.yields = true; const args = {}; for (let i = 0; i < paramIds.length; i++) { let value; if (block.inputs[paramIds[i]] && block.inputs[paramIds[i]].block) { value = this.descendInputOfBlock(block, paramIds[i]); } else { value = { kind: 'constant', value: paramDefaults[i] }; } args[paramNames[i]] = value; } return { kind: 'addons.call', code: procedureCode, arguments: args, blockId: block.id }; } const definitionId = this.blocks.getProcedureDefinition(procedureCode); const definitionBlock = this.blocks.getBlock(definitionId); if (!definitionBlock) { return { kind: 'noop' }; } const innerDefinition = this.blocks.getBlock(definitionBlock.inputs.custom_block.block); let isWarp = this.script.isWarp; if (!isWarp) { if (innerDefinition && innerDefinition.mutation) { const warp = innerDefinition.mutation.warp; if (typeof warp === 'boolean') { isWarp = warp; } else if (typeof warp === 'string') { isWarp = JSON.parse(warp); } } } const variant = generateProcedureVariant(procedureCode, isWarp); if (!this.script.dependedProcedures.includes(variant)) { this.script.dependedProcedures.push(variant); } // Non-warp direct recursion yields. if (!this.script.isWarp) { if (procedureCode === this.script.procedureCode) { this.script.yields = true; } } const args = []; for (let i = 0; i < paramIds.length; i++) { let value; if (block.inputs[paramIds[i]] && block.inputs[paramIds[i]].block) { if (paramIds[i].startsWith("SUBSTACK")) { value = this.descendSubstack(block, paramIds[i]) } else { value = this.descendInputOfBlock(block, paramIds[i]); } } else { value = { kind: 'constant', value: paramDefaults[i] }; } args.push(value); } return { kind: 'procedures.call', code: procedureCode, variant, returns: true, arguments: args, type: JSON.parse(block.mutation.opType || '"string"') }; } case 'tw_getLastKeyPressed': return { kind: 'tw.lastKeyPressed' }; case 'control_dualblock': return { kind: 'control.dualBlock' }; default: { const opcodeFunction = this.runtime.getOpcodeFunction(block.opcode); if (opcodeFunction) { // It might be a non-compiled primitive from a standard category if (compatBlocks.outputBlocks.includes(block.opcode)) { return this.descendCompatLayer(block); } // It might be an extension block. const blockInfo = this.getBlockInfo(block.opcode); if (blockInfo) { const type = blockInfo.info.blockType; const args = this.descendCompatLayer(block); args.block = block; if (block.mutation) args.mutation = block.mutation; if (type === BlockType.REPORTER || type === BlockType.BOOLEAN) { return args; } } } // It might be a menu. const inputs = Object.keys(block.inputs); const fields = Object.keys(block.fields); if (inputs.length === 0 && fields.length === 1) { return { kind: 'constant', value: block.fields[fields[0]].value }; } log.warn(`IR: Unknown input: ${block.opcode}`, block); throw new Error(`IR: Unknown input: ${block.opcode}`); } } } /** * Descend into a stacked block. (eg. "move ( ) steps") * @param {*} block The Scratch block to parse. * @private * @returns {Node} Compiled node for this block. */ descendStackedBlock (block) { // check if we have extension ir for this opcode const extensionId = String(block.opcode).split('_')[0]; const blockId = String(block.opcode).replace(extensionId + '_', ''); if (IRGenerator.hasExtensionIr(extensionId) && IRGenerator.getExtensionIr(extensionId)[blockId]) { // this is an extension block that wants to be compiled const irFunc = IRGenerator.getExtensionIr(extensionId)[blockId]; let irData = null; // make sure irFunc isnt broken try { irData = irFunc(this, block); } catch (err) { log.warn(extensionId + '_' + blockId, 'failed to create IR data;', err); } if (irData) { // check if it is this type, we dont want to descend an input as a stack if (irData.kind === 'stack') { // set proper kind irData.kind = extensionId + '.' + blockId; return irData; } } } switch (block.opcode) { case 'your_mom': return { kind: 'your mom' }; case 'control_switch': return { kind: 'control.switch', test: this.descendInputOfBlock(block, 'CONDITION'), conditions: this.descendSubstack(block, 'SUBSTACK'), default: [] }; case 'control_switch_default': return { kind: 'control.switch', test: this.descendInputOfBlock(block, 'CONDITION'), conditions: this.descendSubstack(block, 'SUBSTACK1'), default: this.descendSubstack(block, 'SUBSTACK2') }; case 'control_case_next': return { kind: 'control.case', condition: this.descendInputOfBlock(block, 'CONDITION'), code: this.descendSubstack(block, 'SUBSTACK'), runsNext: true }; case 'control_case': return { kind: 'control.case', condition: this.descendInputOfBlock(block, 'CONDITION'), code: this.descendSubstack(block, 'SUBSTACK'), runsNext: false }; case 'control_exitCase': return { kind: 'control.exitCase' }; case 'control_exitLoop': return { kind: 'control.exitLoop' }; case 'control_continueLoop': return { kind: 'control.continueLoop' }; case 'control_all_at_once': // In Scratch 3, this block behaves like "if 1 = 1" // WE ARE IN PM NOW IT BEHAVES PROPERLY LESS GO return { kind: 'control.allAtOnce', condition: { kind: 'constant', value: true }, code: this.descendSubstack(block, 'SUBSTACK') }; case 'control_clear_counter': return { kind: 'counter.clear' }; case 'control_create_clone_of': return { kind: 'control.createClone', target: this.descendInputOfBlock(block, 'CLONE_OPTION') }; case 'control_delete_this_clone': this.script.yields = true; return { kind: 'control.deleteClone' }; case 'control_forever': this.analyzeLoop(); return { kind: 'control.while', condition: { kind: 'constant', value: true }, do: this.descendSubstack(block, 'SUBSTACK') }; case 'control_for_each': this.analyzeLoop(); return { kind: 'control.for', variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE), count: this.descendInputOfBlock(block, 'VALUE'), do: this.descendSubstack(block, 'SUBSTACK') }; case 'control_if': return { kind: 'control.if', condition: this.descendInputOfBlock(block, 'CONDITION'), whenTrue: this.descendSubstack(block, 'SUBSTACK'), whenFalse: [] }; case 'control_if_else': return { kind: 'control.if', condition: this.descendInputOfBlock(block, 'CONDITION'), whenTrue: this.descendSubstack(block, 'SUBSTACK'), whenFalse: this.descendSubstack(block, 'SUBSTACK2') }; case 'control_try_catch': return { kind: 'control.trycatch', try: this.descendSubstack(block, 'SUBSTACK'), catch: this.descendSubstack(block, 'SUBSTACK2') }; case 'control_throw_error': return { kind: 'control.throwError', error: this.descendInputOfBlock(block, 'ERROR'), }; case 'control_incr_counter': return { kind: 'counter.increment' }; case 'control_decr_counter': return { kind: 'counter.decrement' }; case 'control_set_counter': return { kind: 'counter.set', value: this.descendInputOfBlock(block, 'VALUE') }; case 'control_repeat': this.analyzeLoop(); return { kind: 'control.repeat', times: this.descendInputOfBlock(block, 'TIMES'), do: this.descendSubstack(block, 'SUBSTACK') }; case 'control_repeatForSeconds': this.analyzeLoop(); return { kind: 'control.repeatForSeconds', times: this.descendInputOfBlock(block, 'TIMES'), do: this.descendSubstack(block, 'SUBSTACK') }; case 'control_repeat_until': { this.analyzeLoop(); // Dirty hack: automatically enable warp timer for this block if it uses timer // This fixes project that do things like "repeat until timer > 0.5" this.usesTimer = false; const condition = this.descendInputOfBlock(block, 'CONDITION'); const needsWarpTimer = this.usesTimer; if (needsWarpTimer) { this.script.yields = true; } return { kind: 'control.while', condition: { kind: 'op.not', operand: condition }, do: this.descendSubstack(block, 'SUBSTACK'), warpTimer: needsWarpTimer }; } case 'control_stop': { const level = block.fields.STOP_OPTION.value; if (level === 'all') { this.script.yields = true; return { kind: 'control.stopAll' }; } else if (level === 'other scripts in sprite' || level === 'other scripts in stage') { return { kind: 'control.stopOthers' }; } else if (level === 'this script') { return { kind: 'control.stopScript' }; } return { kind: 'noop' }; } case 'control_wait': this.script.yields = true; return { kind: 'control.wait', seconds: this.descendInputOfBlock(block, 'DURATION') }; case 'control_waittick': this.script.yields = true; return { kind: 'control.waitTick' }; case 'control_wait_until': this.script.yields = true; return { kind: 'control.waitUntil', condition: this.descendInputOfBlock(block, 'CONDITION') }; case 'control_waitsecondsoruntil': this.script.yields = true; return { kind: 'control.waitOrUntil', seconds: this.descendInputOfBlock(block, 'DURATION'), condition: this.descendInputOfBlock(block, 'CONDITION') }; case 'control_while': this.analyzeLoop(); return { kind: 'control.while', condition: this.descendInputOfBlock(block, 'CONDITION'), do: this.descendSubstack(block, 'SUBSTACK'), // We should consider analyzing this like we do for control_repeat_until warpTimer: false }; case 'control_run_as_sprite': return { kind: 'control.runAsSprite', sprite: this.descendInputOfBlock(block, 'RUN_AS_OPTION'), substack: this.descendSubstack(block, 'SUBSTACK') }; case 'control_new_script': return { kind: 'control.newScript', substack: this.descendSubstack(block, 'SUBSTACK') }; case 'data_addtolist': return { kind: 'list.add', list: this.descendVariable(block, 'LIST', LIST_TYPE), item: this.descendInputOfBlock(block, 'ITEM') }; case 'data_changevariableby': { const variable = this.descendVariable(block, 'VARIABLE', SCALAR_TYPE); return { kind: 'var.set', variable, value: { kind: 'op.add', left: { kind: 'var.get', variable }, right: this.descendInputOfBlock(block, 'VALUE') } }; } case 'data_deletealloflist': return { kind: 'list.deleteAll', list: this.descendVariable(block, 'LIST', LIST_TYPE) }; case 'data_listforeachnum': this.analyzeLoop(); return { kind: 'list.forEach', num: true, list: this.descendVariable(block, 'LIST', LIST_TYPE), variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE), do: this.descendSubstack(block, 'SUBSTACK') }; case 'data_listforeachitem': this.analyzeLoop(); return { kind: 'list.forEach', num: false, list: this.descendVariable(block, 'LIST', LIST_TYPE), variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE), do: this.descendSubstack(block, 'SUBSTACK') }; case 'data_deleteoflist': { const index = this.descendInputOfBlock(block, 'INDEX'); if (index.kind === 'constant' && index.value === 'all') { return { kind: 'list.deleteAll', list: this.descendVariable(block, 'LIST', LIST_TYPE) }; } return { kind: 'list.delete', list: this.descendVariable(block, 'LIST', LIST_TYPE), index: index }; } case 'data_shiftlist': { return { kind: 'list.shift', list: this.descendVariable(block, 'LIST', LIST_TYPE), index: this.descendInputOfBlock(block, 'INDEX') }; } case 'data_hidelist': return { kind: 'list.hide', list: this.descendVariable(block, 'LIST', LIST_TYPE) }; case 'data_hidevariable': return { kind: 'var.hide', variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE) }; case 'data_insertatlist': return { kind: 'list.insert', list: this.descendVariable(block, 'LIST', LIST_TYPE), index: this.descendInputOfBlock(block, 'INDEX'), item: this.descendInputOfBlock(block, 'ITEM') }; case 'data_replaceitemoflist': return { kind: 'list.replace', list: this.descendVariable(block, 'LIST', LIST_TYPE), index: this.descendInputOfBlock(block, 'INDEX'), item: this.descendInputOfBlock(block, 'ITEM') }; case 'data_setvariableto': return { kind: 'var.set', variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE), value: this.descendInputOfBlock(block, 'VALUE') }; case 'data_showlist': return { kind: 'list.show', list: this.descendVariable(block, 'LIST', LIST_TYPE) }; case 'data_showvariable': return { kind: 'var.show', variable: this.descendVariable(block, 'VARIABLE', SCALAR_TYPE) }; case 'data_filterlist': return { kind: 'list.filter', list: this.descendVariable(block, 'LIST', LIST_TYPE), bool: this.descendInputOfBlock(block, 'BOOL') }; case 'event_broadcast': return { kind: 'event.broadcast', broadcast: this.descendInputOfBlock(block, 'BROADCAST_INPUT') }; case 'event_broadcastandwait': this.script.yields = true; return { kind: 'event.broadcastAndWait', broadcast: this.descendInputOfBlock(block, 'BROADCAST_INPUT') }; case 'looks_changeeffectby': return { kind: 'looks.changeEffect', effect: block.fields.EFFECT.value.toLowerCase(), value: this.descendInputOfBlock(block, 'CHANGE') }; case 'looks_changesizeby': return { kind: 'looks.changeSize', size: this.descendInputOfBlock(block, 'CHANGE') }; case 'looks_cleargraphiceffects': return { kind: 'looks.clearEffects' }; case 'looks_goforwardbackwardlayers': if (block.fields.FORWARD_BACKWARD.value === 'forward') { return { kind: 'looks.forwardLayers', layers: this.descendInputOfBlock(block, 'NUM') }; } return { kind: 'looks.backwardLayers', layers: this.descendInputOfBlock(block, 'NUM') }; case 'looks_goTargetLayer': if (block.fields.FORWARD_BACKWARD.value === 'infront') { return { kind: 'looks.targetFront', layers: this.descendInputOfBlock(block, 'VISIBLE_OPTION') }; } return { kind: 'looks.targetBack', layers: this.descendInputOfBlock(block, 'VISIBLE_OPTION') }; case 'looks_gotofrontback': if (block.fields.FRONT_BACK.value === 'front') { return { kind: 'looks.goToFront' }; } return { kind: 'looks.goToBack' }; case 'looks_hide': return { kind: 'looks.hide' }; case 'looks_nextbackdrop': return { kind: 'looks.nextBackdrop' }; case 'looks_nextcostume': return { kind: 'looks.nextCostume' }; case 'looks_seteffectto': return { kind: 'looks.setEffect', effect: block.fields.EFFECT.value.toLowerCase(), value: this.descendInputOfBlock(block, 'VALUE') }; case 'looks_setsizeto': return { kind: 'looks.setSize', size: this.descendInputOfBlock(block, 'SIZE') }; case "looks_setFont": return { kind: 'looks.setFont', font: this.descendInputOfBlock(block, 'font'), size: this.descendInputOfBlock(block, 'size') }; case "looks_setColor": return { kind: 'looks.setColor', prop: block.fields.prop.value, color: this.descendInputOfBlock(block, 'color') }; case "looks_setTintColor": return { kind: 'looks.setTintColor', color: this.descendInputOfBlock(block, 'color') }; case "looks_setShape": return { kind: 'looks.setShape', prop: block.fields.prop.value, value: this.descendInputOfBlock(block, 'color') }; case 'looks_show': return { kind: 'looks.show' }; case 'looks_switchbackdropto': return { kind: 'looks.switchBackdrop', backdrop: this.descendInputOfBlock(block, 'BACKDROP') }; case 'looks_switchcostumeto': return { kind: 'looks.switchCostume', costume: this.descendInputOfBlock(block, 'COSTUME') }; case 'motion_changexby': return { kind: 'motion.changeX', dx: this.descendInputOfBlock(block, 'DX') }; case 'motion_changeyby': return { kind: 'motion.changeY', dy: this.descendInputOfBlock(block, 'DY') }; case 'motion_gotoxy': return { kind: 'motion.setXY', x: this.descendInputOfBlock(block, 'X'), y: this.descendInputOfBlock(block, 'Y') }; case 'motion_ifonedgebounce': return { kind: 'motion.ifOnEdgeBounce' }; case 'motion_movesteps': return { kind: 'motion.step', steps: this.descendInputOfBlock(block, 'STEPS') }; case 'motion_pointindirection': return { kind: 'motion.setDirection', direction: this.descendInputOfBlock(block, 'DIRECTION') }; case 'motion_setrotationstyle': return { kind: 'motion.setRotationStyle', style: block.fields.STYLE.value }; case 'motion_setx': return { kind: 'motion.setX', x: this.descendInputOfBlock(block, 'X') }; case 'motion_sety': return { kind: 'motion.setY', y: this.descendInputOfBlock(block, 'Y') }; case 'motion_turnleft': return { kind: 'motion.setDirection', direction: { kind: 'op.subtract', left: { kind: 'motion.direction' }, right: this.descendInputOfBlock(block, 'DEGREES') } }; case 'motion_turnright': return { kind: 'motion.setDirection', direction: { kind: 'op.add', left: { kind: 'motion.direction' }, right: this.descendInputOfBlock(block, 'DEGREES') } }; case 'pen_clear': return { kind: 'pen.clear' }; case 'pen_changePenColorParamBy': return { kind: 'pen.changeParam', param: this.descendInputOfBlock(block, 'COLOR_PARAM'), value: this.descendInputOfBlock(block, 'VALUE') }; case 'pen_changePenHueBy': return { kind: 'pen.legacyChangeHue', hue: this.descendInputOfBlock(block, 'HUE') }; case 'pen_changePenShadeBy': return { kind: 'pen.legacyChangeShade', shade: this.descendInputOfBlock(block, 'SHADE') }; case 'pen_penDown': return { kind: 'pen.down' }; case 'pen_penUp': return { kind: 'pen.up' }; case 'pen_setPenColorParamTo': return { kind: 'pen.setParam', param: this.descendInputOfBlock(block, 'COLOR_PARAM'), value: this.descendInputOfBlock(block, 'VALUE') }; case 'pen_setPenColorToColor': return { kind: 'pen.setColor', color: this.descendInputOfBlock(block, 'COLOR') }; case 'pen_setPenHueToNumber': return { kind: 'pen.legacySetHue', hue: this.descendInputOfBlock(block, 'HUE') }; case 'pen_setPenShadeToNumber': return { kind: 'pen.legacySetShade', shade: this.descendInputOfBlock(block, 'SHADE') }; case 'pen_setPenSizeTo': return { kind: 'pen.setSize', size: this.descendInputOfBlock(block, 'SIZE') }; case 'pen_changePenSizeBy': return { kind: 'pen.changeSize', size: this.descendInputOfBlock(block, 'SIZE') }; case 'pen_stamp': return { kind: 'pen.stamp' }; case 'procedures_return': return { kind: 'procedures.return', return: this.descendInputOfBlock(block, 'return') }; case 'procedures_set': return { kind: 'procedures.set', param: this.descendInputOfBlock(block, "PARAM"), val: this.descendInputOfBlock(block, "VALUE") }; case 'procedures_call': { // setting of yields will be handled later in the analysis phase // patches output previewing if (block.mutation.returns === 'true') { const Block = Clone.simple(block); Block.opcode = 'procedures_call_return'; return this.descendStackedBlock(Block); } const procedureCode = block.mutation.proccode; if (procedureCode === 'tw:debugger;') { return { kind: 'tw.debugger' }; } const paramNamesIdsAndDefaults = this.blocks.getProcedureParamNamesIdsAndDefaults(procedureCode); if (paramNamesIdsAndDefaults === null) { return { kind: 'noop' }; } const [paramNames, paramIds, paramDefaults] = paramNamesIdsAndDefaults; const addonBlock = this.runtime.getAddonBlock(procedureCode); if (addonBlock) { this.script.yields = true; const args = {}; for (let i = 0; i < paramIds.length; i++) { let value; if (block.inputs[paramIds[i]] && block.inputs[paramIds[i]].block) { value = this.descendInputOfBlock(block, paramIds[i]); } else { value = { kind: 'constant', value: paramDefaults[i] }; } args[paramNames[i]] = value; } return { kind: 'addons.call', code: procedureCode, arguments: args, blockId: block.id }; } const definitionId = this.blocks.getProcedureDefinition(procedureCode); const definitionBlock = this.blocks.getBlock(definitionId); if (!definitionBlock) { return { kind: 'noop' }; } const innerDefinition = this.blocks.getBlock(definitionBlock.inputs.custom_block.block); let isWarp = this.script.isWarp; if (!isWarp) { if (innerDefinition && innerDefinition.mutation) { const warp = innerDefinition.mutation.warp; if (typeof warp === 'boolean') { isWarp = warp; } else if (typeof warp === 'string') { isWarp = JSON.parse(warp); } } } const variant = generateProcedureVariant(procedureCode, isWarp); if (!this.script.dependedProcedures.includes(variant)) { this.script.dependedProcedures.push(variant); } // Non-warp direct recursion yields. if (!this.script.isWarp) { if (procedureCode === this.script.procedureCode) { this.script.yields = true; } } const args = []; for (let i = 0; i < paramIds.length; i++) { let value; if (block.inputs[paramIds[i]] && block.inputs[paramIds[i]].block) { if (paramIds[i].startsWith("SUBSTACK")) { value = this.descendSubstack(block, paramIds[i]) } else { value = this.descendInputOfBlock(block, paramIds[i]); } } else { value = { kind: 'constant', value: paramDefaults[i] }; } args.push(value); } return { kind: 'procedures.call', code: procedureCode, variant, returns: false, arguments: args, type: JSON.parse(block.mutation.optype || '"statement"') }; } case 'sensing_set_of': return { kind: 'sensing.set.of', property: block.fields.PROPERTY.value, object: this.descendInputOfBlock(block, 'OBJECT'), value: this.descendInputOfBlock(block, 'VALUE') }; case 'sensing_resettimer': return { kind: 'timer.reset' }; /* can someone set up the jsgen for these, i dont want to rn case "sensing_regextest": return { kind: "sensing.regextest", regex: this.descendInputOfBlock(block, 'reg'), text: this.descendInputOfBlock(block, 'text') } case "sensing_thing_is_number": return { kind: "sensing.thing.is.number", text: this.descendInputOfBlock(block, 'TEXT1') } case "sensing_mobile": return { kind: "sensing.mobile", } case "sensing_thing_is_text": return { kind: "sensing.thing.is.text", text: this.descendInputOfBlock(block, 'TEXT1') } case "sensing_getspritewithattrib": return { kind: "sensing.getspritewithattrib", variable: this.descendInputOfBlock(block, 'var'), value: this.descendInputOfBlock(block, 'val') } case "operator_regexmatch": return { kind: "operator.regexmatch", regex: this.descendInputOfBlock(block, 'reg'), text: this.descendInputOfBlock(block, 'text') } case "operator_replaceAll": return { kind: "operator.replaceAll", text: this.descendInputOfBlock(block, 'term'), with: this.descendInputOfBlock(block, 'res'), in: this.descendInputOfBlock(block, 'text') } case "operator_getLettersFromIndexToIndexInTextFixed": case "operator_getLettersFromIndexToIndexInText": return { kind: "operator.getLettersFromIndexToIndexInText", from: this.descendInputOfBlock(block, 'INDEX1'), to: this.descendInputOfBlock(block, 'INDEX2'), ammount: this.descendInputOfBlock(block, 'TEXT') } case "operator_readLineInMultilineText": return { kind: "operator.readLineInMultilineText", line: this.descendInputOfBlock(block, 'LINE'), text: this.descendInputOfBlock(block, 'TEXT') } case "operator_newLine": return { kind: "operator.newLine", } case "operator_stringify": return { kind: "operator.stringify", pass: this.descendInputOfBlock(block, 'ONE') } case "operator_lerpFunc": return { kind: "operator.lerpFunc", from: this.descendInputOfBlock(block, 'ONE'), to: this.descendInputOfBlock(block, 'TWO'), ammount: this.descendInputOfBlock(block, 'AMOUNT') } case "operator_advMath": return { kind: "operator.advMath", num1: this.descendInputOfBlock(block, 'ONE'), num2: this.descendInputOfBlock(block, 'TWO'), op: block.fields.OPTION.value } case "operator_constrainnumber": return { kind: "operator.constrainnumber", number: this.descendInputOfBlock(block, 'inp'), min: this.descendInputOfBlock(block, 'min'), max: this.descendInputOfBlock(block, 'max') } case "operator_trueBoolean": return { kind: "operator.trueBoolean", } case "operator_falseBoolean": return { kind: "operator.falseBoolean", } case "operator_randomBoolean": return { kind: "operator.randomBoolean", } case "operator_indexOfTextInText": return { kind: "operator.indexOfTextInText", check: this.descendInputOfBlock(block, 'TEXT1'), text: this.descendInputOfBlock(block, 'TEXT2') } case "event_whenanything": return { kind: "event.whenanything", } case "event_always": return { kind: "event.always", event: this.descendInputOfBlock(block, 'ANYTHING') } case "control_backToGreenFlag": return { kind: "control.backToGreenFlag", } case "control_if_return_else_return": return { kind: "control.if.return.else.return", if: this.descendInputOfBlock(block, 'boolean'), true: this.descendInputOfBlock(block, 'TEXT1'), false: this.descendInputOfBlock(block, 'TEXT2'), } all the names so you dont have to get them sensing.regextest sensing.thing.is.number sensing.mobile sensing.thing.is.text sensing.getspritewithattrib operator.regexmatch operator.replaceAll operator.getLettersFromIndexToIndexInText operator.readLineInMultilineText operator.newLine operator.stringify operator.lerpFunc operator.advMath operator.constrainnumber operator.trueBoolean operator.falseBoolean operator.randomBoolean operator.indexOfTextInText event.whenanything event.always control.backToGreenFlag control.if.return.else.return */ case 'lmsTempVars2_setRuntimeVariable': return { kind: 'tempVars.set', var: this.descendInputOfBlock(block, 'VAR'), val: this.descendInputOfBlock(block, 'STRING'), runtime: true }; case 'lmsTempVars2_setThreadVariable': return { kind: 'tempVars.set', var: this.descendInputOfBlock(block, 'VAR'), val: this.descendInputOfBlock(block, 'STRING'), thread: true }; case 'tempVars_setVariable': return { kind: 'tempVars.set', var: this.descendInputOfBlock(block, 'name'), val: this.descendInputOfBlock(block, 'value') }; case 'lmsTempVars2_changeRuntimeVariable': const name = this.descendInputOfBlock(block, 'VAR'); return { kind: 'tempVars.set', var: name, val: { kind: 'op.add', left: { kind: 'tempVars.get', var: name, runtime: true }, right: this.descendInputOfBlock(block, 'NUM') }, runtime: true }; case 'lmsTempVars2_changeThreadVariable': { const name = this.descendInputOfBlock(block, 'VAR'); return { kind: 'tempVars.set', var: name, val: { kind: 'op.add', left: { kind: 'tempVars.get', var: name, thread: true }, right: this.descendInputOfBlock(block, 'NUM') }, thread: true }; } case 'tempVars_changeVariable': { const name = this.descendInputOfBlock(block, 'name'); return { kind: 'tempVars.set', var: name, val: { kind: 'op.add', left: { kind: 'tempVars.get', var: name }, right: this.descendInputOfBlock(block, 'value') } }; } case 'lmsTempVars2_deleteRuntimeVariable': return { kind: 'tempVars.delete', var: this.descendInputOfBlock(block, 'VAR'), runtime: true }; case 'tempVars_deleteVariable': return { kind: 'tempVars.delete', var: this.descendInputOfBlock(block, 'name') }; case 'lmsTempVars2_deleteAllRuntimeVariables': return { kind: 'tempVars.deleteAll', runtime: true }; case 'tempVars_deleteAllVariables': return { kind: 'tempVars.deleteAll' }; case 'lmsTempVars2_forEachThreadVariable': return { kind: 'tempVars.forEach', var: this.descendInputOfBlock(block, 'VAR'), loops: this.descendInputOfBlock(block, 'NUM'), do: this.descendSubstack(block, 'SUBSTACK'), thread: true }; case 'tempVars_forEachTempVar': this.analyzeLoop(); return { kind: 'tempVars.forEach', var: this.descendInputOfBlock(block, 'NAME'), loops: this.descendInputOfBlock(block, 'REPEAT'), do: this.descendSubstack(block, 'SUBSTACK') }; case 'control_dualblock': return { kind: 'control.dualBlock' }; default: { const opcodeFunction = this.runtime.getOpcodeFunction(block.opcode); if (opcodeFunction) { // It might be a non-compiled primitive from a standard category if (compatBlocks.statementBlocks.includes(block.opcode)) { return this.descendCompatLayer(block); } // It might be an extension block. const blockInfo = this.getBlockInfo(block.opcode); if (blockInfo) { const type = blockInfo.info.blockType; const args = this.descendCompatLayer(block, blockInfo.info); args.block = block; if (block.mutation) args.mutation = block.mutation; if (type === BlockType.COMMAND || type === BlockType.CONDITIONAL || type === BlockType.LOOP) { return args; } } } // When this thread was triggered by a stack click, attempt to compile as an input. // TODO: perhaps this should be moved to generate()? if (this.thread.stackClick) { try { const inputNode = this.descendInput(block); return { kind: 'visualReport', input: inputNode }; } catch (e) { // Ignore } } log.warn(`IR: Unknown stacked block: ${block.opcode}`, block); throw new Error(`IR: Unknown stacked block: ${block.opcode}`); } } } /** * Descend into a stack of blocks (eg. the blocks contained within an "if" block) * @param {*} parentBlock The parent Scratch block that contains the stack to parse. * @param {*} substackName The name of the stack to descend into. * @private * @returns {Node[]} List of stacked block nodes. */ descendSubstack (parentBlock, substackName) { const input = parentBlock.inputs[substackName]; if (!input) { return []; } const stackId = input.block; return this.walkStack(stackId); } /** * Descend into and walk the siblings of a stack. * @param {string} startingBlockId The ID of the first block of a stack. * @private * @returns {Node[]} List of stacked block nodes. */ walkStack (startingBlockId) { const result = []; let blockId = startingBlockId; while (blockId !== null) { const block = this.getBlockById(blockId); if (!block) { break; } const node = this.descendStackedBlock(block); result.push(node); blockId = block.next; } return result; } /** * Descend into a variable. * @param {*} block The block that has the variable. * @param {string} fieldName The name of the field that the variable is stored in. * @param {''|'list'} type Variable type, '' for scalar and 'list' for list. * @private * @returns {*} A parsed variable object. */ 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._descendVariable(id, variable.value, type); this.variableCache[id] = data; return data; } /** * @param {string} id The ID of the variable. * @param {string} name The name of the variable. * @param {''|'list'} type The variable type. * @private * @returns {*} A parsed variable object. */ _descendVariable (id, name, type) { const target = this.target; const stage = this.stage; // Look for by ID in target... if (target.variables.hasOwnProperty(id)) { return createVariableData('target', target.variables[id]); } // Look for by ID in stage... if (!target.isStage) { if (stage && stage.variables.hasOwnProperty(id)) { return createVariableData('stage', stage.variables[id]); } } // Look for by name and type in target... for (const varId in target.variables) { if (target.variables.hasOwnProperty(varId)) { const currVar = target.variables[varId]; if (currVar.name === name && currVar.type === type) { return createVariableData('target', currVar); } } } // Look for by name and type in stage... if (!target.isStage && stage) { for (const varId in stage.variables) { if (stage.variables.hasOwnProperty(varId)) { const currVar = stage.variables[varId]; if (currVar.name === name && currVar.type === type) { return createVariableData('stage', currVar); } } } } // Create it locally... const newVariable = this.runtime.newVariableInstance(type, id, name, false); target.variables[id] = newVariable; if (target.sprite) { // Create the variable in all instances of this sprite. // This is necessary because the script cache is shared between clones. // sprite.clones has all instances of this sprite including the original and all clones for (const clone of target.sprite.clones) { if (!clone.variables.hasOwnProperty(id)) { clone.variables[id] = this.runtime.newVariableInstance(type, id, name, false); } } } return createVariableData('target', newVariable); } /** * Descend into a block that uses the compatibility layer. * @param {*} block The block to use the compatibility layer for. * @private * @returns {Node} The parsed node. */ descendCompatLayer (block, blockInfo) { this.script.yields = true; if (!blockInfo) { blockInfo = this.getBlockInfo(block.opcode); blockInfo = blockInfo ? blockInfo.info : null; } const inputs = {}; for (const name of Object.keys(block.inputs)) { if (!name.startsWith('SUBSTACK')) { inputs[name] = this.descendInputOfBlock(block, name); } } const fields = {}; const substacks = []; const blockType = (blockInfo && blockInfo.blockType) || BlockType.COMMAND; if (blockType === BlockType.CONDITIONAL || blockType === BlockType.LOOP) { for (let i in (blockInfo.branches || [])) { const inputName = i === "0" ? 'SUBSTACK' : `SUBSTACK${Number(i) + 1}`; substacks.push(this.descendSubstack(block, inputName)); } } for (const name of Object.keys(block.fields)) { const type = block.fields[name].variableType; if (typeof type !== 'undefined') { const data = this.descendVariable(block, name, type); fields[name] = data; continue; } fields[name] = block.fields[name].value; } return { kind: 'compat', id: block.id, opcode: block.opcode, blockType, inputs, fields, substacks }; } analyzeLoop () { if (!this.script.isWarp || this.script.warpTimer) { this.script.yields = true; } } readTopBlockComment (commentId) { const comment = this.target.comments[commentId]; if (!comment) { // can't find the comment // this is safe to ignore return; } const text = comment.text; for (const line of text.split('\n')) { if (!/^tw\b/.test(line)) { continue; } const flags = line.split(' '); for (const flag of flags) { switch (flag) { case 'nocompile': throw new Error('Script explicitly disables compilation'); case 'stuck': this.script.warpTimer = true; break; } } // Only the first 'tw' line is parsed. break; } } /** * @param {Block} hatBlock */ walkHat(hatBlock) { const nextBlock = hatBlock.next; const opcode = hatBlock.opcode; const hatInfo = this.runtime._hats[opcode]; if (this.thread.stackClick) { // We still need to treat the hat as a normal block (so executableHat should be false) for // interpreter parity, but the reuslt is ignored. const opcodeFunction = this.runtime.getOpcodeFunction(opcode); if (opcodeFunction) { return [ this.descendCompatLayer(hatBlock), ...this.walkStack(nextBlock) ]; } return this.walkStack(nextBlock); } if (hatInfo.edgeActivated) { // Edge-activated HAT this.script.yields = true; this.script.executableHat = true; return [ { kind: 'hat.edge', id: hatBlock.id, condition: this.descendCompatLayer(hatBlock) }, ...this.walkStack(nextBlock) ]; } const opcodeFunction = this.runtime.getOpcodeFunction(opcode); if (opcodeFunction) { // Predicate-based HAT this.script.yields = true; this.script.executableHat = true; return [ { kind: 'hat.predicate', condition: this.descendCompatLayer(hatBlock) }, ...this.walkStack(nextBlock) ]; } return this.walkStack(nextBlock); } /** * @param {string} topBlockId The ID of the top block of the script. * @returns {IntermediateScript} */ generate (topBlockId) { this.blocks.populateProcedureCache(); this.script.topBlockId = topBlockId; const topBlock = this.getBlockById(topBlockId); if (!topBlock) { if (this.script.isProcedure) { // Empty procedure return this.script; } throw new Error('Cannot find top block'); } if (topBlock.comment) { this.readTopBlockComment(topBlock.comment); } // We do need to evaluate empty hats const hatInfo = this.runtime._hats[topBlock.opcode]; const isHat = !!hatInfo; if (isHat) { this.script.stack = this.walkHat(topBlock); } else { // We don't evaluate the procedures_definition top block as it never does anything // We also don't want it to be treated like a hat block let entryBlock; if ( topBlock.opcode === 'procedures_definition' || topBlock.opcode === 'procedures_definition_return' ) { entryBlock = topBlock.next; } else { entryBlock = topBlockId; } if (entryBlock) { this.script.stack = this.walkStack(entryBlock); } } return this.script; } } class IRGenerator { constructor (thread) { this.thread = thread; this.blocks = thread.blockContainer; this.proceduresToCompile = new Map(); this.compilingProcedures = new Map(); /** @type {Object.} */ this.procedures = {}; this.analyzedProcedures = []; } static _extensionIRInfo = {}; static setExtensionIr(id, data) { IRGenerator._extensionIRInfo[id] = data; } static hasExtensionIr(id) { return Boolean(IRGenerator._extensionIRInfo[id]); } static getExtensionIr(id) { return IRGenerator._extensionIRInfo[id]; } addProcedureDependencies (dependencies) { for (const procedureVariant of dependencies) { if (this.procedures.hasOwnProperty(procedureVariant)) { continue; } if (this.compilingProcedures.has(procedureVariant)) { continue; } if (this.proceduresToCompile.has(procedureVariant)) { continue; } const procedureCode = parseProcedureCode(procedureVariant); const definition = this.blocks.getProcedureDefinition(procedureCode); this.proceduresToCompile.set(procedureVariant, definition); } } /** * @param {ScriptTreeGenerator} generator The generator to run. * @param {string} topBlockId The ID of the top block in the stack. * @returns {IntermediateScript} Intermediate script. */ generateScriptTree (generator, topBlockId) { const result = generator.generate(topBlockId); this.addProcedureDependencies(result.dependedProcedures); return result; } /** * Recursively analyze a script and its dependencies. * @param {IntermediateScript} script Intermediate script. */ analyzeScript (script) { let madeChanges = false; for (const procedureCode of script.dependedProcedures) { const procedureData = this.procedures[procedureCode]; // Analyze newly found procedures. if (!this.analyzedProcedures.includes(procedureCode)) { this.analyzedProcedures.push(procedureCode); if (this.analyzeScript(procedureData)) { madeChanges = true; } this.analyzedProcedures.pop(); } // If a procedure used by a script may yield, the script itself may yield. if (procedureData.yields && !script.yields) { script.yields = true; madeChanges = true; } } return madeChanges; } /** * @returns {IntermediateRepresentation} Intermediate representation. */ generate () { const entry = this.generateScriptTree(new ScriptTreeGenerator(this.thread), this.thread.topBlock); // Compile any required procedures. // As procedures can depend on other procedures, this process may take several iterations. const procedureTreeCache = this.blocks._cache.compiledProcedures; while (this.proceduresToCompile.size > 0) { this.compilingProcedures = this.proceduresToCompile; this.proceduresToCompile = new Map(); for (const [procedureVariant, definitionId] of this.compilingProcedures.entries()) { if (procedureTreeCache[procedureVariant]) { const result = procedureTreeCache[procedureVariant]; this.procedures[procedureVariant] = result; this.addProcedureDependencies(result.dependedProcedures); } else { const isWarp = parseIsWarp(procedureVariant); const generator = new ScriptTreeGenerator(this.thread); generator.setProcedureVariant(procedureVariant); if (isWarp) generator.enableWarp(); const compiledProcedure = this.generateScriptTree(generator, definitionId); this.procedures[procedureVariant] = compiledProcedure; procedureTreeCache[procedureVariant] = compiledProcedure; } } } // Analyze scripts until no changes are made. while (this.analyzeScript(entry)); const ir = new IntermediateRepresentation(); ir.entry = entry; ir.procedures = this.procedures; return ir; } static exports = { ScriptTreeGenerator } } module.exports = IRGenerator;