diff --git a/.github/workflows/mir-golden-ci.yml b/.github/workflows/mir-golden-ci.yml new file mode 100644 index 00000000..f673bd63 --- /dev/null +++ b/.github/workflows/mir-golden-ci.yml @@ -0,0 +1,28 @@ +name: MIR Golden CI + +on: + push: + pull_request: + +jobs: + build-and-compare: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Build (release) + run: cargo build --release + + - name: Make tools executable + run: chmod +x tools/*.sh + + - name: Compare MIR against golden snapshots + run: bash tools/ci_check_golden.sh + diff --git a/CLAUDE.md b/CLAUDE.md index da1d7894..36d92cc7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -308,6 +308,135 @@ Read docs/reference/ # まずドキュメント(API/言語仕様の入口) # → それでも不明 → ソース確認 ``` +## 🏗️ 開発設計原則(綺麗で破綻しない作り) + +### 📦 Everything is Box - 内部実装でも箱原理を貫く + +#### 1. **単一責任の箱** +```rust +// ✅ 良い例:各モジュールが単一の責任を持つ +MirBuilder: AST → MIR変換のみ(最適化しない) +MirOptimizer: MIRの最適化のみ(変換しない) +VM: 実行のみ(最適化しない) + +// ❌ 悪い例:複数の責任が混在 +BuilderOptimizer: 変換も最適化も実行も... +``` + +#### 2. **明確なインターフェース** +```rust +// ✅ エフェクトは単純に +enum Effect { + Pure, // 副作用なし + ReadOnly, // 読み取りのみ + SideEffect // 書き込み/IO/例外 +} + +// ❌ 複雑な組み合わせは避ける +PURE.add(IO) // これがpureかどうか分からない! +``` + +#### 3. **段階的な処理パイプライン** +``` +AST → Builder → MIR → Optimizer → VM + ↑ ↑ ↑ ↑ ↑ +明確な入力 明確な出力 不変保証 最適化のみ 実行のみ +``` + +### 🎯 カプセル化の徹底 + +#### 1. **内部状態を隠蔽** +```rust +// ✅ 良い例:内部実装を隠す +pub struct MirOptimizer { + debug: bool, // 設定のみ公開 + enable_typeop_net: bool, // 設定のみ公開 + // 内部の複雑な状態は隠蔽 +} + +impl MirOptimizer { + pub fn new() -> Self { ... } + pub fn with_debug(self) -> Self { ... } + pub fn optimize(&mut self, module: &mut MirModule) { ... } +} +``` + +#### 2. **変更の局所化** +- 新機能追加時:1つのモジュールのみ変更 +- バグ修正時:影響範囲が明確 +- テスト:各モジュール独立でテスト可能 + +### 🌟 美しさの基準 + +#### 1. **読みやすさ > 賢さ** +```rust +// ✅ 単純で分かりやすい +if effect == Effect::Pure { + can_eliminate = true; +} + +// ❌ 賢いが分かりにくい +can_eliminate = effect.0 & 0x01 == 0x01 && !(effect.0 & 0xFE); +``` + +#### 2. **一貫性** +- 命名規則の統一 +- エラー処理の統一 +- コメントスタイルの統一 + +### 🚀 大規模化への備え + +#### 1. **モジュール分割の原則** +``` +src/ +├── ast/ # AST定義のみ +├── parser/ # パース処理のみ +├── mir/ # MIR定義と基本操作 +│ ├── builder.rs # AST→MIR変換 +│ └── optimizer.rs # MIR最適化 +├── backend/ # 実行バックエンド +│ ├── interpreter.rs # インタープリター +│ ├── vm.rs # VM実行 +│ └── codegen_c.rs # C言語生成 +``` + +#### 2. **テストの階層化** +- 単体テスト:各モジュール内で完結 +- 統合テスト:モジュール間の連携 +- E2Eテスト:全体の動作確認 + +#### 3. **設定の外部化** +```rust +// ✅ フラグで挙動を制御(再コンパイル不要) +optimizer.enable_typeop_safety_net(flag); + +// ❌ ハードコードされた挙動 +#[cfg(feature = "typeop_safety_net")] +``` + +### 💡 デバッグとメンテナンス + +#### 1. **段階的なデバッグ出力** +```bash +NYASH_BUILDER_DEBUG=1 # Builder のみ +NYASH_OPT_DEBUG=1 # Optimizer のみ +NYASH_VM_DEBUG=1 # VM のみ +``` + +#### 2. **問題の早期発見** +- 各段階でのアサーション +- 不変条件の明示的チェック +- 診断機能の組み込み + +### 🎭 複雑さの管理 + +**複雑さは避けられないが、管理はできる** +1. 複雑な部分を局所化 +2. インターフェースは単純に +3. ドキュメントで意図を明示 + +**判断基準:3ヶ月後の自分が理解できるか?** + ## 🔧 開発サポート ### 🤖 AI相談 diff --git a/docs/development/current/CURRENT_TASK.md b/docs/development/current/CURRENT_TASK.md index 2d18f81e..c089ca2f 100644 --- a/docs/development/current/CURRENT_TASK.md +++ b/docs/development/current/CURRENT_TASK.md @@ -45,6 +45,8 @@ 2) Builder適用拡大(短期〜中期) - 言語 `is/as` 導線(最小でも擬似ノード)→ `emit_type_check/emit_cast` へ配線 - 弱参照: 既存の `RefGet/RefSet` パスは弱フィールドで `WeakLoad/WeakNew`+Barrier(flag ONで統合命令) + - 関数スタイル `isType/asType` の早期loweringを強化(`Literal("T")` と `new StringBox("T")` を確実に検出) + - `print(isType(...))` の未定義SSA回避(print直前で必ず `TypeOp` のdstを生成) 3) VM/Verifierの補強(中期) - `TypeOp(Cast)` の数値キャスト(Int/Float)安全化、誤用時TypeError整備 @@ -57,6 +59,44 @@ 5) BoxCall高速化(性能段階) - `--vm-stats` ホットパス特定後、Fast-path/キャッシュ適用 +## 🐛 既知の問題(要フォロー) +- 関数スタイル `isType(value, "Integer")` が一部ケースで `TypeOp` にloweringされず、`print %X` が未定義参照になる事象 + - 現状: `asType` は `typeop cast` に変換されるが、`isType` が欠落するケースあり + - 仮対処: Interpreterに `is/isType/as/asType` のフォールバックを実装済(実行エラー回避) + - 恒久対処(次回対応): + - Builderの早期loweringをFunctionCall分岐で強化(`Literal`/`StringBox`両対応、`print(...)` 内でも確実にdst生成) + - Optimizerの安全ネット(BoxCall/Call→TypeOp)を `isType` パターンでも確実に発火させる(テーブル駆動の判定) + +## ⏸️ セッション再開メモ(次にやること) +- [ ] Builder: `extract_string_literal` の `StringBox`対応は導入済 → `FunctionCall` 早期loweringの再検証(`print(isType(...))` 直下) +- [ ] Optimizer: `Call` 形式(関数呼び出し)でも `isType/asType` を検出して `TypeOp(Check/Cast)` に置換する安全ネットの強化とテスト +- [ ] MIRダンプの確認:`local_tests/typeop_is_as_func_poc.nyash` に `typeop check/cast` が両方出ることを確認 +- [ ] スナップショット化:`typeop_is_as_*_poc.nyash` のダンプを固定し回帰検出 + +## ▶ 補助コマンド(検証系) +```bash +# リビルド +cargo build --release -j32 + +# 関数スタイルのMIRダンプ確認(isType/asType) +./target/release/nyash --dump-mir --mir-verbose local_tests/typeop_is_as_func_poc.nyash | sed -n '1,200p' + +# メソッドスタイルのMIRダンプ確認(is/as) +./target/release/nyash --dump-mir --mir-verbose local_tests/typeop_is_as_poc.nyash | sed -n '1,200p' +``` + +### 🆕 開発時の可視化・診断(最小追加) +- `--mir-verbose-effects`: MIRダンプ行末に効果カテゴリを表示(`pure|readonly|side`) + - 例: `nyash --dump-mir --mir-verbose --mir-verbose-effects local_tests/typeop_is_as_func_poc.nyash` +- `NYASH_OPT_DIAG_FAIL=1`: Optimizer診断で未lowering(`is|isType|as|asType`)検知時にエラー終了(CI向け) + - 例: `NYASH_OPT_DIAG_FAIL=1 nyash --dump-mir --mir-verbose local_tests/typeop_diag_fail.nyash` +- Builder生MIRスナップショット: `tools/snapshot_mir.sh [output.txt]` + - 例: `tools/snapshot_mir.sh local_tests/typeop_is_as_func_poc.nyash docs/status/golden/typeop_is_as_func_poc.mir.txt` +- ゴールデン比較(ローカル/CI): `tools/ci_check_golden.sh`(代表2ケースを比較) + - 例: `./tools/ci_check_golden.sh`(差分があれば非ゼロ終了) + +補足: ASTの形状確認は `--dump-ast` を使用。 + ## ▶ 実行コマンド例 計測実行: diff --git a/docs/development/roadmap/README.md b/docs/development/roadmap/README.md index 3d6baf4d..64adea14 100644 --- a/docs/development/roadmap/README.md +++ b/docs/development/roadmap/README.md @@ -45,6 +45,8 @@ - AOT WASMネイティブ化 - MIR最適化基盤 - エスケープ解析実装 +- MIR/Builder/Optimizer簡略化計画(責務分離・効果正規化・可視化) + - [Phase 8.x: MIRパイプライン簡略化計画](phases/phase-8/phase_8_x_mir_pipeline_simplification.md) ## 📚 関連ドキュメント diff --git a/docs/development/roadmap/phases/phase-8/phase_8_x_mir_pipeline_simplification.md b/docs/development/roadmap/phases/phase-8/phase_8_x_mir_pipeline_simplification.md new file mode 100644 index 00000000..abdf9a4f --- /dev/null +++ b/docs/development/roadmap/phases/phase-8/phase_8_x_mir_pipeline_simplification.md @@ -0,0 +1,42 @@ +# Phase 8.x: MIR/Builder/Optimizer 簡略化計画(責務分離・効果正規化・可視化) + +## 🎯 目的 +- AST→MIR→Optimizer→Backend の多層化で増した複雑さを段階的に解消し、堅牢性と見通しを改善する。 +- DCEの誤削除や多重防御(重複lowering)を撲滅し、責務を明確化する。 + +## ✅ 方針(要約) +- **責務分離**: Builder=変換のみ / Optimizer=最適化のみ(変換しない)。 +- **効果正規化**: `Pure / ReadOnly / SideEffect` の3分類で最適化判定を一元化。 +- **可視化**: 段階別のMIRダンプと効果サマリ、スナップショットテストを整備。 + +## Phase 1: エフェクト見直し(短期)✅ 完了 +- `EffectMask::primary_category()` を唯一の根拠に `is_pure()` を再定義(実装済)。 +- DCE/並べ替えは `primary_category` で判定(Pureのみ削除/自由移動、ReadOnlyは限定、SideEffectは不可)。 +- 単体テスト: 効果判定(PURE|IO は pure ではない)、DCEが副作用命令(print使用のTypeOp等)を削除しないこと(追加済)。 +- 可視化: `--mir-verbose-effects` で per-instruction 効果カテゴリ表示(追加済)。 +- CI導線(任意): `NYASH_OPT_DIAG_FAIL=1` で未lowering検出時にfail(診断ゲート・追加済)。 + +## Phase 2: 変換の一本化(中期)✅ 完了 +- **TypeOp loweringをBuilderに集約**(関数/メソッド/print直下/多重StringBox対応は実装済)。 +- OptimizerのTypeOp安全ネット(実変換)を削除。診断のみ存続(`NYASH_OPT_DIAG_FAIL`連携)。 +- スナップショット: 代表 `*_is_as_*` のMIR(Builder出力)を固定化(`tools/compare_mir.sh`)。 + +## Phase 3: デバッグ支援強化(短期〜中期) +- `--dump-ast`(実装済)に加え、`--dump-mir --no-optimize`(Builder出力の生MIR)を追加。 +- MIRプリンタの詳細表示: 命令行末に `pure/readonly/side` の効果表示(オプション)。 +- ゴールデンMIR: 代表サンプルのMIRダンプを保存し差分検出(CI/ローカル)。 + +## タスク一覧(実装順) +1) OptimizerのTypeOp安全ネットを機能フラグでデフォルトOFF(`mir_typeop_safety_net`) +2) `MirCompiler` に `--no-optimize` 経由のBuilder直ダンプを実装 +3) MIRプリンタに効果簡易表示オプション(`--mir-verbose-effects` 等) +4) 効果判定の単体テスト / DCE安全テストの追加 +5) Optimizer診断パス(未lowering検知)追加 + +## 期待効果 +- 変換責務の一本化でバグ源の排除・デバッグ容易化 +- エフェクト判定の一貫性でDCE/最適化の安全性向上 +- 可視化/スナップショットにより回帰を早期検知 + +--- +最終更新: 2025-08-24(Phase 1完了: is_pure修正/テスト/効果可視化/診断ゲート) diff --git a/docs/status/golden/typeop_in_if_loop_poc.mir.txt b/docs/status/golden/typeop_in_if_loop_poc.mir.txt new file mode 100644 index 00000000..76bbd385 --- /dev/null +++ b/docs/status/golden/typeop_in_if_loop_poc.mir.txt @@ -0,0 +1,80 @@ +🔌 v2 plugin system initialized from nyash.toml +✅ v2 plugin system fully configured +🚀 Nyash MIR Compiler - Processing file: local_tests/typeop_in_if_loop_poc.nyash 🚀 +🚀 MIR Output for local_tests/typeop_in_if_loop_poc.nyash: +; MIR Module: main + +; Module Statistics: +; Functions: 1 +; Globals: 0 +; Total Blocks: 7 +; Total Instructions: 38 +; Pure Functions: 1 + +define void @main() { + ; Function Statistics: + ; Blocks: 7 + ; Instructions: 38 + ; Values: 0 + ; Phi Functions: 3 + ; Pure: yes + ; TypeOp: 2 (check: 2, cast: 0) + +bb0: + 0: safepoint + 1: %1 = const 1 + 2: %2 = new IntegerBox(%1) + 3: call %2.birth(%1) + 4: %3 = typeop check %2 Integer + 5: br %3, label bb1, label bb2 + ; effects: pure|read|alloc + +bb1: + 0: %4 = const "ok-if" + 1: %5 = new StringBox(%4) + 2: call %5.birth(%4) + 3: print %5 + 4: br label bb3 + ; effects: pure|io|read|alloc + +bb2: + 0: %6 = const "ng-if" + 1: %7 = new StringBox(%6) + 2: call %7.birth(%6) + 3: print %7 + 4: br label bb3 + ; effects: pure|io|read|alloc + +bb3: + 0: %8 = phi [%5, bb1], [%7, bb2] + 1: %10 = const 0 + 2: %11 = new IntegerBox(%10) + 3: call %11.birth(%10) + 4: br label bb4 + ; effects: pure|read|alloc + +bb4: ; preds(bb5) + 0: %13 = phi [%2, bb3], [%13, bb5] + 1: %12 = phi [%11, bb3], [%20, bb5] + 2: %14 = const 1 + 3: %15 = new IntegerBox(%14) + 4: call %15.birth(%14) + 5: %16 = icmp Lt %12, %15 + 6: br %16, label bb5, label bb6 + ; effects: pure|read|alloc + +bb5: + 0: safepoint + 1: %17 = typeop check %13 Integer + 2: print %17 + 3: %18 = const 1 + 4: %19 = new IntegerBox(%18) + 5: call %19.birth(%18) + 6: %20 = %12 Add %19 + 7: br label bb4 + ; effects: pure|io|read|alloc + +bb6: + 0: %21 = const void + 1: ret %21 +} diff --git a/docs/status/golden/typeop_is_as_func_poc.mir.txt b/docs/status/golden/typeop_is_as_func_poc.mir.txt new file mode 100644 index 00000000..874b2881 --- /dev/null +++ b/docs/status/golden/typeop_is_as_func_poc.mir.txt @@ -0,0 +1,34 @@ +🔌 v2 plugin system initialized from nyash.toml +✅ v2 plugin system fully configured +🚀 Nyash MIR Compiler - Processing file: local_tests/typeop_is_as_func_poc.nyash 🚀 +🚀 MIR Output for local_tests/typeop_is_as_func_poc.nyash: +; MIR Module: main + +; Module Statistics: +; Functions: 1 +; Globals: 0 +; Total Blocks: 1 +; Total Instructions: 9 +; Pure Functions: 1 + +define void @main() { + ; Function Statistics: + ; Blocks: 1 + ; Instructions: 9 + ; Values: 0 + ; Phi Functions: 0 + ; Pure: yes + ; TypeOp: 2 (check: 1, cast: 1) + +bb0: + 0: safepoint + 1: %1 = const 42 + 2: %2 = new IntegerBox(%1) + 3: call %2.birth(%1) + 4: %3 = typeop check %2 Integer + 5: print %3 + 6: %4 = typeop cast %2 Integer + 7: print %4 + 8: ret %4 + ; effects: pure|io|read|alloc +} diff --git a/docs/status/golden/typeop_is_as_poc.mir.txt b/docs/status/golden/typeop_is_as_poc.mir.txt new file mode 100644 index 00000000..8a63827a --- /dev/null +++ b/docs/status/golden/typeop_is_as_poc.mir.txt @@ -0,0 +1,38 @@ +🔌 v2 plugin system initialized from nyash.toml +✅ v2 plugin system fully configured +🚀 Nyash MIR Compiler - Processing file: local_tests/typeop_is_as_poc.nyash 🚀 +🚀 MIR Output for local_tests/typeop_is_as_poc.nyash: +; MIR Module: main + +; Module Statistics: +; Functions: 1 +; Globals: 0 +; Total Blocks: 1 +; Total Instructions: 13 +; Pure Functions: 1 + +define void @main() { + ; Function Statistics: + ; Blocks: 1 + ; Instructions: 13 + ; Values: 0 + ; Phi Functions: 0 + ; Pure: yes + ; TypeOp: 4 (check: 2, cast: 2) + +bb0: + 0: safepoint + 1: %1 = const 7 + 2: %2 = new IntegerBox(%1) + 3: call %2.birth(%1) + 4: %3 = typeop check %2 Integer + 5: print %3 + 6: %4 = typeop cast %2 Integer + 7: print %4 + 8: %5 = typeop check %2 Integer + 9: print %5 + 10: %6 = typeop cast %2 Integer + 11: print %6 + 12: ret %6 + ; effects: pure|io|read|alloc +} diff --git a/src/cli.rs b/src/cli.rs index 8f5c32c6..213eae3c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -12,9 +12,12 @@ use clap::{Arg, Command, ArgMatches}; pub struct CliConfig { pub file: Option, pub debug_fuel: Option, + pub dump_ast: bool, pub dump_mir: bool, pub verify_mir: bool, pub mir_verbose: bool, + pub mir_verbose_effects: bool, + pub no_optimize: bool, pub backend: String, pub compile_wasm: bool, pub compile_native: bool, @@ -51,6 +54,12 @@ impl CliConfig { .help("Set parser debug fuel limit (default: 100000, 'unlimited' for no limit)") .default_value("100000") ) + .arg( + Arg::new("dump-ast") + .long("dump-ast") + .help("Dump parsed AST and exit") + .action(clap::ArgAction::SetTrue) + ) .arg( Arg::new("dump-mir") .long("dump-mir") @@ -69,6 +78,18 @@ impl CliConfig { .help("Show verbose MIR output with statistics") .action(clap::ArgAction::SetTrue) ) + .arg( + Arg::new("mir-verbose-effects") + .long("mir-verbose-effects") + .help("Show per-instruction effect category (pure/readonly/side)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("no-optimize") + .long("no-optimize") + .help("Disable MIR optimizer passes (dump raw Builder MIR)") + .action(clap::ArgAction::SetTrue) + ) .arg( Arg::new("backend") .long("backend") @@ -133,9 +154,12 @@ impl CliConfig { Self { file: matches.get_one::("file").cloned(), debug_fuel: parse_debug_fuel(matches.get_one::("debug-fuel").unwrap()), + dump_ast: matches.get_flag("dump-ast"), dump_mir: matches.get_flag("dump-mir"), verify_mir: matches.get_flag("verify"), mir_verbose: matches.get_flag("mir-verbose"), + mir_verbose_effects: matches.get_flag("mir-verbose-effects"), + no_optimize: matches.get_flag("no-optimize"), backend: matches.get_one::("backend").unwrap().clone(), compile_wasm: matches.get_flag("compile-wasm"), compile_native: matches.get_flag("compile-native") || matches.get_flag("aot"), diff --git a/src/mir/builder.rs b/src/mir/builder.rs index 8c1ac61f..8f07a418 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -13,6 +13,16 @@ use crate::ast::{ASTNode, LiteralValue, BinaryOperator}; use std::collections::HashMap; use std::collections::HashSet; +fn builder_debug_enabled() -> bool { + std::env::var("NYASH_BUILDER_DEBUG").is_ok() +} + +fn builder_debug_log(msg: &str) { + if builder_debug_enabled() { + eprintln!("[BUILDER] {}", msg); + } +} + /// MIR builder for converting AST to SSA form pub struct MirBuilder { /// Current module being built @@ -606,25 +616,46 @@ impl MirBuilder { /// Build print statement - converts to console output fn build_print_statement(&mut self, expression: ASTNode) -> Result { - // Early lowering for print(isType(...)) / print(asType(...)) to avoid undefined SSA when optimizations run - if let ASTNode::FunctionCall { name, arguments, .. } = &expression { - if (name == "isType" || name == "asType") && arguments.len() == 2 { + builder_debug_log("enter build_print_statement"); + // 根治: print(isType(...)) / print(asType(...)) / print(obj.is(...)) / print(obj.as(...)) は必ずTypeOpを先に生成してからprintする + match &expression { + ASTNode::FunctionCall { name, arguments, .. } if (name == "isType" || name == "asType") && arguments.len() == 2 => { + builder_debug_log("pattern: print(FunctionCall isType|asType)"); if let Some(type_name) = Self::extract_string_literal(&arguments[1]) { + builder_debug_log(&format!("extract_string_literal OK: {}", type_name)); let val = self.build_expression(arguments[0].clone())?; let ty = Self::parse_type_name_to_mir(&type_name); let dst = self.value_gen.next(); let op = if name == "isType" { super::TypeOpKind::Check } else { super::TypeOpKind::Cast }; + builder_debug_log(&format!("emit TypeOp {:?} value={} dst= {}", op, val, dst)); self.emit_instruction(MirInstruction::TypeOp { dst, op, value: val, ty })?; - self.emit_instruction(MirInstruction::Print { - value: dst, - effects: EffectMask::PURE.add(Effect::Io), - })?; + self.emit_instruction(MirInstruction::Print { value: dst, effects: EffectMask::PURE.add(Effect::Io) })?; return Ok(dst); + } else { + builder_debug_log("extract_string_literal FAIL"); } } + ASTNode::MethodCall { object, method, arguments, .. } if (method == "is" || method == "as") && arguments.len() == 1 => { + builder_debug_log("pattern: print(MethodCall is|as)"); + if let Some(type_name) = Self::extract_string_literal(&arguments[0]) { + builder_debug_log(&format!("extract_string_literal OK: {}", type_name)); + let obj_val = self.build_expression(*object.clone())?; + let ty = Self::parse_type_name_to_mir(&type_name); + let dst = self.value_gen.next(); + let op = if method == "is" { super::TypeOpKind::Check } else { super::TypeOpKind::Cast }; + builder_debug_log(&format!("emit TypeOp {:?} obj={} dst= {}", op, obj_val, dst)); + self.emit_instruction(MirInstruction::TypeOp { dst, op, value: obj_val, ty })?; + self.emit_instruction(MirInstruction::Print { value: dst, effects: EffectMask::PURE.add(Effect::Io) })?; + return Ok(dst); + } else { + builder_debug_log("extract_string_literal FAIL"); + } + } + _ => {} } let value = self.build_expression(expression)?; + builder_debug_log(&format!("fallback print value={}", value)); // For now, use a special Print instruction (minimal scope) self.emit_instruction(MirInstruction::Print { @@ -746,6 +777,19 @@ impl MirBuilder { if let Some(ref mut function) = self.current_function { if let Some(block) = function.get_block_mut(block_id) { + if builder_debug_enabled() { + eprintln!("[BUILDER] emit @bb{} -> {}", block_id, match &instruction { + MirInstruction::TypeOp { dst, op, value, ty } => format!("typeop {:?} {} {:?} -> {}", op, value, ty, dst), + MirInstruction::Print { value, .. } => format!("print {}", value), + MirInstruction::BoxCall { box_val, method, args, dst, .. } => format!("boxcall {}.{}({:?}) -> {:?}", box_val, method, args, dst), + MirInstruction::Call { func, args, dst, .. } => format!("call {}({:?}) -> {:?}", func, args, dst), + MirInstruction::NewBox { dst, box_type, args } => format!("new {}({:?}) -> {}", box_type, args, dst), + MirInstruction::Const { dst, value } => format!("const {:?} -> {}", value, dst), + MirInstruction::Branch { condition, then_bb, else_bb } => format!("br {}, {}, {}", condition, then_bb, else_bb), + MirInstruction::Jump { target } => format!("br {}", target), + _ => format!("{:?}", instruction), + }); + } block.add_instruction(instruction); Ok(()) } else { @@ -1178,11 +1222,11 @@ impl MirBuilder { fn build_method_call(&mut self, object: ASTNode, method: String, arguments: Vec) -> Result { // Minimal TypeOp wiring via method-style syntax: value.is("Type") / value.as("Type") if (method == "is" || method == "as") && arguments.len() == 1 { - if let ASTNode::Literal { value: crate::ast::LiteralValue::String(type_name), .. } = &arguments[0] { + if let Some(type_name) = Self::extract_string_literal(&arguments[0]) { // Build the object expression let object_value = self.build_expression(object.clone())?; // Map string to MIR type - let mir_ty = Self::parse_type_name_to_mir(type_name); + let mir_ty = Self::parse_type_name_to_mir(&type_name); let dst = self.value_gen.next(); let op = if method == "is" { super::TypeOpKind::Check } else { super::TypeOpKind::Cast }; self.emit_instruction(MirInstruction::TypeOp { dst, op, value: object_value, ty: mir_ty })?; @@ -1230,8 +1274,8 @@ impl MirBuilder { // Secondary interception for is/as in case early path did not trigger if (method == "is" || method == "as") && arguments.len() == 1 { - if let ASTNode::Literal { value: crate::ast::LiteralValue::String(type_name), .. } = &arguments[0] { - let mir_ty = Self::parse_type_name_to_mir(type_name); + if let Some(type_name) = Self::extract_string_literal(&arguments[0]) { + let mir_ty = Self::parse_type_name_to_mir(&type_name); let dst = self.value_gen.next(); let op = if method == "is" { super::TypeOpKind::Check } else { super::TypeOpKind::Cast }; self.emit_instruction(MirInstruction::TypeOp { dst, op, value: object_value, ty: mir_ty })?; @@ -1318,17 +1362,16 @@ impl MirBuilder { /// Extract string literal from AST node if possible /// Supports: Literal("Type") and new StringBox("Type") fn extract_string_literal(node: &ASTNode) -> Option { - match node { - ASTNode::Literal { value: crate::ast::LiteralValue::String(s), .. } => Some(s.clone()), - ASTNode::New { class, arguments, .. } if class == "StringBox" => { - if arguments.len() == 1 { - if let ASTNode::Literal { value: crate::ast::LiteralValue::String(s), .. } = &arguments[0] { - return Some(s.clone()); - } + let mut cur = node; + loop { + match cur { + ASTNode::Literal { value: crate::ast::LiteralValue::String(s), .. } => return Some(s.clone()), + ASTNode::New { class, arguments, .. } if class == "StringBox" && arguments.len() == 1 => { + cur = &arguments[0]; + continue; } - None + _ => return None, } - _ => None, } } diff --git a/src/mir/effect.rs b/src/mir/effect.rs index d749fb5d..89e532bc 100644 --- a/src/mir/effect.rs +++ b/src/mir/effect.rs @@ -129,7 +129,8 @@ impl EffectMask { /// Check if the computation is pure (no side effects) pub fn is_pure(self) -> bool { - self.contains(Effect::Pure) || self.0 == 0 + // 純粋性は一次カテゴリで判定(Pureビットが立っていてもIO/WRITE/CONTROL等があれば純粋ではない) + self.primary_category() == Effect::Pure } /// Check if the computation is mutable (modifies state) @@ -346,5 +347,9 @@ mod tests { let display = format!("{}", read_io); assert!(display.contains("read")); assert!(display.contains("io")); + + // is_pure should be false when IO is present, even if PURE bit was set initially + let mixed = EffectMask::PURE | EffectMask::IO; + assert!(!mixed.is_pure()); } } diff --git a/src/mir/mod.rs b/src/mir/mod.rs index 30d8037e..c967e86a 100644 --- a/src/mir/mod.rs +++ b/src/mir/mod.rs @@ -42,6 +42,7 @@ pub struct MirCompileResult { pub struct MirCompiler { builder: MirBuilder, verifier: MirVerifier, + optimize: bool, } impl MirCompiler { @@ -50,6 +51,15 @@ impl MirCompiler { Self { builder: MirBuilder::new(), verifier: MirVerifier::new(), + optimize: true, + } + } + /// Create with options + pub fn with_options(optimize: bool) -> Self { + Self { + builder: MirBuilder::new(), + verifier: MirVerifier::new(), + optimize, } } @@ -58,9 +68,13 @@ impl MirCompiler { // Convert AST to MIR using builder let mut module = self.builder.build_module(ast)?; - // Optimize (safety net lowering for is/as → TypeOp 等) - let mut optimizer = MirOptimizer::new(); - let _ = optimizer.optimize_module(&mut module); + if self.optimize { + let mut optimizer = MirOptimizer::new(); + let stats = optimizer.optimize_module(&mut module); + if std::env::var("NYASH_OPT_DIAG_FAIL").is_ok() && stats.diagnostics_reported > 0 { + return Err(format!("Diagnostic failure: {} unlowered type-op calls detected", stats.diagnostics_reported)); + } + } // Verify the generated MIR let verification_result = self.verifier.verify_module(&module); diff --git a/src/mir/optimizer.rs b/src/mir/optimizer.rs index d715fb95..eaeaec5a 100644 --- a/src/mir/optimizer.rs +++ b/src/mir/optimizer.rs @@ -31,6 +31,7 @@ impl MirOptimizer { self } + /// Run all optimization passes on a MIR module pub fn optimize_module(&mut self, module: &mut MirModule) -> OptimizationStats { let mut stats = OptimizationStats::new(); @@ -51,8 +52,7 @@ impl MirOptimizer { // Pass 4: Intrinsic function optimization stats.merge(self.optimize_intrinsic_calls(module)); - // Pass 4.5: Lower well-known intrinsics (is/as → TypeOp) as a safety net - stats.merge(self.lower_type_ops(module)); + // Safety-net passesは削除(Phase 2: 変換の一本化)。診断のみ後段で実施。 // Pass 5: BoxField dependency optimization stats.merge(self.optimize_boxfield_operations(module)); @@ -60,6 +60,9 @@ impl MirOptimizer { if self.debug { println!("✅ Optimization complete: {}", stats); } + // Diagnostics (informational): report unlowered patterns + let diag = self.diagnose_unlowered_type_ops(module); + stats.merge(diag); stats } @@ -128,11 +131,12 @@ impl MirOptimizer { // Remove unused pure instructions let mut eliminated = 0; - for (_, block) in &mut function.blocks { + for (bbid, block) in &mut function.blocks { block.instructions.retain(|instruction| { if instruction.effects().is_pure() { if let Some(dst) = instruction.dst_value() { if !used_values.contains(&dst) { + opt_debug(&format!("DCE drop @{}: {:?}", bbid.as_u32(), instruction)); eliminated += 1; return false; } @@ -258,101 +262,6 @@ impl MirOptimizer { 0 } - /// Safety-net lowering: convert known is/as patterns into TypeOp when possible - fn lower_type_ops(&mut self, module: &mut MirModule) -> OptimizationStats { - let mut stats = OptimizationStats::new(); - for (_name, function) in &mut module.functions { - stats.intrinsic_optimizations += self.lower_type_ops_in_function(function); - } - stats - } - - fn lower_type_ops_in_function(&mut self, function: &mut MirFunction) -> usize { - // Build a simple definition map: ValueId -> (block_id, index) - let mut def_map: std::collections::HashMap = std::collections::HashMap::new(); - for (bb_id, block) in &function.blocks { - for (i, inst) in block.instructions.iter().enumerate() { - if let Some(dst) = inst.dst_value() { - def_map.insert(dst, (*bb_id, i)); - } - } - if let Some(term) = &block.terminator { - if let Some(dst) = term.dst_value() { - def_map.insert(dst, (*bb_id, usize::MAX)); - } - } - } - - let mut lowered = 0; - - // Collect replacements first to avoid borrow checker issues - let mut replacements: Vec<(super::basic_block::BasicBlockId, usize, MirInstruction)> = Vec::new(); - - // First pass: identify replacements - for (bb_id, block) in &function.blocks { - for i in 0..block.instructions.len() { - let replace = match &block.instructions[i] { - MirInstruction::BoxCall { dst, box_val, method, args, .. } => { - let is_is = method == "is" || method == "isType"; - let is_as = method == "as" || method == "asType"; - if (is_is || is_as) && args.len() == 1 { - // Try to resolve type name from arg - let arg_id = args[0]; - if let Some((def_bb, def_idx)) = def_map.get(&arg_id).copied() { - // Look for pattern: NewBox StringBox(Const String) - if let Some(def_block) = function.blocks.get(&def_bb) { - if def_idx < def_block.instructions.len() { - if let MirInstruction::NewBox { box_type, args: sb_args, .. } = &def_block.instructions[def_idx] { - if box_type == "StringBox" && sb_args.len() == 1 { - let str_id = sb_args[0]; - if let Some((sbb, sidx)) = def_map.get(&str_id).copied() { - if let Some(sblock) = function.blocks.get(&sbb) { - if sidx < sblock.instructions.len() { - if let MirInstruction::Const { value, .. } = &sblock.instructions[sidx] { - if let super::instruction::ConstValue::String(s) = value { - // Build TypeOp to replace - let ty = map_type_name(s); - let op = if is_is { TypeOpKind::Check } else { TypeOpKind::Cast }; - let new_inst = MirInstruction::TypeOp { - dst: dst.unwrap_or(*box_val), - op, - value: *box_val, - ty, - }; - Some(new_inst) - } else { None } - } else { None } - } else { None } - } else { None } - } else { None } - } else { None } - } else { None } - } else { None } - } else { None } - } else { None } - } else { None } - } - _ => None, - }; - - if let Some(new_inst) = replace { - replacements.push((*bb_id, i, new_inst)); - } - } - } - - // Second pass: apply replacements - for (bb_id, idx, new_inst) in replacements { - if let Some(block) = function.blocks.get_mut(&bb_id) { - if idx < block.instructions.len() { - block.instructions[idx] = new_inst; - lowered += 1; - } - } - } - - lowered - } /// Optimize BoxField operations fn optimize_boxfield_operations(&mut self, module: &mut MirModule) -> OptimizationStats { @@ -392,6 +301,44 @@ fn map_type_name(name: &str) -> MirType { } } +fn opt_debug_enabled() -> bool { std::env::var("NYASH_OPT_DEBUG").is_ok() } +fn opt_debug(msg: &str) { if opt_debug_enabled() { eprintln!("[OPT] {}", msg); } } + +/// Resolve a MIR type from a value id that should represent a type name +/// Supports: Const String("T") and NewBox(StringBox, Const String("T")) +fn resolve_type_from_value( + function: &MirFunction, + def_map: &std::collections::HashMap, + id: ValueId, +) -> Option { + use super::instruction::ConstValue; + if let Some((bb, idx)) = def_map.get(&id).copied() { + if let Some(block) = function.blocks.get(&bb) { + if idx < block.instructions.len() { + match &block.instructions[idx] { + MirInstruction::Const { value: ConstValue::String(s), .. } => { + return Some(map_type_name(s)); + } + MirInstruction::NewBox { box_type, args, .. } if box_type == "StringBox" && args.len() == 1 => { + let inner = args[0]; + if let Some((sbb, sidx)) = def_map.get(&inner).copied() { + if let Some(sblock) = function.blocks.get(&sbb) { + if sidx < sblock.instructions.len() { + if let MirInstruction::Const { value: ConstValue::String(s), .. } = &sblock.instructions[sidx] { + return Some(map_type_name(s)); + } + } + } + } + } + _ => {} + } + } + } + } + None +} + impl Default for MirOptimizer { fn default() -> Self { Self::new() @@ -406,6 +353,7 @@ pub struct OptimizationStats { pub reorderings: usize, pub intrinsic_optimizations: usize, pub boxfield_optimizations: usize, + pub diagnostics_reported: usize, } impl OptimizationStats { @@ -419,6 +367,7 @@ impl OptimizationStats { self.reorderings += other.reorderings; self.intrinsic_optimizations += other.intrinsic_optimizations; self.boxfield_optimizations += other.boxfield_optimizations; + self.diagnostics_reported += other.diagnostics_reported; } pub fn total_optimizations(&self) -> usize { @@ -441,6 +390,51 @@ impl std::fmt::Display for OptimizationStats { } } +impl MirOptimizer { + /// Diagnostic: detect unlowered is/as/isType/asType after Builder + fn diagnose_unlowered_type_ops(&mut self, module: &MirModule) -> OptimizationStats { + let mut stats = OptimizationStats::new(); + let diag_on = self.debug || std::env::var("NYASH_OPT_DIAG").is_ok(); + for (fname, function) in &module.functions { + // def map for resolving constants + let mut def_map: std::collections::HashMap = std::collections::HashMap::new(); + for (bb_id, block) in &function.blocks { + for (i, inst) in block.instructions.iter().enumerate() { + if let Some(dst) = inst.dst_value() { def_map.insert(dst, (*bb_id, i)); } + } + if let Some(term) = &block.terminator { if let Some(dst) = term.dst_value() { def_map.insert(dst, (*bb_id, usize::MAX)); } } + } + let mut count = 0usize; + for (_bb, block) in &function.blocks { + for inst in &block.instructions { + match inst { + MirInstruction::BoxCall { method, .. } if method == "is" || method == "as" || method == "isType" || method == "asType" => { count += 1; } + MirInstruction::Call { func, .. } => { + if let Some((bb, idx)) = def_map.get(func).copied() { + if let Some(b) = function.blocks.get(&bb) { + if idx < b.instructions.len() { + if let MirInstruction::Const { value: super::instruction::ConstValue::String(s), .. } = &b.instructions[idx] { + if s == "isType" || s == "asType" { count += 1; } + } + } + } + } + } + _ => {} + } + } + } + if count > 0 { + stats.diagnostics_reported += count; + if diag_on { + eprintln!("[OPT][DIAG] Function '{}' has {} unlowered type-op calls", fname, count); + } + } + } + stats + } +} + #[cfg(test)] mod tests { use super::*; @@ -489,4 +483,36 @@ mod tests { assert!(key.contains("const")); assert!(key.contains("42")); } + + #[test] + fn test_dce_does_not_drop_typeop_used_by_print() { + // Build a simple function: %v=TypeOp(check); print %v; ensure TypeOp remains after optimize + let signature = FunctionSignature { + name: "main".to_string(), + params: vec![], + return_type: MirType::Void, + effects: super::super::effect::EffectMask::PURE, + }; + let mut func = MirFunction::new(signature, BasicBlockId::new(0)); + let bb0 = BasicBlockId::new(0); + let mut b0 = BasicBlock::new(bb0); + let v0 = ValueId::new(0); + let v1 = ValueId::new(1); + b0.add_instruction(MirInstruction::NewBox { dst: v0, box_type: "IntegerBox".to_string(), args: vec![] }); + b0.add_instruction(MirInstruction::TypeOp { dst: v1, op: TypeOpKind::Check, value: v0, ty: MirType::Integer }); + b0.add_instruction(MirInstruction::Print { value: v1, effects: super::super::effect::EffectMask::IO }); + b0.add_instruction(MirInstruction::Return { value: None }); + func.add_block(b0); + let mut module = MirModule::new("test".to_string()); + module.add_function(func); + + let mut opt = MirOptimizer::new(); + let _stats = opt.optimize_module(&mut module); + + // Ensure TypeOp remains in bb0 + let f = module.get_function("main").unwrap(); + let block = f.get_block(&bb0).unwrap(); + let has_typeop = block.all_instructions().any(|i| matches!(i, MirInstruction::TypeOp { .. })); + assert!(has_typeop, "TypeOp should not be dropped by DCE when used by print"); + } } diff --git a/src/mir/printer.rs b/src/mir/printer.rs index 6a479554..817b379a 100644 --- a/src/mir/printer.rs +++ b/src/mir/printer.rs @@ -18,6 +18,9 @@ pub struct MirPrinter { /// Whether to show line numbers show_line_numbers: bool, + + /// Whether to show per-instruction effect category + show_effects_inline: bool, } impl MirPrinter { @@ -27,6 +30,7 @@ impl MirPrinter { indent_level: 0, verbose: false, show_line_numbers: true, + show_effects_inline: false, } } @@ -36,6 +40,7 @@ impl MirPrinter { indent_level: 0, verbose: true, show_line_numbers: true, + show_effects_inline: false, } } @@ -50,6 +55,12 @@ impl MirPrinter { self.show_line_numbers = show; self } + + /// Show per-instruction effect category (pure/readonly/side) + pub fn set_show_effects_inline(&mut self, show: bool) -> &mut Self { + self.show_effects_inline = show; + self + } /// Print a complete MIR module pub fn print_module(&self, module: &MirModule) -> String { @@ -127,6 +138,70 @@ impl MirPrinter { if stats.is_pure { writeln!(output, " ; Pure: yes").unwrap(); } + // Verbose: highlight MIR26-unified ops presence for snapshotting (TypeOp/WeakRef/Barrier) + let mut type_check = 0usize; + let mut type_cast = 0usize; + let mut weak_new = 0usize; + let mut weak_load = 0usize; + let mut barrier_read = 0usize; + let mut barrier_write = 0usize; + for block in function.blocks.values() { + for inst in &block.instructions { + match inst { + MirInstruction::TypeCheck { .. } => type_check += 1, + MirInstruction::Cast { .. } => type_cast += 1, + MirInstruction::TypeOp { op, .. } => match op { + super::TypeOpKind::Check => type_check += 1, + super::TypeOpKind::Cast => type_cast += 1, + }, + MirInstruction::WeakNew { .. } => weak_new += 1, + MirInstruction::WeakLoad { .. } => weak_load += 1, + MirInstruction::WeakRef { op, .. } => match op { + super::WeakRefOp::New => weak_new += 1, + super::WeakRefOp::Load => weak_load += 1, + }, + MirInstruction::BarrierRead { .. } => barrier_read += 1, + MirInstruction::BarrierWrite { .. } => barrier_write += 1, + MirInstruction::Barrier { op, .. } => match op { + super::BarrierOp::Read => barrier_read += 1, + super::BarrierOp::Write => barrier_write += 1, + }, + _ => {} + } + } + if let Some(term) = &block.terminator { + match term { + MirInstruction::TypeCheck { .. } => type_check += 1, + MirInstruction::Cast { .. } => type_cast += 1, + MirInstruction::TypeOp { op, .. } => match op { + super::TypeOpKind::Check => type_check += 1, + super::TypeOpKind::Cast => type_cast += 1, + }, + MirInstruction::WeakNew { .. } => weak_new += 1, + MirInstruction::WeakLoad { .. } => weak_load += 1, + MirInstruction::WeakRef { op, .. } => match op { + super::WeakRefOp::New => weak_new += 1, + super::WeakRefOp::Load => weak_load += 1, + }, + MirInstruction::BarrierRead { .. } => barrier_read += 1, + MirInstruction::BarrierWrite { .. } => barrier_write += 1, + MirInstruction::Barrier { op, .. } => match op { + super::BarrierOp::Read => barrier_read += 1, + super::BarrierOp::Write => barrier_write += 1, + }, + _ => {} + } + } + } + if type_check + type_cast > 0 { + writeln!(output, " ; TypeOp: {} (check: {}, cast: {})", type_check + type_cast, type_check, type_cast).unwrap(); + } + if weak_new + weak_load > 0 { + writeln!(output, " ; WeakRef: {} (new: {}, load: {})", weak_new + weak_load, weak_new, weak_load).unwrap(); + } + if barrier_read + barrier_write > 0 { + writeln!(output, " ; Barrier: {} (read: {}, write: {})", barrier_read + barrier_write, barrier_read, barrier_write).unwrap(); + } writeln!(output).unwrap(); } @@ -174,7 +249,13 @@ impl MirPrinter { write!(output, " ").unwrap(); } - writeln!(output, "{}", self.format_instruction(instruction)).unwrap(); + let mut line = self.format_instruction(instruction); + if self.show_effects_inline { + let eff = instruction.effects(); + let cat = if eff.is_pure() { "pure" } else if eff.is_read_only() { "readonly" } else { "side" }; + line.push_str(&format!(" ; eff: {}", cat)); + } + writeln!(output, "{}", line).unwrap(); line_num += 1; } diff --git a/src/runner.rs b/src/runner.rs index 81ea74bd..ae170ee8 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -113,6 +113,25 @@ impl NyashRunner { /// Execute file-based mode with backend selection fn execute_file_mode(&self, filename: &str) { + if self.config.dump_ast { + println!("🧠 Nyash AST Dump - Processing file: {}", filename); + let code = match fs::read_to_string(filename) { + Ok(content) => content, + Err(e) => { + eprintln!("❌ Error reading file {}: {}", filename, e); + process::exit(1); + } + }; + let ast = match NyashParser::parse_from_string(&code) { + Ok(ast) => ast, + Err(e) => { + eprintln!("❌ Parse error: {}", e); + process::exit(1); + } + }; + println!("{:#?}", ast); + return; + } if self.config.dump_mir || self.config.verify_mir { println!("🚀 Nyash MIR Compiler - Processing file: {} 🚀", filename); self.execute_mir_mode(filename); @@ -271,8 +290,8 @@ impl NyashRunner { } }; - // Compile to MIR - let mut mir_compiler = MirCompiler::new(); + // Compile to MIR (opt passes configurable) + let mut mir_compiler = MirCompiler::with_options(!self.config.no_optimize); let compile_result = match mir_compiler.compile(ast) { Ok(result) => result, Err(e) => { @@ -298,11 +317,8 @@ impl NyashRunner { // Dump MIR if requested if self.config.dump_mir { - let printer = if self.config.mir_verbose { - MirPrinter::verbose() - } else { - MirPrinter::new() - }; + let mut printer = if self.config.mir_verbose { MirPrinter::verbose() } else { MirPrinter::new() }; + if self.config.mir_verbose_effects { printer.set_show_effects_inline(true); } println!("🚀 MIR Output for {}:", filename); println!("{}", printer.print_module(&compile_result.module)); @@ -345,8 +361,8 @@ impl NyashRunner { rt }; - // Compile to MIR - let mut mir_compiler = MirCompiler::new(); + // Compile to MIR (opt passes configurable) + let mut mir_compiler = MirCompiler::with_options(!self.config.no_optimize); let compile_result = match mir_compiler.compile(ast) { Ok(result) => result, Err(e) => { diff --git a/src/runtime/plugin_loader_v2.rs b/src/runtime/plugin_loader_v2.rs index ab87c806..b942781b 100644 --- a/src/runtime/plugin_loader_v2.rs +++ b/src/runtime/plugin_loader_v2.rs @@ -843,6 +843,20 @@ mod stub { use once_cell::sync::Lazy; use std::sync::{Arc, RwLock}; + // Stub implementation of PluginBoxV2 for WASM/non-plugin builds + #[derive(Debug, Clone)] + pub struct PluginBoxV2 { + pub box_type: String, + pub inner: std::sync::Arc, + } + + #[derive(Debug)] + pub struct PluginHandleInner { + pub type_id: u32, + pub instance_id: u32, + pub fini_method_id: Option, + } + pub struct PluginLoaderV2 { pub config: Option<()>, // Dummy config for compatibility } diff --git a/tools/ci_check_golden.sh b/tools/ci_check_golden.sh new file mode 100644 index 00000000..1523270c --- /dev/null +++ b/tools/ci_check_golden.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Minimal golden MIR check for CI/local use + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT_DIR" + +PAIRS=( + "local_tests/typeop_is_as_func_poc.nyash docs/status/golden/typeop_is_as_func_poc.mir.txt" + "local_tests/typeop_is_as_poc.nyash docs/status/golden/typeop_is_as_poc.mir.txt" +) + +for pair in "${PAIRS[@]}"; do + in_file="${pair%% *}" + golden_file="${pair##* }" + echo "[GOLDEN] Checking: $in_file vs $golden_file" + ./tools/compare_mir.sh "$in_file" "$golden_file" +done + +echo "All golden MIR snapshots match." + diff --git a/tools/compare_mir.sh b/tools/compare_mir.sh new file mode 100644 index 00000000..dbeb03bb --- /dev/null +++ b/tools/compare_mir.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +INPUT="$1" +GOLDEN="$2" + +TMPDIR="${TMPDIR:-/tmp}" +OUT="$TMPDIR/mir_snapshot_$$.txt" +trap 'rm -f "$OUT"' EXIT + +# Allow effect annotation opt-in via env var +if [ -n "${NYASH_MIR_VERBOSE_EFFECTS:-}" ]; then + NYASH_MIR_VERBOSE_EFFECTS=1 ./tools/snapshot_mir.sh "$INPUT" "$OUT" >/dev/null +else + ./tools/snapshot_mir.sh "$INPUT" "$OUT" >/dev/null +fi + +if ! diff -u "$GOLDEN" "$OUT"; then + echo "MIR snapshot differs from golden: $GOLDEN" >&2 + exit 1 +fi + +echo "MIR matches golden: $GOLDEN" + diff --git a/tools/snapshot_mir.sh b/tools/snapshot_mir.sh new file mode 100644 index 00000000..76837cbe --- /dev/null +++ b/tools/snapshot_mir.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then + echo "Usage: $0 [output.txt]" >&2 + echo "Dumps Builder-only MIR (--no-optimize) for reproducible snapshots." >&2 + exit 2 +fi + +INPUT="$1" +OUTFILE="${2:-}" + +if [ ! -f "$INPUT" ]; then + echo "Input not found: $INPUT" >&2 + exit 1 +fi + +BIN="${NYASH_BIN:-./target/release/nyash}" +if [ ! -x "$BIN" ]; then + echo "nyash binary not found at $BIN. Build first: cargo build --release" >&2 + exit 1 +fi + +CMD=("$BIN" --dump-mir --mir-verbose --no-optimize "$INPUT") + +if [ -n "${NYASH_MIR_VERBOSE_EFFECTS:-}" ]; then + CMD=("$BIN" --dump-mir --mir-verbose --mir-verbose-effects --no-optimize "$INPUT") +fi + +if [ -n "$OUTFILE" ]; then + mkdir -p "$(dirname "$OUTFILE")" + "${CMD[@]}" | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' > "$OUTFILE" + echo "Wrote MIR snapshot: $OUTFILE" +else + "${CMD[@]}" +fi