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
7 changes: 7 additions & 0 deletions packages/scratch-gui/src/lib/furigana-node-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ const nodeHandlers = {
this._addAnnotation(node.location, '偽');
},

_handleSymbolNode (node) {
const unescaped = node.unescaped;
const content = (unescaped && typeof unescaped === 'object') ?
unescaped.value : unescaped;
this._addAnnotation(node.location, `シンボル「${content}」`);
},

_handleStringNode (node) {
const unescaped = node.unescaped;
const content = (unescaped && typeof unescaped === 'object') ?
Expand Down
16 changes: 10 additions & 6 deletions packages/scratch-gui/src/lib/ruby-generator/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@
return `hide_variable(${Generator.quote_(variable)})\n`;
};

// === Smalruby: Start of array syntax ===
const getListName = function (block) {
const comment = Generator.getCommentText(block);
if (comment) {
Expand All @@ -144,7 +143,7 @@
return Generator.listName(Generator.getFieldId(block, 'LIST'));
};

/**

Check warning on line 146 in packages/scratch-gui/src/lib/ruby-generator/data.js

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @param "block" declaration
* Convert Scratch 1-indexed list index to Ruby 0-indexed array index.
* For literal numbers, subtracts 1 directly.
* For expressions, generates "(expr - 1)".
Expand Down Expand Up @@ -173,21 +172,18 @@
// Expression: wrap with "- 1"
return `${index} - 1`;
};
// === Smalruby: End of array syntax ===

Generator.data_listcontents = function (block) {
const list = getListName(block);
return [list, Generator.ORDER_COLLECTION];
};

Generator.data_addtolist = function (block) {
// === Smalruby: Start of array syntax ===
const comment = Generator.getCommentText(block);
if (comment && comment.includes('@ruby:array:literal:element')) {
// Suppressed: handled by data_deletealloflist array literal pattern
return '';
}
// === Smalruby: End of array syntax ===

const item = Generator.valueToCode(block, 'ITEM', Generator.ORDER_NONE) || '0';
const list = getListName(block);
Expand All @@ -203,7 +199,6 @@
Generator.data_deletealloflist = function (block) {
const list = getListName(block);

// === Smalruby: Start of array syntax ===
const comment = Generator.getCommentText(block);
const arrayLiteralMatch = comment ? comment.match(/@ruby:array:literal:(\d+)/) : null;
if (arrayLiteralMatch) {
Expand All @@ -218,7 +213,6 @@
}
return `${list} = [${values.join(', ')}]\n`;
}
// === Smalruby: End of array syntax ===

return `${list}.clear\n`;
};
Expand All @@ -238,12 +232,22 @@
};

Generator.data_itemoflist = function (block) {
const comment = Generator.getCommentText(block);
if (comment === '@ruby:symbol:var') {
const index = Generator.valueToCode(block, 'INDEX', Generator.ORDER_NONE);
return [index, Generator.ORDER_ATOMIC];
}
const index = getListIndex(block);
const list = getListName(block);
return [`${list}[${index}]`, Generator.ORDER_FUNCTION_CALL];
};

Generator.data_itemnumoflist = function (block) {
const comment = Generator.getCommentText(block);
if (comment && comment.startsWith('@ruby:symbol:')) {
const symbolName = comment.slice('@ruby:symbol:'.length);
return [`:${symbolName}`, Generator.ORDER_ATOMIC];
}
const item = Generator.valueToCode(block, 'ITEM', Generator.ORDER_NONE) || '0';
const list = getListName(block);
return [`${list}.index(${Generator.nosToCode(item)})`, Generator.ORDER_FUNCTION_CALL];
Expand Down
10 changes: 10 additions & 0 deletions packages/scratch-gui/src/lib/ruby-generator/looks.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export default function (Generator) {
};

Generator.looks_say = function (block) {
const sayComment = Generator.getCommentText(block);
if (sayComment && sayComment.startsWith('@ruby:symbol:')) {
const symbolName = sayComment.slice('@ruby:symbol:'.length);
return `say(:${symbolName})\n`;
}
const message = Generator.valueToCode(block, 'MESSAGE', Generator.ORDER_NONE) || Generator.quote_('');
return `say(${message})\n`;
};
Expand All @@ -47,6 +52,11 @@ export default function (Generator) {
};

Generator.looks_think = function (block) {
const thinkComment = Generator.getCommentText(block);
if (thinkComment && thinkComment.startsWith('@ruby:symbol:')) {
const symbolName = thinkComment.slice('@ruby:symbol:'.length);
return `think(:${symbolName})\n`;
}
const message = Generator.valueToCode(block, 'MESSAGE', Generator.ORDER_NONE) || Generator.quote_('');
return `think(${message})\n`;
};
Expand Down
6 changes: 4 additions & 2 deletions packages/scratch-gui/src/lib/ruby-generator/operators.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@ export default function (Generator) {
};

Generator.operator_subtract = function (block) {
// === Smalruby: Start of array syntax ===
const comment = Generator.getCommentText(block);
if (comment && comment.includes('@ruby:array:index')) {
// Round-trip pattern: subtract(itemnumoflist, 1) → pass through .index() result
const num1 = Generator.valueToCode(block, 'NUM1', Generator.ORDER_FUNCTION_CALL) || 0;
return [num1, Generator.ORDER_FUNCTION_CALL];
}
// === Smalruby: End of array syntax ===

const order = Generator.ORDER_ADDITIVE;
const num1 = Generator.valueToCode(block, 'NUM1', order) || 0;
Expand Down Expand Up @@ -230,6 +228,10 @@ export default function (Generator) {

Generator.operator_join = function (block) {
const comment = Generator.getCommentText(block);
if (comment && comment.startsWith('@ruby:symbol:')) {
const symbolName = comment.slice('@ruby:symbol:'.length);
return [`:${symbolName}.to_s`, Generator.ORDER_FUNCTION_CALL];
}
if (comment === '@ruby:method:to_s') {
const value = Generator.valueToCode(block, 'STRING1', Generator.ORDER_FUNCTION_CALL) ||
Generator.quote_('');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import _ from 'lodash';
import Primitive from '../primitive';
import {RubyToBlocksConverterError} from '../errors';

/**
* Expression AST handlers for RubyToBlocksConverter.
Expand Down Expand Up @@ -58,6 +59,53 @@ const ExpressionHandlers = {
if (!block) {
this._restoreContext(saved);

const arithmeticOps = ['+', '-', '*', '/', '%', '**'];
const comparisonOps = ['>', '<', '>=', '<='];
const isSymReceiver = this._isPrimitive(receiver) && receiver.type === 'sym';
const symbolArg = args.find(a => this._isPrimitive(a) && a.type === 'sym');

// Symbol in arithmetic → specific error
if (arithmeticOps.indexOf(name) >= 0 && (isSymReceiver || symbolArg)) {
const source = this._truncateSource(this._getSource(node));
const sym = isSymReceiver ? receiver : symbolArg;
const suggestion = source.replace(
`:${sym.value}`,
`:${sym.value}.to_s`
);
throw new RubyToBlocksConverterError(
node,
this._translator(this._symbolCannotArithmeticMessage(), {
SOURCE: source,
SUGGESTION: suggestion
})
);
}

// Symbol in comparison with non-symbol → specific error
if (comparisonOps.indexOf(name) >= 0 && (isSymReceiver || symbolArg)) {
const source = this._truncateSource(this._getSource(node));
throw new RubyToBlocksConverterError(
node,
this._translator(this._symbolCannotCompareMessage(), {SOURCE: source})
);
}

// Symbol in other contexts → needs .to_s
if (symbolArg) {
const source = this._truncateSource(this._getSource(node));
const suggestion = source.replace(
`:${symbolArg.value}`,
`:${symbolArg.value}.to_s`
);
throw new RubyToBlocksConverterError(
node,
this._translator(this._symbolNeedsToSMessage(), {
SOURCE: source,
SUGGESTION: suggestion
})
);
}

if (node.block) {
block = this._createBlock('ruby_statement_with_block', 'statement');
block.node = node;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ const ContextUtils = {
processDepth: 0,
rootNode: null,
modules: {},
currentModuleName: null
currentModuleName: null,
symbols: new Set()
};
if (this.vm && this.vm.runtime && this.vm.runtime.getTargetForStage) {
this._loadVariables(this.vm.runtime.getTargetForStage());
Expand Down Expand Up @@ -186,6 +187,10 @@ const ContextUtils = {
return 'nil';
}

if (this._isSymbol(receiver)) {
return 'symbol';
}

if (this._isConst(receiver)) {
return receiver.toString();
}
Expand Down
52 changes: 44 additions & 8 deletions packages/scratch-gui/src/lib/ruby-to-blocks-converter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ const messages = defineMessages({
defaultMessage: 'Failed to import module "{ NAME }" from other sprites.',
description: 'Error message when module auto-import from other sprites fails',
id: 'gui.smalruby3.rubyToBlocksConverter.moduleImportFailed'
},
symbolNeedsToS: {
defaultMessage: '"{ SOURCE }" — symbols need .to_s to be used as a string.' +
'\nWrite { SUGGESTION } instead.',
description: 'Error message when a symbol is used where a string is expected without .to_s',
id: 'gui.smalruby3.rubyToBlocksConverter.symbolNeedsToS'
},
symbolCannotArithmetic: {
defaultMessage: '"{ SOURCE }" — symbols cannot be used in arithmetic (+, -, *, /).' +
'\nUse .to_s to convert first, e.g. { SUGGESTION }.',
description: 'Error message when a symbol is used in arithmetic operation',
id: 'gui.smalruby3.rubyToBlocksConverter.symbolCannotArithmetic'
},
symbolCannotCompare: {
defaultMessage: '"{ SOURCE }" — symbols can only be compared with other symbols using >, <, >=, <=.' +
'\nUse == instead, or convert with .to_s.',
description: 'Error message when a symbol is compared with non-symbol using >, <, >=, <=',
id: 'gui.smalruby3.rubyToBlocksConverter.symbolCannotCompare'
}
});

Expand Down Expand Up @@ -237,6 +255,18 @@ class RubyToBlocksConverter extends Visitor {
return this._context.broadcastMsgs;
}

_symbolNeedsToSMessage () {
return messages.symbolNeedsToS;
}

_symbolCannotArithmeticMessage () {
return messages.symbolCannotArithmetic;
}

_symbolCannotCompareMessage () {
return messages.symbolCannotCompare;
}

setTranslatorFunction (translator) {
this._translator = translator;
this._prismErrorTranslator = new PrismErrorTranslator(translator);
Expand Down Expand Up @@ -290,6 +320,17 @@ class RubyToBlocksConverter extends Visitor {
} else {
const Primitive = require('./primitive').default;
if (block instanceof Primitive) {
if (block.type === 'sym') {
const source = this._truncateSource(this._getSource(block.node));
const suggestion = `${source}.to_s`;
throw new RubyToBlocksConverterError(
block.node,
this._translator(
messages.symbolNeedsToS,
{SOURCE: source, SUGGESTION: suggestion}
)
);
}
throw new RubyToBlocksConverterError(
block.node,
this._translator(
Expand Down Expand Up @@ -328,6 +369,9 @@ class RubyToBlocksConverter extends Visitor {
}
});

// Create $_symbols_ list if symbols were collected
this._createSymbolsList();

// Associate source comments with blocks
this._associateSourceComments(parseResult, code);

Expand Down Expand Up @@ -702,15 +746,13 @@ class RubyToBlocksConverter extends Visitor {
);
}

// === Smalruby: Start of stage module restriction ===
// module is not supported in Stage (stage and sprite have different available methods)
if (this._context.target && this._context.target.isStage) {
throw new RubyToBlocksConverterError(
node,
this._translator(messages.moduleNotSupportedInStage)
);
}
// === Smalruby: End of stage module restriction ===

// Nested modules are not supported
if (this._context.currentModuleName) {
Expand Down Expand Up @@ -751,7 +793,6 @@ class RubyToBlocksConverter extends Visitor {
return [];
}

// === Smalruby: Start of auto-import module from other sprites ===
/**
* Try to import a module definition from other sprites.
* Searches other sprites' block comments for `@ruby:module_source:moduleName`,
Expand Down Expand Up @@ -800,7 +841,6 @@ class RubyToBlocksConverter extends Visitor {
};
return true;
}
// === Smalruby: End of auto-import module from other sprites ===

visitClassNode (node) {
// class definitions are only supported in version 2
Expand Down Expand Up @@ -942,16 +982,13 @@ class RubyToBlocksConverter extends Visitor {
if (argType === 'ConstantReadNode') {
const moduleName = argNode.name;

// === Smalruby: Start of stage include restriction ===
if (isStageClass) {
throw new RubyToBlocksConverterError(
stmt,
this._translator(messages.includeNotSupportedInStage)
);
}
// === Smalruby: End of stage include restriction ===

// === Smalruby: Start of auto-import module from other sprites ===
if (!this._context.modules[moduleName]) {
// Try to import the module from other sprites
const imported = this._importModuleFromOtherSprites(moduleName);
Expand All @@ -962,7 +999,6 @@ class RubyToBlocksConverter extends Visitor {
);
}
}
// === Smalruby: End of auto-import module from other sprites ===

includedModuleNames.push(moduleName);
includeStatements.add(stmt);
Expand Down
Loading
Loading