feat: Phase 10 reorganization + GC switchable runtime + VM improvements

## 📚 Documentation Updates
- Phase 10 reorganized with comprehensive README
  - Cranelift JIT as main implementation
  - NEW: Phase 10.4 GC Switchable Runtime (world's first\!)
  - Phase 10.5 Self-hosting (parallel)
  - Application migration tests
- Phase 11 created for LLVM AOT research (deferred)
  - Moved phase10_aot_scaffolding.md → Phase 11
  - Moved phase_10_x_llvm_backend_skeleton.md → Phase 11
- Master roadmap updated with GC runtime feature
- Ideas: GC switchable language concept documented

## 🚀 VM Implementation Progress (by ChatGPT5)
- src/backend/vm.rs: Enhanced VM execution
- src/backend/vm_instructions.rs: Instruction improvements
- src/runtime/type_meta.rs: NEW - Type metadata system
- src/boxes/buffer/mod.rs: Buffer optimizations
- src/runtime/mod.rs & plugin_ffi_common.rs: Runtime enhancements

## 🌟 Revolutionary Feature: GC Switchable Runtime
- Development mode: GC on (convenience)
- Production mode: GC off (performance)
- Technical feasibility confirmed by Codex GPT-5
- Implementation plan: After Cranelift JIT

## 📋 Phase 10 Structure
Phase 10.0: Cranelift JIT foundation
Phase 10.1-10.3: JIT implementation & optimization
Phase 10.4: GC Switchable Runtime ← NEW\!
Phase 10.5: Self-hosting (String/Array/Map in Nyash)
Phase 10.9: Application migration tests

🤖 ChatGPT5 says: Ready for Phase 10\! どきどきにゃ!
This commit is contained in:
Moe Charm
2025-08-27 01:03:55 +09:00
parent e5515ea5e9
commit 61800a37a7
14 changed files with 1052 additions and 21 deletions

View File

@ -111,6 +111,7 @@ nyash bid gen --target llvm bid.yaml # AOT用declare生成LLVM実装時
- MIR→VMを維持しつつ、ホットパスをCraneliftでJIT化 - MIR→VMを維持しつつ、ホットパスをCraneliftでJIT化
- 目標: VM比2倍以上の高速化 - 目標: VM比2倍以上の高速化
- LLVM AOTは設計資産は維持しつつ、Phase 11以降に検討 - LLVM AOTは設計資産は維持しつつ、Phase 11以降に検討
- **🌟 NEW: GC切り替え可能ランタイム世界初の柔軟なメモリ管理**
**Start Gate着手前の必須完了**: **Start Gate着手前の必須完了**:
- ✅ MIRダイエット26命令整合完了 - ✅ MIRダイエット26命令整合完了
@ -122,6 +123,8 @@ nyash bid gen --target llvm bid.yaml # AOT用declare生成LLVM実装時
1. **Phase 10.1**: Proof of Concept2週間 1. **Phase 10.1**: Proof of Concept2週間
2. **Phase 10.2**: 基本実装4週間 2. **Phase 10.2**: 基本実装4週間
3. **Phase 10.3**: 非同期の扱い(最小) 3. **Phase 10.3**: 非同期の扱い(最小)
4. **Phase 10.4**: GC切り替え可能ランタイム2-3ヶ月
5. **Phase 10.5**: セルフホスティング(並行実装)
--- ---

View File

@ -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/) - 中間表現

View File

@ -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<Mutex>によるスレッドセーフ設計
### 提案する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<Mutex>の重さ
**課題**: 現在すべてのBoxがArc<Mutex>で重い
**解決**: 必要な場所のみ同期、基本型は非同期に
### 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
- リージョンベースメモリ管理
- プロファイルベース自動モード選択

View File

@ -60,3 +60,93 @@ AST → MIR → Optimizer → VM Dispatcher
--- ---
備考: LLVM AOTはPhase 11以降の研究路線に移行設計ドキュメントは維持 備考: 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→JITJIT→VM
- 引数/戻り値の受け渡し(整数/浮動/void
- 例外/TypeErrorはVMへバイアウトtrap→VM
- 受入: 再帰/多段呼び出しが安定動作
### 10_d: コレクション基礎 Array/Map ホット操作(外部呼び出し)
- 目的: 実用的ホットパスlength/get/set/push/popをJIT側から呼べるように。
- 具体:
- ホスト関数テーブル外部シンボルで既存Rust実装を呼ぶ
- 境界チェック/エラーはRust側に委譲、JITは薄い橋渡し
- 受入: Array操作がVM経由より高速目安1.52.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 24件→ヒット直呼び、ミス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`で観測

View File

@ -0,0 +1,56 @@
# Phase 11: LLVM AOT Backend将来研究
## 🎯 概要
Phase 11は、LLVM を使用した Ahead-of-TimeAOTコンパイル機能の研究・実装フェーズです。
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 10Cranelift 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) - 全体計画

View File

@ -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実装後が理想的
- まずはインタープリターで実験
- 実用性を確認してから本格実装

View File

@ -212,6 +212,8 @@ pub struct VM {
pub(super) boxcall_pic_hits: std::collections::HashMap<String, u32>, pub(super) boxcall_pic_hits: std::collections::HashMap<String, u32>,
/// Mono-PIC: cached direct targets (currently InstanceBox function name) /// Mono-PIC: cached direct targets (currently InstanceBox function name)
pub(super) boxcall_pic_funcname: std::collections::HashMap<String, String>, pub(super) boxcall_pic_funcname: std::collections::HashMap<String, String>,
/// Poly-PIC: per call-site up to 4 entries of (label, version, func_name)
pub(super) boxcall_poly_pic: std::collections::HashMap<String, Vec<(String, u32, String)>>,
/// VTable-like cache: (type, method_id, arity) → direct target (InstanceBox method) /// VTable-like cache: (type, method_id, arity) → direct target (InstanceBox method)
pub(super) boxcall_vtable_funcname: std::collections::HashMap<String, String>, pub(super) boxcall_vtable_funcname: std::collections::HashMap<String, String>,
/// Version map for cache invalidation: label -> version /// Version map for cache invalidation: label -> version
@ -278,6 +280,7 @@ impl VM {
exec_start: None, exec_start: None,
boxcall_pic_hits: std::collections::HashMap::new(), boxcall_pic_hits: std::collections::HashMap::new(),
boxcall_pic_funcname: 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(), boxcall_vtable_funcname: std::collections::HashMap::new(),
type_versions: std::collections::HashMap::new(), type_versions: std::collections::HashMap::new(),
// TODO: Re-enable when interpreter refactoring is complete // TODO: Re-enable when interpreter refactoring is complete
@ -307,6 +310,7 @@ impl VM {
exec_start: None, exec_start: None,
boxcall_pic_hits: std::collections::HashMap::new(), boxcall_pic_hits: std::collections::HashMap::new(),
boxcall_pic_funcname: 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(), boxcall_vtable_funcname: std::collections::HashMap::new(),
type_versions: 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<Box<dyn NyashBox>, VMError> { pub fn execute_module(&mut self, module: &MirModule) -> Result<Box<dyn NyashBox>, VMError> {
// Store module for nested calls // Store module for nested calls
self.module = Some(module.clone()); 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 // Reset stats
self.instr_counter.clear(); self.instr_counter.clear();
self.exec_start = Some(Instant::now()); self.exec_start = Some(Instant::now());
@ -352,10 +360,34 @@ impl VM {
// Optional: print VM stats // Optional: print VM stats
self.maybe_print_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 // Convert result to NyashBox
Ok(result.to_nyash_box()) 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 /// Call a MIR function by name with VMValue arguments
pub(super) fn call_function_by_name(&mut self, func_name: &str, args: Vec<VMValue>) -> Result<VMValue, VMError> { pub(super) fn call_function_by_name(&mut self, func_name: &str, args: Vec<VMValue>) -> Result<VMValue, VMError> {
let module_ref = self.module.as_ref().ok_or_else(|| VMError::InvalidInstruction("No active module".to_string()))?; let module_ref = self.module.as_ref().ok_or_else(|| VMError::InvalidInstruction("No active module".to_string()))?;

View File

@ -58,6 +58,48 @@ impl VM {
format!("VT@v{}:{}#{}{}", ver, class_name, method_id, format!("/{}", arity)) 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<String> {
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 /// Compute cache label for a receiver
fn cache_label_for_recv(&self, recv: &VMValue) -> String { fn cache_label_for_recv(&self, recv: &VMValue) -> String {
match recv { match recv {
@ -579,9 +621,119 @@ impl VM {
let pic_key = self.build_pic_key(&recv, method, method_id); let pic_key = self.build_pic_key(&recv, method, method_id);
self.pic_record_hit(&pic_key); 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) { if let (Some(mid), VMValue::BoxRef(arc_box)) = (method_id, &recv) {
// Determine class label for TypeMeta
let mut class_label: Option<String> = 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::<crate::instance_v2::InstanceBox>() { if let Some(inst) = arc_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
class_label = Some(inst.class_name.clone());
is_instance = true;
} else if let Some(p) = arc_box.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
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::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
// Convert args prepared earlier (we need NyashBox args)
let nyash_args: Vec<Box<dyn NyashBox>> = args.iter()
.map(|arg| {
let val = self.get_value(*arg)?;
Ok(val.to_nyash_box())
})
.collect::<Result<Vec<_>, 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::box_trait::StringBox>() {
crate::runtime::plugin_ffi_common::encode::string(&mut tlv, &s.value);
} else if let Some(i) = a.as_any().downcast_ref::<crate::box_trait::IntegerBox>() {
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_loader_v2::PluginBoxV2>() {
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<Box<dyn NyashBox>> = args.iter()
.map(|arg| {
let val = self.get_value(*arg)?;
Ok(val.to_nyash_box())
})
.collect::<Result<Vec<_>, 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::<crate::instance_v2::InstanceBox>().unwrap();
let vkey = self.build_vtable_key(&inst.class_name, mid, args.len()); let vkey = self.build_vtable_key(&inst.class_name, mid, args.len());
if let Some(func_name) = self.boxcall_vtable_funcname.get(&vkey).cloned() { if let Some(func_name) = self.boxcall_vtable_funcname.get(&vkey).cloned() {
let mut vm_args = Vec::with_capacity(1 + args.len()); let mut vm_args = Vec::with_capacity(1 + args.len());
@ -593,10 +745,20 @@ impl VM {
} }
} }
} }
}
// Mono-PIC direct call: if we cached a target for this site and receiver is InstanceBox, use it // 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 let VMValue::BoxRef(arc_box) = &recv {
if arc_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>().is_some() { if arc_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>().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)?); }
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);
}
// Fallback to Mono-PIC (legacy) if present
if let Some(func_name) = self.boxcall_pic_funcname.get(&pic_key).cloned() { if let Some(func_name) = self.boxcall_pic_funcname.get(&pic_key).cloned() {
// Build VM args: receiver first, then original args // Build VM args: receiver first, then original args
let mut vm_args = Vec::with_capacity(1 + args.len()); 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 tlv = crate::runtime::plugin_ffi_common::encode_tlv_header(nyash_args.len() as u16);
let mut enc_failed = false; let mut enc_failed = false;
for a in &nyash_args { for a in &nyash_args {
// Prefer BufferBox → bytes
if let Some(buf) = a.as_any().downcast_ref::<crate::boxes::buffer::BufferBox>() {
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::box_trait::StringBox>() { if let Some(s) = a.as_any().downcast_ref::<crate::box_trait::StringBox>() {
crate::runtime::plugin_ffi_common::encode::string(&mut tlv, &s.value); crate::runtime::plugin_ffi_common::encode::string(&mut tlv, &s.value);
} else if let Some(i) = a.as_any().downcast_ref::<crate::box_trait::IntegerBox>() { } else if let Some(i) = a.as_any().downcast_ref::<crate::box_trait::IntegerBox>() {
// 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); 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::box_trait::BoolBox>() {
crate::runtime::plugin_ffi_common::encode::bool(&mut tlv, b.value);
} else if let Some(f) = a.as_any().downcast_ref::<crate::boxes::math_box::FloatBox>() {
crate::runtime::plugin_ffi_common::encode::f64(&mut tlv, f.value);
} else if let Some(arr) = a.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
// 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::<crate::box_trait::IntegerBox>() {
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_loader_v2::PluginBoxV2>() { } else if let Some(h) = a.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
crate::runtime::plugin_ffi_common::encode::plugin_handle(&mut tlv, h.inner.type_id, h.inner.instance_id); crate::runtime::plugin_ffi_common::encode::plugin_handle(&mut tlv, h.inner.type_id, h.inner.instance_id);
} else { } else {
@ -657,16 +848,17 @@ impl VM {
) )
}; };
if code == 0 { 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 // 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]) { 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 match tag {
let s = crate::runtime::plugin_ffi_common::decode::string(payload); 1 => crate::runtime::plugin_ffi_common::decode::bool(payload).map(VMValue::Bool).unwrap_or(VMValue::Void),
if !s.is_empty() { 2 => crate::runtime::plugin_ffi_common::decode::i32(payload).map(|v| VMValue::Integer(v as i64)).unwrap_or(VMValue::Void),
VMValue::String(s) 5 => crate::runtime::plugin_ffi_common::decode::f64(payload).map(VMValue::Float).unwrap_or(VMValue::Void),
} else if let Some(v) = crate::runtime::plugin_ffi_common::decode::i32(payload) { 6 => VMValue::String(crate::runtime::plugin_ffi_common::decode::string(payload)),
VMValue::Integer(v as i64) _ => VMValue::Void,
} else {
VMValue::Void
} }
} else { } else {
VMValue::Void VMValue::Void
@ -688,12 +880,18 @@ impl VM {
// If this is a user InstanceBox, redirect to lowered function: Class.method/arity // If this is a user InstanceBox, redirect to lowered function: Class.method/arity
if let Some(inst) = arc_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = arc_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
let func_name = format!("{}.{}{}", inst.class_name, method, format!("/{}", args.len())); 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 { 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()); let vkey = self.build_vtable_key(&inst.class_name, mid, args.len());
self.boxcall_vtable_funcname.entry(vkey).or_insert(func_name.clone()); 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; const PIC_THRESHOLD: u32 = 8;
if self.pic_hits(&pic_key) >= PIC_THRESHOLD { if self.pic_hits(&pic_key) >= PIC_THRESHOLD {
self.boxcall_pic_funcname.insert(pic_key.clone(), func_name.clone()); self.boxcall_pic_funcname.insert(pic_key.clone(), func_name.clone());
@ -711,6 +909,12 @@ impl VM {
} }
// Otherwise, direct box method call // Otherwise, direct box method call
if debug_boxcall { eprintln!("[BoxCall] Taking BoxRef path for method '{}'", method); } 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(); let cloned_box = arc_box.share_box();
self.call_box_method(cloned_box, method, nyash_args)? self.call_box_method(cloned_box, method, nyash_args)?
} }

View File

@ -47,6 +47,11 @@ impl BufferBox {
} }
} }
/// バッファ内容をコピーしてVec<u8>として取得FFIやエンコード向け
pub fn to_vec(&self) -> Vec<u8> {
self.data.read().unwrap().clone()
}
/// Rust向けヘルパー: バッファ長をusizeで取得テスト用 /// Rust向けヘルパー: バッファ長をusizeで取得テスト用
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.data.read().unwrap().len() self.data.read().unwrap().len()

View File

@ -12,6 +12,7 @@ pub mod unified_registry;
pub mod nyash_runtime; pub mod nyash_runtime;
// pub mod plugin_box; // legacy - 古いPluginBox // pub mod plugin_box; // legacy - 古いPluginBox
// pub mod plugin_loader; // legacy - Host VTable使用 // pub mod plugin_loader; // legacy - Host VTable使用
pub mod type_meta;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;

View File

@ -28,27 +28,73 @@ pub mod decode {
if buf.len() < 8 + size { return None; } if buf.len() < 8 + size { return None; }
Some((tag, size, &buf[8..8 + size])) Some((tag, size, &buf[8..8 + size]))
} }
/// Decode bool payload (size must be 1; nonzero => true)
pub fn bool(payload: &[u8]) -> Option<bool> {
if payload.len() != 1 { return None; }
Some(payload[0] != 0)
}
/// Decode i32 payload (size must be 4) /// Decode i32 payload (size must be 4)
pub fn i32(payload: &[u8]) -> Option<i32> { pub fn i32(payload: &[u8]) -> Option<i32> {
if payload.len() != 4 { return None; } if payload.len() != 4 { return None; }
let mut b = [0u8;4]; b.copy_from_slice(payload); let mut b = [0u8;4]; b.copy_from_slice(payload);
Some(i32::from_le_bytes(b)) Some(i32::from_le_bytes(b))
} }
/// Decode f64 payload (size must be 8)
pub fn f64(payload: &[u8]) -> Option<f64> {
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 /// Decode UTF-8 string/bytes
pub fn string(payload: &[u8]) -> String { pub fn string(payload: &[u8]) -> String {
String::from_utf8_lossy(payload).to_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 /// TLV encode helpers for primitive Nyash values
pub mod encode { pub mod encode {
/// tag for Bool
const TAG_BOOL: u8 = 1;
/// tag for I32 /// tag for I32
const TAG_I32: u8 = 2; 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 /// tag for UTF-8 string
const TAG_STRING: u8 = 6; const TAG_STRING: u8 = 6;
/// tag for raw bytes
const TAG_BYTES: u8 = 7;
/// tag for Plugin Handle (type_id + instance_id) /// tag for Plugin Handle (type_id + instance_id)
const TAG_HANDLE: u8 = 8; const TAG_HANDLE: u8 = 8;
/// Append a bool TLV entry (tag=1, size=1)
pub fn bool(buf: &mut Vec<u8>, 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) /// Append an i32 TLV entry (tag=2, size=4, little-endian)
pub fn i32(buf: &mut Vec<u8>, v: i32) { pub fn i32(buf: &mut Vec<u8>, v: i32) {
buf.push(TAG_I32); buf.push(TAG_I32);
@ -56,6 +102,27 @@ pub mod encode {
buf.extend_from_slice(&(4u16).to_le_bytes()); buf.extend_from_slice(&(4u16).to_le_bytes());
buf.extend_from_slice(&v.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<u8>, 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<u8>, 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<u8>, 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) /// Append a string TLV entry (tag=6, size=u16 trunc, UTF-8)
pub fn string(buf: &mut Vec<u8>, s: &str) { pub fn string(buf: &mut Vec<u8>, s: &str) {
@ -66,6 +133,14 @@ pub mod encode {
buf.extend_from_slice(&((len as u16).to_le_bytes())); buf.extend_from_slice(&((len as u16).to_le_bytes()));
buf.extend_from_slice(&bytes[..len]); buf.extend_from_slice(&bytes[..len]);
} }
/// Append bytes TLV (tag=7)
pub fn bytes(buf: &mut Vec<u8>, 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) /// Append a plugin handle TLV entry (tag=8, size=8, type_id:u32 + instance_id:u32)
pub fn plugin_handle(buf: &mut Vec<u8>, type_id: u32, instance_id: u32) { pub fn plugin_handle(buf: &mut Vec<u8>, type_id: u32, instance_id: u32) {
@ -76,3 +151,55 @@ pub mod encode {
buf.extend_from_slice(&instance_id.to_le_bytes()); 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]);
}
}

121
src/runtime/type_meta.rs Normal file
View File

@ -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<Option<ThunkTarget>>,
/// Flags placeholder (e.g., universal, builtin, plugin etc.)
_flags: RwLock<u32>,
}
impl MethodThunk {
pub fn new() -> Self { Self { target: RwLock::new(None), _flags: RwLock::new(0) } }
pub fn get_target(&self) -> Option<ThunkTarget> { 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<Vec<Arc<MethodThunk>>>,
}
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<Arc<MethodThunk>> {
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<Mutex<HashMap<String, Arc<TypeMeta>>>> = 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<TypeMeta> {
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);
}
}
}
}
}