diff --git a/docs/予定/native-plan/issues/phase_9_78c_plugin_delegation_unification.md b/docs/予定/native-plan/issues/phase_9_78c_plugin_delegation_unification.md new file mode 100644 index 00000000..9959148e --- /dev/null +++ b/docs/予定/native-plan/issues/phase_9_78c_plugin_delegation_unification.md @@ -0,0 +1,75 @@ +# Phase 9.78c: プラグインBoxのデリゲーション一体化計画(Interpreter先行) + +作成日: 2025-08-21 +優先度: 高 +担当: Codex + User + +## 目的(Goal) +ユーザー定義/ビルトイン/プラグインの3種のBoxを、デリゲーション(`from Parent.method`)と通常メソッド解決で「ほぼ同じ体験」に揃える。第一段としてInterpreter側での親メソッド呼び出し経路を拡張し、プラグイン親のフォールバック経路を提供する。 + +## 背景(Context) +- 現在: + - ユーザー定義は素直に解決、ビルトインは`__builtin_content`経由で親メソッドを呼び出し可能。 + - プラグインは`PluginBoxV2`として生成・利用可能だが、親メソッド呼び出しのフォールバック経路が未整備。 +- VM側は`BoxCall`で`PluginBoxV2`を検出し、ローダにルーティング可能(最小の引数/戻り値サポートは導入済)。 +- Interpreterでも同じ体験を提供するため、親プラグインへのブリッジを追加する。 + +## スコープ(Scope) +- Interpreter先行の対応: + 1) `InstanceBox` に `__plugin_content`(NyashValue::Box)を導入 + 2) 子Boxが `from PluginBox` を宣言している場合、子の生誕時(birth)にプラグイン親を生成して保持 + 3) メソッド解決: + - ユーザー定義メソッドで見つからない → 既存のビルトイン親チェック → プラグイン親チェック(`__plugin_content`があれば `PluginLoaderV2.invoke_instance_method`) + 4) `from Parent.method` のInterpreter側分岐:Parentがプラグインであれば、上記 `invoke_instance_method` に直接ルーティング +- VM側の“from Parent.method”は次フェーズ(9.78d以降)で整備(Builder/VMの双方に影響大のため) + +## 具体タスク(Plan) +1. InstanceBox拡張(低リスク) + - `fields_ng`に `"__plugin_content"` を保持できるように(キー名は固定) + - birth直後に、プラグイン親(if any)を `PluginLoaderV2.create_box` で生成→`__plugin_content`格納 + - 注意: 生成に必要な引数はゼロ想定。将来は子birth引数から親引数を抽出する設計を追加 + +2. メソッド解決のフォールバック(中リスク) + - 現状の解決順(ユーザー定義 → ビルトイン親)に続けて、プラグイン親を追加 + - `__plugin_content`が `PluginBoxV2` なら、`PluginLoaderV2.invoke_instance_method(parent_box_type, method, instance_id, args)` を実行 + - 引数/戻り値は最小TLV(Int/String/Bool)に限定(VM側と整合) + +3. from Parent.method(Interpreter側)の分岐拡張(中リスク) + - Parentがビルトイン:現状を維持 + - Parentがユーザー定義:現状を維持 + - Parentがプラグイン:`__plugin_content`が存在することを前提に `invoke_instance_method` を呼び出し + +4. テスト(小粒E2E、低リスク) + - ユーザー定義Boxが `from PluginBox` を持つケースで、子メソッドから `from PluginBox.method()` へ到達できること(ゼロ/1引数程度) + - 直接 `plugin_parent.method()` を呼ばず、子 → 親(プラグイン)呼び出しの透過性を確認 + +5. ドキュメント(低リスク) + - `docs/説明書/VM/README.md` に「プラグイン親フォールバック」を追記 + - `docs/説明書/reference` に `__plugin_content` の内部仕様を簡潔に注記(内部予約キー) + +## 非スコープ(Out of Scope) +- VM側の `from Parent.method` の完全対応(次フェーズ) +- TLVの型拡張(Float/配列/Box参照など)—次フェーズで段階的に +- プラグイン親のフィールド継承(行わない) + +## リスクと対策 +- リスク: birth時のプラグイン生成失敗(nyash.toml/共有ライブラリ未整備) + - 対策: 失敗時は`__plugin_content`未設定で続行し、親メソッド呼び出し時に明示エラー +- リスク: 引数/戻り値のTLVが最小型に留まるため、メソッド範囲に制限 + - 対策: 先に成功体験を提供し、必要に応じて段階拡張(9.78d以降) + +## 受け入れ基準(Acceptance Criteria) +- 子Boxが`from PluginBox`を持つ時、子メソッド中の `from PluginBox.method()` がInterpreterで正常に実行される +- `new Child(...).childMethod()` →(内部で)プラグイン親メソッド呼び出しが透過的に成功 +- 失敗時にわかりやすいエラー(設定不足/メソッド未定義) +- 既存E2E/ライブラリ型チェックが維持される + +## 実装順序(Milestones) +1. InstanceBox `__plugin_content`導入 + birth時格納(1日) +2. メソッド解決フォールバック追加(1~2日) +3. from Parent.method 分岐拡張(1日) +4. 小粒E2E・ドキュメント更新(0.5~1日) + +## 備考 +- VM側は既に`BoxCall`でPluginメソッドを呼び出せる。Interpreterを先行して整備し、ユーザー体験を揃える。 +- 将来は「親メソッドテーブル」の共通インターフェイスをランタイムへ寄せ、Interpreter/VMの実装差分を更に縮小する。 diff --git a/docs/説明書/VM/README.md b/docs/説明書/VM/README.md new file mode 100644 index 00000000..766d887f --- /dev/null +++ b/docs/説明書/VM/README.md @@ -0,0 +1,103 @@ +# Nyash VM 実行基盤ガイド + +最終更新: 2025-08-21(Phase 9.78 系対応) + +本書は Nyash の VM バックエンド(MIR 実行機構)と、その周辺の実装・拡張ポイントをまとめた開発者向けドキュメントです。 + +## 全体像 +- 入力: `MirModule`(`mir::MirCompiler` が AST から生成) +- 実行: `backend::vm::VM` + - `execute_module` → `execute_function` → 命令列を逐次 `execute_instruction` +- ランタイム DI: `NyashRuntime` + - `box_registry`(統一 BoxFactory 経由の生成) + - `box_declarations`(ユーザー定義 Box の宣言) +- ライフサイクル: `ScopeTracker` により関数入退出で `fini()` を呼ぶ + +## 主要ファイル +- `src/backend/vm.rs` … VM 本体(命令ディスパッチ、Call/BoxCall、NewBox ほか) +- `src/mir/*` … MIR 命令定義・Builder・Function/Module 管理 +- `src/runtime/nyash_runtime.rs` … ランタイムコンテナ(DI 受け皿) +- `src/box_factory/*` … Builtin/User/Plugin の各 Factory 実装 +- `src/runtime/plugin_loader_v2.rs` … BID-FFI v2 ローダ(ExternCall/Plugin 呼び出し) + +## 実行フロー(概略) +1) Nyash コード → Parser → AST → `MirCompiler` で `MirModule` を生成 +2) `VM::with_runtime(runtime)` で実行(`execute_module`) +3) 命令ごとに処理: + - `Const/Load/Store/BinOp/...` など基本命令 + - `NewBox`: 統一レジストリ経由で Box 生成 + - `Call`: `"{Box}.{method}/{N}"` の関数名で MIR 関数呼び出し + - `BoxCall`: Box の種類で分岐(ユーザー定義/ビルトイン/プラグイン) + - `ExternCall`: `env.console`/`env.canvas` 等をローダへ委譲 + +## Box 生成と種別 +- 生成パス(`NewBox`)は `NyashRuntime::box_registry` が担当 + - Builtin: `BuiltinBoxFactory` が直接生成 + - User-defined: `UserDefinedBoxFactory` → `InstanceBox` + - Plugin: プラグイン設定(`nyash.toml`)に従い BID-FFI で `PluginBoxV2` + +## birth/メソッドの関数化(MIR) +- Lowering ポリシー: AST の `new` は `NewBox` に続けて `BoxCall("birth")` を自動挿入 +- Box 宣言の `birth/N` と通常メソッド `method/N` は `"{Box}.{name}/{N}"` の MIR 関数に関数化 + - 命名例: `Person.birth/1`, `Person.greet/0` + - 引数: `me` が `%0`、ユーザー引数が `%1..N`(`me` は `MirType::Box(BoxName)`) + - 戻り値型: 明示の `return ` があれば `Unknown`、なければ `Void`(軽量推定) +- `VM` の呼び出し + - `Call` 命令: 関数名(`Const(String)`)を解決 → `call_function_by_name` + - `BoxCall` 命令: 下記の種類分岐に委譲 + +## BoxCall の種類分岐 +- ユーザー定義 Box(`InstanceBox`) + - `BoxCall("birth")`: `"{Box}.birth/{argc}"` を `Call` 等価で実行 + - 通常メソッド: `"{Box}.{method}/{argc}"` を `Call` 等価で実行 +- プラグイン Box(`PluginBoxV2`) + - `PluginLoaderV2::invoke_instance_method(box_type, method, instance_id, args)` を呼び出し + - 引数/戻り値は最小 TLV でやり取り(タグ: 1=Int64, 2=String, 3=Bool) + - 戻り値なしは `void` 扱い +- ビルトイン Box + - 現状は VM 内の簡易ディスパッチ(`String/Integer/Array/Math` を中心にサポート) + - 将来はビルトインも MIR 関数へ寄せる計画 + +## ExternCall(ホスト機能) +- `env.console.log`, `env.canvas.*` などを `PluginLoaderV2::extern_call` に委譲 +- いまは最小実装(ログ出力・スタブ)。将来は BID-FFI 由来の外部機能にも接続予定 + +## ライフサイクル管理(ScopeTracker) +- `VM` は関数入退出で `push_scope()/pop_scope()` を実行 +- 退出時に登録 Box を `fini()`(`InstanceBox`/`PluginBoxV2`) +- Interpreter でも同思想で `restore_local_vars()` にて `fini()` 呼び出し + +## ランタイム DI(依存注入) +- `NyashRuntime` + - `box_declarations`: AST から収集(Box 宣言) + - `box_registry`: Builtin/User/Plugin の順で探索・生成 +- Runner(CLI 実行) + - AST パース後、Box 宣言を `runtime.box_declarations` へ登録 + - `UserDefinedBoxFactory` をレジストリに注入 → VM を `with_runtime(runtime)` で起動 + +## 最適化 +- 置換: `new X(...).m(...)` → 直接 `Call("X.m/N", me+args)` に最適化 +- 拡張余地: 変数へ束縛してからの `.m()` も静的に決まる範囲で `Call` 化可能 +- 戻り値型: 軽量推定。将来は `MirType` 推論/注釈の強化 + +## 制約と今後 +- ビルトインのメソッドはまだ簡易ディスパッチ(MIR 関数化は未) +- プラグインの TLV は最小型(Int/String/Bool)のみ。拡張予定 +- 例外(throw/catch)は簡易扱い(将来の unwind/handler 連携は別設計) + +## テスト +- 単体/E2E(抜粋): `src/backend/vm.rs` の `#[cfg(test)]` + - `test_vm_user_box_birth_and_method` … `new Person("Alice").greet()` → "Hello, Alice" + - `test_vm_user_box_var_then_method` … 変数に束縛→メソッド→戻り値(11) + - `test_vm_extern_console_log` … ExternCall の void 確認 +- 実行例 + - `cargo test -j32`(plugins 機能や環境依存に注意) + +## 実行(参考) +- VMルート実行(Runner 経由) + - `nyash --backend vm your_file.nyash` +- WASM(ブラウザ) + - plugins は無効。ExternCall はスタブ。MIR 関数はそのまま再利用される設計 + +--- +開発ポリシー: 小さく安全に最適化 → MIR/VM の共有ロジックを増やす → Extern/Plugin を段階統合 → WASM/LLVM/JIT へ横展開。 diff --git a/docs/説明書/VM/次のお勧め.txt b/docs/説明書/VM/次のお勧め.txt new file mode 100644 index 00000000..4c60f0c1 --- /dev/null +++ b/docs/説明書/VM/次のお勧め.txt @@ -0,0 +1,11 @@ +- おすすめの次手(どれを先にやる?) + - BoxCall→Callの最適化範囲拡大(変数に束縛したnewの直後メソッドもCall化) + - プラグインTLVの型拡張(Float/配列/Boxリファレンスの扱いなど) + - ビルトインメソッドのMIR関数化(Dispatchの簡素化・最適化に寄与) + - E2Eの拡張(throw/catch、pluginメソッドの戻り値ありケース、fini観察) + +VMガイドの場所(Windows) +- `C:\git\nyash-project\nyash\docs\説明書\VM\README.md` + +このまま、BoxCall→Callの最適化拡大から着手しますか?それともプラグインのTLV拡張 +(引数/戻り値の型追加)を先にやる? \ No newline at end of file diff --git a/src/interpreter/expressions/calls.rs b/src/interpreter/expressions/calls.rs index b27e8074..7af8c6cc 100644 --- a/src/interpreter/expressions/calls.rs +++ b/src/interpreter/expressions/calls.rs @@ -680,6 +680,27 @@ impl NyashInterpreter { } } + // プラグイン親のメソッド呼び出し(__plugin_content) + #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] + { + if let Some(plugin_shared) = instance.get_field_legacy("__plugin_content") { + let plugin_ref = &*plugin_shared; + if let Some(plugin) = plugin_ref.as_any().downcast_ref::() { + let mut arg_values: Vec> = Vec::new(); + for arg in arguments { + arg_values.push(self.execute_expression(arg)?); + } + let loader = crate::runtime::get_global_loader_v2(); + let loader = loader.read().unwrap(); + match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id, &arg_values) { + Ok(Some(result_box)) => return Ok(result_box), + Ok(None) => return Ok(Box::new(VoidBox::new())), + Err(_) => {} + } + } + } + } + // メソッドが見つからない Err(RuntimeError::InvalidOperation { message: format!("Method '{}' not found in {}", method, instance.class_name), @@ -710,16 +731,16 @@ impl NyashInterpreter { // 2. 現在のクラスのデリゲーション関係を検証 let current_class = ¤t_instance.class_name; - let box_declarations = self.shared.box_declarations.read().unwrap(); - - let current_box_decl = box_declarations.get(current_class) - .ok_or(RuntimeError::UndefinedClass { - name: current_class.clone() - })?; - + // ここでは短期ロックで必要な情報だけ抜き出してすぐ解放する + let (has_parent_in_ext, has_parent_in_impl) = { + let box_declarations = self.shared.box_declarations.read().unwrap(); + let current_box_decl = box_declarations.get(current_class) + .ok_or(RuntimeError::UndefinedClass { name: current_class.clone() })?; + (current_box_decl.extends.contains(&parent.to_string()), + current_box_decl.implements.contains(&parent.to_string())) + }; // extendsまたはimplementsでparentが指定されているか確認 (Multi-delegation) 🚀 - let is_valid_delegation = current_box_decl.extends.contains(&parent.to_string()) || - current_box_decl.implements.contains(&parent.to_string()); + let is_valid_delegation = has_parent_in_ext || has_parent_in_impl; if !is_valid_delegation { return Err(RuntimeError::InvalidOperation { @@ -730,34 +751,53 @@ impl NyashInterpreter { // 🔥 Phase 8.8: pack透明化システム - ビルトインBox判定 use crate::box_trait::is_builtin_box; - - let mut is_builtin = is_builtin_box(parent); - - // GUI機能が有効な場合はEguiBoxも追加判定 + // GUI機能が有効な場合はEguiBoxも追加判定(mut不要の形に) #[cfg(all(feature = "gui", not(target_arch = "wasm32")))] - { - if parent == "EguiBox" { - is_builtin = true; - } - } + let is_builtin = is_builtin_box(parent) || parent == "EguiBox"; + #[cfg(not(all(feature = "gui", not(target_arch = "wasm32"))))] + let is_builtin = is_builtin_box(parent); // 🔥 Phase 8.9: Transparency system removed - all delegation must be explicit // Removed: if is_builtin && method == parent { ... execute_builtin_constructor_call ... } if is_builtin { - // ビルトインBoxの場合、ロックを解放してからメソッド呼び出し - drop(box_declarations); + // ビルトインBoxの場合、直接ビルトインメソッドを実行 return self.execute_builtin_box_method(parent, method, current_instance_val.clone_box(), arguments); } + // プラグイン親(__plugin_content) + #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] + { + // 親がユーザー定義に見つからない場合は、プラグインとして試行 + // 現在のインスタンスから __plugin_content を参照 + if let Some(plugin_shared) = current_instance.get_field_legacy("__plugin_content") { + // 引数を評価(ロックは既に解放済みの設計) + let plugin_ref = &*plugin_shared; + if let Some(plugin) = plugin_ref.as_any().downcast_ref::() { + let mut arg_values: Vec> = Vec::new(); + for arg in arguments { + arg_values.push(self.execute_expression(arg)?); + } + let loader = crate::runtime::get_global_loader_v2(); + let loader = loader.read().unwrap(); + match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id, &arg_values) { + Ok(Some(result_box)) => return Ok(result_box), + Ok(None) => return Ok(Box::new(VoidBox::new())), + Err(_) => {} + } + } + } + } + // 3. 親クラスのBox宣言を取得(ユーザー定義Boxの場合) - let parent_box_decl = box_declarations.get(parent) + let parent_box_decl = { + let box_declarations = self.shared.box_declarations.read().unwrap(); + box_declarations.get(parent) .ok_or(RuntimeError::UndefinedClass { name: parent.to_string() })? - .clone(); - - drop(box_declarations); // ロック早期解放 + .clone() + }; // 4. constructorまたはinitまたはpackまたはbirthの場合の特別処理 if method == "constructor" || method == "init" || method == "pack" || method == "birth" || method == parent { diff --git a/src/interpreter/objects.rs b/src/interpreter/objects.rs index fc256b46..8f7588ef 100644 --- a/src/interpreter/objects.rs +++ b/src/interpreter/objects.rs @@ -817,7 +817,27 @@ impl NyashInterpreter { // 🌍 革命的実装:Environment tracking廃止 // Create Arc outside if block so it's available in all scopes - let instance_arc = Arc::from(instance_box); + let instance_arc: Arc = Arc::from(instance_box); + + // プラグイン親(extendsに含まれる場合)の生成と保持(__plugin_content) + #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] + { + use crate::runtime::get_global_loader_v2; + use crate::box_trait::SharedNyashBox; + let loader = get_global_loader_v2(); + let loader = loader.read().unwrap(); + if !final_box_decl.extends.is_empty() { + for parent in &final_box_decl.extends { + if let Ok(plugin_box) = loader.create_box(parent, &[]) { + if let Some(inst) = (&*instance_arc).as_any().downcast_ref::() { + let shared: SharedNyashBox = Arc::from(plugin_box); + let _ = inst.set_field_legacy("__plugin_content", shared); + } + break; + } + } + } + } // コンストラクタを呼び出す // 🌟 birth()統一システム: "birth/引数数"のみを許可(Box名コンストラクタ無効化) diff --git a/src/mir/builder.rs b/src/mir/builder.rs index ad69631f..e3d3b0da 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -35,6 +35,10 @@ pub struct MirBuilder { /// Pending phi functions to be inserted #[allow(dead_code)] pub(super) pending_phis: Vec<(BasicBlockId, ValueId, String)>, + + /// Origin tracking for simple optimizations (e.g., object.method after new) + /// Maps a ValueId to the class name if it was produced by NewBox of that class + pub(super) value_origin_newbox: HashMap, } impl MirBuilder { @@ -48,6 +52,7 @@ impl MirBuilder { block_gen: BasicBlockIdGenerator::new(), variable_map: HashMap::new(), pending_phis: Vec::new(), + value_origin_newbox: HashMap::new(), } } @@ -796,10 +801,13 @@ impl MirBuilder { // VM will handle optimization for basic types internally self.emit_instruction(MirInstruction::NewBox { dst, - box_type: class, + box_type: class.clone(), args: arg_values.clone(), })?; + // Record origin for optimization: dst was created by NewBox of class + self.value_origin_newbox.insert(dst, class); + // Immediately call birth(...) on the created instance to run constructor semantics. // birth typically returns void; we don't capture the result here (dst: None) self.emit_instruction(MirInstruction::BoxCall { @@ -1001,15 +1009,32 @@ impl MirBuilder { })?; Ok(result_id) } else { - // Fallback: Emit a BoxCall instruction for regular method calls - self.emit_instruction(MirInstruction::BoxCall { - dst: Some(result_id), - box_val: object_value, - method, - args: arg_values, - effects: EffectMask::READ.add(Effect::ReadHeap), // Method calls may have side effects - })?; - Ok(result_id) + // If the object originates from a NewBox in this function, we can lower to Call as well + if let Some(class_name) = self.value_origin_newbox.get(&object_value).cloned() { + let func_name = format!("{}.{}{}", class_name, method, format!("/{}", arg_values.len())); + let func_val = self.value_gen.next(); + self.emit_instruction(MirInstruction::Const { dst: func_val, value: ConstValue::String(func_name) })?; + let mut call_args = Vec::with_capacity(arg_values.len() + 1); + call_args.push(object_value); + call_args.extend(arg_values); + self.emit_instruction(MirInstruction::Call { + dst: Some(result_id), + func: func_val, + args: call_args, + effects: EffectMask::READ.add(Effect::ReadHeap), + })?; + Ok(result_id) + } else { + // Fallback: Emit a BoxCall instruction for regular method calls + self.emit_instruction(MirInstruction::BoxCall { + dst: Some(result_id), + box_val: object_value, + method, + args: arg_values, + effects: EffectMask::READ.add(Effect::ReadHeap), // Method calls may have side effects + })?; + Ok(result_id) + } } } diff --git a/src/runtime/plugin_loader_v2.rs b/src/runtime/plugin_loader_v2.rs index ad461d29..34c3551a 100644 --- a/src/runtime/plugin_loader_v2.rs +++ b/src/runtime/plugin_loader_v2.rs @@ -11,7 +11,7 @@ mod enabled { use crate::config::nyash_toml_v2::{NyashConfigV2, LibraryDefinition}; use std::collections::HashMap; use std::sync::{Arc, RwLock}; - use std::ffi::c_void; + // use std::ffi::c_void; // unused use std::any::Any; use once_cell::sync::Lazy; diff --git a/tools/nekocode-rust b/tools/nekocode-rust deleted file mode 160000 index 73e0d263..00000000 --- a/tools/nekocode-rust +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 73e0d263bb512b22ab062986cf0239401fab155f