Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/scratch-gui/src/lib/ruby-generator/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ export default function (Generator) {

// Check if this is the last block in procedure definition
if (block._isLastReturnInProcedure) {
// Check if there's a cached super call for this method
const returnMethodMatch = comment.match(/@ruby:return:(\w+)/);
if (returnMethodMatch && Generator.returnCallCache_ &&
Generator.returnCallCache_[returnMethodMatch[1]]) {
const cachedCall = Generator.returnCallCache_[returnMethodMatch[1]];
delete Generator.returnCallCache_[returnMethodMatch[1]];
return `${cachedCall}\n`;
}
// Output just the value (implicit return)
return `${Generator.nosToCode(value)}\n`;
}
Expand Down
132 changes: 129 additions & 3 deletions packages/scratch-gui/src/lib/ruby-generator/procedure.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,87 @@ export default function (Generator) {
.toLowerCase();
};

/**
* Generate super call code from a procedures_call block with super comment.
* @param {object} block - The procedures_call block
* @param {string} superComment - `@ruby`:super or `@ruby`:super:forwarding
* @returns {string} The super call code (without trailing newline)
*/
const generateSuperCall = function (block, superComment) {
if (superComment === '@ruby:super:forwarding') {
return 'super';
}
// SuperNode: output 'super(args)'
const args = [];
const paramNamesIdsAndDefaults =
Generator.currentTarget.blocks.getProcedureParamNamesIdsAndDefaults(block.mutation.proccode);
const ids = paramNamesIdsAndDefaults[1];
const defaults = paramNamesIdsAndDefaults[2];
for (let i = 0; i < ids.length; i++) {
let value;
if (block.inputs[ids[i]]) {
value = Generator.valueToCode(block, ids[i], Generator.ORDER_NONE);
} else {
value = defaults[i];
}
args.push(Generator.nosToCode(value));
}
const argsString = args.length > 0 ? `(${args.join(', ')})` : '';
return `super${argsString}`;
};

/**
* Generate method header with a specific name (for renamed module methods with super_of).
* @param {object} defBlock - The procedures_definition block
* @param {object} customBlock - The procedures_prototype block
* @param {string} originalName - The original method name to use
* @returns {string} The method header code
*/
const blockToMethodWithName = function (defBlock, customBlock, originalName) {
const args = [];
const paramNamesIdsAndDefaults =
Generator.currentTarget.blocks.getProcedureParamNamesIdsAndDefaults(customBlock.mutation.proccode);
for (let i = 0; i < paramNamesIdsAndDefaults[0].length; i++) {
let paramName = Generator.escapeVariableName(paramNamesIdsAndDefaults[0][i]);
paramName = toSnakeCaseLowercase(paramName);
args.push(paramName);
}
const argsString = args.length > 0 ? `(${args.join(', ')})` : '';
if (Generator.version.toString() === '2') {
return `def ${originalName}${argsString}\n`;
}
return `def self.${originalName}${argsString}\n`;
};

Generator.procedures_definition = function (block) {
// Check for @ruby:module_source:ModuleName comment
// Extended format: @ruby:module_source:Mod:super_of:originalName
const comment = Generator.getCommentText(block);
const moduleSourceMatch = comment && comment.match(/^@ruby:module_source:(.+)$/);

// Check for super_of: extract original method name for renamed module methods
let superOfOriginalName = null;
if (moduleSourceMatch) {
const superOfMatch = moduleSourceMatch[1].match(/^(.+):super_of:(.+)$/);
if (superOfMatch) {
superOfOriginalName = superOfMatch[2];
}
}

const customBlock = Generator.getInputTargetBlock(block, 'custom_block');

// Save and temporarily clear block.next to prevent scrub_ from processing it
const savedNext = block.next;
block.next = null;

// Generate method header (def self.method_name(args))
let code = Generator.blockToCode(customBlock);
// If this is a renamed module method (super_of), use the original name
let code;
if (superOfOriginalName) {
code = blockToMethodWithName(block, customBlock, superOfOriginalName);
} else {
code = Generator.blockToCode(customBlock);
}

// Generate method body from the saved next block
const bodyBlock = Generator.getBlock(savedNext);
Expand All @@ -43,6 +111,32 @@ export default function (Generator) {
// Mark the last block so data_setvariableto knows it's the final expression
if (Generator.isRubyReturnAssignment(lastBlock)) {
lastBlock._isLastReturnInProcedure = true;

// Check if the block before the return assignment is a super call.
// If so, store the super call in returnCallCache_ so the return
// assignment's data_variable can retrieve it as the implicit return value.
let prevBlock = bodyBlock;
while (prevBlock.next && Generator.getBlock(prevBlock.next) !== lastBlock) {
prevBlock = Generator.getBlock(prevBlock.next);
}
if (prevBlock !== lastBlock) {
const prevComment = Generator.getCommentText(prevBlock);
if (prevComment === '@ruby:super' || prevComment === '@ruby:super:forwarding') {
const superCode = generateSuperCall(prevBlock, prevComment);
// Set up cache so the return block outputs the super call
// The return comment contains the method name
const returnComment = Generator.getCommentText(lastBlock);
if (returnComment) {
const returnMatch = returnComment.match(/@ruby:return:(\w+)/);
if (returnMatch) {
if (!Generator.returnCallCache_) {
Generator.returnCallCache_ = {};
}
Generator.returnCallCache_[returnMatch[1]] = superCode;
}
}
}
}
}

const bodyCode = Generator.blockToCode(bodyBlock);
Expand All @@ -68,7 +162,12 @@ export default function (Generator) {

// If this is a module method, store the code separately and suppress from main output
if (moduleSourceMatch) {
const moduleName = moduleSourceMatch[1];
// Extract module name (handle super_of: suffix)
let moduleName = moduleSourceMatch[1];
const superOfIdx = moduleName.indexOf(':super_of:');
if (superOfIdx !== -1) {
moduleName = moduleName.substring(0, superOfIdx);
}
if (!Generator._moduleMethodCodes[moduleName]) {
Generator._moduleMethodCodes[moduleName] = [];
}
Expand Down Expand Up @@ -139,8 +238,35 @@ export default function (Generator) {
};

Generator.procedures_call = function (block) {
// Check if this procedures_call has @ruby:return:methodName comment
const comment = Generator.getCommentText(block);

// Check if this is a super call AND next block is a return assignment
// (i.e., super is the last expression in a method with return value)
if (comment === '@ruby:super' || comment === '@ruby:super:forwarding') {
const nextBlock = Generator.getBlock(block.next);
if (Generator.isRubyReturnAssignment(nextBlock)) {
// Super is the implicit return value — suppress this block's output.
// The data_setvariableto handler will output the value.
// Store the super call so data_setvariableto can use it.
const superCode = generateSuperCall(block, comment);
if (!Generator.returnCallCache_) {
Generator.returnCallCache_ = {};
}
// Extract method name from the return assignment comment
const nextComment = Generator.getCommentText(nextBlock);
if (nextComment) {
const match = nextComment.match(/@ruby:return:(\w+)/);
if (match) {
Generator.returnCallCache_[match[1]] = superCode;
}
}
return '';
}
// Super call as a standalone statement
return `${generateSuperCall(block, comment)}\n`;
}

// Check if this procedures_call has @ruby:return:methodName comment
if (comment && comment.startsWith('@ruby:return:')) {
const methodName = comment.replace('@ruby:return:', '');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
import {defineMessages} from 'react-intl';
import _ from 'lodash';
import Primitive from '../primitive';
import {RubyToBlocksConverterError} from '../errors';

const messages = defineMessages({
superNotSupportedInV1: {
defaultMessage: 'super is only available in Ruby version 2.' +
'\nPlease switch to Ruby version 2 from the settings menu.',
description: 'Error message when super is used in Ruby version 1',
id: 'gui.smalruby3.rubyToBlocksConverter.superNotSupportedInV1'
},
superOutsideMethod: {
defaultMessage: 'super can only be used inside a method definition (def).',
description: 'Error message when super is used outside a method',
id: 'gui.smalruby3.rubyToBlocksConverter.superOutsideMethod'
},
superWithoutModuleMethod: {
defaultMessage: 'super in "{ METHOD }" requires a same-named method in an included module.' +
'\nDefine "{ METHOD }" in a module and include it in the class.',
description: 'Error message when super is used but no matching module method exists',
id: 'gui.smalruby3.rubyToBlocksConverter.superWithoutModuleMethod'
}
});

/**
* Expression AST handlers for RubyToBlocksConverter.
* @mixes RubyToBlocksConverter
Expand Down Expand Up @@ -345,6 +366,130 @@ const ExpressionHandlers = {
}

return name;
},

/**
* Handle super(args) call - SuperNode from `@ruby`/prism.
* super is syntactic sugar for calling the same-named method in an included module.
* @param {object} node - SuperNode AST node
* @returns {object} procedures_call block for the module method
*/
visitSuperNode (node) {
return this._handleSuper(node, false);
},

/**
* Handle bare super call (forwarding args) - ForwardingSuperNode from `@ruby`/prism.
* @param {object} node - ForwardingSuperNode AST node
* @returns {object} procedures_call block for the module method
*/
visitForwardingSuperNode (node) {
return this._handleSuper(node, true);
},

/**
* Common handler for SuperNode and ForwardingSuperNode.
* @param {object} node - AST node
* @param {boolean} isForwarding - true for bare super (forward all args)
* @returns {object} procedures_call block
*/
_handleSuper (node, isForwarding) {
// Validate: v1 not supported
if (String(this.version) === '1') {
throw new RubyToBlocksConverterError(
node,
this._translator(messages.superNotSupportedInV1)
);
}

// Validate: must be inside a method definition
const procedureName = this._context.currentProcedureName;
if (!procedureName) {
throw new RubyToBlocksConverterError(
node,
this._translator(messages.superOutsideMethod)
);
}

// Validate: a same-named method must exist in an included module
const superInfo = this._context.superMethodMap && this._context.superMethodMap[procedureName];
if (!superInfo) {
throw new RubyToBlocksConverterError(
node,
this._translator(messages.superWithoutModuleMethod, {METHOD: procedureName})
);
}

// Get the renamed module procedure
const renamedProcName = superInfo.renamedProcName;
const procedure = this._lookupProcedure(renamedProcName);
if (!procedure) {
throw new RubyToBlocksConverterError(
node,
this._translator(messages.superWithoutModuleMethod, {METHOD: procedureName})
);
}

// Build arguments
let args;
if (isForwarding) {
// Forward all arguments from the current method
const currentProc = this._lookupProcedure(procedureName);
if (currentProc) {
args = currentProc.argumentVariables.map(v => {
const block = this._callConvertersHandler('onVar', 'local', v);
return block || v.name;
});
} else {
args = [];
}
} else {
// Explicit arguments from super(a, b, ...)
const savedIsValue = this._context.isValue;
this._context.isValue = true;
args = (node.arguments_ ? node.arguments_.arguments_ : []).map(
childNode => this.visit(childNode)
);
this._context.isValue = savedIsValue;
}

// Create procedures_call block directly (not via callMethod) to avoid
// side effects like argument boolean type conversion
const callBlock = this._createBlock('procedures_call', 'statement', {
mutation: {
argumentids: JSON.stringify(procedure.argumentIds),
proccode: procedure.procCode.join(' '),
tagName: 'mutation',
children: [],
warp: 'false'
}
});

// Add arguments as inputs
for (let i = 0; i < procedure.argumentIds.length; i++) {
const inputId = procedure.argumentIds[i];
const arg = i < args.length ? args[i] : procedure.argumentDefaults[i];
if (this._isNumberOrBlock(arg) || this._isStringOrBlock(arg)) {
this._addTextInput(callBlock, inputId, this._isNumber(arg) ? arg.toString() : arg,
procedure.argumentDefaults[i] || '');
} else if (this._isFalse(arg)) {
// boolean false: don't add input (default)
} else {
this._addInput(callBlock, inputId, arg, null);
}
}

// Track this call for procedure boolean detection
if (!this._context.procedureCallBlocks[procedure.id]) {
this._context.procedureCallBlocks[procedure.id] = [];
}
this._context.procedureCallBlocks[procedure.id].push(callBlock.id);

// Attach @ruby:super or @ruby:super:forwarding comment
const commentText = isForwarding ? '@ruby:super:forwarding' : '@ruby:super';
callBlock.comment = this._createComment(commentText, callBlock.id);

return callBlock;
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ const ContextUtils = {
rootNode: null,
modules: {},
currentModuleName: null,
superMethodMap: {},
superRenameTarget: null,
symbols: new Set()
};
if (this.vm && this.vm.runtime && this.vm.runtime.getTargetForStage) {
Expand Down
Loading
Loading