はじめに / セットアップ / CLI / 対応ブロック一覧
xyo-rust の処理は、大きく分けると 5 段階です。
.sb3からproject.jsonを取り出す- Scratch プロジェクト全体を Rust の構造体へ変換する
- hat block を起点にスレッド単位の中間表現へ変換する
- 一部のスレッドを LLVM IR へ変換する
- 生成した IR を JIT で実行し、状態を定期表示する
Scratch VM 相当の完全な実行ランタイムはまだ完成していないため、現在の中心は解析・変換・JIT 実行のパイプラインです。
| モジュール | 役割 |
|---|---|
src/sb3.rs |
ZIP アーカイブとして .sb3 を開き、project.json を読み込む。失敗時には位置情報付きのエラー整形も担当する |
src/types/ |
Scratch の JSON 構造を受ける型定義を提供する |
src/parser/ |
Scratch ブロックを Stmt / Expr に変換し、hat block からスレッド単位に解析する |
src/compiler/ |
inkwell を使って LLVM IR を生成し、最適化パスと JIT 実行を担当する |
.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
├── 実行状態 (SpriteStruct)
├── 文字列リテラル (StringStruct)
├── 関数 @thread_0(ptr) → スレッドの IR
├── 関数 @thread_1(ptr) → スレッドの IR
└── ...
│
│ run_passes("default<O3>")
▼
JIT 実行
├── thread_0(ptr state) を呼ぶ
└── 変更後の状態を標準出力へ表示
.sb3 ファイルは ZIP アーカイブです。sb3.rs はこのアーカイブを開いて project.json を取り出す処理を担当します。
// 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>File::open(path)でファイルを開くZipArchive::new(stream)で ZIP として読み込むarchive.by_name("project.json")でエントリを取得する- バイト列として読み込む
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 行の周辺テキスト(長い行は ... で省略)と ^ カーソルが表示されます。
types/ モジュールは Scratch の project.json 構造を Rust の型として表現します。
プロジェクト全体を表すルート構造体です。
pub struct ScratchProject {
pub targets: Vec<StageOrSprite>,
pub meta: Metadata,
// ...
}count_blocks() メソッドでブロック総数を、check_op_codes() メソッドで使用 opcode 一覧を取得できます。
ステージとスプライトを統合した型です。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>)>,
// ...
}個々のブロックを表す型です。
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: ブロック連鎖のリンク
ブロック辞書の値型です。通常のブロックと、ブロック参照なしに直接埋め込まれるプリミティブ値の両方を表します。
pub enum BlockAndTopLevelPrimitive {
Block(Block),
Primitive(TopLevelPrimitive),
}ブロックの入力値を表す型です。Scratch は入力形式として V2 互換形式と V3 形式の両方をサポートしており、この型でその違いを吸収します。
types/mod.rs には型定義を簡潔に書くためのカスタムマクロが定義されています。
文字列と列挙型を双方向に変換できる列挙型を定義します。Scratch の JSON には opcode などが文字列で格納されているため、デシリアライズ時にこのマクロが活用されています。
str_enum! {
pub enum BlockOpCodes {
MotionMoveSteps => "motion_movesteps",
MotionTurnRight => "motion_turnright",
// ...
}
}カテゴリ情報も付属した列挙型を定義します。
パーサーは ScratchProject を受け取り、各スレッドを表す Vec<Thread> を返します。
Scratch では、hat block(「緑の旗が押されたとき」など)がスクリプトの起点になります。hat block に続くブロック列が一つの実行単位(スレッド)です。
Thread 構造体は次のフィールドを持ちます。
pub struct Thread {
pub hat: HatStmt, // スレッドを起動するイベント
pub stmts: Vec<Stmt>, // 実行するブロック列
pub target_idx: usize, // どのスプライト/ステージのスレッドか
}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 関数は次の手順でスレッドを抽出します。
- すべてのターゲット(スプライト・ステージ)をループで処理する
- 各ターゲットのすべてのブロックをループで処理する
top_level: trueかつ hat block の opcode を持つブロックを見つける- hat block の種別を
hat.rsで判定してHatStmtに変換する nextフィールドをたどって続くブロック列を解析し、Vec<Stmt>を作るThread { hat, stmts, target_idx }を生成して結果リストに追加する
パーサーが生成する AST は parser/types.rs で定義されています。
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 として保持します。
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 はブロック以外の生の入力値を表します。
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 |
動き | 18 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 やターゲットインデックスを連鎖的に付加できます。これにより「どのスプライトのどのブロックで失敗したか」が分かります。
コンパイラは Vec<Thread> を受け取り、各スレッドを LLVM IR の関数として生成します。CLI の compile --output <path> はモジュール全体の LLVM IR を指定パスへ保存し、run は生成した関数を JIT で実行して状態を定期表示します。
compiler/types.rs では、LLVM の基本オブジェクトに加えて、実行時 ABI や変数割り当てをまとめた Builders 構造体が定義されています。
Builders が保持する主な情報は次のとおりです。
context/module/builder: LLVM の基本オブジェクトfunctions:llvm.*intrinsic、fmin/fmaxとxyo_atod,xyo_dtoa,xyo_bool_to_str,xyo_str_cmp_gt,xyo_str_cmp_lt,xyo_str_cmp_eq,str_to_bool,str_is_num,xorshift128plusの関数ハンドルglobal_variables/local_variables: Scratch の変数 ID をスロット番号へ割り当てる表get_variable(): 変数 ID をVariableInfoに解決して、グローバルかローカルかとスロット番号を返すrolling_hash_seed_*/rolling_hash_base_*: 文字列ハッシュ生成に使う定数string_literals: 生成済みのStringStructグローバルを再利用するキャッシュ
#[repr(C)]
pub struct StringStruct {
pub length: u64,
pub container: *mut u16,
pub hash1: u64,
pub hash2: u64,
}
#[repr(C)]
pub struct SpriteStruct {
pub sprite_x: f64,
pub sprite_y: f64,
pub sprite_rotate: f64,
pub sprite_size: f64,
pub sprite_costume_id: i64,
pub sprite_costumes: *mut CostumeInfo,
pub sprite_costume_number: i64,
pub sprite_rotation_style: i8,
}StringStruct は UTF-16 の文字列本体と 2 系統のローリングハッシュを持ち、SpriteStruct はスプライトの実行状態を表します。SpriteStruct には位置・向きだけでなく、大きさ、コスチューム配列、コスチューム数、回転方法も含まれます。StringKeys と SpriteKeys はこれらの構造体の field index を LLVM から扱いやすくするための列挙型です。
動的に型が決まる値は DynamicStruct で表します。
#[repr(u8)]
pub enum DynamicKind {
String = 0,
Number = 1,
Bool = 2,
}
#[repr(C)]
pub struct DynamicStruct {
pub kind: DynamicKind,
pub pointer: *mut core::ffi::c_void,
}DynamicStruct は kind と payload pointer を持ち、文字列 / 数値 / 真偽値の coercion や比較で実行時分岐するために使います。
各 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, thread.target_idx),
Stmt::Looks(v) => parse_looks_stmt(builders, v, &function, thread.target_idx),
Stmt::DataStmt(v) => parse_data_stmt(builders, v, &function, thread.target_idx),
Stmt::Control(v) => parse_control_stmt(builders, v, &function, thread.target_idx),
Stmt::Sensing(v) => parse_sensing_stmt(builders, v, &function, thread.target_idx),
_ => todo!("やります"), // 未実装
}
}
builder.build_return(None); // void return各スレッドは SpriteStruct の状態ポインタを受け取り、MotionSetX や LooksSetSizeTo などは build_struct_gep を使ってそのフィールドを更新します。大きさ変更系の Looks ブロックは、現在コスチュームの幅・高さから Scratch と同じ最小・最大スケールを計算し、fmin / fmax で丸めた値を sprite_size に保存します。LooksSay / LooksThink は print ヘルパーに文字列を渡し、DataSetVariableTo と DataChangeVariableBy は対象変数 ID を解決してグローバル変数スロットへ反映します。ControlRepeat / ControlForever / ControlIf / ControlIfElse / ControlWaitUntil は、サブスタックを別関数として呼び出すか、wait_tick を挟むループに変換されます。JIT 実行時にも同じ SpriteStruct が Debug 形式で表示されます。
generate_expr_ir 関数が式を LLVM の値に変換します。戻り値の型は ScratchReturnTypes 列挙型で表現されます。
pub enum ScratchReturnTypes<'ctx> {
Number(FloatValue<'ctx>), // f64 の LLVM 値
String(PointerValue<'ctx>), // 文字列ポインタ
Bool(IntValue<'ctx>), // i1 の LLVM 値(真偽値)
NumberLiteral((f64, FloatValue<'ctx>)), // コンパイル時定数(数値)
StringLiteral((String, PointerValue<'ctx>)), // コンパイル時定数(文字列)
BoolLiteral((bool, IntValue<'ctx>)), // コンパイル時定数(真偽値)
Dynamic(PointerValue<'ctx>), // kind + payload pointer
}Literal バリアントは実際の LLVM 値と Rust 側の定数の両方を保持します。これにより、定数畳み込みなどの最適化が可能になります。String / StringLiteral は StringStruct で表現された文字列を指し、BoolLiteral / NumberLiteral は coercion のための定数としても使われます。
Dynamic は実行時に DynamicKind を見て Number / String / Bool へ分岐します。変数参照はグローバル変数スロットから Dynamic として読み出されます。scratch_return_to_number, scratch_return_to_string, scratch_return_to_bool, is_num は、静的型だけでなく Dynamic 値も扱います。OperatorGt / OperatorLt / OperatorEquals は片側が文字列または Dynamic の場合、文字列比較と数値比較の分岐を生成します。
IR 生成後、default<O3> パスが適用されます。有効化されている最適化オプション:
- ループインターリービング: ループの実行順序を最適化してキャッシュ効率を上げる
- ループベクトル化: SIMD 命令を使って複数の計算を同時処理
- SLP ベクトル化: 隣接するスカラー演算をベクトル演算にまとめる
- ループアンロール: ループを展開して分岐オーバーヘッドを削減
cargo build 時に build.rs が実行され、bitcodes/c/ にあるトップレベルの C ソースを LLVM bitcode と LLVM IR に変換します。各ソースは clang -emit-llvm -c -O3 で .bc、clang -S -emit-llvm -O3 で .ll に変換され、出力は bitcodes/bc/ と bitcodes/ll/ に保存されます。生成済み .bc は embedded_bitcodes.rs にも列挙されます。
現在のターゲット:
| C ソース | 生成物 | 役割 |
|---|---|---|
bitcodes/c/numeric.c |
bitcodes/bc/numeric.bc, bitcodes/ll/numeric.ll |
数値変換と数値判定のヘルパー |
bitcodes/c/str.c |
bitcodes/bc/str.bc, bitcodes/ll/str.ll |
文字列比較と真偽値変換のヘルパー |
bitcodes/c/tick.c |
bitcodes/bc/tick.bc, bitcodes/ll/tick.ll |
単調時計と sleep のヘルパー |
numeric.c は bitcodes/c/lib/dtoa.c と bitcodes/c/lib/cutils.c を取り込み、xyo_atod / xyo_dtoa / str_is_num / str_is_double を実装します。str.c は ICU の Unicode API を使って string_to_bool と文字列比較を実装し、tick.c は xyo_now_ns / xyo_sleep_until_ns を提供します。
build.rs は CLANG、PATH 上の clang、llvm-config --bindir の順で clang を解決し、bitcodes/c/lib と ICU の include path を追加して各ソースをコンパイルします。ICU ヘッダは XYO_ICU_PREBUILT_DIR/include か XYO_ICU_ROOT/source/common から見つけます。
ビルドスクリプトは次の手順で動作します:
CLANG、PATH上のclang、llvm-config --bindirの順でclangを解決するbitcodes/c/配下のトップレベル.cを列挙する- 各ソースに
-I bitcodes/c/libと ICU の include path を足してclang -emit-llvm -c -O3とclang -S -emit-llvm -O3を実行する - 生成した
.bcをbitcodes/bc/、.llをbitcodes/ll/に保存する .bc一覧をもとにembedded_bitcodes.rsを書き出す
- IR 生成: スレッド本体は動き系の一部、見た目の say/think と大きさ変更、変数代入と加算、制御の repeat/forever/if/ifelse/wait until、タイマーリセットのみ。式はリテラル、演算子、変数参照、見た目の大きさレポーター、タイマーレポーターが中心。
runで残りの文 opcode や未実装式に当たるとtodo!()パニックが起きる - ランタイム: Scratch のイベントループや broadcast / clone を含む完全な VM は未実装。いまの
runは JIT で各 thread を実行し、状態を標準出力へ定期的に返す - コスチューム・サウンド: コスチューム寸法は大きさの丸めに使うが、コスチューム切り替えやサウンド再生の IR 生成は未対応
- スレッド間通信: ブロードキャスト・メッセージ処理は未実装
ControlStmtの残り分岐(wait, repeat until, while, clone, stop, counter など)の IR 生成DataExpr/DataStmtのリスト系 IR 生成LooksStmtの残り分岐(say/think 以外の見た目変更)の IR 生成(実際のレンダリングは別ライブラリが必要)runの未実装分岐に対する安全なフォールバック(パニックを避けてエラー報告する)- 生成した IR を
clangやllcでリンク・コンパイルするフロー
前のページ: CLI
次のページ: 開発メモ