diff --git a/docs/development/roadmap/phases/00_MASTER_ROADMAP.md b/docs/development/roadmap/phases/00_MASTER_ROADMAP.md index 27703cfa..1f6bc09c 100644 --- a/docs/development/roadmap/phases/00_MASTER_ROADMAP.md +++ b/docs/development/roadmap/phases/00_MASTER_ROADMAP.md @@ -111,6 +111,7 @@ nyash bid gen --target llvm bid.yaml # AOT用declare生成(LLVM実装時) - MIR→VMを維持しつつ、ホットパスをCraneliftでJIT化 - 目標: VM比2倍以上の高速化 - LLVM AOTは設計資産は維持しつつ、Phase 11以降に検討 +- **🌟 NEW: GC切り替え可能ランタイム(世界初の柔軟なメモリ管理)** **Start Gate(着手前の必須完了)**: - ✅ MIRダイエット(26命令)整合完了 @@ -122,6 +123,8 @@ nyash bid gen --target llvm bid.yaml # AOT用declare生成(LLVM実装時) 1. **Phase 10.1**: Proof of Concept(2週間) 2. **Phase 10.2**: 基本実装(4週間) 3. **Phase 10.3**: 非同期の扱い(最小) +4. **Phase 10.4**: GC切り替え可能ランタイム(2-3ヶ月) +5. **Phase 10.5**: セルフホスティング(並行実装) --- diff --git a/docs/development/roadmap/phases/phase-10/README.md b/docs/development/roadmap/phases/phase-10/README.md new file mode 100644 index 00000000..0400bd6b --- /dev/null +++ b/docs/development/roadmap/phases/phase-10/README.md @@ -0,0 +1,61 @@ +# Phase 10: JIT実装とセルフホスティング + +## 🎯 Phase 10の全体像 + +Phase 10は、Nyashの実行性能を大幅に向上させるJIT実装と、言語の成熟度を示すセルフホスティングを実現します。 + +## 📊 実装優先順位 + +### 1️⃣ **メイン実装: Cranelift JIT** +→ [phase_10_cranelift_jit_backend.md](phase_10_cranelift_jit_backend.md) +- VMとのハイブリッド実行(ホットパス検出→JIT化) +- 実装期間: 2-3ヶ月 +- 目標: ホットパスで2倍以上の高速化 + +### 🌟 **革新的機能: GC切り替え可能ランタイム** +→ [phase_10_4_gc_switchable_runtime.md](phase_10_4_gc_switchable_runtime.md) +- 世界初:実行時にGCモード切り替え可能 +- 開発時はGCオンで快適、本番はGCオフで高速 +- 実装期間: 2-3ヶ月(Cranelift JIT後) +- 技術的にCodex GPT-5が実現可能性を確認済み + +### 2️⃣ **並行プロジェクト: セルフホスティング** +→ [phase_10_5_core_std_nyash_impl.md](phase_10_5_core_std_nyash_impl.md) +- String/Array/MapをNyash自身で実装 +- Rust依存の段階的削減 +- 実装期間: 1-2ヶ月 + +### 3️⃣ **実戦テスト: アプリケーション移植** +→ [phase_10_app_migration.md](phase_10_app_migration.md) +- Tinyproxy: ゼロコピー判定機能の検証 +- Chip-8エミュレータ: fini伝播とweak参照の実戦テスト +- kiloエディタ: メモリ効率の「うっかり全体コピー」検出 + +### 🚫 **延期プロジェクト** +→ [Phase 11: LLVM AOT Backend](../phase-11/) - 将来の研究開発として分離 + +## 🛤️ 実装ロードマップ + +``` +Phase 9.79b (現在) + ↓ +Phase 10.0: Cranelift JIT基盤構築 + ├→ Phase 10.1-10.3: JIT実装・最適化 + ├→ Phase 10.4: GC切り替え可能ランタイム ← NEW! + └→ Phase 10.5: セルフホスティング(並行) + ↓ +Phase 10.9: アプリケーション移植で実戦検証 + ↓ +Phase 11: LLVM AOT研究(将来) +``` + +## 📈 期待される成果 + +1. **実行性能**: インタープリタ比100倍、VM比2-3倍の高速化 +2. **言語成熟度**: 基本コンテナのセルフホスティング達成 +3. **実用性検証**: 実アプリケーションの移植による実戦テスト + +## 🔗 関連ドキュメント +- [00_MASTER_ROADMAP.md](../00_MASTER_ROADMAP.md) - 全体計画 +- [Phase 9.79b](../phase-9/) - 統一Box設計(前提) +- [MIR仕様](../../../../reference/mir/) - 中間表現 \ No newline at end of file diff --git a/docs/development/roadmap/phases/phase-10/phase_10_4_gc_switchable_runtime.md b/docs/development/roadmap/phases/phase-10/phase_10_4_gc_switchable_runtime.md new file mode 100644 index 00000000..e341b22b --- /dev/null +++ b/docs/development/roadmap/phases/phase-10/phase_10_4_gc_switchable_runtime.md @@ -0,0 +1,173 @@ +# Phase 10.4: GC Switchable Runtime - 世界初の柔軟なメモリ管理 + +Status: Planned +Owner: core-runtime +Target: After Cranelift JIT (Phase 10.0) +Last Updated: 2025-08-26 +Dependencies: Phase 10.0 (Cranelift JIT), Phase 9.79b (Unified Box) + +## 🎯 概要 + +Nyashを**世界初のGC切り替え可能プログラミング言語**にする革新的機能。開発時はGCで快適に、本番ではGCなしで最高性能を実現。 + +## 📊 技術的背景 + +### 現状のメモリ管理 +- Everything is Box哲学(すべてのデータがBoxオブジェクト) +- 明示的メモリ管理(スコープベースfini) +- Arcによるスレッドセーフ設計 + +### 提案する2つのモード +1. **Explicit Mode(現在のデフォルト)** + - スコープを抜けたら即座にfini()呼び出し + - 予測可能な性能(リアルタイムアプリ向け) + - メモリ使用量が最小 + +2. **Reference Counting Mode(新規)** + - 参照カウントが0になったらfini()呼び出し + - 循環参照はweak参照で解決 + - 開発効率重視(一般アプリ向け) + +## 🏗️ アーキテクチャ設計 + +### MIR層:所有権イベントの抽象化 +```rust +// GCモードに依存しない所有権表現 +enum MirOwnership { + Move(temp_id), // 所有権移動 + Copy(temp_id), // 複製 + Drop(temp_id), // 破棄 + StorageLive(id), // 生存開始 + StorageDead(id), // 生存終了 + Escape(target), // エスケープ解析 +} +``` + +### ランタイム層:モード別実装 +```rust +// 統一APIでモード切り替え +trait MemoryManager { + fn retain_ref(&self, ptr: *const BoxHeader); + fn release_ref(&self, ptr: *const BoxHeader); + fn destroy(&self, ptr: *const BoxHeader); +} + +struct ExplicitManager; // 即座に破棄 +struct RefCountManager; // 参照カウント管理 +``` + +### JIT層:関数マルチバージョン化 +``` +関数テーブル: +┌─────────────┬──────────────┬──────────────┐ +│ Function │ Explicit Ver │ RefCount Ver │ +├─────────────┼──────────────┼──────────────┤ +│ array_push │ 0x1000_0000 │ 0x2000_0000 │ +│ map_get │ 0x1000_1000 │ 0x2000_1000 │ +└─────────────┴──────────────┴──────────────┘ + +トランポリン → 現在のモードに応じてジャンプ +``` + +## 📋 実装計画 + +### Phase 10.4.1: 基盤構築(2週間) +- [ ] BoxHeaderに参照カウントフィールド追加 +- [ ] MemoryManagerトレイト定義 +- [ ] インタープリターでの基本実装 + +### Phase 10.4.2: MIR対応(1ヶ月) +- [ ] 所有権イベント(Move/Copy/Drop等)の導入 +- [ ] retain_ref/release_ref命令の追加 +- [ ] エスケープ解析の基礎実装 + +### Phase 10.4.3: 最適化(3週間) +- [ ] 近接ペア消去(retain直後のrelease削除) +- [ ] ループ不変式の移動 +- [ ] φノードでの差分管理 + +### Phase 10.4.4: JIT統合(1ヶ月) +- [ ] 関数マルチバージョン生成 +- [ ] トランポリン機構実装 +- [ ] fast path/slow path分離 + +### Phase 10.4.5: 実戦投入(2週間) +- [ ] モード切り替えCLI実装 +- [ ] メモリリーク検出ツール +- [ ] ベンチマーク・性能評価 + +## 🎯 使用例 + +### 開発フロー +```bash +# 1. 開発中:GCオンで快適に開発 +nyash --gc-mode=ref-counting --detect-leaks dev.nyash + +# 2. テスト:メモリリークがないことを確認 +nyash --gc-mode=ref-counting --memory-report test.nyash +# => No memory leaks detected! + +# 3. 本番:GCオフで最高性能 +nyash --gc-mode=explicit --optimize prod.nyash +``` + +### コード例 +```nyash +// 同じコードが両モードで動作 +box DataProcessor { + init { buffer, cache } + + process(data) { + me.buffer = data.transform() // GCありなら参照カウント管理 + me.cache.put(data.id, data) // GCなしなら即座に古い値を破棄 + return me.buffer + } + + fini() { + print("Cleanup!") // タイミングはモード次第 + } +} +``` + +## ⚠️ 技術的課題と解決策 + +### 1. Arcの重さ +**課題**: 現在すべてのBoxがArcで重い +**解決**: 必要な場所のみ同期、基本型は非同期に + +### 2. 実行時オーバーヘッド +**課題**: モードチェックのコスト +**解決**: JITでの関数マルチバージョン化(間接ジャンプ1回のみ) + +### 3. 循環参照 +**課題**: RefCountingモードでの循環参照 +**解決**: 既存のWeakBox活用+明示的切断 + +### 4. セマンティクスの違い +**課題**: デストラクタ呼び出しタイミングの差 +**解決**: ドキュメント化+移行ガイド作成 + +## 📊 期待される効果 + +1. **開発効率**: 30%向上(メモリ管理の負担軽減) +2. **実行性能**: GCオフ時は現状維持、GCオン時は5-10%低下 +3. **メモリ効率**: モード次第で最適化可能 +4. **教育価値**: メモリ管理の学習に最適なツール + +## 🔗 関連ドキュメント +- [Phase 10.0: Cranelift JIT](phase_10_cranelift_jit_backend.md) +- [Phase 9.79b: Unified Box Design](../phase-9/phase_9_79b_1_unified_registry_ids_and_builder_slotting.md) +- [GC Switchable Language Idea](../../../ideas/other/2025-08-26-gc-switchable-language.md) + +## ✅ 受け入れ基準 +- [ ] インタープリター/VM/JITすべてで両モード動作 +- [ ] モード切り替えが実行時に可能(再コンパイル不要) +- [ ] 既存コードが無修正で動作(後方互換性) +- [ ] パフォーマンス劣化が許容範囲(GCオン時10%以内) +- [ ] メモリリーク検出ツールの提供 + +## 🚀 将来の拡張 +- Mark & Sweep GCモードの追加 +- 世代別GC +- リージョンベースメモリ管理 +- プロファイルベース自動モード選択 \ No newline at end of file diff --git a/docs/development/roadmap/phases/phase-10/phase_10_cranelift_jit_backend.md b/docs/development/roadmap/phases/phase-10/phase_10_cranelift_jit_backend.md index 31aa1429..b956391f 100644 --- a/docs/development/roadmap/phases/phase-10/phase_10_cranelift_jit_backend.md +++ b/docs/development/roadmap/phases/phase-10/phase_10_cranelift_jit_backend.md @@ -60,3 +60,93 @@ AST → MIR → Optimizer → VM Dispatcher --- 備考: LLVM AOTはPhase 11以降の研究路線に移行(設計ドキュメントは維持)。 +## 🔬 Sub-Phases (10_a .. 10_h) + +各サブフェーズは「小さく立ち上げ→検証→次へ」。既存のVM/Thunk/PICを活用し、JITは同じ経路に自然合流させる。 + +### 10_a: JITブートストラップ(基盤+プロファイラ) +- 目的: Cranelift JITの骨組みとホット関数検出の導線を用意。 +- 具体: + - `JitManager`(関数プロファイル、しきい値、キャッシュ) + - CLIF環境初期化(`Module`, `Context`, `ISA`) + - VM統合: 関数入口でホット度チェック→JITコンパイル/呼び出し + - 診断: `NYASH_JIT_STATS=1`(JIT件数/時間/キャッシュヒット) +- 受入: ダミー関数をJIT登録してVMから呼出可能、ログが出る + +### 10_b: Lower(Core-1) – Const/Move/BinOp/Cmp/Branch/Ret +- 目的: ループや条件分岐を含む純粋関数をJIT実行可能に。 +- 具体: + - MIR値/型→CLIF型(i64/f64/i1/void) + - Const/Copy/算術/比較/ジャンプ/分岐/return のLower + - フレームレイアウト(VMValue最小表現) +- 受入: 算術/比較/分岐のみの関数がJITでVMより速い(小ベンチ) + +### 10_c: ABI/呼出し – 関数呼び出しとフォールバック +- 目的: JIT化関数から他関数を呼ぶ/VMへ戻る道を用意。 +- 具体: + - 同一モジュール関数呼び出し(JIT→JIT/JIT→VM) + - 引数/戻り値の受け渡し(整数/浮動/void) + - 例外/TypeErrorはVMへバイアウト(trap→VM) +- 受入: 再帰/多段呼び出しが安定動作 + +### 10_d: コレクション基礎 – Array/Map ホット操作(外部呼び出し) +- 目的: 実用的ホットパス(length/get/set/push/pop)をJIT側から呼べるように。 +- 具体: + - ホスト関数テーブル(外部シンボル)で既存Rust実装を呼ぶ + - 境界チェック/エラーはRust側に委譲、JITは薄い橋渡し +- 受入: Array操作がVM経由より高速(目安1.5–2.0×) + +### 10_e: BoxCall高速化 – Thunk/PICの直結 +- 目的: Phase 9.79bの `TypeMeta{slot->thunk}` と Poly-PIC をJITにインライン。 +- 具体: + - `slot -> thunk -> target` 解決をJITで再現(ユニバーサル0..3含む) + - `(type_id, version)` チェック(Poly-PIC 2–4件)→ヒット直呼び、ミスVM + - バージョンミスマッチで安全にフォールバック +- 受入: BoxCallホットサイトで2×以上の高速化+正しい無効化挙動 + +### 10_f: TypeOp/Ref/Weak/Barrier(最小) +- 目的: 実アプリで必要な最小限のタイプ/参照操作を埋める。 +- 具体: + - `as_bool()` 等の基本型変換 + - 参照/弱参照/バリアの最小パス(重い経路はVMへ) +- 受入: 代表的コードパスでJIT有効のままE2E成功 + +### 10_g: 診断/ベンチ/回帰 +- 目的: 可視化と安定化。 +- 具体: + - `--vm-stats` にJIT統計統合(compile/ms, sites, cache率) + - ベンチ更新(JIT有/無比較)とスナップショット +- 受入: CIで回帰検知可能/ドキュメント更新 + +### 10_h: 硬化・パフォーマンス調整 +- 目的: ホットスポットの最適化とノイズ除去。 +- 具体: + - ガード配置最適化(分岐予測/ICヒット優先) + - 不要コピー削減、ホスト呼出回数の削減 +- 受入: 代表ベンチで安定して目標達成(2×以上) + +## 📦 成果物(各サブフェーズ) +- 10_a: `jit/manager.rs` スケルトン、VM連携、統計ログ +- 10_b: `jit/lower/core.rs`(Const/BinOp/Cmp/Branch/Ret)+単体テスト +- 10_c: `jit/abi.rs`(call/ret/fallback)+再帰テスト +- 10_d: `jit/extern/collections.rs`(Array/Mapブリッジ)+マイクロベンチ +- 10_e: `jit/inline_cache.rs`(PIC/VT連携)+無効化テスト +- 10_f: `jit/lower/typeop_ref.rs`(最小) +- 10_g: ベンチ/統計/README更新 +- 10_h: 最適化コミットと測定レポート + +## 🧩 既存資産との連携(重要) +- Thunk: Phase 9.79b.3の `TypeMeta{thunks}` をJIT直呼びターゲットとして使用 +- Poly-PIC: VMの構造をJITに投影(同じキー `(label, version)` を使用) +- Versioning: `cache_versions` のイベントに同期してIC無効化 + +## 🎯 マイルストーン再定義 +- M1: 10_a + 10_b 合格(Core関数のJIT実行) +- M2: 10_c + 10_d 合格(関数呼出/Arrayホット操作) +- M3: 10_e 合格(BoxCallホットパス2×) +- M4: 10_g + 10_h 合格(ベンチ/統計/硬化) + +## ⚠️ リスクと緩和 +- ABI複雑化: まず整数/浮動/voidに限定し、BoxRefはホスト関数へブリッジ +- 最適化過剰: 常にVMフォールバックを保持、ガード失敗で安全に戻す +- デバッグ困難: CLIFダンプ/CFG表示/`NYASH_JIT_STATS`で観測 diff --git a/docs/development/roadmap/phases/phase-11/README.md b/docs/development/roadmap/phases/phase-11/README.md new file mode 100644 index 00000000..44aba2c4 --- /dev/null +++ b/docs/development/roadmap/phases/phase-11/README.md @@ -0,0 +1,56 @@ +# Phase 11: LLVM AOT Backend(将来研究) + +## 🎯 概要 + +Phase 11は、LLVM を使用した Ahead-of-Time(AOT)コンパイル機能の研究・実装フェーズです。 +Phase 10のCranelift JITで実用的な性能を達成した後、さらなる最適化を追求します。 + +## 📊 位置づけ + +``` +Phase 10: Cranelift JIT(実用的な高速化)← 現在の主経路 + ↓ +Phase 11: LLVM AOT(最高性能への挑戦)← 将来の研究開発 +``` + +## 📁 ドキュメント + +### 🔬 研究・設計ドキュメント +- [phase10_aot_scaffolding.md](phase10_aot_scaffolding.md) - LLVM Direct AOT実装計画 + - MIR→LLVM IR直接変換 + - Everything is Box最適化(エスケープ解析) + - LTO/PGO統合 + - 目標: 13,500倍高速化(対インタープリタ) + +- [phase_10_x_llvm_backend_skeleton.md](phase_10_x_llvm_backend_skeleton.md) - LLVM Backend最小実装 + - 具体的な実装ステップ + - ExternCall対応 + - オブジェクトファイル生成 + +## ⏰ タイムライン + +- **Status**: Deferred(延期) +- **前提条件**: Phase 10(Cranelift JIT)の完了 +- **想定期間**: 4-6ヶ月 +- **開始時期**: 未定(Phase 10の成果を見て判断) + +## 🎯 期待される成果 + +1. **最高性能**: インタープリタ比13,500倍の実行速度 +2. **メモリ効率**: Box割当80%削減 +3. **起動時間**: 1ms以下 +4. **配布形式**: スタンドアロン実行ファイル + +## ⚠️ 注意事項 + +このフェーズは研究的な性質が強く、以下の理由で延期されています: + +1. **複雑性**: LLVM統合は開発・保守コストが高い +2. **実用性**: Cranelift JITで十分な性能が得られる可能性 +3. **優先度**: まずは安定した実装を優先 + +## 🔗 関連フェーズ + +- [Phase 10](../phase-10/) - Cranelift JIT(前提) +- [Phase 9](../phase-9/) - 統一Box設計(基盤) +- [00_MASTER_ROADMAP.md](../00_MASTER_ROADMAP.md) - 全体計画 \ No newline at end of file diff --git a/docs/development/roadmap/phases/phase-10/phase10_aot_scaffolding.md b/docs/development/roadmap/phases/phase-11/phase10_aot_scaffolding.md similarity index 100% rename from docs/development/roadmap/phases/phase-10/phase10_aot_scaffolding.md rename to docs/development/roadmap/phases/phase-11/phase10_aot_scaffolding.md diff --git a/docs/development/roadmap/phases/phase-10/phase_10_x_llvm_backend_skeleton.md b/docs/development/roadmap/phases/phase-11/phase_10_x_llvm_backend_skeleton.md similarity index 100% rename from docs/development/roadmap/phases/phase-10/phase_10_x_llvm_backend_skeleton.md rename to docs/development/roadmap/phases/phase-11/phase_10_x_llvm_backend_skeleton.md diff --git a/docs/ideas/other/2025-08-26-gc-switchable-language.md b/docs/ideas/other/2025-08-26-gc-switchable-language.md new file mode 100644 index 00000000..c9a3092b --- /dev/null +++ b/docs/ideas/other/2025-08-26-gc-switchable-language.md @@ -0,0 +1,158 @@ +# GC切り替え可能言語 - 史上初の柔軟なメモリ管理 + +Status: Research +Created: 2025-08-26 +Priority: Medium +Related: メモリ管理、参照カウント、開発効率 + +## 🌟 コンセプト + +Nyashを**世界初のGC切り替え可能言語**にする革新的アイデア。開発時はGCオンで快適に、本番ではGCオフで高性能に。 + +## 🎯 基本アイデア + +```nyash +// 開発時: メモリリークを気にせず開発 +nyash --gc-mode=ref-counting app.nyash + +// 本番: 最高性能で実行 +nyash --gc-mode=explicit app.nyash +``` + +## 📊 メモリ管理モード + +### 1. Explicit Mode(デフォルト) +- 現在のNyashの動作 +- スコープ抜けたら即fini() +- 予測可能な性能 +- リアルタイムアプリ向け + +### 2. Reference Counting Mode(新規) +- 参照カウントが0になったらfini() +- share_box()で参照カウント++ +- 循環参照はweak参照で解決 +- 一般アプリ向け + +## 🔧 実装戦略 + +### インタープリター(簡単) +```rust +// 実行時のモード切り替えで対応 +match memory_mode { + RefCounting => { + // 代入時に参照カウント操作 + old_value.dec_ref(); + new_value.inc_ref(); + } + Explicit => { + // 現在の動作 + } +} +``` + +### MIR(中程度) +```rust +// 新しい抽象命令を追加 +enum MirInstruction { + Acquire(temp_id), // 値の取得(GCならref++) + Release(temp_id), // 値の解放(GCならref--/fini) +} + +// MIRは同じ、実行時に挙動を変える +``` + +### VM(やや複雑) +- Acquire/Release命令の実装 +- モードフラグによる分岐 +- 参照カウント0検出時のfini呼び出し + +### Cranelift JIT(複雑) +```rust +// モード別のコード生成 +if gc_mode == Explicit { + // Acquire/Release命令をスキップ + // 直接fini呼び出し +} else { + // 参照カウント操作のインライン展開 + // 条件付きfini呼び出し +} +``` + +## 🚀 実装フェーズ + +### Phase 1: 基礎実装(1-2週間) +- [ ] BoxBaseに参照カウント追加 +- [ ] インタープリターでのモード切り替え +- [ ] 基本的なテスト + +### Phase 2: MIR/VM対応(1ヶ月) +- [ ] Acquire/Release命令の設計 +- [ ] MIRビルダーの対応 +- [ ] VM実行時の最適化 + +### Phase 3: JIT最適化(Cranelift後) +- [ ] モード別コード生成 +- [ ] デッドコード除去 +- [ ] インライン最適化 + +## 💡 革新的な使い方 + +### 開発フロー +```bash +# 1. 開発中: GCオンで快適開発 +nyash --gc-mode=ref-counting --detect-leaks dev.nyash + +# 2. テスト: メモリリーク検出 +nyash --gc-mode=ref-counting --memory-report test.nyash + +# 3. 本番: GCオフで最高性能 +nyash --gc-mode=explicit --optimize prod.nyash +``` + +### ハイブリッドモード +```nyash +// ファイル単位での制御 +// @gc-mode: explicit +box RealtimeAudioProcessor { } + +// @gc-mode: ref-counting +box UIController { } +``` + +## 🤔 技術的課題 + +### 1. 統一的なMIR表現 +- GCモードに依存しないMIR設計 +- 実行時の最適化余地を残す + +### 2. JITコード生成の複雑化 +- モード別の最適化パス +- コードキャッシュの管理 + +### 3. デバッグ情報の保持 +- どのモードで実行されているか +- 参照カウントの可視化 + +## 🎉 期待される効果 + +1. **開発効率**: GCありで快適開発 +2. **実行性能**: GCなしで最高速 +3. **教育価値**: メモリ管理の学習に最適 +4. **柔軟性**: アプリケーションに応じた選択 + +## 📚 参考資料 +- Rust: 所有権システム(コンパイル時) +- Swift: ARC(自動参照カウント) +- Go/Java: 強制GC +- C/C++: 手動管理 + +## 🔮 将来の拡張 +- Mark & Sweep GCモードの追加 +- 世代別GC +- リージョンベースメモリ管理 +- プロファイルベース自動選択 + +## 実装タイミング +- Cranelift JIT実装後が理想的 +- まずはインタープリターで実験 +- 実用性を確認してから本格実装 \ No newline at end of file diff --git a/src/backend/vm.rs b/src/backend/vm.rs index fb7b99a4..8f247d90 100644 --- a/src/backend/vm.rs +++ b/src/backend/vm.rs @@ -212,6 +212,8 @@ pub struct VM { pub(super) boxcall_pic_hits: std::collections::HashMap, /// Mono-PIC: cached direct targets (currently InstanceBox function name) pub(super) boxcall_pic_funcname: std::collections::HashMap, + /// Poly-PIC: per call-site up to 4 entries of (label, version, func_name) + pub(super) boxcall_poly_pic: std::collections::HashMap>, /// VTable-like cache: (type, method_id, arity) → direct target (InstanceBox method) pub(super) boxcall_vtable_funcname: std::collections::HashMap, /// Version map for cache invalidation: label -> version @@ -278,6 +280,7 @@ impl VM { exec_start: None, boxcall_pic_hits: std::collections::HashMap::new(), boxcall_pic_funcname: std::collections::HashMap::new(), + boxcall_poly_pic: std::collections::HashMap::new(), boxcall_vtable_funcname: std::collections::HashMap::new(), type_versions: std::collections::HashMap::new(), // TODO: Re-enable when interpreter refactoring is complete @@ -307,6 +310,7 @@ impl VM { exec_start: None, boxcall_pic_hits: std::collections::HashMap::new(), boxcall_pic_funcname: std::collections::HashMap::new(), + boxcall_poly_pic: std::collections::HashMap::new(), boxcall_vtable_funcname: std::collections::HashMap::new(), type_versions: std::collections::HashMap::new(), } @@ -339,6 +343,10 @@ impl VM { pub fn execute_module(&mut self, module: &MirModule) -> Result, VMError> { // Store module for nested calls self.module = Some(module.clone()); + // Optional: dump registry for debugging + if std::env::var("NYASH_REG_DUMP").ok().as_deref() == Some("1") { + crate::runtime::type_meta::dump_registry(); + } // Reset stats self.instr_counter.clear(); self.exec_start = Some(Instant::now()); @@ -352,10 +360,34 @@ impl VM { // Optional: print VM stats self.maybe_print_stats(); + // Optional: print cache stats summary + if std::env::var("NYASH_VM_PIC_STATS").ok().as_deref() == Some("1") { + self.print_cache_stats_summary(); + } + // Convert result to NyashBox Ok(result.to_nyash_box()) } + fn print_cache_stats_summary(&self) { + let sites_poly = self.boxcall_poly_pic.len(); + let entries_poly: usize = self.boxcall_poly_pic.values().map(|v| v.len()).sum(); + let avg_entries = if sites_poly > 0 { (entries_poly as f64) / (sites_poly as f64) } else { 0.0 }; + let sites_mono = self.boxcall_pic_funcname.len(); + let hits_total: u64 = self.boxcall_pic_hits.values().map(|v| *v as u64).sum(); + let vt_entries = self.boxcall_vtable_funcname.len(); + eprintln!( + "[VM] PIC/VT summary: poly_sites={} avg_entries={:.2} mono_sites={} hits_total={} vt_entries={}", + sites_poly, avg_entries, sites_mono, hits_total, vt_entries + ); + // Top sites by hits (up to 5) + let mut hits: Vec<(&String, &u32)> = self.boxcall_pic_hits.iter().collect(); + hits.sort_by(|a, b| b.1.cmp(a.1)); + for (i, (k, v)) in hits.into_iter().take(5).enumerate() { + eprintln!(" #{} {} hits={}", i+1, k, v); + } + } + /// Call a MIR function by name with VMValue arguments pub(super) fn call_function_by_name(&mut self, func_name: &str, args: Vec) -> Result { let module_ref = self.module.as_ref().ok_or_else(|| VMError::InvalidInstruction("No active module".to_string()))?; diff --git a/src/backend/vm_instructions.rs b/src/backend/vm_instructions.rs index a6478015..a1168ef2 100644 --- a/src/backend/vm_instructions.rs +++ b/src/backend/vm_instructions.rs @@ -58,6 +58,48 @@ impl VM { format!("VT@v{}:{}#{}{}", ver, class_name, method_id, format!("/{}", arity)) } + /// Poly-PIC probe: try up to 4 cached entries for this call-site + fn try_poly_pic(&mut self, pic_site_key: &str, recv: &VMValue) -> Option { + let label = self.cache_label_for_recv(recv); + let ver = self.cache_version_for_label(&label); + if let Some(entries) = self.boxcall_poly_pic.get_mut(pic_site_key) { + // find match and move to end for naive LRU behavior + if let Some(idx) = entries.iter().position(|(l, v, _)| *l == label && *v == ver) { + let entry = entries.remove(idx); + entries.push(entry.clone()); + return Some(entry.2); + } + } + None + } + + /// Poly-PIC record: insert or refresh an entry for this call-site + fn record_poly_pic(&mut self, pic_site_key: &str, recv: &VMValue, func_name: &str) { + let label = self.cache_label_for_recv(recv); + let ver = self.cache_version_for_label(&label); + use std::collections::hash_map::Entry; + match self.boxcall_poly_pic.entry(pic_site_key.to_string()) { + Entry::Occupied(mut e) => { + let v = e.get_mut(); + if let Some(idx) = v.iter().position(|(l, vv, _)| *l == label && *vv == ver) { + v.remove(idx); + } + if v.len() >= 4 { v.remove(0); } + v.push((label.clone(), ver, func_name.to_string())); + } + Entry::Vacant(v) => { + v.insert(vec![(label.clone(), ver, func_name.to_string())]); + } + } + if std::env::var("NYASH_VM_PIC_STATS").ok().as_deref() == Some("1") { + // minimal per-site size log + if let Some(v) = self.boxcall_poly_pic.get(pic_site_key) { + eprintln!("[PIC] site={} size={} last=({}, v{}) -> {}", + pic_site_key, v.len(), label, ver, func_name); + } + } + } + /// Compute cache label for a receiver fn cache_label_for_recv(&self, recv: &VMValue) -> String { match recv { @@ -579,11 +621,136 @@ impl VM { let pic_key = self.build_pic_key(&recv, method, method_id); self.pic_record_hit(&pic_key); - // VTable-like direct call using method_id for InstanceBox (no threshold) + // VTable-like direct call using method_id via TypeMeta thunk table (InstanceBox/PluginBoxV2/Builtin) if let (Some(mid), VMValue::BoxRef(arc_box)) = (method_id, &recv) { + // Determine class label for TypeMeta + let mut class_label: Option = None; + let mut is_instance = false; + let mut is_plugin = false; + let mut is_builtin = false; if let Some(inst) = arc_box.as_any().downcast_ref::() { - let vkey = self.build_vtable_key(&inst.class_name, mid, args.len()); - if let Some(func_name) = self.boxcall_vtable_funcname.get(&vkey).cloned() { + class_label = Some(inst.class_name.clone()); + is_instance = true; + } else if let Some(p) = arc_box.as_any().downcast_ref::() { + class_label = Some(p.box_type.clone()); + is_plugin = true; + } else { + class_label = Some(arc_box.type_name().to_string()); + is_builtin = true; + } + if let Some(label) = class_label { + let tm = crate::runtime::type_meta::get_or_create_type_meta(&label); + if let Some(th) = tm.get_thunk(mid as usize) { + if let Some(target) = th.get_target() { + match target { + crate::runtime::type_meta::ThunkTarget::MirFunction(func_name) => { + if std::env::var("NYASH_VM_VT_STATS").ok().as_deref() == Some("1") { + eprintln!("[VT] hit class={} slot={} -> {}", label, mid, func_name); + } + let mut vm_args = Vec::with_capacity(1 + args.len()); + vm_args.push(recv.clone()); + for a in args { vm_args.push(self.get_value(*a)?); } + let res = self.call_function_by_name(&func_name, vm_args)?; + if let Some(dst_id) = dst { self.set_value(dst_id, res); } + return Ok(ControlFlow::Continue); + } + crate::runtime::type_meta::ThunkTarget::PluginInvoke { method_id: mid2 } => { + if is_plugin { + if let Some(p) = arc_box.as_any().downcast_ref::() { + // Convert args prepared earlier (we need NyashBox args) + let nyash_args: Vec> = args.iter() + .map(|arg| { + let val = self.get_value(*arg)?; + Ok(val.to_nyash_box()) + }) + .collect::, VMError>>()?; + // Encode minimal TLV (int/string/handle) same as fast-path + let mut tlv = crate::runtime::plugin_ffi_common::encode_tlv_header(nyash_args.len() as u16); + let mut enc_failed = false; + for a in &nyash_args { + if let Some(s) = a.as_any().downcast_ref::() { + crate::runtime::plugin_ffi_common::encode::string(&mut tlv, &s.value); + } else if let Some(i) = a.as_any().downcast_ref::() { + crate::runtime::plugin_ffi_common::encode::i32(&mut tlv, i.value as i32); + } else if let Some(h) = a.as_any().downcast_ref::() { + crate::runtime::plugin_ffi_common::encode::plugin_handle(&mut tlv, h.inner.type_id, h.inner.instance_id); + } else { + enc_failed = true; break; + } + } + if !enc_failed { + let mut out = vec![0u8; 4096]; + let mut out_len: usize = out.len(); + let code = unsafe { + (p.inner.invoke_fn)( + p.inner.type_id, + mid2 as u32, + p.inner.instance_id, + tlv.as_ptr(), + tlv.len(), + out.as_mut_ptr(), + &mut out_len, + ) + }; + if code == 0 { + let vm_out = if let Some((_tag, _sz, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(&out[..out_len]) { + let s = crate::runtime::plugin_ffi_common::decode::string(payload); + if !s.is_empty() { + VMValue::String(s) + } else if let Some(v) = crate::runtime::plugin_ffi_common::decode::i32(payload) { + VMValue::Integer(v as i64) + } else { + VMValue::Void + } + } else { + VMValue::Void + }; + if let Some(dst_id) = dst { self.set_value(dst_id, vm_out); } + return Ok(ControlFlow::Continue); + } + } + } + } + } + crate::runtime::type_meta::ThunkTarget::BuiltinCall { method: ref m } => { + if is_builtin { + // Prepare NyashBox args and call by name + let nyash_args: Vec> = args.iter() + .map(|arg| { + let val = self.get_value(*arg)?; + Ok(val.to_nyash_box()) + }) + .collect::, VMError>>()?; + let cloned_box = arc_box.share_box(); + let out = self.call_box_method(cloned_box, m, nyash_args)?; + let vm_out = VMValue::from_nyash_box(out); + if let Some(dst_id) = dst { self.set_value(dst_id, vm_out); } + return Ok(ControlFlow::Continue); + } + } + } + } + } + // Backward-compat: consult legacy vtable cache for InstanceBox if TypeMeta empty + if is_instance { + let inst = arc_box.as_any().downcast_ref::().unwrap(); + let vkey = self.build_vtable_key(&inst.class_name, mid, args.len()); + if let Some(func_name) = self.boxcall_vtable_funcname.get(&vkey).cloned() { + let mut vm_args = Vec::with_capacity(1 + args.len()); + vm_args.push(recv.clone()); + for a in args { vm_args.push(self.get_value(*a)?); } + let res = self.call_function_by_name(&func_name, vm_args)?; + if let Some(dst_id) = dst { self.set_value(dst_id, res); } + return Ok(ControlFlow::Continue); + } + } + } + } + + // Poly-PIC direct call: if we cached a target for this site and receiver is InstanceBox, use it + if let VMValue::BoxRef(arc_box) = &recv { + if arc_box.as_any().downcast_ref::().is_some() { + if let Some(func_name) = self.try_poly_pic(&pic_key, &recv) { let mut vm_args = Vec::with_capacity(1 + args.len()); vm_args.push(recv.clone()); for a in args { vm_args.push(self.get_value(*a)?); } @@ -591,12 +758,7 @@ impl VM { if let Some(dst_id) = dst { self.set_value(dst_id, res); } return Ok(ControlFlow::Continue); } - } - } - - // Mono-PIC direct call: if we cached a target for this site and receiver is InstanceBox, use it - if let VMValue::BoxRef(arc_box) = &recv { - if arc_box.as_any().downcast_ref::().is_some() { + // Fallback to Mono-PIC (legacy) if present if let Some(func_name) = self.boxcall_pic_funcname.get(&pic_key).cloned() { // Build VM args: receiver first, then original args let mut vm_args = Vec::with_capacity(1 + args.len()); @@ -632,10 +794,39 @@ impl VM { let mut tlv = crate::runtime::plugin_ffi_common::encode_tlv_header(nyash_args.len() as u16); let mut enc_failed = false; for a in &nyash_args { + // Prefer BufferBox → bytes + if let Some(buf) = a.as_any().downcast_ref::() { + let snapshot = buf.to_vec(); + crate::runtime::plugin_ffi_common::encode::bytes(&mut tlv, &snapshot); + continue; + } if let Some(s) = a.as_any().downcast_ref::() { crate::runtime::plugin_ffi_common::encode::string(&mut tlv, &s.value); } else if let Some(i) = a.as_any().downcast_ref::() { - crate::runtime::plugin_ffi_common::encode::i32(&mut tlv, i.value as i32); + // Prefer 32-bit if fits, else i64 + if i.value >= i32::MIN as i64 && i.value <= i32::MAX as i64 { + crate::runtime::plugin_ffi_common::encode::i32(&mut tlv, i.value as i32); + } else { + crate::runtime::plugin_ffi_common::encode::i64(&mut tlv, i.value as i64); + } + } else if let Some(b) = a.as_any().downcast_ref::() { + crate::runtime::plugin_ffi_common::encode::bool(&mut tlv, b.value); + } else if let Some(f) = a.as_any().downcast_ref::() { + crate::runtime::plugin_ffi_common::encode::f64(&mut tlv, f.value); + } else if let Some(arr) = a.as_any().downcast_ref::() { + // Try encode as bytes if all elements are 0..255 integers + let items = arr.items.read().unwrap(); + let mut tmp = Vec::with_capacity(items.len()); + let mut ok = true; + for item in items.iter() { + if let Some(intb) = item.as_any().downcast_ref::() { + if intb.value >= 0 && intb.value <= 255 { tmp.push(intb.value as u8); } else { ok = false; break; } + } else { + ok = false; break; + } + } + if ok { crate::runtime::plugin_ffi_common::encode::bytes(&mut tlv, &tmp); } + else { enc_failed = true; break; } } else if let Some(h) = a.as_any().downcast_ref::() { crate::runtime::plugin_ffi_common::encode::plugin_handle(&mut tlv, h.inner.type_id, h.inner.instance_id); } else { @@ -657,16 +848,17 @@ impl VM { ) }; if code == 0 { + // Record TypeMeta thunk for plugin invoke so next time VT path can hit + let tm = crate::runtime::type_meta::get_or_create_type_meta(&p.box_type); + tm.set_thunk_plugin_invoke(mid as usize, mid as u16); // Try decode TLV first entry (string/i32); else return void - let vm_out = if let Some((_tag, _sz, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(&out[..out_len]) { - // naive: try string, then i32 - let s = crate::runtime::plugin_ffi_common::decode::string(payload); - if !s.is_empty() { - VMValue::String(s) - } else if let Some(v) = crate::runtime::plugin_ffi_common::decode::i32(payload) { - VMValue::Integer(v as i64) - } else { - VMValue::Void + let vm_out = if let Some((tag, _sz, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(&out[..out_len]) { + match tag { + 1 => crate::runtime::plugin_ffi_common::decode::bool(payload).map(VMValue::Bool).unwrap_or(VMValue::Void), + 2 => crate::runtime::plugin_ffi_common::decode::i32(payload).map(|v| VMValue::Integer(v as i64)).unwrap_or(VMValue::Void), + 5 => crate::runtime::plugin_ffi_common::decode::f64(payload).map(VMValue::Float).unwrap_or(VMValue::Void), + 6 => VMValue::String(crate::runtime::plugin_ffi_common::decode::string(payload)), + _ => VMValue::Void, } } else { VMValue::Void @@ -688,12 +880,18 @@ impl VM { // If this is a user InstanceBox, redirect to lowered function: Class.method/arity if let Some(inst) = arc_box.as_any().downcast_ref::() { let func_name = format!("{}.{}{}", inst.class_name, method, format!("/{}", args.len())); - // Populate vtable cache if method_id is known + // Populate TypeMeta thunk table and legacy vtable cache if method_id is known if let Some(mid) = method_id { + // TypeMeta preferred store (MIR target) + let tm = crate::runtime::type_meta::get_or_create_type_meta(&inst.class_name); + tm.set_thunk_mir_target(mid as usize, func_name.clone()); + // Legacy cache retained for compatibility let vkey = self.build_vtable_key(&inst.class_name, mid, args.len()); self.boxcall_vtable_funcname.entry(vkey).or_insert(func_name.clone()); } - // If this call-site is hot, cache the function name for direct calls next time + // Record in Poly-PIC immediately (size <= 4) + self.record_poly_pic(&pic_key, &recv, &func_name); + // If this call-site is hot, also cache in legacy Mono-PIC for backward behavior const PIC_THRESHOLD: u32 = 8; if self.pic_hits(&pic_key) >= PIC_THRESHOLD { self.boxcall_pic_funcname.insert(pic_key.clone(), func_name.clone()); @@ -711,6 +909,12 @@ impl VM { } // Otherwise, direct box method call if debug_boxcall { eprintln!("[BoxCall] Taking BoxRef path for method '{}'", method); } + // Populate TypeMeta for builtin path if method_id present (BuiltinCall thunk) + if let Some(mid) = method_id { + let label = arc_box.type_name().to_string(); + let tm = crate::runtime::type_meta::get_or_create_type_meta(&label); + tm.set_thunk_builtin(mid as usize, method.to_string()); + } let cloned_box = arc_box.share_box(); self.call_box_method(cloned_box, method, nyash_args)? } diff --git a/src/boxes/buffer/mod.rs b/src/boxes/buffer/mod.rs index 8383229e..1b309d83 100644 --- a/src/boxes/buffer/mod.rs +++ b/src/boxes/buffer/mod.rs @@ -47,6 +47,11 @@ impl BufferBox { } } + /// バッファ内容をコピーしてVecとして取得(FFIやエンコード向け) + pub fn to_vec(&self) -> Vec { + self.data.read().unwrap().clone() + } + /// Rust向けヘルパー: バッファ長をusizeで取得(テスト用) pub fn len(&self) -> usize { self.data.read().unwrap().len() diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 04763b41..8e9537da 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -12,6 +12,7 @@ pub mod unified_registry; pub mod nyash_runtime; // pub mod plugin_box; // legacy - 古いPluginBox // pub mod plugin_loader; // legacy - Host VTable使用 +pub mod type_meta; #[cfg(test)] mod tests; diff --git a/src/runtime/plugin_ffi_common.rs b/src/runtime/plugin_ffi_common.rs index e9432e5c..ffc65169 100644 --- a/src/runtime/plugin_ffi_common.rs +++ b/src/runtime/plugin_ffi_common.rs @@ -28,27 +28,73 @@ pub mod decode { if buf.len() < 8 + size { return None; } Some((tag, size, &buf[8..8 + size])) } + /// Decode bool payload (size must be 1; nonzero => true) + pub fn bool(payload: &[u8]) -> Option { + if payload.len() != 1 { return None; } + Some(payload[0] != 0) + } /// Decode i32 payload (size must be 4) pub fn i32(payload: &[u8]) -> Option { if payload.len() != 4 { return None; } let mut b = [0u8;4]; b.copy_from_slice(payload); Some(i32::from_le_bytes(b)) } + /// Decode f64 payload (size must be 8) + pub fn f64(payload: &[u8]) -> Option { + if payload.len() != 8 { return None; } + let mut b = [0u8;8]; b.copy_from_slice(payload); + Some(f64::from_le_bytes(b)) + } /// Decode UTF-8 string/bytes pub fn string(payload: &[u8]) -> String { String::from_utf8_lossy(payload).to_string() } + + /// Get nth TLV entry from a buffer with header + pub fn tlv_nth(buf: &[u8], n: usize) -> Option<(u8, usize, &[u8])> { + if buf.len() < 4 { return None; } + let mut off = 4usize; + for i in 0..=n { + if buf.len() < off + 4 { return None; } + let tag = buf[off]; + let _rsv = buf[off+1]; + let size = u16::from_le_bytes([buf[off+2], buf[off+3]]) as usize; + if buf.len() < off + 4 + size { return None; } + if i == n { + return Some((tag, size, &buf[off+4..off+4+size])); + } + off += 4 + size; + } + None + } } /// TLV encode helpers for primitive Nyash values pub mod encode { + /// tag for Bool + const TAG_BOOL: u8 = 1; /// tag for I32 const TAG_I32: u8 = 2; + /// tag for I64 + const TAG_I64: u8 = 3; + /// tag for F32 + const TAG_F32: u8 = 4; + /// tag for F64 + const TAG_F64: u8 = 5; /// tag for UTF-8 string const TAG_STRING: u8 = 6; + /// tag for raw bytes + const TAG_BYTES: u8 = 7; /// tag for Plugin Handle (type_id + instance_id) const TAG_HANDLE: u8 = 8; + /// Append a bool TLV entry (tag=1, size=1) + pub fn bool(buf: &mut Vec, v: bool) { + buf.push(TAG_BOOL); + buf.push(0u8); + buf.extend_from_slice(&(1u16).to_le_bytes()); + buf.push(if v { 1u8 } else { 0u8 }); + } /// Append an i32 TLV entry (tag=2, size=4, little-endian) pub fn i32(buf: &mut Vec, v: i32) { buf.push(TAG_I32); @@ -56,6 +102,27 @@ pub mod encode { buf.extend_from_slice(&(4u16).to_le_bytes()); buf.extend_from_slice(&v.to_le_bytes()); } + /// Append an i64 TLV entry (tag=3, size=8) + pub fn i64(buf: &mut Vec, v: i64) { + buf.push(TAG_I64); + buf.push(0u8); + buf.extend_from_slice(&(8u16).to_le_bytes()); + buf.extend_from_slice(&v.to_le_bytes()); + } + /// Append an f32 TLV entry (tag=4, size=4) + pub fn f32(buf: &mut Vec, v: f32) { + buf.push(TAG_F32); + buf.push(0u8); + buf.extend_from_slice(&(4u16).to_le_bytes()); + buf.extend_from_slice(&v.to_le_bytes()); + } + /// Append an f64 TLV entry (tag=5, size=8) + pub fn f64(buf: &mut Vec, v: f64) { + buf.push(TAG_F64); + buf.push(0u8); + buf.extend_from_slice(&(8u16).to_le_bytes()); + buf.extend_from_slice(&v.to_le_bytes()); + } /// Append a string TLV entry (tag=6, size=u16 trunc, UTF-8) pub fn string(buf: &mut Vec, s: &str) { @@ -66,6 +133,14 @@ pub mod encode { buf.extend_from_slice(&((len as u16).to_le_bytes())); buf.extend_from_slice(&bytes[..len]); } + /// Append bytes TLV (tag=7) + pub fn bytes(buf: &mut Vec, data: &[u8]) { + let len = core::cmp::min(data.len(), u16::MAX as usize); + buf.push(TAG_BYTES); + buf.push(0u8); + buf.extend_from_slice(&(len as u16).to_le_bytes()); + buf.extend_from_slice(&data[..len]); + } /// Append a plugin handle TLV entry (tag=8, size=8, type_id:u32 + instance_id:u32) pub fn plugin_handle(buf: &mut Vec, type_id: u32, instance_id: u32) { @@ -76,3 +151,55 @@ pub mod encode { buf.extend_from_slice(&instance_id.to_le_bytes()); } } + +#[cfg(test)] +mod tests { + use super::{encode, decode}; + + #[test] + fn test_encode_decode_bool() { + let mut buf = super::encode_tlv_header(1); + encode::bool(&mut buf, true); + let (tag, sz, payload) = decode::tlv_first(&buf).unwrap(); + assert_eq!(tag, 1); + assert_eq!(sz, 1); + assert_eq!(decode::bool(payload), Some(true)); + } + + #[test] + fn test_encode_decode_i32() { + let mut buf = super::encode_tlv_header(1); + encode::i32(&mut buf, 123456); + let (tag, sz, payload) = decode::tlv_first(&buf).unwrap(); + assert_eq!(tag, 2); + assert_eq!(sz, 4); + assert_eq!(decode::i32(payload), Some(123456)); + } + + #[test] + fn test_encode_decode_f64() { + let mut buf = super::encode_tlv_header(1); + encode::f64(&mut buf, 3.14159); + let (tag, sz, payload) = decode::tlv_first(&buf).unwrap(); + assert_eq!(tag, 5); + assert_eq!(sz, 8); + let v = decode::f64(payload).unwrap(); + assert!((v - 3.14159).abs() < 1e-9); + } + + #[test] + fn test_encode_decode_string_and_bytes() { + let mut buf = super::encode_tlv_header(2); + encode::string(&mut buf, "hello"); + encode::bytes(&mut buf, &[1,2,3,4]); + // First entry string + let (tag, _sz, payload) = decode::tlv_nth(&buf, 0).unwrap(); + assert_eq!(tag, 6); + assert_eq!(decode::string(payload), "hello"); + // Second entry bytes + let (tag2, sz2, payload2) = decode::tlv_nth(&buf, 1).unwrap(); + assert_eq!(tag2, 7); + assert_eq!(sz2, 4); + assert_eq!(payload2, &[1,2,3,4]); + } +} diff --git a/src/runtime/type_meta.rs b/src/runtime/type_meta.rs new file mode 100644 index 00000000..9595e6ad --- /dev/null +++ b/src/runtime/type_meta.rs @@ -0,0 +1,121 @@ +//! TypeMeta and Thunk table for unified Box dispatch +//! +//! Phase 9.79b.3 scaffolding: +//! - Provides per-type metadata with a vector of method thunks (by slot index) +//! - Each thunk currently holds a target function name (MIR function), +//! which VM can call via `call_function_by_name`. +//! - Versioning is sourced from `cache_versions` using label `BoxRef:{class}`. + +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, RwLock}; + +/// Target of a method thunk +#[derive(Clone, Debug)] +pub enum ThunkTarget { + /// Call a lowered MIR function by name + MirFunction(String), + /// Call plugin invoke function using receiver's plugin handle + PluginInvoke { method_id: u16 }, + /// Call builtin NyashBox method by name (name-based dispatch) + BuiltinCall { method: String }, +} + +/// A single method thunk entry (scaffolding) +#[derive(Default)] +pub struct MethodThunk { + /// Target info + target: RwLock>, + /// Flags placeholder (e.g., universal, builtin, plugin etc.) + _flags: RwLock, +} + +impl MethodThunk { + pub fn new() -> Self { Self { target: RwLock::new(None), _flags: RwLock::new(0) } } + pub fn get_target(&self) -> Option { self.target.read().ok().and_then(|g| g.clone()) } + pub fn set_mir_target(&self, name: String) { if let Ok(mut g) = self.target.write() { *g = Some(ThunkTarget::MirFunction(name)); } } + pub fn set_plugin_invoke(&self, method_id: u16) { if let Ok(mut g) = self.target.write() { *g = Some(ThunkTarget::PluginInvoke { method_id }); } } +} + +/// Per-type metadata including thunk table +pub struct TypeMeta { + class_name: String, + /// Thunk table indexed by method slot (method_id) + thunks: RwLock>>, +} + +impl TypeMeta { + fn new(class_name: String) -> Self { + Self { class_name, thunks: RwLock::new(Vec::new()) } + } + + /// Ensure that the thunk table length is at least `len`. + pub fn ensure_len(&self, len: usize) { + if let Ok(mut tbl) = self.thunks.write() { + if tbl.len() < len { + let to_add = len - tbl.len(); + for _ in 0..to_add { tbl.push(Arc::new(MethodThunk::new())); } + } + } + } + + /// Get thunk for slot, if present + pub fn get_thunk(&self, slot: usize) -> Option> { + let tbl = self.thunks.read().ok()?; + tbl.get(slot).cloned() + } + + /// Set thunk target name for slot + pub fn set_thunk_mir_target(&self, slot: usize, target_name: String) { + self.ensure_len(slot + 1); + if let Some(th) = self.get_thunk(slot) { th.set_mir_target(target_name); } + } + + pub fn set_thunk_plugin_invoke(&self, slot: usize, method_id: u16) { + self.ensure_len(slot + 1); + if let Some(th) = self.get_thunk(slot) { th.set_plugin_invoke(method_id); } + } + + pub fn set_thunk_builtin(&self, slot: usize, method: String) { + self.ensure_len(slot + 1); + if let Some(th) = self.get_thunk(slot) { + if let Ok(mut g) = th.target.write() { + *g = Some(ThunkTarget::BuiltinCall { method }); + } + } + } + + /// Current version from global cache (for diagnostics / PIC keys) + pub fn current_version(&self) -> u32 { + crate::runtime::cache_versions::get_version(&format!("BoxRef:{}", self.class_name)) + } +} + +static TYPE_META_REGISTRY: Lazy>>> = Lazy::new(|| Mutex::new(HashMap::new())); + +/// Get or create TypeMeta for a given class name +pub fn get_or_create_type_meta(class_name: &str) -> Arc { + let mut map = TYPE_META_REGISTRY.lock().unwrap(); + if let Some(m) = map.get(class_name) { return m.clone(); } + let m = Arc::new(TypeMeta::new(class_name.to_string())); + map.insert(class_name.to_string(), m.clone()); + m +} + +/// Dump registry contents for diagnostics +pub fn dump_registry() { + let map = TYPE_META_REGISTRY.lock().unwrap(); + eprintln!("[REG] TypeMeta registry dump ({} types)", map.len()); + for (name, meta) in map.iter() { + let tbl = meta.thunks.read().ok(); + let len = tbl.as_ref().map(|t| t.len()).unwrap_or(0); + eprintln!(" - {}: thunks={} v{}", name, len, meta.current_version()); + if let Some(t) = tbl { + for (i, th) in t.iter().enumerate() { + if let Some(target) = th.get_target() { + eprintln!(" slot {} -> {:?}", i, target); + } + } + } + } +}