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
14 changes: 11 additions & 3 deletions packages/scratch-gui/src/lib/ruby-generator/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,17 @@

// === Smalruby: Start of array syntax ===
const getListName = function (block) {
const comment = Generator.getCommentText(block);
if (comment) {
const lvarMatch = comment.match(/@ruby:lvar:([^:,\s]+)/);
if (lvarMatch) {
return lvarMatch[1];
}
}
return Generator.listName(Generator.getFieldId(block, 'LIST'));
};

/**

Check warning on line 147 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 All @@ -150,7 +157,7 @@
const indexBlock = Generator.getBlock(indexBlockId);
if (indexBlock && indexBlock.opcode === 'operator_add') {
const comment = Generator.getCommentText(indexBlock);
if (comment && comment.includes('@ruby:array:index_offset')) {
if (comment && comment.includes('@ruby:array:index')) {
// Use NUM1 directly (the original 0-indexed value)
return Generator.valueToCode(indexBlock, 'NUM1', Generator.ORDER_NONE) || 0;
}
Expand Down Expand Up @@ -198,8 +205,9 @@

// === Smalruby: Start of array syntax ===
const comment = Generator.getCommentText(block);
if (comment && comment.startsWith('@ruby:array:literal:')) {
const count = parseInt(comment.split(':')[3], 10);
const arrayLiteralMatch = comment ? comment.match(/@ruby:array:literal:(\d+)/) : null;
if (arrayLiteralMatch) {
const count = parseInt(arrayLiteralMatch[1], 10);
const values = [];
let nextId = block.next;
for (let i = 0; i < count; i++) {
Expand Down
32 changes: 20 additions & 12 deletions packages/scratch-gui/src/lib/ruby-to-blocks-converter/variables.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ const VariablesConverter = {
prefixedName = `$${varName}`;
} else if (variable.scope === 'instance') {
prefixedName = `@${varName}`;
} else if (variable.scope === 'local') {
prefixedName = variable.originalName;
} else {
return {block: null, converted: false};
}
Expand All @@ -87,18 +89,16 @@ const VariablesConverter = {

/**
* Adjust a 0-indexed Ruby array index to 1-indexed Scratch list index.
* Only adjusts when the receiver was converted from variable to list (array syntax).
* Always wraps in operator_add(index, 1) with @ruby:array:index comment
* to enable round-trip conversion.
*/
const adjustIndex = function (index, converted) {
if (!converted) return index;
if (typeof index === 'number') return index + 1;
if (converter._isPrimitive(index) &&
(index.type === 'int' || index.type === 'float')) {
return index.value + 1;
}
// For block expressions, wrap in operator_add(index, 1) with comment
// This is handled at the generator level via @ruby:array:index_offset
return index;
const addBlock = converter._createBlock('operator_add', 'value');
converter._addNumberInput(addBlock, 'NUM1', 'math_number', index, 0);
converter._addNumberInput(addBlock, 'NUM2', 'math_number', 1, 0);
addBlock.comment = converter._createComment('@ruby:array:index', addBlock.id);
return addBlock;
};
// === Smalruby: End of array syntax ===

Expand Down Expand Up @@ -632,7 +632,8 @@ const VariablesConverter = {

converter.registerOnVasgn((scope, variable, rh) => {
// === Smalruby: Start of array syntax ===
if ((scope === 'global' || scope === 'instance') &&
if ((scope === 'global' || scope === 'instance' ||
(scope === 'local' && !variable.isArgument)) &&
converter._isArray(rh)) {
if (converter.version < 2) {
throw new RubyToBlocksConverterError(
Expand All @@ -644,8 +645,10 @@ const VariablesConverter = {
let prefixedName;
if (variable.scope === 'global') {
prefixedName = `$${variable.name}`;
} else {
} else if (variable.scope === 'instance') {
prefixedName = `@${variable.name}`;
} else {
prefixedName = variable.originalName;
}
const listVar = converter._lookupOrCreateList(prefixedName);

Expand All @@ -660,8 +663,13 @@ const VariablesConverter = {
}
}
});
let arrayLiteralComment = `@ruby:array:literal:${elements.length}`;
if (scope === 'local') {
arrayLiteralComment =
`@ruby:lvar:${variable.originalName}:${variable.scopeIndex},${arrayLiteralComment}`;
}
clearBlock.comment = converter._createComment(
`@ruby:array:literal:${elements.length}`, clearBlock.id
arrayLiteralComment, clearBlock.id
);

// Create push blocks for each element
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// === Smalruby: This file is Smalruby-specific (v2 array syntax roundtrip tests) ===
/**
* V2 array syntax roundtrip tests.
* Verifies that Ruby → Blocks → Ruby produces correct output
* for array operations using the operator_add(index, 1) pattern.
*/
import dedent from 'dedent';
import {
makeSpriteTarget,
makeConverter,
setupRubyGenerator,
expectRoundTrip
} from '../../helpers/ruby-roundtrip-helper';

describe('Ruby Roundtrip: V2 array syntax', () => {
let target, runtime, converter;

beforeEach(() => {
({target, runtime} = makeSpriteTarget());
setupRubyGenerator();
converter = makeConverter(target, runtime, {version: '2'});
});

test('global array literal and indexing', async () => {
await expectRoundTrip(converter, target, dedent`
$a = ["a", "b"]
say($a[0])
`);
});

test('instance array literal and indexing', async () => {
await expectRoundTrip(converter, target, dedent`
@a = ["a", "b"]
say(@a[0])
`);
});

test('array indexing with literal index', async () => {
await expectRoundTrip(converter, target, dedent`
$a = ["x", "y", "z"]
say($a[2])
`);
});

test('array indexing with variable index', async () => {
await expectRoundTrip(converter, target, dedent`
@a = ["a", "b"]
@b = 1
say(@a[@b])
`);
});

test('array delete_at with literal index', async () => {
await expectRoundTrip(converter, target, dedent`
$a = ["a", "b", "c"]
$a.delete_at(0)
`);
});

test('array insert with literal index', async () => {
await expectRoundTrip(converter, target, dedent`
$a = ["a", "b"]
$a.insert(0, "x")
`);
});

test('array replace with literal index', async () => {
await expectRoundTrip(converter, target, dedent`
$a = ["a", "b"]
$a[0] = "x"
`);
});

test('array push', async () => {
await expectRoundTrip(converter, target, dedent`
$a = ["a"]
$a.push("b")
`);
});

test('array push with << operator', async () => {
await expectRoundTrip(converter, target, dedent`
$a = ["a"]
$a << "b"
`, dedent`
$a = ["a"]
$a.push("b")
`);
});

test('array clear', async () => {
await expectRoundTrip(converter, target, dedent`
$a = ["a", "b"]
$a.clear
`);
});

test('array index method', async () => {
await expectRoundTrip(converter, target, dedent`
$a = ["a", "b"]

$a.index("b")
`, dedent`
$a = ["a", "b"]

($a.index("b"))
`);
});

test('array length', async () => {
await expectRoundTrip(converter, target, dedent`
$a = ["a", "b"]

$a.length
`);
});

test('array include?', async () => {
await expectRoundTrip(converter, target, dedent`
$a = ["a", "b"]

$a.include?("a")
`);
});

test('empty array literal', async () => {
await expectRoundTrip(converter, target, dedent`
$a = []
`);
});

test('mixed array operations', async () => {
await expectRoundTrip(converter, target, dedent`
@a = ["a", "b"]
say(@a[0])
@b = 1
say(@a[@b])
`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,21 @@ describe('RubyToBlocksConverter/Variables/ArraySyntax', () => {
inputs: [
{
name: 'INDEX',
block: expectedInfo.makeNumber(1, 'math_integer')
block: {
opcode: 'operator_add',
comment: {text: '@ruby:array:index', minimized: true},
inputs: [
{
name: 'NUM1',
block: expectedInfo.makeNumber(0)
},
{
name: 'NUM2',
block: expectedInfo.makeNumber(1)
}
]
},
shadow: expectedInfo.makeNumber(1, 'math_integer')
}
]
}
Expand Down Expand Up @@ -113,7 +127,21 @@ describe('RubyToBlocksConverter/Variables/ArraySyntax', () => {
inputs: [
{
name: 'INDEX',
block: expectedInfo.makeNumber(1, 'math_integer')
block: {
opcode: 'operator_add',
comment: {text: '@ruby:array:index', minimized: true},
inputs: [
{
name: 'NUM1',
block: expectedInfo.makeNumber(0)
},
{
name: 'NUM2',
block: expectedInfo.makeNumber(1)
}
]
},
shadow: expectedInfo.makeNumber(1, 'math_integer')
},
{
name: 'ITEM',
Expand All @@ -139,7 +167,21 @@ describe('RubyToBlocksConverter/Variables/ArraySyntax', () => {
inputs: [
{
name: 'INDEX',
block: expectedInfo.makeNumber(1, 'math_integer')
block: {
opcode: 'operator_add',
comment: {text: '@ruby:array:index', minimized: true},
inputs: [
{
name: 'NUM1',
block: expectedInfo.makeNumber(0)
},
{
name: 'NUM2',
block: expectedInfo.makeNumber(1)
}
]
},
shadow: expectedInfo.makeNumber(1, 'math_integer')
},
{
name: 'ITEM',
Expand All @@ -165,7 +207,21 @@ describe('RubyToBlocksConverter/Variables/ArraySyntax', () => {
inputs: [
{
name: 'INDEX',
block: expectedInfo.makeNumber(1, 'math_integer')
block: {
opcode: 'operator_add',
comment: {text: '@ruby:array:index', minimized: true},
inputs: [
{
name: 'NUM1',
block: expectedInfo.makeNumber(0)
},
{
name: 'NUM2',
block: expectedInfo.makeNumber(1)
}
]
},
shadow: expectedInfo.makeNumber(1, 'math_integer')
}
]
}
Expand Down
Loading
Loading