diff --git a/block-lexical-variables/src/blocks/procedures.js b/block-lexical-variables/src/blocks/procedures.js
index 765cad7..bae5202 100644
--- a/block-lexical-variables/src/blocks/procedures.js
+++ b/block-lexical-variables/src/blocks/procedures.js
@@ -166,6 +166,8 @@ Blockly.Blocks['procedures_defnoreturn'] = {
}
const procName = this.getFieldValue('NAME');
+ const stackInput = this.getInput('STACK');
+ const returnInput = this.getInput('RETURN');
// remove first input
// console.log("updateParams_: remove input HEADER");
@@ -182,7 +184,7 @@ Blockly.Blocks['procedures_defnoreturn'] = {
// necessary)
// Only args and body are left
- const oldArgCount = this.inputList.length - 1;
+ const oldArgCount = this.inputList.length - (stackInput ? 1 : 0) - (returnInput ? 1 : 0);
if (oldArgCount > 0) {
const paramInput0 = this.getInput('VAR0');
if (paramInput0) { // Yes, they were vertical
@@ -227,8 +229,20 @@ Blockly.Blocks['procedures_defnoreturn'] = {
}
}
- // Now put back last (= body) input
- this.moveInputBefore(this.bodyInputName);
+ // Add stack to defreturn block
+ const wantStack = (this.bodyInputName === 'STACK') || !!this.stackEnabled_;
+ if (wantStack) {
+ if (!stackInput && this.bodyInputName !== 'STACK') {
+ // defreturn with STACK enabled but missing -> create it
+ this.appendStatementInput('STACK')
+ .appendField(Blockly.Msg['LANG_PROCEDURES_DEFNORETURN_DO']);
+ }
+ this.moveInputBefore('STACK');
+ }
+
+ if (returnInput) {
+ this.moveInputBefore('RETURN');
+ }
// set in BlocklyPanel.java on successful load
if (this.workspace.loadCompleted) {
@@ -365,6 +379,17 @@ Blockly.Blocks['procedures_defnoreturn'] = {
xmlElement.getAttribute('vertical_parameters') !== 'true';
this.updateParams_(params);
},
+ saveExtraState() {
+ const state = {};
+ if (!this.horizontalParameters) state.vertical_parameters = true;
+ if (this.arguments_?.length) state.args = [...this.arguments_];
+ return state;
+ },
+ loadExtraState(state) {
+ const params = state?.args || [];
+ this.horizontalParameters = state?.vertical_parameters !== true;
+ this.updateParams_(params);
+ },
decompose: function(workspace) {
const containerBlock = workspace.newBlock('procedures_mutatorcontainer');
containerBlock.initSvg();
@@ -549,13 +574,16 @@ Blockly.Blocks['procedures_defreturn'] = {
Blockly.Msg['LANG_PROCEDURES_DEFRETURN_PROCEDURE'], this);
this.createHeader(legalName);
this.horizontalParameters = true; // horizontal by default
+ this.appendStatementInput('STACK')
+ .appendField(Blockly.Msg['LANG_PROCEDURES_DOTHENRETURN_DO']);
this.appendInputFromRegistry('indented_input', 'RETURN')
- .setAlign(Blockly.inputs.Align.RIGHT)
- .appendField(Blockly.Msg['LANG_PROCEDURES_DEFRETURN_RETURN']);
+ .setAlign(Blockly.inputs.Align.RIGHT)
+ .appendField(Blockly.Msg['LANG_PROCEDURES_DEFRETURN_RETURN']);
this.setMutator(new Blockly.icons.MutatorIcon(['procedures_mutatorarg'], this));
this.setTooltip(Blockly.Msg['LANG_PROCEDURES_DEFRETURN_TOOLTIP']);
this.arguments_ = [];
this.warnings = [{name: 'checkEmptySockets', sockets: ['RETURN']}];
+ this.stackEnabled_ = true;
},
createHeader: function(procName) {
return this.appendDummyInput('HEADER')
@@ -570,10 +598,65 @@ Blockly.Blocks['procedures_defreturn'] = {
parameterFlydown: Blockly.Blocks.procedures_defnoreturn.parameterFlydown,
setParameterOrientation:
Blockly.Blocks.procedures_defnoreturn.setParameterOrientation,
- mutationToDom: Blockly.Blocks.procedures_defnoreturn.mutationToDom,
- domToMutation: Blockly.Blocks.procedures_defnoreturn.domToMutation,
- decompose: Blockly.Blocks.procedures_defnoreturn.decompose,
- compose: Blockly.Blocks.procedures_defnoreturn.compose,
+ mutationToDom: function () {
+ const m = Blockly.Blocks.procedures_defnoreturn.mutationToDom.call(this);
+ m.setAttribute('stack_enabled', this.stackEnabled_ ? 'true' : 'false');
+ return m;
+ },
+ domToMutation: function (xml) {
+ Blockly.Blocks.procedures_defnoreturn.domToMutation.call(this, xml);
+
+ this.stackEnabled_ = xml.getAttribute('stack_enabled') === 'true';
+ const hasStack = !!this.getInput('STACK');
+ if (this.stackEnabled_ && !hasStack) {
+ this.appendStatementInput('STACK')
+ .appendField(Blockly.Msg['LANG_PROCEDURES_DOTHENRETURN_DO']);
+ if (this.getInput('RETURN')) this.moveInputBefore('STACK', 'RETURN');
+ } else if (!this.stackEnabled_ && hasStack) {
+ this.removeInput('STACK');
+ }
+ },
+ saveExtraState: function () {
+ const element = Blockly.Blocks.procedures_defnoreturn.saveExtraState.call(this);
+ element.stackEnabled = !!this.stackEnabled_;
+ return element;
+ },
+ loadExtraState: function (state) {
+ Blockly.Blocks.procedures_defnoreturn.loadExtraState.call(this, state);
+ this.stackEnabled_ = state.stackEnabled;
+ const hasStack = !!this.getInput('STACK');
+ if (this.stackEnabled_ && !hasStack) {
+ this.appendStatementInput('STACK')
+ .appendField(Blockly.Msg['LANG_PROCEDURES_DOTHENRETURN_DO']);
+ if (this.getInput('RETURN')) this.moveInputBefore('STACK', 'RETURN');
+ } else if (!this.stackEnabled_ && hasStack) {
+ this.removeInput('STACK');
+ }
+ },
+ decompose: function (workspace) {
+ const containerBlock =
+ Blockly.Blocks.procedures_defnoreturn.decompose.call(this, workspace);
+
+ const cb = containerBlock.getField('STACK_ENABLED');
+ if (cb) cb.setValue(this.stackEnabled_ ? 'TRUE' : 'FALSE');
+
+ return containerBlock;
+ },
+ compose: function (containerBlock) {
+ const cb = containerBlock.getField('STACK_ENABLED');
+ this.stackEnabled_ = cb ? cb.getValue() === 'TRUE' : false;
+
+ const hasStack = !!this.getInput('STACK');
+ if (this.stackEnabled_ && !hasStack) {
+ this.appendStatementInput('STACK')
+ .appendField(Blockly.Msg['LANG_PROCEDURES_DOTHENRETURN_DO']);
+ if (this.getInput('RETURN')) this.moveInputBefore('STACK', 'RETURN');
+ } else if (!this.stackEnabled_ && hasStack) {
+ this.removeInput('STACK');
+ }
+
+ Blockly.Blocks.procedures_defnoreturn.compose.call(this, containerBlock);
+ },
dispose: Blockly.Blocks.procedures_defnoreturn.dispose,
getProcedureDef: Blockly.Blocks.procedures_defnoreturn.getProcedureDef,
getDeclaredVars: Blockly.Blocks.procedures_defnoreturn.getDeclaredVars,
@@ -596,7 +679,7 @@ Blockly.Blocks['procedures_mutatorcontainer'] = {
// this.setColour(Blockly.PROCEDURE_CATEGORY_HUE);
this.setStyle('procedure_blocks');
this.appendDummyInput()
- .appendField(Blockly.Msg['LANG_PROCEDURES_MUTATORCONTAINER_TITLE']);
+ .appendField(Blockly.Msg['LANG_PROCEDURES_MUTATORCONTAINER_TITLE']);
this.appendStatementInput('STACK');
this.setTooltip(Blockly.Msg['LANG_PROCEDURES_MUTATORCONTAINER_TOOLTIP']);
this.contextMenu = false;
@@ -605,6 +688,22 @@ Blockly.Blocks['procedures_mutatorcontainer'] = {
// [lyn. 11/24/12] Set procBlock associated with this container.
setProcBlock: function(procBlock) {
this.procBlock_ = procBlock;
+ const isDefReturn = !!procBlock && procBlock.type === 'procedures_defreturn';
+
+ if (this.getInput('ENABLE_STACK')) this.removeInput('ENABLE_STACK');
+
+ if (isDefReturn) {
+ const row = this.appendDummyInput('ENABLE_STACK')
+ .appendField(Blockly.Msg['LANG_PROCEDURES_DEFRETURN_ENABLE_STACK'])
+ .appendField(new Blockly.FieldCheckbox('false'), Blockly.Msg['LANG_PROCEDURES_DEFRETURN_STACK_ENABLE_FIELD'])
+ .appendField(Blockly.Msg['LANG_PROCEDURES_MUTATORCONTAINER_STACK']);
+ row.init();
+
+ const cb = this.getField('STACK_ENABLED');
+ if (cb) cb.setValue(procBlock.stackEnabled_ ? 'TRUE' : 'FALSE');
+ }
+
+ if (this.rendered) this.render();
},
// [lyn. 11/24/12] Set procBlock associated with this container.
// Invariant: should not be null, since only created as mutator for a
diff --git a/block-lexical-variables/src/msg.js b/block-lexical-variables/src/msg.js
index cdaa364..3ee5c71 100644
--- a/block-lexical-variables/src/msg.js
+++ b/block-lexical-variables/src/msg.js
@@ -72,6 +72,8 @@ Blockly.Msg['LANG_PROCEDURES_DEFRETURN_RETURN'] = 'result';
Blockly.Msg['LANG_PROCEDURES_DEFRETURN_COLLAPSED_PREFIX'] = 'to ';
Blockly.Msg['LANG_PROCEDURES_DEFRETURN_TOOLTIP'] =
'A procedure returning a result value.';
+Blockly.Msg['LANG_PROCEDURES_DEFRETURN_ENABLE_STACK'] = 'allow statements'
+Blockly.Msg['LANG_PROCEDURES_DEFRETURN_STACK_ENABLE_FIELD'] = 'STACK_ENABLED'
Blockly.Msg['LANG_PROCEDURES_DEF_DUPLICATE_WARNING'] =
'Warning:\nThis procedure has\nduplicate inputs.';
Blockly.Msg['LANG_PROCEDURES_CALLNORETURN_CALL'] = 'call ';
diff --git a/block-lexical-variables/test/procedures.mocha.js b/block-lexical-variables/test/procedures.mocha.js
new file mode 100644
index 0000000..b8e8410
--- /dev/null
+++ b/block-lexical-variables/test/procedures.mocha.js
@@ -0,0 +1,271 @@
+import * as Blockly from 'blockly/core';
+import * as libraryBlocks from 'blockly/blocks';
+
+import '../src/msg';
+import '../src/utilities';
+import '../src/workspace';
+import '../src/procedure_utils';
+import '../src/fields/flydown';
+import '../src/fields/field_flydown';
+import '../src/fields/field_global_flydown';
+import '../src/fields/field_nocheck_dropdown';
+import '../src/fields/field_parameter_flydown';
+import '../src/fields/field_procedurename';
+import '../src/blocks/lexical-variables';
+import '../src/blocks/controls';
+import '../src/blocks/variable-get-set.js';
+import '../src/procedure_database';
+import '../src/blocks/procedures';
+import '../src/generators/controls';
+import '../src/generators/procedures';
+import '../src/generators/lexical-variables';
+
+
+import chai from 'chai';
+
+
+/**
+ * Utility function to check if a block contains another block by type or id.
+ * @param {Blockly.Block} block - The block to search within.
+ * @param {string} identifier - The type or id of the block to search for.
+ * @param {boolean} isId - Whether to search by id (true) or type (false).
+ * @returns {boolean} - True if the block contains the specified block, false otherwise.
+ */
+function containsBlocks(block, identifier, isId = true) {
+ if (!block) return false;
+
+ if (isId && block.id === identifier) return true;
+ if (!isId && block.type === identifier) return true;
+
+ const children = block.getChildren(true);
+ for (const child of children) {
+ if (containsBlocks(child, identifier, isId)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Utility function to check if a procedure block has a stack field.
+ * @param procedureBlock - The procedure block to check.
+ * @param stackEnabled - Whether the stack field should be present (true) or not (false).
+ */
+function checkStackFieldExists(procedureBlock, stackEnabled = false) {
+ const stackField = procedureBlock.getInput('STACK');
+ if (stackEnabled) {
+ chai.assert.isDefined(stackField, 'Procedure block should have a stack field');
+ } else {
+ chai.assert.isNull(stackField, 'Procedure block should not have a stack field');
+ }
+}
+
+suite ('Procedures', function() {
+ setup(function () {
+ this.workspace = new Blockly.Workspace();
+ Blockly.common.setMainWorkspace(this.workspace);
+ });
+ teardown(function () {
+ delete this.workspace;
+ })
+
+ suite('procedures_defreturn', function() {
+ test('Load procedure from old version (without stack_enable)', function() {
+ const xml = Blockly.utils.xml.textToDom(
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' do_something' +
+ ' x' +
+ ' y' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' name' +
+ ' ' +
+ ' ' +
+ ' name' +
+ ' ' +
+ ' ' +
+ ' y' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' x' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ''
+ );
+
+ Blockly.Xml.domToWorkspace(xml, this.workspace);
+ const block = this.workspace.getBlockById('p');
+ chai.assert.isDefined(block, 'Procedure block should be defined');
+ chai.assert.equal(block.type, 'procedures_defreturn', 'Block type should be procedures_defreturn');
+
+ const mutation = block.mutationToDom();
+ chai.assert.isNotNull(mutation, 'Mutation should not be null');
+ chai.assert.equal(mutation.getAttribute('stack_enabled'), 'false', 'stack_enabled should be false');
+
+ const blockIds = ['a', 'b', 'c', 'd', 'f'];
+ blockIds.forEach(id => {
+ chai.assert.isTrue(containsBlocks(block, id), `Block with id ${id} should be contained`);
+ });
+ })
+
+ test('Load procedure from old version using JSON (without stack_enable)', function() {
+ const json = {
+ blocks: {
+ languageVersion: 0,
+ blocks: [
+ {
+ type: 'procedures_defreturn',
+ id: 'p',
+ x: 310,
+ y: 213,
+ fields: {
+ NAME: 'do_something',
+ VAR0: 'x',
+ VAR1: 'y',
+ },
+ extraState: { args: ['x', 'y'] },
+ inputs: {
+ RETURN: {
+ block: {
+ type: 'controls_do_then_return',
+ id: 'a',
+ inputs: {
+ STM: {
+ block: {
+ type: 'simple_local_declaration_statement',
+ id: 'b',
+ fields: { VAR: 'name' },
+ inputs: {
+ DO: {
+ block: {
+ type: 'lexical_variable_set',
+ id: 'c',
+ fields: { VAR: 'name' },
+ inputs: {
+ VALUE: {
+ block: {
+ type: 'lexical_variable_get',
+ id: 'd',
+ fields: { VAR: 'y' },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ VALUE: {
+ block: {
+ type: 'lexical_variable_get',
+ id: 'f',
+ fields: { VAR: 'x' },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ };
+
+ Blockly.serialization.workspaces.load(json, this.workspace);
+ const block = this.workspace.getBlockById('p');
+ chai.assert.isDefined(block, 'Procedure block should be defined');
+ chai.assert.equal(block.type, 'procedures_defreturn', 'Block type should be procedures_defreturn');
+
+ const extra = block.saveExtraState ? block.saveExtraState() : null;
+ chai.assert.isNotNull(extra, 'Extra state should not be null');
+ chai.assert.strictEqual(extra.stackEnabled, false, 'stack_enabled should be false');
+
+ const blockIds = ['a', 'b', 'c', 'd', 'f'];
+ blockIds.forEach(id => {
+ chai.assert.isTrue(containsBlocks(block, id), `Block with id ${id} should be contained`);
+ });
+ });
+
+
+ test('Mutation button click should enable stack', function() {
+ const xml = Blockly.utils.xml.textToDom(
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' do_something' +
+ ' ' +
+ ''
+ );
+
+ Blockly.Xml.domToWorkspace(xml, this.workspace);
+ const block = this.workspace.getBlockById('p');
+ chai.assert.isDefined(block, 'Procedure block should be defined');
+
+ const mutation = block.mutationToDom();
+ chai.assert.equal(mutation.getAttribute('stack_enabled'), 'false', 'stack_enabled should initially be false');
+ checkStackFieldExists(block, false);
+
+ block.domToMutation(Blockly.utils.xml.textToDom(
+ '' +
+ ' ' +
+ ' ' +
+ ''
+ ));
+ const updatedMutation = block.mutationToDom();
+ chai.assert.equal(updatedMutation.getAttribute('stack_enabled'), 'true', 'stack_enabled should be true after toggle');
+ checkStackFieldExists(block, true);
+ });
+
+ test('Mutation button click should enable stack using JSON', function() {
+ const json = {
+ blocks: {
+ languageVersion: 0,
+ blocks: [
+ {
+ type: 'procedures_defreturn',
+ id: 'p',
+ x: 310,
+ y: 213,
+ fields: { NAME: 'do_something' },
+ extraState: { stack_enabled: false, args: ['x', 'y'] },
+ },
+ ],
+ },
+ };
+
+ Blockly.serialization.workspaces.load(json, this.workspace);
+ const block = this.workspace.getBlockById('p');
+ chai.assert.isDefined(block, 'Procedure block should be defined');
+
+ const extra = block.saveExtraState();
+ chai.assert.strictEqual(extra.stackEnabled, false, 'stack_enabled should initially be false');
+ checkStackFieldExists(block, false);
+
+ block.loadExtraState({ ...extra, stackEnabled: true });
+
+ const updated = block.saveExtraState();
+ chai.assert.strictEqual(updated.stackEnabled, true, 'stack_enabled should be true after toggle');
+ checkStackFieldExists(block, true);
+ });
+ })
+})
\ No newline at end of file