はじめに / セットアップ / CLI / 対応ブロック一覧

xyo-rust の処理は、大きく分けると 4 段階です。

  1. .sb3 から project.json を取り出す
  2. Scratch プロジェクト全体を Rust の構造体へ変換する
  3. hat block を起点にスレッド単位の中間表現へ変換する
  4. 一部のスレッドを LLVM IR へ変換する

実行ランタイムはまだ完成していないため、現在の中心は解析と変換のパイプラインです。

モジュール概要
モジュール 役割
src/sb3.rs ZIP アーカイブとして .sb3 を開き、 project.json を読み込む。失敗時には位置情報付きのエラー整形も担当する
src/types/ Scratch の JSON 構造を受ける型定義を提供する
src/parser/ Scratch ブロックを Stmt / Expr に変換し、hat block からスレッド単位に解析する
src/compiler/ inkwell を使って LLVM IR を生成し、最適化パスを適用する
データフロー詳細

全体フロー

.sb3 ファイル (ZIP アーカイブ)
        │
        │ File::open + ZipArchive
        ▼
   project.json (バイト列)
        │
        │ serde_path_to_error::deserialize
        ▼
   ScratchProject (Rust 構造体)
   ├── targets: Vec<StageOrSprite>
   │   ├── isStage: bool
   │   ├── name: String
   │   ├── blocks: HashMap<String, BlockAndTopLevelPrimitive>
   │   ├── variables: HashMap<String, Variable>
   │   └── lists: HashMap<String, List>
   └── meta: Metadata
        │
        │ project_parser()
        ▼
   Vec<Thread>
   ├── Thread { hat: HatStmt, stmts: Vec<Stmt>, target_idx: usize }
   ├── Thread { hat: HatStmt, stmts: Vec<Stmt>, target_idx: usize }
   └── ...
        │
        │ compiler()
        ▼
   LLVM Module
   ├── グローバル変数 (スプライトの X/Y/方向など)
   ├── 関数 @thread_0(ptr) → スレッドの IR
   ├── 関数 @thread_1(ptr) → スレッドの IR
   └── ...
        │
        │ run_passes("default<O3>")
        ▼
   最適化済み LLVM IR (テキスト形式で標準出力)
ステージ 1: SB3 ロード ( src/sb3.rs )

.sb3 ファイルは ZIP アーカイブです。 sb3.rs はこのアーカイブを開いて project.json を取り出す処理を担当します。

主要な公開 API

// project.json を文字列として返す
pub fn read_json(path: impl AsRef<Path>) -> Result<String, ReadSb3Error>

// project.json をデシリアライズして ScratchProject を返す
pub fn read_sb3(path: impl AsRef<Path>) -> Result<ScratchProject, ReadSb3Error>

処理の流れ

  1. File::open(path) でファイルを開く
  2. ZipArchive::new(stream) で ZIP として読み込む
  3. archive.by_name("project.json") でエントリを取得する
  4. バイト列として読み込む
  5. read_sb3 の場合は serde_path_to_error::deserialize でデシリアライズする

エラー報告の仕組み

sb3.rs は JSON パースエラーが発生した際に、単純なエラーメッセージではなく詳細なコンテキスト情報を生成します。

pub enum ReadSb3Error {
    OpenFile { path: String, source: std::io::Error },
    OpenZip { path: String, source: zip::result::ZipError },
    MissingProjectJson { path: String, source: zip::result::ZipError },
    ReadProjectJson { path: String, source: std::io::Error },
    ParseProjectJson {
        path: String,
        source: serde_json::Error,
        json_path: Option<String>,  // JSON パス (例: .targets[0].blocks[xxx])
        context: String,            // 行番号・列番号・周辺テキスト
    },
    ReadAsUTF8 { path: String, source: FromUtf8Error },
}

serde_path_to_error クレートを使うことで、デシリアライズが失敗した JSON パスを正確に特定できます。さらに refine_json_path 関数によって、エラーの起きたブロック ID までパスを絞り込む場合があります。

エラー表示には json_error_context 関数が使われ、行番号・列番号とともに前後 1 行の周辺テキスト(長い行は ... で省略)と ^ カーソルが表示されます。

ステージ 2: 型定義とデシリアライズ ( src/types/ )

types/ モジュールは Scratch の project.json 構造を Rust の型として表現します。

主要な型

ScratchProject

プロジェクト全体を表すルート構造体です。

pub struct ScratchProject {
    pub targets: Vec<StageOrSprite>,
    pub meta: Metadata,
    // ...
}

count_blocks() メソッドでブロック総数を、 check_op_codes() メソッドで使用 opcode 一覧を取得できます。

StageOrSprite

ステージとスプライトを統合した型です。 isStage フィールドで区別します。

pub struct StageOrSprite {
    pub is_stage: bool,
    pub name: String,
    pub blocks: HashMap<String, BlockAndTopLevelPrimitive>,
    pub variables: HashMap<String, (String, serde_json::Value)>,
    pub lists: HashMap<String, (String, Vec<serde_json::Value>)>,
    // ...
}

Block

個々のブロックを表す型です。

pub struct Block {
    pub opcode: BlockOpCodes,
    pub inputs: HashMap<String, Input>,
    pub fields: HashMap<String, Vec<Option<String>>>,
    pub next: Option<String>,
    pub parent: Option<String>,
    pub shadow: bool,
    pub top_level: bool,
}
  • opcode : BlockOpCodes 列挙型( str_enum! マクロで定義)
  • inputs : 入力値の辞書(数値・文字列・他ブロックへの参照など)
  • fields : ドロップダウンメニューの選択値
  • next / parent : ブロック連鎖のリンク

BlockAndTopLevelPrimitive

ブロック辞書の値型です。通常のブロックと、ブロック参照なしに直接埋め込まれるプリミティブ値の両方を表します。

pub enum BlockAndTopLevelPrimitive {
    Block(Block),
    Primitive(TopLevelPrimitive),
}

Input

ブロックの入力値を表す型です。Scratch は入力形式として V2 互換形式と V3 形式の両方をサポートしており、この型でその違いを吸収します。

カスタムマクロ

types/mod.rs には型定義を簡潔に書くためのカスタムマクロが定義されています。

str_enum!

文字列と列挙型を双方向に変換できる列挙型を定義します。Scratch の JSON には opcode などが文字列で格納されているため、デシリアライズ時にこのマクロが活用されています。

str_enum! {
    pub enum BlockOpCodes {
        MotionMoveSteps => "motion_movesteps",
        MotionTurnRight => "motion_turnright",
        // ...
    }
}

str_enum_with_enum!

カテゴリ情報も付属した列挙型を定義します。

ステージ 3: パーサー ( src/parser/ )

パーサーは ScratchProject を受け取り、各スレッドを表す Vec<Thread> を返します。

スレッドとは

Scratch では、hat block(「緑の旗が押されたとき」など)がスクリプトの起点になります。hat block に続くブロック列が一つの実行単位(スレッド)です。

Thread 構造体は次のフィールドを持ちます。

pub struct Thread {
    pub hat: HatStmt,       // スレッドを起動するイベント
    pub stmts: Vec<Stmt>,   // 実行するブロック列
    pub target_idx: usize,  // どのスプライト/ステージのスレッドか
}

hat block の種類

HatStmt 列挙型は 10 種類の hat block を表します。

バリアント Scratch での表示 opcode
WhenFlagClicked 緑の旗が押されたとき EventWhenFlagClicked
WhenKeyPressed { key } [キー] キーが押されたとき EventWhenKeyPressed
WhenThisSpriteClicked このスプライトが押されたとき EventWhenThisSpriteClicked
WhenStageClicked ステージが押されたとき EventWhenStageClicked
WhenBacdropSwitchesTo { backdrop } 背景が [名前] に切り替わったとき EventWhenBackdropSwitchesTo
WhenGreaterThan { target, value } [音量/タイマー] [値] より大きくなったとき EventWhenGreaterThan
WhenBroadcastReceived { target } [メッセージ] を受け取ったとき EventWhenBroadcastReceived
ControlStartAsClone クローンされたとき ControlStartAsClone
ProcedureDefinition { prototype } 手続き定義(独自ブロック) ProceduresDefinition
WhenTouchingObject { object } [対象] に触れたとき EventWhenTouchingObject

パースの流れ

project_parser 関数は次の手順でスレッドを抽出します。

  1. すべてのターゲット(スプライト・ステージ)をループで処理する
  2. 各ターゲットのすべてのブロックをループで処理する
  3. top_level: true かつ hat block の opcode を持つブロックを見つける
  4. hat block の種別を hat.rs で判定して HatStmt に変換する
  5. next フィールドをたどって続くブロック列を解析し、 Vec<Stmt> を作る
  6. Thread { hat, stmts, target_idx } を生成して結果リストに追加する

AST 型

パーサーが生成する AST は parser/types.rs で定義されています。

Stmt (文ブロック)

pub enum Stmt {
    Motion(MotionStmt),       // 動き
    Looks(LooksStmt),         // 見た目
    Sound(SoundStmt),         // 音
    Event(EventStmt),         // イベント
    Control(ControlStmt),     // 制御
    Sensing(SensingStmt),     // 調べる
    Operator(OperatorStmt),   // 演算 (文はほぼない)
    DataStmt(DataStmt),       // データ
    Procedures(ProceduresStmt), // 独自ブロック
    PenStmt(PenStmt),         // ペン
}

各カテゴリは対応する Scratch ブロックのすべての入力・フィールドを保持します。例えば MotionStmt::GlideToXY は秒数・X 座標・Y 座標を Expr として保持します。

Expr (値ブロック / レポーター)

pub enum Expr {
    Motion(MotionExpr),
    Looks(LooksExpr),
    Sound(SoundExpr),
    Event(EventExpr),
    Control(ControlExpr),
    Sensing(SensingExpr),
    Operator(OperatorExpr),
    Data(DataExpr),
    Procedures(ProceduresExpr),
    Pen(PenExpr),
    Literal(Literal),  // 数値・文字列・変数参照などのリテラル
}

Expr はネストできます。例えば「 (10 + x座標) 歩動かす」は次のように表現されます。

MotionStmt::MoveStep {
    steps: Expr::Operator(OperatorExpr::Add {
        left: Box::new(Expr::Literal(Literal::Number("10".to_string()))),
        right: Box::new(Expr::Motion(MotionExpr::XPosition)),
    })
}

Literal (入力プリミティブ)

Literal はブロック以外の生の入力値を表します。

pub enum Literal {
    String(String),              // テキスト入力
    Number(String),              // 数値入力(文字列として保持)
    Variable { target: String }, // 変数参照 (変数 ID)
    List { target: String },     // リスト参照 (リスト ID)
    Color { color: String },     // 色 (#RRGGBB 形式)
    Broadcast { id: String },    // ブロードキャスト参照
    Null,                        // 空入力
}

数値は String として保持されており、IR 生成時に f64 へ変換されます。これは Scratch の数値が浮動小数点演算を前提としているためです。

カテゴリ別パーサー

parser/blocks/ ディレクトリには、カテゴリごとのパーサーが 11 ファイルあります。

ファイル 担当カテゴリ 対応ブロック数(概算)
motion.rs 動き 17 Stmt + 8 Expr
looks.rs 見た目 21 Stmt + 5 Expr
sound.rs 8 Stmt + 4 Expr
event.rs イベント 2 Stmt
control.rs 制御 15 Stmt + 2 Expr
sensing.rs 調べる 3 Stmt + 22 Expr
operator.rs 演算 18 Expr
data.rs データ 11 Stmt + 6 Expr
procedures.rs 独自ブロック 1 Stmt + 3 Expr
pen.rs ペン 13 Stmt + 1 Expr

エラー処理

ParserError 列挙型でパースエラーを表現します。

pub enum ParserError<'a> {
    NotHandledOp(BlockOpCodes),       // 未実装 opcode
    InvalidValue(&'a str),            // 不正な値
    UnknownBlock(String),             // 未知のブロック ID
    InvalidTargetIndex(usize),        // 不正なターゲットインデックス
    UnexpectedTopLevelPrimitive(String), // 予期しないプリミティブ
    Context {
        context: String,              // コンテキスト情報(ブロック ID など)
        source: Box<ParserError<'a>>, // 原因エラー
    },
}

Context バリアントにより、エラーが発生したブロック ID やターゲットインデックスを連鎖的に付加できます。これにより「どのスプライトのどのブロックで失敗したか」が分かります。

ステージ 4: IR 生成 ( src/compiler/ )

コンパイラは Vec<Thread> を受け取り、各スレッドを LLVM IR の関数として生成します。

Builders 構造体

compiler/types.rs では inkwell の主要オブジェクトをまとめた Builders 構造体が定義されています。

pub struct Builders<'ctx> {
    pub context: &'ctx Context,     // LLVM コンテキスト(型・定数の作成に使用)
    pub module: Module<'ctx>,       // LLVM モジュール(関数・グローバルの格納先)
    pub builder: Builder<'ctx>,     // IR 命令を追加するビルダー
    // スプライトごとのグローバル変数
    // (X 座標、Y 座標、向きなどを float64 のグローバル変数として保持)
}

スレッドごとの IR 生成

Thread は次のようにして LLVM 関数に変換されます。

// 関数のシグネチャ: void thread_N(ptr)
let fn_type = context.void_type().fn_type(&[ptr_type.into()], false);
let function = module.add_function(&function_name, fn_type, None);

// エントリブロックを作成してビルダーを配置
let entry = context.append_basic_block(function, "entry");
builder.position_at_end(entry);

// 各 Stmt を IR に変換
for stmt in &thread.stmts {
    match stmt {
        Stmt::Motion(v) => parse_motion_stmt(builders, v, &function, strings, target_idx),
        _ => todo!("やります"),  // 未実装
    }
}

builder.build_return(None);  // void return

グローバル変数

各スプライトの状態(X 座標、Y 座標、向きなど)は f64 型の LLVM グローバル変数として表現されます。例えば、スプライト 0 の X 座標は @sprite_0_x というグローバル変数です。

動き系命令( MotionSetX など)はこれらのグローバル変数を読み書きする命令( load / store )として変換されます。

式の IR 変換

generate_expr_ir 関数が式を LLVM の値に変換します。戻り値の型は ScratchReturnTypes 列挙型で表現されます。

pub enum ScratchReturnTypes<'ctx> {
    Number(FloatValue<'ctx>),                    // f64 の LLVM 値
    String(PointerValue<'ctx>),                  // 文字列ポインタ
    Bool(IntValue<'ctx>),                        // i1 の LLVM 値(真偽値)
    NumberLiteral(f64),                          // コンパイル時定数(数値)
    StringLiteral((String, PointerValue<'ctx>)), // コンパイル時定数(文字列)
    BoolLiteral((bool, IntValue<'ctx>)),         // コンパイル時定数(真偽値)
}

Literal バリアントは実際の LLVM 値と Rust 側の定数の両方を保持します。これにより、定数畳み込みなどの最適化が可能になります。

最適化

IR 生成後、 default<O3> パスが適用されます。有効化されている最適化オプション:

  • ループインターリービング : ループの実行順序を最適化してキャッシュ効率を上げる
  • ループベクトル化 : SIMD 命令を使って複数の計算を同時処理
  • SLP ベクトル化 : 隣接するスカラー演算をベクトル演算にまとめる
  • ループアンロール : ループを展開して分岐オーバーヘッドを削減
ビルドスクリプト ( build.rs )

cargo build 時に build.rs が実行され、 bitcodes/c/ にあるトップレベルの C ソースを LLVM bitcode に変換します。

現在のターゲット:

C ソース 生成物 役割
bitcodes/c/dtoa.c bitcodes/bc/dtoa.bc , bitcodes/ll/dtoa.ll 浮動小数点数を文字列に変換するライブラリ
bitcodes/c/atod.c bitcodes/bc/atod.bc , bitcodes/ll/atod.ll 文字列を浮動小数点数へ変換する補助ライブラリ
bitcodes/c/to_lower.c bitcodes/bc/to_lower.bc , bitcodes/ll/to_lower.ll Scratch 文字列の真偽値判定に使う自己完結の補助ライブラリ

dtoa.c は将来、生成した IR とリンクして数値→文字列変換( (0.1 + 0.2) の結果を表示するときなど)に使用することを想定しています。 atod.c to_lower.c は、Scratch 文字列の数値化・真偽値化を補助するために使われます。

to_lower.c は vendored ICU と連携する補助ライブラリです。標準運用では XYO_ICU_ROOT で指定した ICU source tree、または bitcodes/c/lib/icu-prebuilt include/ を使って to_lower.bc / to_lower.ll を軽量生成し、実際の動作確認は同じ ICU から生成した prebuilt static archive を tools/check_to_lower_native.sh から native link します。 to_lower.c と ICU ソース群を個別に bitcode 化して llvm-link で自己完結化する重い経路は、 XYO_EMBED_ICU_BITCODE=1 を付けたときだけ有効です。

ビルドスクリプトは次の手順で動作します:

  1. clang の実行パスを解決する( CLANG PATH 上の clang llvm-config --bindir / LLVM_CONFIG_PATH の順)
  2. 通常の C ファイルは clang -emit-llvm -c clang -S -emit-llvm .bc / .ll を生成する
  3. to_lower.c は通常は vendored ICU ヘッダだけを使って軽量に .bc / .ll を生成する
  4. XYO_EMBED_ICU_BITCODE=1 のときだけ ICU ソース群も個別に .bc 化して llvm-link で結合する
  5. 必要に応じて llvm-dis で最終的な .ll を生成する
  6. 出力を bitcodes/bc/ bitcodes/ll/ に保存する
制約と今後の方針

現在の制約

  • IR 生成 : 動き系命令と演算子のみ。 run で残りの opcode に当たると todo!() パニックが起きる
  • ランタイム : 生成した IR を実際に実行する仕組みがない
  • コスチューム・サウンド : メタデータは読み込めるが IR 生成には未対応
  • スレッド間通信 : ブロードキャスト・メッセージ処理は未実装

今後の実装が期待される部分

  • ControlStmt (if/else, repeat, forever など) の IR 生成
  • DataStmt (変数操作) の IR 生成
  • LooksStmt (見た目変更) の IR 生成(実際のレンダリングは別ライブラリが必要)
  • run の未実装分岐に対する安全なフォールバック(パニックを避けてエラー報告する)
  • 生成した IR を clang llc でリンク・コンパイルするフロー

前のページ: CLI