soiz1's picture
Upload 811 files
30c32c8 verified
raw
history blame
87.5 kB
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;