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
    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