Skip to content

feat: support Ruby hash literals and bracket access with dual-list storage #315

@takaokouji

Description

@takaokouji

Goal

v2 限定で、Ruby ハッシュリテラルとブラケットアクセスを、既存の Scratch リスト2本(_hash_<name>_keys_ / _hash_<name>_values_)にマッピングして実現する。拡張機能・新データ構造は導入しない。

前提

  • feat: support Ruby symbol literals (:symbol) with $_symbols_ global list #314 でマージ済みのシンボルリテラル(:symbol)機能を前提とする
  • シンボルは $_symbols_ グローバルリスト(値は ":foo" 等のコロン付き文字列)で管理され、data_itemnumoflist のインデックスで表現される
  • シンボル関連ユーティリティ: variable-utils.js_collectSymbol(), _symbolToBlock(), _resolveSymbolVariable()

設計概要

基本設計

項目 決定事項
ストレージ 既存リスト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.jsvisitHashNode 既存処理確認(変更不要の可能性)

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

  • Phase 1: ハッシュリテラル$a = {name: "Alice", age: 30} の converter + generator(TDD)。シンボル値({status: :active})も含む
  • Phase 2: ハッシュ読み取り$a[:name] / $a["name"] の converter + generator(TDD)
  • Phase 3: ハッシュ書き込み(upsert)$a[:name] = "Bob" の delete+push パターン converter + generator(TDD)
  • Phase 4: v1 バージョンゲーティング — v1 でハッシュ構文使用時のエラーメッセージ
  • Phase 5: ラウンドトリップテスト — リテラル/読取/書込/sym・str 混在/ローカル変数/シンボル値のラウンドトリップ
  • Phase 6: Integration Tests + ブラウザ確認 — CI green 後 Playwright MCP でブラウザ確認

Definition of Done

  • ユニットテスト pass
  • ラウンドトリップテスト pass
  • Integration テスト pass
  • lint pass
  • CI green
  • ブラウザ確認(Playwright MCP):
    • $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} のラウンドトリップ
    • v1 モードでハッシュ構文を書くとエラー表示
    • ローカル変数 a = {x: 1} のラウンドトリップ
    • 空ハッシュ $a = {} のラウンドトリップ

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 は挿入順を保持)。教育環境では許容範囲と判断

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions