Goal
v2 限定で、Ruby ハッシュリテラルとブラケットアクセスを、既存の Scratch リスト2本(_hash_<name>_keys_ / _hash_<name>_values_)にマッピングして実現する。拡張機能・新データ構造は導入しない。
前提
設計概要
基本設計
| 項目 |
決定事項 |
| ストレージ |
既存リスト2本: _hash_<name>_keys_ + _hash_<name>_values_ |
| キーの型 |
シンボル(:foo) または文字列("foo") のみ。数値不可(配列と区別できないため) |
| 値の型 |
数値、文字列、シンボル(:foo → _symbolToBlock() で data_itemnumoflist に変換) |
| 操作 |
{} リテラル初期化、[:key] 読取、[:key]= 書込(upsert)の3操作 |
| v1 |
エラー(hashSyntaxNotAvailableInV1) |
| v2 |
対応 |
| ローカル変数 |
対応(@ruby:lvar マーカーで配列と同方式) |
| 空ハッシュ |
$a = {} 対応(clear のみ) |
構文サポート
| Ruby 構文 |
受付 |
生成時 |
{name: "Alice"} (シンボルキー省略記法) |
✓ |
✓(デフォルト) |
{:name => "Alice"} (ハッシュロケット+シンボル) |
✓ |
✗ |
{"name" => "Alice"} (ハッシュロケット+文字列) |
✓ |
✓(文字列キー時) |
リスト命名規則
| Ruby 変数 |
Keys リスト名 |
Values リスト名 |
$a |
$_hash_a_keys_ |
$_hash_a_values_ |
@a |
@_hash_a_keys_ |
@_hash_a_values_ |
a(ローカル) |
_hash_a_keys_ |
_hash_a_values_(@ruby:lvar 付) |
シンボルキーと文字列キーの区別
シンボルキーはコロン付き文字列 ":foo" で keys リストに格納する($_symbols_ リストと同じ表現)。文字列キーはそのまま "foo" で格納する。これにより Scratch の Cast.compare で衝突しない。
| Ruby キー |
keys リストに格納される値 |
:foo(シンボル) |
":foo" |
"foo"(文字列) |
"foo" |
$_symbols_ のインデックスではなく文字列表現を使うことで、シンボル順序変更の影響を受けない。
シンボルが値の場合
ハッシュの値としてシンボルが使われた場合(例: {status: :active})、値は _symbolToBlock() を使って data_itemnumoflist ブロックに変換する。_collectSymbol() で $_symbols_ への登録も行う。
# $a = {status: :active}
data_addtolist ":status" to [$_hash_a_keys_] // @ruby:hash:literal:key:sym
data_addtolist // @ruby:hash:literal:value
ITEM: data_itemnumoflist ":active" in [$_symbols_] // @ruby:symbol:active
LIST: [$_hash_a_values_]
操作のブロックマッピング
1. ハッシュリテラル: $a = {name: "Alice", age: 30}
data_deletealloflist [$_hash_a_keys_] // @ruby:hash:literal:2
data_deletealloflist [$_hash_a_values_] // @ruby:hash:literal:values
data_addtolist ":name" to [$_hash_a_keys_] // @ruby:hash:literal:key:sym
data_addtolist "Alice" to [$_hash_a_values_] // @ruby:hash:literal:value
data_addtolist ":age" to [$_hash_a_keys_] // @ruby:hash:literal:key:sym
data_addtolist 30 to [$_hash_a_values_] // @ruby:hash:literal:value
2. ハッシュ読み取り: $a[:name] → レポーター
data_itemoflist // @ruby:hash:get:sym
INDEX: data_itemnumoflist ":name" in [$_hash_a_keys_]
LIST: [$_hash_a_values_]
- キー未発見 →
item# = 0 → LIST_INVALID → '' を返す
3. ハッシュ書き込み(upsert): $a[:name] = "Bob"
delete+push パターン(4ブロック)。Scratch の index 0 = LIST_INVALID → no-op 仕様を活用し、if/else 不要。
data_deleteoflist // @ruby:hash:set:sym
INDEX: data_itemnumoflist ":name" in [$_hash_a_keys_]
LIST: [$_hash_a_values_]
data_deleteoflist // @ruby:hash:set:delete:key
INDEX: data_itemnumoflist ":name" in [$_hash_a_keys_]
LIST: [$_hash_a_keys_]
data_addtolist ":name" to [$_hash_a_keys_] // @ruby:hash:set:push:key
data_addtolist "Bob" to [$_hash_a_values_] // @ruby:hash:set:push:value
| ケース |
delete (values) |
delete (keys) |
push both |
結果 |
| キー存在 (index=N) |
値削除 |
キー削除 |
末尾に追加 |
更新 ✓ |
| キー未存在 (index=0) |
no-op |
no-op |
末尾に追加 |
新規追加 ✓ |
delete 順序: values → keys(values 削除時に keys でまだインデックス検索可能なため)
配列との区別ロジック
converter の [] / []= ハンドラ内で引数の AST ノード型で分岐:
| 引数 |
Prism AST ノード |
判定 |
$a[0] |
IntegerNode / FloatNode |
配列アクセス(既存) |
$a[:name] |
SymbolNode |
ハッシュアクセス(新規) |
$a["name"] |
StringNode |
ハッシュアクセス(新規) |
ラウンドトリップ(generator)
| コメントマーカー |
検出ブロック |
生成 Ruby |
@ruby:hash:literal:N |
data_deletealloflist |
$a = {k1: v1, k2: v2} |
@ruby:hash:get:sym |
data_itemoflist |
$a[:key] |
@ruby:hash:get:str |
data_itemoflist |
$a["key"] |
@ruby:hash:set:sym |
data_deleteoflist |
$a[:key] = value |
@ruby:hash:set:str |
data_deleteoflist |
$a["key"] = value |
Affected Files
Ruby → Blocks(コンバーター)
packages/scratch-gui/src/lib/ruby-to-blocks-converter/variables.js — []/[]= ハンドラにハッシュ分岐追加、ハッシュリテラル代入処理追加
packages/scratch-gui/src/lib/ruby-to-blocks-converter/variable-utils.js — ハッシュ用リスト作成ユーティリティ追加(_collectSymbol() / _symbolToBlock() を活用)
packages/scratch-gui/src/lib/ruby-to-blocks-converter/expressions.js — visitHashNode 既存処理確認(変更不要の可能性)
Blocks → Ruby(ジェネレーター)
packages/scratch-gui/src/lib/ruby-generator/data.js — ハッシュマーカー検出、ハッシュ構文生成
ロケール
packages/scratch-gui/src/locales/ja.js — エラーメッセージ(v1 ゲーティング等)
packages/scratch-gui/src/locales/ja-Hira.js — エラーメッセージ(ひらがな)
Implementation Steps
Definition of Done
Test Plan
| Type |
Timing |
Target |
| Unit tests (TDD) |
各 Phase で RED → GREEN |
converter / generator 各操作 |
| Round-trip tests |
Phase 5 |
Ruby → Blocks → Ruby の往復 |
| Integration tests |
Phase 6 |
ブラウザ上の UI 動作 |
| Browser verification |
CI green 後 |
Playwright MCP で DoD 確認 |
Risks & Open Questions
- 既存キー更新時の順序変更: delete+push パターンにより、既存キーを更新すると末尾に移動する(Ruby の Hash は挿入順を保持)。教育環境では許容範囲と判断
Goal
v2 限定で、Ruby ハッシュリテラルとブラケットアクセスを、既存の Scratch リスト2本(
_hash_<name>_keys_/_hash_<name>_values_)にマッピングして実現する。拡張機能・新データ構造は導入しない。前提
:symbol)機能を前提とする$_symbols_グローバルリスト(値は":foo"等のコロン付き文字列)で管理され、data_itemnumoflistのインデックスで表現されるvariable-utils.jsの_collectSymbol(),_symbolToBlock(),_resolveSymbolVariable()設計概要
基本設計
_hash_<name>_keys_+_hash_<name>_values_:foo) または文字列("foo") のみ。数値不可(配列と区別できないため):foo→_symbolToBlock()でdata_itemnumoflistに変換){}リテラル初期化、[:key]読取、[:key]=書込(upsert)の3操作hashSyntaxNotAvailableInV1)@ruby:lvarマーカーで配列と同方式)$a = {}対応(clear のみ)構文サポート
{name: "Alice"}(シンボルキー省略記法){:name => "Alice"}(ハッシュロケット+シンボル){"name" => "Alice"}(ハッシュロケット+文字列)リスト命名規則
$a$_hash_a_keys_$_hash_a_values_@a@_hash_a_keys_@_hash_a_values_a(ローカル)_hash_a_keys__hash_a_values_(@ruby:lvar付)シンボルキーと文字列キーの区別
シンボルキーはコロン付き文字列
":foo"で keys リストに格納する($_symbols_リストと同じ表現)。文字列キーはそのまま"foo"で格納する。これにより Scratch のCast.compareで衝突しない。:foo(シンボル)":foo""foo"(文字列)"foo"$_symbols_のインデックスではなく文字列表現を使うことで、シンボル順序変更の影響を受けない。シンボルが値の場合
ハッシュの値としてシンボルが使われた場合(例:
{status: :active})、値は_symbolToBlock()を使ってdata_itemnumoflistブロックに変換する。_collectSymbol()で$_symbols_への登録も行う。操作のブロックマッピング
1. ハッシュリテラル:
$a = {name: "Alice", age: 30}2. ハッシュ読み取り:
$a[:name]→ レポーターitem#= 0 →LIST_INVALID→''を返す3. ハッシュ書き込み(upsert):
$a[:name] = "Bob"delete+push パターン(4ブロック)。Scratch の index 0 =
LIST_INVALID→ no-op 仕様を活用し、if/else 不要。delete 順序: values → keys(values 削除時に keys でまだインデックス検索可能なため)
配列との区別ロジック
converter の
[]/[]=ハンドラ内で引数の AST ノード型で分岐:$a[0]IntegerNode/FloatNode$a[:name]SymbolNode$a["name"]StringNodeラウンドトリップ(generator)
@ruby:hash:literal:Ndata_deletealloflist$a = {k1: v1, k2: v2}@ruby:hash:get:symdata_itemoflist$a[:key]@ruby:hash:get:strdata_itemoflist$a["key"]@ruby:hash:set:symdata_deleteoflist$a[:key] = value@ruby:hash:set:strdata_deleteoflist$a["key"] = valueAffected Files
Ruby → Blocks(コンバーター)
packages/scratch-gui/src/lib/ruby-to-blocks-converter/variables.js—[]/[]=ハンドラにハッシュ分岐追加、ハッシュリテラル代入処理追加packages/scratch-gui/src/lib/ruby-to-blocks-converter/variable-utils.js— ハッシュ用リスト作成ユーティリティ追加(_collectSymbol()/_symbolToBlock()を活用)packages/scratch-gui/src/lib/ruby-to-blocks-converter/expressions.js—visitHashNode既存処理確認(変更不要の可能性)Blocks → Ruby(ジェネレーター)
packages/scratch-gui/src/lib/ruby-generator/data.js— ハッシュマーカー検出、ハッシュ構文生成ロケール
packages/scratch-gui/src/locales/ja.js— エラーメッセージ(v1 ゲーティング等)packages/scratch-gui/src/locales/ja-Hira.js— エラーメッセージ(ひらがな)Implementation Steps
$a = {name: "Alice", age: 30}の converter + generator(TDD)。シンボル値({status: :active})も含む$a[:name]/$a["name"]の converter + generator(TDD)$a[:name] = "Bob"の delete+push パターン converter + generator(TDD)Definition of Done
$a = {name: "Alice", age: 30}→ ブロック化 → Ruby 復元$a[:name]読み取りラウンドトリップ$a[:name] = "Bob"書き込み(既存キー更新)ラウンドトリップ$a[:new_key] = "value"書き込み(新規キー追加)ラウンドトリップ$a = {"foo" => "bar"}文字列キーのラウンドトリップ$a = {name: "Alice", "age" => 30}$a = {status: :active}のラウンドトリップa = {x: 1}のラウンドトリップ$a = {}のラウンドトリップTest Plan
Risks & Open Questions