diff --git a/.gitignore b/.gitignore index e8fd3506..88433d8c 100644 --- a/.gitignore +++ b/.gitignore @@ -123,4 +123,15 @@ local_tests/ *.otf # Backup files -/backups/ \ No newline at end of file +/backups/ + +# 📚 論文関連フォルダ(作業中と公開用を分離) +# 作業中(WIP: Work In Progress)- Git追跡除外 +docs/research/papers-wip/ +docs/research/drafts/ +docs/research/notes/ + +# 査読中・未公開論文 - Git追跡除外 +docs/research/papers-under-review/ + +# 完成・公開済み論文は docs/research/papers-published/ に配置(Git追跡対象) \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 127ccc5d..8607292c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,36 @@ - 現在のタスク: docs/development/current/CURRENT_TASK.md - ドキュメントハブ: README.md - 🚀 **開発マスタープラン**: docs/development/roadmap/phases/00_MASTER_ROADMAP.md + - 📊 **JIT統計JSONスキーマ(v1)**: docs/reference/jit/jit_stats_json_v1.md + +## 🧱 先頭原則: 「箱理論(Box-First)」で足場を積む +Nyashは「Everything is Box」。実装・最適化・検証のすべてを「箱」で分離・固定し、いつでも戻せる足場を積み木のように重ねる。 + +- 基本姿勢: 「まず箱に切り出す」→「境界をはっきりさせる」→「差し替え可能にする」 + - 環境依存や一時的なフラグは、可能な限り「箱経由」に集約(例: JitConfigBox) + - VM/JIT/GC/スケジューラは箱化されたAPI越しに連携(直参照・直結合を避ける) +- いつでも戻せる: 機能フラグ・スコープ限定・デフォルトオフを活用し、破壊的変更を避ける + - 「限定スコープの足場」を先に立ててから最適化(戻りやすい積み木) +- AI補助時の注意: 「力づく最適化」を抑え、まず箱で境界を確立→小さく通す→可視化→次の一手 + +進め方(積み木が大きくなりすぎないために) +- 1) 足場は最小限(設定/境界/可視化/フォールバック)→ 2) すぐ実効カバレッジ拡大へ +- 返り値ヒントなどは「フックだけ」用意し、切替ポイントを1箇所に固定(将来1行切替) +- 同じ分岐を複数箇所に作らない(ParamKind→型決定は1関数に集約) +- 観測(統計/CFG)で回れるようにして「止まらない実装サイクル」を維持 + + +実践テンプレート(開発時の合言葉) +- 「箱にする」: 設定・状態・橋渡しはBox化(例: JitConfigBox, HandleRegistry) +- 「境界を作る」: 変換は境界1箇所で(VMValue↔JitValue, Handle↔Arc) +- 「戻せる」: フラグ・feature・env/Boxで切替。panic→フォールバック経路を常設 +- 「見える化」: ダンプ/JSON/DOTで可視化、回帰テストを最小構成で先に入れる + +標準化(例) +- 設定の箱: JIT/VMフラグは `JitConfigBox` に集約し、`apply()`でenvにも反映 +- 実行時読み: ホットパスでは `jit::config::current()` を参照(env直読みを排除) +- ハンドルの箱: JIT↔Hostは `HandleRegistry`(u64↔Arc)で疎結合化 +- 可視化: `NYASH_JIT_DUMP/…_JSON/NYASH_JIT_DOT` を活用し、変化を常に観測 Notes: - ここから先の導線は README.md に集約。Claude Codeくんがこのファイルを上書きしても最低限のリンクは保たれるよ。 @@ -91,6 +121,15 @@ python3 -m http.server 8010 **注意**: WASMビルドでは一部のBox(TimerBox、AudioBox等)は除外されます。 +### 🔧 JIT-direct(独立JIT)運用メモ(最小) +- 方針: 当面は read-only(書き込み命令はjit-directで拒否) +- 失敗の見える化: `NYASH_JIT_STATS_JSON=1` または `NYASH_JIT_ERROR_JSON=1` でエラーを1行JSON出力 +- すぐ試す例(Cranelift有効時) + - `examples/jit_direct_local_store_load.nyash`(最小Store/Load → 3) + - `examples/jit_direct_bool_ret.nyash`(Bool戻り → true) + - `examples/jit_direct_f64_ret.nyash`(F64戻り → 3.75, `NYASH_JIT_NATIVE_F64=1`) +- Box-First運用キット: `docs/engineering/box_first_enforcement.md`(PRテンプレ/CIガード/運用指標) + ## 📚 ドキュメント構造 ### 🎯 **最重要ドキュメント(開発者向け)** @@ -578,6 +617,22 @@ echo 'print("Hello Nyash!")' > local_tests/test_hello.nyash # 実用アプリテスト ./target/debug/nyash app_dice_rpg.nyash + +# JIT 実行フラグ(CLI) +./target/release/nyash --backend vm \ + --jit-exec --jit-stats --jit-dump --jit-threshold 1 \ + --jit-phi-min --jit-hostcall --jit-handle-debug \ + examples/jit_branch_demo.nyash +# 既存の環境変数でも可: +# NYASH_JIT_EXEC/NYASH_JIT_STATS(/_JSON)/NYASH_JIT_DUMP/NYASH_JIT_THRESHOLD +# NYASH_JIT_PHI_MIN/NYASH_JIT_HOSTCALL/NYASH_JIT_HANDLE_DEBUG + +# HostCallハンドルPoCの例 +./target/release/nyash --backend vm --jit-exec --jit-hostcall examples/jit_array_param_call.nyash +./target/release/nyash --backend vm --jit-exec --jit-hostcall examples/jit_map_param_call.nyash +./target/release/nyash --backend vm --jit-exec --jit-hostcall examples/jit_map_int_keys_param_call.nyash +./target/release/nyash --backend vm --jit-exec --jit-hostcall examples/jit_string_param_length.nyash +./target/release/nyash --backend vm --jit-exec --jit-hostcall examples/jit_string_is_empty.nyash ``` #### 🔌 **プラグインテスター(BID-FFI診断ツール)** diff --git a/docs/development/current/CURRENT_TASK.md b/docs/development/current/CURRENT_TASK.md index 86a6cfb3..99280d97 100644 --- a/docs/development/current/CURRENT_TASK.md +++ b/docs/development/current/CURRENT_TASK.md @@ -2,7 +2,14 @@ フェーズ10はJIT実用化へ!Core-1 Lowerの雛形を固めつつ、呼出/フォールバック導線を整えるよ。 -## ⏱️ 今日のサマリ(10_c実行経路の堅牢化+10_7分岐配線+GC/スケジューラ導線) +## 🆕 Quick Hot Update(2025-08-27-2) +- JitStatsBox: perFunction() 追加(関数単位の統計をJSON配列で取得) +- b1 PHI格子 強化(Compare/Const/Cast(TypeOp)/Branch/Copy/PHI/Store/Loadの固定点伝播) +- 独立JIT: `--jit-direct` に引数対応(`NYASH_JIT_ARGS` をMIR型にcoerce)、結果の型名も表示 +- b1 ABI切替の下地: `NYASH_JIT_ABI_B1_SUPPORT=1` でcapを強制ONできるように(将来のtoolchain更新に備えたスイッチ) +- リンク: JSONスキーマ(v1) → `docs/reference/jit/jit_stats_json_v1.md` + +## ⏱️ 今日のサマリ(10_c実行経路の堅牢化+10_7分岐/PHI/独立ABI+GC/スケジューラ導線) - 目的: JIT実行を安全に通す足場を仕上げつつ、GC/スケジューラ導線を整備し回帰検出力を上げる。 - 10_c: panic→VMフォールバック(`catch_unwind`)/ JIT経路のroot区域化 / Core-1 i64 param minimal pass(`emit_param_i64` + LowerCoreで供給)✅ 完了 - 10_4a/10_4b: GC導線 + Write-Barrier挿入(ArraySet/RefSet/BoxCall)、root API(enter/pin/leave) @@ -10,8 +17,81 @@ - 10_4d: STRICTバリア検証(CountingGc前後比較で漏れ即検出)✅ 完了(`NYASH_GC_BARRIER_STRICT=1`) - 10_6b: シングルスレ・スケジューラ(spawn/spawn_after/poll)、Safepointで`poll()`連携、`NYASH_SCHED_POLL_BUDGET`対応 ✅ 完了 - 10_7: JIT分岐配線(Cranelift)— MIR Branch/Jump→CLIFブロック配線、条件b1保持、分岐b1/`i64!=0`両対応(feature: `cranelift-jit`) + - 10_7: 最小PHI(単純ダイアモンド)— `NYASH_JIT_PHI_MIN=1` で有効、ブロック引数で合流値を受け渡し ✅ 初期対応 + - 10_7: JIT独立ABI導入 — `JitValue`(i64/f64/bool/handle) + VMValue↔JitValueアダプタ、TLS分離(legacy VMArgs / JIT Args)✅ 完了 - ベンチ: CLIベンチへJIT比較追加(ウォームアップあり)、`branch_return`ケース追加、スクリプト版`examples/ny_bench.nyash`追加(TimerBoxでops/sec) +### 🆕 追加アップデート(10_7進捗) +- ハンドルレジストリ + スコープ管理(JIT呼び出し単位で自動クリーンアップ)✅ +- HostCallブリッジ(ハンドル経由・read-only中心)✅ + - Array/Map/String: `length`/`isEmpty`、Map: `has`、String: `charCodeAt` + - Array/Map get/set/push/size: 既存PoCをハンドル経由に段階移行 +- 多値PHIの配線(then/else/jumpで順序つき引数渡し)✅ + - DOT可視化(`NYASH_JIT_DOT=path.dot` でCFG出力)✅ 初期対応 + - 引数ネイティブ型(10_7hの前段): MIRシグネチャから `Float→F64`, `Bool→I64(0/1)` を反映(`NYASH_JIT_NATIVE_F64/BOOL`)✅ 初期対応 +- ダンプの簡潔化(CFG/PHI要点表示、f64/boolネイティブ有効フラグ表示)✅ +- CFG/PHIダンプの詳細化(`NYASH_JIT_DUMP=1`)✅ 初期強化 + - 例: `phi: bb=3 slots=2 preds=1|2` に続けて `dst vX <- pred:val, ...` を列挙(合流値の源が一目で分かる) +- JIT設定の集約(Rust側)✅ 初期導入 + - `src/jit/config.rs` に `JitConfig` を追加し、RunnerからCLIと環境変数を一元反映 +- JitConfigBox 追加(10_7f)✅ 初期対応 + - `new JitConfigBox()` で `exec/stats/dump/phi_min/hostcall/.../threshold` を操作し `apply()` でenv反映 + - `toJson()/fromJson()` で再現性確保、`summary()` で状態確認 +- f64ネイティブ最小経路(`NYASH_JIT_NATIVE_F64=1`)✅ + - f64定数、F64同士の四則、F64戻りシグネチャ・トランポリン + - トランポリンは`desired_ret_is_f64`を捕捉(env依存のズレを排除)✅ +- Boolネイティブ最小経路(`NYASH_JIT_NATIVE_BOOL=1`)↗ 準備済み + - 比較はb1で保持し分岐に直接供給(既定)/戻りは当面i64(0/1)で正規化(将来b1戻りに拡張) +- 細かな修正: P2PBoxでtransportコールバック登録時の借用期間エラーを修正(lock解放順を明示) +- Bugfix: Cranelift `push_block_param_i64_at` で `switch_to_block` 未呼出によるpanicを修正(CFG/PHI経路の安定化)✅ + - LowerCore小型足場追加 ✅ + - ret_boolヒント: 返り値がBoolの関数で `builder.hint_ret_bool(true)` を呼ぶ切替点を用意(現状no-op、将来1行で切替) + - Castの最小下ろし: `I::Cast` を値供給+既知整数の継承で安全通過(副作用なし領域) + - 観測強化 ✅ + - b1正規化カウンタ: `b1_norm_count`(分岐条件・b1ブロック引数の正規化で加算) + - JitStatsBox: `{abi_mode, abi_b1_enabled, abi_b1_supported, b1_norm_count}` を `toJson()` で取得可能(BoxFactory登録済み) + - 統合JIT統計(テキスト/JSON)に `abi_mode/…/b1_norm_count` を追加 + - 便利CLI ✅ + - `--emit-cfg ` 追加(`NYASH_JIT_DOT` のCLI版。`NYASH_JIT_DUMP`も自動オン) +- 重要: Cranelift未有効時の挙動を是正 ✅ + - 以前: `NYASH_JIT_EXEC=1` でJITスタブが0を返すことがあり、結果が変わり得た + - 変更: Cranelift未有効では「未コンパイル扱い」にしてVMへ完全フォールバック(JIT有効でも結果は不変) + +- 独立JITモード ✅ 追加 + - `--jit-direct`: VM実行ループを介さず、JITエンジンで `main` を直接コンパイル実行(Cranelift有効時) + - `--jit-only`: フォールバック禁止(JIT未コンパイル/失敗は即エラー)— 独立性検証に最適 + +- b1 PHIの型/格子解析 強化 ✅ + - Compare/Const Bool起点+Copy/PHIに加え、Load/Storeヒューリスティックを導入(固定点伝播) + - PHI統計は常時計測(`phi_total_slots`/`phi_b1_slots`)→ JSON/ダンプ一致 + +- 関数単位JIT統計 ✅ + - LowerCore→JitEngine→JitManagerで `phi_total/phi_b1/ret_bool_hint/hits/compiled/handle` を集約 + - `JitStatsBox.summary()` に `perFunction` 配列を追加(関数ごとの明細) + +- f64 E2Eと簡易ベンチ(Cranelift向け)✅ 追加 + - 例: `examples/jit_f64_e2e_add_compare.nyash` / ベンチ: `examples/ny_bench_f64.nyash` + - 注意: VM単体ではf64演算/比較は未対応。`--features cranelift-jit` + `NYASH_JIT_EXEC=1 NYASH_JIT_NATIVE_F64=1` で実行 + +- JSONスキーマの文書化 ✅ + - 参照: `docs/reference/jit/jit_stats_json_v1.md`(version=1) + - 統一JSON/Box JSONともに `version` フィールドを付与 + +- f64 E2Eと簡易ベンチ(Cranelift向け)✅ 追加 + - 例: `examples/jit_f64_e2e_add_compare.nyash` + - ベンチ: `examples/ny_bench_f64.nyash` + - 注意: VM単体ではf64演算/比較は未対応。`--features cranelift-jit` + `NYASH_JIT_EXEC=1 NYASH_JIT_NATIVE_F64=1` で実行 + +- JSONスキーマの文書化 ✅ 追加 + - 参照: `docs/reference/jit/jit_stats_json_v1.md`(version=1) + - 統一JSON/Box JSONともに `version` フィールドを付与 + +- PHI統計(常時計測)✅ + - `phi_total_slots`/`phi_b1_slots` をLowerCoreで常時計測し、JSONに出力 + +- Cranelift互換性修正 ✅ + - b1シグネチャを使用せず、戻りはi64に正規化(`select(b1,1,0)`)。`types::B1`/`bint` 非依存でビルド安定化 + ### 直近タスク(小さく早く) 1) 10_b: Lower/Core-1 最小化(進行中 → ほぼ完了) - IRBuilder抽象 + `NoopBuilder`(emit数カウント)✅ 完了 @@ -20,25 +100,96 @@ - Engine.compile: builder選択(feature連動)+Lower実行+JIT handle発行✅ 完了 - JIT関数テーブル(stub: handle→ダミー関数)✅ 完了 - 残: 最小emit(const/binop/ret)をCLIFで生成し、関数ポインタをテーブル登録(feature有効時) - → 実装: CraneliftBuilderでi64用の`const/binop/ret`を生成し、JIT関数テーブルへクロージャとして登録完了(args未対応・i64専用) + → 実装: CraneliftBuilderでi64用の`const/binop/ret`を生成し、JIT関数テーブルへクロージャとして登録完了(i64引数0〜6個対応/bool,f64はi64正規化)✅ 更新 2) 10_c: 呼出/フォールバック(最小経路)✅ 完了 - VM側の疑似ディスパッチログ(compiled時/実行時ログ)✅ - JIT実行→`VMValue`返却、panic時VMフォールバック ✅(`engine.execute_handle`で`catch_unwind`) - Core-1最小: i64 param/return、Const(i64/bool→0/1)、BinOp/Compare/Return ✅ - HostCall最小(Array/Map: len/get/set/push/size)ゲート`NYASH_JIT_HOSTCALL=1` ✅ - Branch/JumpはCranelift配線導入済み(feature `cranelift-jit`)。副作用命令は未lowerのためVMへフォールバック -3) 10_7: 分岐配線(Cranelift)— 進捗中 +3) 10_7: 分岐/PHI/独立ABI(Cranelift)— 進捗中 - LowerCore: BB整列・マッピング→builderの`prepare_blocks/switch/seal/br_if/jump`呼出 ✅ - CraneliftBuilder: ブロック配列管理、`brif/jump`実装、条件b1/`i64!=0`両対応 ✅ - 最小PHI(単純ダイアモンド)導入(`NYASH_JIT_PHI_MIN=1`ガード)✅ 初期対応 - - 残: 副作用命令の扱い方針(当面VMへ)、CFG可視化の拡張(`NYASH_JIT_DUMP=1`) + - JIT独立化: `JitValue` ABI + 変換アダプタ + TLS分離でVM内部へ非依存化 ✅ 完了 + - 残: 副作用命令の扱い方針(当面VMへ)、CFG可視化の追加改善(ブロック引数の整形など) + - 検証: `--features cranelift-jit` 有効ビルドで `examples/jit_branch_demo.nyash` / `jit_phi_demo.nyash` がJIT経路で正常動作を確認(`NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1`)✅ + +### 🆕 追加(2025-08-28) +- JitStatsBox: `perFunction()` をVMディスパッチへ実装(JSON配列で name/hits/phi_total/phi_b1/ret_bool_hint/compiled/handle を返却)✅ +- PHI(b1)読出しの安定化(最小)✅ + - `NYASH_JIT_PHI_MIN=1` 有効時、ブールと推定されたPHIは b1 としてブロック引数を読み出し(分岐に直結) + - 分岐条件をブール格子のシードに追加(pre-scanで `condition` をbooleanとして扱う) +- デモ更新: `examples/jit_stats_summary_demo.nyash` に `perFunction()` 出力を追加 ✅ +- jit-direct(独立JIT)足場の強化 ✅ + - read-onlyガード(WriteHeap検出で明示エラー)/ 統一エラーJSON(`NYASH_JIT_ERROR_JSON=1`) + - 最小E2E: `examples/jit_direct_local_store_load.nyash` / `jit_direct_bool_ret.nyash` / `jit_direct_f64_ret.nyash` + - 参考: Box-First運用キット `docs/engineering/box_first_enforcement.md`(PR/CIはアドバイザリ導入) + +## 🔜 次:Phase 10.9 Builtin-Box JIT(Box-Firstで最小から) + +目的: VMとJITでビルトインBoxの読み取り系を同等動作にし、後続(生成/書き込み)へ進む足場を固める。 + +### 必要な箱(最小セット) +- JitPolicyBox(ポリシー箱) + - 役割: read-onlyゲート/書き込み検出/HostCall許可リストの一本化(1箇所で切替) + - 経路: runner/jit-direct/engine/lower が参照(env直読みはしない) +- JitEventsBox(観測箱) + - 役割: compile/execute/fallback/trapを JSONL へAppend({fn, abi_mode, reason, ms}) + - 経路: 既存statsと共存。回帰の目視を容易に。 +- HostcallRegistryBox(read-only) + - 役割: 許可HostCallと引数/戻りの種別を宣言。型検査の単一点にする。 + - 初期: String.length/isEmpty/charCodeAt, Array.length/isEmpty/get, Map.size/has +- FrameSlotsBox(スロット箱) + - 役割: ptr→slot の管理と型注釈(今はi64のみ)。LowerCoreから委譲。 +- CallBoundaryBox(呼出し境界箱/薄い) + - 役割: JIT↔JIT/JIT↔VM呼出しの単一点(将来の多関数対応の足場)。今は型変換の箱だけ用意。 + +最小原則: まず箱を置く(no-op/ログだけでもOK)→ 切替点が1箇所になったら機能を足す。 + +### 実装計画(小さい積み木で順に) +1) Week 10.9-α(足場) + - JitPolicyBox v0: read-only/HostCall whitelist を箱へ移動(runnerの散在チェックを統合) + - JitEventsBox v0: compile/executeのJSONLイベント(オプトイン) + - CURRENT_TASK/CLAUDEへポリシーと使い方を簡潔追記 +2) Week 10.9-β(読み取り系カバレッジ) + - HostcallRegistryBox v0: String/Array/Mapの読み取りAPIを登録・型検査 + - LowerCore: BoxCallのread-only経路をRegistry参照に切替(散在ロジックの削減) + - E2E: length/isEmpty/charCodeAt/get/size/has(jit-direct + VMで一致) +3) Week 10.9-γ(生成の足場) + - CallBoundaryBox v0: JIT→VM(new演算子など)を一旦VMへ委譲する薄い箱 + - new StringBox/IntegerBox/ArrayBox の最小経路(jit-directは方針次第で拒否でも可) +4) Week 10.9-δ(書き込みの導線のみ) + - JitPolicyBox: write許可のスイッチを箱に実装(既定OFF) + - LowerCore: 書き込み命令はPolicy参照で拒否/委譲/許可(1箇所で判断) + +### 成功判定(DoD) +- 機能: 読み取り系ビルトインBoxがJIT経路でVMと一致(上記API) +- 箱: Policy/Events/Registryが1箇所で参照される(散在ロジック解消) +- 観測: JSONLイベントが最低1件以上出力(明示opt-in) +- 回帰: `jit-direct` read-only方針が維持され、拒否理由がイベント/JSONで判読可 + +### リスクと箱での緩和 +- HostCallの型崩れ: HostcallRegistryBoxで型検査→不一致はPolicy経由で明示拒否 +- 方針変更の揺れ: JitPolicyBoxのフラグだけで切替可能(コード散在を回避) +- 観測不足: JitEventsBoxのイベント粒度を先に用意(必要最小限でOK) + 備考(制限と次の着手点) -- 返り値はi64(VMValue::Integer)に限定。f64はconst最小emit、boolはi64 0/1へ正規化(分岐条件入力に対応) -- 引数はi64のみ最小パス。複数引数はparamマッピングで通過、非i64は未対応 → 次対応 +- 返り値はi64(VMValue::Integer)に限定。f64/boolは設計着手(ダンプで有効フラグ観測のみ) +- 引数はi64最小パス。複数引数はparamマッピングで通過、非i64はハンドル/正規化で暫定対応 - Branch/JumpのCLIF配線は導入済み(feature `cranelift-jit`)。条件はb1で保持し、必要に応じて`i64!=0`で正規化 - 副作用命令(print等)はJIT未対応のためVMへ委譲(安全性優先) -- JIT/VM統合統計(フォールバック率/時間の一括出力)未統合 → 次対応 +- JIT/VM統合統計(フォールバック率/時間の一括出力)✅ 実装済み(最小) + - `maybe_print_jit_unified_stats()` により、テキスト/JSON(`NYASH_JIT_STATS_JSON=1`)でsites/compiled/hits/exec_ok/trap/fallback_rate/handlesを出力 + +## 📦 箱作戦(JIT独立化の方針) +- ABIの箱: `JitValue`(I64/F64/Bool/Handle)でJIT入出力を統一、VM型に非依存 +- 変換アダプタ: VMValue↔JitValueはアダプタに集約、境界は必ずアダプタ経由 +- TLS分離: legacy VMArgs(既存HostCall用)とJIT Args(新ABI)を分離し段階移行 +- HostCall独立化(計画): Handle(u64) + ハンドルレジストリ導入でJIT→ホストを完全独立(VM型非参照) +- 設定の箱(進捗): Rust側で`JitConfig`を導入しenv/CLI設定を集約(Nyash側Box化は後続) +- 可視化: `NYASH_JIT_DUMP=1`でCFG/ブロック引数を可視化し回帰検出力を強化 ### すぐ試せるコマンド ```bash @@ -67,6 +218,17 @@ NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 ./target/release/nyash --backend vm examp ./target/release/nyash --benchmark --iterations 200 ``` +#### HostCall(ハンドルPoC) +```bash +./target/release/nyash --backend vm --jit-exec --jit-hostcall examples/jit_array_param_call.nyash +./target/release/nyash --backend vm --jit-exec --jit-hostcall examples/jit_map_param_call.nyash +./target/release/nyash --backend vm --jit-exec --jit-hostcall examples/jit_map_int_keys_param_call.nyash +./target/release/nyash --backend vm --jit-exec --jit-hostcall examples/jit_map_has_int_keys.nyash +./target/release/nyash --backend vm --jit-exec --jit-hostcall examples/jit_string_param_length.nyash +./target/release/nyash --backend vm --jit-exec --jit-hostcall examples/jit_string_is_empty.nyash +./target/release/nyash --backend vm --jit-exec --jit-hostcall examples/jit_string_charcode_at.nyash +``` + ## 現在の地図(Done / Next) ### ✅ 完了(Phase 9.79b) @@ -92,12 +254,13 @@ NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 ./target/release/nyash --backend vm examp - Lower emitのテスト雛形 - CLIFダンプ/CFG表示(`NYASH_JIT_DUMP=1`) - VM `--vm-stats` とJIT統計の統合 + → 実装済み(VM終了時にJIT統合サマリ出力。JSON出力も可) ### 残タスク(箇条書き) - 10_c: - CLIF: Branch/Jumpの実ブロック配線、Compare結果の適用確認 - 返り値/引数の型拡張(bool/f64)、複数引数の網羅 - - JIT/VM統合統計(フォールバック率/時間の一括出力) + - JIT/VM統合統計(フォールバック率/時間の一括出力)✅ 済(最小) - 10_4c: - リーチャビリティ観測の深さ拡張(depth=2→N)と軽量ダンプ - (将来)実Mark/TraverseのPoC(解放はしない) @@ -106,13 +269,32 @@ NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 ./target/release/nyash --backend vm examp - 10_6b: - スケジューラ: poll予算の設定ファイル化、将来のscript API検討(継続) - 10_7: - - 最小PHI(単純ダイアモンド)の導入(`NYASH_JIT_PHI_MIN=1`ガード) - - IRBuilder APIの整理(block param/分岐引数の正式化)とCranelift実装の安定化 + - Hostハンドルレジストリ導入(JIT側はHandleのみを見る) + - IRBuilder APIの整理(block param/分岐引数の正式化)とCranelift実装の安定化(暫定APIの整形) - 副作用命令のJIT扱い(方針: 当面VMへ、将来はHostCall化) - - CFG検証と`NYASH_JIT_DUMP=1`でのCFG可視化 + - CFG検証と`NYASH_JIT_DUMP=1`でのCFG可視化(ブロックと引数のダンプ)✅ 初期強化済み(更なる整形・網羅は継続) + - JitConfigBoxで設定の箱集約(env/CLI整合、テスト容易化)✅ Rust側の`JitConfig`導入済(Box化は後続) + - Bool戻り最小化: `hint_ret_bool`をCranelift側に配線し、b1→i64とb1ネイティブを切替可能に + - Compare/BinOpの副作用なし領域の拡大(Copy/Const/Castは開始済み、And/Orは当面VMフォールバック) + - PHI(b1)の扱い整理: ブロック引数のb1対応を正式化(i64 0/1正規化とb1の両立) + - 統計/ダンプの整備: `b1_norm_count`のJSON/テキスト両方での検証手順をドキュメント化 + - b1 PHI格子の精度UP(簡易エイリアス/型注釈の取り込み) + - ネイティブb1 ABI: ツールチェーンcap連動で返り/引数b1署名への切替(切替点は一本化済) + - VMのf64演算/比較パリティ(10.8へ移送) + - JitStatsBox: `perFunction()` の箱API追加(summaryを介さず単体取得) + - b1 PHIタグの頑健化(Load/Store越しの由来推定を型/格子解析で補強) + - VMのf64演算/比較パリティ(10.8へ移送予定) + - JSONスキーマのversion管理とサンプル更新(`v1`整理済み、今後の拡張計画をドキュメント化) - ベンチ: - `examples/ny_bench.nyash`のケース追加(関数呼出/Map set-get)とループ回数のenv化 +### 🛠️ メンテ・ビルドノート(環境依存) +- WSL配下(`/mnt/c/...`)で `cargo build` 時に `Invalid cross-device link (os error 18)` が発生する場合あり + - 対策: リポジトリをLinux FS側(例: `~/work/nyash`)に配置/`CARGO_TARGET_DIR` と `TMPDIR` を同一FS配下へ設定 + - 備考: リンク動作とFS差分が原因のため、コード修正ではなく環境調整で解決 +- Codex CLIの実行権限・サンドボックス設定忘れで「ビルドできない」誤検知になる場合あり + - 対策: `codex --ask-for-approval never --sandbox danger-full-access` を付与して起動する(この指定忘れが原因だったケースあり) + --- 10_d まとめ(完了) diff --git a/docs/development/current/JIT_10_7_known_issues.txt b/docs/development/current/JIT_10_7_known_issues.txt new file mode 100644 index 00000000..bd6b84c4 --- /dev/null +++ b/docs/development/current/JIT_10_7_known_issues.txt @@ -0,0 +1,52 @@ +JIT Phase 10_7 — Known Issues and Future Refinements (2025-08-27) + +Scope: LowerCore(Core-1), Branch/PHI wiring, ABI(min), Stats/CFG dump + +1) b1 PHI tagging heuristics +- Symptom: (b1) tag may not appear for some PHI nodes when inputs are routed via variables (Load/Store) or multi-step copies. +- Current mitigation: + - Pre-scan: mark Compare/Const Bool as boolean producers. + - Propagate through Copy; close over PHI if all inputs are boolean-like. + - CFG dump heuristic: mark (b1) if pre-analysis true OR all inputs look boolean OR the PHI output is used as branch condition. +- Residual risk: Complex IR (loads/stores, deep copies, optimizer rewrites) can hide provenance; tag may still be missed. +- Future: Track boolean type via MIR type system or a dedicated lattice in analysis pass; extend provenance across Load/Store when safe. + +2) f64 arithmetic/compare on VM backend +- Symptom: VM path errors on f64 BinOp/Compare (e.g., Add/>, mixed types). +- Cause: VM does not yet implement floating-point arithmetic/compare ops. +- Current mitigation: Examples using f64 are marked for Cranelift-enabled JIT environments; VM returns type errors as expected. +- Future: Implement f64 BinOp/Compare in VM or add auto-promotion layer consistent with JIT behavior. + +3) JIT execution stub semantics +- Symptom: Previously, enabling NYASH_JIT_EXEC could alter results via stub returns when Cranelift was not enabled. +- Fix: Stub disabled; compilation skipped without Cranelift, full VM fallback retains semantics. +- Future: None (resolved). Keep behavior documented. + +4) ret_bool_hint_count visibility +- Symptom: Counter can remain 0 when MIR signatures don’t mark Bool return even if code returns boolean values via VM-level boxes. +- Cause: Counter increments on MIR signature Bool return detection; not on dynamic boolean returns. +- Future: Consider counting dynamic sites or adding per-function attributes post-inference. + +5) PHI dump dependency on phi_min +- Symptom: Detailed PHI dump (with (b1) tags) only shows when NYASH_JIT_PHI_MIN=1. +- Current stance: Intended; PHI path is opt-in while stabilizing. +- Future: Always-on summary rows possible; keep detail behind flag to reduce noise. + +6) Stats scope and sources +- Symptom: JitStatsBox and unified stats currently reflect process-local counters; function-level detail via top5 only. +- Future: Add per-function structured stats via Box API; expose compilation state/timings when available. + +7) Toolchain capability gating (b1 ABI) +- Symptom: Native boolean (b1) return in signatures remains disabled (cap false). +- Current stance: Switch point centralized; flips on when toolchain supports b1 in signatures. +- Future: Periodically probe toolchain; add CI check to toggle automatically. + +8) Warning noise (unexpected cfg: llvm) +- Symptom: Warnings about unknown `llvm` feature in conditional cfg. +- Cause: Feature not declared in Cargo.toml; code guards are placeholders. +- Future: Add feature or silence warnings for non-supported builds. + +9) Documentation sync +- Symptom: Some flags visible via CLI and boxes; ensure README/CURRENT_TASK stay aligned. +- Future: Add short “JIT quick flags” section with examples in docs. + diff --git a/docs/development/current/jit-enhancements-20250827.md b/docs/development/current/jit-enhancements-20250827.md new file mode 100644 index 00000000..b50f521b --- /dev/null +++ b/docs/development/current/jit-enhancements-20250827.md @@ -0,0 +1,68 @@ +# JIT機能拡張 - 2025年8月27日 + +## ChatGPT5による最新実装 + +### 1. PHI可視化強化 +- CFGダンプに「PHIがboolean(b1)かどうか」を明示表示 +- 例: `dst v12 (b1) <- 1:8, 2:9` +- PHI命令の型情報が一目で分かるように改善 + +### 2. JIT統計の上位表示 +- 統合JIT統計(JSON)に `top5` を追加 + - 関数名 + - ヒット数 + - compiled状態 + - handle番号 +- 例実行で `top5` に `main` が入ることを確認 + +### 3. 返り値ヒントの観測 +- `ret_bool_hint_count` をJIT統計に追加 +- JitStatsBox/統合JIT統計の両方で確認可能 +- ブール返り値の最適化機会を可視化 + +### 4. 新しい例の追加 + +#### `examples/jit_stats_bool_ret.nyash` +- 統計JSONをプリントする最小デモ +- 最後にブールを返す +- JIT統計の動作確認用 + +#### `examples/jit_mixed_f64_compare.nyash` +- f64比較のデモ +- **注意**: VMのf64演算/比較未対応のため、Cranelift有効環境向けサンプル + +## 使い方(観測) + +### 統計+JSON出力 +```bash +NYASH_JIT_STATS=1 NYASH_JIT_STATS_JSON=1 NYASH_JIT_THRESHOLD=1 \ + ./target/release/nyash --backend vm examples/jit_stats_bool_ret.nyash +``` + +JSONに以下が出力される: +- `abi_mode` +- `b1_norm_count` +- `ret_bool_hint_count` +- `top5` + +### CFG/PHIダンプ +```bash +NYASH_JIT_DUMP=1 ./target/release/nyash --backend vm examples/phi_bool_merge.nyash +``` +- b1 PHIには `(b1)` タグが付与される + +## 注意事項 + +- VMのf64演算/比較は未対応 +- `jit_mixed_f64_compare.nyash` はCranelift有効環境(JIT実行)での確認用 +- VMでの実行はエラーになる + +## 実装の意義 + +これらの機能拡張により: +1. **可視性の向上**: PHI命令の型情報が明確に +2. **統計の充実**: top5による頻繁に呼ばれる関数の把握 +3. **最適化ヒント**: ブール返り値のカウントによる最適化機会の発見 +4. **デバッグ支援**: より詳細な情報による問題解析の容易化 + +Box-First方法論の「観測箱」の具現化として、これらの機能は論文の実証例となる。 \ No newline at end of file diff --git a/docs/development/roadmap/phases/phase-10.1/chatgpt5_integrated_plan.md b/docs/development/roadmap/phases/phase-10.1/chatgpt5_integrated_plan.md new file mode 100644 index 00000000..d7267cbf --- /dev/null +++ b/docs/development/roadmap/phases/phase-10.1/chatgpt5_integrated_plan.md @@ -0,0 +1,239 @@ +# Phase 10.1 - Python統合計画(ChatGPT5高速開発版) +最終更新: 2025-08-27 + +## 🚀 概要:2週間での爆速実装 + +ChatGPT5の最小Box設計により、元の1ヶ月計画を**2週間**に圧縮。Nyash既存アーキテクチャ(MirBuilder 100%実装済み、HandleRegistry 80%実装済み)を最大活用。 + +## 📦 ChatGPT5の6つの必須Box(最小実装) + +### 1. **PythonParserBox** - CPython AST取得(3日) +```rust +// 既存: pyo3統合済み +// 追加: JSON出力とバージョン固定 +pub struct PythonParserBox { + base: BoxBase, + py_helper: Arc>, + version: String, // "3.11"固定 +} + +// メソッド(最小限) +- parse_to_json(src: String) -> String // ast.parse() → JSON +- get_version() -> String // "3.11" +``` + +### 2. **Py2NyASTBox** - AST変換(3日) +```rust +// 新規実装 +pub struct Py2NyASTBox { + base: BoxBase, + normalizer: AstNormalizer, +} + +// メソッド(制御フロー正規化) +- convert(json: String) -> NyashAst +- normalize_for_else(ast: &mut PyAst) // for/else → if分岐 +- normalize_comprehensions(ast: &mut PyAst) +``` + +### 3. **MirBuilderBox** - MIR生成(0日 - 既存活用) +```rust +// 既存実装100%活用 +// 追加: Python由来フラグのみ +pub struct MirBuilderBox { + // 既存フィールド + is_python_origin: bool, // 追加 +} +``` + +### 4. **BoundaryBox** - 型変換(2日) +```rust +// Python版のHandleRegistry相当 +pub struct BoundaryBox { + base: BoxBase, + handle_registry: Arc>, // 既存80%活用 +} + +// メソッド +- py_to_jit(py_val: PyValBox) -> JitValue +- jit_to_py(jit_val: JitValue) -> PyValBox +- register_handle(obj: Arc) -> u64 +``` + +### 5. **PyRuntimeBox** - 実行制御(2日) +```rust +pub struct PyRuntimeBox { + base: BoxBase, + fallback_stats: FallbackStats, +} + +// メソッド(関数単位フォールバック) +- execute_function(name: &str, args: Vec) -> JitValue +- should_fallback(func_ast: &PyAst) -> bool // Phase1機能判定 +- fallback_to_cpython(code: &str) -> PyObject +``` + +### 6. **ObservabilityBox** - 統計収集(1日) +```rust +// 既存のJIT統計システム(70%実装済み)を拡張 +pub struct ObservabilityBox { + base: BoxBase, + stats_collector: StatsCollector, +} + +// JSONLフォーマット出力 +- log_attempt(module: &str, func: &str, compiled: bool, reason: Option<&str>) +- output_jsonl() -> String +``` + +## 🗓️ 実装タイムライン(2週間) + +### Week 1: 基盤実装(7日) +- **Day 1-3**: PythonParserBox実装 + - pyo3統合(既存活用) + - Python 3.11固定 + - JSON出力実装 + +- **Day 4-6**: Py2NyASTBox実装 + - 制御フロー正規化 + - for/else, while/else変換 + - Phase1機能のみサポート + +- **Day 7**: ObservabilityBox実装 + - 既存JIT統計拡張 + - JSONLフォーマット + +### Week 2: 統合と検証(7日) +- **Day 8-9**: BoundaryBox実装 + - HandleRegistry活用 + - 型変換ルール確立 + +- **Day 10-11**: PyRuntimeBox実装 + - 関数単位フォールバック + - CPython連携 + +- **Day 12-13**: 統合テスト + - Differential Testing + - ベンチマーク実行 + +- **Day 14**: ドキュメント・リリース + - 使用例作成 + - パフォーマンス測定 + +## 📊 既存アーキテクチャとの整合性 + +### 活用率 +- **MirBuilderBox**: 100%(変更なし) +- **HandleRegistry**: 80%(BoundaryBoxで再利用) +- **JIT統計**: 70%(ObservabilityBoxで拡張) +- **VM/JIT実行**: 100%(そのまま使用) + +### 新規実装 +- **PythonParserBox**: 30%(pyo3部分は既存) +- **Py2NyASTBox**: 100%新規 +- **PyRuntimeBox**: 100%新規 + +## 🎯 Phase 1でサポートする機能(Codex先生推奨) + +### 必須実装 +1. **LEGB + locals/freevars** - スコーピング規則 +2. **デフォルト引数の評価タイミング** - 定義時評価 +3. **イテレータベースのfor文** +4. **for/else + while/else** +5. **Python真偽値判定** +6. **短絡評価** + +### サポートする文 +- def(関数定義) +- if/elif/else +- for(else節対応) +- while(else節対応) +- break/continue +- return + +### サポートする式 +- 算術演算子(+,-,*,/,//,%) +- 比較演算子(==,!=,<,>,<=,>=) +- 論理演算子(and,or,not) +- 関数呼び出し +- リテラル(数値/文字列/bool) + +## 📈 成功指標(2週間後) + +### 定量的 +- **関数コンパイル率**: 70%以上(Phase 1機能) +- **実行速度**: 純Pythonループで2倍以上 +- **メモリ効率**: CPython比50%削減 + +### 定性的 +- **統計可視化**: JSONL形式で全実行を記録 +- **デバッグ容易性**: 関数単位でフォールバック理由明示 +- **将来拡張性**: Phase 2-4への明確な道筋 + +## 🔧 実装例(最終形) + +```nyash +// Nyashから使用 +local py = new PythonParserBox() +local converter = new Py2NyASTBox() +local builder = new MirBuilderBox() +local runtime = new PyRuntimeBox() +local stats = new ObservabilityBox() + +// Pythonコードをコンパイル・実行 +local code = "def fib(n): return n if n <= 1 else fib(n-1) + fib(n-2)" +local json_ast = py.parse_to_json(code) +local ny_ast = converter.convert(json_ast) +local mir = builder.build(ny_ast) + +// 実行(自動的にJIT/VMで高速化) +local result = runtime.execute_function("fib", [10]) +print(result) // 55 + +// 統計出力 +print(stats.output_jsonl()) +// {"mod":"test","func":"fib","attempt":1,"jitted":true,"native":true} +``` + +## 🚨 重要な設計判断 + +### 1. 関数単位の境界 +- ファイル単位ではなく**関数単位**でコンパイル/フォールバック +- 未対応機能を含む関数のみCPython実行 + +### 2. Python 3.11固定 +- AST安定性の確保 +- 将来のバージョンアップは別Phase + +### 3. 箱境界の明確化 +- 各Boxは単一責任 +- 相互依存を最小化 +- テスト可能な粒度 + +### 4. 既存資産の最大活用 +- MirBuilder/VM/JITはそのまま使用 +- 新規実装は変換層のみ + +## 🎉 期待される成果 + +### 即時的効果(2週間後) +- Pythonコードの70%がNyashで高速実行 +- バグ検出力の飛躍的向上(Differential Testing) +- 統計による最適化ポイントの可視化 + +### 長期的効果 +- Python→Nyash→Native の世界初パイプライン確立 +- Nyash言語の成熟度向上 +- エコシステムの爆発的拡大 + +## 📝 次のステップ + +1. **Phase 10.7完了確認** - JIT統計JSONの安定化 +2. **PythonParserBox実装開始** - pyo3統合から着手 +3. **テストコーパス準備** - Python標準ライブラリから抜粋 + +--- + +**作成者**: Claude(Claude Code) +**承認者**: ChatGPT5(予定) +**開始予定**: Phase 10.7完了直後 \ No newline at end of file diff --git a/docs/development/roadmap/phases/phase-10/phase_10_7_master_plan.txt b/docs/development/roadmap/phases/phase-10/phase_10_7_master_plan.txt new file mode 100644 index 00000000..32e376c7 --- /dev/null +++ b/docs/development/roadmap/phases/phase-10/phase_10_7_master_plan.txt @@ -0,0 +1,135 @@ +Phase 10.7 — JIT CFG/PHI/ABI Hardening (Master Plan) + +Intent +- Deliver a practical, observable JIT path for control flow and minimal data flow while keeping VM as the safe fallback. +- Decompose into small, shippable sub-phases with flags and examples, minimizing blast radius. + +Related docs +- 10.7a details: phase_10_7a_jit_phi_cfg_and_abi_min.txt +- Current work log: docs/development/current/CURRENT_TASK.md +- Cranelift JIT backend notes: phase_10_cranelift_jit_backend.md + +Flags and CLI (common across 10.7) +- Env: NYASH_JIT_EXEC, NYASH_JIT_STATS, NYASH_JIT_STATS_JSON, NYASH_JIT_DUMP, NYASH_JIT_THRESHOLD, NYASH_JIT_PHI_MIN, NYASH_JIT_HOSTCALL +- CLI: --jit-exec --jit-stats --jit-stats-json --jit-dump --jit-threshold N --jit-phi-min --jit-hostcall + +Examples to validate +- examples/jit_branch_demo.nyash +- examples/jit_loop_early_return.nyash +- examples/jit_phi_demo.nyash +- examples/jit_array_param_call.nyash, jit_map_param_call.nyash (when hostcall is enabled) + +Sub-phases (10.7a → 10.7h) + +10.7a — Minimal branch/PHI/ABI (Done or in-flight) +- Branch wiring with b1 and i64!=0 normalization +- Minimal PHI: single-value diamond via block param +- Independent ABI (JitValue) with integer-first calling +- Panic-safe dispatch (catch_unwind) and VM fallback +- Minimal hostcall bridge (Array/Map) behind NYASH_JIT_HOSTCALL +- Observability: unified JIT summary/JSON, lower summary (argc/phi_min), CFG edge dump (phi_min) + +Acceptance: see 10.7a doc + +10.7b — PHI generalization and block-parameterization +- Support multiple PHIs in a merge block; pass N values via block params +- LowerCore: gather PHI inputs per successor, produce explicit arg lists; handle straight merges and simple loops +- IRBuilder API formalization: + - ensure_block_params_i64(index, count) + - br_if_with_args(then_idx, else_idx, then_n, else_n) + - jump_with_args(target_idx, n) +- CraneliftBuilder: append N I64 params per block, materialize current block params onto stack when requested +- Validation: multi-PHI diamonds, if-else chains, early returns; keep loops as simple as possible in 10.7b + +10.7c — Independent host-handle registry (JIT↔Host decoupling) +- Introduce JitHandleRegistry: u64 ↔ Arc +- JIT hostcalls accept/return only handles and PODs; VMValue↔JitValue boundary converts BoxRef <-> Handle via registry +- Rooting policy: enter/pin roots for handles crossing the boundary; leave on return +- Flags: NYASH_JIT_HOSTCALL=1 (still gated); add NYASH_JIT_HANDLE_DEBUG for dumps +- Deliverables: array/map operations via handles, no direct VMValue access on JIT side + +10.7d — Side-effect boundary and hostcall coverage expansion +- Keep side-effecting ops (print/IO) on VM for now; document deopt path +- Expand safe hostcalls (read-only or confined): + - String.len / slice basics (POD returns) + - Map.size; Map.get with integer and string keys (key boxing handled at boundary) +- Record effect categories on lowered calls for future optimizer (metadata only) + +10.7e — CFG diagnostics and visualization +- Add DOT export for CLIF CFG with block params and PHI bindings +- Env: NYASH_JIT_DOT=path.dot (produces per-function graph) +- Optional: ASCII CFG summary for CI logs +- Cross-check with VM-side MIR printer for block and PHI consistency + +10.7f — JitConfigBox (configuration as a Box) +- Encapsulate all JIT toggles into a runtime-managed box (JitConfigBox) +- Harmonize env/CLI with programmatic overrides for tests +- Feed config into JIT engine and lowerers (no direct env reads inside hot paths) +- Serialization: dump/load config JSON for reproducible runs + +10.7g — Stability, tests, and benchmarks +- Golden tests: ensure JIT/VM outputs match on curated programs +- Fallback ratio regression guard: alert when fallback_rate spikes beyond threshold +- JSON schema stability for stats; include version field +- Microbenchmarks: branch-heavy, phi-heavy, and hostcall-heavy cases; gated perf checks + +10.7h — Type widening for native ABI +- Add native F64 and Bool parameter/return paths in CLIF +- Condition handling: keep b1 where possible; map Bool to b1 cleanly +- Conversions: explicit i64<->f64 ops in lowerer where needed (no silent truncation) +- Update adapter and closure trampoline to select proper function signatures (arity×type shapes) + +Out of scope for 10.7 (shift to 10.8/10.9) +- Global register allocation strategies or codegen-level optimizations +- Deoptimization machinery beyond simple VM fallback +- Advanced exception propagation across JIT/VM boundary + +Risks and mitigation +- PHI N-arity correctness: introduce targeted unit tests over synthetic MIR +- Handle registry leaks: counting diagnostics, strict mode; tie roots to scope regions +- CLIF block-param misuse: deterministic block order + seal discipline + assertions in builder + +Verification checklist (roll-up) +- cargo check (± --features cranelift-jit) +- Run examples with JIT flags; diff with pure VM runs +- Inspect: unified JIT summary/JSON, lower logs, CFG dumps/DOT +- Leak/roots checks when NYASH_GC_TRACE=1/2/3 and strict barrier mode is on + +Suggested timeline (tentative) +- 10.7a: Minimal branch/PHI/ABI (done / in-flight) +- 10.7b: PHI generalization + builder API formalization (1–2 days) +- 10.7c: Host-handle registry PoC (1–2 days) +- 10.7d/e: Hostcall coverage + CFG DOT (2–3 days) +- 10.7f: JitConfigBox + integration (1 day) +- 10.7g/h: QA + type widening for f64/bool (2–4 days) + +10.7z — Follow-ups and Open Items (post-10.7) +- b1 PHI tagging robustness + - Problem: Provenance can be obscured by Load/Store and multi-step copies; (b1) tag may be missed in dumps. + - Action: + - Add a lightweight boolean-lattice analysis over MIR to classify boolean-producing values independent of path shape. + - Extend dump to include phi_summary JSON or structured rows when NYASH_JIT_STATS_JSON is on. + - Placement: 10.7g (stability/tests) — does not block 10.7 close. + +- VM f64 arithmetic/compare parity + - Problem: VM backend currently errors on f64 BinOp/Compare; JIT (Cranelift) supports f64 when enabled. + - Action: Implement f64 ops in VM or add consistent auto-promotion; add golden tests. + - Placement: 10.8 (VM parity/perf) — out-of-scope for 10.7. + +- Native b1 ABI in function signatures + - Problem: Toolchain capability for b1 return/params is currently disabled. + - Action: Keep centralized switch; add CI probe to flip automatically when supported; wire return path fully. + - Placement: 10.7h or later (gated by toolchain). + +- Stats/diagnostics polish + - Action: Version the unified JIT JSON schema; expose phi(b1) slot counts in JSON; enrich JitStatsBox with summary/topN (partially done). + - Placement: 10.7g. + +- Build warnings (unexpected cfg: llvm) + - Problem: Warning noise from unused/unknown cfg. + - Action: Declare feature flags in Cargo.toml or gate code behind existing features; optionally silence for non-supported builds. + - Placement: 10.7g (cleanup) — non-blocking. + +- Documentation sync + - Action: Add a "JIT quick flags" section with common env/CLI combos; ensure CURRENT_TASK and examples remain aligned. + - Placement: 10.7e (docs) — non-blocking. diff --git a/docs/development/roadmap/phases/phase-10/phase_10_7a_jit_phi_cfg_and_abi_min.txt b/docs/development/roadmap/phases/phase-10/phase_10_7a_jit_phi_cfg_and_abi_min.txt new file mode 100644 index 00000000..c7d13872 --- /dev/null +++ b/docs/development/roadmap/phases/phase-10/phase_10_7a_jit_phi_cfg_and_abi_min.txt @@ -0,0 +1,92 @@ +Phase 10.7a — JIT Branch/PHI/Independent ABI: Minimal Path Hardening (Plan) + +Purpose +- Solidify the first functional slice of the JIT control-flow path: + - Branch wiring (b1 and i64!=0 normalization) + - Minimal PHI via block parameters (single-value diamond) + - Independent ABI (JitValue) and safe dispatch + fallback + - Observable, toggleable, and easy to verify via flags + examples + +Scope (10.7a only) +1) Minimal PHI handoff + - LowerCore: detect single-PHI merge; pass i64 value via block param + - CraneliftBuilder: ensure block param (I64), push/pop on branch/jump with args + - Flag gate: NYASH_JIT_PHI_MIN=1 + +2) Branch wiring and condition normalization + - Keep compare result on stack as b1 when possible + - If top-of-stack is I64, normalize by icmp_imm != 0 + - Implement br_if/jump to pre-created blocks (deterministic order) + +3) Independent ABI (minimum viable) + - JitValue(I64/F64/Bool/Handle) in/out; normalize non-i64 args to i64 for now + - TLS split: legacy VM args (for hostcalls) and JIT args + - Engine.execute_handle uses catch_unwind; panic → VM fallback with stats + +4) Minimal hostcall bridge (safe; off by default) + - Symbols: nyash.array.{len,get,set,push}, nyash.map.{size} + - Gate: NYASH_JIT_HOSTCALL=1 + - Only integer indices/values (PoC); other types map to handles later + +5) Observability and ergonomics + - Flags: NYASH_JIT_EXEC, NYASH_JIT_STATS, NYASH_JIT_STATS_JSON, NYASH_JIT_DUMP, NYASH_JIT_THRESHOLD, NYASH_JIT_PHI_MIN, NYASH_JIT_HOSTCALL + - CLI: --jit-exec --jit-stats --jit-stats-json --jit-dump --jit-threshold N --jit-phi-min --jit-hostcall + - Unified JIT summary on VM exit (sites/compiled/hits/exec_ok/trap/fallback_rate) + - Lower log includes argc/phi_min + CFG light dump (phi edges) when NYASH_JIT_DUMP=1 + +Non-Goals (later 10.7b+) +- Full PHI generalization (multiple values, loops, complex CFG forms) +- Non-i64 native path (true F64/Bool return/params in CLIF) +- Side-effect instruction lowering (print, IO) — keep in VM path +- Host handle registry for real object bridging (u64↔Arc) + +Deliverables +- Working minimal PHI + branch JIT execution on curated examples: + - examples/jit_branch_demo.nyash + - examples/jit_loop_early_return.nyash + - examples/jit_phi_demo.nyash (single-PHI diamond) +- Fallback correctness: traps/panic → VM path; results match VM +- Configurable via CLI flags; metrics visible via JIT summary/JSON + +Acceptance Criteria +- With: --backend vm --jit-exec --jit-stats --jit-threshold 1 + - For branch/phi examples, JIT executes without panic + - VM fallback occurs only for unsupported ops (logged) + - JIT summary shows exec_ok > 0 and reasonable fallback_rate +- With: --jit-phi-min + - CFG dump lists phi_min edges and blocks count + - Results match the same program without JIT enabled + +Risk & Mitigation +- Mismatch between VMValue and JitValue adapters + - Mitigation: normalize non-i64 to i64 defensively; expand tests on adapters +- Cranelift block parameter misuse + - Mitigation: deterministic block order and explicit ensure_block_param_i64() +- Panic in hostcall stubs (unexpected Box types) + - Mitigation: gated by NYASH_JIT_HOSTCALL=1; default off; fallback to VM on panic + +Verification Checklist +- cargo check (w/ and w/o --features cranelift-jit) +- Run examples with JIT flags; compare outputs with pure VM +- Inspect logs: lower summary + CFG dump + unified summary/JSON + +Timeline (10.7a) +Day 1: +- Finalize ABI normalization and branch wiring; add unified stats (done) +- Wire CLI flags and JSON stats (done) +Day 2: +- Harden minimal PHI path and CFG dump (done) +- Curate examples and sanity-run on flags +Day 3: +- Stabilize logging format, trim rough edges, doc polish +→ Then roll into 10.7b (multi-PHI, multi-arg block params, real handle registry) + +Follow-ups (10.7b/10.7c seeds) +- Host handle registry (u64↔Arc) and type-safe bridging +- True F64/Bool native ABI, multi-arg params/returns +- CFG visualization improvements (dot export) and JitConfigBox + +Refs +- docs/development/current/CURRENT_TASK.md (10_7 items) +- src/jit/{lower,engine,manager,abi,rt}/ +- examples: jit_* demos diff --git a/docs/development/roadmap/phases/phase-10/phase_10_7h_native_abi_types.md b/docs/development/roadmap/phases/phase-10/phase_10_7h_native_abi_types.md new file mode 100644 index 00000000..8891fde0 --- /dev/null +++ b/docs/development/roadmap/phases/phase-10/phase_10_7h_native_abi_types.md @@ -0,0 +1,45 @@ +# Phase 10.7h — Native ABI Types (F64/Bool) + +Goal +- Extend the minimal i64-only JIT ABI to support f64 and bool as native parameter/return types in CLIF. + +Principles +- Keep JIT independent from VM internals (use JitValue + adapters at boundary) +- Avoid silent truncation; perform explicit conversions in the lowerer +- Maintain safety-first fallback to VM for unsupported ops + +Plan +1) JitValue widening + - JitValue already has I64/F64/Bool/Handle — keep this as the ABI surface + - Adapter: refine to/from VMValue mappings (no lossy coercion by default) + +2) CLIF signature selection + - Augment CraneliftBuilder to build signatures based on (arity × type shape) + - Start with small shapes: (I64|F64|Bool)* → I64|F64|Bool + - Closure trampoline: transmute to matching extern "C" fn type; dispatch by shape id + +3) Condition handling + - Bool: prefer b1 in IR; allow i64!=0 normalization when comparing integers + - Comparisons yield b1; lower branch consumes b1 directly + +4) Conversions in lowerer (explicit only) + - add_const_f64, add_convert_{i64_to_f64, f64_to_i64} + - prohibit implicit int<->float coercion in arithmetic; gate conversions via explicit MIR ops or intrinsics + +5) Observability and flags + - NYASH_JIT_NATIVE_F64=1 / NYASH_JIT_NATIVE_BOOL=1 to enable paths + - Dump: show chosen signature shape and conversions when NYASH_JIT_DUMP=1 + +6) Rollout + - Phase A: const/binop/ret for f64; comparisons yield b1 + - Phase B: mixed-type ops via explicit converts + - Phase C: HostCall bridging for f64/bool PODs (read-only first) + +Risks / Mitigation +- Signature explosion: start with a few common shapes; fallback to i64 path +- Platform ABI mismatches: rely on Cranelift default call conv; e2e-perf and correctness first + +Acceptance +- Examples with pure f64 pipelines run under JIT with matching results vs VM +- No silent lossy conversions; conversions visible in MIR/Lower logs + diff --git a/docs/development/roadmap/phases/phase-10/phase_10_9_builtin_box_jit_support.md b/docs/development/roadmap/phases/phase-10/phase_10_9_builtin_box_jit_support.md new file mode 100644 index 00000000..fede62f2 --- /dev/null +++ b/docs/development/roadmap/phases/phase-10/phase_10_9_builtin_box_jit_support.md @@ -0,0 +1,175 @@ +# Phase 10.9 - ビルトインBox JITサポート + +## 🎯 目的 +ビルトインBoxをJITで使えるようにし、Python統合(Phase 10.1)への道を開く。 + +## 📦 対象Box(優先順位順) + +### 第1段階:読み取り専用メソッド +```nyash +// StringBox +str.length() // → i64 +str.isEmpty() // → bool +str.charAt(idx) // → String(新Box生成) + +// ArrayBox +arr.length() // → i64 +arr.isEmpty() // → bool +arr.get(idx) // → Box(既存参照) + +// IntegerBox/FloatBox +int.toFloat() // → f64 +float.toInt() // → i64 +``` + +### 第2段階:Box生成 +```nyash +// new演算子のJIT化 +new StringBox("hello") // → Handle +new IntegerBox(42) // → Handle(または直接i64) +new ArrayBox() // → Handle +``` + +### 第3段階:書き込みメソッド +```nyash +// 状態変更を伴う操作 +arr.push(item) // Mutex操作必要 +arr.set(idx, value) // 境界チェック必要 +map.set(key, value) // ハッシュ操作 +``` + +## 🔧 実装戦略 + +### 1. HandleRegistry活用 +```rust +// 既存のHandleRegistry(80%実装済み)を拡張 +pub fn jit_get_box_method(handle: u64, method: &str) -> Option { + // ハンドル → Box → メソッドポインタ +} +``` + +### 2. HostCall拡張 +```rust +// 現在の限定的なHostCallを段階的に拡張 +enum HostCallKind { + // 既存 + ArrayIsEmpty, + StringLength, + + // Phase 10.9で追加 + StringIsEmpty, + StringCharAt, + ArrayGet, + IntToFloat, + FloatToInt, + + // new演算子サポート + NewStringBox, + NewIntegerBox, + NewArrayBox, +} +``` + +### 3. 型安全性の確保 +```rust +// JIT時の型チェック +match method { + "length" => { + // StringBox/ArrayBoxのみ許可 + verify_box_type(handle, &[BoxType::String, BoxType::Array])? + } + "isEmpty" => { + // より多くのBoxで使用可能 + verify_box_type(handle, &[BoxType::String, BoxType::Array, BoxType::Map])? + } +} +``` + +## 📊 成功指標 + +### 機能面 +- [ ] StringBox.length() がJITで実行可能 +- [ ] ArrayBox.isEmpty() がJITで実行可能 +- [ ] new StringBox() がJITで生成可能 +- [ ] 型チェックが正しく動作 + +### 性能面 +- [ ] HostCall経由でも10倍以上高速化 +- [ ] Handle解決のオーバーヘッド最小化 +- [ ] Mutex競合の回避(読み取り専用) + +### Python統合への貢献 +- [ ] PythonParserBoxの基本メソッドが使用可能 +- [ ] MirBuilderBoxへのデータ受け渡し可能 +- [ ] 最小限のPython→Nyash変換が動作 + +## 🚧 技術的課題 + +### 1. Arcパターンとの整合性 +```rust +// 読み取り専用でもMutexロックが必要? +// → 読み取り専用APIを別途用意? +``` + +### 2. Box生成時のメモリ管理 +```rust +// JIT内でのArc生成 +// → HandleRegistryで一元管理 +``` + +### 3. エラーハンドリング +```rust +// パニックしない設計 +// → Result型での丁寧なエラー伝播 +``` + +## 📈 実装ロードマップ + +### Week 1:基盤整備 +- HandleRegistry拡張 +- HostCallインターフェース設計 +- 型チェック機構 + +### Week 2:読み取りメソッド実装 +- StringBox:length, isEmpty, charAt +- ArrayBox:length, isEmpty, get +- 数値変換:toInt, toFloat + +### Week 3:Box生成サポート +- new演算子のMIR→JIT変換 +- コンストラクタ呼び出し +- HandleRegistry登録 + +### Week 4:テストと最適化 +- E2Eテストスイート +- パフォーマンス測定 +- Python統合の動作確認 + +## 🎉 期待される成果 + +```nyash +// これが高速に動く! +static box FastPython { + main() { + local py = new PythonParserBox() // JITで生成! + local code = "def add(a, b): return a + b" + local ast = py.parse(code) // JITで実行! + + local builder = new MirBuilderBox() // JITで生成! + local mir = builder.build(ast) // JITで実行! + + // Python関数がネイティブ速度で動く! + return "Python is now Native!" + } +} +``` + +## 🚀 次のステップ + +→ Phase 10.10:プラグインBox JITサポート +→ Phase 10.1:Python統合(いよいよ実現!) + +--- + +作成者:Claude(Nyashくんの要望により) +目的:「うるさい、Nyashつかえ」を真に実現するため \ No newline at end of file diff --git a/docs/reference/jit/jit_stats_json_v1.md b/docs/reference/jit/jit_stats_json_v1.md new file mode 100644 index 00000000..52c6a057 --- /dev/null +++ b/docs/reference/jit/jit_stats_json_v1.md @@ -0,0 +1,76 @@ +JIT Stats JSON Schema — Version 1 +================================= + +This document describes the fields emitted in the JIT statistics JSON outputs (version=1). + +Sources +- Unified JIT stats (VM-side): printed when `NYASH_JIT_STATS_JSON=1` (or CLI equivalent) +- Box API: `new JitStatsBox().toJson()` returns a compact JSON, `summary()` returns a pretty JSON summary (via VM dispatch) + +Version +- `version`: number — Schema version (currently `1`) + +Unified JIT Stats (JSON) +- `version`: number — Schema version (1) +- `sites`: number — Number of JIT call sites observed +- `compiled`: number — Count of functions compiled by the JIT +- `hits`: number — Sum of entries across all functions (hotness hits) +- `exec_ok`: number — Count of successful JIT executions +- `trap`: number — Count of JIT executions that trapped and fell back to VM +- `fallback_rate`: number — Ratio `trap / (exec_ok + trap)` (0 when denominator is 0) +- `handles`: number — Current number of live JIT handles in the registry +- `abi_mode`: string — `"i64_bool"` or `"b1_bool"` (when toolchain supports b1) +- `abi_b1_enabled`: boolean — Whether b1 ABI is requested by config +- `abi_b1_supported`: boolean — Whether current toolchain supports b1 in signatures +- `b1_norm_count`: number — Count of b1 normalizations (e.g., i64!=0 to b1) +- `ret_bool_hint_count`: number — Count of functions lowered with return-bool hint +- `phi_total_slots`: number — Total PHI slots encountered (always-on, LowerCore-based) +- `phi_b1_slots`: number — PHI slots classified as boolean (heuristics) +- `top5`: array of objects — Top hot functions + - `name`: string + - `hits`: number + - `compiled`: boolean + - `handle`: number (0 when not compiled) + +Compact Box JSON — `JitStatsBox.toJson()` +- `version`: number — Schema version (1) +- `abi_mode`: string — Current ABI mode for booleans (see above) +- `abi_b1_enabled`: boolean — Whether b1 ABI is requested by config +- `abi_b1_supported`: boolean — Whether current toolchain supports b1 in signatures +- `b1_norm_count`: number — b1 normalizations count +- `ret_bool_hint_count`: number — return-bool hint count +- `phi_total_slots`: number — Total PHI slots (accumulated) +- `phi_b1_slots`: number — Boolean PHI slots (accumulated) + +Summary Box JSON — `JitStatsBox.summary()` +- Pretty-printed JSON containing: + - All fields from `toJson()` + - `top5`: same structure as unified stats + +Notes +- Counters reflect process-lifetime totals (reset only on process start). +- `phi_*` counters are accumulated per LowerCore invocation (function lower), independent of dump flags. +- `abi_mode` switches to `b1_bool` when toolchain support is detected and b1 ABI is enabled. + +Examples +``` +{ + "version": 1, + "sites": 1, + "compiled": 0, + "hits": 1, + "exec_ok": 0, + "trap": 0, + "fallback_rate": 0.0, + "handles": 0, + "abi_mode": "i64_bool", + "abi_b1_enabled": false, + "abi_b1_supported": false, + "b1_norm_count": 0, + "ret_bool_hint_count": 0, + "phi_total_slots": 2, + "phi_b1_slots": 1, + "top5": [ { "name": "main", "hits": 1, "compiled": false, "handle": 0 } ] +} +``` + diff --git a/examples/jit_array_is_empty.nyash b/examples/jit_array_is_empty.nyash new file mode 100644 index 00000000..ec397a36 --- /dev/null +++ b/examples/jit_array_is_empty.nyash @@ -0,0 +1,13 @@ +// Param-array JIT HostCall PoC (isEmpty) +box Utils { + isEmpty(a) { + return a.isEmpty() + } +} + +a = new ArrayBox() +u = new Utils() +print(u.isEmpty(a)) +a.push(1) +print(u.isEmpty(a)) + diff --git a/examples/jit_compare_i64_boolret.nyash b/examples/jit_compare_i64_boolret.nyash new file mode 100644 index 00000000..5b7b9364 --- /dev/null +++ b/examples/jit_compare_i64_boolret.nyash @@ -0,0 +1,13 @@ +// i64 compare returning boolean directly (ret_bool hint) +// Enable: NYASH_JIT_THRESHOLD=1 NYASH_JIT_DUMP=1 + +static box Main { + main() { + local a, b + a = 10 + b = 4 + // return boolean result directly + return a > b + } +} + diff --git a/examples/jit_config_demo.nyash b/examples/jit_config_demo.nyash new file mode 100644 index 00000000..cd0f12e4 --- /dev/null +++ b/examples/jit_config_demo.nyash @@ -0,0 +1,13 @@ +// JitConfigBox demo +static box Main { + main() { + local jc + jc = new JitConfigBox() + jc.enable("exec") + jc.setThreshold(1) + jc.enable("phi_min") + jc.apply() + print(jc.summary()) + return 0 + } +} diff --git a/examples/jit_copy_const_cast.nyash b/examples/jit_copy_const_cast.nyash new file mode 100644 index 00000000..d26cf6a4 --- /dev/null +++ b/examples/jit_copy_const_cast.nyash @@ -0,0 +1,19 @@ +// Copy/Const/Cast coverage demo +// Use with: NYASH_JIT_THRESHOLD=1 NYASH_JIT_DUMP=1 + +static box Main { + main() { + local a, b, f + a = 42 + b = a // Copy + f = 100.0 + // Mixed compare (int vs float) to exercise promote/cast path in optimizer + if (b < f) { + // Return boolean directly + return b < f + } else { + return false + } + } +} + diff --git a/examples/jit_direct_bool_ret.nyash b/examples/jit_direct_bool_ret.nyash new file mode 100644 index 00000000..7c9c4eee --- /dev/null +++ b/examples/jit_direct_bool_ret.nyash @@ -0,0 +1,14 @@ +// jit-direct: boolean return normalization +// Build: cargo build --release --features cranelift-jit +// Run: NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 \ +// ./target/release/nyash --jit-direct examples/jit_direct_bool_ret.nyash + +static box Main { + main() { + local a, b + a = 3 + b = 5 + return a < b // expect true + } +} + diff --git a/examples/jit_direct_f64_ret.nyash b/examples/jit_direct_f64_ret.nyash new file mode 100644 index 00000000..bdae6406 --- /dev/null +++ b/examples/jit_direct_f64_ret.nyash @@ -0,0 +1,15 @@ +// jit-direct: f64 return demo +// Build: cargo build --release --features cranelift-jit +// Run: NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_NATIVE_F64=1 \ +// ./target/release/nyash --jit-direct examples/jit_direct_f64_ret.nyash + +static box Main { + main() { + local x, y, s + x = 1.5 + y = 2.25 + s = x + y + return s // expect 3.75 + } +} + diff --git a/examples/jit_direct_local_store_load.nyash b/examples/jit_direct_local_store_load.nyash new file mode 100644 index 00000000..367ed0ad --- /dev/null +++ b/examples/jit_direct_local_store_load.nyash @@ -0,0 +1,15 @@ +// jit-direct: minimal local Store/Load path +// Build: cargo build --release --features cranelift-jit +// Run: NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 \ +// ./target/release/nyash --jit-direct examples/jit_direct_local_store_load.nyash + +static box Main { + main() { + local x, y + x = 1 + y = 2 + x = x + y // store/load around add + return x // expect 3 + } +} + diff --git a/examples/jit_f64_arith.nyash b/examples/jit_f64_arith.nyash new file mode 100644 index 00000000..13538bc8 --- /dev/null +++ b/examples/jit_f64_arith.nyash @@ -0,0 +1,12 @@ +// JIT native f64 arithmetic demo +// Enable: NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_NATIVE_F64=1 + +static box Main { + main() { + local x, y + x = 1.5 + y = 2.25 + return x + y + } +} + diff --git a/examples/jit_f64_e2e_add_compare.nyash b/examples/jit_f64_e2e_add_compare.nyash new file mode 100644 index 00000000..ef7e804e --- /dev/null +++ b/examples/jit_f64_e2e_add_compare.nyash @@ -0,0 +1,19 @@ +// f64 E2E demo (Cranelift JIT) +// Build: cargo build --release --features cranelift-jit +// Run: NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_NATIVE_F64=1 \ +// ./target/release/nyash --backend vm examples/jit_f64_e2e_add_compare.nyash + +static box Main { + main() { + local x, y + x = 1.5 + y = 2.25 + // 1) return f64 add + local s + s = x + y + print("sum=" + s) + // 2) return f64 compare as boolean + return s > 3.0 + } +} + diff --git a/examples/jit_i64_binops.nyash b/examples/jit_i64_binops.nyash new file mode 100644 index 00000000..e08be440 --- /dev/null +++ b/examples/jit_i64_binops.nyash @@ -0,0 +1,14 @@ +// i64 BinOp coverage demo (Add/Sub/Mul/Div/Mod) +// Enable: NYASH_JIT_THRESHOLD=1 NYASH_JIT_DUMP=1 + +static box Main { + main() { + local a, b, r + a = 7 + b = 3 + r = a + b + r = r - (a * b) + r = r + (a / b) + return r + } +} diff --git a/examples/jit_map_has_int_keys.nyash b/examples/jit_map_has_int_keys.nyash new file mode 100644 index 00000000..97e3a17e --- /dev/null +++ b/examples/jit_map_has_int_keys.nyash @@ -0,0 +1,12 @@ +// Param-map JIT HostCall PoC (has) +box Utils { + has12(m) { + return m.has(1) + m.has(2) + } +} + +m = new MapBox() +m.set(1, 10) +u = new Utils() +print(u.has12(m)) + diff --git a/examples/jit_map_int_keys_param_call.nyash b/examples/jit_map_int_keys_param_call.nyash new file mode 100644 index 00000000..66a77b77 --- /dev/null +++ b/examples/jit_map_int_keys_param_call.nyash @@ -0,0 +1,14 @@ +// Param-map JIT HostCall PoC with integer keys +box Utils { + sumKeys(m) { + // get(1) + get(2) + return m.get(1) + m.get(2) + } +} + +m = new MapBox() +m.set(1, 10) +m.set(2, 20) +u = new Utils() +print(u.sumKeys(m)) + diff --git a/examples/jit_mixed_f64_compare.nyash b/examples/jit_mixed_f64_compare.nyash new file mode 100644 index 00000000..d4a187cd --- /dev/null +++ b/examples/jit_mixed_f64_compare.nyash @@ -0,0 +1,12 @@ +// Mixed f64 compare demo (int vs float) +// Enable: NYASH_JIT_THRESHOLD=1 NYASH_JIT_NATIVE_F64=1 + +static box Main { + main() { + local a, b + a = 1.5 + 2.5 + b = 3.5 + // f64 compare + return a > b + } +} diff --git a/examples/jit_multi_phi_demo.nyash b/examples/jit_multi_phi_demo.nyash new file mode 100644 index 00000000..7c950d7f --- /dev/null +++ b/examples/jit_multi_phi_demo.nyash @@ -0,0 +1,19 @@ +// Multi-PHI diamond demo +// Enable: NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_PHI_MIN=1 + +static box Main { + main() { + local a, b, x, y + a = 2 + b = 5 + if (a < b) { + x = a + 1 + y = b + 2 + } else { + x = a + 3 + y = b + 4 + } + return x + y + } +} + diff --git a/examples/jit_stats_bool_ret.nyash b/examples/jit_stats_bool_ret.nyash new file mode 100644 index 00000000..7d8862f9 --- /dev/null +++ b/examples/jit_stats_bool_ret.nyash @@ -0,0 +1,24 @@ +// JIT stats + boolean return hint demo +// Enable: NYASH_JIT_THRESHOLD=1 NYASH_JIT_DUMP=1 +// Optional: NYASH_JIT_EXEC=1 (Cranelift未有効でもVM結果は不変) + +static box Main { + main() { + local a, b, c, s, r + a = 3 + b = 5 + // Force boolean value via branch and PHI-like merge + if (a < b) { + c = true + } else { + c = false + } + r = c + // Observe JIT runtime stats (JSON string) + s = new JitStatsBox() + print(s.toString()) + // Return boolean directly (exercise ret_bool_hint) + return r + } +} + diff --git a/examples/jit_stats_summary_demo.nyash b/examples/jit_stats_summary_demo.nyash new file mode 100644 index 00000000..b04d3542 --- /dev/null +++ b/examples/jit_stats_summary_demo.nyash @@ -0,0 +1,13 @@ +// JitStatsBox.summary demo (pretty JSON) +// Enable: NYASH_JIT_STATS=1 NYASH_JIT_THRESHOLD=1 + +static box Main { + main() { + local s + s = new JitStatsBox() + print(s.summary()) + // Also show per-function stats JSON array + print(s.perFunction()) + return 0 + } +} diff --git a/examples/jit_stats_tojson.nyash b/examples/jit_stats_tojson.nyash new file mode 100644 index 00000000..601600f6 --- /dev/null +++ b/examples/jit_stats_tojson.nyash @@ -0,0 +1,9 @@ +// print JitStatsBox.toJson() +static box Main { + main() { + local s + s = new JitStatsBox() + print(s.toJson()) + return 0 + } +} diff --git a/examples/jit_stats_top5.nyash b/examples/jit_stats_top5.nyash new file mode 100644 index 00000000..f9682465 --- /dev/null +++ b/examples/jit_stats_top5.nyash @@ -0,0 +1,12 @@ +// JitStatsBox.top5 demo (JSON array) +// Enable: NYASH_JIT_STATS=1 NYASH_JIT_THRESHOLD=1 + +static box Main { + main() { + local s + s = new JitStatsBox() + print(s.top5()) + return 0 + } +} + diff --git a/examples/jit_string_charcode_at.nyash b/examples/jit_string_charcode_at.nyash new file mode 100644 index 00000000..e1039e67 --- /dev/null +++ b/examples/jit_string_charcode_at.nyash @@ -0,0 +1,13 @@ +// Param-string JIT HostCall PoC (charCodeAt) +box Utils { + codeAt(s, i) { + return s.charCodeAt(i) + } +} + +u = new Utils() +print(u.codeAt("ABC", 0)) +print(u.codeAt("ABC", 1)) +print(u.codeAt("ABC", 2)) +print(u.codeAt("ABC", 3)) // -1 (out of range) + diff --git a/examples/jit_string_is_empty.nyash b/examples/jit_string_is_empty.nyash new file mode 100644 index 00000000..9bc0024c --- /dev/null +++ b/examples/jit_string_is_empty.nyash @@ -0,0 +1,11 @@ +// Param-string JIT HostCall PoC (isEmpty) +box Utils { + isEmpty(s) { + return s.isEmpty() + } +} + +u = new Utils() +print(u.isEmpty("")) +print(u.isEmpty("nyash")) + diff --git a/examples/jit_string_param_length.nyash b/examples/jit_string_param_length.nyash new file mode 100644 index 00000000..c806233a --- /dev/null +++ b/examples/jit_string_param_length.nyash @@ -0,0 +1,11 @@ +// Param-string JIT HostCall PoC (length) +box Utils { + strlen(s) { + return s.length() + } +} + +s = "Hello, Nyash!" +u = new Utils() +print(u.strlen(s)) + diff --git a/examples/mix_num_bool_promote.nyash b/examples/mix_num_bool_promote.nyash new file mode 100644 index 00000000..a9790247 --- /dev/null +++ b/examples/mix_num_bool_promote.nyash @@ -0,0 +1,16 @@ +// Mixed numeric/boolean promote demo +// Enable: NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 + +static box Main { + main() { + local a, f + a = 7 + f = 7.5 + // mixed compare (i64 < f64) should promote to f64 compare and yield b1 + if (a < f) { + return 1 + } else { + return 0 + } + } +} diff --git a/examples/ny_bench_f64.nyash b/examples/ny_bench_f64.nyash new file mode 100644 index 00000000..49d1c5d7 --- /dev/null +++ b/examples/ny_bench_f64.nyash @@ -0,0 +1,53 @@ +// f64 micro benchmark (Cranelift JIT) +// Requirements: +// - Build with: cargo build --release --features cranelift-jit +// - Run with: NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_NATIVE_F64=1 \ +// ./target/release/nyash --backend vm examples/ny_bench_f64.nyash +// Notes: +// - VM backend alone may not support f64 BinOp/Compare; use JIT as above. + +static box Main { + main() { + local ITER, timer + ITER = 200000 + timer = new TimerBox() + + print("\n=== f64 Micro Bench (ITER=" + ITER + ") ===") + + // 1) f64 add/mul loop + local i, x, y, z, t0, t1, ms, ops + i = 0 + x = 1.5 + y = 2.25 + t0 = timer.now() + loop(i < ITER) { + z = x + y + x = z * 1.000001 + i = i + 1 + } + t1 = timer.now() + ms = t1 - t0 + ops = (ITER * 1000.0) / ms + print("[f64_addmul] elapsed_ms=" + ms + ", ops/sec=" + ops) + + // 2) f64 compare loop + local c + i = 0 + x = 10.0 + y = 9.5 + t0 = timer.now() + loop(i < ITER) { + c = x > y + y = y + 0.000001 + i = i + 1 + } + t1 = timer.now() + ms = t1 - t0 + ops = (ITER * 1000.0) / ms + print("[f64_compare] elapsed_ms=" + ms + ", ops/sec=" + ops) + + print("\nDone (f64).") + return 0 + } +} + diff --git a/examples/phi_bool_merge.nyash b/examples/phi_bool_merge.nyash new file mode 100644 index 00000000..a0516442 --- /dev/null +++ b/examples/phi_bool_merge.nyash @@ -0,0 +1,23 @@ +// Boolean PHI merge demo (b1 internal path) +// Enable: NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_PHI_MIN=1 +// Optional DOT: NYASH_JIT_DOT=tmp/phi_bool.dot + +static box Main { + main() { + local a, b, c + a = 3 + b = 5 + // Create a boolean via branch to force PHI merge of boolean + if (a < b) { + c = true + } else { + c = false + } + // Use merged boolean + if (c) { + return 1 + } else { + return 0 + } + } +} diff --git a/examples/phi_bool_tag_demo.nyash b/examples/phi_bool_tag_demo.nyash new file mode 100644 index 00000000..da023ac4 --- /dev/null +++ b/examples/phi_bool_tag_demo.nyash @@ -0,0 +1,18 @@ +// PHI with boolean inputs demo (shows (b1) tag) +// Enable: NYASH_JIT_PHI_MIN=1 NYASH_JIT_DUMP=1 + +static box Main { + main() { + local a, b, x + a = 1 + b = 2 + if (a < b) { + x = (3 < 4) + } else { + x = (5 < 6) + } + // Use x so it stays live + if (x) { return 1 } else { return 0 } + } +} + diff --git a/examples/ret_bool_demo.nyash b/examples/ret_bool_demo.nyash new file mode 100644 index 00000000..dc225a81 --- /dev/null +++ b/examples/ret_bool_demo.nyash @@ -0,0 +1,12 @@ +// Return boolean demo +// Enable: NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 + +static box Main { + main() { + local a, b + a = 3 + b = 5 + // Return result of comparison directly (boolean) + return a < b + } +} diff --git a/src/backend/vm.rs b/src/backend/vm.rs index 4d6717f0..f317e673 100644 --- a/src/backend/vm.rs +++ b/src/backend/vm.rs @@ -454,13 +454,15 @@ impl VM { // Optional: print VM stats self.maybe_print_stats(); + // Optional: print concise JIT unified stats + self.maybe_print_jit_unified_stats(); // Optional: print cache stats summary if std::env::var("NYASH_VM_PIC_STATS").ok().as_deref() == Some("1") { self.print_cache_stats_summary(); } - // Optional: print JIT stats summary (Phase 10_a) + // Optional: print JIT detailed summary (top functions) if let Some(jm) = &self.jit_manager { jm.print_summary(); } // Optional: GC diagnostics if enabled @@ -560,6 +562,8 @@ impl VM { jm.record_entry(&function.signature.name); // Try compile if hot (no-op for now, returns fake handle) let _ = jm.maybe_compile(&function.signature.name, function); + // Record per-function lower stats captured during last JIT lower (if any) + // Note: The current engine encapsulates its LowerCore; expose via last_stats on a new instance as needed. if jm.is_compiled(&function.signature.name) && std::env::var("NYASH_JIT_STATS").ok().as_deref() == Some("1") { if let Some(h) = jm.handle_of(&function.signature.name) { eprintln!("[JIT] dispatch would go to handle={} for {} (stub)", h, function.signature.name); @@ -581,12 +585,13 @@ impl VM { .filter_map(|pid| self.get_value(*pid).ok()) .collect(); if std::env::var("NYASH_JIT_EXEC").ok().as_deref() == Some("1") { + let jit_only = std::env::var("NYASH_JIT_ONLY").ok().as_deref() == Some("1"); // Root regionize args for JIT call self.enter_root_region(); self.pin_roots(args_vec.iter()); - if let Some(jm_ref) = self.jit_manager.as_ref() { - if jm_ref.is_compiled(&function.signature.name) { - if let Some(val) = jm_ref.execute_compiled(&function.signature.name, &args_vec) { + if let Some(jm_mut) = self.jit_manager.as_mut() { + if jm_mut.is_compiled(&function.signature.name) { + if let Some(val) = jm_mut.execute_compiled(&function.signature.name, &args_vec) { // Exit scope before returning self.leave_root_region(); self.scope_tracker.pop_scope(); @@ -594,6 +599,29 @@ impl VM { } else if std::env::var("NYASH_JIT_STATS").ok().as_deref() == Some("1") || std::env::var("NYASH_JIT_TRAP_LOG").ok().as_deref() == Some("1") { eprintln!("[JIT] fallback: VM path taken for {}", function.signature.name); + if jit_only { + self.leave_root_region(); + self.scope_tracker.pop_scope(); + return Err(VMError::InvalidInstruction(format!("JIT-only enabled and JIT trap occurred for {}", function.signature.name))); + } + } + } else if jit_only { + // Try to compile now and execute; if not possible, error out + let _ = jm_mut.maybe_compile(&function.signature.name, function); + if jm_mut.is_compiled(&function.signature.name) { + if let Some(val) = jm_mut.execute_compiled(&function.signature.name, &args_vec) { + self.leave_root_region(); + self.scope_tracker.pop_scope(); + return Ok(val); + } else { + self.leave_root_region(); + self.scope_tracker.pop_scope(); + return Err(VMError::InvalidInstruction(format!("JIT-only enabled and JIT execution failed for {}", function.signature.name))); + } + } else { + self.leave_root_region(); + self.scope_tracker.pop_scope(); + return Err(VMError::InvalidInstruction(format!("JIT-only enabled but function not compiled: {}", function.signature.name))); } } } @@ -802,6 +830,77 @@ impl VM { // Legacy box_trait::ResultBox is no longer handled here (migration complete) + // JitStatsBox methods (process-local JIT counters) + if let Some(_jsb) = box_value.as_any().downcast_ref::() { + match method { + "toJson" | "toJSON" => { + return Ok(crate::boxes::jit_stats_box::JitStatsBox::new().to_json()); + } + // Return detailed per-function stats as JSON array + // Each item: { name, phi_total, phi_b1, ret_bool_hint, hits, compiled, handle } + "perFunction" | "per_function" => { + if let Some(jm) = &self.jit_manager { + let v = jm.per_function_stats(); + let arr: Vec = v.into_iter().map(|(name, phi_t, phi_b1, rb, hits, compiled, handle)| { + serde_json::json!({ + "name": name, + "phi_total": phi_t, + "phi_b1": phi_b1, + "ret_bool_hint": rb, + "hits": hits, + "compiled": compiled, + "handle": handle + }) + }).collect(); + let s = serde_json::to_string(&arr).unwrap_or_else(|_| "[]".to_string()); + return Ok(Box::new(crate::box_trait::StringBox::new(s))); + } + return Ok(Box::new(crate::box_trait::StringBox::new("[]"))); + } + "top5" => { + if let Some(jm) = &self.jit_manager { + let v = jm.top_hits(5); + let arr: Vec = v.into_iter().map(|(name, hits, compiled, handle)| { + serde_json::json!({ + "name": name, + "hits": hits, + "compiled": compiled, + "handle": handle + }) + }).collect(); + let s = serde_json::to_string(&arr).unwrap_or_else(|_| "[]".to_string()); + return Ok(Box::new(crate::box_trait::StringBox::new(s))); + } + return Ok(Box::new(crate::box_trait::StringBox::new("[]"))); + } + "summary" => { + let cfg = crate::jit::config::current(); + let caps = crate::jit::config::probe_capabilities(); + let abi_mode = if cfg.native_bool_abi && caps.supports_b1_sig { "b1_bool" } else { "i64_bool" }; + let b1_norm = crate::jit::rt::b1_norm_get(); + let ret_b1 = crate::jit::rt::ret_bool_hint_get(); + let mut payload = serde_json::json!({ + "abi_mode": abi_mode, + "abi_b1_enabled": cfg.native_bool_abi, + "abi_b1_supported": caps.supports_b1_sig, + "b1_norm_count": b1_norm, + "ret_bool_hint_count": ret_b1, + "top5": [] + }); + if let Some(jm) = &self.jit_manager { + let v = jm.top_hits(5); + let top5: Vec = v.into_iter().map(|(name, hits, compiled, handle)| serde_json::json!({ + "name": name, "hits": hits, "compiled": compiled, "handle": handle + })).collect(); + if let Some(obj) = payload.as_object_mut() { obj.insert("top5".to_string(), serde_json::Value::Array(top5)); } + } + let s = serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".to_string()); + return Ok(Box::new(crate::box_trait::StringBox::new(s))); + } + _ => return Ok(Box::new(crate::box_trait::VoidBox::new())), + } + } + // StringBox methods if let Some(string_box) = box_value.as_any().downcast_ref::() { match method { diff --git a/src/backend/vm_boxcall.rs b/src/backend/vm_boxcall.rs index 001f7cf9..b839193e 100644 --- a/src/backend/vm_boxcall.rs +++ b/src/backend/vm_boxcall.rs @@ -30,6 +30,119 @@ impl VM { return Ok(Box::new(StringBox::new(box_value.to_string_box().value))); } + // JitConfigBox methods + if let Some(jcb) = box_value.as_any().downcast_ref::() { + match method { + "get" => { + if let Some(k) = _args.get(0) { return Ok(jcb.get_flag(&k.to_string_box().value).unwrap_or_else(|e| Box::new(StringBox::new(e.to_string())))); } + return Ok(Box::new(StringBox::new("get(name) requires 1 arg"))); + } + "set" => { + if _args.len() >= 2 { + let k = _args[0].to_string_box().value; + let v = _args[1].to_string_box().value; + let on = matches!(v.as_str(), "1" | "true" | "True" | "on" | "ON"); + return Ok(jcb.set_flag(&k, on).unwrap_or_else(|e| Box::new(StringBox::new(e.to_string())))); + } + return Ok(Box::new(StringBox::new("set(name, bool) requires 2 args"))); + } + "getThreshold" => { return Ok(jcb.get_threshold()); } + "setThreshold" => { + if let Some(v) = _args.get(0) { + let iv = v.to_string_box().value.parse::().unwrap_or(0); + return Ok(jcb.set_threshold(iv).unwrap_or_else(|e| Box::new(StringBox::new(e.to_string())))); + } + return Ok(Box::new(StringBox::new("setThreshold(n) requires 1 arg"))); + } + "apply" => { return Ok(jcb.apply()); } + "toJson" => { return Ok(jcb.to_json()); } + "fromJson" => { + if let Some(s) = _args.get(0) { return Ok(jcb.from_json(&s.to_string_box().value).unwrap_or_else(|e| Box::new(StringBox::new(e.to_string())))); } + return Ok(Box::new(StringBox::new("fromJson(json) requires 1 arg"))); + } + "enable" => { + if let Some(k) = _args.get(0) { return Ok(jcb.set_flag(&k.to_string_box().value, true).unwrap_or_else(|e| Box::new(StringBox::new(e.to_string())))); } + return Ok(Box::new(StringBox::new("enable(name) requires 1 arg"))); + } + "disable" => { + if let Some(k) = _args.get(0) { return Ok(jcb.set_flag(&k.to_string_box().value, false).unwrap_or_else(|e| Box::new(StringBox::new(e.to_string())))); } + return Ok(Box::new(StringBox::new("disable(name) requires 1 arg"))); + } + "summary" => { return Ok(jcb.summary()); } + _ => { return Ok(Box::new(VoidBox::new())); } + } + } + + // JitStatsBox methods + if let Some(jsb) = box_value.as_any().downcast_ref::() { + match method { + "toJson" => { return Ok(jsb.to_json()); } + "top5" => { + if let Some(jm) = &self.jit_manager { + let v = jm.top_hits(5); + let arr: Vec = v.into_iter().map(|(name, hits, compiled, handle)| { + serde_json::json!({ + "name": name, + "hits": hits, + "compiled": compiled, + "handle": handle + }) + }).collect(); + let s = serde_json::to_string(&arr).unwrap_or_else(|_| "[]".to_string()); + return Ok(Box::new(StringBox::new(s))); + } + return Ok(Box::new(StringBox::new("[]"))); + } + "summary" => { + let cfg = crate::jit::config::current(); + let caps = crate::jit::config::probe_capabilities(); + let abi_mode = if cfg.native_bool_abi && caps.supports_b1_sig { "b1_bool" } else { "i64_bool" }; + let b1_norm = crate::jit::rt::b1_norm_get(); + let ret_b1 = crate::jit::rt::ret_bool_hint_get(); + let mut payload = serde_json::json!({ + "abi_mode": abi_mode, + "abi_b1_enabled": cfg.native_bool_abi, + "abi_b1_supported": caps.supports_b1_sig, + "b1_norm_count": b1_norm, + "ret_bool_hint_count": ret_b1, + "top5": [], + "perFunction": [] + }); + if let Some(jm) = &self.jit_manager { + let v = jm.top_hits(5); + let top5: Vec = v.into_iter().map(|(name, hits, compiled, handle)| serde_json::json!({ + "name": name, "hits": hits, "compiled": compiled, "handle": handle + })).collect(); + let perf = jm.per_function_stats(); + let per_arr: Vec = perf.into_iter().map(|(name, phi_t, phi_b1, rb, hits, compiled, handle)| serde_json::json!({ + "name": name, "phi_total": phi_t, "phi_b1": phi_b1, "ret_bool_hint": rb, "hits": hits, "compiled": compiled, "handle": handle + })).collect(); + if let Some(obj) = payload.as_object_mut() { obj.insert("top5".to_string(), serde_json::Value::Array(top5)); obj.insert("perFunction".to_string(), serde_json::Value::Array(per_arr)); } + } + let s = serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".to_string()); + return Ok(Box::new(StringBox::new(s))); + } + "perFunction" | "per_function" => { + if let Some(jm) = &self.jit_manager { + let v = jm.per_function_stats(); + let arr: Vec = v.into_iter().map(|(name, phi_t, phi_b1, rb, hits, compiled, handle)| serde_json::json!({ + "name": name, + "phi_total": phi_t, + "phi_b1": phi_b1, + "ret_bool_hint": rb, + "hits": hits, + "compiled": compiled, + "handle": handle, + })).collect(); + let s = serde_json::to_string_pretty(&arr).unwrap_or_else(|_| "[]".to_string()); + return Ok(Box::new(StringBox::new(s))); + } + return Ok(Box::new(StringBox::new("[]"))); + } + _ => { return Ok(Box::new(VoidBox::new())); } + } + } + // StringBox methods if let Some(string_box) = box_value.as_any().downcast_ref::() { match method { diff --git a/src/backend/vm_stats.rs b/src/backend/vm_stats.rs index 45bd4bc6..da537bd5 100644 --- a/src/backend/vm_stats.rs +++ b/src/backend/vm_stats.rs @@ -50,5 +50,61 @@ impl VM { } } } -} + /// Print a concise unified JIT summary alongside VM stats when enabled + pub(super) fn maybe_print_jit_unified_stats(&self) { + // Show when either JIT stats requested or VM stats are on + let jit_enabled = std::env::var("NYASH_JIT_STATS").ok().as_deref() == Some("1"); + let vm_enabled = std::env::var("NYASH_VM_STATS").ok().map(|v| v != "0").unwrap_or(false); + let jit_json = std::env::var("NYASH_JIT_STATS_JSON").ok().as_deref() == Some("1"); + if !jit_enabled && !vm_enabled { return; } + if let Some(jm) = &self.jit_manager { + // Gather basic counters + let sites = jm.sites(); + let compiled = jm.compiled_count(); + let total_hits: u64 = jm.total_hits(); + let ok = jm.exec_ok_count(); + let tr = jm.exec_trap_count(); + let total_exec = ok + tr; + let fb_rate = if total_exec > 0 { (tr as f64) / (total_exec as f64) } else { 0.0 }; + let handles = crate::jit::rt::handles::len(); + let cfg = crate::jit::config::current(); + let caps = crate::jit::config::probe_capabilities(); + let abi_mode = if cfg.native_bool_abi && caps.supports_b1_sig { "b1_bool" } else { "i64_bool" }; + let b1_norm = crate::jit::rt::b1_norm_get(); + let ret_b1_hints = crate::jit::rt::ret_bool_hint_get(); + if jit_json { + let payload = serde_json::json!({ + "version": 1, + "sites": sites, + "compiled": compiled, + "hits": total_hits, + "exec_ok": ok, + "trap": tr, + "fallback_rate": fb_rate, + "handles": handles, + "abi_mode": abi_mode, + "abi_b1_enabled": cfg.native_bool_abi, + "abi_b1_supported": caps.supports_b1_sig, + "b1_norm_count": b1_norm, + "ret_bool_hint_count": ret_b1_hints, + "phi_total_slots": crate::jit::rt::phi_total_get(), + "phi_b1_slots": crate::jit::rt::phi_b1_get(), + "top5": jm.top_hits(5).into_iter().map(|(name, hits, compiled, handle)| { + serde_json::json!({ + "name": name, + "hits": hits, + "compiled": compiled, + "handle": handle + }) + }).collect::>() + }); + println!("{}", serde_json::to_string_pretty(&payload).unwrap_or_else(|_| String::from("{}"))); + } else { + eprintln!("[JIT] summary: sites={} compiled={} hits={} exec_ok={} trap={} fallback_rate={:.2} handles={}", + sites, compiled, total_hits, ok, tr, fb_rate, handles); + eprintln!(" abi_mode={} b1_norm_count={} ret_bool_hint_count={}", abi_mode, b1_norm, ret_b1_hints); + } + } + } +} diff --git a/src/box_factory/builtin.rs b/src/box_factory/builtin.rs index 2fa8d56d..01d42b88 100644 --- a/src/box_factory/builtin.rs +++ b/src/box_factory/builtin.rs @@ -272,6 +272,26 @@ impl BuiltinBoxFactory { } Ok(Box::new(DebugBox::new())) }); + + // JitStatsBox (runtime counters & modes) + self.register("JitStatsBox", |args| { + if !args.is_empty() { + return Err(RuntimeError::InvalidOperation { + message: format!("JitStatsBox constructor expects 0 arguments, got {}", args.len()), + }); + } + Ok(Box::new(crate::boxes::jit_stats_box::JitStatsBox::new())) + }); + + // JitConfigBox (runtime JIT configuration as a Box) + self.register("JitConfigBox", |args| { + if !args.is_empty() { + return Err(RuntimeError::InvalidOperation { + message: format!("JitConfigBox constructor expects 0 arguments, got {}", args.len()), + }); + } + Ok(Box::new(crate::boxes::jit_config_box::JitConfigBox::new())) + }); } /// Register I/O types diff --git a/src/box_trait.rs b/src/box_trait.rs index 391b2bac..4920ada2 100644 --- a/src/box_trait.rs +++ b/src/box_trait.rs @@ -33,7 +33,7 @@ pub const BUILTIN_BOXES: &[&str] = &[ "SoundBox", "DebugBox", "MethodBox", "ConsoleBox", "BufferBox", "RegexBox", "JSONBox", "StreamBox", "HTTPClientBox", "IntentBox", "P2PBox", "SocketBox", - "HTTPServerBox", "HTTPRequestBox", "HTTPResponseBox" + "HTTPServerBox", "HTTPRequestBox", "HTTPResponseBox", "JitConfigBox" ]; /// 🔥 ビルトインBox判定関数 - pack透明化システムの核心 diff --git a/src/boxes/jit_config_box.rs b/src/boxes/jit_config_box.rs new file mode 100644 index 00000000..e18c811f --- /dev/null +++ b/src/boxes/jit_config_box.rs @@ -0,0 +1,139 @@ +use crate::box_trait::{NyashBox, StringBox, BoolBox, IntegerBox, VoidBox, BoxCore, BoxBase}; +use crate::jit::config::JitConfig; +use crate::interpreter::RuntimeError; +use std::any::Any; +use std::sync::RwLock; + +#[derive(Debug)] +pub struct JitConfigBox { + base: BoxBase, + pub config: RwLock, +} + +impl JitConfigBox { + pub fn new() -> Self { Self { base: BoxBase::new(), config: RwLock::new(JitConfig::from_env()) } } + /// Update internal config flags from runtime capability probe + pub fn from_runtime_probe(&self) -> Box { + let caps = crate::jit::config::probe_capabilities(); + let mut cfg = self.config.write().unwrap(); + if cfg.native_bool_abi && !caps.supports_b1_sig { cfg.native_bool_abi = false; } + Box::new(VoidBox::new()) + } + pub fn set_flag(&self, name: &str, on: bool) -> Result, RuntimeError> { + let mut cfg = self.config.write().unwrap(); + match name { + "exec" => cfg.exec = on, + "stats" => cfg.stats = on, + "stats_json" => cfg.stats_json = on, + "dump" => cfg.dump = on, + "phi_min" => cfg.phi_min = on, + "hostcall" => cfg.hostcall = on, + "handle_debug" => cfg.handle_debug = on, + "native_f64" => cfg.native_f64 = on, + "native_bool" => cfg.native_bool = on, + "bool_abi" | "native_bool_abi" => cfg.native_bool_abi = on, + "ret_b1" | "ret_bool_b1" => cfg.ret_bool_b1 = on, + _ => return Err(RuntimeError::InvalidOperation { message: format!("Unknown flag: {}", name) }), + } + Ok(Box::new(VoidBox::new())) + } + pub fn get_flag(&self, name: &str) -> Result, RuntimeError> { + let cfg = self.config.read().unwrap(); + let b = match name { + "exec" => cfg.exec, + "stats" => cfg.stats, + "stats_json" => cfg.stats_json, + "dump" => cfg.dump, + "phi_min" => cfg.phi_min, + "hostcall" => cfg.hostcall, + "handle_debug" => cfg.handle_debug, + "native_f64" => cfg.native_f64, + "native_bool" => cfg.native_bool, + "bool_abi" | "native_bool_abi" => cfg.native_bool_abi, + "ret_b1" | "ret_bool_b1" => cfg.ret_bool_b1, + _ => return Err(RuntimeError::InvalidOperation { message: format!("Unknown flag: {}", name) }), + }; + Ok(Box::new(BoolBox::new(b))) + } + pub fn set_threshold(&self, v: i64) -> Result, RuntimeError> { + let mut cfg = self.config.write().unwrap(); + if v <= 0 { cfg.threshold = None; } + else { cfg.threshold = Some(v as u32); } + Ok(Box::new(VoidBox::new())) + } + pub fn get_threshold(&self) -> Box { + let cfg = self.config.read().unwrap(); + Box::new(IntegerBox::new(cfg.threshold.map(|v| v as i64).unwrap_or(0))) + } + pub fn to_json(&self) -> Box { + let cfg = self.config.read().unwrap(); + let val = serde_json::json!({ + "exec": cfg.exec, + "stats": cfg.stats, + "stats_json": cfg.stats_json, + "dump": cfg.dump, + "threshold": cfg.threshold, + "phi_min": cfg.phi_min, + "hostcall": cfg.hostcall, + "handle_debug": cfg.handle_debug, + "native_f64": cfg.native_f64, + "native_bool": cfg.native_bool, + "native_bool_abi": cfg.native_bool_abi, + "ret_bool_b1": cfg.ret_bool_b1, + }); + Box::new(StringBox::new(val.to_string())) + } + pub fn from_json(&self, s: &str) -> Result, RuntimeError> { + let mut cfg = self.config.write().unwrap(); + let v: serde_json::Value = serde_json::from_str(s).map_err(|e| RuntimeError::InvalidOperation { message: format!("Invalid JSON: {}", e) })?; + if let Some(b) = v.get("exec").and_then(|x| x.as_bool()) { cfg.exec = b; } + if let Some(b) = v.get("stats").and_then(|x| x.as_bool()) { cfg.stats = b; } + if let Some(b) = v.get("stats_json").and_then(|x| x.as_bool()) { cfg.stats_json = b; } + if let Some(b) = v.get("dump").and_then(|x| x.as_bool()) { cfg.dump = b; } + if let Some(n) = v.get("threshold").and_then(|x| x.as_u64()) { cfg.threshold = Some(n as u32); } + if let Some(b) = v.get("phi_min").and_then(|x| x.as_bool()) { cfg.phi_min = b; } + if let Some(b) = v.get("hostcall").and_then(|x| x.as_bool()) { cfg.hostcall = b; } + if let Some(b) = v.get("handle_debug").and_then(|x| x.as_bool()) { cfg.handle_debug = b; } + if let Some(b) = v.get("native_f64").and_then(|x| x.as_bool()) { cfg.native_f64 = b; } + if let Some(b) = v.get("native_bool").and_then(|x| x.as_bool()) { cfg.native_bool = b; } + if let Some(b) = v.get("native_bool_abi").and_then(|x| x.as_bool()) { cfg.native_bool_abi = b; } + if let Some(b) = v.get("ret_bool_b1").and_then(|x| x.as_bool()) { cfg.ret_bool_b1 = b; } + Ok(Box::new(VoidBox::new())) + } + pub fn apply(&self) -> Box { + let cfg = self.config.read().unwrap().clone(); + // Apply to env for CLI parity + cfg.apply_env(); + // Also set global current JIT config for hot paths (env-less) + crate::jit::config::set_current(cfg); + Box::new(VoidBox::new()) + } + pub fn summary(&self) -> Box { + let cfg = self.config.read().unwrap(); + let s = format!( + "exec={} stats={} json={} dump={} thr={:?} phi_min={} hostcall={} hdbg={} f64={} bool={}", + cfg.exec, cfg.stats, cfg.stats_json, cfg.dump, cfg.threshold, + cfg.phi_min, cfg.hostcall, cfg.handle_debug, cfg.native_f64, cfg.native_bool + ); + Box::new(StringBox::new(s)) + } +} + +impl BoxCore for JitConfigBox { + fn box_id(&self) -> u64 { self.base.id } + fn parent_type_id(&self) -> Option { self.base.parent_type_id } + fn fmt_box(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "JitConfigBox") } + fn as_any(&self) -> &dyn Any { self } + fn as_any_mut(&mut self) -> &mut dyn Any { self } +} + +impl NyashBox for JitConfigBox { + fn to_string_box(&self) -> StringBox { StringBox::new(self.summary().to_string_box().value) } + fn equals(&self, other: &dyn NyashBox) -> BoolBox { BoolBox::new(other.as_any().is::()) } + fn type_name(&self) -> &'static str { "JitConfigBox" } + fn clone_box(&self) -> Box { + let cfg = self.config.read().unwrap().clone(); + Box::new(JitConfigBox { base: self.base.clone(), config: RwLock::new(cfg) }) + } + fn share_box(&self) -> Box { self.clone_box() } +} diff --git a/src/boxes/jit_events_box.rs b/src/boxes/jit_events_box.rs new file mode 100644 index 00000000..08c06d9a --- /dev/null +++ b/src/boxes/jit_events_box.rs @@ -0,0 +1,41 @@ +use crate::box_trait::{NyashBox, StringBox, BoolBox, VoidBox, BoxCore, BoxBase}; +use std::any::Any; + +#[derive(Debug, Clone)] +pub struct JitEventsBox { base: BoxBase } + +impl JitEventsBox { pub fn new() -> Self { Self { base: BoxBase::new() } } } + +impl BoxCore for JitEventsBox { + fn box_id(&self) -> u64 { self.base.id } + fn parent_type_id(&self) -> Option { self.base.parent_type_id } + fn fmt_box(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "JitEventsBox") } + fn as_any(&self) -> &dyn Any { self } + fn as_any_mut(&mut self) -> &mut dyn Any { self } +} + +impl NyashBox for JitEventsBox { + fn to_string_box(&self) -> StringBox { StringBox::new("JitEventsBox") } + fn equals(&self, other: &dyn NyashBox) -> BoolBox { BoolBox::new(other.as_any().is::()) } + fn type_name(&self) -> &'static str { "JitEventsBox" } + fn clone_box(&self) -> Box { Box::new(Self { base: self.base.clone() }) } + fn share_box(&self) -> Box { self.clone_box() } +} + +impl JitEventsBox { + pub fn set_path(&self, path: &str) -> Box { + std::env::set_var("NYASH_JIT_EVENTS_PATH", path); + Box::new(VoidBox::new()) + } + pub fn enable(&self, on: bool) -> Box { + if on { std::env::set_var("NYASH_JIT_EVENTS", "1"); } + else { std::env::remove_var("NYASH_JIT_EVENTS"); } + Box::new(VoidBox::new()) + } + pub fn log(&self, kind: &str, function: &str, note_json: &str) -> Box { + let extra = serde_json::from_str::(note_json).unwrap_or_else(|_| serde_json::json!({"note": note_json})); + crate::jit::events::emit(kind, function, None, None, extra); + Box::new(VoidBox::new()) + } +} + diff --git a/src/boxes/jit_policy_box.rs b/src/boxes/jit_policy_box.rs new file mode 100644 index 00000000..51e2a0f3 --- /dev/null +++ b/src/boxes/jit_policy_box.rs @@ -0,0 +1,52 @@ +use crate::box_trait::{NyashBox, StringBox, BoolBox, VoidBox, BoxCore, BoxBase}; +use std::any::Any; + +#[derive(Debug, Clone)] +pub struct JitPolicyBox { base: BoxBase } + +impl JitPolicyBox { pub fn new() -> Self { Self { base: BoxBase::new() } } } + +impl BoxCore for JitPolicyBox { + fn box_id(&self) -> u64 { self.base.id } + fn parent_type_id(&self) -> Option { self.base.parent_type_id } + fn fmt_box(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "JitPolicyBox") } + fn as_any(&self) -> &dyn Any { self } + fn as_any_mut(&mut self) -> &mut dyn Any { self } +} + +impl NyashBox for JitPolicyBox { + fn to_string_box(&self) -> StringBox { + let p = crate::jit::policy::current(); + let s = format!("read_only={} whitelist={}", p.read_only, p.hostcall_whitelist.join(",")); + StringBox::new(s) + } + fn equals(&self, other: &dyn NyashBox) -> BoolBox { BoolBox::new(other.as_any().is::()) } + fn type_name(&self) -> &'static str { "JitPolicyBox" } + fn clone_box(&self) -> Box { Box::new(Self { base: self.base.clone() }) } + fn share_box(&self) -> Box { self.clone_box() } +} + +// Methods (exposed via VM dispatch): +impl JitPolicyBox { + pub fn set_flag(&self, name: &str, on: bool) -> Box { + let mut cur = crate::jit::policy::current(); + match name { + "read_only" | "readonly" => cur.read_only = on, + _ => return Box::new(StringBox::new(format!("Unknown flag: {}", name))) + } + crate::jit::policy::set_current(cur); + Box::new(VoidBox::new()) + } + pub fn get_flag(&self, name: &str) -> Box { + let cur = crate::jit::policy::current(); + let v = match name { "read_only" | "readonly" => cur.read_only, _ => false }; + Box::new(BoolBox::new(v)) + } + pub fn set_whitelist_csv(&self, csv: &str) -> Box { + let mut cur = crate::jit::policy::current(); + cur.hostcall_whitelist = csv.split(',').map(|t| t.trim().to_string()).filter(|s| !s.is_empty()).collect(); + crate::jit::policy::set_current(cur); + Box::new(VoidBox::new()) + } +} + diff --git a/src/boxes/jit_stats_box.rs b/src/boxes/jit_stats_box.rs new file mode 100644 index 00000000..51f0936a --- /dev/null +++ b/src/boxes/jit_stats_box.rs @@ -0,0 +1,41 @@ +use crate::box_trait::{NyashBox, StringBox, BoolBox, IntegerBox, VoidBox, BoxCore, BoxBase}; +use std::any::Any; + +#[derive(Debug, Clone)] +pub struct JitStatsBox { base: BoxBase } + +impl JitStatsBox { + pub fn new() -> Self { Self { base: BoxBase::new() } } + pub fn to_json(&self) -> Box { + let cfg = crate::jit::config::current(); + let caps = crate::jit::config::probe_capabilities(); + let mode = if cfg.native_bool_abi && caps.supports_b1_sig { "b1_bool" } else { "i64_bool" }; + let payload = serde_json::json!({ + "version": 1, + "abi_mode": mode, + "abi_b1_enabled": cfg.native_bool_abi, + "abi_b1_supported": caps.supports_b1_sig, + "b1_norm_count": crate::jit::rt::b1_norm_get(), + "ret_bool_hint_count": crate::jit::rt::ret_bool_hint_get(), + "phi_total_slots": crate::jit::rt::phi_total_get(), + "phi_b1_slots": crate::jit::rt::phi_b1_get(), + }); + Box::new(StringBox::new(payload.to_string())) + } +} + +impl BoxCore for JitStatsBox { + fn box_id(&self) -> u64 { self.base.id } + fn parent_type_id(&self) -> Option { self.base.parent_type_id } + fn fmt_box(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "JitStatsBox") } + fn as_any(&self) -> &dyn Any { self } + fn as_any_mut(&mut self) -> &mut dyn Any { self } +} + +impl NyashBox for JitStatsBox { + fn to_string_box(&self) -> StringBox { StringBox::new(self.to_json().to_string_box().value) } + fn equals(&self, other: &dyn NyashBox) -> BoolBox { BoolBox::new(other.as_any().is::()) } + fn type_name(&self) -> &'static str { "JitStatsBox" } + fn clone_box(&self) -> Box { Box::new(self.clone()) } + fn share_box(&self) -> Box { self.clone_box() } +} diff --git a/src/boxes/mod.rs b/src/boxes/mod.rs index e2ff6347..d3042baa 100644 --- a/src/boxes/mod.rs +++ b/src/boxes/mod.rs @@ -74,6 +74,10 @@ pub mod qr_box; pub mod sound_box; pub mod map_box; pub mod console_box; +pub mod jit_config_box; +pub mod jit_stats_box; +pub mod jit_policy_box; +pub mod jit_events_box; // Web専用Box群(ブラウザ環境でのみ利用可能) #[cfg(target_arch = "wasm32")] @@ -104,6 +108,10 @@ pub use qr_box::QRBox; pub use sound_box::SoundBox; pub use map_box::MapBox; pub use console_box::ConsoleBox; +pub use jit_config_box::JitConfigBox; +pub use jit_stats_box::JitStatsBox; +pub use jit_policy_box::JitPolicyBox; +pub use jit_events_box::JitEventsBox; // EguiBoxの再エクスポート(非WASM環境のみ) #[cfg(all(feature = "gui", not(target_arch = "wasm32")))] diff --git a/src/boxes/p2p_box.rs b/src/boxes/p2p_box.rs index 48d29849..9ec65435 100644 --- a/src/boxes/p2p_box.rs +++ b/src/boxes/p2p_box.rs @@ -122,9 +122,34 @@ impl P2PBox { last_intent_name: Arc::new(RwLock::new(None)), }; - // Note: InProcess callback registration is postponed until a unified - // Transport subscription API is provided. For now, loopback tracing is - // handled in send() when sending to self. + // Minimal built-in system handler: auto-respond to sys.ping + // This enables health checks via ping() without requiring user wiring. + if attach_cb { + // capture for receive-side traces + let last_from = Arc::clone(&p2p.last_from); + let last_intent = Arc::clone(&p2p.last_intent_name); + // capture transport Arc to use inside handler + let transport_arc_outer = Arc::clone(&p2p.transport); + { + if let Ok(mut t) = transport_arc_outer.write() { + let transport_arc_for_cb = Arc::clone(&transport_arc_outer); + t.register_intent_handler("sys.ping", Box::new(move |env| { + if let Ok(mut lf) = last_from.write() { *lf = Some(env.from.clone()); } + if let Ok(mut li) = last_intent.write() { *li = Some(env.intent.get_name().to_string_box().value); } + // Reply asynchronously to avoid deep call stacks + let to = env.from.clone(); + let reply = crate::boxes::IntentBox::new("sys.pong".to_string(), serde_json::json!({})); + let transport_arc = Arc::clone(&transport_arc_for_cb); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(1)); + if let Ok(transport) = transport_arc.read() { + let _ = transport.send(&to, reply, Default::default()); + } + }); + })); + }; + } + } p2p } @@ -134,6 +159,51 @@ impl P2PBox { let node_id = self.node_id.read().unwrap().clone(); Box::new(StringBox::new(node_id)) } + + /// Blocking ping: send sys.ping to target and wait for sys.pong + /// Returns BoolBox(true) on success within timeout, else false. + pub fn ping_with_timeout(&self, to: Box, timeout_ms: u64) -> Box { + use std::sync::{mpsc, Arc}; + let to_str = to.to_string_box().value; + + // Create oneshot channel for pong + let (tx, rx) = mpsc::channel::<()>(); + let active = Arc::new(AtomicBool::new(true)); + let active_cb = Arc::clone(&active); + + // Register temporary transport-level handler for sys.pong + if let Ok(mut t) = self.transport.write() { + t.register_intent_handler("sys.pong", Box::new(move |env| { + if active_cb.load(Ordering::SeqCst) { + // record last receive for visibility + // Note: we cannot access self here safely; rely on tx notify only + let _ = env; // suppress unused + let _ = tx.send(()); + } + })); + + // Send sys.ping + let ping = IntentBox::new("sys.ping".to_string(), serde_json::json!({})); + match t.send(&to_str, ping, Default::default()) { + Ok(()) => { /* proceed to wait */ } + Err(_) => { + return Box::new(BoolBox::new(false)); + } + } + } else { + return Box::new(BoolBox::new(false)); + } + + // Wait for pong with timeout + let ok = rx.recv_timeout(std::time::Duration::from_millis(timeout_ms)).is_ok(); + active.store(false, Ordering::SeqCst); + Box::new(BoolBox::new(ok)) + } + + /// Convenience default-timeout ping (200ms) + pub fn ping(&self, to: Box) -> Box { + self.ping_with_timeout(to, 200) + } /// 特定ノードにメッセージを送信 pub fn send(&self, to: Box, intent: Box) -> Box { @@ -453,4 +523,28 @@ mod tests { let c1 = p.debug_active_handler_count(Box::new(StringBox::new("bye"))); assert_eq!(c1.to_string_box().value, "0"); } + + #[test] + fn ping_success_between_two_nodes() { + let alice = P2PBox::new("alice".to_string(), TransportKind::InProcess); + let bob = P2PBox::new("bob".to_string(), TransportKind::InProcess); + // bob has built-in sys.ping -> sys.pong + let ok = alice.ping(Box::new(StringBox::new("bob"))); + if let Some(b) = ok.as_any().downcast_ref::() { + assert!(b.value); + } else { + panic!("ping did not return BoolBox"); + } + } + + #[test] + fn ping_timeout_on_missing_node() { + let alice = P2PBox::new("alice".to_string(), TransportKind::InProcess); + let ok = alice.ping_with_timeout(Box::new(StringBox::new("nobody")), 20); + if let Some(b) = ok.as_any().downcast_ref::() { + assert!(!b.value); + } else { + panic!("ping_with_timeout did not return BoolBox"); + } + } } diff --git a/src/cli.rs b/src/cli.rs index 74f87197..e45d958c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -26,6 +26,21 @@ pub struct CliConfig { pub iterations: u32, pub vm_stats: bool, pub vm_stats_json: bool, + // JIT controls + pub jit_exec: bool, + pub jit_stats: bool, + pub jit_stats_json: bool, + pub jit_dump: bool, + pub jit_threshold: Option, + pub jit_phi_min: bool, + pub jit_hostcall: bool, + pub jit_handle_debug: bool, + pub jit_native_f64: bool, + pub jit_native_bool: bool, + pub jit_only: bool, + pub jit_direct: bool, + // DOT emit helper + pub emit_cfg: Option, } impl CliConfig { @@ -147,6 +162,84 @@ impl CliConfig { .help("Output VM statistics in JSON format") .action(clap::ArgAction::SetTrue) ) + .arg( + Arg::new("jit-exec") + .long("jit-exec") + .help("Enable JIT execution where available (NYASH_JIT_EXEC=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-stats") + .long("jit-stats") + .help("Print JIT compilation/execution statistics (NYASH_JIT_STATS=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-stats-json") + .long("jit-stats-json") + .help("Output JIT statistics in JSON format (NYASH_JIT_STATS_JSON=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-dump") + .long("jit-dump") + .help("Dump JIT lowering summary (NYASH_JIT_DUMP=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-threshold") + .long("jit-threshold") + .value_name("N") + .help("Set hotness threshold for JIT compilation (NYASH_JIT_THRESHOLD)") + ) + .arg( + Arg::new("jit-phi-min") + .long("jit-phi-min") + .help("Enable minimal PHI path for branches (NYASH_JIT_PHI_MIN=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-hostcall") + .long("jit-hostcall") + .help("Enable JIT hostcall bridge for Array/Map (NYASH_JIT_HOSTCALL=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-handle-debug") + .long("jit-handle-debug") + .help("Print JIT handle allocation debug logs (NYASH_JIT_HANDLE_DEBUG=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-native-f64") + .long("jit-native-f64") + .help("Enable native f64 ABI path in JIT (NYASH_JIT_NATIVE_F64=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-native-bool") + .long("jit-native-bool") + .help("Enable native bool ABI path in JIT (NYASH_JIT_NATIVE_BOOL=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-only") + .long("jit-only") + .help("Run JIT only (no VM fallback). Fails if JIT is unavailable (NYASH_JIT_ONLY=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-direct") + .long("jit-direct") + .help("Run program via independent JIT engine (no VM interpreter/executor). Requires --features cranelift-jit") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("emit-cfg") + .long("emit-cfg") + .value_name("DOT_FILE") + .help("Emit JIT CFG as DOT to file (equivalent to setting NYASH_JIT_DOT)") + ) } /// Convert ArgMatches to CliConfig @@ -168,6 +261,19 @@ impl CliConfig { iterations: matches.get_one::("iterations").unwrap().parse().unwrap_or(10), vm_stats: matches.get_flag("vm-stats"), vm_stats_json: matches.get_flag("vm-stats-json"), + jit_exec: matches.get_flag("jit-exec"), + jit_stats: matches.get_flag("jit-stats"), + jit_stats_json: matches.get_flag("jit-stats-json"), + jit_dump: matches.get_flag("jit-dump"), + jit_threshold: matches.get_one::("jit-threshold").and_then(|s| s.parse::().ok()), + jit_phi_min: matches.get_flag("jit-phi-min"), + jit_hostcall: matches.get_flag("jit-hostcall"), + jit_handle_debug: matches.get_flag("jit-handle-debug"), + jit_native_f64: matches.get_flag("jit-native-f64"), + jit_native_bool: matches.get_flag("jit-native-bool"), + emit_cfg: matches.get_one::("emit-cfg").cloned(), + jit_only: matches.get_flag("jit-only"), + jit_direct: matches.get_flag("jit-direct"), } } } @@ -213,6 +319,16 @@ mod tests { iterations: 10, vm_stats: false, vm_stats_json: false, + jit_exec: false, + jit_stats: false, + jit_stats_json: false, + jit_dump: false, + jit_threshold: None, + jit_phi_min: false, + jit_hostcall: false, + jit_handle_debug: false, + jit_native_f64: false, + jit_native_bool: false, }; assert_eq!(config.backend, "interpreter"); diff --git a/src/interpreter/methods/p2p_methods.rs b/src/interpreter/methods/p2p_methods.rs index 3e39d9f0..290b4e78 100644 --- a/src/interpreter/methods/p2p_methods.rs +++ b/src/interpreter/methods/p2p_methods.rs @@ -76,6 +76,21 @@ impl NyashInterpreter { Ok(p2p_box.send(to_result, intent_result)) } + // ping: health check using sys.ping/sys.pong + "ping" => { + if arguments.is_empty() { + return Err(RuntimeError::InvalidOperation { message: "ping requires (to [, timeout_ms]) arguments".to_string() }); + } + let to_result = self.execute_expression(&arguments[0])?; + if arguments.len() >= 2 { + let tmo_val = self.execute_expression(&arguments[1])?; + let tmo_ms = tmo_val.to_string_box().value.parse::().unwrap_or(200); + Ok(p2p_box.ping_with_timeout(to_result, tmo_ms)) + } else { + Ok(p2p_box.ping(to_result)) + } + } + // on メソッド実装(ResultBox返却) "on" => { if arguments.len() < 2 { diff --git a/src/jit/abi.rs b/src/jit/abi.rs new file mode 100644 index 00000000..a400d9b2 --- /dev/null +++ b/src/jit/abi.rs @@ -0,0 +1,62 @@ +//! JIT minimal ABI types independent from VM internals + +#[derive(Debug, Clone, Copy)] +pub enum JitValue { + I64(i64), + F64(f64), + Bool(bool), + /// Opaque handle for host objects (future use) + Handle(u64), +} + +impl JitValue { + pub fn as_i64(&self) -> Option { if let JitValue::I64(v) = self { Some(*v) } else { None } } +} + +/// Adapter between VMValue and JitValue — keeps JIT decoupled from VM internals +pub mod adapter { + use super::JitValue; + use crate::backend::vm::VMValue; + + pub fn to_jit_values(args: &[VMValue]) -> Vec { + args.iter().map(|v| match v { + VMValue::Integer(i) => JitValue::I64(*i), + VMValue::Float(f) => JitValue::F64(*f), + VMValue::Bool(b) => JitValue::Bool(*b), + VMValue::BoxRef(arc) => { + let h = crate::jit::rt::handles::to_handle(arc.clone()); + JitValue::Handle(h) + } + // For now, map others to handle via boxing where reasonable + VMValue::String(s) => { + let bx = Box::new(crate::box_trait::StringBox::new(s)); + let bx_dyn: Box = bx; + let arc: std::sync::Arc = std::sync::Arc::from(bx_dyn); + let h = crate::jit::rt::handles::to_handle(arc); + JitValue::Handle(h) + } + VMValue::Void => JitValue::Handle(0), + VMValue::Future(f) => { + let bx_dyn: Box = Box::new(f.clone()); + let arc: std::sync::Arc = std::sync::Arc::from(bx_dyn); + let h = crate::jit::rt::handles::to_handle(arc); + JitValue::Handle(h) + } + }).collect() + } + + pub fn from_jit_value(v: JitValue) -> VMValue { + match v { + JitValue::I64(i) => VMValue::Integer(i), + JitValue::F64(f) => VMValue::Float(f), + JitValue::Bool(b) => VMValue::Bool(b), + JitValue::Handle(h) => { + if let Some(arc) = crate::jit::rt::handles::get(h) { + VMValue::BoxRef(arc) + } else { + VMValue::Void + } + } + } + } +} diff --git a/src/jit/config.rs b/src/jit/config.rs new file mode 100644 index 00000000..aec330e4 --- /dev/null +++ b/src/jit/config.rs @@ -0,0 +1,102 @@ +//! JIT configuration aggregator +//! +//! Centralizes JIT-related flags so callers and tests can use a single +//! source of truth instead of scattering env access across modules. + +#[derive(Debug, Clone, Default)] +pub struct JitConfig { + pub exec: bool, // NYASH_JIT_EXEC + pub stats: bool, // NYASH_JIT_STATS + pub stats_json: bool, // NYASH_JIT_STATS_JSON + pub dump: bool, // NYASH_JIT_DUMP + pub threshold: Option,// NYASH_JIT_THRESHOLD + pub phi_min: bool, // NYASH_JIT_PHI_MIN + pub hostcall: bool, // NYASH_JIT_HOSTCALL + pub handle_debug: bool, // NYASH_JIT_HANDLE_DEBUG + pub native_f64: bool, // NYASH_JIT_NATIVE_F64 + pub native_bool: bool, // NYASH_JIT_NATIVE_BOOL + pub native_bool_abi: bool, // NYASH_JIT_ABI_B1 (experimental) + pub ret_bool_b1: bool, // NYASH_JIT_RET_B1 (footing; currently returns i64 0/1) +} + +impl JitConfig { + pub fn from_env() -> Self { + let getb = |k: &str| std::env::var(k).ok().as_deref() == Some("1"); + let threshold = std::env::var("NYASH_JIT_THRESHOLD").ok().and_then(|s| s.parse::().ok()); + Self { + exec: getb("NYASH_JIT_EXEC"), + stats: getb("NYASH_JIT_STATS"), + stats_json: getb("NYASH_JIT_STATS_JSON"), + dump: getb("NYASH_JIT_DUMP"), + threshold, + phi_min: getb("NYASH_JIT_PHI_MIN"), + hostcall: getb("NYASH_JIT_HOSTCALL"), + handle_debug: getb("NYASH_JIT_HANDLE_DEBUG"), + native_f64: getb("NYASH_JIT_NATIVE_F64"), + native_bool: getb("NYASH_JIT_NATIVE_BOOL"), + native_bool_abi: getb("NYASH_JIT_ABI_B1"), + ret_bool_b1: getb("NYASH_JIT_RET_B1"), + } + } + + /// Apply current struct values into environment variables. + /// This keeps existing env untouched unless the value is explicitly set here. + pub fn apply_env(&self) { + let setb = |k: &str, v: bool| { if v { std::env::set_var(k, "1"); } }; + setb("NYASH_JIT_EXEC", self.exec); + setb("NYASH_JIT_STATS", self.stats); + setb("NYASH_JIT_STATS_JSON", self.stats_json); + setb("NYASH_JIT_DUMP", self.dump); + if let Some(t) = self.threshold { std::env::set_var("NYASH_JIT_THRESHOLD", t.to_string()); } + setb("NYASH_JIT_PHI_MIN", self.phi_min); + setb("NYASH_JIT_HOSTCALL", self.hostcall); + setb("NYASH_JIT_HANDLE_DEBUG", self.handle_debug); + setb("NYASH_JIT_NATIVE_F64", self.native_f64); + setb("NYASH_JIT_NATIVE_BOOL", self.native_bool); + setb("NYASH_JIT_ABI_B1", self.native_bool_abi); + setb("NYASH_JIT_RET_B1", self.ret_bool_b1); + } +} + +// Global current JIT config (thread-safe), defaults to env when unset +use once_cell::sync::OnceCell; +use std::sync::RwLock; + +static GLOBAL_JIT_CONFIG: OnceCell> = OnceCell::new(); + +/// Get current JIT config (falls back to env-derived default if unset) +pub fn current() -> JitConfig { + if let Some(lock) = GLOBAL_JIT_CONFIG.get() { + if let Ok(cfg) = lock.read() { return cfg.clone(); } + } + JitConfig::from_env() +} + +/// Set current JIT config (overrides env lookups in hot paths) +pub fn set_current(cfg: JitConfig) { + if let Some(lock) = GLOBAL_JIT_CONFIG.get() { + if let Ok(mut w) = lock.write() { *w = cfg; return; } + } + let _ = GLOBAL_JIT_CONFIG.set(RwLock::new(cfg)); +} + +// --- Runtime capability probing (minimal, safe defaults) --- + +#[derive(Debug, Clone, Copy, Default)] +pub struct JitCapabilities { + pub supports_b1_sig: bool, +} + +/// Probe JIT backend capabilities once. Safe default: b1 signatures are unsupported. +pub fn probe_capabilities() -> JitCapabilities { + // Current toolchain: allow forcing via env for experiments; otherwise false. + // When upgrading Cranelift to a version with B1 signature support, set NYASH_JIT_ABI_B1_SUPPORT=1 + let forced = std::env::var("NYASH_JIT_ABI_B1_SUPPORT").ok().as_deref() == Some("1"); + JitCapabilities { supports_b1_sig: forced } +} + +/// Apply runtime capabilities onto a JitConfig (e.g., disable b1 ABI when unsupported) +pub fn apply_runtime_caps(mut cfg: JitConfig, caps: JitCapabilities) -> JitConfig { + if cfg.native_bool_abi && !caps.supports_b1_sig { cfg.native_bool_abi = false; } + cfg +} diff --git a/src/jit/engine.rs b/src/jit/engine.rs index 0d57e55f..dd903459 100644 --- a/src/jit/engine.rs +++ b/src/jit/engine.rs @@ -12,11 +12,15 @@ pub struct JitEngine { initialized: bool, next_handle: u64, /// Stub function table: handle -> callable closure - fntab: HashMap crate::backend::vm::VMValue + Send + Sync>>, + fntab: HashMap crate::jit::abi::JitValue + Send + Sync>>, /// Host externs by symbol name (Phase 10_d) externs: HashMap crate::backend::vm::VMValue + Send + Sync>>, #[cfg(feature = "cranelift-jit")] isa: Option, + // Last lower stats (per function) + last_phi_total: u64, + last_phi_b1: u64, + last_ret_bool_hint: bool, } impl JitEngine { @@ -27,6 +31,9 @@ impl JitEngine { fntab: HashMap::new(), externs: HashMap::new(), #[cfg(feature = "cranelift-jit")] isa: None, + last_phi_total: 0, + last_phi_b1: 0, + last_ret_bool_hint: false, }; #[cfg(feature = "cranelift-jit")] { this.isa = None; } @@ -47,26 +54,46 @@ impl JitEngine { eprintln!("[JIT] lower failed for {}: {}", func_name, e); return None; } - if std::env::var("NYASH_JIT_DUMP").ok().as_deref() == Some("1") { + // Capture per-function lower stats for manager to query later + let (phi_t, phi_b1, ret_b) = lower.last_stats(); + 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(); + if cfg_now.dump { + let phi_min = cfg_now.phi_min; + let native_f64 = cfg_now.native_f64; + let native_bool = cfg_now.native_bool; #[cfg(feature = "cranelift-jit")] { let s = builder.stats; - eprintln!("[JIT] lower {}: covered={} unsupported={} (consts={}, binops={}, cmps={}, branches={}, rets={})", - func_name, lower.covered, lower.unsupported, + eprintln!("[JIT] lower {}: argc={} phi_min={} f64={} bool={} covered={} unsupported={} (consts={}, binops={}, cmps={}, branches={}, rets={})", + func_name, mir.params.len(), phi_min, native_f64, native_bool, + lower.covered, lower.unsupported, s.0, s.1, s.2, s.3, s.4); } #[cfg(not(feature = "cranelift-jit"))] { - eprintln!("[JIT] lower {}: covered={} unsupported={} (consts={}, binops={}, cmps={}, branches={}, rets={})", - func_name, lower.covered, lower.unsupported, + eprintln!("[JIT] lower {}: argc={} phi_min={} f64={} bool={} covered={} unsupported={} (consts={}, binops={}, cmps={}, branches={}, rets={})", + func_name, mir.params.len(), phi_min, native_f64, native_bool, + lower.covered, lower.unsupported, builder.consts, builder.binops, builder.cmps, builder.branches, builder.rets); } + // Optional DOT export + if let Ok(path) = std::env::var("NYASH_JIT_DOT") { + if !path.is_empty() { + if let Err(e) = crate::jit::lower::core::dump_cfg_dot(mir, &path, phi_min) { + eprintln!("[JIT] DOT export failed: {}", e); + } else { + eprintln!("[JIT] DOT written to {}", path); + } + } + } } - // Create a handle and register an executable closure - let h = self.next_handle; - self.next_handle = self.next_handle.saturating_add(1); + // Create a handle and register an executable closure if available #[cfg(feature = "cranelift-jit")] { + let h = self.next_handle; + self.next_handle = self.next_handle.saturating_add(1); if let Some(closure) = builder.take_compiled_closure() { self.fntab.insert(h, closure); if std::env::var("NYASH_JIT_STATS").ok().as_deref() == Some("1") { @@ -75,21 +102,27 @@ impl JitEngine { } return Some(h); } + // If Cranelift path did not produce a closure, treat as not compiled + return None; } - // Fallback: insert a stub closure - self.fntab.insert(h, Arc::new(|_args: &[crate::backend::vm::VMValue]| { - crate::backend::vm::VMValue::Void - })); - if std::env::var("NYASH_JIT_STATS").ok().as_deref() == Some("1") { - let dt = t0.elapsed(); - eprintln!("[JIT] compile_time_ms={} for {} (stub)", dt.as_millis(), func_name); + #[cfg(not(feature = "cranelift-jit"))] + { + // Without Cranelift, do not register a stub that alters program semantics. + // Report as not compiled so VM path remains authoritative. + if std::env::var("NYASH_JIT_STATS").ok().as_deref() == Some("1") { + let dt = t0.elapsed(); + eprintln!("[JIT] compile skipped (no cranelift) for {} after {}ms", func_name, dt.as_millis()); + } + return None; } - Some(h) } + /// Get statistics from the last lowered function + pub fn last_lower_stats(&self) -> (u64, u64, bool) { (self.last_phi_total, self.last_phi_b1, self.last_ret_bool_hint) } + /// Execute compiled function by handle with trap fallback. /// Returns Some(VMValue) if executed successfully; None on missing handle or trap (panic). - pub fn execute_handle(&self, handle: u64, args: &[crate::backend::vm::VMValue]) -> Option { + pub fn execute_handle(&self, handle: u64, args: &[crate::jit::abi::JitValue]) -> Option { let f = match self.fntab.get(&handle) { Some(f) => f, None => return None }; let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (f)(args))); match res { diff --git a/src/jit/events.rs b/src/jit/events.rs new file mode 100644 index 00000000..0b5c98f8 --- /dev/null +++ b/src/jit/events.rs @@ -0,0 +1,42 @@ +//! JIT Events (v0): minimal JSONL appender for compile/execute/fallback/trap +//! +//! Emission is opt-in via env: +//! - NYASH_JIT_EVENTS=1 prints to stdout (one JSON per line) +//! - NYASH_JIT_EVENTS_PATH=/path/to/file.jsonl appends to file + +use serde::Serialize; + +fn should_emit() -> bool { + std::env::var("NYASH_JIT_EVENTS").ok().as_deref() == Some("1") + || std::env::var("NYASH_JIT_EVENTS_PATH").is_ok() +} + +fn write_line(s: &str) { + if let Ok(path) = std::env::var("NYASH_JIT_EVENTS_PATH") { + let _ = std::fs::OpenOptions::new().create(true).append(true).open(path).and_then(|mut f| { + use std::io::Write; + writeln!(f, "{}", s) + }); + } else { + println!("{}", s); + } +} + +#[derive(Serialize)] +struct Event<'a, T: Serialize> { + kind: &'a str, + function: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + handle: Option, + #[serde(skip_serializing_if = "Option::is_none")] + ms: Option, + #[serde(flatten)] + extra: T, +} + +pub fn emit(kind: &str, function: &str, handle: Option, ms: Option, extra: T) { + if !should_emit() { return; } + let ev = Event { kind, function, handle, ms, extra }; + if let Ok(s) = serde_json::to_string(&ev) { write_line(&s); } +} + diff --git a/src/jit/extern/collections.rs b/src/jit/extern/collections.rs index 65b8f1b5..22f8653c 100644 --- a/src/jit/extern/collections.rs +++ b/src/jit/extern/collections.rs @@ -13,6 +13,21 @@ pub const SYM_MAP_GET: &str = "nyash.map.get"; pub const SYM_MAP_SET: &str = "nyash.map.set"; pub const SYM_MAP_SIZE: &str = "nyash.map.size"; +// Handle-based variants for direct JIT bridging +pub const SYM_ARRAY_LEN_H: &str = "nyash.array.len_h"; +pub const SYM_ARRAY_GET_H: &str = "nyash.array.get_h"; +pub const SYM_ARRAY_SET_H: &str = "nyash.array.set_h"; +pub const SYM_ARRAY_PUSH_H: &str = "nyash.array.push_h"; +pub const SYM_ARRAY_LAST_H: &str = "nyash.array.last_h"; +pub const SYM_MAP_SIZE_H: &str = "nyash.map.size_h"; +pub const SYM_MAP_GET_H: &str = "nyash.map.get_h"; +pub const SYM_MAP_SET_H: &str = "nyash.map.set_h"; +pub const SYM_MAP_HAS_H: &str = "nyash.map.has_h"; +// Generic read-only helper +pub const SYM_ANY_LEN_H: &str = "nyash.any.length_h"; +pub const SYM_ANY_IS_EMPTY_H: &str = "nyash.any.is_empty_h"; +pub const SYM_STRING_CHARCODE_AT_H: &str = "nyash.string.charCodeAt_h"; + fn as_array(args: &[VMValue]) -> Option<&crate::boxes::array::ArrayBox> { match args.get(0) { Some(VMValue::BoxRef(b)) => b.as_any().downcast_ref::(), @@ -88,4 +103,3 @@ pub fn map_size(args: &[VMValue]) -> VMValue { } VMValue::Integer(0) } - diff --git a/src/jit/hostcall_registry.rs b/src/jit/hostcall_registry.rs new file mode 100644 index 00000000..bc323210 --- /dev/null +++ b/src/jit/hostcall_registry.rs @@ -0,0 +1,18 @@ +//! Minimal hostcall registry (v0): classify symbols as read-only or mutating + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HostcallKind { ReadOnly, Mutating } + +pub fn classify(symbol: &str) -> HostcallKind { + match symbol { + // Read-only (safe under read_only policy) + "nyash.array.len_h" | "nyash.any.length_h" | "nyash.any.is_empty_h" | + "nyash.map.size_h" | "nyash.map.get_h" | "nyash.string.charCodeAt_h" | + "nyash.array.get_h" => HostcallKind::ReadOnly, + // Mutating + "nyash.array.push_h" | "nyash.array.set_h" | "nyash.map.set_h" => HostcallKind::Mutating, + // Default to read-only to be permissive in v0 + _ => HostcallKind::ReadOnly, + } +} + diff --git a/src/jit/lower/builder.rs b/src/jit/lower/builder.rs index 45f1ed81..ef9e9d24 100644 --- a/src/jit/lower/builder.rs +++ b/src/jit/lower/builder.rs @@ -9,11 +9,16 @@ pub enum BinOpKind { Add, Sub, Mul, Div, Mod } #[derive(Debug, Clone, Copy)] pub enum CmpKind { Eq, Ne, Lt, Le, Gt, Ge } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParamKind { I64, F64, B1 } + pub trait IRBuilder { fn begin_function(&mut self, name: &str); fn end_function(&mut self); /// Optional: prepare a simple `i64` ABI signature with `argc` params fn prepare_signature_i64(&mut self, _argc: usize, _has_ret: bool) { } + /// Optional: prepare typed ABI signature for params and f64 return flag + fn prepare_signature_typed(&mut self, _params: &[ParamKind], _ret_is_f64: bool) { } /// Load i64 parameter at index and push to value stack (Core-1 path) fn emit_param_i64(&mut self, _index: usize) { } fn emit_const_i64(&mut self, _val: i64); @@ -36,10 +41,18 @@ pub trait IRBuilder { fn br_if_top_is_true(&mut self, _then_index: usize, _else_index: usize) { } /// Optional: unconditional jump to target block index fn jump_to(&mut self, _target_index: usize) { } - /// Optional: ensure target block has one i64 block param (for minimal PHI) - fn ensure_block_param_i64(&mut self, _index: usize) { } - /// Optional: push current block's first param (i64) onto the value stack - fn push_block_param_i64(&mut self) { } + /// Optional: ensure target block has N i64 block params (for PHI) + fn ensure_block_params_i64(&mut self, _index: usize, _count: usize) { } + /// Optional: ensure target block has N b1 block params (for PHI of bool) + fn ensure_block_params_b1(&mut self, index: usize, count: usize) { self.ensure_block_params_i64(index, count); } + /// Optional: ensure target block has one i64 block param (backward compat) + fn ensure_block_param_i64(&mut self, index: usize) { self.ensure_block_params_i64(index, 1); } + /// Optional: push current block's param at position onto the value stack (default=0) + fn push_block_param_i64_at(&mut self, _pos: usize) { } + /// Optional: push current block's boolean param (b1) at position; default converts i64 0/1 → b1 + fn push_block_param_b1_at(&mut self, _pos: usize) { self.push_block_param_i64_at(_pos); } + /// Optional: push current block's first param (i64) onto the value stack (backward compat) + fn push_block_param_i64(&mut self) { self.push_block_param_i64_at(0); } /// Optional: conditional branch with explicit arg counts for then/else; pops args from stack fn br_if_with_args(&mut self, _then_index: usize, _else_index: usize, _then_n: usize, _else_n: usize) { // fallback to no-arg br_if @@ -47,6 +60,16 @@ pub trait IRBuilder { } /// Optional: jump with explicit arg count; pops args from stack fn jump_with_args(&mut self, _target_index: usize, _n: usize) { self.jump_to(_target_index); } + /// Optional: hint that function returns a boolean (b1) value (footing only) + fn hint_ret_bool(&mut self, _is_b1: bool) { } + + // ==== Minimal local slots for Load/Store (i64 only) ==== + /// Ensure an i64 local slot exists for the given index + fn ensure_local_i64(&mut self, _index: usize) { } + /// Store top-of-stack (normalized to i64) into local slot + fn store_local_i64(&mut self, _index: usize) { } + /// Load i64 from local slot and push to stack + fn load_local_i64(&mut self, _index: usize) { } } pub struct NoopBuilder { @@ -72,6 +95,9 @@ impl IRBuilder for NoopBuilder { fn emit_jump(&mut self) { self.branches += 1; } fn emit_branch(&mut self) { self.branches += 1; } fn emit_return(&mut self) { self.rets += 1; } + fn ensure_local_i64(&mut self, _index: usize) { /* no-op */ } + fn store_local_i64(&mut self, _index: usize) { self.consts += 1; } + fn load_local_i64(&mut self, _index: usize) { self.consts += 1; } } #[cfg(feature = "cranelift-jit")] @@ -88,11 +114,17 @@ pub struct CraneliftBuilder { blocks: Vec, current_block_index: Option, block_param_counts: std::collections::HashMap, + // Local stack slots for minimal Load/Store lowering (i64 only) + local_slots: std::collections::HashMap, // Finalized function pointer (if any) - compiled_closure: Option crate::backend::vm::VMValue + Send + Sync>>, + compiled_closure: Option crate::jit::abi::JitValue + Send + Sync>>, // Desired simple ABI (Phase 10_c minimal): i64 params count and i64 return desired_argc: usize, desired_has_ret: bool, + desired_ret_is_f64: bool, + typed_sig_prepared: bool, + // Return-type hint: function returns boolean (footing only; ABI remains i64 for now) + ret_hint_is_b1: bool, } #[cfg(feature = "cranelift-jit")] @@ -106,7 +138,7 @@ extern "C" fn nyash_host_stub0() -> i64 { 0 } extern "C" fn nyash_array_len(arr_param_index: i64) -> i64 { // Interpret first arg as function param index and fetch from thread-local args if arr_param_index < 0 { return 0; } - crate::jit::rt::with_args(|args| { + crate::jit::rt::with_legacy_vm_args(|args| { let idx = arr_param_index as usize; if let Some(crate::backend::vm::VMValue::BoxRef(b)) = args.get(idx) { if let Some(ab) = b.as_any().downcast_ref::() { @@ -121,7 +153,7 @@ extern "C" fn nyash_array_len(arr_param_index: i64) -> i64 { #[cfg(feature = "cranelift-jit")] extern "C" fn nyash_array_push(arr_param_index: i64, val: i64) -> i64 { if arr_param_index < 0 { return 0; } - crate::jit::rt::with_args(|args| { + crate::jit::rt::with_legacy_vm_args(|args| { let idx = arr_param_index as usize; if let Some(crate::backend::vm::VMValue::BoxRef(b)) = args.get(idx) { if let Some(ab) = b.as_any().downcast_ref::() { @@ -137,7 +169,7 @@ extern "C" fn nyash_array_push(arr_param_index: i64, val: i64) -> i64 { #[cfg(feature = "cranelift-jit")] extern "C" fn nyash_array_get(arr_param_index: i64, idx: i64) -> i64 { if arr_param_index < 0 { return 0; } - crate::jit::rt::with_args(|args| { + crate::jit::rt::with_legacy_vm_args(|args| { let pidx = arr_param_index as usize; if let Some(crate::backend::vm::VMValue::BoxRef(b)) = args.get(pidx) { if let Some(ab) = b.as_any().downcast_ref::() { @@ -153,7 +185,7 @@ extern "C" fn nyash_array_get(arr_param_index: i64, idx: i64) -> i64 { #[cfg(feature = "cranelift-jit")] extern "C" fn nyash_array_set(arr_param_index: i64, idx: i64, val: i64) -> i64 { if arr_param_index < 0 { return 0; } - crate::jit::rt::with_args(|args| { + crate::jit::rt::with_legacy_vm_args(|args| { let pidx = arr_param_index as usize; if let Some(crate::backend::vm::VMValue::BoxRef(b)) = args.get(pidx) { if let Some(ab) = b.as_any().downcast_ref::() { @@ -174,7 +206,7 @@ extern "C" fn nyash_map_set(_map: u64, _key: i64, _val: i64) -> i64 { 0 } #[cfg(feature = "cranelift-jit")] extern "C" fn nyash_map_size(map_param_index: i64) -> i64 { if map_param_index < 0 { return 0; } - crate::jit::rt::with_args(|args| { + crate::jit::rt::with_legacy_vm_args(|args| { let idx = map_param_index as usize; if let Some(crate::backend::vm::VMValue::BoxRef(b)) = args.get(idx) { if let Some(mb) = b.as_any().downcast_ref::() { @@ -187,8 +219,234 @@ extern "C" fn nyash_map_size(map_param_index: i64) -> i64 { }) } +// === Handle-based externs (10.7c) === +#[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_array_len_h(handle: u64) -> i64 { + if let Some(obj) = crate::jit::rt::handles::get(handle) { + if let Some(arr) = obj.as_any().downcast_ref::() { + if let Some(ib) = arr.length().as_any().downcast_ref::() { return ib.value; } + } + } + 0 +} +#[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_array_push_h(handle: u64, val: i64) -> i64 { + // Policy/Events: classify and decide + use crate::jit::hostcall_registry::{classify, HostcallKind}; + let sym = crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H; + match (classify(sym), crate::jit::policy::current().read_only) { + (HostcallKind::Mutating, true) => { + crate::jit::events::emit("hostcall", "", None, None, serde_json::json!({"id": sym, "decision":"fallback"})); + return 0; + } + _ => {} + } + if let Some(obj) = crate::jit::rt::handles::get(handle) { + if let Some(arr) = obj.as_any().downcast_ref::() { + let ib = crate::box_trait::IntegerBox::new(val); + let _ = arr.push(Box::new(ib)); + crate::jit::events::emit("hostcall", "", None, None, serde_json::json!({"id": sym, "decision":"allow"})); + return 0; + } + } + 0 +} +#[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_array_get_h(handle: u64, idx: i64) -> i64 { + if let Some(obj) = crate::jit::rt::handles::get(handle) { + if let Some(arr) = obj.as_any().downcast_ref::() { + let val = arr.get(Box::new(crate::box_trait::IntegerBox::new(idx))); + if let Some(ib) = val.as_any().downcast_ref::() { return ib.value; } + } + } + 0 +} +#[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_array_last_h(handle: u64) -> i64 { + if let Some(obj) = crate::jit::rt::handles::get(handle) { + if let Some(arr) = obj.as_any().downcast_ref::() { + // Return last element as i64 if IntegerBox, else 0 + if let Ok(items) = arr.items.read() { + if let Some(last) = items.last() { + if let Some(ib) = last.as_any().downcast_ref::() { + return ib.value; + } + } + } + } + } + 0 +} +#[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_array_set_h(handle: u64, idx: i64, val: i64) -> i64 { + use crate::jit::hostcall_registry::{classify, HostcallKind}; + let sym = crate::jit::r#extern::collections::SYM_ARRAY_SET_H; + if classify(sym) == HostcallKind::Mutating && crate::jit::policy::current().read_only { + crate::jit::events::emit("hostcall", "", None, None, serde_json::json!({"id": sym, "decision":"fallback"})); + return 0; + } + if let Some(obj) = crate::jit::rt::handles::get(handle) { + if let Some(arr) = obj.as_any().downcast_ref::() { + let _ = arr.set( + Box::new(crate::box_trait::IntegerBox::new(idx)), + Box::new(crate::box_trait::IntegerBox::new(val)), + ); + crate::jit::events::emit("hostcall", "", None, None, serde_json::json!({"id": sym, "decision":"allow"})); + return 0; + } + } + 0 +} +#[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_map_size_h(handle: u64) -> i64 { + crate::jit::events::emit("hostcall", "", None, None, serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SIZE_H, "decision":"allow"})); + if let Some(obj) = crate::jit::rt::handles::get(handle) { + if let Some(map) = obj.as_any().downcast_ref::() { + if let Some(ib) = map.size().as_any().downcast_ref::() { return ib.value; } + } + } + 0 +} +#[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_map_get_h(handle: u64, key: i64) -> i64 { + crate::jit::events::emit("hostcall", "", None, None, serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_GET_H, "decision":"allow"})); + if let Some(obj) = crate::jit::rt::handles::get(handle) { + if let Some(map) = obj.as_any().downcast_ref::() { + let key_box = Box::new(crate::box_trait::IntegerBox::new(key)); + let val = map.get(key_box); + if let Some(ib) = val.as_any().downcast_ref::() { return ib.value; } + } + } + 0 +} +#[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_map_set_h(handle: u64, key: i64, val: i64) -> i64 { + use crate::jit::hostcall_registry::{classify, HostcallKind}; + let sym = crate::jit::r#extern::collections::SYM_MAP_SET_H; + if classify(sym) == HostcallKind::Mutating && crate::jit::policy::current().read_only { + crate::jit::events::emit("hostcall", "", None, None, serde_json::json!({"id": sym, "decision":"fallback"})); + return 0; + } + if let Some(obj) = crate::jit::rt::handles::get(handle) { + if let Some(map) = obj.as_any().downcast_ref::() { + let key_box = Box::new(crate::box_trait::IntegerBox::new(key)); + let val_box = Box::new(crate::box_trait::IntegerBox::new(val)); + let _ = map.set(key_box, val_box); + crate::jit::events::emit("hostcall", "", None, None, serde_json::json!({"id": sym, "decision":"allow"})); + return 0; + } + } + 0 +} +#[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_map_has_h(handle: u64, key: i64) -> i64 { + if let Some(obj) = crate::jit::rt::handles::get(handle) { + if let Some(map) = obj.as_any().downcast_ref::() { + let key_box = Box::new(crate::box_trait::IntegerBox::new(key)); + let val = map.get(key_box); + // Treat presence if result is not Void + let is_present = !val.as_any().is::(); + return if is_present { 1 } else { 0 }; + } + } + 0 +} +#[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_any_length_h(handle: u64) -> i64 { + crate::jit::events::emit("hostcall", "", None, None, serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_LEN_H, "decision":"allow"})); + if let Some(obj) = crate::jit::rt::handles::get(handle) { + // Array length + if let Some(arr) = obj.as_any().downcast_ref::() { + if let Some(ib) = arr.length().as_any().downcast_ref::() { return ib.value; } + } + // String length + if let Some(sb) = obj.as_any().downcast_ref::() { + return sb.value.len() as i64; + } + } + 0 +} +#[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_any_is_empty_h(handle: u64) -> i64 { + crate::jit::events::emit("hostcall", "", None, None, serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_IS_EMPTY_H, "decision":"allow"})); + if let Some(obj) = crate::jit::rt::handles::get(handle) { + // Array empty? + if let Some(arr) = obj.as_any().downcast_ref::() { + if let Ok(items) = arr.items.read() { return if items.is_empty() { 1 } else { 0 }; } + } + // String empty? + if let Some(sb) = obj.as_any().downcast_ref::() { + return if sb.value.is_empty() { 1 } else { 0 }; + } + // Map empty? + if let Some(map) = obj.as_any().downcast_ref::() { + if let Some(ib) = map.size().as_any().downcast_ref::() { return if ib.value == 0 { 1 } else { 0 }; } + } + } + 0 +} +#[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_string_charcode_at_h(handle: u64, idx: i64) -> i64 { + crate::jit::events::emit("hostcall", "", None, None, serde_json::json!({"id": crate::jit::r#extern::collections::SYM_STRING_CHARCODE_AT_H, "decision":"allow"})); + if idx < 0 { return -1; } + if let Some(obj) = crate::jit::rt::handles::get(handle) { + if let Some(sb) = obj.as_any().downcast_ref::() { + let s = &sb.value; + let i = idx as usize; + if i < s.len() { + // Return UTF-8 byte at index (ASCII-friendly PoC) + return s.as_bytes()[i] as i64; + } else { return -1; } + } + } + -1 +} + #[cfg(feature = "cranelift-jit")] impl IRBuilder for CraneliftBuilder { + fn prepare_signature_typed(&mut self, params: &[ParamKind], ret_is_f64: bool) { + use cranelift_codegen::ir::{AbiParam, Signature, types}; + fn abi_param_for_kind(k: ParamKind, cfg: &crate::jit::config::JitConfig) -> cranelift_codegen::ir::AbiParam { + use cranelift_codegen::ir::types; + match k { + ParamKind::I64 => cranelift_codegen::ir::AbiParam::new(types::I64), + ParamKind::F64 => cranelift_codegen::ir::AbiParam::new(types::F64), + ParamKind::B1 => { + let _ = cfg.native_bool_abi; + #[cfg(feature = "jit-b1-abi")] + { + if crate::jit::config::probe_capabilities().supports_b1_sig && cfg.native_bool_abi { return cranelift_codegen::ir::AbiParam::new(types::B1); } + } + cranelift_codegen::ir::AbiParam::new(types::I64) + } + } + } + self.desired_argc = params.len(); + self.desired_has_ret = true; + self.desired_ret_is_f64 = ret_is_f64; + let call_conv = self.module.isa().default_call_conv(); + let mut sig = Signature::new(call_conv); + let cfg_now = crate::jit::config::current(); + for &k in params { sig.params.push(abi_param_for_kind(k, &cfg_now)); } + if self.desired_has_ret { + // Decide return ABI: prefer F64 if requested; otherwise Bool may use B1 when supported + if self.desired_ret_is_f64 { sig.returns.push(AbiParam::new(types::F64)); } + else { + let mut used_b1 = false; + #[cfg(feature = "jit-b1-abi")] + { + let cfg_now = crate::jit::config::current(); + if crate::jit::config::probe_capabilities().supports_b1_sig && cfg_now.native_bool_abi && self.ret_hint_is_b1 { + sig.returns.push(AbiParam::new(types::B1)); + used_b1 = true; + } + } + if !used_b1 { sig.returns.push(AbiParam::new(types::I64)); } + } + } + self.ctx.func.signature = sig; + self.typed_sig_prepared = true; + } fn emit_param_i64(&mut self, index: usize) { if let Some(v) = self.entry_param(index) { self.value_stack.push(v); @@ -197,6 +455,7 @@ impl IRBuilder for CraneliftBuilder { fn prepare_signature_i64(&mut self, argc: usize, has_ret: bool) { self.desired_argc = argc; self.desired_has_ret = has_ret; + self.desired_ret_is_f64 = crate::jit::config::current().native_f64; } fn begin_function(&mut self, name: &str) { use cranelift_codegen::ir::{AbiParam, Signature, types}; @@ -204,14 +463,31 @@ impl IRBuilder for CraneliftBuilder { self.current_name = Some(name.to_string()); self.value_stack.clear(); - // Keep any pre-created blocks (from prepare_blocks) + // Keep any pre-created blocks (from prepare_blocks or typed signature) - // Minimal signature: (i64 x argc) -> i64? (Core-1 integer path) - let call_conv = self.module.isa().default_call_conv(); - let mut sig = Signature::new(call_conv); - for _ in 0..self.desired_argc { sig.params.push(AbiParam::new(types::I64)); } - if self.desired_has_ret { sig.returns.push(AbiParam::new(types::I64)); } - self.ctx.func.signature = sig; + // Build default signature only if a typed one wasn't prepared + if !self.typed_sig_prepared { + // Minimal signature: (i64 x argc) -> i64? (Core-1 integer path) + let call_conv = self.module.isa().default_call_conv(); + let mut sig = Signature::new(call_conv); + for _ in 0..self.desired_argc { sig.params.push(AbiParam::new(types::I64)); } + if self.desired_has_ret { + if self.desired_ret_is_f64 { sig.returns.push(AbiParam::new(types::F64)); } + else { + let mut used_b1 = false; + #[cfg(feature = "jit-b1-abi")] + { + let cfg_now = crate::jit::config::current(); + if crate::jit::config::probe_capabilities().supports_b1_sig && cfg_now.native_bool_abi && self.ret_hint_is_b1 { + sig.returns.push(AbiParam::new(types::B1)); + used_b1 = true; + } + } + if !used_b1 { sig.returns.push(AbiParam::new(types::I64)); } + } + } + self.ctx.func.signature = sig; + } self.ctx.func.name = cranelift_codegen::ir::UserFuncName::user(0, 0); let mut fb = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbc); @@ -253,15 +529,69 @@ impl IRBuilder for CraneliftBuilder { // Get finalized code pointer and wrap into a safe closure let code = self.module.get_finalized_function(func_id); - // SAFETY: We compiled a function with simple i64 ABI; we still call without args for now + // SAFETY: We compiled a function with simple (i64 x N) -> i64/f64 というABIだよ。 + // ランタイムでは JitValue から i64 へ正規化して、引数個数に応じた関数型にtransmuteして呼び出すにゃ。 + let argc = self.desired_argc; + let ret_is_f64 = self.desired_has_ret && self.desired_ret_is_f64; + // capture code as usize to avoid raw pointer Send/Sync issues in closure + let code_usize = code as usize; unsafe { - let f: extern "C" fn() -> i64 = std::mem::transmute(code); - let closure = std::sync::Arc::new(move |_args: &[crate::backend::vm::VMValue]| -> crate::backend::vm::VMValue { - let v = f(); - crate::backend::vm::VMValue::Integer(v) + let closure = std::sync::Arc::new(move |args: &[crate::jit::abi::JitValue]| -> crate::jit::abi::JitValue { + // 正規化: 足りなければ0で埋め、余分は切り捨て + let mut a: [i64; 6] = [0; 6]; + let take = core::cmp::min(core::cmp::min(argc, 6), args.len()); + for i in 0..take { + a[i] = match args[i] { crate::jit::abi::JitValue::I64(v) => v, crate::jit::abi::JitValue::Bool(b) => if b {1} else {0}, crate::jit::abi::JitValue::F64(f) => f as i64, crate::jit::abi::JitValue::Handle(h) => h as i64 }; + } + let ret_i64 = match argc { + 0 => { + let f: extern "C" fn() -> i64 = std::mem::transmute(code_usize); + f() + } + 1 => { + let f: extern "C" fn(i64) -> i64 = std::mem::transmute(code_usize); + f(a[0]) + } + 2 => { + let f: extern "C" fn(i64, i64) -> i64 = std::mem::transmute(code_usize); + f(a[0], a[1]) + } + 3 => { + let f: extern "C" fn(i64, i64, i64) -> i64 = std::mem::transmute(code_usize); + f(a[0], a[1], a[2]) + } + 4 => { + let f: extern "C" fn(i64, i64, i64, i64) -> i64 = std::mem::transmute(code_usize); + f(a[0], a[1], a[2], a[3]) + } + 5 => { + let f: extern "C" fn(i64, i64, i64, i64, i64) -> i64 = std::mem::transmute(code_usize); + f(a[0], a[1], a[2], a[3], a[4]) + } + _ => { + // 上限6(十分なPoC) + let f: extern "C" fn(i64, i64, i64, i64, i64, i64) -> i64 = std::mem::transmute(code_usize); + f(a[0], a[1], a[2], a[3], a[4], a[5]) + } + }; + if ret_is_f64 { + let ret_f64 = match argc { + 0 => { let f: extern "C" fn() -> f64 = std::mem::transmute(code_usize); f() } + 1 => { let f: extern "C" fn(i64) -> f64 = std::mem::transmute(code_usize); f(a[0]) } + 2 => { let f: extern "C" fn(i64,i64) -> f64 = std::mem::transmute(code_usize); f(a[0],a[1]) } + 3 => { let f: extern "C" fn(i64,i64,i64) -> f64 = std::mem::transmute(code_usize); f(a[0],a[1],a[2]) } + 4 => { let f: extern "C" fn(i64,i64,i64,i64) -> f64 = std::mem::transmute(code_usize); f(a[0],a[1],a[2],a[3]) } + 5 => { let f: extern "C" fn(i64,i64,i64,i64,i64) -> f64 = std::mem::transmute(code_usize); f(a[0],a[1],a[2],a[3],a[4]) } + _ => { let f: extern "C" fn(i64,i64,i64,i64,i64,i64) -> f64 = std::mem::transmute(code_usize); f(a[0],a[1],a[2],a[3],a[4],a[5]) } + }; + return crate::jit::abi::JitValue::F64(ret_f64); + } + crate::jit::abi::JitValue::I64(ret_i64) }); self.compiled_closure = Some(closure); } + // Reset typed signature flag for next function + self.typed_sig_prepared = false; } fn emit_const_i64(&mut self, val: i64) { @@ -277,22 +607,56 @@ impl IRBuilder for CraneliftBuilder { fb.finalize(); } - fn emit_const_f64(&mut self, _val: f64) { self.stats.0 += 1; /* not yet supported in Core-1 */ } - - fn emit_binop(&mut self, op: BinOpKind) { + fn emit_const_f64(&mut self, val: f64) { + self.stats.0 += 1; + if !crate::jit::config::current().native_f64 { return; } + use cranelift_codegen::ir::types; use cranelift_frontend::FunctionBuilder; - if self.value_stack.len() < 2 { return; } - let rhs = self.value_stack.pop().unwrap(); - let lhs = self.value_stack.pop().unwrap(); 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 res = match op { - BinOpKind::Add => fb.ins().iadd(lhs, rhs), - BinOpKind::Sub => fb.ins().isub(lhs, rhs), - BinOpKind::Mul => fb.ins().imul(lhs, rhs), - BinOpKind::Div => fb.ins().sdiv(lhs, rhs), - BinOpKind::Mod => fb.ins().srem(lhs, rhs), + let v = fb.ins().f64const(val); + self.value_stack.push(v); + fb.finalize(); + } + + fn emit_binop(&mut self, op: BinOpKind) { + use cranelift_frontend::FunctionBuilder; + use cranelift_codegen::ir::types; + if self.value_stack.len() < 2 { return; } + let mut rhs = self.value_stack.pop().unwrap(); + let mut lhs = self.value_stack.pop().unwrap(); + 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); } + // Choose op by operand type (I64 vs F64). If mixed and native_f64, promote to F64. + let lty = fb.func.dfg.value_type(lhs); + let rty = fb.func.dfg.value_type(rhs); + let native_f64 = crate::jit::config::current().native_f64; + let mut use_f64 = native_f64 && (lty == types::F64 || rty == types::F64); + if use_f64 { + if lty != types::F64 { lhs = fb.ins().fcvt_from_sint(types::F64, lhs); } + if rty != types::F64 { rhs = fb.ins().fcvt_from_sint(types::F64, rhs); } + } + let res = if use_f64 { + match op { + BinOpKind::Add => fb.ins().fadd(lhs, rhs), + BinOpKind::Sub => fb.ins().fsub(lhs, rhs), + BinOpKind::Mul => fb.ins().fmul(lhs, rhs), + BinOpKind::Div => fb.ins().fdiv(lhs, rhs), + BinOpKind::Mod => { + // Minimal path: produce 0.0 (fmod未実装)。将来はホストコール/Libcallに切替 + fb.ins().f64const(0.0) + } + } + } else { + match op { + BinOpKind::Add => fb.ins().iadd(lhs, rhs), + BinOpKind::Sub => fb.ins().isub(lhs, rhs), + BinOpKind::Mul => fb.ins().imul(lhs, rhs), + BinOpKind::Div => fb.ins().sdiv(lhs, rhs), + BinOpKind::Mod => fb.ins().srem(lhs, rhs), + } }; self.value_stack.push(res); self.stats.1 += 1; @@ -300,24 +664,42 @@ impl IRBuilder for CraneliftBuilder { } fn emit_compare(&mut self, op: CmpKind) { - use cranelift_codegen::ir::{condcodes::IntCC}; + use cranelift_codegen::ir::{condcodes::{IntCC, FloatCC}, types}; use cranelift_frontend::FunctionBuilder; if self.value_stack.len() < 2 { return; } - let rhs = self.value_stack.pop().unwrap(); - let lhs = self.value_stack.pop().unwrap(); + let mut rhs = self.value_stack.pop().unwrap(); + let mut lhs = self.value_stack.pop().unwrap(); 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 cc = match op { - CmpKind::Eq => IntCC::Equal, - CmpKind::Ne => IntCC::NotEqual, - CmpKind::Lt => IntCC::SignedLessThan, - CmpKind::Le => IntCC::SignedLessThanOrEqual, - CmpKind::Gt => IntCC::SignedGreaterThan, - CmpKind::Ge => IntCC::SignedGreaterThanOrEqual, + let lty = fb.func.dfg.value_type(lhs); + let rty = fb.func.dfg.value_type(rhs); + let native_f64 = crate::jit::config::current().native_f64; + let use_f64 = native_f64 && (lty == types::F64 || rty == types::F64); + let b1 = if use_f64 { + if lty != types::F64 { lhs = fb.ins().fcvt_from_sint(types::F64, lhs); } + if rty != types::F64 { rhs = fb.ins().fcvt_from_sint(types::F64, rhs); } + let cc = match op { + CmpKind::Eq => FloatCC::Equal, + CmpKind::Ne => FloatCC::NotEqual, + CmpKind::Lt => FloatCC::LessThan, + CmpKind::Le => FloatCC::LessThanOrEqual, + CmpKind::Gt => FloatCC::GreaterThan, + CmpKind::Ge => FloatCC::GreaterThanOrEqual, + }; + fb.ins().fcmp(cc, lhs, rhs) + } else { + let cc = match op { + CmpKind::Eq => IntCC::Equal, + CmpKind::Ne => IntCC::NotEqual, + CmpKind::Lt => IntCC::SignedLessThan, + CmpKind::Le => IntCC::SignedLessThanOrEqual, + CmpKind::Gt => IntCC::SignedGreaterThan, + CmpKind::Ge => IntCC::SignedGreaterThanOrEqual, + }; + fb.ins().icmp(cc, lhs, rhs) }; - let b1 = fb.ins().icmp(cc, lhs, rhs); - // Keep b1 on the stack; users (branch) can consume directly, arithmetic should not assume compare value as i64 + // Keep b1 on the stack; users (branch) can consume directly self.value_stack.push(b1); self.stats.2 += 1; fb.finalize(); @@ -331,13 +713,44 @@ impl IRBuilder for CraneliftBuilder { 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); } - if let Some(v) = self.value_stack.pop() { + if let Some(mut v) = self.value_stack.pop() { + // Normalize return type if needed + let ret_ty = fb.func.signature.returns.get(0).map(|p| p.value_type).unwrap_or(cranelift_codegen::ir::types::I64); + let v_ty = fb.func.dfg.value_type(v); + if v_ty != ret_ty { + use cranelift_codegen::ir::types; + if ret_ty == types::F64 && v_ty == types::I64 { + v = fb.ins().fcvt_from_sint(types::F64, v); + } else if ret_ty == types::I64 && v_ty == types::F64 { + v = fb.ins().fcvt_to_sint(types::I64, v); + } else if ret_ty == types::I64 { + // If returning i64 but we currently have a boolean, normalize via select(b1,1,0) + use cranelift_codegen::ir::types; + let one = fb.ins().iconst(types::I64, 1); + let zero = fb.ins().iconst(types::I64, 0); + v = fb.ins().select(v, one, zero); + } + #[cfg(feature = "jit-b1-abi")] + { + use cranelift_codegen::ir::types; + if ret_ty == types::B1 && v_ty == types::I64 { + use cranelift_codegen::ir::condcodes::IntCC; + v = fb.ins().icmp_imm(IntCC::NotEqual, v, 0); + } + } + } fb.ins().return_(&[v]); } else { // Return 0 if empty stack (defensive) use cranelift_codegen::ir::types; - let zero = fb.ins().iconst(types::I64, 0); - fb.ins().return_(&[zero]); + let ret_ty = fb.func.signature.returns.get(0).map(|p| p.value_type).unwrap_or(types::I64); + if ret_ty == types::F64 { + let z = fb.ins().f64const(0.0); + fb.ins().return_(&[z]); + } else { + let zero = fb.ins().iconst(types::I64, 0); + fb.ins().return_(&[zero]); + } } fb.finalize(); } @@ -416,14 +829,18 @@ impl IRBuilder for CraneliftBuilder { let cond_b1 = if let Some(v) = self.value_stack.pop() { let ty = fb.func.dfg.value_type(v); if ty == types::I64 { - fb.ins().icmp_imm(IntCC::NotEqual, v, 0) + let out = fb.ins().icmp_imm(IntCC::NotEqual, v, 0); + crate::jit::rt::b1_norm_inc(1); + out } else { // assume already b1 v } } else { let zero = fb.ins().iconst(types::I64, 0); - fb.ins().icmp_imm(IntCC::NotEqual, zero, 0) + let out = fb.ins().icmp_imm(IntCC::NotEqual, zero, 0); + crate::jit::rt::b1_norm_inc(1); + out }; fb.ins().brif(cond_b1, self.blocks[then_index], &[], self.blocks[else_index], &[]); self.stats.3 += 1; @@ -440,34 +857,60 @@ impl IRBuilder for CraneliftBuilder { fb.finalize(); } fn ensure_block_param_i64(&mut self, index: usize) { + self.ensure_block_params_i64(index, 1); + } + fn ensure_block_params_i64(&mut self, index: usize, needed: usize) { use cranelift_codegen::ir::types; use cranelift_frontend::FunctionBuilder; if index >= self.blocks.len() { return; } let mut fb = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbc); - let count = self.block_param_counts.get(&index).copied().unwrap_or(0); - if count == 0 { + let have = self.block_param_counts.get(&index).copied().unwrap_or(0); + if needed > have { let b = self.blocks[index]; - let _v = fb.append_block_param(b, types::I64); - self.block_param_counts.insert(index, 1); + for _ in have..needed { + let _v = fb.append_block_param(b, types::I64); + } + self.block_param_counts.insert(index, needed); } fb.finalize(); } - fn push_block_param_i64(&mut self) { + fn ensure_block_params_b1(&mut self, index: usize, needed: usize) { + // Store as i64 block params for ABI stability; consumers can convert to b1 + self.ensure_block_params_i64(index, needed); + } + fn push_block_param_i64_at(&mut self, pos: usize) { use cranelift_frontend::FunctionBuilder; use cranelift_codegen::ir::types; let mut fb = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbc); - // Determine current block let b = if let Some(idx) = self.current_block_index { self.blocks[idx] } else if let Some(b) = self.entry_block { b } else { fb.create_block() }; - // Fetch first param if exists + // Ensure we have an active insertion point before emitting any instructions + fb.switch_to_block(b); let params = fb.func.dfg.block_params(b).to_vec(); - if let Some(v) = params.get(0).copied() { self.value_stack.push(v); } + if let Some(v) = params.get(pos).copied() { self.value_stack.push(v); } else { - // defensive: push 0 + // defensive fallback let zero = fb.ins().iconst(types::I64, 0); self.value_stack.push(zero); } fb.finalize(); } + fn push_block_param_b1_at(&mut self, pos: usize) { + use cranelift_frontend::FunctionBuilder; + use cranelift_codegen::ir::{types, condcodes::IntCC}; + let mut fb = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbc); + let b = if let Some(idx) = self.current_block_index { self.blocks[idx] } else if let Some(b) = self.entry_block { b } else { fb.create_block() }; + let params = fb.func.dfg.block_params(b).to_vec(); + if let Some(v) = params.get(pos).copied() { + let ty = fb.func.dfg.value_type(v); + let b1 = if ty == types::I64 { fb.ins().icmp_imm(IntCC::NotEqual, v, 0) } else { v }; + self.value_stack.push(b1); + } else { + let zero = fb.ins().iconst(types::I64, 0); + let b1 = fb.ins().icmp_imm(IntCC::NotEqual, zero, 0); + self.value_stack.push(b1); + } + fb.finalize(); + } fn br_if_with_args(&mut self, then_index: usize, else_index: usize, then_n: usize, else_n: usize) { use cranelift_codegen::ir::{types, condcodes::IntCC}; use cranelift_frontend::FunctionBuilder; @@ -478,10 +921,12 @@ impl IRBuilder for CraneliftBuilder { // Condition let cond_b1 = if let Some(v) = self.value_stack.pop() { let ty = fb.func.dfg.value_type(v); - if ty == types::I64 { fb.ins().icmp_imm(IntCC::NotEqual, v, 0) } else { v } + if ty == types::I64 { let out = fb.ins().icmp_imm(IntCC::NotEqual, v, 0); crate::jit::rt::b1_norm_inc(1); out } else { v } } else { let zero = fb.ins().iconst(types::I64, 0); - fb.ins().icmp_imm(IntCC::NotEqual, zero, 0) + let out = fb.ins().icmp_imm(IntCC::NotEqual, zero, 0); + crate::jit::rt::b1_norm_inc(1); + out }; // Pop else args then then args (so stack order can be value-friendly) let mut else_args: Vec = Vec::new(); @@ -507,6 +952,58 @@ impl IRBuilder for CraneliftBuilder { self.stats.3 += 1; fb.finalize(); } + + fn hint_ret_bool(&mut self, is_b1: bool) { self.ret_hint_is_b1 = is_b1; } + + fn ensure_local_i64(&mut self, index: usize) { + use cranelift_codegen::ir::{StackSlotData, StackSlotKind}; + use cranelift_frontend::FunctionBuilder; + if self.local_slots.contains_key(&index) { return; } + let mut fb = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbc); + let slot = fb.create_sized_stack_slot(StackSlotData::new(StackSlotKind::ExplicitSlot, 8)); + self.local_slots.insert(index, slot); + fb.finalize(); + } + fn store_local_i64(&mut self, index: usize) { + use cranelift_codegen::ir::{types, condcodes::IntCC}; + use cranelift_frontend::FunctionBuilder; + if let Some(mut v) = self.value_stack.pop() { + // Ensure slot without overlapping FunctionBuilder borrows + if !self.local_slots.contains_key(&index) { self.ensure_local_i64(index); } + let slot = self.local_slots.get(&index).copied(); + 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 ty = fb.func.dfg.value_type(v); + if ty != types::I64 { + if ty == types::F64 { + v = fb.ins().fcvt_to_sint(types::I64, v); + } else { + // Convert unknown ints/bools to i64 via (v!=0)?1:0 + let one = fb.ins().iconst(types::I64, 1); + let zero = fb.ins().iconst(types::I64, 0); + let b1 = fb.ins().icmp_imm(IntCC::NotEqual, v, 0); + v = fb.ins().select(b1, one, zero); + } + } + if let Some(slot) = slot { fb.ins().stack_store(v, slot, 0); } + fb.finalize(); + } + } + fn load_local_i64(&mut self, index: usize) { + use cranelift_codegen::ir::types; + use cranelift_frontend::FunctionBuilder; + if !self.local_slots.contains_key(&index) { self.ensure_local_i64(index); } + if let Some(&slot) = self.local_slots.get(&index) { + 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 v = fb.ins().stack_load(types::I64, slot, 0); + self.value_stack.push(v); + self.stats.0 += 1; + fb.finalize(); + } + } } #[cfg(feature = "cranelift-jit")] @@ -542,6 +1039,19 @@ impl CraneliftBuilder { builder.symbol(c::SYM_MAP_GET, nyash_map_get as *const u8); builder.symbol(c::SYM_MAP_SET, nyash_map_set as *const u8); builder.symbol(c::SYM_MAP_SIZE, nyash_map_size as *const u8); + // Handle-based symbols + builder.symbol(c::SYM_ARRAY_LEN_H, nyash_array_len_h as *const u8); + builder.symbol(c::SYM_ARRAY_GET_H, nyash_array_get_h as *const u8); + builder.symbol(c::SYM_ARRAY_SET_H, nyash_array_set_h as *const u8); + builder.symbol(c::SYM_ARRAY_PUSH_H, nyash_array_push_h as *const u8); + builder.symbol(c::SYM_ARRAY_LAST_H, nyash_array_last_h as *const u8); + builder.symbol(c::SYM_MAP_SIZE_H, nyash_map_size_h as *const u8); + builder.symbol(c::SYM_MAP_GET_H, nyash_map_get_h as *const u8); + builder.symbol(c::SYM_MAP_SET_H, nyash_map_set_h as *const u8); + builder.symbol(c::SYM_MAP_HAS_H, nyash_map_has_h as *const u8); + builder.symbol(c::SYM_ANY_LEN_H, nyash_any_length_h as *const u8); + builder.symbol(c::SYM_ANY_IS_EMPTY_H, nyash_any_is_empty_h as *const u8); + builder.symbol(c::SYM_STRING_CHARCODE_AT_H, nyash_string_charcode_at_h as *const u8); } let module = cranelift_jit::JITModule::new(builder); let ctx = cranelift_codegen::Context::new(); @@ -555,14 +1065,18 @@ impl CraneliftBuilder { blocks: Vec::new(), current_block_index: None, block_param_counts: std::collections::HashMap::new(), + local_slots: std::collections::HashMap::new(), compiled_closure: None, desired_argc: 0, desired_has_ret: true, + desired_ret_is_f64: false, + typed_sig_prepared: false, + ret_hint_is_b1: false, } } /// Take ownership of compiled closure if available - pub fn take_compiled_closure(&mut self) -> Option crate::backend::vm::VMValue + Send + Sync>> { + pub fn take_compiled_closure(&mut self) -> Option crate::jit::abi::JitValue + Send + Sync>> { self.compiled_closure.take() } } diff --git a/src/jit/lower/core.rs b/src/jit/lower/core.rs index da9b83b7..ff7823c8 100644 --- a/src/jit/lower/core.rs +++ b/src/jit/lower/core.rs @@ -12,15 +12,33 @@ pub struct LowerCore { param_index: std::collections::HashMap, /// Track values produced by Phi (for minimal PHI path) phi_values: std::collections::HashSet, + /// Map (block, phi dst) -> param index in that block (for multi-PHI) + phi_param_index: std::collections::HashMap<(crate::mir::BasicBlockId, ValueId), usize>, + /// Track values that are boolean (b1) results, e.g., Compare destinations + bool_values: std::collections::HashSet, + /// Track PHI destinations that are boolean (all inputs derived from bool_values) + bool_phi_values: std::collections::HashSet, + // Per-function statistics (last lowered) + last_phi_total: u64, + last_phi_b1: u64, + last_ret_bool_hint_used: bool, + // Minimal local slot mapping for Load/Store (ptr ValueId -> slot index) + local_index: std::collections::HashMap, + next_local: usize, } impl LowerCore { - pub fn new() -> Self { Self { unsupported: 0, covered: 0, known_i64: std::collections::HashMap::new(), param_index: std::collections::HashMap::new(), phi_values: std::collections::HashSet::new() } } + pub fn new() -> Self { Self { unsupported: 0, covered: 0, known_i64: std::collections::HashMap::new(), param_index: std::collections::HashMap::new(), phi_values: std::collections::HashSet::new(), phi_param_index: std::collections::HashMap::new(), bool_values: std::collections::HashSet::new(), bool_phi_values: std::collections::HashSet::new(), last_phi_total: 0, last_phi_b1: 0, last_ret_bool_hint_used: false, local_index: std::collections::HashMap::new(), next_local: 0 } } + + /// Get statistics for the last lowered function + pub fn last_stats(&self) -> (u64, u64, bool) { (self.last_phi_total, self.last_phi_b1, self.last_ret_bool_hint_used) } /// Walk the MIR function and count supported/unsupported instructions. /// In the future, this will build CLIF via Cranelift builders. pub fn lower_function(&mut self, func: &MirFunction, builder: &mut dyn IRBuilder) -> Result<(), String> { // Prepare a simple i64 ABI based on param count; always assume i64 return for now + // Reset per-function stats + self.last_phi_total = 0; self.last_phi_b1 = 0; self.last_ret_bool_hint_used = false; // Build param index map self.param_index.clear(); for (i, v) in func.params.iter().copied().enumerate() { @@ -30,34 +48,221 @@ impl LowerCore { let mut bb_ids: Vec<_> = func.blocks.keys().copied().collect(); bb_ids.sort_by_key(|b| b.0); builder.prepare_blocks(bb_ids.len()); - // Optional: collect single-PHI targets for minimal PHI path - let enable_phi_min = std::env::var("NYASH_JIT_PHI_MIN").ok().as_deref() == Some("1"); - let mut phi_targets: std::collections::HashMap> = std::collections::HashMap::new(); - if enable_phi_min { - for (bb_id, bb) in func.blocks.iter() { - // gather Phi instructions in this block - let mut phis: Vec<&crate::mir::MirInstruction> = Vec::new(); - for ins in bb.instructions.iter() { if let crate::mir::MirInstruction::Phi { .. } = ins { phis.push(ins); } } - if phis.len() == 1 { - if let crate::mir::MirInstruction::Phi { inputs, .. } = phis[0] { - let mut map: std::collections::HashMap = std::collections::HashMap::new(); - for (pred, val) in inputs.iter() { map.insert(*pred, *val); } - phi_targets.insert(*bb_id, map); + // Seed boolean lattice with boolean parameters from MIR signature + if !func.signature.params.is_empty() { + for (idx, vid) in func.params.iter().copied().enumerate() { + if let Some(mt) = func.signature.params.get(idx) { + if matches!(mt, crate::mir::MirType::Bool) { + self.bool_values.insert(vid); } } } } - builder.prepare_signature_i64(func.params.len(), true); + // Pre-scan to classify boolean-producing values and propagate via Copy/Phi/Load-Store heuristics. + self.bool_values.clear(); + let mut copy_edges: Vec<(crate::mir::ValueId, crate::mir::ValueId)> = Vec::new(); + let mut phi_defs: Vec<(crate::mir::ValueId, Vec)> = Vec::new(); + let mut stores: Vec<(crate::mir::ValueId, crate::mir::ValueId)> = Vec::new(); // (ptr, value) + let mut loads: Vec<(crate::mir::ValueId, crate::mir::ValueId)> = Vec::new(); // (dst, ptr) + for bb in bb_ids.iter() { + if let Some(block) = func.blocks.get(bb) { + for ins in block.instructions.iter() { + match ins { + crate::mir::MirInstruction::Compare { dst, .. } => { self.bool_values.insert(*dst); } + crate::mir::MirInstruction::Const { dst, value } => { + if let ConstValue::Bool(_) = value { self.bool_values.insert(*dst); } + } + crate::mir::MirInstruction::Cast { dst, target_type, .. } => { + if matches!(target_type, crate::mir::MirType::Bool) { self.bool_values.insert(*dst); } + } + crate::mir::MirInstruction::TypeOp { dst, op, ty, .. } => { + // Check and cast-to-bool produce boolean + if matches!(op, crate::mir::TypeOpKind::Check) || matches!(ty, crate::mir::MirType::Bool) { self.bool_values.insert(*dst); } + } + crate::mir::MirInstruction::Copy { dst, src } => { copy_edges.push((*dst, *src)); } + crate::mir::MirInstruction::Phi { dst, inputs } => { + let vs: Vec<_> = inputs.iter().map(|(_, v)| *v).collect(); + phi_defs.push((*dst, vs)); + } + crate::mir::MirInstruction::Store { value, ptr } => { stores.push((*ptr, *value)); } + crate::mir::MirInstruction::Load { dst, ptr } => { loads.push((*dst, *ptr)); } + _ => {} + } + } + if let Some(term) = &block.terminator { + match term { + crate::mir::MirInstruction::Compare { dst, .. } => { self.bool_values.insert(*dst); } + crate::mir::MirInstruction::Const { dst, value } => { + if let ConstValue::Bool(_) = value { self.bool_values.insert(*dst); } + } + crate::mir::MirInstruction::Cast { dst, target_type, .. } => { + if matches!(target_type, crate::mir::MirType::Bool) { self.bool_values.insert(*dst); } + } + crate::mir::MirInstruction::TypeOp { dst, op, ty, .. } => { + if matches!(op, crate::mir::TypeOpKind::Check) || matches!(ty, crate::mir::MirType::Bool) { self.bool_values.insert(*dst); } + } + crate::mir::MirInstruction::Copy { dst, src } => { copy_edges.push((*dst, *src)); } + crate::mir::MirInstruction::Phi { dst, inputs } => { + let vs: Vec<_> = inputs.iter().map(|(_, v)| *v).collect(); + phi_defs.push((*dst, vs)); + } + crate::mir::MirInstruction::Branch { condition, .. } => { self.bool_values.insert(*condition); } + crate::mir::MirInstruction::Store { value, ptr } => { stores.push((*ptr, *value)); } + crate::mir::MirInstruction::Load { dst, ptr } => { loads.push((*dst, *ptr)); } + _ => {} + } + } + } + } + // Fixed-point boolean lattice propagation + let mut changed = true; + let mut store_bool_ptrs: std::collections::HashSet = std::collections::HashSet::new(); + while changed { + changed = false; + // Copy propagation + for (dst, src) in copy_edges.iter().copied() { + if self.bool_values.contains(&src) && !self.bool_values.contains(&dst) { + self.bool_values.insert(dst); + changed = true; + } + // Pointer alias propagation for Store/Load lattice + if store_bool_ptrs.contains(&src) && !store_bool_ptrs.contains(&dst) { + store_bool_ptrs.insert(dst); + changed = true; + } + } + // Store marking + for (ptr, val) in stores.iter().copied() { + if self.bool_values.contains(&val) && !store_bool_ptrs.contains(&ptr) { + store_bool_ptrs.insert(ptr); + changed = true; + } + } + // Load propagation + for (dst, ptr) in loads.iter().copied() { + if store_bool_ptrs.contains(&ptr) && !self.bool_values.contains(&dst) { + self.bool_values.insert(dst); + changed = true; + } + } + // PHI closure for value booleans + for (dst, inputs) in phi_defs.iter() { + if inputs.iter().all(|v| self.bool_values.contains(v)) && !self.bool_values.contains(dst) { + self.bool_values.insert(*dst); + self.bool_phi_values.insert(*dst); + changed = true; + } + } + // PHI closure for pointer aliases: if all inputs are bool-storing pointers, mark dst pointer as such + for (dst, inputs) in phi_defs.iter() { + if inputs.iter().all(|v| store_bool_ptrs.contains(v)) && !store_bool_ptrs.contains(dst) { + store_bool_ptrs.insert(*dst); + changed = true; + } + } + } + // Always-on PHI statistics: count total/b1 phi slots using current heuristics + { + use crate::mir::MirInstruction; + let mut total_phi_slots: usize = 0; + let mut total_phi_b1_slots: usize = 0; + for (dst, inputs) in phi_defs.iter() { + total_phi_slots += 1; + // Heuristics consistent with dump path + let used_as_branch = func.blocks.values().any(|bbx| { + if let Some(MirInstruction::Branch { condition, .. }) = &bbx.terminator { condition == dst } else { false } + }); + let is_b1 = self.bool_phi_values.contains(dst) + || inputs.iter().all(|v| { + self.bool_values.contains(v) || self.known_i64.get(v).map(|&iv| iv == 0 || iv == 1).unwrap_or(false) + }) + || used_as_branch; + if is_b1 { total_phi_b1_slots += 1; } + } + if total_phi_slots > 0 { + crate::jit::rt::phi_total_inc(total_phi_slots as u64); + crate::jit::rt::phi_b1_inc(total_phi_b1_slots as u64); + self.last_phi_total = total_phi_slots as u64; + self.last_phi_b1 = total_phi_b1_slots as u64; + } + } + // Optional: collect PHI targets and ordering per successor for minimal/multi PHI path + let cfg_now = crate::jit::config::current(); + let enable_phi_min = cfg_now.phi_min; + // For each successor block, store ordered list of phi dst and a map pred->input for each phi + let mut succ_phi_order: std::collections::HashMap> = std::collections::HashMap::new(); + let mut succ_phi_inputs: std::collections::HashMap> = std::collections::HashMap::new(); + if enable_phi_min { + for (bb_id, bb) in func.blocks.iter() { + let mut order: Vec = Vec::new(); + for ins in bb.instructions.iter() { + if let crate::mir::MirInstruction::Phi { dst, inputs } = ins { + order.push(*dst); + // store all (pred,val) pairs in flat vec grouped by succ + for (pred, val) in inputs.iter() { succ_phi_inputs.entry(*bb_id).or_default().push((*pred, *val)); } + } + } + if !order.is_empty() { succ_phi_order.insert(*bb_id, order); } + } + } + // Decide ABI: typed or i64-only + let native_f64 = cfg_now.native_f64; + let native_bool = cfg_now.native_bool; + let mut use_typed = false; + let mut kinds: Vec = Vec::new(); + for mt in func.signature.params.iter() { + let k = match mt { + crate::mir::MirType::Float if native_f64 => { use_typed = true; super::builder::ParamKind::F64 } + crate::mir::MirType::Bool if native_bool => { use_typed = true; super::builder::ParamKind::B1 } + _ => super::builder::ParamKind::I64, + }; + kinds.push(k); + } + let ret_is_f64 = native_f64 && matches!(func.signature.return_type, crate::mir::MirType::Float); + // Hint return bool footing (no-op in current backend; keeps switch point centralized) + let ret_is_bool = matches!(func.signature.return_type, crate::mir::MirType::Bool); + if ret_is_bool { + builder.hint_ret_bool(true); + // Track how many functions are lowered with boolean return hint (for stats) + crate::jit::rt::ret_bool_hint_inc(1); + self.last_ret_bool_hint_used = true; + } + if use_typed || ret_is_f64 { + builder.prepare_signature_typed(&kinds, ret_is_f64); + } else { + builder.prepare_signature_i64(func.params.len(), true); + } builder.begin_function(&func.signature.name); // Iterate blocks in the sorted order to keep indices stable self.phi_values.clear(); + self.phi_param_index.clear(); for (idx, bb_id) in bb_ids.iter().enumerate() { let bb = func.blocks.get(bb_id).unwrap(); builder.switch_to_block(idx); + // Pre-scan PHIs in this block and ensure block parameters count (multi-PHI) + if enable_phi_min { + let mut local_phi_order: Vec = Vec::new(); + // Also detect boolean PHIs: inputs all from boolean-producing values + for ins in bb.instructions.iter() { + if let crate::mir::MirInstruction::Phi { dst, inputs } = ins { + local_phi_order.push(*dst); + // decide if this phi is boolean + if inputs.iter().all(|(_, v)| self.bool_values.contains(v)) && !inputs.is_empty() { + self.bool_phi_values.insert(*dst); + } + } + } + if !local_phi_order.is_empty() { + builder.ensure_block_params_i64(idx, local_phi_order.len()); + for (i, v) in local_phi_order.into_iter().enumerate() { + self.phi_values.insert(v); + self.phi_param_index.insert((*bb_id, v), i); + } + } + } for instr in bb.instructions.iter() { self.cover_if_supported(instr); - if let MirInstruction::Phi { dst, .. } = instr { self.phi_values.insert(*dst); } - self.try_emit(builder, instr); + self.try_emit(builder, instr, *bb_id); } if let Some(term) = &bb.terminator { self.cover_if_supported(term); @@ -70,14 +275,47 @@ impl LowerCore { let then_index = bb_ids.iter().position(|x| x == then_bb).unwrap_or(0); let else_index = bb_ids.iter().position(|x| x == else_bb).unwrap_or(0); if enable_phi_min { - // For minimal PHI, pass one i64 arg if successor defines a single PHI with this block as pred - let mut then_n = 0usize; - let mut else_n = 0usize; - if let Some(pred_map) = phi_targets.get(then_bb) { - if let Some(v) = pred_map.get(bb_id) { self.push_value_if_known_or_param(builder, v); then_n = 1; builder.ensure_block_param_i64(then_index); } + // For multi-PHI, push args in successor's phi order + let mut then_n = 0usize; let mut else_n = 0usize; + if let Some(order) = succ_phi_order.get(then_bb) { + let mut cnt = 0usize; + for dst in order.iter() { + // find input from current block + if let Some(bb_succ) = func.blocks.get(then_bb) { + // locate the specific phi to read its inputs + for ins in bb_succ.instructions.iter() { + if let crate::mir::MirInstruction::Phi { dst: d2, inputs } = ins { + if d2 == dst { + if let Some((_, val)) = inputs.iter().find(|(pred, _)| pred == bb_id) { + self.push_value_if_known_or_param(builder, val); + cnt += 1; + } + } + } + } + } + } + if cnt > 0 { builder.ensure_block_params_i64(then_index, cnt); } + then_n = cnt; } - if let Some(pred_map) = phi_targets.get(else_bb) { - if let Some(v) = pred_map.get(bb_id) { self.push_value_if_known_or_param(builder, v); else_n = 1; builder.ensure_block_param_i64(else_index); } + if let Some(order) = succ_phi_order.get(else_bb) { + let mut cnt = 0usize; + for dst in order.iter() { + if let Some(bb_succ) = func.blocks.get(else_bb) { + for ins in bb_succ.instructions.iter() { + if let crate::mir::MirInstruction::Phi { dst: d2, inputs } = ins { + if d2 == dst { + if let Some((_, val)) = inputs.iter().find(|(pred, _)| pred == bb_id) { + self.push_value_if_known_or_param(builder, val); + cnt += 1; + } + } + } + } + } + } + if cnt > 0 { builder.ensure_block_params_i64(else_index, cnt); } + else_n = cnt; } builder.br_if_with_args(then_index, else_index, then_n, else_n); } else { @@ -90,8 +328,24 @@ impl LowerCore { let target_index = bb_ids.iter().position(|x| x == target).unwrap_or(0); if enable_phi_min { let mut n = 0usize; - if let Some(pred_map) = phi_targets.get(target) { - if let Some(v) = pred_map.get(bb_id) { self.push_value_if_known_or_param(builder, v); n = 1; builder.ensure_block_param_i64(target_index); } + if let Some(order) = succ_phi_order.get(target) { + let mut cnt = 0usize; + if let Some(bb_succ) = func.blocks.get(target) { + for dst in order.iter() { + for ins in bb_succ.instructions.iter() { + if let crate::mir::MirInstruction::Phi { dst: d2, inputs } = ins { + if d2 == dst { + if let Some((_, val)) = inputs.iter().find(|(pred, _)| pred == bb_id) { + self.push_value_if_known_or_param(builder, val); + cnt += 1; + } + } + } + } + } + } + if cnt > 0 { builder.ensure_block_params_i64(target_index, cnt); } + n = cnt; } builder.jump_with_args(target_index, n); } else { @@ -100,20 +354,72 @@ impl LowerCore { builder.seal_block(target_index); } _ => { - self.try_emit(builder, term); + self.try_emit(builder, term, *bb_id); } } } } builder.end_function(); + if std::env::var("NYASH_JIT_DUMP").ok().as_deref() == Some("1") { + let succs = succ_phi_order.len(); + eprintln!("[JIT] cfg: blocks={} phi_succ={} (phi_min={})", bb_ids.len(), succs, enable_phi_min); + if enable_phi_min { + let mut total_phi_slots: usize = 0; + let mut total_phi_b1_slots: usize = 0; + for (succ, order) in succ_phi_order.iter() { + let mut preds_set: std::collections::BTreeSet = std::collections::BTreeSet::new(); + let mut phi_lines: Vec = Vec::new(); + if let Some(bb_succ) = func.blocks.get(succ) { + for ins in bb_succ.instructions.iter() { + if let crate::mir::MirInstruction::Phi { dst, inputs } = ins { + // collect preds for block-level summary + for (pred, _) in inputs.iter() { preds_set.insert(pred.0 as i64); } + // build detailed mapping text: dst<-pred:val,... + let mut pairs: Vec = Vec::new(); + for (pred, val) in inputs.iter() { + pairs.push(format!("{}:{}", pred.0, val.0)); + } + // Heuristics: boolean PHI if (1) pre-analysis marked it, or + // (2) all inputs look boolean-like (from bool producers or 0/1 const), or + // (3) used as a branch condition somewhere. + let used_as_branch = func.blocks.values().any(|bbx| { + if let Some(MirInstruction::Branch { condition, .. }) = &bbx.terminator { condition == dst } else { false } + }); + let is_b1 = self.bool_phi_values.contains(dst) + || inputs.iter().all(|(_, v)| { + self.bool_values.contains(v) || self.known_i64.get(v).map(|&iv| iv == 0 || iv == 1).unwrap_or(false) + }) + || used_as_branch; + let tag = if is_b1 { " (b1)" } else { "" }; + phi_lines.push(format!(" dst v{}{} <- {}", dst.0, tag, pairs.join(", "))); + total_phi_slots += 1; + if is_b1 { total_phi_b1_slots += 1; } + } + } + } + let preds_list: Vec = preds_set.into_iter().map(|p| p.to_string()).collect(); + eprintln!("[JIT] phi: bb={} slots={} preds={}", succ.0, order.len(), preds_list.join("|")); + for ln in phi_lines { eprintln!("[JIT]{}", ln); } + } + eprintln!("[JIT] phi_summary: total_slots={} b1_slots={}", total_phi_slots, total_phi_b1_slots); + } + } Ok(()) } /// Push a value onto the builder stack if it is a known i64 const or a parameter. fn push_value_if_known_or_param(&self, b: &mut dyn IRBuilder, id: &ValueId) { if self.phi_values.contains(id) { - // Minimal PHI: read current block param - b.push_block_param_i64(); + // Multi-PHI: find the param index for this phi in the current block + // We don't have the current block id here; rely on builder's current block context and our stored index being positional. + // As an approximation, prefer position 0 if unknown. + let pos = self.phi_param_index.iter().find_map(|((_, vid), idx)| if vid == id { Some(*idx) } else { None }).unwrap_or(0); + // Use b1 loader for boolean PHIs when enabled + if crate::jit::config::current().native_bool && self.bool_phi_values.contains(id) { + b.push_block_param_b1_at(pos); + } else { + b.push_block_param_i64_at(pos); + } return; } if let Some(pidx) = self.param_index.get(id).copied() { @@ -131,6 +437,7 @@ impl LowerCore { instr, I::Const { .. } | I::Copy { .. } + | I::Cast { .. } | I::BinOp { .. } | I::Compare { .. } | I::Jump { .. } @@ -142,9 +449,16 @@ impl LowerCore { if supported { self.covered += 1; } else { self.unsupported += 1; } } - fn try_emit(&mut self, b: &mut dyn IRBuilder, instr: &MirInstruction) { + fn try_emit(&mut self, b: &mut dyn IRBuilder, instr: &MirInstruction, cur_bb: crate::mir::BasicBlockId) { use crate::mir::MirInstruction as I; match instr { + I::Cast { dst, value, target_type: _ } => { + // Minimal cast footing: materialize source when param/known + // Bool→Int: rely on producers (compare) and branch/b1 loaders; here we just reuse integer path + self.push_value_if_known_or_param(b, value); + // Track known i64 if source known + if let Some(v) = self.known_i64.get(value).copied() { self.known_i64.insert(*dst, v); } + } I::Const { dst, value } => match value { ConstValue::Integer(i) => { b.emit_const_i64(*i); @@ -155,6 +469,8 @@ impl LowerCore { let iv = if *bv { 1 } else { 0 }; b.emit_const_i64(iv); self.known_i64.insert(*dst, iv); + // Mark this value as boolean producer + self.bool_values.insert(*dst); } ConstValue::String(_) | ConstValue::Null | ConstValue::Void => { // leave unsupported for now @@ -166,6 +482,8 @@ impl LowerCore { if let Some(pidx) = self.param_index.get(src).copied() { b.emit_param_i64(pidx); } + // Propagate boolean classification through Copy + if self.bool_values.contains(src) { self.bool_values.insert(*dst); } // Otherwise no-op for codegen (stack-machine handles sources directly later) } I::BinOp { dst, op, lhs, rhs } => { @@ -208,6 +526,8 @@ impl LowerCore { CompareOp::Ge => CmpKind::Ge, }; b.emit_compare(kind); + // Mark the last dst (compare produces a boolean) + if let MirInstruction::Compare { dst, .. } = instr { self.bool_values.insert(*dst); } } I::Jump { .. } => b.emit_jump(), I::Branch { .. } => b.emit_branch(), @@ -215,53 +535,146 @@ impl LowerCore { if let Some(v) = value { self.push_value_if_known_or_param(b, v); } b.emit_return() } - I::Phi { .. } => { - // Minimal PHI: load current block param as value (i64) - b.push_block_param_i64(); + I::Store { value, ptr } => { + // Minimal lowering: materialize value if known/param and store to a local slot keyed by ptr + self.push_value_if_known_or_param(b, value); + let slot = *self.local_index.entry(*ptr).or_insert_with(|| { let id = self.next_local; self.next_local += 1; id }); + b.ensure_local_i64(slot); + b.store_local_i64(slot); + } + I::Load { dst: _, ptr } => { + // Minimal lowering: load from local slot keyed by ptr, default 0 if unset + let slot = *self.local_index.entry(*ptr).or_insert_with(|| { let id = self.next_local; self.next_local += 1; id }); + b.ensure_local_i64(slot); + b.load_local_i64(slot); + } + I::Phi { dst, .. } => { + // Minimal PHI: load current block param; b1 when classified boolean + let pos = self.phi_param_index.get(&(cur_bb, *dst)).copied().unwrap_or(0); + if self.bool_phi_values.contains(dst) { + b.push_block_param_b1_at(pos); + } else { + b.push_block_param_i64_at(pos); + } } I::ArrayGet { array, index, .. } => { if std::env::var("NYASH_JIT_HOSTCALL").ok().as_deref() == Some("1") { - // Push args: array param index (or -1), index (known or 0) let idx = self.known_i64.get(index).copied().unwrap_or(0); - let arr_idx = self.param_index.get(array).copied().map(|x| x as i64).unwrap_or(-1); - b.emit_const_i64(arr_idx); - b.emit_const_i64(idx); - b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_GET, 2, true); + if let Some(pidx) = self.param_index.get(array).copied() { + // Handle-based: push handle value from param, then index + b.emit_param_i64(pidx); + b.emit_const_i64(idx); + b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_GET_H, 2, true); + } else { + // Fallback to index-based (param index unknown) + let arr_idx = -1; + b.emit_const_i64(arr_idx); + b.emit_const_i64(idx); + b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_GET, 2, true); + } } } I::ArraySet { array, index, value } => { if std::env::var("NYASH_JIT_HOSTCALL").ok().as_deref() == Some("1") { let idx = self.known_i64.get(index).copied().unwrap_or(0); let val = self.known_i64.get(value).copied().unwrap_or(0); - let arr_idx = self.param_index.get(array).copied().map(|x| x as i64).unwrap_or(-1); - b.emit_const_i64(arr_idx); - b.emit_const_i64(idx); - b.emit_const_i64(val); - b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_SET, 3, false); + if let Some(pidx) = self.param_index.get(array).copied() { + b.emit_param_i64(pidx); + b.emit_const_i64(idx); + b.emit_const_i64(val); + b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_SET_H, 3, false); + } else { + let arr_idx = -1; + b.emit_const_i64(arr_idx); + b.emit_const_i64(idx); + b.emit_const_i64(val); + b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_SET, 3, false); + } } } I::BoxCall { box_val: array, method, args, dst, .. } => { if std::env::var("NYASH_JIT_HOSTCALL").ok().as_deref() == Some("1") { match method.as_str() { "len" | "length" => { - // argc=1: (array_param_index) - let arr_idx = self.param_index.get(array).copied().map(|x| x as i64).unwrap_or(-1); - b.emit_const_i64(arr_idx); - b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_LEN, 1, dst.is_some()); + if let Some(pidx) = self.param_index.get(array).copied() { + // Handle-based generic length: supports ArrayBox and StringBox + b.emit_param_i64(pidx); + b.emit_host_call(crate::jit::r#extern::collections::SYM_ANY_LEN_H, 1, dst.is_some()); + } else { + let arr_idx = -1; + b.emit_const_i64(arr_idx); + b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_LEN, 1, dst.is_some()); + } + } + "isEmpty" | "empty" => { + if let Some(pidx) = self.param_index.get(array).copied() { + b.emit_param_i64(pidx); + // returns i64 0/1 + b.emit_host_call(crate::jit::r#extern::collections::SYM_ANY_IS_EMPTY_H, 1, dst.is_some()); + } } "push" => { - // argc=2: (array, value) + // argc=2: (array_handle, value) let val = args.get(0).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); - let arr_idx = self.param_index.get(array).copied().map(|x| x as i64).unwrap_or(-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); + if let Some(pidx) = self.param_index.get(array).copied() { + b.emit_param_i64(pidx); + b.emit_const_i64(val); + b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H, 2, false); + } else { + 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_param_index) - let map_idx = self.param_index.get(array).copied().map(|x| x as i64).unwrap_or(-1); - b.emit_const_i64(map_idx); - b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_SIZE, 1, dst.is_some()); + // MapBox.size(): argc=1 (map_handle) + if let Some(pidx) = self.param_index.get(array).copied() { + b.emit_param_i64(pidx); + b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_SIZE_H, 1, dst.is_some()); + } else { + 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): (map_handle, key_i64) + 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_GET_H, 2, dst.is_some()); + } + } + "set" => { + // MapBox.set(key, value): (map_handle, key_i64, val_i64) — PoC: integer-only + 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); + b.emit_param_i64(pidx); + b.emit_const_i64(key); + b.emit_const_i64(val); + b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_SET_H, 3, false); + } + } + "charCodeAt" => { + // String.charCodeAt(index) + if let Some(pidx) = self.param_index.get(array).copied() { + let idx = args.get(0).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); + b.emit_param_i64(pidx); + b.emit_const_i64(idx); + b.emit_host_call(crate::jit::r#extern::collections::SYM_STRING_CHARCODE_AT_H, 2, dst.is_some()); + } + } + "has" => { + // MapBox.has(key_i64) -> 0/1 + 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()); + } } _ => {} } @@ -271,3 +684,66 @@ impl LowerCore { } } } + +/// Emit a simple DOT graph for a MIR function's CFG. +/// Includes block ids, successor edges, and PHI counts per block when phi_min is enabled. +pub fn dump_cfg_dot(func: &crate::mir::MirFunction, path: &str, phi_min: bool) -> std::io::Result<()> { + use std::io::Write; + let mut out = String::new(); + out.push_str(&format!("digraph \"{}\" {{\n", func.signature.name)); + out.push_str(" node [shape=box, fontsize=10];\n"); + // Derive simple bool sets: compare dsts are bool; phi of all-bool inputs are bool + let mut bool_values: std::collections::HashSet = std::collections::HashSet::new(); + for (_bb_id, bb) in func.blocks.iter() { + for ins in bb.instructions.iter() { + if let crate::mir::MirInstruction::Compare { dst, .. } = ins { bool_values.insert(*dst); } + } + } + let mut bool_phi: std::collections::HashSet = std::collections::HashSet::new(); + if phi_min { + for (_bb_id, bb) in func.blocks.iter() { + for ins in bb.instructions.iter() { + if let crate::mir::MirInstruction::Phi { dst, inputs } = ins { + if !inputs.is_empty() && inputs.iter().all(|(_, v)| bool_values.contains(v)) { + bool_phi.insert(*dst); + } + } + } + } + } + // Sort blocks for deterministic output + let mut bb_ids: Vec<_> = func.blocks.keys().copied().collect(); + bb_ids.sort_by_key(|b| b.0); + // Emit nodes with labels + for bb_id in bb_ids.iter() { + let bb = func.blocks.get(bb_id).unwrap(); + let phi_count = bb.instructions.iter().filter(|ins| matches!(ins, crate::mir::MirInstruction::Phi { .. })).count(); + let phi_b1_count = bb.instructions.iter().filter(|ins| match ins { crate::mir::MirInstruction::Phi { dst, .. } => bool_phi.contains(dst), _ => false }).count(); + let mut label = format!("bb{}", bb_id.0); + if phi_min && phi_count > 0 { + if phi_b1_count > 0 { label = format!("{}\\nphi:{} (b1:{})", label, phi_count, phi_b1_count); } + else { label = format!("{}\\nphi:{}", label, phi_count); } + } + if *bb_id == func.entry_block { label = format!("{}\\nENTRY", label); } + out.push_str(&format!(" n{} [label=\"{}\"];\n", bb_id.0, label)); + } + // Emit edges based on terminators + for bb_id in bb_ids.iter() { + let bb = func.blocks.get(bb_id).unwrap(); + if let Some(term) = &bb.terminator { + match term { + crate::mir::MirInstruction::Jump { target } => { + out.push_str(&format!(" n{} -> n{};\n", bb_id.0, target.0)); + } + crate::mir::MirInstruction::Branch { then_bb, else_bb, .. } => { + // Branch condition is boolean (b1) + out.push_str(&format!(" n{} -> n{} [label=\"then cond:b1\"];\n", bb_id.0, then_bb.0)); + out.push_str(&format!(" n{} -> n{} [label=\"else cond:b1\"];\n", bb_id.0, else_bb.0)); + } + _ => {} + } + } + } + out.push_str("}\n"); + std::fs::write(path, out) +} diff --git a/src/jit/manager.rs b/src/jit/manager.rs index f1f2a86e..df30c16c 100644 --- a/src/jit/manager.rs +++ b/src/jit/manager.rs @@ -9,11 +9,17 @@ pub struct JitManager { hits: HashMap, compiled: HashMap, engine: crate::jit::engine::JitEngine, + exec_ok: u64, + exec_trap: u64, + // Per-function lowering stats (accumulated) + func_phi_total: HashMap, + func_phi_b1: HashMap, + func_ret_bool_hint: HashMap, } impl JitManager { pub fn new(threshold: u32) -> Self { - Self { threshold, hits: HashMap::new(), compiled: HashMap::new(), engine: crate::jit::engine::JitEngine::new() } + Self { threshold, hits: HashMap::new(), compiled: HashMap::new(), engine: crate::jit::engine::JitEngine::new(), exec_ok: 0, exec_trap: 0, func_phi_total: HashMap::new(), func_phi_b1: HashMap::new(), func_ret_bool_hint: HashMap::new() } } pub fn record_entry(&mut self, func: &str) { @@ -35,6 +41,9 @@ impl JitManager { if self.should_jit(func) { if let Some(handle) = self.engine.compile_function(func, mir) { self.mark_compiled(func, handle); + // Record per-function lower stats captured by engine + let (phi_t, phi_b1, ret_b) = self.engine.last_lower_stats(); + self.record_lower_stats(func, phi_t, phi_b1, ret_b); if std::env::var("NYASH_JIT_STATS").ok().as_deref() == Some("1") { eprintln!("[JIT] compiled {} -> handle={}", func, handle); } @@ -47,12 +56,59 @@ impl JitManager { pub fn is_compiled(&self, func: &str) -> bool { self.compiled.contains_key(func) } pub fn handle_of(&self, func: &str) -> Option { self.compiled.get(func).copied() } + // --- Stats accessors for unified reporting --- + pub fn sites(&self) -> usize { self.hits.len() } + pub fn compiled_count(&self) -> usize { self.compiled.len() } + pub fn total_hits(&self) -> u64 { self.hits.values().map(|v| *v as u64).sum() } + pub fn exec_ok_count(&self) -> u64 { self.exec_ok } + pub fn exec_trap_count(&self) -> u64 { self.exec_trap } + + // --- Per-function stats --- + pub fn record_lower_stats(&mut self, func: &str, phi_total: u64, phi_b1: u64, ret_bool_hint: bool) { + if phi_total > 0 { *self.func_phi_total.entry(func.to_string()).or_insert(0) += phi_total; } + if phi_b1 > 0 { *self.func_phi_b1.entry(func.to_string()).or_insert(0) += phi_b1; } + if ret_bool_hint { *self.func_ret_bool_hint.entry(func.to_string()).or_insert(0) += 1; } + } + pub fn per_function_stats(&self) -> Vec<(String, u64, u64, u64, u32, bool, u64)> { + // name, phi_total, phi_b1, ret_bool_hint, hits, compiled, handle + let mut names: std::collections::BTreeSet = std::collections::BTreeSet::new(); + names.extend(self.hits.keys().cloned()); + names.extend(self.func_phi_total.keys().cloned()); + names.extend(self.func_phi_b1.keys().cloned()); + names.extend(self.func_ret_bool_hint.keys().cloned()); + let mut out = Vec::new(); + for name in names { + let phi_t = self.func_phi_total.get(&name).copied().unwrap_or(0); + let phi_b1 = self.func_phi_b1.get(&name).copied().unwrap_or(0); + let rb = self.func_ret_bool_hint.get(&name).copied().unwrap_or(0); + let hits = self.hits.get(&name).copied().unwrap_or(0); + let compiled = self.compiled.contains_key(&name); + let handle = self.compiled.get(&name).copied().unwrap_or(0); + out.push((name, phi_t, phi_b1, rb, hits, compiled, handle)); + } + out + } + + /// Return top-N hot functions by hits, with compiled flag and handle + pub fn top_hits(&self, n: usize) -> Vec<(String, u32, bool, u64)> { + let mut v: Vec<(&String, &u32)> = self.hits.iter().collect(); + v.sort_by(|a, b| b.1.cmp(a.1)); + v.into_iter() + .take(n) + .map(|(k, h)| { + let compiled = self.compiled.contains_key(k); + let handle = self.compiled.get(k).copied().unwrap_or(0); + (k.clone(), *h, compiled, handle) + }) + .collect() + } + pub fn print_summary(&self) { if std::env::var("NYASH_JIT_STATS").ok().as_deref() != Some("1") { return; } let sites = self.hits.len(); let total_hits: u64 = self.hits.values().map(|v| *v as u64).sum(); let compiled = self.compiled.len(); - eprintln!("[JIT] sites={} compiled={} hits_total={}", sites, compiled, total_hits); + eprintln!("[JIT] sites={} compiled={} hits_total={} exec_ok={} exec_trap={}", sites, compiled, total_hits, self.exec_ok, self.exec_trap); // Top 5 hot functions let mut v: Vec<(&String, &u32)> = self.hits.iter().collect(); v.sort_by(|a, b| b.1.cmp(a.1)); @@ -78,17 +134,27 @@ impl JitManager { } /// 10_c: execute compiled function if present (stub: empty args). Returns Some(VMValue) if JIT path was taken. - pub fn execute_compiled(&self, func: &str, args: &[crate::backend::vm::VMValue]) -> Option { + pub fn execute_compiled(&mut self, func: &str, args: &[crate::backend::vm::VMValue]) -> Option { if let Some(h) = self.handle_of(func) { - // Expose current args to hostcall shims - crate::jit::rt::set_current_args(args); + // Expose args to both legacy VM hostcalls and new JIT ABI TLS + crate::jit::rt::set_legacy_vm_args(args); + let jit_args = crate::jit::abi::adapter::to_jit_values(args); + crate::jit::rt::set_current_jit_args(&jit_args); let t0 = std::time::Instant::now(); - let out = self.engine.execute_handle(h, args); + // Begin handle scope so temporary handles are reclaimed after the call + crate::jit::rt::handles::begin_scope(); + let out = self.engine.execute_handle(h, &jit_args); if std::env::var("NYASH_JIT_STATS").ok().as_deref() == Some("1") { let dt = t0.elapsed(); eprintln!("[JIT] exec_time_ms={} for {}", dt.as_millis(), func); } - return out; + let res = match out { + Some(v) => { self.exec_ok = self.exec_ok.saturating_add(1); Some(crate::jit::abi::adapter::from_jit_value(v)) } + None => { self.exec_trap = self.exec_trap.saturating_add(1); None } + }; + // Clear handles created during this call + crate::jit::rt::handles::end_scope_clear(); + return res; } None } diff --git a/src/jit/mod.rs b/src/jit/mod.rs index f525e405..e0a57c02 100644 --- a/src/jit/mod.rs +++ b/src/jit/mod.rs @@ -5,3 +5,8 @@ pub mod engine; pub mod lower; pub mod r#extern; pub mod rt; +pub mod abi; +pub mod config; +pub mod policy; +pub mod events; +pub mod hostcall_registry; diff --git a/src/jit/policy.rs b/src/jit/policy.rs new file mode 100644 index 00000000..585cdc7b --- /dev/null +++ b/src/jit/policy.rs @@ -0,0 +1,42 @@ +//! JIT Policy (Box-First): centralizes runtime decisions +//! +//! Minimal v0: +//! - read_only: if true, deny write-effects in jit-direct and other independent paths +//! - hostcall_whitelist: symbolic names allowed (future use) + +use once_cell::sync::OnceCell; +use std::sync::RwLock; + +#[derive(Debug, Clone, Default)] +pub struct JitPolicy { + pub read_only: bool, + pub hostcall_whitelist: Vec, +} + +impl JitPolicy { + pub fn from_env() -> Self { + let ro = std::env::var("NYASH_JIT_READ_ONLY").ok().as_deref() == Some("1"); + // Comma-separated hostcall names + let hc = std::env::var("NYASH_JIT_HOSTCALL_WHITELIST").ok().map(|s| { + s.split(',').map(|t| t.trim().to_string()).filter(|s| !s.is_empty()).collect::>() + }).unwrap_or_default(); + Self { read_only: ro, hostcall_whitelist: hc } + } +} + +static GLOBAL: OnceCell> = OnceCell::new(); + +pub fn current() -> JitPolicy { + if let Some(l) = GLOBAL.get() { + if let Ok(g) = l.read() { return g.clone(); } + } + JitPolicy::from_env() +} + +pub fn set_current(p: JitPolicy) { + if let Some(l) = GLOBAL.get() { + if let Ok(mut w) = l.write() { *w = p; return; } + } + let _ = GLOBAL.set(RwLock::new(p)); +} + diff --git a/src/jit/rt.rs b/src/jit/rt.rs index 34e3437c..dbc05b91 100644 --- a/src/jit/rt.rs +++ b/src/jit/rt.rs @@ -1,25 +1,140 @@ use std::cell::RefCell; use crate::backend::vm::VMValue; +use crate::jit::abi::JitValue; +// Legacy TLS for hostcalls that still expect VMValue — keep for compatibility thread_local! { - static CURRENT_ARGS: RefCell> = RefCell::new(Vec::new()); + static LEGACY_VM_ARGS: RefCell> = RefCell::new(Vec::new()); } -pub fn set_current_args(args: &[VMValue]) { - CURRENT_ARGS.with(|cell| { +pub fn set_legacy_vm_args(args: &[VMValue]) { + LEGACY_VM_ARGS.with(|cell| { let mut v = cell.borrow_mut(); v.clear(); v.extend_from_slice(args); }); } -pub fn with_args(f: F) -> R +pub fn with_legacy_vm_args(f: F) -> R where F: FnOnce(&[VMValue]) -> R, { - CURRENT_ARGS.with(|cell| { + LEGACY_VM_ARGS.with(|cell| { let v = cell.borrow(); f(&v) }) } + +// New TLS for independent JIT ABI values +thread_local! { + static CURRENT_JIT_ARGS: RefCell> = RefCell::new(Vec::new()); +} + +pub fn set_current_jit_args(args: &[JitValue]) { + CURRENT_JIT_ARGS.with(|cell| { + let mut v = cell.borrow_mut(); + v.clear(); + v.extend_from_slice(args); + }); +} + +pub fn with_jit_args(f: F) -> R +where + F: FnOnce(&[JitValue]) -> R, +{ + CURRENT_JIT_ARGS.with(|cell| { + let v = cell.borrow(); + f(&v) + }) +} + +// === JIT runtime counters (minimal) === +use std::sync::atomic::{AtomicU64, Ordering}; +static B1_NORM_COUNT: AtomicU64 = AtomicU64::new(0); +static RET_BOOL_HINT_COUNT: AtomicU64 = AtomicU64::new(0); +static PHI_TOTAL_SLOTS: AtomicU64 = AtomicU64::new(0); +static PHI_B1_SLOTS: AtomicU64 = AtomicU64::new(0); + +pub fn b1_norm_inc(delta: u64) { B1_NORM_COUNT.fetch_add(delta, Ordering::Relaxed); } +pub fn b1_norm_get() -> u64 { B1_NORM_COUNT.load(Ordering::Relaxed) } + +pub fn ret_bool_hint_inc(delta: u64) { RET_BOOL_HINT_COUNT.fetch_add(delta, Ordering::Relaxed); } +pub fn ret_bool_hint_get() -> u64 { RET_BOOL_HINT_COUNT.load(Ordering::Relaxed) } + +pub fn phi_total_inc(delta: u64) { PHI_TOTAL_SLOTS.fetch_add(delta, Ordering::Relaxed); } +pub fn phi_total_get() -> u64 { PHI_TOTAL_SLOTS.load(Ordering::Relaxed) } +pub fn phi_b1_inc(delta: u64) { PHI_B1_SLOTS.fetch_add(delta, Ordering::Relaxed); } +pub fn phi_b1_get() -> u64 { PHI_B1_SLOTS.load(Ordering::Relaxed) } + +// === 10.7c PoC: JIT Handle Registry (thread-local) === +use std::collections::HashMap; +use std::sync::Arc; + +pub mod handles { + use super::*; + + thread_local! { + static REG: RefCell = RefCell::new(HandleRegistry::new()); + static CREATED: RefCell> = RefCell::new(Vec::new()); + static SCOPES: RefCell> = RefCell::new(Vec::new()); + } + + struct HandleRegistry { + next: u64, + map: HashMap>, // BoxRef-compatible + } + + impl HandleRegistry { + fn new() -> Self { Self { next: 1, map: HashMap::new() } } + fn to_handle(&mut self, obj: Arc) -> u64 { + // Reuse existing handle if already present (pointer equality check) + // For PoC simplicity, always assign new handle + let h = self.next; + self.next = self.next.saturating_add(1); + self.map.insert(h, obj); + if std::env::var("NYASH_JIT_HANDLE_DEBUG").ok().as_deref() == Some("1") { + eprintln!("[JIT][handle] new h={}", h); + } + h + } + fn get(&self, h: u64) -> Option> { self.map.get(&h).cloned() } + #[allow(dead_code)] + fn drop_handle(&mut self, h: u64) { self.map.remove(&h); } + #[allow(dead_code)] + fn clear(&mut self) { self.map.clear(); self.next = 1; } + } + + pub fn to_handle(obj: Arc) -> u64 { + let h = REG.with(|cell| cell.borrow_mut().to_handle(obj)); + CREATED.with(|c| c.borrow_mut().push(h)); + h + } + pub fn get(h: u64) -> Option> { + REG.with(|cell| cell.borrow().get(h)) + } + #[allow(dead_code)] + pub fn clear() { REG.with(|cell| cell.borrow_mut().clear()); } + pub fn len() -> usize { REG.with(|cell| cell.borrow().map.len()) } + + // Scope management: track and clear handles created within a JIT call + pub fn begin_scope() { + CREATED.with(|c| { + let cur_len = c.borrow().len(); + SCOPES.with(|s| s.borrow_mut().push(cur_len)); + }); + } + pub fn end_scope_clear() { + let start = SCOPES.with(|s| s.borrow_mut().pop()).unwrap_or(0); + let to_drop: Vec = CREATED.with(|c| { + let mut v = c.borrow_mut(); + let slice = v[start..].to_vec(); + v.truncate(start); + slice + }); + REG.with(|cell| { + let mut reg = cell.borrow_mut(); + for h in to_drop { reg.map.remove(&h); } + }); + } +} diff --git a/src/runner.rs b/src/runner.rs index ea0db619..323fdc97 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -64,6 +64,32 @@ impl NyashRunner { // Prefer explicit JSON flag over any default std::env::set_var("NYASH_VM_STATS_JSON", "1"); } + // Optional: JIT controls via CLI flags (centralized) + { + let mut jc = nyash_rust::jit::config::JitConfig::from_env(); + jc.exec |= self.config.jit_exec; + jc.stats |= self.config.jit_stats; + jc.stats_json |= self.config.jit_stats_json; + jc.dump |= self.config.jit_dump; + if self.config.jit_threshold.is_some() { jc.threshold = self.config.jit_threshold; } + jc.phi_min |= self.config.jit_phi_min; + jc.hostcall |= self.config.jit_hostcall; + jc.handle_debug |= self.config.jit_handle_debug; + jc.native_f64 |= self.config.jit_native_f64; + jc.native_bool |= self.config.jit_native_bool; + if self.config.jit_only { std::env::set_var("NYASH_JIT_ONLY", "1"); } + // Apply runtime capability probe (e.g., disable b1 ABI if unsupported) + let caps = nyash_rust::jit::config::probe_capabilities(); + jc = nyash_rust::jit::config::apply_runtime_caps(jc, caps); + // Optional DOT emit via CLI (ensures dump is on when path specified) + if let Some(path) = &self.config.emit_cfg { + std::env::set_var("NYASH_JIT_DOT", path); + jc.dump = true; + } + // Persist to env (CLI parity) and set as current + jc.apply_env(); + nyash_rust::jit::config::set_current(jc.clone()); + } // Benchmark mode - can run without a file if self.config.benchmark { println!("📊 Nyash Performance Benchmark Suite"); @@ -76,6 +102,11 @@ impl NyashRunner { } if let Some(ref filename) = self.config.file { + // Independent JIT direct mode (no VM execute path) + if self.config.jit_direct { + self.run_file_jit_direct(filename); + return; + } // Delegate file-mode execution to modes::common dispatcher self.run_file(filename); } else { @@ -501,6 +532,202 @@ impl NyashRunner { } } +impl NyashRunner { + /// Run a file through independent JIT engine (no VM execute loop) + fn run_file_jit_direct(&self, filename: &str) { + use std::fs; + use nyash_rust::{parser::NyashParser, mir::MirCompiler}; + // Small helper for unified error output (text or JSON) + let emit_err = |phase: &str, code: &str, msg: &str| { + if std::env::var("NYASH_JIT_STATS_JSON").ok().as_deref() == Some("1") + || std::env::var("NYASH_JIT_ERROR_JSON").ok().as_deref() == Some("1") { + let payload = serde_json::json!({ + "kind": "jit_direct_error", + "phase": phase, + "code": code, + "message": msg, + "file": filename, + }); + println!("{}", payload.to_string()); + } else { + eprintln!("[JIT-direct][{}][{}] {}", phase, code, msg); + } + }; + // Require cranelift feature at runtime by attempting compile; if unavailable compile_function returns None + let code = match fs::read_to_string(filename) { + Ok(s) => s, + Err(e) => { emit_err("read_file", "IO", &format!("{}", e)); std::process::exit(1); } + }; + let ast = match NyashParser::parse_from_string(&code) { + Ok(a) => a, Err(e) => { emit_err("parse", "SYNTAX", &format!("{}", e)); std::process::exit(1); } + }; + let mut mc = MirCompiler::new(); + let cr = match mc.compile(ast) { Ok(m) => m, Err(e) => { emit_err("mir", "MIR_COMPILE", &format!("{}", e)); std::process::exit(1); } }; + let func = match cr.module.functions.get("main") { Some(f) => f, None => { emit_err("mir", "NO_MAIN", "No main function found"); std::process::exit(1); } }; + + // Guard: refuse write-effects in jit-direct when policy.read_only + { + use nyash_rust::mir::MirInstruction; + use nyash_rust::mir::effect::Effect; + let policy = nyash_rust::jit::policy::current(); + let mut writes = 0usize; + for (_bbid, bb) in func.blocks.iter() { + for inst in bb.instructions.iter() { + let mask = inst.effects(); + if mask.contains(Effect::WriteHeap) { + writes += 1; + } + } + if let Some(term) = &bb.terminator { + if term.effects().contains(Effect::WriteHeap) { writes += 1; } + } + } + if policy.read_only && writes > 0 { + emit_err("policy", "WRITE_EFFECTS", &format!("write-effects detected ({} ops). jit-direct is read-only at this stage.", writes)); + std::process::exit(1); + } + } + + let mut engine = nyash_rust::jit::engine::JitEngine::new(); + match engine.compile_function("main", func) { + Some(h) => { + // Optional event: compile + nyash_rust::jit::events::emit("compile", &func.signature.name, Some(h), None, serde_json::json!({})); + // Parse JIT args from env: NYASH_JIT_ARGS (comma-separated), with optional type prefixes + // Formats per arg: i:123, f:3.14, b:true/false, h:42 (handle), or bare numbers (int), true/false (bool) + let mut jit_args: Vec = Vec::new(); + if let Ok(s) = std::env::var("NYASH_JIT_ARGS") { + for raw in s.split(',') { + let t = raw.trim(); + if t.is_empty() { continue; } + let v = if let Some(rest) = t.strip_prefix("i:") { + rest.parse::().ok().map(nyash_rust::jit::abi::JitValue::I64) + } else if let Some(rest) = t.strip_prefix("f:") { + rest.parse::().ok().map(nyash_rust::jit::abi::JitValue::F64) + } else if let Some(rest) = t.strip_prefix("b:") { + let b = matches!(rest, "1"|"true"|"True"|"TRUE"); + Some(nyash_rust::jit::abi::JitValue::Bool(b)) + } else if let Some(rest) = t.strip_prefix("h:") { + rest.parse::().ok().map(nyash_rust::jit::abi::JitValue::Handle) + } else if t.eq_ignore_ascii_case("true") || t == "1" { Some(nyash_rust::jit::abi::JitValue::Bool(true)) } + else if t.eq_ignore_ascii_case("false") || t == "0" { Some(nyash_rust::jit::abi::JitValue::Bool(false)) } + else if let Ok(iv) = t.parse::() { Some(nyash_rust::jit::abi::JitValue::I64(iv)) } + else if let Ok(fv) = t.parse::() { Some(nyash_rust::jit::abi::JitValue::F64(fv)) } + else { None }; + if let Some(jv) = v { jit_args.push(jv); } + } + } + // Coerce args to expected MIR types + use nyash_rust::mir::MirType; + let expected = &func.signature.params; + if expected.len() != jit_args.len() { + emit_err("args", "COUNT_MISMATCH", &format!("expected={}, passed={}", expected.len(), jit_args.len())); + eprintln!("Hint: set NYASH_JIT_ARGS as comma-separated values, e.g., i:42,f:3.14,b:true"); + std::process::exit(1); + } + let mut coerced: Vec = Vec::with_capacity(jit_args.len()); + for (i, (exp, got)) in expected.iter().zip(jit_args.iter()).enumerate() { + let cv = match exp { + MirType::Integer => match got { + nyash_rust::jit::abi::JitValue::I64(v) => nyash_rust::jit::abi::JitValue::I64(*v), + nyash_rust::jit::abi::JitValue::F64(f) => nyash_rust::jit::abi::JitValue::I64(*f as i64), + nyash_rust::jit::abi::JitValue::Bool(b) => nyash_rust::jit::abi::JitValue::I64(if *b {1} else {0}), + _ => { emit_err("args", "TYPE_MISMATCH", &format!("param#{} expects Integer", i)); std::process::exit(1); } + }, + MirType::Float => match got { + nyash_rust::jit::abi::JitValue::F64(f) => nyash_rust::jit::abi::JitValue::F64(*f), + nyash_rust::jit::abi::JitValue::I64(v) => nyash_rust::jit::abi::JitValue::F64(*v as f64), + nyash_rust::jit::abi::JitValue::Bool(b) => nyash_rust::jit::abi::JitValue::F64(if *b {1.0} else {0.0}), + _ => { emit_err("args", "TYPE_MISMATCH", &format!("param#{} expects Float", i)); std::process::exit(1); } + }, + MirType::Bool => match got { + nyash_rust::jit::abi::JitValue::Bool(b) => nyash_rust::jit::abi::JitValue::Bool(*b), + nyash_rust::jit::abi::JitValue::I64(v) => nyash_rust::jit::abi::JitValue::Bool(*v != 0), + nyash_rust::jit::abi::JitValue::F64(f) => nyash_rust::jit::abi::JitValue::Bool(*f != 0.0), + _ => { emit_err("args", "TYPE_MISMATCH", &format!("param#{} expects Bool", i)); std::process::exit(1); } + }, + MirType::String | MirType::Box(_) | MirType::Array(_) | MirType::Future(_) => match got { + nyash_rust::jit::abi::JitValue::Handle(h) => nyash_rust::jit::abi::JitValue::Handle(*h), + _ => { emit_err("args", "TYPE_MISMATCH", &format!("param#{} expects handle (h:)", i)); std::process::exit(1); } + }, + MirType::Void | MirType::Unknown => { + // Keep as-is + *got + } + }; + coerced.push(cv); + } + nyash_rust::jit::rt::set_current_jit_args(&coerced); + let t0 = std::time::Instant::now(); + let out = engine.execute_handle(h, &coerced); + match out { + Some(v) => { + let ms = t0.elapsed().as_millis(); + nyash_rust::jit::events::emit("execute", &func.signature.name, Some(h), Some(ms), serde_json::json!({})); + // Normalize result according to MIR return type for friendly output + use nyash_rust::mir::MirType; + let ret_ty = &func.signature.return_type; + let vmv = match (ret_ty, v) { + (MirType::Bool, nyash_rust::jit::abi::JitValue::I64(i)) => nyash_rust::backend::vm::VMValue::Bool(i != 0), + (MirType::Bool, nyash_rust::jit::abi::JitValue::Bool(b)) => nyash_rust::backend::vm::VMValue::Bool(b), + (MirType::Float, nyash_rust::jit::abi::JitValue::F64(f)) => nyash_rust::backend::vm::VMValue::Float(f), + (MirType::Float, nyash_rust::jit::abi::JitValue::I64(i)) => nyash_rust::backend::vm::VMValue::Float(i as f64), + // Default adapter for other combos + _ => nyash_rust::jit::abi::adapter::from_jit_value(v), + }; + println!("✅ JIT-direct execution completed successfully!"); + // Pretty print with expected type tag + let (ety, sval) = match (ret_ty, &vmv) { + (MirType::Bool, nyash_rust::backend::vm::VMValue::Bool(b)) => ("Bool", b.to_string()), + (MirType::Float, nyash_rust::backend::vm::VMValue::Float(f)) => ("Float", format!("{}", f)), + (MirType::Integer, nyash_rust::backend::vm::VMValue::Integer(i)) => ("Integer", i.to_string()), + // Fallbacks + (_, nyash_rust::backend::vm::VMValue::Integer(i)) => ("Integer", i.to_string()), + (_, nyash_rust::backend::vm::VMValue::Float(f)) => ("Float", format!("{}", f)), + (_, nyash_rust::backend::vm::VMValue::Bool(b)) => ("Bool", b.to_string()), + (_, nyash_rust::backend::vm::VMValue::String(s)) => ("String", s.clone()), + (_, nyash_rust::backend::vm::VMValue::BoxRef(arc)) => ("BoxRef", arc.type_name().to_string()), + (_, nyash_rust::backend::vm::VMValue::Future(_)) => ("Future", "".to_string()), + (_, nyash_rust::backend::vm::VMValue::Void) => ("Void", "void".to_string()), + }; + println!("ResultType(MIR): {}", ety); + println!("Result: {}", sval); + // Optional JSON stats + if std::env::var("NYASH_JIT_STATS_JSON").ok().as_deref() == Some("1") { + let cfg = nyash_rust::jit::config::current(); + let caps = nyash_rust::jit::config::probe_capabilities(); + let (phi_t, phi_b1, ret_b) = engine.last_lower_stats(); + let abi_mode = if cfg.native_bool_abi && caps.supports_b1_sig { "b1_bool" } else { "i64_bool" }; + let payload = serde_json::json!({ + "version": 1, + "function": func.signature.name, + "abi_mode": abi_mode, + "abi_b1_enabled": cfg.native_bool_abi, + "abi_b1_supported": caps.supports_b1_sig, + "b1_norm_count": nyash_rust::jit::rt::b1_norm_get(), + "ret_bool_hint_count": nyash_rust::jit::rt::ret_bool_hint_get(), + "phi_total_slots": phi_t, + "phi_b1_slots": phi_b1, + "ret_bool_hint_used": ret_b, + }); + println!("{}", payload.to_string()); + } + } + None => { + nyash_rust::jit::events::emit("fallback", &func.signature.name, Some(h), None, serde_json::json!({"reason":"trap_or_missing"})); + emit_err("execute", "TRAP_OR_MISSING", "execution failed (trap or missing handle)"); + std::process::exit(1); + } + } + } + None => { + emit_err("compile", "UNAVAILABLE", "Build with --features cranelift-jit"); + std::process::exit(1); + } + } + } +} + // Demo functions (moved from main.rs) fn demo_basic_boxes() { println!("\n📦 1. Basic Box Creation:"); diff --git a/src/runner/modes/bench.rs b/src/runner/modes/bench.rs index b7c8a127..39221c3e 100644 --- a/src/runner/modes/bench.rs +++ b/src/runner/modes/bench.rs @@ -5,8 +5,9 @@ impl NyashRunner { /// Execute benchmark mode (split) pub(crate) fn execute_benchmark_mode(&self) { println!("🏁 Running benchmark mode with {} iterations", self.config.iterations); - // Two tests: simple add, arithmetic loop - let tests: Vec<(&str, &str)> = vec![ + // Tests: some run on all backends, some are JIT+f64 only + // Third element indicates JIT+f64 only (skip VM/Interpreter) + let tests: Vec<(&str, &str, bool)> = vec![ ( "simple_add", r#" @@ -16,6 +17,7 @@ impl NyashRunner { y = x + 58 return y "#, + false, ), ( "arith_loop_100k", @@ -29,6 +31,7 @@ impl NyashRunner { } return sum "#, + false, ), ( "branch_return", @@ -42,33 +45,63 @@ impl NyashRunner { return 2 } "#, + false, + ), + ( + "f64_add_jit", + r#" + local x, y + x = 1.5 + y = 2.25 + return x + y + "#, + true, ), ]; - for (name, code) in tests { + for (name, code, jit_f64_only) in tests { println!("\n===================================="); println!("🧪 Test: {}", name); - // Warmup (not measured) - let warmup = (self.config.iterations / 10).max(1); - self.bench_interpreter(code, warmup); - self.bench_vm(code, warmup); - self.bench_jit(code, warmup); + if jit_f64_only { + println!("(JIT+f64 only) Skipping VM/Interpreter; requires --features cranelift-jit"); + // Warmup JIT + let warmup = (self.config.iterations / 10).max(1); + self.bench_jit(code, warmup); + // Measured + let jit_time = self.bench_jit(code, self.config.iterations); + println!("\n📊 Performance Summary [{}]:", name); + println!(" JIT f64 ops: {} iters in {:?} ({:.2} ops/sec)", self.config.iterations, jit_time, self.config.iterations as f64 / jit_time.as_secs_f64()); + } else { + // Quick correctness check across modes (golden): Interpreter vs VM vs VM+JIT + if let Err(e) = self.verify_outputs_match(code) { + println!("❌ Output mismatch: {}", e); + } else { + println!("✅ Outputs match across Interpreter/VM/JIT"); + } + // Warmup (not measured) + let warmup = (self.config.iterations / 10).max(1); + self.bench_interpreter(code, warmup); + self.bench_vm(code, warmup); + self.bench_jit(code, warmup); - // Measured runs - let interpreter_time = self.bench_interpreter(code, self.config.iterations); - let vm_time = self.bench_vm(code, self.config.iterations); - let jit_time = self.bench_jit(code, self.config.iterations); + // Measured runs + let interpreter_time = self.bench_interpreter(code, self.config.iterations); + let vm_time = self.bench_vm(code, self.config.iterations); + let jit_time = self.bench_jit(code, self.config.iterations); - // Summary - let vm_vs_interp = interpreter_time.as_secs_f64() / vm_time.as_secs_f64(); - let jit_vs_vm = vm_time.as_secs_f64() / jit_time.as_secs_f64(); - println!("\n📊 Performance Summary [{}]:", name); - println!(" VM is {:.2}x {} than Interpreter", if vm_vs_interp > 1.0 { vm_vs_interp } else { 1.0 / vm_vs_interp }, if vm_vs_interp > 1.0 { "faster" } else { "slower" }); - println!(" JIT is {:.2}x {} than VM (note: compile cost included)", if jit_vs_vm > 1.0 { jit_vs_vm } else { 1.0 / jit_vs_vm }, if jit_vs_vm > 1.0 { "faster" } else { "slower" }); + // Summary + let vm_vs_interp = interpreter_time.as_secs_f64() / vm_time.as_secs_f64(); + let jit_vs_vm = vm_time.as_secs_f64() / jit_time.as_secs_f64(); + println!("\n📊 Performance Summary [{}]:", name); + println!(" VM is {:.2}x {} than Interpreter", if vm_vs_interp > 1.0 { vm_vs_interp } else { 1.0 / vm_vs_interp }, if vm_vs_interp > 1.0 { "faster" } else { "slower" }); + println!(" JIT is {:.2}x {} than VM (note: compile cost included)", if jit_vs_vm > 1.0 { jit_vs_vm } else { 1.0 / jit_vs_vm }, if jit_vs_vm > 1.0 { "faster" } else { "slower" }); + } } } fn bench_interpreter(&self, code: &str, iters: u32) -> std::time::Duration { + // Enable native f64 when available to exercise widened ABI + std::env::set_var("NYASH_JIT_NATIVE_F64", "1"); let start = std::time::Instant::now(); for _ in 0..iters { if let Ok(ast) = NyashParser::parse_from_string(code) { @@ -101,6 +134,8 @@ impl NyashRunner { // Force JIT mode for this run std::env::set_var("NYASH_JIT_EXEC", "1"); std::env::set_var("NYASH_JIT_THRESHOLD", "1"); + if self.config.jit_stats { std::env::set_var("NYASH_JIT_STATS", "1"); } + if self.config.jit_stats_json { std::env::set_var("NYASH_JIT_STATS_JSON", "1"); } let start = std::time::Instant::now(); for _ in 0..iters { if let Ok(ast) = NyashParser::parse_from_string(code) { @@ -115,4 +150,32 @@ impl NyashRunner { println!(" 🔥 JIT: {} iters in {:?} ({:.2} ops/sec)", iters, elapsed, iters as f64 / elapsed.as_secs_f64()); elapsed } + + /// Verify that outputs match across VM and JIT-enabled VM (golden) + fn verify_outputs_match(&self, code: &str) -> Result<(), String> { + // VM + let vm_out = { + let ast = NyashParser::parse_from_string(code).map_err(|e| format!("vm parse: {}", e))?; + let mut mc = MirCompiler::new(); + let cr = mc.compile(ast).map_err(|e| format!("vm compile: {}", e))?; + let mut vm = VM::new(); + let out = vm.execute_module(&cr.module).map_err(|e| format!("vm exec: {}", e))?; + out.to_string_box().value + }; + // VM+JIT + let jit_out = { + std::env::set_var("NYASH_JIT_EXEC", "1"); + std::env::set_var("NYASH_JIT_THRESHOLD", "1"); + let ast = NyashParser::parse_from_string(code).map_err(|e| format!("jit parse: {}", e))?; + let mut mc = MirCompiler::new(); + let cr = mc.compile(ast).map_err(|e| format!("jit compile: {}", e))?; + let mut vm = VM::new(); + let out = vm.execute_module(&cr.module).map_err(|e| format!("jit exec: {}", e))?; + out.to_string_box().value + }; + if vm_out != jit_out { + return Err(format!("vm='{}' jit='{}'", vm_out, jit_out)); + } + Ok(()) + } }