Skip to content

Commit

Permalink
Merge pull request #150 from TurboWarp/return
Browse files Browse the repository at this point in the history
Allow custom blocks to return values - VM part
  • Loading branch information
GarboMuffin authored Jul 29, 2023
2 parents edae502 + 8453a47 commit fbc62ba
Show file tree
Hide file tree
Showing 103 changed files with 1,694 additions and 391 deletions.
96 changes: 64 additions & 32 deletions src/blocks/scratch3_procedures.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Scratch3ProcedureBlocks {
return {
procedures_definition: this.definition,
procedures_call: this.call,
procedures_return: this.return,
argument_reporter_string_number: this.argumentReporterStringNumber,
argument_reporter_boolean: this.argumentReporterBoolean
};
Expand All @@ -25,45 +26,76 @@ class Scratch3ProcedureBlocks {
}

call (args, util) {
if (!util.stackFrame.executed) {
const procedureCode = args.mutation.proccode;
const paramNamesIdsAndDefaults = util.getProcedureParamNamesIdsAndDefaults(procedureCode);

// If null, procedure could not be found, which can happen if custom
// block is dragged between sprites without the definition.
// Match Scratch 2.0 behavior and noop.
if (paramNamesIdsAndDefaults === null) {
return;
const stackFrame = util.stackFrame;
const isReporter = !!args.mutation.return;

if (stackFrame.executed) {
if (isReporter) {
const returnValue = stackFrame.returnValue;
// This stackframe will be reused for other reporters in this block, so clean it up for them.
// Can't use reset() because that will reset too much.
const threadStackFrame = util.thread.peekStackFrame();
threadStackFrame.params = null;
threadStackFrame.executionContext = null;
return returnValue;
}
return;
}

const [paramNames, paramIds, paramDefaults] = paramNamesIdsAndDefaults;

// Initialize params for the current stackFrame to {}, even if the procedure does
// not take any arguments. This is so that `getParam` down the line does not look
// at earlier stack frames for the values of a given parameter (#1729)
util.initParams();
for (let i = 0; i < paramIds.length; i++) {
if (args.hasOwnProperty(paramIds[i])) {
util.pushParam(paramNames[i], args[paramIds[i]]);
} else {
util.pushParam(paramNames[i], paramDefaults[i]);
}
const procedureCode = args.mutation.proccode;
const paramNamesIdsAndDefaults = util.getProcedureParamNamesIdsAndDefaults(procedureCode);

// If null, procedure could not be found, which can happen if custom
// block is dragged between sprites without the definition.
// Match Scratch 2.0 behavior and noop.
if (paramNamesIdsAndDefaults === null) {
if (isReporter) {
return '';
}
return;
}

const addonBlock = util.runtime.getAddonBlock(procedureCode);
if (addonBlock) {
const result = addonBlock.callback(util.thread.getAllparams(), util);
if (util.thread.status === 1 /* STATUS_PROMISE_WAIT */) {
// If the addon block is using STATUS_PROMISE_WAIT to force us to sleep,
// make sure to not re-run this block when we resume.
util.stackFrame.executed = true;
}
return result;
const [paramNames, paramIds, paramDefaults] = paramNamesIdsAndDefaults;

// Initialize params for the current stackFrame to {}, even if the procedure does
// not take any arguments. This is so that `getParam` down the line does not look
// at earlier stack frames for the values of a given parameter (#1729)
util.initParams();
for (let i = 0; i < paramIds.length; i++) {
if (args.hasOwnProperty(paramIds[i])) {
util.pushParam(paramNames[i], args[paramIds[i]]);
} else {
util.pushParam(paramNames[i], paramDefaults[i]);
}
}

util.stackFrame.executed = true;
const addonBlock = util.runtime.getAddonBlock(procedureCode);
if (addonBlock) {
const result = addonBlock.callback(util.thread.getAllparams(), util);
if (util.thread.status === 1 /* STATUS_PROMISE_WAIT */) {
// If the addon block is using STATUS_PROMISE_WAIT to force us to sleep,
// make sure to not re-run this block when we resume.
stackFrame.executed = true;
}
return result;
}

stackFrame.executed = true;

if (isReporter) {
util.thread.peekStackFrame().waitingReporter = true;
// Default return value
stackFrame.returnValue = '';
}

util.startProcedure(procedureCode);
}

util.startProcedure(procedureCode);
return (args, util) {
util.stopThisScript();
// If used outside of a custom block, there may be no stackframe.
if (util.thread.peekStackFrame()) {
util.stackFrame.returnValue = args.VALUE;
}
}

Expand Down
222 changes: 122 additions & 100 deletions src/compiler/irgen.js
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,9 @@ class ScriptTreeGenerator {
right: this.descendInputOfBlock(block, 'NUM2')
};

case 'procedures_call':
return this.descendProcedure(block);

case 'sensing_answer':
return {
kind: 'sensing.answer'
Expand Down Expand Up @@ -1106,102 +1109,25 @@ class ScriptTreeGenerator {
};

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;
if (block.mutation.return) {
const visualReport = this.descendVisualReport(block);
if (visualReport) {
return visualReport;
}
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) {
if (procedureCode === 'tw:debugger;') {
return {
kind: 'noop'
kind: 'tw.debugger'
};
}
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) {
value = this.descendInputOfBlock(block, paramIds[i]);
} else {
value = {
kind: 'constant',
value: paramDefaults[i]
};
}
args.push(value);
}

return this.descendProcedure(block);
}
case 'procedures_return':
return {
kind: 'procedures.call',
code: procedureCode,
variant,
arguments: args
kind: 'procedures.return',
value: this.descendInputOfBlock(block, 'VALUE')
};
}

case 'sensing_resettimer':
return {
Expand All @@ -1225,18 +1151,9 @@ class ScriptTreeGenerator {
}
}

// 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
}
const asVisualReport = this.descendVisualReport(block);
if (asVisualReport) {
return asVisualReport;
}

log.warn(`IR: Unknown stacked block: ${block.opcode}`, block);
Expand Down Expand Up @@ -1370,6 +1287,97 @@ class ScriptTreeGenerator {
return createVariableData('target', newVariable);
}

descendProcedure (block) {
const procedureCode = block.mutation.proccode;
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) {
value = this.descendInputOfBlock(block, paramIds[i]);
} else {
value = {
kind: 'constant',
value: paramDefaults[i]
};
}
args.push(value);
}

return {
kind: 'procedures.call',
code: procedureCode,
variant,
arguments: args
};
}

/**
* Descend into a block that uses the compatibility layer.
* @param {*} block The block to use the compatibility layer for.
Expand Down Expand Up @@ -1431,6 +1439,20 @@ class ScriptTreeGenerator {
}
}

descendVisualReport (block) {
if (!this.thread.stackClick || block.next) {
return null;
}
try {
return {
kind: 'visualReport',
input: this.descendInput(block)
};
} catch (e) {
return null;
}
}

/**
* @param {string} topBlockId The ID of the top block of the script.
* @returns {IntermediateScript}
Expand Down
Loading

0 comments on commit fbc62ba

Please sign in to comment.