Spaces:
Running
Running
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.<string, *>} 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.<string, 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.<string, IntermediateScript>} */ | |
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; | |