diff --git a/docs/development/current/CURRENT_TASK.md b/docs/development/current/CURRENT_TASK.md index 0fe744da..230e6f41 100644 --- a/docs/development/current/CURRENT_TASK.md +++ b/docs/development/current/CURRENT_TASK.md @@ -15,6 +15,178 @@ Phase 10.10 は完了(DoD確認済)。**重大な発見**:プラグイン - サンプル: array/map デモを追加し VM 実行で正常動作確認 - 次: 10.2(Craneliftの実呼び出し)に着手 +### 10.2 追加アップデート(2025-08-29 PM) +- ✅ static box 内メソッドのMIR関数化に成功 + - 例: `Main.helper/1`, `Main.add/2` が独立関数として生成(MIR dumpで確認) + - VM実行でも JIT マネージャの sites に現れる(`sites=2`) +- ✅ JITコンパイル成功 + - `Main.helper/1` が JIT コンパイルされ handle 付与(handle=1) + - 単純算術(`Main.add/2` 等)は JIT 実行 `exec_ok=1` を確認 +- ✅ ArrayBox.length() の値は正しく返る(JIT無効時に Result: 3) + +- ❌ 残課題(ブロッカー) + 1) プラグイン呼び出しの JIT 実行時に Segfault 発生 + - 事象: `arr.length()` のようなプラグインメソッドで JIT 実行時にクラッシュ + - 状態: JITコンパイル自体は成功するが実行で落ちるため、DebugBox の i64 シムイベント取得に未到達 + 2) i64 シムトレース未取得 + - Segfault解消後に `DebugBox.tracePluginCalls(true)` → `getJitEvents()` で i64.start/end, tlv 等を観測予定 + +- ▶ 次の具体ステップ(提案) + - [ ] Lowerer: `ArrayBox.length()` を hostcall 経路(ANY_LEN_H)から plugin_invoke 経路へ切替 + - 目的: i64 シム(`nyash_plugin_invoke3_i64`)を真正面から踏ませ、シムの前後でのcanary・TLVを観測 + - [ ] Segfault 再現最小ケースの確立と原因究明 + - 候補: 受け手 a0(param index)/ argc / TLV ヘッダの組み立て、戻りTLVのdecode、ハンドル走査の境界 + - [ ] DebugBox での i64 シムイベントログ取得(start/end, tlv, rc/out_len/canary) + - [ ] 必要に応じて f64 シム (`NYASH_JIT_PLUGIN_F64="type:method"`) の点検 + +--- + +## 2025-08-29 PM3 再起動スナップショット(Strict前倒し版) + +### 現在の着地(Strict準備済み) +- InvokePolicy/Observe を導入し、Lowerer の分岐をスリム化 + - ArrayBox: length/get/push/set → policy+observe 経由(plugin/hostcallの一元化) + - MapBox: size/get/has/set → 同上 + - StringBox: length/is_empty/charCodeAt → 同上 +- VM→Plugin 引数整合の安定化 + - 整数は I64 (tag=3) に統一/Plugin IntegerBox は自動プリミティブ化(get) +- 予約型の安全な上書き制御 + - `NYASH_PLUGIN_OVERRIDE_TYPES=ArrayBox,MapBox`(デフォルト同値)で型別に制御 +- StringBoxのpost-birth初期化 + - `new StringBox()` 直後の `length()` でsegfaultしないよう、空文字で初期化 +- 特殊コメント(最小) + - `// @env KEY=VALUE`, `// @jit-debug`, `// @plugin-builtins`, `// @jit-strict` + +### Strict モード(Fail-Fast / ノーフォールバック) +- 目的: 「VM=仕様 / JIT=高速実装」。JITで動かない=JITのバグを即可視化 +- 有効化: `// @jit-strict`(または `NYASH_JIT_STRICT=1`) +- 仕様(現状) + - Lowerer/Engine: unsupported>0 がある関数はコンパイル中止(fail-fast) + - 実行: `NYASH_JIT_ONLY=1` と併用でフォールバック禁止(エラー) + - シム: 受け手解決は HandleRegistry 優先(`NYASH_JIT_ARGS_HANDLE_ONLY=1` 自動ON) + +### 再起動チェックリスト +- Build(Cranelift有効): `cargo build --release -j32 --features cranelift-jit` +- Array(param受け): `examples/jit_plugin_invoke_param_array.nyash` → Result: 3 +- Map(E2E): `examples/jit_map_policy_demo.nyash` → Result: 2 +- String(RO): `examples/jit_string_length_policy_demo.nyash` → Result: 0(空文字) +- Strict 観測(fail-fast動作確認): + - ファイル先頭: `// @jit-strict` `// @jit-debug` `// @plugin-builtins` + - 実行: `NYASH_JIT_ONLY=1 ./target/release/nyash --backend vm ` + - 期待: 未対応lowerがあれば compile失敗→JIT-onlyでエラー(フォールバックなし) + +### 観測の標準手順(compile/runtime/シム) +```bash +cargo build --release --features cranelift-jit + +# Array(param受け、JIT観測一式) +NYASH_USE_PLUGIN_BUILTINS=1 \ +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 \ +NYASH_JIT_EVENTS=1 NYASH_JIT_EVENTS_COMPILE=1 NYASH_JIT_EVENTS_RUNTIME=1 \ +NYASH_JIT_EVENTS_PATH=jit_events.jsonl NYASH_JIT_SHIM_TRACE=1 \ + ./target/release/nyash --backend vm examples/jit_plugin_invoke_param_array.nyash + +# Map(policy/observe経由の確認) +NYASH_USE_PLUGIN_BUILTINS=1 NYASH_PLUGIN_OVERRIDE_TYPES=ArrayBox,MapBox \ +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 \ +NYASH_JIT_EVENTS=1 NYASH_JIT_EVENTS_COMPILE=1 NYASH_JIT_EVENTS_RUNTIME=1 \ +NYASH_JIT_EVENTS_PATH=jit_events.jsonl \ + ./target/release/nyash --backend vm examples/jit_map_policy_demo.nyash + +# String(length RO) +NYASH_USE_PLUGIN_BUILTINS=1 NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 \ +NYASH_JIT_EVENTS=1 NYASH_JIT_EVENTS_COMPILE=1 NYASH_JIT_EVENTS_RUNTIME=1 \ +NYASH_JIT_EVENTS_PATH=jit_events.jsonl \ + ./target/release/nyash --backend vm examples/jit_string_length_policy_demo.nyash + +# Strictモード(フォールバック禁止)最小観測 +NYASH_USE_PLUGIN_BUILTINS=1 NYASH_JIT_EXEC=1 NYASH_JIT_ONLY=1 NYASH_JIT_STRICT=1 \ +NYASH_JIT_EVENTS=1 NYASH_JIT_EVENTS_RUNTIME=1 NYASH_JIT_EVENTS_COMPILE=1 \ +NYASH_JIT_EVENTS_PATH=jit_events.jsonl \ + ./target/release/nyash --backend vm examples/jit_plugin_invoke_param_array.nyash +``` + +### これからの実装(優先順) +1) 算術/比較 emit の穴埋め(Strictで落ちる箇所を優先) +2) String RO の必要最小を policy に追加(過剰に増やさない) +3) 追加サンプルは最小限(回帰用の小粒のみ) +4) 必要に応じて Strict 診断のJSONイベントを最小追加(compile-fail時) + +### 現在の達成状況(✅) +- ✅ static box メソッドのMIR関数化に成功 + - 例: `Main.helper/1`, `Main.add/2` が独立関数として生成され、JITの sites に出現 +- ✅ JITコンパイル成功/実行成功 + - `Main.helper/1` に handle が付与(handle=1)、`compiled=1`、`exec_ok=1` +- ✅ compile-phase イベント出力 + - `plugin:ArrayBox:push` / `plugin:ArrayBox:length`(型ヒントにより StringBox へ寄るケースも増加見込み) +- ✅ length() の正値化 + - `arr.length()` が 3 を返す(受け手解決の安全化・フォールバック整備済み) + +### 既知の課題(❌) +- ❌ runtime-phase イベントが出ない環境がある + - 対処: `NYASH_JIT_EVENTS=1` を併用(ベース出力ON)、必要に応じて `NYASH_JIT_EVENTS_PATH=jit_events.jsonl` + - 純JIT固定: `NYASH_JIT_ONLY=1` を付与してフォールバック経路を抑止 +- ❌ シムトレース([JIT-SHIM i64])が出ない環境がある + - 対処: 上記と同時に `NYASH_JIT_SHIM_TRACE=1` を指定。plugin_invoke シム経路を確実に踏むため length は plugin 優先 + +### 直近で入れた変更(要点) +- 「型ヒント伝搬」パスを追加(箱化) + - 追加: `src/mir/passes/type_hints.rs`、呼び出し元→callee の param 型反映(String/Integer/Bool/Float) + - 反映: `optimizer.rs` から呼び出し、責務を分割 +- length() の plugin_invoke 優先 + - BoxCall簡易hostcall(simple_reads)から length を除外、Lowerer の plugin_invoke 経路に誘導 +- シムの受け手解釈を「ハンドル優先」に変更 + - `nyash_plugin_invoke3_{i64,f64}` で a0 を HandleRegistry から解決→PluginBoxV2/ネイティブ(Array/String) + - レガシー互換のparam indexも残し、安全フォールバック +- runtime観測の強化 + - シム入り口で runtime JSON を出力(kind:"plugin", id:"plugin_invoke.i64/f64"、type_id/method_id/inst/argc) + - ANY長さ(`nyash_any_length_h`)にparam indexフォールバックを追加 + +### 観測の標準手順(必ずこれで確認) +```bash +cargo build --release --features cranelift-jit + +# 標準出力に compile/runtime/シムトレースを出す(純JIT固定) +NYASH_USE_PLUGIN_BUILTINS=1 \ +NYASH_JIT_EXEC=1 NYASH_JIT_ONLY=1 NYASH_JIT_THRESHOLD=1 \ +NYASH_JIT_EVENTS=1 NYASH_JIT_EVENTS_COMPILE=1 NYASH_JIT_EVENTS_RUNTIME=1 \ +NYASH_JIT_SHIM_TRACE=1 \ + ./target/release/nyash --backend vm examples/jit_plugin_invoke_param_array.nyash + +# もしくはruntime JSONをファイルに +NYASH_JIT_EVENTS_RUNTIME=1 NYASH_JIT_EVENTS_PATH=jit_events.jsonl \ + ./target/release/nyash --backend vm examples/jit_plugin_invoke_param_array.nyash +cat jit_events.jsonl +``` + +### 設計ルールの曖昧さと「箱」整理(次の箱) +- TypeHintPass(完了): `src/mir/passes/type_hints.rs` — 型伝搬をここに集約(最小実装済) +- InvokePolicyPass(新規): `src/jit/policy/invoke.rs` — plugin/hostcall/ANY の経路選択を一元化(Lowerer から分離) +- Observe(新規): `src/jit/observe.rs` — compile/runtime/trace 出力の統一(ガード/出力先/JSONスキーマ) + +### 今後のToDo(優先度順) +1) InvokePolicyPass の導入 + - 目的: Lowerer 内の分岐を薄くし、経路選択を一箇所に固定(read-only/allowlist/ANY fallbackを明確化) + - DoD: length()/push/get/set の経路が policy 設定で一意に決まる(compile/runtimeのイベント差異が「設定」由来で説明可能) +2) Observe の導入 + - 目的: runtime/trace の出力有無を一箇所で制御、`NYASH_JIT_EVENTS(_COMPILE/_RUNTIME)` の挙動を統一 + - DoD: `NYASH_JIT_EVENTS=1` で compile/runtime が必ず出る。PATH 指定時はJSONLに確実追記 +3) String/Array の誤ラベル最終解消 + - 型不明時は `Any.length` としてcompile-phaseに出す/または plugin_invoke の type_id を runtime で必ず記録 +4) f64戻り/ハンドル返却(tag=8)の仕上げ + - `NYASH_JIT_PLUGIN_F64` なしの自動選択、handle返却シムの導入(tag=8) + +### 受け入れ条件(DoD) +- compile-phase: `plugin:*` のイベントが関数ごとに安定 +- runtime-phase: `plugin_invoke.*` が必ず出力(stdout または JSONL) +- シムトレース: `NYASH_JIT_SHIM_TRACE=1` で [JIT-SHIM …] が可視 +- length(): `arr=ArrayBox([…])`→3、`s=StringBox("Hello")`→5(どちらもJIT実行時に正値) + +### 備考(TIPS) +- ConsoleBox.log はVMでは標準出力に流れません。観測は `print(...)` か runtime JSON を利用してください。 +- runtime JSON が見えない場合は `NYASH_JIT_EVENTS=1` を必ず併用(ベース出力ON)。 + + ## 現在地(Done / Doing / Next) - ✅ Done(Phase 10.10) - GC Switchable Runtime(GcConfigBox)/ Unified Debug(DebugConfigBox) @@ -56,7 +228,7 @@ Phase 10.10 は完了(DoD確認済)。**重大な発見**:プラグイン - JIT→Plugin呼び出しパス確立 - パフォーマンス測定と最適化 -## Phase 10.5(旧10.1):Python統合 +## Phase 10.5(旧10.1):Python統合 / JIT Strict 前倒し - 参照: `docs/development/roadmap/phases/phase-10.5/` (移動済み) - ChatGPT5の当初計画を後段フェーズへ @@ -89,10 +261,15 @@ Phase 10.10 は完了(DoD確認済)。**重大な発見**:プラグイン - 例・テスト・CIをプラグイン経路に寄せ、十分に安定してから段階的に外す。 ### 次アクション(小さく通す) -1) DebugBox(Phase 1): JITシムイベント取得(getJitEvents)/プラグイン呼び出し履歴トレース(tracePluginCalls) -2) JIT typedシムの拡張: f64の自動選択(Lowerer型ヒント)→ Handle/String返却シムの導入(ハンドルID/文字列返却) -3) AOTスモーク: Python数値戻りの .o 生成をもう1本追加(f64/Bool/i64いずれか) -4) ガイド: R系APIとNYASH_PY_AUTODECODEの使い分け、JITシムトレースの使い方を追記 +1) JIT Strict モード(最優先) + - // @jit-strict(ENV: NYASH_JIT_STRICT=1)で有効化 + - Lowerer: unsupported>0 でコンパイル中止(診断を返す) + - 実行: JIT_ONLYと併用でフォールバック禁止(fail-fast) + - シム: 受け手解決は HandleRegistry 優先(param-index 経路は無効化) +2) Array/Map のパリティ検証(strict) + - examples/jit_plugin_invoke_param_array.nyash / examples/jit_map_policy_demo.nyash で compile/runtime/シム整合を確認 +3) Python統合(RO中心)の継続 + - eval/import/getattr/call の strict 観測と整合、数値/Bool/文字列の戻りデコード ## すぐ試せるコマンド(現状維持の確認) ```bash @@ -115,6 +292,12 @@ NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 \ NYASH_JIT_EVENTS_COMPILE=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS_PATH=events.jsonl \ ./target/release/nyash --backend vm examples/jit_map_get_param_hh.nyash +# Strictモード(フォールバック禁止)最小観測 +NYASH_USE_PLUGIN_BUILTINS=1 NYASH_JIT_EXEC=1 NYASH_JIT_ONLY=1 NYASH_JIT_STRICT=1 \ + NYASH_JIT_EVENTS=1 NYASH_JIT_EVENTS_RUNTIME=1 NYASH_JIT_EVENTS_COMPILE=1 \ + NYASH_JIT_EVENTS_PATH=jit_events.jsonl \ + ./target/release/nyash --backend vm examples/jit_plugin_invoke_param_array.nyash + # Plugin demos(Array/Map) (cd plugins/nyash-array-plugin && cargo build --release) (cd plugins/nyash-map-plugin && cargo build --release) diff --git a/docs/development/roadmap/phases/phase-10.5/README.md b/docs/development/roadmap/phases/phase-10.5/README.md index 956ceeb2..7d5dfa27 100644 --- a/docs/development/roadmap/phases/phase-10.5/README.md +++ b/docs/development/roadmap/phases/phase-10.5/README.md @@ -1,10 +1,22 @@ -# Phase 10.5 – Python ネイティブ統合(Embedding & FFI) +# Phase 10.5 – Python ネイティブ統合(Embedding & FFI)/ JIT Strict 化の前倒し *(旧10.1の一部を後段フェーズに再編。Everything is Plugin/AOTの基盤上で実現)* -NyashとPythonを双方向に“ネイティブ”接続する。第一段はNyash→Python呼び出し(Embedding)、続いてPython→Nyash(Extending)。JIT/AOT/Pluginの統一面を活かし、最小のC ABIで着地する。 +NyashとPythonを双方向に“ネイティブ”接続する前に、JITの開発・検証効率を最大化するため、VM=仕様/JIT=高速実装 という原則に沿った「JIT Strict モード」を前倒し導入し、フォールバック起因の複雑性を排除する。 ## 📂 サブフェーズ構成(10.5a → 10.5e) +先行タスク(最優先) +- 10.5s JIT Strict モード導入(Fail-Fast / ノーフォールバック) + - 目的: 「VMで動く=正。JITで動かない=JITのバグ」を可視化、開発ループを短縮 + - 仕様: + - // @jit-strict または NYASH_JIT_STRICT=1 で有効化 + - Lowerer: unsupported>0 の場合はコンパイルを中止(診断を返す) + - 実行: JIT_ONLY と併用時はフォールバック禁止(失敗は明示エラー) + - シム: 受け手解決は HandleRegistry 優先。param-index 互換経路は無効化 + - DoD: + - Array/Map の代表ケースで Strict 実行時に compile/runtime/シムイベントの整合が取れる + - VM=JIT の差が発生したときに即座に落ち、原因特定がしやすい(フォールバックに逃げない) + ### 10.5a 設計・ABI整合(1–2日) - ルート選択: - Embedding: NyashプロセスにCPythonを埋め込み、PyObject*をハンドル管理 @@ -39,6 +51,7 @@ NyashとPythonを双方向に“ネイティブ”接続する。第一段はNya - NyashからPythonコードを評価し、PyObjectをHandleで往復できる - 代表的なプロパティ取得/呼び出し(RO)がJIT/VMで動作 - AOTリンク後のEXEで `py.eval()` 代表例が起動できる(動的ロード前提) + - 10.5s Strict: VM=仕様/JIT=高速実装の原則に基づき、フォールバック無しで fail-fast が機能 ## ⌛ 目安 | サブフェーズ | 目安 | diff --git a/examples/jit_map_policy_demo.nyash b/examples/jit_map_policy_demo.nyash new file mode 100644 index 00000000..970283a3 --- /dev/null +++ b/examples/jit_map_policy_demo.nyash @@ -0,0 +1,19 @@ +// @jit-debug +// @plugin-builtins + +// MapBox end-to-end via InvokePolicy/Observe +// - Exercises: size/get/has/set on plugin-builtins path + +static box Main { + main() { + local m, ok, v, sz + m = new MapBox() + m.set(1, 100) + m.set(2, 200) + ok = m.has(1) + v = m.get(1) + sz = m.size() + return sz // expect: 2 + } +} + diff --git a/examples/jit_plugin_invoke_box_helper.nyash b/examples/jit_plugin_invoke_box_helper.nyash new file mode 100644 index 00000000..7402f17f --- /dev/null +++ b/examples/jit_plugin_invoke_box_helper.nyash @@ -0,0 +1,26 @@ +// Box Helper JIT: method lowered as MIR function (Helper.helper/1) +// Build plugin: +// (cd plugins/nyash-array-plugin && cargo build --release) +// Run: +// NYASH_USE_PLUGIN_BUILTINS=1 NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 \ +// NYASH_JIT_SHIM_TRACE=1 NYASH_CLI_VERBOSE=1 \ +// ./target/release/nyash --backend vm examples/jit_plugin_invoke_box_helper.nyash + +box Helper { + helper(arr) { + return arr.length() + } +} + +debug = new DebugBox() +debug.tracePluginCalls(true) + +a = new ArrayBox() +a.push(1) +a.push(2) +a.push(3) + +h = new Helper() +print(h.helper(a)) +print(debug.getJitEvents()) + diff --git a/examples/jit_plugin_invoke_global_helper.nyash b/examples/jit_plugin_invoke_global_helper.nyash new file mode 100644 index 00000000..adc22ad6 --- /dev/null +++ b/examples/jit_plugin_invoke_global_helper.nyash @@ -0,0 +1,29 @@ +// Global helper JIT smoke: avoid static box method resolution +// Build plugin: +// (cd plugins/nyash-array-plugin && cargo build --release) +// Run: +// NYASH_USE_PLUGIN_BUILTINS=1 NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 \ +// NYASH_JIT_SHIM_TRACE=1 NYASH_CLI_VERBOSE=1 \ +// ./target/release/nyash --backend vm examples/jit_plugin_invoke_global_helper.nyash + +helper(arr) { + // JIT target: plugin_invoke on parameter only + return arr.length() +} + +main() { + local a, debug + debug = new DebugBox() + debug.tracePluginCalls(true) + + a = new ArrayBox() + a.push(1) + a.push(2) + a.push(3) + + // Call global helper (should become a distinct MIR function) + print(helper(a)) + // Dump recent JIT shim events + print(debug.getJitEvents()) +} + diff --git a/examples/jit_plugin_invoke_static_helper.nyash b/examples/jit_plugin_invoke_static_helper.nyash new file mode 100644 index 00000000..7398109e --- /dev/null +++ b/examples/jit_plugin_invoke_static_helper.nyash @@ -0,0 +1,25 @@ +// Static box Helper with static method to avoid instance birth +// Build plugin: +// (cd plugins/nyash-array-plugin && cargo build --release) +// Run: +// NYASH_USE_PLUGIN_BUILTINS=1 NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 \ +// NYASH_JIT_SHIM_TRACE=1 NYASH_CLI_VERBOSE=1 \ +// ./target/release/nyash --backend vm examples/jit_plugin_invoke_static_helper.nyash + +static box Helper { + static helper(arr) { + return arr.length() + } +} + +debug = new DebugBox() +debug.tracePluginCalls(true) + +a = new ArrayBox() +a.push(1) +a.push(2) +a.push(3) + +print(Helper.helper(a)) +print(debug.getJitEvents()) + diff --git a/examples/jit_shim_trace_param_array.nyash b/examples/jit_shim_trace_param_array.nyash new file mode 100644 index 00000000..2c9f02a8 --- /dev/null +++ b/examples/jit_shim_trace_param_array.nyash @@ -0,0 +1,33 @@ +// JIT shim trace smoke: helper JIT invokes plugin on param +// Build plugins first: +// (cd plugins/nyash-array-plugin && cargo build --release) +// Run: +// NYASH_USE_PLUGIN_BUILTINS=1 NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 \ +// NYASH_JIT_SHIM_TRACE=1 NYASH_CLI_VERBOSE=1 \ +// ./target/release/nyash --backend vm examples/jit_shim_trace_param_array.nyash + +static box Main { + main() { + local a, debug, len + me.console = new ConsoleBox() + debug = new DebugBox() + debug.tracePluginCalls(true) + + // Prepare Array in VM path (NewBox on Array is JIT-unsupported) + a = new ArrayBox() + a.push(1) + a.push(2) + a.push(3) + + // Call helper (should be JIT-able): only arr.length() on param + len = me.helper(a) + me.console.log("len=", len) + + // Dump recent JIT shim events (expect i64.start/end etc.) + me.console.log(debug.getJitEvents()) + } + + helper(arr) { + return arr.length() + } +} diff --git a/examples/jit_string_length_policy_demo.nyash b/examples/jit_string_length_policy_demo.nyash new file mode 100644 index 00000000..af3bd8c0 --- /dev/null +++ b/examples/jit_string_length_policy_demo.nyash @@ -0,0 +1,13 @@ +// @jit-debug +// @plugin-builtins + +static box Main { + main() { + local s + s = new StringBox("") // empty string + // In a real case we'd set content; plugin StringBox.length() is RO path demo + // For observable behavior, length() on empty should be 0 + return s.length() + } +} + diff --git a/src/box_factory/mod.rs b/src/box_factory/mod.rs index 9e2a9b22..586c70e9 100644 --- a/src/box_factory/mod.rs +++ b/src/box_factory/mod.rs @@ -115,6 +115,25 @@ impl UnifiedBoxRegistry { name: &str, args: &[Box], ) -> Result, RuntimeError> { + // Prefer plugin-builtins when enabled and provider is available in v2 registry + if std::env::var("NYASH_USE_PLUGIN_BUILTINS").ok().as_deref() == Some("1") { + use crate::runtime::{get_global_registry, BoxProvider}; + // Allowlist types for override: env NYASH_PLUGIN_OVERRIDE_TYPES="ArrayBox,MapBox" (default: ArrayBox,MapBox) + let allow: Vec = if let Ok(list) = std::env::var("NYASH_PLUGIN_OVERRIDE_TYPES") { + list.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect() + } else { + vec!["ArrayBox".into(), "MapBox".into()] + }; + if allow.iter().any(|t| t == name) { + let v2 = get_global_registry(); + if let Some(provider) = v2.get_provider(name) { + if let BoxProvider::Plugin(_lib) = provider { + return v2.create_box(name, args) + .map_err(|e| RuntimeError::InvalidOperation { message: format!("Plugin Box creation failed: {}", e) }); + } + } + } + } // Check cache first let cache = self.type_cache.read().unwrap(); if let Some(&factory_index) = cache.get(name) { diff --git a/src/jit/engine.rs b/src/jit/engine.rs index 57b3d302..426c3c2d 100644 --- a/src/jit/engine.rs +++ b/src/jit/engine.rs @@ -59,6 +59,11 @@ impl JitEngine { self.last_phi_total = phi_t; self.last_phi_b1 = phi_b1; self.last_ret_bool_hint = ret_b; // Record per-function stats into manager via callback if available (handled by caller) let cfg_now = crate::jit::config::current(); + // Strict mode: any unsupported lowering must fail-fast + if lower.unsupported > 0 && std::env::var("NYASH_JIT_STRICT").ok().as_deref() == Some("1") { + eprintln!("[JIT][strict] unsupported lowering ops for {}: {} — failing compile", func_name, lower.unsupported); + return None; + } if cfg_now.dump { let phi_min = cfg_now.phi_min; let native_f64 = cfg_now.native_f64; diff --git a/src/jit/lower/builder.rs b/src/jit/lower/builder.rs index 635a17fe..b29d0f00 100644 --- a/src/jit/lower/builder.rs +++ b/src/jit/lower/builder.rs @@ -143,21 +143,69 @@ extern "C" fn nyash_host_stub0() -> i64 { 0 } #[cfg(feature = "cranelift-jit")] extern "C" fn nyash_plugin_invoke3_i64(type_id: i64, method_id: i64, argc: i64, a0: i64, a1: i64, a2: i64) -> i64 { use crate::runtime::plugin_loader_v2::PluginBoxV2; - let trace = crate::jit::shim_trace::is_enabled(); + let trace = crate::jit::observe::trace_enabled(); + // Emit early shim-enter event for observability regardless of path taken + crate::jit::events::emit_runtime( + serde_json::json!({ + "id": "shim.enter.i64", "type_id": type_id, "method_id": method_id, "argc": argc + }), + "shim", "" + ); // Resolve receiver instance from legacy VM args (param index) let mut instance_id: u32 = 0; let mut invoke: Optioni32> = None; - if a0 >= 0 { + // Try handle registry first: a0 may be a handle (preferred) + if a0 > 0 { + if let Some(obj) = crate::jit::rt::handles::get(a0 as u64) { + if let Some(p) = obj.as_any().downcast_ref::() { + instance_id = p.instance_id(); + invoke = Some(p.inner.invoke_fn); + } else { + // Builtin/native object fallback for common methods + if method_id as u32 == 1 { + // length + if let Some(arr) = obj.as_any().downcast_ref::() { + if let Some(ib) = arr.length().as_any().downcast_ref::() { return ib.value; } + } + if let Some(sb) = obj.as_any().downcast_ref::() { + return sb.value.len() as i64; + } + } + } + } + } + // Also capture a direct pointer to native objects via legacy VM args index (compat) + let mut native_array_len: Option = None; + if a0 >= 0 && std::env::var("NYASH_JIT_ARGS_HANDLE_ONLY").ok().as_deref() != Some("1") { crate::jit::rt::with_legacy_vm_args(|args| { let idx = a0 as usize; if let Some(crate::backend::vm::VMValue::BoxRef(b)) = args.get(idx) { if let Some(p) = b.as_any().downcast_ref::() { instance_id = p.instance_id(); invoke = Some(p.inner.invoke_fn); + } else if let Some(arr) = b.as_any().downcast_ref::() { + // Fallback length for ArrayBox when not plugin-backed + if method_id as u32 == 1 { // length + if let Some(ib) = arr.length().as_any().downcast_ref::() { + native_array_len = Some(ib.value); + } + } } } }); } + if invoke.is_none() { + if let Some(v) = native_array_len { + if trace { eprintln!("[JIT-SHIM i64] native_fallback return {}", v); } + crate::jit::events::emit_runtime( + serde_json::json!({ + "id": "shim.native.i64", "type_id": type_id, "method_id": method_id, "argc": argc, "ret": v + }), + "shim", "" + ); + return v; + } + } // If not resolved, scan all VM args for a matching PluginBoxV2 by type_id if invoke.is_none() { crate::jit::rt::with_legacy_vm_args(|args| { @@ -190,18 +238,19 @@ extern "C" fn nyash_plugin_invoke3_i64(type_id: i64, method_id: i64, argc: i64, let mut out_len: usize = 4096; let out_ptr = unsafe { out.as_mut_ptr().add(canary_len) }; if trace { eprintln!("[JIT-SHIM i64] invoke type={} method={} argc={} inst_id={} a1={} a2={} buf_len={}", type_id, method_id, argc, instance_id, a1, a2, buf.len()); } - crate::jit::shim_trace::push(format!("i64.start type={} method={} argc={} inst={} a1={} a2={}", type_id, method_id, argc, instance_id, a1, a2)); + crate::jit::observe::runtime_plugin_shim_i64(type_id, method_id, argc, instance_id); + crate::jit::observe::trace_push(format!("i64.start type={} method={} argc={} inst={} a1={} a2={}", type_id, method_id, argc, instance_id, a1, a2)); let rc = unsafe { invoke.unwrap()(type_id as u32, method_id as u32, instance_id, buf.as_ptr(), buf.len(), out_ptr, &mut out_len) }; // Canary check let pre_ok = out[..canary_len].iter().all(|&b| b==canary_val); let post_ok = out[canary_len + out_len .. canary_len + out_len + canary_len].iter().all(|&b| b==canary_val); if trace { eprintln!("[JIT-SHIM i64] rc={} out_len={} canary_pre={} canary_post={}", rc, out_len, pre_ok, post_ok); } - crate::jit::shim_trace::push(format!("i64.end rc={} out_len={} pre_ok={} post_ok={}", rc, out_len, pre_ok, post_ok)); + crate::jit::observe::trace_push(format!("i64.end rc={} out_len={} pre_ok={} post_ok={}", rc, out_len, pre_ok, post_ok)); if rc != 0 { return 0; } let out_slice = unsafe { std::slice::from_raw_parts(out_ptr, out_len) }; if let Some((tag, sz, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(out_slice) { if trace { eprintln!("[JIT-SHIM i64] TLV tag={} sz={}", tag, sz); } - crate::jit::shim_trace::push(format!("i64.tlv tag={} sz={}", tag, sz)); + crate::jit::observe::trace_push(format!("i64.tlv tag={} sz={}", tag, sz)); match tag { 2 => { // I32 if let Some(v) = crate::runtime::plugin_ffi_common::decode::i32(payload) { return v as i64; } @@ -231,21 +280,60 @@ extern "C" fn nyash_plugin_invoke3_i64(type_id: i64, method_id: i64, argc: i64, // F64-typed shim: decodes TLV first entry and returns f64 when possible extern "C" fn nyash_plugin_invoke3_f64(type_id: i64, method_id: i64, argc: i64, a0: i64, a1: i64, a2: i64) -> f64 { use crate::runtime::plugin_loader_v2::PluginBoxV2; - let trace = crate::jit::shim_trace::is_enabled(); + let trace = crate::jit::observe::trace_enabled(); + crate::jit::events::emit_runtime( + serde_json::json!({ + "id": "shim.enter.f64", "type_id": type_id, "method_id": method_id, "argc": argc + }), + "shim", "" + ); // Resolve receiver + invoke_fn from legacy VM args let mut instance_id: u32 = 0; let mut invoke: Optioni32> = None; - if a0 >= 0 { + // Try handle registry first + let mut native_array_len: Option = None; + if a0 > 0 { + if let Some(obj) = crate::jit::rt::handles::get(a0 as u64) { + if let Some(p) = obj.as_any().downcast_ref::() { + instance_id = p.instance_id(); + invoke = Some(p.inner.invoke_fn); + } else if method_id as u32 == 1 { + if let Some(arr) = obj.as_any().downcast_ref::() { + if let Some(ib) = arr.length().as_any().downcast_ref::() { native_array_len = Some(ib.value as f64); } + } + if let Some(sb) = obj.as_any().downcast_ref::() { native_array_len = Some(sb.value.len() as f64); } + } + } + } + if a0 >= 0 && std::env::var("NYASH_JIT_ARGS_HANDLE_ONLY").ok().as_deref() != Some("1") { crate::jit::rt::with_legacy_vm_args(|args| { let idx = a0 as usize; if let Some(crate::backend::vm::VMValue::BoxRef(b)) = args.get(idx) { if let Some(p) = b.as_any().downcast_ref::() { instance_id = p.instance_id(); invoke = Some(p.inner.invoke_fn); + } else if let Some(arr) = b.as_any().downcast_ref::() { + if method_id as u32 == 1 { // length + if let Some(ib) = arr.length().as_any().downcast_ref::() { + native_array_len = Some(ib.value as f64); + } + } } } }); } + if invoke.is_none() { + if let Some(v) = native_array_len { + if trace { eprintln!("[JIT-SHIM f64] native_fallback return {}", v); } + crate::jit::events::emit_runtime( + serde_json::json!({ + "id": "shim.native.f64", "type_id": type_id, "method_id": method_id, "argc": argc, "ret": v + }), + "shim", "" + ); + return v; + } + } if invoke.is_none() { crate::jit::rt::with_legacy_vm_args(|args| { for v in args.iter() { @@ -276,17 +364,28 @@ extern "C" fn nyash_plugin_invoke3_f64(type_id: i64, method_id: i64, argc: i64, let mut out_len: usize = 4096; let out_ptr = unsafe { out.as_mut_ptr().add(canary_len) }; if trace { eprintln!("[JIT-SHIM f64] invoke type={} method={} argc={} inst_id={} a1={} a2={} buf_len={}", type_id, method_id, argc, instance_id, a1, a2, buf.len()); } - crate::jit::shim_trace::push(format!("f64.start type={} method={} argc={} inst={} a1={} a2={}", type_id, method_id, argc, instance_id, a1, a2)); + crate::jit::events::emit_runtime( + serde_json::json!({ + "id": "plugin_invoke.f64", + "type_id": type_id, + "method_id": method_id, + "argc": argc, + "inst": instance_id + }), + "plugin", "" + ); + crate::jit::observe::runtime_plugin_shim_i64(type_id, method_id, argc, instance_id); + crate::jit::observe::trace_push(format!("f64.start type={} method={} argc={} inst={} a1={} a2={}", type_id, method_id, argc, instance_id, a1, a2)); let rc = unsafe { invoke.unwrap()(type_id as u32, method_id as u32, instance_id, buf.as_ptr(), buf.len(), out_ptr, &mut out_len) }; let pre_ok = out[..canary_len].iter().all(|&b| b==canary_val); let post_ok = out[canary_len + out_len .. canary_len + out_len + canary_len].iter().all(|&b| b==canary_val); if trace { eprintln!("[JIT-SHIM f64] rc={} out_len={} canary_pre={} canary_post={}", rc, out_len, pre_ok, post_ok); } - crate::jit::shim_trace::push(format!("f64.end rc={} out_len={} pre_ok={} post_ok={}", rc, out_len, pre_ok, post_ok)); + crate::jit::observe::trace_push(format!("f64.end rc={} out_len={} pre_ok={} post_ok={}", rc, out_len, pre_ok, post_ok)); if rc != 0 { return 0.0; } let out_slice = unsafe { std::slice::from_raw_parts(out_ptr, out_len) }; if let Some((tag, sz, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(out_slice) { if trace { eprintln!("[JIT-SHIM f64] TLV tag={} sz={}", tag, sz); } - crate::jit::shim_trace::push(format!("f64.tlv tag={} sz={}", tag, sz)); + crate::jit::observe::trace_push(format!("f64.tlv tag={} sz={}", tag, sz)); match tag { 5 => { // F64 if sz == 8 { let mut b=[0u8;8]; b.copy_from_slice(payload); return f64::from_le_bytes(b); } @@ -861,20 +960,21 @@ impl IRBuilder for CraneliftBuilder { use cranelift_frontend::FunctionBuilder; use cranelift_module::{Linkage, Module}; + // Use a single FunctionBuilder to construct all IR in this method to avoid dominance issues + let mut fb = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbc); + if let Some(idx) = self.current_block_index { fb.switch_to_block(self.blocks[idx]); } + else if let Some(b) = self.entry_block { fb.switch_to_block(b); } + // Pop argc values (right-to-left): receiver + up to 2 args let mut arg_vals: Vec = Vec::new(); let take_n = argc.min(self.value_stack.len()); for _ in 0..take_n { if let Some(v) = self.value_stack.pop() { arg_vals.push(v); } } arg_vals.reverse(); - // Pad to 3 values (receiver + a1 + a2) - while arg_vals.len() < 3 { arg_vals.push({ - let mut fb = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbc); - if let Some(idx) = self.current_block_index { fb.switch_to_block(self.blocks[idx]); } - else if let Some(b) = self.entry_block { fb.switch_to_block(b); } + // Pad to 3 values (receiver + a1 + a2) using the same builder + while arg_vals.len() < 3 { let z = fb.ins().iconst(types::I64, 0); - fb.finalize(); - z - }); } + arg_vals.push(z); + } // Choose f64 shim if allowlisted let use_f64 = if has_ret { @@ -895,11 +995,8 @@ impl IRBuilder for CraneliftBuilder { let func_id = self.module .declare_function(symbol, Linkage::Import, &sig) .expect("declare plugin shim failed"); - - let mut fb = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbc); - if let Some(idx) = self.current_block_index { fb.switch_to_block(self.blocks[idx]); } - else if let Some(b) = self.entry_block { fb.switch_to_block(b); } let fref = self.module.declare_func_in_func(func_id, fb.func); + let c_type = fb.ins().iconst(types::I64, type_id as i64); let c_meth = fb.ins().iconst(types::I64, method_id as i64); let c_argc = fb.ins().iconst(types::I64, argc as i64); @@ -1441,6 +1538,9 @@ impl CraneliftBuilder { builder.symbol(c::SYM_STRING_CHARCODE_AT_H, nyash_string_charcode_at_h as *const u8); builder.symbol(c::SYM_STRING_BIRTH_H, nyash_string_birth_h as *const u8); builder.symbol(c::SYM_INTEGER_BIRTH_H, nyash_integer_birth_h as *const u8); + // Plugin invoke shims (i64/f64) + builder.symbol("nyash_plugin_invoke3_i64", nyash_plugin_invoke3_i64 as *const u8); + builder.symbol("nyash_plugin_invoke3_f64", nyash_plugin_invoke3_f64 as *const u8); } let module = cranelift_jit::JITModule::new(builder); let ctx = cranelift_codegen::Context::new(); diff --git a/src/jit/lower/core.rs b/src/jit/lower/core.rs index ca2b3b1c..2721ba22 100644 --- a/src/jit/lower/core.rs +++ b/src/jit/lower/core.rs @@ -595,49 +595,40 @@ impl LowerCore { } } I::ArrayGet { array, index, .. } => { - if std::env::var("NYASH_USE_PLUGIN_BUILTINS").ok().as_deref() == Some("1") { - // Plugin path: ArrayBox.get(index) - if let Ok(ph) = crate::runtime::plugin_loader_unified::get_global_plugin_host().read() { - if let Ok(h) = ph.resolve_method("ArrayBox", "get") { - // receiver - if let Some(pidx) = self.param_index.get(array).copied() { b.emit_param_i64(pidx); } else { b.emit_const_i64(-1); } - // index - if let Some(iv) = self.known_i64.get(index).copied() { b.emit_const_i64(iv); } else { self.push_value_if_known_or_param(b, index); } - b.emit_plugin_invoke(h.type_id, h.method_id, 2, true); - crate::jit::events::emit_lower( - serde_json::json!({ - "id": format!("plugin:{}:{}", h.box_type, "get"), - "decision":"allow","reason":"plugin_invoke","argc": 2, - "type_id": h.type_id, "method_id": h.method_id - }), - "plugin","" - ); - } + // Prepare receiver + index on stack + let argc = 2usize; + if let Some(pidx) = self.param_index.get(array).copied() { b.emit_param_i64(pidx); } else { b.emit_const_i64(-1); } + if let Some(iv) = self.known_i64.get(index).copied() { b.emit_const_i64(iv); } else { self.push_value_if_known_or_param(b, index); } + // Decide policy + let decision = crate::jit::policy::invoke::decide_box_method("ArrayBox", "get", argc, true); + match decision { + crate::jit::policy::invoke::InvokeDecision::PluginInvoke { type_id, method_id, box_type, .. } => { + b.emit_plugin_invoke(type_id, method_id, argc, true); + crate::jit::observe::lower_plugin_invoke(&box_type, "get", type_id, method_id, argc); } - } else { - super::core_hostcall::lower_array_get(b, &self.param_index, &self.known_i64, array, index); + crate::jit::policy::invoke::InvokeDecision::HostCall { symbol, .. } => { + crate::jit::observe::lower_hostcall(&symbol, argc, &["Handle","I64"], "allow", "mapped_symbol"); + b.emit_host_call(&symbol, argc, true); + } + _ => super::core_hostcall::lower_array_get(b, &self.param_index, &self.known_i64, array, index), } } I::ArraySet { array, index, value } => { - if std::env::var("NYASH_USE_PLUGIN_BUILTINS").ok().as_deref() == Some("1") { - if let Ok(ph) = crate::runtime::plugin_loader_unified::get_global_plugin_host().read() { - if let Ok(h) = ph.resolve_method("ArrayBox", "set") { - if let Some(pidx) = self.param_index.get(array).copied() { b.emit_param_i64(pidx); } else { b.emit_const_i64(-1); } - if let Some(iv) = self.known_i64.get(index).copied() { b.emit_const_i64(iv); } else { self.push_value_if_known_or_param(b, index); } - if let Some(vv) = self.known_i64.get(value).copied() { b.emit_const_i64(vv); } else { self.push_value_if_known_or_param(b, value); } - b.emit_plugin_invoke(h.type_id, h.method_id, 3, false); - crate::jit::events::emit_lower( - serde_json::json!({ - "id": format!("plugin:{}:{}", h.box_type, "set"), - "decision":"allow","reason":"plugin_invoke","argc": 3, - "type_id": h.type_id, "method_id": h.method_id - }), - "plugin","" - ); - } + let argc = 3usize; + if let Some(pidx) = self.param_index.get(array).copied() { b.emit_param_i64(pidx); } else { b.emit_const_i64(-1); } + if let Some(iv) = self.known_i64.get(index).copied() { b.emit_const_i64(iv); } else { self.push_value_if_known_or_param(b, index); } + if let Some(vv) = self.known_i64.get(value).copied() { b.emit_const_i64(vv); } else { self.push_value_if_known_or_param(b, value); } + let decision = crate::jit::policy::invoke::decide_box_method("ArrayBox", "set", argc, false); + match decision { + crate::jit::policy::invoke::InvokeDecision::PluginInvoke { type_id, method_id, box_type, .. } => { + b.emit_plugin_invoke(type_id, method_id, argc, false); + crate::jit::observe::lower_plugin_invoke(&box_type, "set", type_id, method_id, argc); } - } else { - super::core_hostcall::lower_array_set(b, &self.param_index, &self.known_i64, array, index, value); + crate::jit::policy::invoke::InvokeDecision::HostCall { symbol, .. } => { + crate::jit::observe::lower_hostcall(&symbol, argc, &["Handle","I64","I64"], "allow", "mapped_symbol"); + b.emit_host_call(&symbol, argc, false); + } + _ => super::core_hostcall::lower_array_set(b, &self.param_index, &self.known_i64, array, index, value), } } I::BoxCall { box_val: array, method, args, dst, .. } => { @@ -655,50 +646,29 @@ impl LowerCore { dst.clone(), ); } else if std::env::var("NYASH_USE_PLUGIN_BUILTINS").ok().as_deref() == Some("1") { - // Prefer unique StringBox methods first to avoid name collisions with Array/Map - if matches!(method.as_str(), "is_empty" | "charCodeAt") { - if let Ok(ph) = crate::runtime::plugin_loader_unified::get_global_plugin_host().read() { - if let Ok(h) = ph.resolve_method("StringBox", method.as_str()) { - // Receiver - if let Some(pidx) = self.param_index.get(array).copied() { b.emit_param_i64(pidx); } else { b.emit_const_i64(-1); } - let mut argc = 1usize; - if method.as_str() == "charCodeAt" { - if let Some(v) = args.get(0) { self.push_value_if_known_or_param(b, v); } else { b.emit_const_i64(0); } - argc = 2; - } - if method.as_str() == "is_empty" { b.hint_ret_bool(true); } - b.emit_plugin_invoke(h.type_id, h.method_id, argc, dst.is_some()); - crate::jit::events::emit_lower( - serde_json::json!({ - "id": format!("plugin:{}:{}", h.box_type, method.as_str()), - "decision":"allow","reason":"plugin_invoke","argc": argc, - "type_id": h.type_id, "method_id": h.method_id - }), - "plugin","" - ); + // StringBox(length/is_empty/charCodeAt): policy+observe経由に統一 + if matches!(method.as_str(), "length" | "is_empty" | "charCodeAt") { + // receiver + if let Some(pidx) = self.param_index.get(array).copied() { b.emit_param_i64(pidx); } else { b.emit_const_i64(-1); } + let mut argc = 1usize; + if method.as_str() == "charCodeAt" { + if let Some(v) = args.get(0) { self.push_value_if_known_or_param(b, v); } else { b.emit_const_i64(0); } + argc = 2; + } + if method.as_str() == "is_empty" { b.hint_ret_bool(true); } + let decision = crate::jit::policy::invoke::decide_box_method("StringBox", method.as_str(), argc, dst.is_some()); + match decision { + crate::jit::policy::invoke::InvokeDecision::PluginInvoke { type_id, method_id, box_type, .. } => { + b.emit_plugin_invoke(type_id, method_id, argc, dst.is_some()); + crate::jit::observe::lower_plugin_invoke(&box_type, method.as_str(), type_id, method_id, argc); return Ok(()); } - } - } - // String.length() specialized when receiver is String (avoid Array collision) - if method.as_str() == "length" { - let recv_is_string = func.metadata.value_types.get(array).map(|mt| matches!(mt, crate::mir::MirType::String)).unwrap_or(false); - if recv_is_string { - if let Ok(ph) = crate::runtime::plugin_loader_unified::get_global_plugin_host().read() { - if let Ok(h) = ph.resolve_method("StringBox", "length") { - if let Some(pidx) = self.param_index.get(array).copied() { b.emit_param_i64(pidx); } else { b.emit_const_i64(-1); } - b.emit_plugin_invoke(h.type_id, h.method_id, 1, dst.is_some()); - crate::jit::events::emit_lower( - serde_json::json!({ - "id": format!("plugin:{}:{}", h.box_type, "length"), - "decision":"allow","reason":"plugin_invoke","argc": 1, - "type_id": h.type_id, "method_id": h.method_id - }), - "plugin","" - ); - return Ok(()); - } + crate::jit::policy::invoke::InvokeDecision::HostCall { symbol, .. } => { + crate::jit::observe::lower_hostcall(&symbol, argc, &if argc==1 { ["Handle"][..].to_vec() } else { ["Handle","I64"][..].to_vec() }, "allow", "mapped_symbol"); + b.emit_host_call(&symbol, argc, dst.is_some()); + return Ok(()); } + _ => {} } } // Integer.get/set specialized when receiver is Integer (avoid Map collision) @@ -728,27 +698,15 @@ impl LowerCore { } } match method.as_str() { - "len" | "length" | "push" | "get" | "set" => { + "len" | "length" => { // Resolve ArrayBox plugin method and emit plugin_invoke (symbolic) if let Ok(ph) = crate::runtime::plugin_loader_unified::get_global_plugin_host().read() { - let mname = if method.as_str() == "len" { "length" } else { method.as_str() }; + let mname = "length"; if let Ok(h) = ph.resolve_method("ArrayBox", mname) { // Receiver if let Some(pidx) = self.param_index.get(array).copied() { b.emit_param_i64(pidx); } else { b.emit_const_i64(-1); } let mut argc = 1usize; - match mname { - "push" | "get" => { - if let Some(v) = args.get(0) { self.push_value_if_known_or_param(b, v); } else { b.emit_const_i64(0); } - argc += 1; - } - "set" => { - // two args: index, value - if let Some(v) = args.get(0) { self.push_value_if_known_or_param(b, v); } else { b.emit_const_i64(0); } - if let Some(v2) = args.get(1) { self.push_value_if_known_or_param(b, v2); } else { b.emit_const_i64(0); } - argc += 2; - } - _ => {} - } + // length only b.emit_plugin_invoke(h.type_id, h.method_id, argc, dst.is_some()); crate::jit::events::emit_lower( serde_json::json!({ @@ -938,200 +896,124 @@ impl LowerCore { } } "push" => { - // argc=2: (array_handle, value) + // argc=2: (array, value) + let argc = 2usize; let val = args.get(0).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); if let Some(pidx) = self.param_index.get(array).copied() { - let pol = crate::jit::policy::current(); - let wh = &pol.hostcall_whitelist; - let sym = crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H; - let allowed = !pol.read_only || wh.iter().any(|s| s == sym); - crate::jit::events::emit_lower( - serde_json::json!({ - "id": sym, - "decision": if allowed {"allow"} else {"fallback"}, - "reason": if allowed {"sig_ok"} else {"policy_denied_mutating"}, - "argc": 2, - "arg_types": ["Handle","I64"] - }), - "hostcall","" - ); + // Prepare args b.emit_param_i64(pidx); b.emit_const_i64(val); - b.emit_host_call(sym, 2, false); - } else { - crate::jit::events::emit_lower( - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H, "decision":"fallback", "reason":"receiver_not_param", "argc":2, "arg_types":["Handle","I64"]}), - "hostcall","" - ); - let arr_idx = -1; - b.emit_const_i64(arr_idx); - b.emit_const_i64(val); - b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_PUSH, 2, false); - } - } - "size" => { - // MapBox.size(): argc=1 (map_handle) - if let Some(pidx) = self.param_index.get(array).copied() { - crate::jit::events::emit_lower( - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SIZE_H, "decision":"allow", "reason":"sig_ok", "argc":1, "arg_types":["Handle"]}), - "hostcall","" - ); - b.emit_param_i64(pidx); - b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_SIZE_H, 1, dst.is_some()); - } else { - crate::jit::events::emit_lower( - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SIZE_H, "decision":"fallback", "reason":"receiver_not_param", "argc":1, "arg_types":["Handle"]}), - "hostcall","" - ); - let map_idx = -1; - b.emit_const_i64(map_idx); - b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_SIZE, 1, dst.is_some()); - } - } - "get" => { - // MapBox.get(key): check TyEnv to choose signature (handle|i64) - if let Some(pidx) = self.param_index.get(array).copied() { - // Build observed arg kinds using TyEnv when available - let mut observed_kinds: Vec = Vec::new(); - // First arg = map handle - observed_kinds.push(crate::jit::hostcall_registry::ArgKind::Handle); - // Second arg = key (classify from TyEnv; fallback to I64 if known integer literal) - let key_kind = if let Some(key_vid) = args.get(0) { - if let Some(mt) = func.metadata.value_types.get(key_vid) { - match mt { - crate::mir::MirType::Float => crate::jit::hostcall_registry::ArgKind::I64, // coerced via VM path - crate::mir::MirType::Integer => crate::jit::hostcall_registry::ArgKind::I64, - crate::mir::MirType::Bool => crate::jit::hostcall_registry::ArgKind::I64, - crate::mir::MirType::String | crate::mir::MirType::Box(_) => crate::jit::hostcall_registry::ArgKind::Handle, - _ => { - if let Some(_) = self.known_i64.get(key_vid) { crate::jit::hostcall_registry::ArgKind::I64 } else { crate::jit::hostcall_registry::ArgKind::Handle } - } - } - } else if let Some(_) = self.known_i64.get(key_vid) { - crate::jit::hostcall_registry::ArgKind::I64 - } else { - crate::jit::hostcall_registry::ArgKind::Handle + // Decide policy + let decision = crate::jit::policy::invoke::decide_box_method("ArrayBox", "push", argc, false); + match decision { + crate::jit::policy::invoke::InvokeDecision::PluginInvoke { type_id, method_id, box_type, .. } => { + b.emit_plugin_invoke(type_id, method_id, argc, false); + crate::jit::observe::lower_plugin_invoke(&box_type, "push", type_id, method_id, argc); } - } else { crate::jit::hostcall_registry::ArgKind::I64 }; - observed_kinds.push(key_kind); - - // Prepare arg_types strings for events - let arg_types: Vec<&'static str> = observed_kinds.iter().map(|k| match k { crate::jit::hostcall_registry::ArgKind::I64 => "I64", crate::jit::hostcall_registry::ArgKind::F64 => "F64", crate::jit::hostcall_registry::ArgKind::Handle => "Handle" }).collect(); - - // Signature check against registry (supports overloads) using canonical id - let canonical = "nyash.map.get_h"; - match crate::jit::hostcall_registry::check_signature(canonical, &observed_kinds) { - Ok(()) => { - // Choose symbol id for event/emit - let event_id = if matches!(key_kind, crate::jit::hostcall_registry::ArgKind::Handle) - && args.get(0).and_then(|v| self.param_index.get(v)).is_some() { - crate::jit::r#extern::collections::SYM_MAP_GET_HH - } else { - crate::jit::r#extern::collections::SYM_MAP_GET_H - }; - // Emit allow event - crate::jit::events::emit_lower( - serde_json::json!({ - "id": event_id, - "decision": "allow", - "reason": "sig_ok", - "argc": observed_kinds.len(), - "arg_types": arg_types - }), - "hostcall","" - ); - // If key is i64, emit hostcall; if key is Handle and also a param, emit HH variant; otherwise fallback - if matches!(key_kind, crate::jit::hostcall_registry::ArgKind::I64) { - let key_i = args.get(0).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); - b.emit_param_i64(pidx); - b.emit_const_i64(key_i); - b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_GET_H, 2, dst.is_some()); - } else if let Some(kp) = args.get(0).and_then(|v| self.param_index.get(v)).copied() { - // key is a function parameter (handle), use HH variant - b.emit_param_i64(pidx); - b.emit_param_i64(kp); - b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_GET_HH, 2, dst.is_some()); - } else { - // Not a param: fall back (receiver_not_param or key_not_param already logged) - // no emission; VM will execute - } + crate::jit::policy::invoke::InvokeDecision::HostCall { symbol, .. } => { + crate::jit::observe::lower_hostcall(&symbol, argc, &["Handle","I64"], "allow", "mapped_symbol"); + b.emit_host_call(&symbol, argc, false); } - Err(reason) => { - // Signature mismatch - log and fallback - crate::jit::events::emit_lower( - serde_json::json!({ - "id": canonical, - "decision": "fallback", - "reason": reason, - "argc": observed_kinds.len(), - "arg_types": arg_types - }), - "hostcall","" - ); - // No emission; VM path will handle + _ => { + // Fallback to existing hostcall path + let sym = crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H; + crate::jit::observe::lower_hostcall(sym, argc, &["Handle","I64"], "fallback", "policy_or_unknown"); + b.emit_host_call(sym, argc, false); } } } else { - // Receiver is not a function parameter; we cannot obtain a stable runtime handle. - // Still classify and emit an event for visibility, then fallback to VM. - let mut observed_kinds: Vec = Vec::new(); - observed_kinds.push(crate::jit::hostcall_registry::ArgKind::Handle); // Map receiver (conceptually a handle) - let key_kind = if let Some(key_vid) = args.get(0) { - if let Some(mt) = func.metadata.value_types.get(key_vid) { - match mt { - crate::mir::MirType::Integer => crate::jit::hostcall_registry::ArgKind::I64, - crate::mir::MirType::Float => crate::jit::hostcall_registry::ArgKind::I64, - crate::mir::MirType::Bool => crate::jit::hostcall_registry::ArgKind::I64, - crate::mir::MirType::String | crate::mir::MirType::Box(_) => crate::jit::hostcall_registry::ArgKind::Handle, - _ => crate::jit::hostcall_registry::ArgKind::Handle, - } - } else { crate::jit::hostcall_registry::ArgKind::Handle } - } else { crate::jit::hostcall_registry::ArgKind::Handle }; - observed_kinds.push(key_kind); - let arg_types: Vec<&'static str> = observed_kinds.iter().map(|k| match k { crate::jit::hostcall_registry::ArgKind::I64 => "I64", crate::jit::hostcall_registry::ArgKind::F64 => "F64", crate::jit::hostcall_registry::ArgKind::Handle => "Handle" }).collect(); - let sym = "nyash.map.get_h"; - let decision = match crate::jit::hostcall_registry::check_signature(sym, &observed_kinds) { Ok(()) => ("fallback", "receiver_not_param"), Err(reason) => ("fallback", reason) }; - crate::jit::events::emit_lower( - serde_json::json!({ - "id": sym, - "decision": decision.0, - "reason": decision.1, - "argc": observed_kinds.len(), - "arg_types": arg_types - }), - "hostcall","" - ); - // no-op: VM側が処理する + // No receiver param index + let arr_idx = -1; + b.emit_const_i64(arr_idx); + b.emit_const_i64(val); + let sym = crate::jit::r#extern::collections::SYM_ARRAY_PUSH; + crate::jit::observe::lower_hostcall(sym, argc, &["I64","I64"], "fallback", "receiver_not_param"); + b.emit_host_call(sym, argc, false); + } + } + "size" => { + let argc = 1usize; + if let Some(pidx) = self.param_index.get(array).copied() { + b.emit_param_i64(pidx); + let decision = crate::jit::policy::invoke::decide_box_method("MapBox", "size", argc, dst.is_some()); + match decision { + crate::jit::policy::invoke::InvokeDecision::PluginInvoke { type_id, method_id, box_type, .. } => { + b.emit_plugin_invoke(type_id, method_id, argc, dst.is_some()); + crate::jit::observe::lower_plugin_invoke(&box_type, "size", type_id, method_id, argc); + } + crate::jit::policy::invoke::InvokeDecision::HostCall { symbol, .. } => { + crate::jit::observe::lower_hostcall(&symbol, argc, &["Handle"], "allow", "mapped_symbol"); + b.emit_host_call(&symbol, argc, dst.is_some()); + } + _ => { + let sym = crate::jit::r#extern::collections::SYM_MAP_SIZE_H; + crate::jit::observe::lower_hostcall(sym, argc, &["Handle"], "fallback", "policy_or_unknown"); + b.emit_host_call(sym, argc, dst.is_some()); + } + } + } else { + let map_idx = -1; + b.emit_const_i64(map_idx); + let sym = crate::jit::r#extern::collections::SYM_MAP_SIZE; + crate::jit::observe::lower_hostcall(sym, argc, &["I64"], "fallback", "receiver_not_param"); + b.emit_host_call(sym, argc, dst.is_some()); + } + } + "get" => { + let argc = 2usize; + if let Some(pidx) = self.param_index.get(array).copied() { + b.emit_param_i64(pidx); + if let Some(k) = args.get(0).and_then(|v| self.known_i64.get(v)).copied() { b.emit_const_i64(k); } else if let Some(kvid) = args.get(0) { self.push_value_if_known_or_param(b, kvid); } else { b.emit_const_i64(0); } + let decision = crate::jit::policy::invoke::decide_box_method("MapBox", "get", argc, dst.is_some()); + match decision { + crate::jit::policy::invoke::InvokeDecision::PluginInvoke { type_id, method_id, box_type, .. } => { + b.emit_plugin_invoke(type_id, method_id, argc, dst.is_some()); + crate::jit::observe::lower_plugin_invoke(&box_type, "get", type_id, method_id, argc); + } + crate::jit::policy::invoke::InvokeDecision::HostCall { symbol, .. } => { + crate::jit::observe::lower_hostcall(&symbol, argc, &["Handle","I64"], "allow", "mapped_symbol"); + b.emit_host_call(&symbol, argc, dst.is_some()); + } + _ => { + let sym = crate::jit::r#extern::collections::SYM_MAP_GET_H; + crate::jit::observe::lower_hostcall(sym, argc, &["Handle","I64"], "fallback", "policy_or_unknown"); + b.emit_host_call(sym, argc, dst.is_some()); + } + } + } else { + let sym = crate::jit::r#extern::collections::SYM_MAP_GET_H; + crate::jit::observe::lower_hostcall(sym, argc, &["I64","I64"], "fallback", "receiver_not_param"); + b.emit_host_call(sym, argc, dst.is_some()); } } "set" => { - // MapBox.set(key, value): (map_handle, key_i64, val_i64) — PoC: integer-only + let argc = 3usize; if let Some(pidx) = self.param_index.get(array).copied() { let key = args.get(0).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); let val = args.get(1).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); - let pol = crate::jit::policy::current(); - let wh = &pol.hostcall_whitelist; - let sym = crate::jit::r#extern::collections::SYM_MAP_SET_H; - let allowed = !pol.read_only || wh.iter().any(|s| s == sym); - crate::jit::events::emit_lower( - serde_json::json!({ - "id": sym, - "decision": if allowed {"allow"} else {"fallback"}, - "reason": if allowed {"sig_ok"} else {"policy_denied_mutating"}, - "argc": 3, - "arg_types": ["Handle","I64","I64"] - }), - "hostcall","" - ); b.emit_param_i64(pidx); b.emit_const_i64(key); b.emit_const_i64(val); - b.emit_host_call(sym, 3, false); + let decision = crate::jit::policy::invoke::decide_box_method("MapBox", "set", argc, false); + match decision { + crate::jit::policy::invoke::InvokeDecision::PluginInvoke { type_id, method_id, box_type, .. } => { + b.emit_plugin_invoke(type_id, method_id, argc, false); + crate::jit::observe::lower_plugin_invoke(&box_type, "set", type_id, method_id, argc); + } + crate::jit::policy::invoke::InvokeDecision::HostCall { symbol, .. } => { + crate::jit::observe::lower_hostcall(&symbol, argc, &["Handle","I64","I64"], "allow", "mapped_symbol"); + b.emit_host_call(&symbol, argc, false); + } + _ => { + let sym = crate::jit::r#extern::collections::SYM_MAP_SET_H; + crate::jit::observe::lower_hostcall(sym, argc, &["Handle","I64","I64"], "fallback", "policy_or_unknown"); + b.emit_host_call(sym, argc, false); + } + } } else { - crate::jit::events::emit_lower( - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SET_H, "decision":"fallback", "reason":"receiver_not_param", "argc":3, "arg_types":["Handle","I64","I64"]}), - "hostcall","" - ); + let sym = crate::jit::r#extern::collections::SYM_MAP_SET; + crate::jit::observe::lower_hostcall(sym, argc, &["I64","I64","I64"], "fallback", "receiver_not_param"); + b.emit_host_call(sym, argc, false); } } "charCodeAt" => { @@ -1153,12 +1035,27 @@ impl LowerCore { } } "has" => { - // MapBox.has(key_i64) -> 0/1 + let argc = 2usize; if let Some(pidx) = self.param_index.get(array).copied() { let key = args.get(0).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); b.emit_param_i64(pidx); b.emit_const_i64(key); - b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_HAS_H, 2, dst.is_some()); + let decision = crate::jit::policy::invoke::decide_box_method("MapBox", "has", argc, dst.is_some()); + match decision { + crate::jit::policy::invoke::InvokeDecision::PluginInvoke { type_id, method_id, box_type, .. } => { + b.emit_plugin_invoke(type_id, method_id, argc, dst.is_some()); + crate::jit::observe::lower_plugin_invoke(&box_type, "has", type_id, method_id, argc); + } + crate::jit::policy::invoke::InvokeDecision::HostCall { symbol, .. } => { + crate::jit::observe::lower_hostcall(&symbol, argc, &["Handle","I64"], "allow", "mapped_symbol"); + b.emit_host_call(&symbol, argc, dst.is_some()); + } + _ => { + let sym = crate::jit::r#extern::collections::SYM_MAP_HAS_H; + crate::jit::observe::lower_hostcall(sym, argc, &["Handle","I64"], "fallback", "policy_or_unknown"); + b.emit_host_call(sym, argc, dst.is_some()); + } + } } } _ => {} diff --git a/src/jit/lower/core_hostcall.rs b/src/jit/lower/core_hostcall.rs index 20dcb459..514a630f 100644 --- a/src/jit/lower/core_hostcall.rs +++ b/src/jit/lower/core_hostcall.rs @@ -68,18 +68,19 @@ pub fn lower_box_call( "len" | "length" => { if let Some(pidx) = param_index.get(recv).copied() { crate::jit::events::emit_lower( - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_LEN_H, "decision":"allow", "reason":"sig_ok", "argc":1, "arg_types":["Handle"]}), + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ARRAY_LEN, "decision":"allow", "reason":"sig_ok", "argc":1, "arg_types":["I64(index)"]}), "hostcall","" ); - b.emit_param_i64(pidx); - b.emit_host_call(crate::jit::r#extern::collections::SYM_ANY_LEN_H, 1, dst.is_some()); + // Pass parameter index directly (JIT thunks read legacy VM args by index) + b.emit_param_i64(pidx as i64 as usize); + b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_LEN, 1, dst.is_some()); } else { crate::jit::events::emit( "hostcall","",None,None, serde_json::json!({ "id": crate::jit::r#extern::collections::SYM_ARRAY_LEN, "decision": "fallback", "reason": "receiver_not_param", - "argc": 1, "arg_types": ["I64"] + "argc": 1, "arg_types": ["I64(index)"] }) ); b.emit_const_i64(-1); @@ -279,9 +280,12 @@ pub fn lower_boxcall_simple_reads( dst: Option, ) -> bool { if !crate::jit::config::current().hostcall { return false; } + // When plugin builtins are enabled, prefer plugin_invoke for length to exercise shim path + let use_plugin = std::env::var("NYASH_USE_PLUGIN_BUILTINS").ok().as_deref() == Some("1"); match method { // Any.length / Array.length "len" | "length" => { + if use_plugin { return false; } if let Some(pidx) = param_index.get(recv).copied() { crate::jit::events::emit_lower( serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_LEN_H, "decision":"allow", "reason":"sig_ok", "argc":1, "arg_types":["Handle"]}), diff --git a/src/jit/lower/extern_thunks.rs b/src/jit/lower/extern_thunks.rs index 5d7c36e6..5356d457 100644 --- a/src/jit/lower/extern_thunks.rs +++ b/src/jit/lower/extern_thunks.rs @@ -187,6 +187,27 @@ pub(super) extern "C" fn nyash_any_length_h(handle: u64) -> i64 { if let Some(sb) = obj.as_any().downcast_ref::() { return sb.value.len() as i64; } + } else { + // Fallback: some call sites may still pass a parameter index instead of a handle (legacy path) + // Try to interpret small values as param index and read from legacy VM args + if handle <= 16 { + let idx = handle as usize; + let val = crate::jit::rt::with_legacy_vm_args(|args| args.get(idx).cloned()); + if let Some(v) = val { + match v { + crate::backend::vm::VMValue::BoxRef(b) => { + if let Some(arr) = b.as_any().downcast_ref::() { + if let Some(ib) = arr.length().as_any().downcast_ref::() { return ib.value; } + } + if let Some(sb) = b.as_any().downcast_ref::() { + return sb.value.len() as i64; + } + } + crate::backend::vm::VMValue::String(s) => { return s.len() as i64; } + _ => {} + } + } + } } 0 } diff --git a/src/jit/mod.rs b/src/jit/mod.rs index b08ea95a..a1cd910e 100644 --- a/src/jit/mod.rs +++ b/src/jit/mod.rs @@ -7,8 +7,9 @@ pub mod r#extern; pub mod rt; pub mod abi; pub mod config; -pub mod policy; pub mod events; pub mod hostcall_registry; pub mod boundary; pub mod shim_trace; +pub mod observe; +pub mod policy; diff --git a/src/jit/observe.rs b/src/jit/observe.rs new file mode 100644 index 00000000..49cc710c --- /dev/null +++ b/src/jit/observe.rs @@ -0,0 +1,43 @@ +//! Observe facade: centralize compile/runtime/trace output rules. +//! Thin wrappers around jit::events and shim_trace to keep callsites tidy. + +pub fn lower_plugin_invoke(box_type: &str, method: &str, type_id: u32, method_id: u32, argc: usize) { + crate::jit::events::emit_lower( + serde_json::json!({ + "id": format!("plugin:{}:{}", box_type, method), + "decision":"allow","reason":"plugin_invoke","argc": argc, + "type_id": type_id, "method_id": method_id + }), + "plugin","" + ); +} + +pub fn lower_hostcall(symbol: &str, argc: usize, arg_types: &[&str], decision: &str, reason: &str) { + crate::jit::events::emit_lower( + serde_json::json!({ + "id": symbol, + "decision": decision, + "reason": reason, + "argc": argc, + "arg_types": arg_types + }), + "hostcall","" + ); +} + +pub fn runtime_plugin_shim_i64(type_id: i64, method_id: i64, argc: i64, inst: u32) { + crate::jit::events::emit_runtime( + serde_json::json!({ + "id": "plugin_invoke.i64", + "type_id": type_id, + "method_id": method_id, + "argc": argc, + "inst": inst + }), + "plugin", "" + ); +} + +pub fn trace_push(msg: String) { crate::jit::shim_trace::push(msg); } +pub fn trace_enabled() -> bool { crate::jit::shim_trace::is_enabled() } + diff --git a/src/jit/policy.rs b/src/jit/policy.rs index 585cdc7b..9e9e01bf 100644 --- a/src/jit/policy.rs +++ b/src/jit/policy.rs @@ -40,3 +40,5 @@ pub fn set_current(p: JitPolicy) { let _ = GLOBAL.set(RwLock::new(p)); } +// Submodule: invoke decision policy +pub mod invoke; diff --git a/src/jit/policy/invoke.rs b/src/jit/policy/invoke.rs new file mode 100644 index 00000000..af3c504f --- /dev/null +++ b/src/jit/policy/invoke.rs @@ -0,0 +1,47 @@ +//! InvokePolicyPass (minimal scaffold) +//! Centralizes decision for plugin/hostcall/any to keep lowerer slim. +//! Current implementation covers a small subset (ArrayBox length/get/set/push, +//! MapBox size/get/has/set) when NYASH_USE_PLUGIN_BUILTINS=1, falling back +//! to existing hostcall symbols otherwise. Extend incrementally. + +#[derive(Debug, Clone)] +pub enum InvokeDecision { + PluginInvoke { type_id: u32, method_id: u32, box_type: String, method: String, argc: usize, has_ret: bool }, + HostCall { symbol: String, argc: usize, has_ret: bool, reason: &'static str }, + Fallback { reason: &'static str }, +} + +fn use_plugin_builtins() -> bool { + std::env::var("NYASH_USE_PLUGIN_BUILTINS").ok().as_deref() == Some("1") +} + +/// Decide invocation policy for a known Box method. +pub fn decide_box_method(box_type: &str, method: &str, argc: usize, has_ret: bool) -> InvokeDecision { + // Prefer plugin path when enabled and method is resolvable + if use_plugin_builtins() { + if let Ok(ph) = crate::runtime::plugin_loader_unified::get_global_plugin_host().read() { + if let Ok(h) = ph.resolve_method(box_type, method) { + return InvokeDecision::PluginInvoke { type_id: h.type_id, method_id: h.method_id, box_type: h.box_type, method: method.to_string(), argc, has_ret }; + } + } + } + // Minimal hostcall mapping for common collections/math symbols + let symbol = match (box_type, method) { + ("ArrayBox", "length") | ("StringBox", "length") | ("StringBox", "len") => crate::jit::r#extern::collections::SYM_ANY_LEN_H, + ("ArrayBox", "get") => crate::jit::r#extern::collections::SYM_ARRAY_GET_H, + ("ArrayBox", "set") => crate::jit::r#extern::collections::SYM_ARRAY_SET_H, + ("ArrayBox", "push") => crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H, + ("MapBox", "size") => crate::jit::r#extern::collections::SYM_MAP_SIZE_H, + ("MapBox", "get") => crate::jit::r#extern::collections::SYM_MAP_GET_HH, + ("MapBox", "has") => crate::jit::r#extern::collections::SYM_MAP_HAS_H, + ("MapBox", "set") => crate::jit::r#extern::collections::SYM_MAP_SET_H, + ("StringBox","is_empty") => crate::jit::r#extern::collections::SYM_ANY_IS_EMPTY_H, + ("StringBox","charCodeAt") => crate::jit::r#extern::collections::SYM_STRING_CHARCODE_AT_H, + _ => "" // unknown + }; + if symbol.is_empty() { + InvokeDecision::Fallback { reason: "unknown_method" } + } else { + InvokeDecision::HostCall { symbol: symbol.to_string(), argc, has_ret, reason: "mapped_symbol" } + } +} diff --git a/src/mir/builder.rs b/src/mir/builder.rs index 583c1e47..b40d2826 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -64,6 +64,8 @@ pub struct MirBuilder { /// Optional per-value type annotations (MIR-level): ValueId -> MirType pub(super) value_types: HashMap, + /// Current static box name when lowering a static box body (e.g., "Main") + current_static_box: Option, } impl MirBuilder { @@ -82,6 +84,7 @@ impl MirBuilder { weak_fields_by_box: HashMap::new(), field_origin_class: HashMap::new(), value_types: HashMap::new(), + current_static_box: None, } } @@ -399,7 +402,7 @@ impl MirBuilder { ASTNode::BoxDeclaration { name, methods, is_static, fields, constructors, weak_fields, .. } => { if is_static && name == "Main" { - self.build_static_main_box(methods.clone()) + self.build_static_main_box(name.clone(), methods.clone()) } else { // Support user-defined boxes - handle as statement, return void // Track as user-defined (eligible for method lowering) @@ -1035,10 +1038,22 @@ impl MirBuilder { Ok(return_value) } - /// Build static box Main - extracts main() method body and converts to Program - fn build_static_main_box(&mut self, methods: std::collections::HashMap) -> Result { + /// Build static box (e.g., Main) - extracts main() method body and converts to Program + /// Also lowers other static methods into standalone MIR functions: BoxName.method/N + fn build_static_main_box(&mut self, box_name: String, methods: std::collections::HashMap) -> Result { + // Lower other static methods (except main) to standalone MIR functions so JIT can see them + for (mname, mast) in methods.iter() { + if mname == "main" { continue; } + if let ASTNode::FunctionDeclaration { params, body, .. } = mast { + let func_name = format!("{}.{}{}", box_name, mname, format!("/{}", params.len())); + self.lower_static_method_as_function(func_name, params.clone(), body.clone())?; + } + } + // Within this lowering, treat `me` receiver as this static box + let saved_static = self.current_static_box.clone(); + self.current_static_box = Some(box_name.clone()); // Look for the main() method - if let Some(main_method) = methods.get("main") { + let out = if let Some(main_method) = methods.get("main") { if let ASTNode::FunctionDeclaration { body, .. } = main_method { // Convert the method body to a Program AST node and lower it let program_ast = ASTNode::Program { @@ -1049,11 +1064,14 @@ impl MirBuilder { // Use existing Program lowering logic self.build_expression(program_ast) } else { - Err("main method in static box Main is not a FunctionDeclaration".to_string()) + Err("main method in static box is not a FunctionDeclaration".to_string()) } } else { - Err("static box Main must contain a main() method".to_string()) - } + Err("static box must contain a main() method".to_string()) + }; + // Restore static box context + self.current_static_box = saved_static; + out } /// Build field access: object.field @@ -1287,10 +1305,8 @@ impl MirBuilder { // Fallback: use a symbolic constant (legacy behavior) let me_value = self.value_gen.next(); - self.emit_instruction(MirInstruction::Const { - dst: me_value, - value: ConstValue::String("__me__".to_string()), - })?; + let me_tag = if let Some(ref cls) = self.current_static_box { cls.clone() } else { "__me__".to_string() }; + self.emit_instruction(MirInstruction::Const { dst: me_value, value: ConstValue::String(me_tag) })?; // Register a stable mapping so subsequent 'me' resolves to the same ValueId self.variable_map.insert("me".to_string(), me_value); Ok(me_value) @@ -1347,6 +1363,23 @@ impl MirBuilder { } } + // If object is `me` within a static box, lower to direct Call: BoxName.method/N + if let ASTNode::Me { .. } = object { + if let Some(cls_name) = self.current_static_box.clone() { + // Build args first + let mut arg_values: Vec = Vec::new(); + for a in &arguments { arg_values.push(self.build_expression(a.clone())?); } + let result_id = self.value_gen.next(); + // Create const function name + let fun_name = format!("{}.{}{}", cls_name, method, format!("/{}", arg_values.len())); + let fun_val = self.value_gen.next(); + self.emit_instruction(MirInstruction::Const { dst: fun_val, value: ConstValue::String(fun_name) })?; + // Emit Call + self.emit_instruction(MirInstruction::Call { dst: Some(result_id), func: fun_val, args: arg_values, effects: EffectMask::READ.add(Effect::ReadHeap) })?; + return Ok(result_id); + } + } + // Build the object expression let object_value = self.build_expression(object.clone())?; @@ -1528,6 +1561,65 @@ impl MirBuilder { Ok(result_id) } + + /// Lower a static method body into a standalone MIR function (no `me` parameter) + fn lower_static_method_as_function( + &mut self, + func_name: String, + params: Vec, + body: Vec, + ) -> Result<(), String> { + let mut param_types = Vec::new(); + for _ in ¶ms { param_types.push(MirType::Unknown); } + let mut returns_value = false; + for st in &body { if let ASTNode::Return { value: Some(_), .. } = st { returns_value = true; break; } } + let ret_ty = if returns_value { MirType::Unknown } else { MirType::Void }; + + let signature = FunctionSignature { name: func_name, params: param_types, return_type: ret_ty, effects: EffectMask::READ.add(Effect::ReadHeap) }; + let entry = self.block_gen.next(); + let function = MirFunction::new(signature, entry); + + // Save state + let saved_function = self.current_function.take(); + let saved_block = self.current_block.take(); + let saved_var_map = std::mem::take(&mut self.variable_map); + let saved_value_gen = self.value_gen.clone(); + self.value_gen.reset(); + + // Switch + self.current_function = Some(function); + self.current_block = Some(entry); + self.ensure_block_exists(entry)?; + + // Bind parameters + if let Some(ref mut f) = self.current_function { + for p in ¶ms { let pid = self.value_gen.next(); f.params.push(pid); self.variable_map.insert(p.clone(), pid); } + } + + // Lower body + let program_ast = ASTNode::Program { statements: body, span: crate::ast::Span::unknown() }; + let _last = self.build_expression(program_ast)?; + + // Ensure terminator + if let Some(ref mut f) = self.current_function { + if let Some(block) = f.get_block(self.current_block.unwrap()) { if !block.is_terminated() { + let void_val = self.value_gen.next(); + self.emit_instruction(MirInstruction::Const { dst: void_val, value: ConstValue::Void })?; + self.emit_instruction(MirInstruction::Return { value: Some(void_val) })?; + }} + } + + // Add to module + let finalized = self.current_function.take().unwrap(); + if let Some(ref mut module) = self.current_module { module.add_function(finalized); } + + // Restore state + self.current_function = saved_function; + self.current_block = saved_block; + self.variable_map = saved_var_map; + self.value_gen = saved_value_gen; + Ok(()) + } /// Build box declaration: box Name { fields... methods... } fn build_box_declaration(&mut self, name: String, methods: std::collections::HashMap, fields: Vec, weak_fields: Vec) -> Result<(), String> { diff --git a/src/mir/mod.rs b/src/mir/mod.rs index e83596fd..d65e1581 100644 --- a/src/mir/mod.rs +++ b/src/mir/mod.rs @@ -22,6 +22,7 @@ pub mod value_id; pub mod effect; pub mod optimizer; pub mod slot_registry; // Phase 9.79b.1: method slot resolution (IDs) +pub mod passes; // Optimization subpasses (e.g., type_hints) // Re-export main types for easy access pub use instruction::{MirInstruction, BinaryOp, CompareOp, UnaryOp, ConstValue, MirType, TypeOpKind, WeakRefOp, BarrierOp}; diff --git a/src/mir/optimizer.rs b/src/mir/optimizer.rs index 121f5001..4fa46eee 100644 --- a/src/mir/optimizer.rs +++ b/src/mir/optimizer.rs @@ -59,6 +59,13 @@ impl MirOptimizer { // Pass 5: BoxField dependency optimization stats.merge(self.optimize_boxfield_operations(module)); + + // Pass 6: 受け手型ヒントの伝搬(callsite→callee) + // 目的: helper(arr){ return arr.length() } のようなケースで、 + // 呼び出し元の引数型(String/Integer/Bool/Float)を callee の params に反映し、 + // Lowererがより正確にBox種別を選べるようにする。 + let updates = crate::mir::passes::type_hints::propagate_param_type_hints(module); + if updates > 0 { stats.intrinsic_optimizations += updates as usize; } if self.debug { println!("✅ Optimization complete: {}", stats); @@ -72,6 +79,7 @@ impl MirOptimizer { stats } + /// Eliminate dead code (unused values) fn eliminate_dead_code(&mut self, module: &mut MirModule) -> OptimizationStats { diff --git a/src/mir/passes/mod.rs b/src/mir/passes/mod.rs new file mode 100644 index 00000000..996a7d68 --- /dev/null +++ b/src/mir/passes/mod.rs @@ -0,0 +1,5 @@ +// MIR optimization subpasses module +// Minimal scaffold to unblock builds when type hint propagation is not yet implemented. + +pub mod type_hints; + diff --git a/src/mir/passes/type_hints.rs b/src/mir/passes/type_hints.rs new file mode 100644 index 00000000..52f4d5f4 --- /dev/null +++ b/src/mir/passes/type_hints.rs @@ -0,0 +1,13 @@ +//! Propagate simple param type hints from callsites to callees. +//! +//! This is a minimal, no-op scaffold used to keep the build green +//! while the full pass is being implemented. It returns 0 updates. + +use crate::mir::MirModule; + +/// Walks the module and would propagate basic type hints when implemented. +/// Returns the number of updates applied. +pub fn propagate_param_type_hints(_module: &mut MirModule) -> u32 { + 0 +} + diff --git a/src/runner.rs b/src/runner.rs index 81191d52..8fa9ebaa 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -47,6 +47,52 @@ impl NyashRunner { /// Run Nyash based on the configuration pub fn run(&self) { + // Script-level env directives (special comments) — parse early + // Supported: + // // @env KEY=VALUE + // // @jit-debug (preset: exec, threshold=1, events+trace) + // // @plugin-builtins (NYASH_USE_PLUGIN_BUILTINS=1) + if let Some(ref filename) = self.config.file { + if let Ok(code) = fs::read_to_string(filename) { + // Scan first 128 lines for directives + for (i, line) in code.lines().take(128).enumerate() { + let l = line.trim(); + if !(l.starts_with("//") || l.starts_with("#!") || l.is_empty()) { + // Stop early at first non-comment line to avoid scanning full file + if i > 0 { break; } + } + // Shebang with envs: handled by shell normally; keep placeholder + if let Some(rest) = l.strip_prefix("//") { let rest = rest.trim(); + if let Some(dir) = rest.strip_prefix("@env ") { + if let Some((k,v)) = dir.split_once('=') { + let key = k.trim(); let val = v.trim(); + if !key.is_empty() { std::env::set_var(key, val); } + } + } else if rest == "@jit-debug" { + std::env::set_var("NYASH_JIT_EXEC", "1"); + std::env::set_var("NYASH_JIT_THRESHOLD", "1"); + std::env::set_var("NYASH_JIT_EVENTS", "1"); + std::env::set_var("NYASH_JIT_EVENTS_COMPILE", "1"); + std::env::set_var("NYASH_JIT_EVENTS_RUNTIME", "1"); + std::env::set_var("NYASH_JIT_SHIM_TRACE", "1"); + } else if rest == "@plugin-builtins" { + std::env::set_var("NYASH_USE_PLUGIN_BUILTINS", "1"); + } else if rest == "@jit-strict" { + std::env::set_var("NYASH_JIT_STRICT", "1"); + std::env::set_var("NYASH_JIT_ARGS_HANDLE_ONLY", "1"); + } + } + } + } + } + + // If strict mode requested via env, ensure handle-only shim behavior is enabled + if std::env::var("NYASH_JIT_STRICT").ok().as_deref() == Some("1") { + if std::env::var("NYASH_JIT_ARGS_HANDLE_ONLY").ok().is_none() { + std::env::set_var("NYASH_JIT_ARGS_HANDLE_ONLY", "1"); + } + } + // 🏭 Phase 9.78b: Initialize unified registry runtime::init_global_unified_registry(); diff --git a/src/runtime/plugin_loader_v2.rs b/src/runtime/plugin_loader_v2.rs index 83bd5e8b..a8f5365e 100644 --- a/src/runtime/plugin_loader_v2.rs +++ b/src/runtime/plugin_loader_v2.rs @@ -408,6 +408,19 @@ impl PluginBoxV2 { } for (idx, a) in args.iter().enumerate() { + // Coerce commonly-used plugin boxed primitives to builtin primitives when schema is weak (Name) + // Example: Plugin IntegerBox passed to ArrayBox.push(value) should encode as I64 + let mut enc_ref: &Box = a; + let mut enc_owned: Option> = None; + if let Some(p) = a.as_any().downcast_ref::() { + if p.box_type == "IntegerBox" { + // Read integer value via get(); on success, encode that value instead of a handle + if let Ok(val_opt) = self.invoke_instance_method("IntegerBox", "get", p.inner.instance_id, &[]) { + if let Some(val_box) = val_opt { enc_owned = Some(val_box); enc_ref = enc_owned.as_ref().unwrap(); } + } + } + // Future: StringBox/BoolBox/F64 plugin-to-primitive coercions + } // If schema exists, validate per expected kind if let Some(exp) = expected_args.as_ref() { let decl = &exp[idx]; @@ -419,17 +432,17 @@ impl PluginBoxV2 { if category.as_deref() != Some("plugin") { return Err(BidError::InvalidArgs); } - if a.as_any().downcast_ref::().is_none() { + if enc_ref.as_any().downcast_ref::().is_none() { return Err(BidError::InvalidArgs); } } "string" => { - if a.as_any().downcast_ref::().is_none() { + if enc_ref.as_any().downcast_ref::().is_none() { return Err(BidError::InvalidArgs); } } "int" | "i32" => { - if a.as_any().downcast_ref::().is_none() { + if enc_ref.as_any().downcast_ref::().is_none() { return Err(BidError::InvalidArgs); } } @@ -442,8 +455,8 @@ impl PluginBoxV2 { } crate::config::nyash_toml_v2::ArgDecl::Name(_) => { // Back-compat: allow common primitives (string or int) - let is_string = a.as_any().downcast_ref::().is_some(); - let is_int = a.as_any().downcast_ref::().is_some(); + let is_string = enc_ref.as_any().downcast_ref::().is_some(); + let is_int = enc_ref.as_any().downcast_ref::().is_some(); if !(is_string || is_int) { eprintln!("[PluginLoaderV2] InvalidArgs: expected string/int for {}.{} arg[{}]", box_type, method_name, idx); @@ -454,32 +467,31 @@ impl PluginBoxV2 { } // Plugin Handle (BoxRef): tag=8, size=8 - if let Some(p) = a.as_any().downcast_ref::() { + if let Some(p) = enc_ref.as_any().downcast_ref::() { eprintln!("[PluginLoaderV2] arg[{}]: PluginBoxV2({}, id={}) -> Handle(tag=8)", idx, p.box_type, p.inner.instance_id); crate::runtime::plugin_ffi_common::encode::plugin_handle(&mut buf, p.inner.type_id, p.inner.instance_id); continue; } - // Integer: prefer i32 - if let Some(i) = a.as_any().downcast_ref::() { - eprintln!("[PluginLoaderV2] arg[{}]: Integer({}) -> I32(tag=2)", idx, i.value); - let v = i.value as i32; - crate::runtime::plugin_ffi_common::encode::i32(&mut buf, v); + // Integer: use I64 (tag=3) for broad plugin compatibility + if let Some(i) = enc_ref.as_any().downcast_ref::() { + eprintln!("[PluginLoaderV2] arg[{}]: Integer({}) -> I64(tag=3)", idx, i.value); + crate::runtime::plugin_ffi_common::encode::i64(&mut buf, i.value); continue; } // Bool - if let Some(b) = a.as_any().downcast_ref::() { + if let Some(b) = enc_ref.as_any().downcast_ref::() { eprintln!("[PluginLoaderV2] arg[{}]: Bool({}) -> Bool(tag=1)", idx, b.value); crate::runtime::plugin_ffi_common::encode::bool(&mut buf, b.value); continue; } // Float (F64) - if let Some(f) = a.as_any().downcast_ref::() { + if let Some(f) = enc_ref.as_any().downcast_ref::() { eprintln!("[PluginLoaderV2] arg[{}]: Float({}) -> F64(tag=5)", idx, f.value); crate::runtime::plugin_ffi_common::encode::f64(&mut buf, f.value); continue; } // Bytes from Array - if let Some(arr) = a.as_any().downcast_ref::() { + if let Some(arr) = enc_ref.as_any().downcast_ref::() { let items = arr.items.read().unwrap(); let mut tmp = Vec::with_capacity(items.len()); let mut ok = true; @@ -495,7 +507,7 @@ impl PluginBoxV2 { } } // String: tag=6 - if let Some(s) = a.as_any().downcast_ref::() { + if let Some(s) = enc_ref.as_any().downcast_ref::() { eprintln!("[PluginLoaderV2] arg[{}]: String(len={}) -> String(tag=6)", idx, s.value.len()); crate::runtime::plugin_ffi_common::encode::string(&mut buf, &s.value); continue; @@ -503,7 +515,7 @@ impl PluginBoxV2 { // No schema or unsupported type: only allow fallback when schema is None if expected_args.is_none() { eprintln!("[PluginLoaderV2] arg[{}]: fallback stringify", idx); - let sv = a.to_string_box().value; + let sv = enc_ref.to_string_box().value; crate::runtime::plugin_ffi_common::encode::string(&mut buf, &sv); } else { return Err(BidError::InvalidArgs); @@ -830,6 +842,25 @@ impl PluginBoxV2 { finalized: std::sync::atomic::AtomicBool::new(false), }), }; + // Post-birth default initialization for known types + // StringBox: ensure empty content to avoid plugin-side uninitialized access when length() is called immediately + if box_type == "StringBox" { + let cfg_path = self.config_path.as_ref().map(|s| s.as_str()).unwrap_or("nyash.toml"); + if let Ok(toml_content) = std::fs::read_to_string(cfg_path) { + if let Ok(toml_value) = toml::from_str::(&toml_content) { + if let Some(box_conf) = self.config.as_ref().and_then(|c| c.get_box_config(lib_name, box_type, &toml_value)) { + if let Some(from_utf8) = box_conf.methods.get("fromUtf8") { + // TLV: header argc=1, then Bytes(tag=7, len=0) + let mut tlv = crate::runtime::plugin_ffi_common::encode_tlv_header(1); + crate::runtime::plugin_ffi_common::encode::bytes(&mut tlv, &[]); + let mut out = [0u8; 8]; + let mut out_len: usize = out.len(); + let _ = unsafe { (plugin.invoke_fn)(type_id, from_utf8.method_id, instance_id, tlv.as_ptr(), tlv.len(), out.as_mut_ptr(), &mut out_len) }; + } + } + } + } + } leak_tracker::init(); leak_tracker::register_plugin(&plugin_box.box_type, instance_id);