Skip to content

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

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions