forked from scratchfoundation/scratch-editor
-
Notifications
You must be signed in to change notification settings - Fork 1
Closed
Description
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.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
- 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 は挿入順を保持)。教育環境では許容範囲と判断
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels