Skip to content

feat: Ruby module/include (MixIn) support with cross-sprite sync #295

@takaokouji

Description

@takaokouji

Goal

Ruby の module / include をサポートし、複数スプライトでメソッド定義を共有できるようにする。モジュール編集時に即座に全スプライトへ同期する、スモウルビーの目玉機能。Ruby version 2 専用。

動作イメージ

Ruby タブの表示(各スプライト共通)

Sprite1:

module Utils
  def add(a, b)
    a + b
  end
end

class Sprite1
  include Utils

  when_flag_clicked do
    move(add(1, 5))
  end
end

Sprite2:

module Utils
  def add(a, b)
    a + b
  end
end

class Sprite2
  include Utils

  when_clicked do
    turn_right(add(1, 5))
  end
end

Scratch ブロック表現(各スプライト)

各スプライトに同一の procedure blocks が複製される:

procedures_definition: add(a, b)  ← comment: @ruby:module_source:Utils
  [a + b を返すブロック群]

ファイルダウンロード(全ターゲット)

require "smalruby3"

module Utils
  def add(a, b)
    a + b
  end
end

class Stage < ::Smalruby3::Stage
end

class Sprite1 < ::Smalruby3::Sprite
  include Utils
  when_flag_clicked do
    move(add(1, 5))
  end
end

class Sprite2 < ::Smalruby3::Sprite
  include Utils
  when_clicked do
    turn_right(add(1, 5))
  end
end

コメント設計

procedures_definition ブロック

@ruby:module_source:Utils    ← このメソッドは module Utils 由来

クラスコメント(include 順序保持)

@ruby:class:name,include=Utils,include=Helpers

include= の出現順が include 順序を表す。

モジュール同期メカニズム(即時同期)

トリガー: あるスプライトの Ruby コードが変換(Ruby → Blocks)されたとき

フロー:

  1. Sprite1 のコードを変換 → module Utils のメソッド定義を検出
  2. Sprite1 の procedure blocks に @ruby:module_source:Utils コメント付きで作成
  3. 同期処理: Utils を include している他の全スプライトを検出(procedure blocks のコメントから @ruby:module_source:Utils を検索)
  4. 各対象スプライトについて:
    a. そのスプライトの Ruby コードを生成(Blocks → Ruby)
    b. 生成コード内の古い module Utils ... end を新しい定義に置換
    c. 置換後のコードを再変換(Ruby → Blocks)し、ブロックを適用

Scratch ブロックの仕組みにより、プロシージャの引数追加時に呼び出し側に "" がデフォルト引数として自動追加されるため、同期エラーは発生しない。

Ruby version 制限

  • Ruby version 1: module はエラー(「module は Ruby version 2 でのみ使用できます」)
  • v2 → v1 への切替防止: いずれかのターゲットに @ruby:module_source:* コメント付きプロシージャまたは @ruby:class コメントが存在する場合、v1 切替ボタンをグレーアウトし、ツールチップで理由を表示

エラーハンドリング

ケース メッセージ
v1 で module を使用 module は Ruby version 2 でのみ使用できます
include で未定義モジュールを参照 モジュール Utils が定義されていません
module 内にネストした module 現在のスモウルビーでは使うことができません
module_function を使用 現在のスモウルビーでは使うことができません
extend を使用 現在のスモウルビーでは使うことができません
module 内にメソッド定義以外 現在のスモウルビーではモジュール内にメソッド定義のみ記述できます

Affected Files

Ruby → Blocks(コンバーター)

  • packages/scratch-gui/src/lib/ruby-to-blocks-converter/index.jsvisitModuleNode 追加、visitClassNodeinclude 処理追加、v1 エラー処理

Blocks → Ruby(ジェネレーター)

  • packages/scratch-gui/src/lib/ruby-generator/index.jsfinish() で module 生成 + include 挿入、finishTargets() で重複排除
  • packages/scratch-gui/src/lib/ruby-generator/procedure.js@ruby:module_source:* 検出時に def methodself. なし)生成

モジュール同期

  • packages/scratch-gui/src/containers/ruby-tab.jsx — コード変換後にモジュール同期処理を実行
  • packages/scratch-gui/src/lib/module-sync.js (新規) — 同期ロジック

v1 切替防止

  • Ruby version 切替 UI — v2 専用機能使用時の v1 切替無効化

テスト(新規)

  • packages/scratch-gui/test/unit/lib/ruby-to-blocks-converter/module.test.js
  • packages/scratch-gui/test/unit/lib/ruby-generator/module.test.js
  • packages/scratch-gui/test/unit/lib/module-sync.test.js
  • packages/scratch-gui/test/integration/module-include.test.js

Implementation Steps

  • Phase 1: visitModuleNode + include → Blocks

    • visitModuleNode 実装(module body のメソッド AST を context に保存)
    • visitClassNode に include 処理追加(module メソッドを展開し @ruby:module_source:* コメント付与)
    • クラスコメントに include=ModuleName を追加
    • v1 での module エラー処理
    • TDD: ユニットテスト先行
    • feat: add module/include support to ruby-to-blocks converter
  • Phase 2: Blocks → Ruby(単一ターゲット)

    • procedure.js: @ruby:module_source:* 検出時に def methodself. なし)生成
    • finish() / _wrapWithClass(): module メソッドを分離し module ... end + include 生成
    • クラスコメントの include= 順序で include 文を生成
    • TDD: ユニットテスト先行
    • feat: generate module/include syntax from procedure comments
  • Phase 3: モジュール同期

    • module-sync.js 新規作成
    • ruby-tab.jsx に変換後の同期処理追加
    • 他ターゲットの検出 → Ruby 生成 → module 置換 → 再変換 → ブロック適用
    • TDD: ユニットテスト先行
    • feat: add immediate module sync across sprites
  • Phase 4: ファイル出力の重複排除

    • finishTargets() で全ターゲットの module 定義を重複排除し先頭に1回出力
    • TDD: ユニットテスト先行
    • feat: deduplicate module definitions in multi-target output
  • Phase 5: エラーハンドリング + v1 切替防止

    • ネスト module、module_function、extend の検出・エラー表示
    • module 内の非メソッド文のエラー
    • v2 専用機能使用時の v1 切替無効化 UI
    • feat: add module error handling and v1 switch prevention
  • Phase Final: Integration Tests

    • ラウンドトリップ統合テスト(Ruby → Blocks → Ruby)
    • モジュール同期統合テスト
    • ファイル出力統合テスト
    • test: add integration tests for module/include feature

Test Plan

Type Timing Target
Unit tests (TDD) Before implementation (RED → GREEN) visitModuleNode, include 展開, generator, module-sync
Integration tests After implementation ラウンドトリップ, 同期, ファイル出力

Risks & Open Questions

  1. 同期のパフォーマンス: スプライト数が多い場合、全ターゲットの Ruby 生成 → 再変換がボトルネックになる可能性。非同期処理で UI をブロックしない工夫が必要。
  2. ネスト module / module_function / extend: サポートしない。検出時にエラー表示。
  3. module 内の定数やクラス変数: サポートしない。メソッド定義のみ。
  4. ふりがな対応: furigana-annotator.js に module/include 関連のキーワードマッピング追加が必要(別 Issue で対応可)。

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