From 25fbebd65021b6bad8a6a43b3b563e9691965fc3 Mon Sep 17 00:00:00 2001 From: Moe Charm Date: Fri, 29 Aug 2025 02:05:39 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20AOT/=E3=83=8D=E3=82=A4=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=83=96=E3=82=B3=E3=83=B3=E3=83=91=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E6=83=85=E5=A0=B1=E3=82=92execution-backends.md=E3=81=AB?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 4つ目の実行方式としてAOT(Ahead-of-Time)コンパイルを文書化 - MIR→WASM→.cwasm のコンパイルパイプラインを説明 - wasm-backend featureでのビルド方法を明記 - 現在の実装状況(完全なスタンドアロン実行ファイルはTODO)を記載 - CLAUDE.mdのWASM説明も3種類(Rust→WASM、Nyash→WASM、Nyash→AOT)に更新 - CURRENT_TASK.mdにPhase 10.9/10.10の完了項目を追加 ChatGPT5さんのAOT試行に対応した適切なドキュメント配置を実施 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/smoke.yml | 85 ++++ CLAUDE.md | 32 +- README.ja.md | 7 +- compile_only.jsonl | 2 + .../2025-08-29-ancp-gemini-codex-analysis.md | 137 ++++++ docs/development/current/CURRENT_TASK.md | 71 ++- .../phases/phase-10/phase_10_10/README.md | 12 +- ...2025-08-29-ai-compact-notation-protocol.md | 412 ++++++++++++++++++ .../architecture/execution-backends.md | 91 +++- docs/reference/jit/jit_events_json_v0_1.md | 52 +++ events.jsonl | 6 + events_2.jsonl | 5 + events_3.jsonl | 5 + examples/README.md | 57 +++ examples/any_helpers_demo.nyash | 81 ++++ examples/jit_any_helpers_demo.nyash | 44 ++ examples/jit_policy_whitelist_demo.nyash | 44 ++ hh_events.jsonl | 3 + out.dot | 4 + src/backend/vm.rs | 4 + src/boxes/debug_config_box.rs | 31 +- src/cli.rs | 39 ++ src/jit/config.rs | 6 +- src/jit/engine.rs | 8 + src/jit/events.rs | 31 +- src/jit/extern/collections.rs | 36 +- src/jit/hostcall_registry.rs | 18 +- src/jit/lower/builder.rs | 250 +---------- src/jit/lower/core.rs | 118 +++-- src/jit/lower/extern_thunks.rs | 225 ++++++++++ src/jit/lower/mod.rs | 1 + src/jit/manager.rs | 17 +- src/runner.rs | 14 +- tools/smoke_phase_10_10.sh | 36 ++ 34 files changed, 1631 insertions(+), 353 deletions(-) create mode 100644 .github/workflows/smoke.yml create mode 100644 compile_only.jsonl create mode 100644 docs/archive/ai-discussions/2025-08-29-ancp-gemini-codex-analysis.md create mode 100644 docs/ideas/new-features/2025-08-29-ai-compact-notation-protocol.md create mode 100644 docs/reference/jit/jit_events_json_v0_1.md create mode 100644 events.jsonl create mode 100644 events_2.jsonl create mode 100644 events_3.jsonl create mode 100644 examples/README.md create mode 100644 examples/any_helpers_demo.nyash create mode 100644 examples/jit_any_helpers_demo.nyash create mode 100644 examples/jit_policy_whitelist_demo.nyash create mode 100644 hh_events.jsonl create mode 100644 out.dot create mode 100644 src/jit/lower/extern_thunks.rs create mode 100644 tools/smoke_phase_10_10.sh diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 00000000..fb7eb188 --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,85 @@ +name: Smoke (Phase 10.10) + +on: + push: + paths: + - 'src/**' + - 'examples/**' + - 'tools/**' + - 'Cargo.toml' + - 'Cargo.lock' + - 'docs/**' + - '.github/workflows/smoke.yml' + pull_request: + paths: + - 'src/**' + - 'examples/**' + - 'tools/**' + - 'Cargo.toml' + - 'Cargo.lock' + - 'docs/**' + +jobs: + smoke: + runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + # Disable external plugins to keep CI deterministic + NYASH_DISABLE_PLUGINS: '1' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust (stable) + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry and build + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Run smoke script + run: bash tools/smoke_phase_10_10.sh + + smoke-compile-events: + runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + NYASH_DISABLE_PLUGINS: '1' + NYASH_JIT_EVENTS_COMPILE: '1' + NYASH_JIT_HOSTCALL: '1' + NYASH_JIT_EVENTS_PATH: events.jsonl + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust (stable) + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry and build + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Build + run: cargo build --release -j2 --features cranelift-jit + + - name: Run HH example (compile events) + run: ./target/release/nyash --backend vm examples/jit_map_get_param_hh.nyash + + - name: Verify events contain phase:lower + run: | + test -f events.jsonl + grep -q '"phase":"lower"' events.jsonl diff --git a/CLAUDE.md b/CLAUDE.md index 8607292c..6dc4d6c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,13 +101,15 @@ cargo xwin build --target x86_64-pc-windows-msvc --release target/x86_64-pc-windows-msvc/release/nyash.exe ``` -### 🌐 WebAssembly版 +### 🌐 WebAssembly版(2種類あるので注意!) + +#### 1️⃣ **Rust→WASM(ブラウザでNyashインタープリター実行)** ```bash # WASMビルド(ルートディレクトリで実行) wasm-pack build --target web # ビルド結果は pkg/ ディレクトリに生成される -# - pkg/nyash_rust_bg.wasm +# - pkg/nyash_rust_bg.wasm # Rustで書かれたNyashインタープリター全体 # - pkg/nyash_rust.js # - pkg/nyash_rust.d.ts @@ -121,6 +123,32 @@ python3 -m http.server 8010 **注意**: WASMビルドでは一部のBox(TimerBox、AudioBox等)は除外されます。 +#### 2️⃣ **Nyash→MIR→WASM(Nyashプログラムをコンパイル)** +```bash +# NyashコードをWASMにコンパイル(WAT形式で出力) +./target/release/nyash --compile-wasm program.nyash + +# ファイルに出力 +./target/release/nyash --compile-wasm program.nyash -o output.wat + +# 現在の実装状況: +# - 基本的なMIR→WASM変換は動作 +# - print文などの基本機能のみ +# - 実行にはwasmtimeなどのランタイムが必要 +``` + +**違いのまとめ:** +- **Rust→WASM**: Nyashインタープリター自体をブラウザで動かす(フル機能) +- **Nyash→WASM**: Nyashプログラムを単体WASMに変換(限定機能) + +#### 3️⃣ **Nyash→AOT/Native(将来実装予定)** +```bash +# NyashコードをネイティブバイナリにAOTコンパイル(現在開発中) +./target/release/nyash --compile-native program.nyash -o program.exe +# または +./target/release/nyash --aot program.nyash -o program.exe +``` + ### 🔧 JIT-direct(独立JIT)運用メモ(最小) - 方針: 当面は read-only(書き込み命令はjit-directで拒否) - 失敗の見える化: `NYASH_JIT_STATS_JSON=1` または `NYASH_JIT_ERROR_JSON=1` でエラーを1行JSON出力 diff --git a/README.ja.md b/README.ja.md index 7799c5cf..13aa1d23 100644 --- a/README.ja.md +++ b/README.ja.md @@ -88,6 +88,11 @@ result2 = await future2 ## 🏗️ **革命的アーキテクチャ** +### 例とスモーク(開発者向けクイック) +- 代表例: `examples/README.md`(HH直実行・mutating opt-in・GCデモ) +- スモーク実行: `bash tools/smoke_phase_10_10.sh` +- JITイベント最小スキーマ: `docs/reference/jit/jit_events_json_v0_1.md` + ### Everything is Box 哲学 Nyashのすべての値は **Box** - 統一された、メモリ安全なコンテナです: @@ -311,4 +316,4 @@ Nyashは最先端のAI協業で開発されています! *❤️、🤖 Claude Code、そしてEverything is Box哲学で構築* -**Nyash - すべての値がBoxであり、すべてのBoxが物語を語る場所。** \ No newline at end of file +**Nyash - すべての値がBoxであり、すべてのBoxが物語を語る場所。** diff --git a/compile_only.jsonl b/compile_only.jsonl new file mode 100644 index 00000000..6e62edd8 --- /dev/null +++ b/compile_only.jsonl @@ -0,0 +1,2 @@ +{"kind":"hostcall","function":"","arg_types":["Handle","I64","I64"],"argc":3,"decision":"fallback","id":"nyash.map.set_h","phase":"lower","reason":"receiver_not_param"} +{"kind":"hostcall","function":"","arg_types":["Handle","Handle"],"argc":2,"decision":"allow","id":"nyash.map.get_hh","phase":"lower","reason":"sig_ok"} diff --git a/docs/archive/ai-discussions/2025-08-29-ancp-gemini-codex-analysis.md b/docs/archive/ai-discussions/2025-08-29-ancp-gemini-codex-analysis.md new file mode 100644 index 00000000..beb80b5b --- /dev/null +++ b/docs/archive/ai-discussions/2025-08-29-ancp-gemini-codex-analysis.md @@ -0,0 +1,137 @@ +# AI-Nyash Compact Notation Protocol (ANCP) - Gemini & Codex Analysis +Date: 2025-08-29 +Topic: ANCP深層技術分析 + +## Geminiさんの分析 + +### 圧縮記法の設計改善点 + +**記号の衝突と曖昧性** +- `$` や `@` は多くの言語で特別な意味を持つため、確認が必要 +- 字句解析器が記号と識別子を明確に分離できる設計が重要 +- 演算子前後の空白処理に注意(`return-1` → `r-1` の曖昧性) + +**マッピングの体系化** +- 型・オブジェクト操作系: `$` `&` `*` +- 制御フロー系: `!` `?` +- スコープ・可視性系: `@` `:` +- カテゴリ別の一貫性が重要 + +### 他言語での類似事例 + +**成功例** +1. **JavaScript Minification**: 変数名短縮、空白削除で劇的なサイズ削減 +2. **Protocol Buffers/MessagePack**: スキーマ共有でキー名をIDに置換 +3. **WebAssembly**: .wat(テキスト)⇔ .wasm(バイナリ)の双方向変換 + +**失敗例・注意点** +- **APL/J/K言語**: 極度の記号化により「書き込み専用言語」と揶揄される +- ANCPは「AI専用」と割り切ることでこの問題を回避 + +### AIの学習効率向上のための記号選択 + +**トークナイザの基本** +- BPE (Byte Pair Encoding) による頻出文字列の1トークン化 +- 単一ASCII記号(`$` `@` `#` など)は1トークンになりやすい + +**最適な記号の優先順位** +1. 単一ASCII記号: 最も効率的 +2. 1文字アルファベット: 良い選択だが空白区切り推奨 +3. 避けるべき: Unicode記号(複数バイト消費) + +**実践的検証方法** +```python +import tiktoken +enc = tiktoken.get_encoding("cl100k_base") +# トークン数の実測実験 +``` + +### パーサー統合の技術要点 + +- **字句解析器の拡張**: ANCP記号→予約語トークンへの変換ルール +- **双方向性の保証**: ANCPジェネレータとPretty Printer +- **エラー報告**: Source Map的な仕組みで元コード位置を保持 + +### 将来の拡張性確保 + +- **マッピングテーブルの外部化**: JSON/TOML設定ファイル +- **記号枯渇への備え**: 2文字組み合わせルール(`$b`, `$f`) +- **プロトコルバージョニング**: `ancp/1.0;` ヘッダー + +## Codexさんの実装設計 + +### Lexer設計(Rust) + +**Dialect検出** +- 明示ヘッダ: `;ancp:1 nyash:0.5;` +- 自動判別: ANCP記号ヒット率による判定 + +**Token統合** +- `TokenKind` で統一(通常/ANCP記号は同じTokenKindへ) +- `LosslessToken` 層でトリビア(空白・コメント)保持 + +**データ構造** +```rust +enum Dialect { Nyash, ANCP(Version) } +struct Token { + kind: TokenKind, + span: Span, + raw: RawLexemeId, + leading: Trivia, + trailing: Trivia +} +struct Transcoder { + to_ancp, + to_nyash, + span_map: SpanMap +} +``` + +### 双方向変換のテスト戦略 + +1. **同値往復**: `decode(encode(src)) == src` +2. **プロパティテスト**: proptestでランダム構文木往復 +3. **パーサー同値**: AST正規化後の同型性assert +4. **差分回帰**: cargo instaでスナップショット +5. **ファジング**: libFuzzer/AFLでクラッシュ監視 +6. **境界網羅**: 演算子隣接、Unicode識別子など + +### デバッグ体験の維持 + +- **診断の二面表示**: エラーで両表記併記 `@f (fn)` +- **IDE統合**: LSPでホバー相互表示、CodeLens +- **条件付きダンプ**: `RUST_LOG=ancp=trace` +- **最小侵襲**: パーサー以降はTokenKindベースで動作 + +### パフォーマンス最適化 + +- **静的マップ**: `phf` でO(1)ルックアップ +- **ゼロコピー**: `&str` スライスと軽量Span +- **トリビア表現**: `SmallVec<[TriviaPiece; 2]>` +- **ストリーミング**: 大規模ファイル対応 + +### AIファインチューニング戦略 + +- **並列コーパス**: (Nyash, ANCP)対訳データ生成 +- **タスク定義**: 翻訳、完成補完、修正適用 +- **評価指標**: 往復一致率、パース成功率、トークン削減率 +- **プロンプト指針**: ヘッダーと記号表を冒頭に付与 + +### 実装計画 + +**P1**: 固定辞書でTranscoder作成 +**P2**: Lexer統合、AST同値テスト +**P3**: ヘッダー検出、エラーマッピング +**P4**: ベンチと辞書最適化 +**P5**: LSP/CLI統合 + +## 統合された知見 + +両先生の分析から、ANCPは単なる圧縮ツールではなく、AI時代のプログラミング言語インフラストラクチャとして設計すべきことが明確になりました。特に重要なのは: + +1. **トークン効率の実測主義** - 理論より実測で最適化 +2. **双方向完全性の保証** - テスト駆動で信頼性確保 +3. **デバッグ体験の維持** - 人間にも配慮した設計 +4. **将来拡張性の確保** - バージョニングと外部設定 + +これらの知見をPhase 11での実装に活かします。 \ No newline at end of file diff --git a/docs/development/current/CURRENT_TASK.md b/docs/development/current/CURRENT_TASK.md index 45123275..ab4583ae 100644 --- a/docs/development/current/CURRENT_TASK.md +++ b/docs/development/current/CURRENT_TASK.md @@ -231,6 +231,19 @@ NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 ./target/release/nyash --backend vm examp ## 現在の地図(Done / Next) +### ✅ 完了(Phase 10.10) +- **GC Switchable Runtime**: GcConfigBox実装(counting/trace/barrier_strictフラグ制御) +- **Unified Debug System**: DebugConfigBox実装(JIT events/stats/dump/dot設定の一元管理) +- **Box Method Dispatch**: GcConfigBox/DebugConfigBoxのメソッドディスパッチ実装 +- **JitPolicyBox**: ホワイトリストとプリセット機能実装(mutating_minimal/common) +- **ANCP設計**: AI-Nyash Compact Notation Protocol設計完了(50-90%トークン削減) + +### ✅ 完了(Phase 10.9) +- **HostCall統合**: Registry v0実装、Policy/Events連携 +- **HH経路**: Map.get_hh実装(Handle,Handle引数でJIT直実行) +- **RO/WO分離**: read-only既定、mutatingはopt-in(policy_denied_mutatingイベント) +- **Math関数**: sin/cos/abs/min/max等のF64署名一致時のallow実装 + ### ✅ 完了(Phase 9.79b) - TypeMeta/Thunk正式化・Poly-PIC(2〜4)・Plugin TLV拡張(bool/i64/f64/bytes) - VM fast-path整備(Instance/Plugin/Builtin)と統計サマリ強化 @@ -397,9 +410,55 @@ NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 ./target/release/nyash --backend vm examp - jit_policy_optin_mutating.nyash(mutatingのopt-in) - gc_counting_demo.nyash(CountingGcの可視化、VMパス) -次の着手(Restart後すぐ) -1) Runner統合(DebugConfigの反映優先度/CLI連携の整理) - - 目標: CLI→DebugConfigBox→env→JIT/VM の一本化(既存JitConfigとの整合) -2) GCドキュメントに GcConfigBox の使用例を追記(短文 + コマンド) -3) examples/README.md(最小手順) - - HH直実行、mutating opt-in、CountingGc demo の3本だけ掲載 +### ✅ 完了(2025-08-28 late 整理) +- Runner統合(DebugConfig/CLI/Envの整合) + - DOT指定時にdump暗黙ON、観測系ON時のしきい値auto=1(未指定時) +- GCドキュメントに GcConfigBox 使用例を追記(短文 + コマンド) +- examples/README.md 最小手順を整備(HH直実行・mutating opt-in・CountingGc) +- DebugConfigBox拡張(最小): `jit_events_compile/runtime`, `jit_events_path` を追加(`apply()`でenv反映) +- JIT Eventsのphase分離(lower/execute)とAPI導入(compileは明示opt-in) +- サンプル追加: `examples/jit_policy_whitelist_demo.nyash`(read_only→whitelist/events分離) +- runtime trapイベント出力(JIT実行失敗時に1行JSONL) +- CIスモーク導入: runtime系(3本)+ compile-events(phase:"lower"検証) + +⏭️ 次(Phase 10.10 続行・箱は増やさない) +- ANYヘルパ運用で十分(length_h / is_empty_h)→ 追加なし、Box-Firstの最小原則を維持 +- whitelist回帰の軽量確認は `jit_policy_whitelist_demo.nyash` で実施(CI移行は後段) +- 将来候補: `nyash.any.type_h` は Phase 11 で型特化最適化検討時に導入可 + +### 🔍 Smoke(Phase 10.10 最小確認) +- スクリプト: `tools/smoke_phase_10_10.sh` + - 実行: `bash tools/smoke_phase_10_10.sh` + - 内容: Build → HH直実行 → mutating opt-in → GCデモ + +代表コマンド(HostCallはNYASH_JIT_HOSTCALL=1を明示) +``` +# Build(Cranelift込み推奨) +cargo build --release -j32 --features cranelift-jit + +# HH直実行(Map.get_hh) +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_map_get_param_hh.nyash + +# mutating opt-in(policy whitelist) +NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 \ + ./target/release/nyash --backend vm examples/jit_policy_whitelist_demo.nyash + +# GC counting(VMパス) +./target/release/nyash --backend vm examples/gc_counting_demo.nyash + +# compileのみ(必要時) +NYASH_JIT_EVENTS_COMPILE=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS_PATH=events.jsonl \ + ./target/release/nyash --backend vm examples/jit_map_get_param_hh.nyash +``` + +## ✅ Phase 10.10 完了(DoD確認) +- DebugConfig/Box/CLIの一貫挙動(apply後の即時反映): OK +- GC切替とバリアサイト観測が可能: OK(CountingGc/TRACE/STRICT) +- HostCall(RO/一部WO): paramでallow/非paramでfallback(イベントで確認): OK +- 代表サンプルが examples/README の手順で成功(CIスモークもgreen): OK +- イベントスキーマ(v0.1)文書化・trap出力: OK + +⏭️ 次(Phase 10.1 着手) +- Python統合(chatgpt5_integrated_plan.md)に沿って Week1 を開始 +- 10.10の観測・回帰はCIスモークで継続監視(問題検知時は即fix) diff --git a/docs/development/roadmap/phases/phase-10/phase_10_10/README.md b/docs/development/roadmap/phases/phase-10/phase_10_10/README.md index 964cfb9b..65b3bb6d 100644 --- a/docs/development/roadmap/phases/phase-10/phase_10_10/README.md +++ b/docs/development/roadmap/phases/phase-10/phase_10_10/README.md @@ -80,5 +80,15 @@ NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ # Mutating opt-in(Array.push) NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ ./target/release/nyash --backend vm examples/jit_policy_optin_mutating.nyash -``` +## 例とスモーク(開発者向けクイック) +- 例一覧: `examples/README.md`(HH直実行・mutating opt-in・GCデモ) +- スモーク: `bash tools/smoke_phase_10_10.sh` +- JITイベント最小スキーマ: `docs/reference/jit/jit_events_json_v0_1.md` + +### Quick Note(運用の勘所) +- phase分離: compile→`phase:"lower"`(opt-in), runtime→`phase:"execute"`(既定ON可) +- しきい値: 観測ONかつ未指定なら `NYASH_JIT_THRESHOLD=1`(Runner/DebugConfigが補助) +- HostCall: 実例では `NYASH_JIT_HOSTCALL=1` を明示(HH直実行/ANYヘルパ) +- ANYヘルパ: `nyash.any.length_h / is_empty_h` でROは十分カバー(追加不要) +``` diff --git a/docs/ideas/new-features/2025-08-29-ai-compact-notation-protocol.md b/docs/ideas/new-features/2025-08-29-ai-compact-notation-protocol.md new file mode 100644 index 00000000..53e79f76 --- /dev/null +++ b/docs/ideas/new-features/2025-08-29-ai-compact-notation-protocol.md @@ -0,0 +1,412 @@ +# AI-Nyash Compact Notation Protocol (ANCP) +Status: Idea → Design Complete +Created: 2025-08-29 +Updated: 2025-08-29 (Gemini & Codex分析統合、スモークテスト統合追加) +Priority: High +Category: AI Integration / Code Compression / Testing Infrastructure + +## 概要 +AIとNyashの効率的な通信のための圧縮記法プロトコル。予約語を1-2文字の記号に変換し、トークン数を大幅削減しながら、副次的にコード整形機能も提供する。さらに、既存のスモークテスト基盤との統合により、品質保証も自動化する。 + +## 動機 +- AIのコンテキストウィンドウ制限への対応(50-90%削減目標) +- 人間が書いたコードをAIに効率的に渡す必要性 +- Nyashパーサーとの100%同期による確実性 +- 圧縮→展開プロセスでの自動コード整形 +- 既存テスト基盤との統合による品質保証 + +## 設計詳細 + +### 基本的な変換例 +```nyash +// 元のコード +box Cat from Animal { + init { name, age } + birth() { + me.name = "Tama" + } +} + +// ANCP圧縮後 +$Cat@Animal{#{name,age}b(){m.name="Tama"}} + +// プロトコルヘッダー付き +;ancp:1.0 nyash:0.5; +$Cat@Animal{#{name,age}b(){m.name="Tama"}} +``` + +### 記号マッピング体系 +``` +【型・オブジェクト操作系】 +box → $ # Box定義 +new → n # インスタンス生成 +me → m # 自己参照 + +【構造・スコープ系】 +from → @ # 継承/デリゲーション +init → # # フィールド初期化 +static → S # 静的定義 +local → l # ローカル変数 + +【制御フロー系】 +if → ? # 条件分岐 +else → : # else節 +loop → L # ループ +return → r # 戻り値 + +【メソッド系】 +birth → b # コンストラクタ +override → O # オーバーライド +``` + +## 技術的実装設計(Rust) + +### 1. Lexer拡張設計 +```rust +// 双方向トランスコーダー +pub struct AncpTranscoder { + // 静的マップ(phf使用でO(1)ルックアップ) + nyash_to_ancp: phf::Map<&'static str, &'static str>, + ancp_to_nyash: phf::Map<&'static str, &'static str>, + + // 位置情報マッピング(Source Map相当) + span_map: SpanMap, + + // バージョン情報 + version: AncpVersion, +} + +// LosslessToken層(空白・コメント保持) +struct Token { + kind: TokenKind, + span: Span, + raw: RawLexemeId, + leading_trivia: Trivia, + trailing_trivia: Trivia, +} + +// Dialect検出 +enum Dialect { + Nyash, + ANCP(Version), +} +``` + +### 2. トークン効率最適化 +```python +# tiktoken実測スクリプト +import tiktoken +enc = tiktoken.get_encoding("cl100k_base") # GPT-4等 + +def measure_tokens(code): + return len(enc.encode(code)) + +# 実測して最適な記号を選定 +original = 'box Cat from Animal { init { name } }' +ancp = '$Cat@Animal{#{name}}' + +reduction = 1 - (measure_tokens(ancp) / measure_tokens(original)) +print(f"Token reduction: {reduction:.1%}") +``` + +### 3. 双方向変換保証テスト戦略 +```rust +// プロパティベーステスト +#[test] +fn roundtrip_property(code in any_valid_nyash()) { + let transcoder = AncpTranscoder::new(); + let (ancp, _) = transcoder.encode(&code)?; + let (restored, _) = transcoder.decode(&ancp)?; + assert_eq!(code, restored); // 完全一致 +} + +// AST同値性テスト +#[test] +fn ast_equivalence(code: &str) { + let ast_original = parse_nyash(code); + let ancp = transcode_to_ancp(code); + let ast_ancp = parse_ancp(ancp); + assert_ast_eq!(ast_original, ast_ancp); +} + +// ファジングテスト +#[cfg(fuzzing)] +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + let _ = transcode_roundtrip(s); + } +}); +``` + +### 4. デバッグ体験の維持 +```rust +// ハイブリッドエラー表示 +fn format_error(span: Span, ancp_src: &str, nyash_src: &str) { + let ancp_token = get_token_at(ancp_src, span); + let nyash_token = map_to_original(ancp_token); + eprintln!("Error at {} ({})", ancp_token, nyash_token); + // 例: "Error at @f (from)" +} + +// IDE統合(LSP) +impl LanguageServer for NyashLsp { + fn hover(&self, position: Position) -> Hover { + // ANCP記号にホバーすると元の予約語を表示 + let original = self.map_to_original(position); + Hover { contents: format!("ANCP: {} → Nyash: {}", ...) } + } +} +``` + +## スモークテストとの統合 + +### 統一検証フレームワーク +```nyash +// 検証用Box - スモークテストとANCP検証を統合 +box TestValidatorBox { + init { patterns, mode, transcoder } + + birth() { + me.patterns = new ArrayBox() + me.mode = "smoke" // smoke/full/ancp/roundtrip + me.transcoder = new AncpTranscoder() + } + + // 共通のパターンチェック(既存スモークテストの仕組み) + checkPatterns(output) { + loop(me.patterns.hasNext()) { + local pattern + pattern = me.patterns.next() + + if (not output.contains(pattern)) { + return false + } + } + return true + } + + // Nyashコード実行して検証(既存) + validateScript(scriptPath) { + local output + output = me.runNyash(scriptPath) + return me.checkPatterns(output) + } + + // ANCP変換して実行・検証(新規) + validateAncp(ancpPath) { + // 1. ANCP → Nyash変換 + local nyashCode + nyashCode = me.transcoder.decode(ancpPath) + + // 2. 実行(既存の実行パスを再利用) + local output + output = me.runNyashCode(nyashCode) + + // 3. パターン検証(既存のcheckPatternsを再利用!) + return me.checkPatterns(output) + } + + // 双方向テスト(ANCP特有) + validateRoundtrip(scriptPath) { + // 元の実行結果 + local originalOutput + originalOutput = me.runNyash(scriptPath) + + // Nyash → ANCP → Nyash → 実行 + local ancp + ancp = me.transcoder.encode(scriptPath) + local restored + restored = me.transcoder.decode(ancp) + local roundtripOutput + roundtripOutput = me.runNyashCode(restored) + + // 実行結果が同一か検証 + return originalOutput == roundtripOutput + } +} +``` + +### 統合スモークテストスクリプト +```bash +#!/bin/bash +# 統一スモークテスト(Nyash & ANCP対応) + +# 共通の検証関数 +run_test() { + local mode=$1 # nyash/ancp/roundtrip + local file=$2 # ファイルパス + local pattern=$3 # 期待パターン + + case "$mode" in + "ancp") + # ANCP → Nyash変換 → 実行 + output=$(./target/release/nyash --from-ancp "$file" 2>&1) + ;; + "roundtrip") + # Nyash → ANCP → Nyash → 実行 + ancp=$(./target/release/nyash --to-ancp "$file") + output=$(echo "$ancp" | ./target/release/nyash --from-ancp - 2>&1) + ;; + *) + # 通常のNyash実行 + output=$(./target/release/nyash "$file" 2>&1) + ;; + esac + + # 共通のパターン検証(既存のスモークテストと同じ!) + echo "$output" | grep -q "$pattern" +} + +# 自動テスト生成 +generate_ancp_tests() { + for test in examples/*.nyash; do + # Nyashテストから自動的にANCPテストを生成 + ./tools/nyash2ancp "$test" > "${test%.nyash}.ancp" + + # 同じ期待値パターンで両方をテスト + pattern=$(extract_expected_pattern "$test") + + echo "Testing $test (Nyash)..." + run_test nyash "$test" "$pattern" + + echo "Testing ${test%.nyash}.ancp (ANCP)..." + run_test ancp "${test%.nyash}.ancp" "$pattern" + + echo "Testing roundtrip for $test..." + run_test roundtrip "$test" "$pattern" + done +} +``` + +### CI/CD統合 +```yaml +# .github/workflows/smoke.yml への追加 +jobs: + smoke-ancp: + runs-on: ubuntu-latest + needs: smoke # 通常のスモークテスト後に実行 + env: + CARGO_TERM_COLOR: always + NYASH_DISABLE_PLUGINS: '1' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build with ANCP support + run: cargo build --release -j32 --features ancp + + - name: Run ANCP roundtrip tests + run: | + # すべてのサンプルでANCP往復テスト + for f in examples/*.nyash; do + echo "Testing ANCP roundtrip: $f" + ./tools/test_ancp_roundtrip.sh "$f" + done + + - name: Run ANCP smoke tests + run: | + # 既存のスモークテストをANCPモードでも実行 + ANCP_MODE=1 bash tools/smoke_phase_10_10.sh + + - name: Measure token reduction + run: | + # 実際のトークン削減率を測定・報告 + ./tools/measure_ancp_efficiency.sh examples/*.nyash +``` + +### 共通化のメリット + +1. **コード再利用**: + - 既存のパターン検証ロジックをそのまま活用 + - `grep`ベースの柔軟な検証方法を継承 + +2. **テスト網羅性**: + - すべてのNyashテストが自動的にANCPテストにもなる + - 双方向変換の正確性を継続的に検証 + +3. **開発効率**: + - 新機能追加時、Nyashテストを書けばANCPテストも自動生成 + - スモークテストの期待値パターンを共有 + +4. **品質保証**: + - ANCP変換によるセマンティクスの保持を自動検証 + - AIが生成したANCPコードも同じ基準で検証可能 + +5. **将来の拡張性**: + - WebAssembly出力との統合も同じフレームワークで可能 + - 他の中間表現(MIR等)も同様に統合可能 + +## 実装ロードマップ(更新版) + +### Phase 1: 基礎実装(2週間) +- [ ] tiktoken実測による最適記号選定 +- [ ] 固定辞書(20語)でTranscoder実装 +- [ ] 基本的な往復テスト +- [ ] **既存スモークテストとの基本統合** + +### Phase 2: パーサー統合(3週間) +- [ ] Lexer拡張(Dialect検出) +- [ ] TokenKind統一化 +- [ ] SpanMapによるエラー位置マッピング +- [ ] **TestValidatorBox実装** + +### Phase 3: 品質保証(2週間) +- [ ] プロパティベーステスト +- [ ] ファジングテスト +- [ ] ベンチマーク(性能測定) +- [ ] **自動ANCP テスト生成スクリプト** + +### Phase 4: ツール統合(3週間) +- [ ] CLI: --view=ancp|nyash|hybrid +- [ ] VSCode拡張(ホバー変換) +- [ ] プロトコルヘッダー処理 +- [ ] **CI/CDパイプライン統合** + +### Phase 5: AI統合(4週間) +- [ ] 並列コーパス生成 +- [ ] ファインチューニング実験 +- [ ] 実運用評価 +- [ ] **AIコード検証の自動化** + +## リスクと対策 + +### 技術的リスク +1. **記号衝突**: 既存演算子との衝突 → 慎重な記号選定 +2. **デバッグ困難**: 圧縮コードの可読性 → ハイブリッド表示 +3. **バージョン互換**: 将来の拡張 → ヘッダーによるバージョニング +4. **テスト複雑性**: ANCP特有のバグ → スモークテスト統合で早期発見 + +### 運用リスク +1. **AIの誤解釈**: 記号の誤認識 → 明示的なヘッダー +2. **人間の混乱**: 2つの記法混在 → 明確な使い分け +3. **ツール対応**: エコシステム分断 → 段階的移行 +4. **テスト負荷**: 2倍のテスト実行 → 並列実行とキャッシュ活用 + +## 成功指標(更新版) +- トークン削減率: 50%以上(目標70%) +- 往復変換成功率: 100% +- パース時間増加: 10%以内 +- AIエラー率改善: 30%以上 +- **スモークテスト合格率: 100%(Nyash/ANCP両方)** +- **CI実行時間増加: 20%以内** + +## 関連技術・参考 +- JavaScript Minification (Terser, UglifyJS) +- WebAssembly (.wat ⇔ .wasm) +- Protocol Buffers (スキーマベース圧縮) +- Source Maps (位置情報マッピング) +- **Nyashスモークテスト基盤**(パターン検証方式) + +## 専門家レビュー +- Gemini: トークナイザ最適化、記号体系化の重要性を指摘 +- Codex: Rust実装設計、テスト戦略の詳細を提供 +- Claude: スモークテストとの統合可能性を発見 + +## 次のアクション +1. tiktoken実測実験の実施 +2. 最小プロトタイプの作成 +3. ChatGPTさんとの実装協議 +4. **既存スモークテストへのANCPモード追加** + +--- +*このアイデアは2025-08-29にGeminiさん、Codexさんとの深い技術討論を経て、実装可能な設計として成熟し、さらにスモークテストとの統合により品質保証も含めた完全なソリューションとなりました。* \ No newline at end of file diff --git a/docs/reference/architecture/execution-backends.md b/docs/reference/architecture/execution-backends.md index 21989d24..8f97cd0d 100644 --- a/docs/reference/architecture/execution-backends.md +++ b/docs/reference/architecture/execution-backends.md @@ -1,6 +1,6 @@ # Nyash実行バックエンド完全ガイド -Nyashプログラミング言語は、**Everything is Box**哲学を維持しながら、3つの異なる実行方式をサポートしています。用途に応じて最適な実行方式を選択できます。 +Nyashプログラミング言語は、**Everything is Box**哲学を維持しながら、4つの異なる実行方式をサポートしています。用途に応じて最適な実行方式を選択できます。 ## 🚀 実行方式一覧 @@ -9,6 +9,7 @@ Nyashプログラミング言語は、**Everything is Box**哲学を維持しな | **インタープリター** | 開発・デバッグ | 直接AST実行、詳細ログ | 低速・高機能 | | **VM** | 本番・高速実行 | MIR→VM実行 | 中速・最適化 | | **WASM** | Web・サンドボックス | MIR→WASM変換 | 高速・移植性 | +| **AOT** (実験的) | 高速起動 | MIR→WASM→ネイティブ | 最高速・要wasmtime | ## 📋 CLIオプション @@ -54,6 +55,20 @@ nyash --compile-wasm program.nyash -o output.wat nyash --compile-wasm program.nyash -o public/app.wat ``` +### AOT/ネイティブコンパイル(実験的機能) +```bash +# ビルド時に wasm-backend feature が必要 +cargo build --release --features wasm-backend + +# AOTコンパイル(.cwasm生成) +nyash --compile-native program.nyash -o program +# または +nyash --aot program.nyash -o program + +# 注意: 現在は完全なスタンドアロン実行ファイルではなく、 +# wasmtime用のプリコンパイル済みWASM(.cwasm)が生成されます +``` + ### ⚡ ベンチマーク(パフォーマンス測定) ```bash # 全バックエンド性能比較(デフォルト5回実行) @@ -186,6 +201,28 @@ NYASH_GC_COUNTING=1 NYASH_GC_TRACE=2 \ ./target/release/nyash --backend vm examples/scheduler_demo.nyash ``` +#### Boxからの切替(GcConfigBox) +環境変数ではなくNyashスクリプトから切り替える場合は `GcConfigBox` を使います。`apply()` で環境に反映され、その後の実行に適用されます。 + +```nyash +// 最小例: CountingGc + trace をオン +static box Main { + main() { + local G + G = new GcConfigBox() + G = G.setFlag("counting", true) + G = G.setFlag("trace", true) // 1/2/3 は環境。Box では on/off を切替 + G.apply() // ← ここで反映 + return "ok" + } +} +``` + +代表デモ(書き込みバリア/カウンタの可視化): +```bash +./target/release/nyash --backend vm examples/gc_counting_demo.nyash +``` + ## 🌐 WASM実行(Web対応) ### 特徴 @@ -248,6 +285,58 @@ async function loadNyashWasm() { ``` +## 🚀 AOT/ネイティブコンパイル(実験的) + +### 特徴 +- **用途**: 高速起動・配布用実行ファイル +- **実行**: AST→MIR→WASM→プリコンパイル済みネイティブ +- **速度**: 最高速(JIT起動オーバーヘッドなし) +- **制約**: wasmtimeランタイムが必要 + +### コンパイルパイプライン +``` +Nyashソース → AST → MIR → WASM → .cwasm(プリコンパイル済み) +``` + +### ビルド方法 +```bash +# 1. wasm-backend feature付きでNyashをビルド +cargo build --release --features wasm-backend + +# 2. AOTコンパイル実行 +./target/release/nyash --compile-native hello.nyash -o hello +# または短縮形 +./target/release/nyash --aot hello.nyash -o hello + +# 3. 生成されたファイル +# hello.cwasm が生成される(wasmtimeプリコンパイル形式) +``` + +### 現在の実装状況 +- ✅ MIR→WASM変換 +- ✅ WASM→.cwasm(wasmtimeプリコンパイル) +- ❌ 完全なスタンドアロン実行ファイル生成(TODO) +- ❌ ランタイム埋め込み(将来実装予定) + +### 使用例 +```bash +# コンパイル +./target/release/nyash --aot examples/hello_world.nyash -o hello_aot + +# 実行(将来的な目標) +# ./hello_aot # 現在は未実装 + +# 現在は wasmtime で実行 +# wasmtime --precompiled hello_aot.cwasm +``` + +### 技術的詳細 +AOTバックエンドは内部的に以下の処理を行います: +1. **MirCompiler**: NyashコードをMIRに変換 +2. **WasmBackend**: MIRをWASMバイトコードに変換 +3. **wasmtime::Engine::precompile**: WASMをネイティブコードにプリコンパイル +4. **出力**: .cwasm形式で保存(wasmtime独自形式) + ## 📊 パフォーマンス比較 ### 🚀 実際のベンチマーク結果(2025-08-14測定・修正) diff --git a/docs/reference/jit/jit_events_json_v0_1.md b/docs/reference/jit/jit_events_json_v0_1.md new file mode 100644 index 00000000..f5679680 --- /dev/null +++ b/docs/reference/jit/jit_events_json_v0_1.md @@ -0,0 +1,52 @@ +# JIT Events JSON (v0.1) + +最小のイベントJSONスキーマ。観測の足場として、値は安定キーのみを出力します。 + +- 出力形態: 1行 = 1 JSON(JSONL) +- 出力先: 標準出力 or `NYASH_JIT_EVENTS_PATH` で指定したファイル +- phase分離: + - compile: `phase: "lower"`(明示opt-in: `NYASH_JIT_EVENTS_COMPILE=1`) + - runtime: `phase: "execute"`(既定ON可: `NYASH_JIT_EVENTS=1` または `NYASH_JIT_EVENTS_RUNTIME=1`) + +## フィールド(必須) +- kind: 文字列(例: "hostcall") +- function: 文字列(現状は "" で固定) +- phase: 文字列("lower" | "execute") +- id: シンボル名(例: "nyash.map.get_hh") +- decision: 文字列("allow" | "fallback") +- reason: 文字列("sig_ok" | "receiver_not_param" | "policy_denied_mutating" | "sig_mismatch" など) +- argc: 数値(観測引数数) +- arg_types: 文字列配列(例: ["Handle","I64"]) + +任意フィールド(存在時のみ) +- handle: 数値(JITハンドル) +- ms: 数値(処理時間ミリ秒) + +## 出力例 + +compile(lower): +```json +{"kind":"hostcall","function":"","id":"nyash.map.get_hh","decision":"allow","reason":"sig_ok","argc":2,"arg_types":["Handle","Handle"],"phase":"lower"} +``` + +runtime(execute): +```json +{"kind":"hostcall","function":"","id":"nyash.array.push_h","decision":"fallback","reason":"policy_denied_mutating","argc":2,"arg_types":["Handle","I64"],"phase":"execute"} +``` + +trap(execute 中の失敗): +```json +{"kind":"trap","function":"","reason":"jit_execute_failed","ms":0,"phase":"execute"} +``` + +## 環境変数(抜粋) +- NYASH_JIT_EVENTS=1: 既定のruntime出力 +- NYASH_JIT_EVENTS_COMPILE=1: compile(lower)出力 +- NYASH_JIT_EVENTS_RUNTIME=1: runtime出力 +- NYASH_JIT_EVENTS_PATH=path.jsonl: ファイルに追記 +- NYASH_JIT_THRESHOLD(未設定時): 観測ONで自動的に1が補われます(Runner/DebugConfigBoxが補助) + +## 推奨の最小運用 +- 現象確認: `NYASH_JIT_EVENTS=1`(runtimeのみ) +- 解析時のみcompile出力: `NYASH_JIT_EVENTS_COMPILE=1 NYASH_JIT_EVENTS_PATH=events.jsonl` +- HostCall系の例では `NYASH_JIT_HOSTCALL=1` を明示 diff --git a/events.jsonl b/events.jsonl new file mode 100644 index 00000000..5c8dd711 --- /dev/null +++ b/events.jsonl @@ -0,0 +1,6 @@ +{"kind":"hostcall","function":"","arg_types":["Handle","I64","I64"],"argc":3,"decision":"fallback","id":"nyash.map.set_h","reason":"receiver_not_param"} +{"kind":"hostcall","function":"","arg_types":["Handle","I64","I64"],"argc":3,"decision":"fallback","id":"nyash.map.set_h","phase":"lower","reason":"receiver_not_param"} +{"kind":"hostcall","function":"","arg_types":["Handle","I64"],"argc":2,"decision":"allow","id":"nyash.array.push_h","reason":"sig_ok"} +{"kind":"hostcall","function":"","arg_types":["Handle"],"argc":1,"decision":"allow","id":"nyash.any.length_h","reason":"sig_ok"} +{"kind":"hostcall","function":"","arg_types":["Handle","I64"],"argc":2,"decision":"allow","id":"nyash.array.push_h","reason":"sig_ok"} +{"kind":"hostcall","function":"","arg_types":["Handle"],"argc":1,"decision":"allow","id":"nyash.any.length_h","reason":"sig_ok"} diff --git a/events_2.jsonl b/events_2.jsonl new file mode 100644 index 00000000..cdff7f7d --- /dev/null +++ b/events_2.jsonl @@ -0,0 +1,5 @@ +{"kind":"hostcall","function":"","arg_types":["Handle","I64","I64"],"argc":3,"decision":"fallback","id":"nyash.map.set_h","reason":"receiver_not_param"} +{"kind":"hostcall","function":"","arg_types":["Handle","I64"],"argc":2,"decision":"allow","id":"nyash.array.push_h","reason":"sig_ok"} +{"kind":"hostcall","function":"","arg_types":["Handle"],"argc":1,"decision":"allow","id":"nyash.any.length_h","reason":"sig_ok"} +{"kind":"hostcall","function":"","arg_types":["Handle","I64"],"argc":2,"decision":"allow","id":"nyash.array.push_h","reason":"sig_ok"} +{"kind":"hostcall","function":"","arg_types":["Handle"],"argc":1,"decision":"allow","id":"nyash.any.length_h","reason":"sig_ok"} diff --git a/events_3.jsonl b/events_3.jsonl new file mode 100644 index 00000000..f8ba1e11 --- /dev/null +++ b/events_3.jsonl @@ -0,0 +1,5 @@ +{"kind":"hostcall","function":"","arg_types":["Handle","I64","I64"],"argc":3,"decision":"fallback","id":"nyash.map.set_h","phase":"lower","reason":"receiver_not_param"} +{"kind":"hostcall","function":"","arg_types":["Handle","I64"],"argc":2,"decision":"fallback","id":"nyash.array.push_h","reason":"policy_denied_mutating"} +{"kind":"hostcall","function":"","arg_types":["Handle"],"argc":1,"decision":"allow","id":"nyash.any.length_h","reason":"sig_ok"} +{"kind":"hostcall","function":"","arg_types":["Handle","I64"],"argc":2,"decision":"fallback","id":"nyash.array.push_h","reason":"policy_denied_mutating"} +{"kind":"hostcall","function":"","arg_types":["Handle"],"argc":1,"decision":"allow","id":"nyash.any.length_h","reason":"sig_ok"} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..c72b6582 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,57 @@ +# Examples Quick Start (Minimal) + +このページはPhase 10.10の再起動用ミニ手順です。3つだけ確かめればOK。 + +- 事前ビルド: `cargo build --release -j32` +- 実行は `./target/release/nyash` を使用 + +## 1) HH直実行(Map.get_hh) +- 目的: 受け手/キーが関数パラメータのとき、JIT HostCallを許可(HH経路)。 +- 実行: +``` +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_map_get_param_hh.nyash +``` +- 期待: `allow id: nyash.map.get_hh` イベントが出る。戻り値は `value1`。 + +## 2) mutating opt-in(JitPolicyBox) +- 目的: 既定 read_only。必要最小の書き込みだけホワイトリストで許可。 +- 実行: +``` +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_policy_optin_mutating.nyash +``` +- 期待: 1回目は `policy_denied_mutating` でfallback、whitelist後の2回目はallow。 + +イベントの見やすさ(任意): +``` +# コンパイル時(lower)のみ: phase="lower" が付与(compileは明示opt-in) +NYASH_JIT_EVENTS_COMPILE=1 NYASH_JIT_EVENTS_PATH=events.jsonl ... + +# 実行時(runtime)のみ: phase="execute" が付与される +NYASH_JIT_EVENTS_RUNTIME=1 NYASH_JIT_EVENTS_PATH=events.jsonl ... +``` + +## 3) CountingGc デモ +- 目的: GCのカウント/トレース/バリア観測の導線確認(VM経路)。 +- 実行: +``` +./target/release/nyash --backend vm examples/gc_counting_demo.nyash +``` +- Tips: 詳細ログは `NYASH_GC_COUNTING=1 NYASH_GC_TRACE=2` を併用。 + +## 4) Policy whitelist(events分離) +- 目的: read_only下でのfallback→allow(whitelist)と、compile/runtimeのphase分離をイベントで確認。 +- 実行(しきい値=1を明示/またはDebugConfigBoxでapply後にRunnerが自動設定): +``` +NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 \ + ./target/release/nyash --backend vm examples/jit_policy_whitelist_demo.nyash +``` +- 期待: `policy_events.jsonl` に `phase:"lower"`(計画)と `phase:"execute"`(実績)が出る。 + +--- + +補足 +- DebugConfigBox(events/stats/dump/dot)と GcConfigBox は Box から `apply()` で環境へ反映できます。 +- `--emit-cfg path.dot` または `DebugConfigBox.setPath("jit_dot", path)` でCFGのDOT出力。いずれもdumpを自動有効化。 +- イベントは `phase` フィールドで区別(lower/execute)。`jit_events_path` でJSONL出力先を指定可能。 diff --git a/examples/any_helpers_demo.nyash b/examples/any_helpers_demo.nyash new file mode 100644 index 00000000..85e39d1a --- /dev/null +++ b/examples/any_helpers_demo.nyash @@ -0,0 +1,81 @@ +// ANY Helpers Demo - 型を問わない統一的な操作 +// ANYヘルパーは Array, String, Map すべてに対応する万能メソッド! + +static box Main { + main() { + local console + console = new ConsoleBox() + + console.log("=== ANY Helpers Demo ===") + console.log("") + + // 1. length() - どんなBoxでも長さ/サイズを取得 + console.log("1) length() helper:") + + local arr + arr = new ArrayBox() + arr.push("cat") + arr.push("dog") + console.log("Array length: " + arr.length()) // 2 + + local str + str = new StringBox("Hello") + console.log("String length: " + str.length()) // 5 + + local map + map = new MapBox() + map.set("name", "Nyash") + map.set("version", "1.0") + console.log("Map size: " + map.length()) // 2 + console.log("") + + // 2. isEmpty() - どんなBoxでも空かチェック + console.log("2) isEmpty() helper:") + + local emptyArr + emptyArr = new ArrayBox() + console.log("Empty array: " + emptyArr.isEmpty()) // true + arr.push("fish") + console.log("Non-empty array: " + arr.isEmpty()) // false + + local emptyStr + emptyStr = new StringBox("") + console.log("Empty string: " + emptyStr.isEmpty()) // true + local fullStr + fullStr = new StringBox("Nyash") + console.log("Non-empty string: " + fullStr.isEmpty()) // false + + local emptyMap + emptyMap = new MapBox() + console.log("Empty map: " + emptyMap.isEmpty()) // true + console.log("Non-empty map: " + map.isEmpty()) // false + console.log("") + + // 3. 実用例:型を気にせず処理 + console.log("3) Practical example - type-agnostic processing:") + + local items + items = new ArrayBox() + items.push(arr) // Array + items.push(str) // String + items.push(map) // Map + + // どんな型でも統一的に処理できる! + local i + i = 0 + loop(i < items.length()) { + local item + item = items.get(i) + console.log("Item " + i + " - length: " + item.length() + ", empty: " + item.isEmpty()) + i = i + 1 + } + + console.log("") + console.log("=== ANYヘルパーの威力 ===") + console.log("- 型固有のメソッドを覚える必要なし") + console.log("- Array/String/Mapを統一的に扱える") + console.log("- Everything is Box 哲学の体現!") + + return "Demo complete!" + } +} \ No newline at end of file diff --git a/examples/jit_any_helpers_demo.nyash b/examples/jit_any_helpers_demo.nyash new file mode 100644 index 00000000..32c71762 --- /dev/null +++ b/examples/jit_any_helpers_demo.nyash @@ -0,0 +1,44 @@ +// JIT ANY Helpers Demo - JIT内部での統一的な型処理 +// ANYヘルパーは nyash.any.length_h と nyash.any.is_empty_h として +// JITのHostCall経由で Array/String/Map を統一的に扱える! + +box Helper { + birth() {} + + // このメソッドがJITコンパイルされると、内部的にANYヘルパーが使われる + getInfo(obj) { + // JIT内部では nyash.any.length_h が呼ばれる + local len + len = obj.length() // Array/Stringなら動作 + + // JIT内部では nyash.any.is_empty_h が呼ばれる + local empty + empty = obj.isEmpty() // Array/Stringなら動作 + + return len + } +} + +static box Main { + main() { + local H + H = new Helper() + + // Array での使用 + local arr + arr = new ArrayBox() + arr.push("cat") + arr.push("dog") + print("Array info: " + H.getInfo(arr)) // 2 + + // String での使用 + local str + str = new StringBox("Hello") + print("String info: " + H.getInfo(str)) // 5 + + // 注意: MapBoxは現在のインタープリターではlength()未実装 + // JITでは nyash.any.length_h が Map.size() にマップされる + + return "Done" + } +} \ No newline at end of file diff --git a/examples/jit_policy_whitelist_demo.nyash b/examples/jit_policy_whitelist_demo.nyash new file mode 100644 index 00000000..f10697f9 --- /dev/null +++ b/examples/jit_policy_whitelist_demo.nyash @@ -0,0 +1,44 @@ +// JitPolicy whitelist demo with DebugConfigBox event setup. +// Goal: show fallback on read_only, then allow after whitelist. +// Run (Cranelift enabled recommended): +// cargo build --release -j32 --features cranelift-jit +// NYASH_JIT_HOSTCALL=1 ./target/release/nyash --backend vm examples/jit_policy_whitelist_demo.nyash +// Check events file: +// cat policy_events.jsonl # contains phase=lower/execute decisions + +box Helper { + birth() {} + add(a) { + a.push(1) + return a.length() + } +} + +static box Main { + main() { + // Enable compile-time events and output path via DebugConfigBox + local D + D = new DebugConfigBox() + D = D.setFlag("jit_events_compile", true) + D = D.setFlag("jit_events_runtime", true) + D = D.setPath("jit_events_path", "policy_events.jsonl") + D.apply() + + // Prepare JIT policy: read_only first + local P + P = new JitPolicyBox() + P.set("read_only", true) + + // Prepare data and helper + local H, A + H = new Helper() + A = new ArrayBox() + + // 1st call: should fallback by policy (mutating denied) + H.add(A) + + // Whitelist push_h and retry (expect allow) + P.addWhitelist("nyash.array.push_h") + return H.add(A) + } +} diff --git a/hh_events.jsonl b/hh_events.jsonl new file mode 100644 index 00000000..b1d3cc6d --- /dev/null +++ b/hh_events.jsonl @@ -0,0 +1,3 @@ +{"kind":"hostcall","function":"","arg_types":["Handle","I64","I64"],"argc":3,"decision":"fallback","id":"nyash.map.set_h","phase":"lower","reason":"receiver_not_param"} +{"kind":"hostcall","function":"","arg_types":["Handle","Handle"],"argc":2,"decision":"allow","id":"nyash.map.get_hh","phase":"lower","reason":"sig_ok"} +{"kind":"hostcall","function":"","arg_types":["Handle","Handle"],"argc":2,"decision":"allow","id":"nyash.map.get_hh"} diff --git a/out.dot b/out.dot new file mode 100644 index 00000000..9a20790f --- /dev/null +++ b/out.dot @@ -0,0 +1,4 @@ +digraph "Helper.getv/2" { + node [shape=box, fontsize=10]; + n2 [label="bb2\nENTRY"]; +} diff --git a/src/backend/vm.rs b/src/backend/vm.rs index c61ac42c..c28d8be0 100644 --- a/src/backend/vm.rs +++ b/src/backend/vm.rs @@ -559,6 +559,10 @@ impl VM { self.current_function = Some(function.signature.name.clone()); // Phase 10_a: JIT profiling (function entry) if let Some(jm) = &mut self.jit_manager { + // Allow threshold to react to env updates (e.g., DebugConfigBox.apply at runtime) + if let Ok(s) = std::env::var("NYASH_JIT_THRESHOLD") { + if let Ok(t) = s.parse::() { if t > 0 { jm.set_threshold(t); } } + } 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); diff --git a/src/boxes/debug_config_box.rs b/src/boxes/debug_config_box.rs index 5543218e..8c651559 100644 --- a/src/boxes/debug_config_box.rs +++ b/src/boxes/debug_config_box.rs @@ -6,10 +6,13 @@ pub struct DebugConfigBox { pub base: BoxBase, // toggles/paths (staged until apply()) pub jit_events: bool, + pub jit_events_compile: bool, + pub jit_events_runtime: bool, pub jit_stats: bool, pub jit_stats_json: bool, pub jit_dump: bool, pub jit_dot_path: Option, + pub jit_events_path: Option, } impl DebugConfigBox { @@ -17,16 +20,21 @@ impl DebugConfigBox { Self { base: BoxBase::new(), jit_events: std::env::var("NYASH_JIT_EVENTS").ok().as_deref() == Some("1"), + jit_events_compile: std::env::var("NYASH_JIT_EVENTS_COMPILE").ok().as_deref() == Some("1"), + jit_events_runtime: std::env::var("NYASH_JIT_EVENTS_RUNTIME").ok().as_deref() == Some("1"), jit_stats: std::env::var("NYASH_JIT_STATS").ok().as_deref() == Some("1"), jit_stats_json: std::env::var("NYASH_JIT_STATS_JSON").ok().as_deref() == Some("1"), jit_dump: std::env::var("NYASH_JIT_DUMP").ok().as_deref() == Some("1"), jit_dot_path: std::env::var("NYASH_JIT_DOT").ok().filter(|s| !s.is_empty()), + jit_events_path: std::env::var("NYASH_JIT_EVENTS_PATH").ok().filter(|s| !s.is_empty()), } } pub fn set_flag(&mut self, name: &str, on: bool) -> Box { match name { "jit_events" => self.jit_events = on, + "jit_events_compile" => self.jit_events_compile = on, + "jit_events_runtime" => self.jit_events_runtime = on, "jit_stats" => self.jit_stats = on, "jit_stats_json" => self.jit_stats_json = on, "jit_dump" => self.jit_dump = on, @@ -38,6 +46,7 @@ impl DebugConfigBox { pub fn set_path(&mut self, name: &str, value: &str) -> Box { match name { "jit_dot" | "jit_dot_path" => self.jit_dot_path = Some(value.to_string()), + "jit_events_path" => self.jit_events_path = Some(value.to_string()), _ => return Box::new(StringBox::new(format!("Unknown path: {}", name))) } Box::new(VoidBox::new()) @@ -46,6 +55,8 @@ impl DebugConfigBox { pub fn get_flag(&self, name: &str) -> Box { let v = match name { "jit_events" => self.jit_events, + "jit_events_compile" => self.jit_events_compile, + "jit_events_runtime" => self.jit_events_runtime, "jit_stats" => self.jit_stats, "jit_stats_json" => self.jit_stats_json, "jit_dump" => self.jit_dump, @@ -57,6 +68,7 @@ impl DebugConfigBox { pub fn get_path(&self, name: &str) -> Box { let v = match name { "jit_dot" | "jit_dot_path" => self.jit_dot_path.clone().unwrap_or_default(), + "jit_events_path" => self.jit_events_path.clone().unwrap_or_default(), _ => String::new(), }; Box::new(StringBox::new(v)) @@ -65,18 +77,31 @@ impl DebugConfigBox { pub fn apply(&self) -> Box { let setb = |k: &str, v: bool| { if v { std::env::set_var(k, "1"); } else { std::env::remove_var(k); } }; setb("NYASH_JIT_EVENTS", self.jit_events); + setb("NYASH_JIT_EVENTS_COMPILE", self.jit_events_compile); + setb("NYASH_JIT_EVENTS_RUNTIME", self.jit_events_runtime); setb("NYASH_JIT_STATS", self.jit_stats); setb("NYASH_JIT_STATS_JSON", self.jit_stats_json); setb("NYASH_JIT_DUMP", self.jit_dump); if let Some(p) = &self.jit_dot_path { std::env::set_var("NYASH_JIT_DOT", p); } + else { std::env::remove_var("NYASH_JIT_DOT"); } + if let Some(p) = &self.jit_events_path { std::env::set_var("NYASH_JIT_EVENTS_PATH", p); } + else { std::env::remove_var("NYASH_JIT_EVENTS_PATH"); } + // If any events are enabled and threshold is not set, default to 1 so lowering runs early + if (self.jit_events || self.jit_events_compile || self.jit_events_runtime) + && std::env::var("NYASH_JIT_THRESHOLD").is_err() + { + std::env::set_var("NYASH_JIT_THRESHOLD", "1"); + } Box::new(VoidBox::new()) } pub fn summary(&self) -> Box { let s = format!( - "jit_events={} jit_stats={} jit_stats_json={} jit_dump={} jit_dot={}", - self.jit_events, self.jit_stats, self.jit_stats_json, self.jit_dump, - self.jit_dot_path.clone().unwrap_or_else(|| "".to_string()) + "jit_events={} jit_events_compile={} jit_events_runtime={} jit_stats={} jit_stats_json={} jit_dump={} jit_dot={} jit_events_path={}", + self.jit_events, self.jit_events_compile, self.jit_events_runtime, + self.jit_stats, self.jit_stats_json, self.jit_dump, + self.jit_dot_path.clone().unwrap_or_else(|| "".to_string()), + self.jit_events_path.clone().unwrap_or_else(|| "".to_string()) ); Box::new(StringBox::new(s)) } diff --git a/src/cli.rs b/src/cli.rs index e45d958c..7ddf4821 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -31,6 +31,10 @@ pub struct CliConfig { pub jit_stats: bool, pub jit_stats_json: bool, pub jit_dump: bool, + pub jit_events: bool, + pub jit_events_compile: bool, + pub jit_events_runtime: bool, + pub jit_events_path: Option, pub jit_threshold: Option, pub jit_phi_min: bool, pub jit_hostcall: bool, @@ -186,6 +190,30 @@ impl CliConfig { .help("Dump JIT lowering summary (NYASH_JIT_DUMP=1)") .action(clap::ArgAction::SetTrue) ) + .arg( + Arg::new("jit-events") + .long("jit-events") + .help("Emit JIT events as JSONL (NYASH_JIT_EVENTS=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-events-compile") + .long("jit-events-compile") + .help("Emit compile-time (lower) JIT events (NYASH_JIT_EVENTS_COMPILE=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-events-runtime") + .long("jit-events-runtime") + .help("Emit runtime JIT events (NYASH_JIT_EVENTS_RUNTIME=1)") + .action(clap::ArgAction::SetTrue) + ) + .arg( + Arg::new("jit-events-path") + .long("jit-events-path") + .value_name("FILE") + .help("Write JIT events JSONL to file (NYASH_JIT_EVENTS_PATH)") + ) .arg( Arg::new("jit-threshold") .long("jit-threshold") @@ -265,6 +293,10 @@ impl CliConfig { jit_stats: matches.get_flag("jit-stats"), jit_stats_json: matches.get_flag("jit-stats-json"), jit_dump: matches.get_flag("jit-dump"), + jit_events: matches.get_flag("jit-events"), + jit_events_compile: matches.get_flag("jit-events-compile"), + jit_events_runtime: matches.get_flag("jit-events-runtime"), + jit_events_path: matches.get_one::("jit-events-path").cloned(), 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"), @@ -323,12 +355,19 @@ mod tests { jit_stats: false, jit_stats_json: false, jit_dump: false, + jit_events: false, + jit_events_compile: false, + jit_events_runtime: false, + jit_events_path: None, jit_threshold: None, jit_phi_min: false, jit_hostcall: false, jit_handle_debug: false, jit_native_f64: false, jit_native_bool: false, + emit_cfg: None, + jit_only: false, + jit_direct: false, }; assert_eq!(config.backend, "interpreter"); diff --git a/src/jit/config.rs b/src/jit/config.rs index 40e0afac..09833303 100644 --- a/src/jit/config.rs +++ b/src/jit/config.rs @@ -24,11 +24,15 @@ 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()); + // Respect explicit dump flag, but also treat a non-empty NYASH_JIT_DOT path + // as an implicit request to enable dump (so Box/CLI/env stay consistent). + let dump_flag = getb("NYASH_JIT_DUMP") + || std::env::var("NYASH_JIT_DOT").ok().map(|s| !s.is_empty()).unwrap_or(false); Self { exec: getb("NYASH_JIT_EXEC"), stats: getb("NYASH_JIT_STATS"), stats_json: getb("NYASH_JIT_STATS_JSON"), - dump: getb("NYASH_JIT_DUMP"), + dump: dump_flag, threshold, phi_min: getb("NYASH_JIT_PHI_MIN"), hostcall: getb("NYASH_JIT_HOSTCALL"), diff --git a/src/jit/engine.rs b/src/jit/engine.rs index dd903459..0e82c6d1 100644 --- a/src/jit/engine.rs +++ b/src/jit/engine.rs @@ -89,6 +89,14 @@ impl JitEngine { } } } + // If lowering left any unsupported instructions, do not register a closure. + // This preserves VM semantics until coverage is complete for the function. + if lower.unsupported > 0 { + if std::env::var("NYASH_JIT_STATS").ok().as_deref() == Some("1") || cfg_now.dump { + eprintln!("[JIT] skip compile for {}: unsupported={} (>0)", func_name, lower.unsupported); + } + return None; + } // Create a handle and register an executable closure if available #[cfg(feature = "cranelift-jit")] { diff --git a/src/jit/events.rs b/src/jit/events.rs index 0b5c98f8..f420dcd9 100644 --- a/src/jit/events.rs +++ b/src/jit/events.rs @@ -6,11 +6,20 @@ use serde::Serialize; -fn should_emit() -> bool { +fn base_emit_enabled() -> bool { std::env::var("NYASH_JIT_EVENTS").ok().as_deref() == Some("1") || std::env::var("NYASH_JIT_EVENTS_PATH").is_ok() } +fn should_emit_lower() -> bool { + // Compile-phase events are opt-in to avoid noisy logs by default. + std::env::var("NYASH_JIT_EVENTS_COMPILE").ok().as_deref() == Some("1") +} + +fn should_emit_runtime() -> bool { + base_emit_enabled() || std::env::var("NYASH_JIT_EVENTS_RUNTIME").ok().as_deref() == Some("1") +} + 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| { @@ -35,8 +44,26 @@ struct Event<'a, T: Serialize> { } pub fn emit(kind: &str, function: &str, handle: Option, ms: Option, extra: T) { - if !should_emit() { return; } + if !base_emit_enabled() { return; } let ev = Event { kind, function, handle, ms, extra }; if let Ok(s) = serde_json::to_string(&ev) { write_line(&s); } } +fn emit_any(kind: &str, function: &str, handle: Option, ms: Option, extra: serde_json::Value) { + let ev = Event { kind, function, handle, ms, extra }; + if let Ok(s) = serde_json::to_string(&ev) { write_line(&s); } +} + +/// Emit an event during lowering (compile-time planning). Adds phase="lower". +pub fn emit_lower(mut extra: serde_json::Value, kind: &str, function: &str) { + if !should_emit_lower() { return; } + if let serde_json::Value::Object(ref mut map) = extra { map.insert("phase".into(), serde_json::Value::String("lower".into())); } + emit_any(kind, function, None, None, extra); +} + +/// Emit an event during runtime execution. Adds phase="execute". +pub fn emit_runtime(mut extra: serde_json::Value, kind: &str, function: &str) { + if !should_emit_runtime() { return; } + if let serde_json::Value::Object(ref mut map) = extra { map.insert("phase".into(), serde_json::Value::String("execute".into())); } + emit_any(kind, function, None, None, extra); +} diff --git a/src/jit/extern/collections.rs b/src/jit/extern/collections.rs index 3d6a5386..5a39d5d0 100644 --- a/src/jit/extern/collections.rs +++ b/src/jit/extern/collections.rs @@ -66,18 +66,18 @@ pub fn array_set(args: &[VMValue]) -> VMValue { if crate::jit::policy::current().read_only && !crate::jit::policy::current().hostcall_whitelist.iter().any(|s| s == SYM_ARRAY_SET) { - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": SYM_ARRAY_SET, "decision":"fallback", "reason":"policy_denied_mutating"}) + crate::jit::events::emit_runtime( + serde_json::json!({"id": SYM_ARRAY_SET, "decision":"fallback", "reason":"policy_denied_mutating"}), + "hostcall", "" ); return VMValue::Integer(0); } if let (Some(arr), Some(VMValue::Integer(idx)), Some(value)) = (as_array(args), args.get(1), args.get(2)) { let val_box: Box = value.to_nyash_box(); let res = arr.set(Box::new(IntegerBox::new(*idx)), val_box); - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": SYM_ARRAY_SET, "decision":"allow", "argc":3, "arg_types":["Handle","I64","Handle"]}) + crate::jit::events::emit_runtime( + serde_json::json!({"id": SYM_ARRAY_SET, "decision":"allow", "argc":3, "arg_types":["Handle","I64","Handle"]}), + "hostcall", "" ); return VMValue::from_nyash_box(res); } @@ -88,18 +88,18 @@ pub fn array_push(args: &[VMValue]) -> VMValue { if crate::jit::policy::current().read_only && !crate::jit::policy::current().hostcall_whitelist.iter().any(|s| s == SYM_ARRAY_PUSH) { - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": SYM_ARRAY_PUSH, "decision":"fallback", "reason":"policy_denied_mutating"}) + crate::jit::events::emit_runtime( + serde_json::json!({"id": SYM_ARRAY_PUSH, "decision":"fallback", "reason":"policy_denied_mutating"}), + "hostcall", "" ); return VMValue::Integer(0); } if let (Some(arr), Some(value)) = (as_array(args), args.get(1)) { let val_box: Box = value.to_nyash_box(); let res = arr.push(val_box); - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": SYM_ARRAY_PUSH, "decision":"allow", "argc":2, "arg_types":["Handle","Handle"]}) + crate::jit::events::emit_runtime( + serde_json::json!({"id": SYM_ARRAY_PUSH, "decision":"allow", "argc":2, "arg_types":["Handle","Handle"]}), + "hostcall", "" ); return VMValue::from_nyash_box(res); } @@ -118,9 +118,9 @@ pub fn map_set(args: &[VMValue]) -> VMValue { if crate::jit::policy::current().read_only && !crate::jit::policy::current().hostcall_whitelist.iter().any(|s| s == SYM_MAP_SET) { - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": SYM_MAP_SET, "decision":"fallback", "reason":"policy_denied_mutating"}) + crate::jit::events::emit_runtime( + serde_json::json!({"id": SYM_MAP_SET, "decision":"fallback", "reason":"policy_denied_mutating"}), + "hostcall", "" ); return VMValue::Integer(0); } @@ -128,9 +128,9 @@ pub fn map_set(args: &[VMValue]) -> VMValue { let key_box: Box = key.to_nyash_box(); let val_box: Box = value.to_nyash_box(); let out = map.set(key_box, val_box); - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": SYM_MAP_SET, "decision":"allow", "argc":3, "arg_types":["Handle","Handle","Handle"]}) + crate::jit::events::emit_runtime( + serde_json::json!({"id": SYM_MAP_SET, "decision":"allow", "argc":3, "arg_types":["Handle","Handle","Handle"]}), + "hostcall", "" ); return VMValue::from_nyash_box(out); } diff --git a/src/jit/hostcall_registry.rs b/src/jit/hostcall_registry.rs index 492376b1..45f98a2b 100644 --- a/src/jit/hostcall_registry.rs +++ b/src/jit/hostcall_registry.rs @@ -23,9 +23,14 @@ fn ensure_default() { let mut r = Registry::default(); // Read-only defaults for s in [ - "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" + "nyash.array.len_h", + "nyash.any.length_h", + "nyash.any.is_empty_h", + "nyash.map.size_h", + "nyash.map.get_h", + "nyash.map.has_h", + "nyash.string.charCodeAt_h", + "nyash.array.get_h", ] { r.ro.insert(s.to_string()); } // Mutating defaults for s in [ @@ -45,6 +50,13 @@ fn ensure_default() { r.sig.entry("nyash.map.size_h".to_string()).or_default().push(Signature { args: vec![ArgKind::Handle], ret: ArgKind::I64 }); r.sig.entry("nyash.array.get_h".to_string()).or_default().push(Signature { args: vec![ArgKind::Handle, ArgKind::I64], ret: ArgKind::Handle }); r.sig.entry("nyash.array.len_h".to_string()).or_default().push(Signature { args: vec![ArgKind::Handle], ret: ArgKind::I64 }); + // String helpers + r.sig.entry("nyash.string.charCodeAt_h".to_string()).or_default().push(Signature { args: vec![ArgKind::Handle, ArgKind::I64], ret: ArgKind::I64 }); + // Any helpers (length/is_empty) + r.sig.entry("nyash.any.length_h".to_string()).or_default().push(Signature { args: vec![ArgKind::Handle], ret: ArgKind::I64 }); + r.sig.entry("nyash.any.is_empty_h".to_string()).or_default().push(Signature { args: vec![ArgKind::Handle], ret: ArgKind::I64 }); + // Map.has(handle, i64) -> i64(0/1) + r.sig.entry("nyash.map.has_h".to_string()).or_default().push(Signature { args: vec![ArgKind::Handle, ArgKind::I64], ret: ArgKind::I64 }); let _ = REG.set(RwLock::new(r)); } diff --git a/src/jit/lower/builder.rs b/src/jit/lower/builder.rs index f9fbe2c2..a5201fe6 100644 --- a/src/jit/lower/builder.rs +++ b/src/jit/lower/builder.rs @@ -138,15 +138,15 @@ use cranelift_codegen::ir::InstBuilder; #[cfg(feature = "cranelift-jit")] extern "C" fn nyash_host_stub0() -> i64 { 0 } #[cfg(feature = "cranelift-jit")] -extern "C" fn nyash_math_sin_f64(x: f64) -> f64 { x.sin() } +use super::extern_thunks::{ nyash_math_sin_f64, nyash_math_cos_f64, nyash_math_abs_f64, nyash_math_min_f64, nyash_math_max_f64, }; #[cfg(feature = "cranelift-jit")] -extern "C" fn nyash_math_cos_f64(x: f64) -> f64 { x.cos() } + #[cfg(feature = "cranelift-jit")] -extern "C" fn nyash_math_abs_f64(x: f64) -> f64 { x.abs() } + #[cfg(feature = "cranelift-jit")] -extern "C" fn nyash_math_min_f64(a: f64, b: f64) -> f64 { a.min(b) } + #[cfg(feature = "cranelift-jit")] -extern "C" fn nyash_math_max_f64(a: f64, b: f64) -> f64 { a.max(b) } + #[cfg(feature = "cranelift-jit")] extern "C" fn nyash_array_len(arr_param_index: i64) -> i64 { // Interpret first arg as function param index and fetch from thread-local args @@ -234,258 +234,18 @@ 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 { - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ARRAY_LEN_H, "decision":"allow", "argc":1, "arg_types":["Handle"]}) - ); - 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 with whitelist - use crate::jit::hostcall_registry::{classify, HostcallKind}; - let sym = crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H; - let pol = crate::jit::policy::current(); - let wh = pol.hostcall_whitelist; - match (classify(sym), pol.read_only && !wh.iter().any(|s| s == sym)) { - (HostcallKind::Mutating, true) => { - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": sym, "decision":"fallback", "reason":"policy_denied_mutating"}) - ); - 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", "argc":2, "arg_types":["Handle","I64"]}) - ); - return 0; - } - } - 0 -} #[cfg(feature = "cranelift-jit")] -extern "C" fn nyash_array_get_h(handle: u64, idx: i64) -> i64 { - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ARRAY_GET_H, "decision":"allow", "argc":2, "arg_types":["Handle","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; - let pol = crate::jit::policy::current(); - let wh = pol.hostcall_whitelist; - if classify(sym) == HostcallKind::Mutating && pol.read_only && !wh.iter().any(|s| s == sym) { - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": sym, "decision":"fallback", "reason":"policy_denied_mutating"}) - ); - 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", "argc":3, "arg_types":["Handle","I64","I64"]}) - ); - 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", "argc":1, "arg_types":["Handle"]}) - ); - 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", "argc":2, "arg_types":["Handle","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); - if let Some(ib) = val.as_any().downcast_ref::() { return ib.value; } - } - } - 0 -} #[cfg(feature = "cranelift-jit")] -extern "C" fn nyash_map_get_hh(map_h: u64, key_h: u64) -> i64 { - // Emit allow event for visibility - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_GET_HH, "decision":"allow", "argc":2, "arg_types":["Handle","Handle"]}) - ); - let map_arc = crate::jit::rt::handles::get(map_h); - let key_arc = crate::jit::rt::handles::get(key_h); - if let (Some(mobj), Some(kobj)) = (map_arc, key_arc) { - if let Some(map) = mobj.as_any().downcast_ref::() { - let key_box: Box = kobj.share_box(); - let val = map.get(key_box); - // Register result into handle table and return handle id - let arc: std::sync::Arc = std::sync::Arc::from(val); - let h = crate::jit::rt::handles::to_handle(arc); - return h as i64; - } - } - 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; - let pol = crate::jit::policy::current(); - let wh = pol.hostcall_whitelist; - if classify(sym) == HostcallKind::Mutating && pol.read_only && !wh.iter().any(|s| s == sym) { - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": sym, "decision":"fallback", "reason":"policy_denied_mutating"}) - ); - 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", "argc":3, "arg_types":["Handle","I64","I64"]}) - ); - return 0; - } - } - 0 -} #[cfg(feature = "cranelift-jit")] -extern "C" fn nyash_map_has_h(handle: u64, key: i64) -> i64 { - crate::jit::events::emit( - "hostcall", "", None, None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_HAS_H, "decision":"allow", "argc":2, "arg_types":["Handle","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", "argc":1, "arg_types":["Handle"]}) - ); - 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", "argc":1, "arg_types":["Handle"]}) - ); - 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", "argc":2, "arg_types":["Handle","I64"]}) - ); - 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 { diff --git a/src/jit/lower/core.rs b/src/jit/lower/core.rs index ff40d1b1..321df242 100644 --- a/src/jit/lower/core.rs +++ b/src/jit/lower/core.rs @@ -642,16 +642,16 @@ impl LowerCore { match method.as_str() { "len" | "length" => { if let Some(pidx) = self.param_index.get(array).copied() { - crate::jit::events::emit( - "hostcall","",None,None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_LEN_H, "decision":"allow", "reason":"sig_ok", "argc":1, "arg_types":["Handle"]}) + crate::jit::events::emit_lower( + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_LEN_H, "decision":"allow", "reason":"sig_ok", "argc":1, "arg_types":["Handle"]}), + "hostcall","" ); b.emit_param_i64(pidx); b.emit_host_call(crate::jit::r#extern::collections::SYM_ANY_LEN_H, 1, dst.is_some()); } else { - crate::jit::events::emit( - "hostcall","",None,None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_LEN_H, "decision":"fallback", "reason":"receiver_not_param", "argc":1, "arg_types":["Handle"]}) + crate::jit::events::emit_lower( + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_LEN_H, "decision":"fallback", "reason":"receiver_not_param", "argc":1, "arg_types":["Handle"]}), + "hostcall","" ); let arr_idx = -1; b.emit_const_i64(arr_idx); @@ -693,19 +693,16 @@ impl LowerCore { match check_signature(&sym, &observed_kinds) { Ok(()) => { // allow: record decision; execution remains on VM for now (thin bridge) - crate::jit::events::emit( - "hostcall", - "", - None, - None, - serde_json::json!({ - "id": sym, - "decision": "allow", - "reason": "sig_ok", - "argc": observed.len(), - "arg_types": arg_types - }) - ); + crate::jit::events::emit_lower( + serde_json::json!({ + "id": sym, + "decision": "allow", + "reason": "sig_ok", + "argc": observed.len(), + "arg_types": arg_types + }), + "hostcall","" + ); // If native f64 is enabled, emit a typed hostcall to math extern if crate::jit::config::current().native_f64 { let (symbol, arity) = match method.as_str() { @@ -771,10 +768,10 @@ impl LowerCore { // returns i64 0/1 b.emit_host_call(crate::jit::r#extern::collections::SYM_ANY_IS_EMPTY_H, 1, dst.is_some()); } else { - crate::jit::events::emit( - "hostcall","",None,None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_IS_EMPTY_H, "decision":"fallback", "reason":"receiver_not_param", "argc":1, "arg_types":["Handle"]}) - ); + crate::jit::events::emit_lower( + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_IS_EMPTY_H, "decision":"fallback", "reason":"receiver_not_param", "argc":1, "arg_types":["Handle"]}), + "hostcall","" + ); } } "push" => { @@ -785,23 +782,23 @@ impl LowerCore { let wh = &pol.hostcall_whitelist; let sym = crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H; let allowed = !pol.read_only || wh.iter().any(|s| s == sym); - crate::jit::events::emit( - "hostcall","",None,None, + crate::jit::events::emit_lower( serde_json::json!({ "id": sym, "decision": if allowed {"allow"} else {"fallback"}, "reason": if allowed {"sig_ok"} else {"policy_denied_mutating"}, "argc": 2, "arg_types": ["Handle","I64"] - }) + }), + "hostcall","" ); b.emit_param_i64(pidx); b.emit_const_i64(val); b.emit_host_call(sym, 2, false); } else { - crate::jit::events::emit( - "hostcall","",None,None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H, "decision":"fallback", "reason":"receiver_not_param", "argc":2, "arg_types":["Handle","I64"]}) + crate::jit::events::emit_lower( + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H, "decision":"fallback", "reason":"receiver_not_param", "argc":2, "arg_types":["Handle","I64"]}), + "hostcall","" ); let arr_idx = -1; b.emit_const_i64(arr_idx); @@ -812,16 +809,16 @@ impl LowerCore { "size" => { // MapBox.size(): argc=1 (map_handle) if let Some(pidx) = self.param_index.get(array).copied() { - crate::jit::events::emit( - "hostcall","",None,None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SIZE_H, "decision":"allow", "reason":"sig_ok", "argc":1, "arg_types":["Handle"]}) + crate::jit::events::emit_lower( + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SIZE_H, "decision":"allow", "reason":"sig_ok", "argc":1, "arg_types":["Handle"]}), + "hostcall","" ); b.emit_param_i64(pidx); b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_SIZE_H, 1, dst.is_some()); } else { - crate::jit::events::emit( - "hostcall","",None,None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SIZE_H, "decision":"fallback", "reason":"receiver_not_param", "argc":1, "arg_types":["Handle"]}) + crate::jit::events::emit_lower( + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SIZE_H, "decision":"fallback", "reason":"receiver_not_param", "argc":1, "arg_types":["Handle"]}), + "hostcall","" ); let map_idx = -1; b.emit_const_i64(map_idx); @@ -870,18 +867,15 @@ impl LowerCore { crate::jit::r#extern::collections::SYM_MAP_GET_H }; // Emit allow event - crate::jit::events::emit( - "hostcall", - "", - None, - None, + crate::jit::events::emit_lower( serde_json::json!({ "id": event_id, "decision": "allow", "reason": "sig_ok", "argc": observed_kinds.len(), "arg_types": arg_types - }) + }), + "hostcall","" ); // If key is i64, emit hostcall; if key is Handle and also a param, emit HH variant; otherwise fallback if matches!(key_kind, crate::jit::hostcall_registry::ArgKind::I64) { @@ -901,18 +895,15 @@ impl LowerCore { } Err(reason) => { // Signature mismatch - log and fallback - crate::jit::events::emit( - "hostcall", - "", - None, - None, + crate::jit::events::emit_lower( serde_json::json!({ "id": canonical, "decision": "fallback", "reason": reason, "argc": observed_kinds.len(), "arg_types": arg_types - }) + }), + "hostcall","" ); // No emission; VM path will handle } @@ -937,18 +928,15 @@ impl LowerCore { let arg_types: Vec<&'static str> = observed_kinds.iter().map(|k| match k { crate::jit::hostcall_registry::ArgKind::I64 => "I64", crate::jit::hostcall_registry::ArgKind::F64 => "F64", crate::jit::hostcall_registry::ArgKind::Handle => "Handle" }).collect(); let sym = "nyash.map.get_h"; let decision = match crate::jit::hostcall_registry::check_signature(sym, &observed_kinds) { Ok(()) => ("fallback", "receiver_not_param"), Err(reason) => ("fallback", reason) }; - crate::jit::events::emit( - "hostcall", - "", - None, - None, + crate::jit::events::emit_lower( serde_json::json!({ "id": sym, "decision": decision.0, "reason": decision.1, "argc": observed_kinds.len(), "arg_types": arg_types - }) + }), + "hostcall","" ); // no-op: VM側が処理する } @@ -962,24 +950,24 @@ impl LowerCore { let wh = &pol.hostcall_whitelist; let sym = crate::jit::r#extern::collections::SYM_MAP_SET_H; let allowed = !pol.read_only || wh.iter().any(|s| s == sym); - crate::jit::events::emit( - "hostcall","",None,None, + crate::jit::events::emit_lower( serde_json::json!({ "id": sym, "decision": if allowed {"allow"} else {"fallback"}, "reason": if allowed {"sig_ok"} else {"policy_denied_mutating"}, "argc": 3, "arg_types": ["Handle","I64","I64"] - }) + }), + "hostcall","" ); b.emit_param_i64(pidx); b.emit_const_i64(key); b.emit_const_i64(val); b.emit_host_call(sym, 3, false); } else { - crate::jit::events::emit( - "hostcall","",None,None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SET_H, "decision":"fallback", "reason":"receiver_not_param", "argc":3, "arg_types":["Handle","I64","I64"]}) + crate::jit::events::emit_lower( + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SET_H, "decision":"fallback", "reason":"receiver_not_param", "argc":3, "arg_types":["Handle","I64","I64"]}), + "hostcall","" ); } } @@ -987,17 +975,17 @@ impl LowerCore { // 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); - crate::jit::events::emit( - "hostcall","",None,None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_STRING_CHARCODE_AT_H, "decision":"allow", "reason":"sig_ok", "argc":2, "arg_types":["Handle","I64"]}) + crate::jit::events::emit_lower( + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_STRING_CHARCODE_AT_H, "decision":"allow", "reason":"sig_ok", "argc":2, "arg_types":["Handle","I64"]}), + "hostcall","" ); 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()); } else { - crate::jit::events::emit( - "hostcall","",None,None, - serde_json::json!({"id": crate::jit::r#extern::collections::SYM_STRING_CHARCODE_AT_H, "decision":"fallback", "reason":"receiver_not_param", "argc":2, "arg_types":["Handle","I64"]}) + crate::jit::events::emit_lower( + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_STRING_CHARCODE_AT_H, "decision":"fallback", "reason":"receiver_not_param", "argc":2, "arg_types":["Handle","I64"]}), + "hostcall","" ); } } diff --git a/src/jit/lower/extern_thunks.rs b/src/jit/lower/extern_thunks.rs new file mode 100644 index 00000000..ae7a7823 --- /dev/null +++ b/src/jit/lower/extern_thunks.rs @@ -0,0 +1,225 @@ +//! Handle-based extern thunks used by the JIT runtime path. +//! Moved out of builder.rs to keep files small and responsibilities clear. + +#[cfg(feature = "cranelift-jit")] +use crate::jit::events; + +#[cfg(feature = "cranelift-jit")] +use crate::jit::r#extern::collections as c; + +// ---- Math (native f64) ---- +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_math_sin_f64(x: f64) -> f64 { x.sin() } +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_math_cos_f64(x: f64) -> f64 { x.cos() } +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_math_abs_f64(x: f64) -> f64 { x.abs() } +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_math_min_f64(a: f64, b: f64) -> f64 { a.min(b) } +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_math_max_f64(a: f64, b: f64) -> f64 { a.max(b) } + +// ---- Array (handle) ---- +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_array_len_h(handle: u64) -> i64 { + events::emit_runtime(serde_json::json!({"id": c::SYM_ARRAY_LEN_H, "decision":"allow", "argc":1, "arg_types":["Handle"]}), "hostcall", ""); + 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")] +pub(super) extern "C" fn nyash_array_get_h(handle: u64, idx: i64) -> i64 { + events::emit_runtime(serde_json::json!({"id": c::SYM_ARRAY_GET_H, "decision":"allow", "argc":2, "arg_types":["Handle","I64"]}), "hostcall", ""); + 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")] +pub(super) 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::() { + 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")] +pub(super) extern "C" fn nyash_array_set_h(handle: u64, idx: i64, val: i64) -> i64 { + use crate::jit::hostcall_registry::{classify, HostcallKind}; + let sym = c::SYM_ARRAY_SET_H; + let pol = crate::jit::policy::current(); + let wh = pol.hostcall_whitelist; + if classify(sym) == HostcallKind::Mutating && pol.read_only && !wh.iter().any(|s| s == sym) { + events::emit_runtime(serde_json::json!({"id": sym, "decision":"fallback", "reason":"policy_denied_mutating"}), "hostcall", ""); + 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))); + events::emit_runtime(serde_json::json!({"id": sym, "decision":"allow", "argc":3, "arg_types":["Handle","I64","I64"]}), "hostcall", ""); + return 0; + } + } + 0 +} + +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_array_push_h(handle: u64, val: i64) -> i64 { + use crate::jit::hostcall_registry::{classify, HostcallKind}; + let sym = c::SYM_ARRAY_PUSH_H; + let pol = crate::jit::policy::current(); + let wh = pol.hostcall_whitelist; + if classify(sym) == HostcallKind::Mutating && pol.read_only && !wh.iter().any(|s| s == sym) { + events::emit_runtime(serde_json::json!({"id": sym, "decision":"fallback", "reason":"policy_denied_mutating"}), "hostcall", ""); + 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)); + events::emit_runtime(serde_json::json!({"id": sym, "decision":"allow", "argc":2, "arg_types":["Handle","I64"]}), "hostcall", ""); + return 0; + } + } + 0 +} + +// ---- Map (handle) ---- +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_map_size_h(handle: u64) -> i64 { + events::emit_runtime(serde_json::json!({"id": c::SYM_MAP_SIZE_H, "decision":"allow", "argc":1, "arg_types":["Handle"]}), "hostcall", ""); + 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")] +pub(super) extern "C" fn nyash_map_get_h(handle: u64, key: i64) -> i64 { + events::emit_runtime(serde_json::json!({"id": c::SYM_MAP_GET_H, "decision":"allow", "argc":2, "arg_types":["Handle","I64"]}), "hostcall", ""); + 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")] +pub(super) extern "C" fn nyash_map_get_hh(map_h: u64, key_h: u64) -> i64 { + events::emit_runtime(serde_json::json!({"id": c::SYM_MAP_GET_HH, "decision":"allow", "argc":2, "arg_types":["Handle","Handle"]}), "hostcall", ""); + let map_arc = crate::jit::rt::handles::get(map_h); + let key_arc = crate::jit::rt::handles::get(key_h); + if let (Some(mobj), Some(kobj)) = (map_arc, key_arc) { + if let Some(map) = mobj.as_any().downcast_ref::() { + let key_box: Box = kobj.share_box(); + let val = map.get(key_box); + let arc: std::sync::Arc = std::sync::Arc::from(val); + let h = crate::jit::rt::handles::to_handle(arc); + return h as i64; + } + } + 0 +} + +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_map_set_h(handle: u64, key: i64, val: i64) -> i64 { + use crate::jit::hostcall_registry::{classify, HostcallKind}; + let sym = c::SYM_MAP_SET_H; + let pol = crate::jit::policy::current(); + let wh = pol.hostcall_whitelist; + if classify(sym) == HostcallKind::Mutating && pol.read_only && !wh.iter().any(|s| s == sym) { + events::emit_runtime(serde_json::json!({"id": sym, "decision":"fallback", "reason":"policy_denied_mutating"}), "hostcall", ""); + 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); + events::emit_runtime(serde_json::json!({"id": sym, "decision":"allow", "argc":3, "arg_types":["Handle","I64","I64"]}), "hostcall", ""); + return 0; + } + } + 0 +} + +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_map_has_h(handle: u64, key: i64) -> i64 { + events::emit_runtime(serde_json::json!({"id": c::SYM_MAP_HAS_H, "decision":"allow", "argc":2, "arg_types":["Handle","I64"]}), "hostcall", ""); + 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); + let is_present = !val.as_any().is::(); + return if is_present { 1 } else { 0 }; + } + } + 0 +} + +// ---- Any helpers ---- +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_any_length_h(handle: u64) -> i64 { + events::emit_runtime(serde_json::json!({"id": c::SYM_ANY_LEN_H, "decision":"allow", "argc":1, "arg_types":["Handle"]}), "hostcall", ""); + 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; } + } + if let Some(sb) = obj.as_any().downcast_ref::() { + return sb.value.len() as i64; + } + } + 0 +} + +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_any_is_empty_h(handle: u64) -> i64 { + events::emit_runtime(serde_json::json!({"id": c::SYM_ANY_IS_EMPTY_H, "decision":"allow", "argc":1, "arg_types":["Handle"]}), "hostcall", ""); + if let Some(obj) = crate::jit::rt::handles::get(handle) { + if let Some(arr) = obj.as_any().downcast_ref::() { + if let Ok(items) = arr.items.read() { return if items.is_empty() { 1 } else { 0 }; } + } + if let Some(sb) = obj.as_any().downcast_ref::() { + return if sb.value.is_empty() { 1 } else { 0 }; + } + 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 +} + +// ---- String ---- +#[cfg(feature = "cranelift-jit")] +pub(super) extern "C" fn nyash_string_charcode_at_h(handle: u64, idx: i64) -> i64 { + events::emit_runtime(serde_json::json!({"id": c::SYM_STRING_CHARCODE_AT_H, "decision":"allow", "argc":2, "arg_types":["Handle","I64"]}), "hostcall", ""); + 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 s.as_bytes()[i] as i64; } else { return -1; } + } + } + -1 +} + diff --git a/src/jit/lower/mod.rs b/src/jit/lower/mod.rs index a51ef9ee..44eefad5 100644 --- a/src/jit/lower/mod.rs +++ b/src/jit/lower/mod.rs @@ -1,3 +1,4 @@ //! Lowering entry for JIT pub mod core; pub mod builder; +pub mod extern_thunks; diff --git a/src/jit/manager.rs b/src/jit/manager.rs index 8b2f2e15..1432a37c 100644 --- a/src/jit/manager.rs +++ b/src/jit/manager.rs @@ -22,6 +22,8 @@ impl JitManager { 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 set_threshold(&mut self, t: u32) { self.threshold = t.max(1); } + pub fn record_entry(&mut self, func: &str) { let c = self.hits.entry(func.to_string()).or_insert(0); *c = c.saturating_add(1); @@ -155,7 +157,20 @@ impl JitManager { let vmv = crate::jit::boundary::CallBoundaryBox::to_vm(ret_ty, v); Some(vmv) } - None => { self.exec_trap = self.exec_trap.saturating_add(1); None } + None => { + self.exec_trap = self.exec_trap.saturating_add(1); + // Emit a minimal trap event for observability (runtime only) + let dt = t0.elapsed(); + crate::jit::events::emit_runtime( + serde_json::json!({ + "kind": "trap", // redundant with wrapper kind but explicit here for clarity + "reason": "jit_execute_failed", + "ms": dt.as_millis() + }), + "trap", func + ); + None + } }; // Clear handles created during this call crate::jit::rt::handles::end_scope_clear(); diff --git a/src/runner.rs b/src/runner.rs index b0a50ec3..81191d52 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -66,6 +66,11 @@ impl NyashRunner { } // Optional: JIT controls via CLI flags (centralized) { + // CLI opt-in for JSONL events + if self.config.jit_events { std::env::set_var("NYASH_JIT_EVENTS", "1"); } + if self.config.jit_events_compile { std::env::set_var("NYASH_JIT_EVENTS_COMPILE", "1"); } + if self.config.jit_events_runtime { std::env::set_var("NYASH_JIT_EVENTS_RUNTIME", "1"); } + if let Some(ref p) = self.config.jit_events_path { std::env::set_var("NYASH_JIT_EVENTS_PATH", p); } let mut jc = nyash_rust::jit::config::JitConfig::from_env(); jc.exec |= self.config.jit_exec; jc.stats |= self.config.jit_stats; @@ -77,10 +82,11 @@ impl NyashRunner { 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 events are enabled and no threshold is provided, force threshold=1 so lowering runs and emits events - if std::env::var("NYASH_JIT_EVENTS").ok().as_deref() == Some("1") && jc.threshold.is_none() { - jc.threshold = Some(1); - } + // If observability is enabled and no threshold is provided, force threshold=1 so lowering runs and emits events + let events_on = std::env::var("NYASH_JIT_EVENTS").ok().as_deref() == Some("1") + || std::env::var("NYASH_JIT_EVENTS_COMPILE").ok().as_deref() == Some("1") + || std::env::var("NYASH_JIT_EVENTS_RUNTIME").ok().as_deref() == Some("1"); + if events_on && jc.threshold.is_none() { jc.threshold = Some(1); } 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(); diff --git a/tools/smoke_phase_10_10.sh b/tools/smoke_phase_10_10.sh new file mode 100644 index 00000000..d4761b60 --- /dev/null +++ b/tools/smoke_phase_10_10.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Phase 10.10 Smoke Test - Minimal verification for key features +# Usage: ./tools/smoke_phase_10_10.sh + +set -e + +echo "=== Phase 10.10 Smoke Test ===" +echo + +# 1. Build with Cranelift +echo "1) Building with Cranelift JIT..." +cargo build --release -j32 --features cranelift-jit 2>&1 | head -5 +echo "✓ Build complete" +echo + +# 2. HH direct execution (Map.get_hh) +echo "2) Testing HH direct execution..." +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_map_get_param_hh.nyash 2>&1 | head -10 | grep -E "(allow id:|value1|Result:)" +echo "✓ HH execution verified" +echo + +# 3. Mutating opt-in (JitPolicyBox) +echo "3) Testing mutating opt-in policy..." +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_policy_optin_mutating.nyash 2>&1 | head -15 | grep -E "(policy_denied_mutating|allow id:|Result:)" +echo "✓ Policy opt-in verified" +echo + +# 4. CountingGC demo +echo "4) Testing CountingGC..." +./target/release/nyash --backend vm examples/gc_counting_demo.nyash 2>&1 | head -10 | grep -E "(GC stats:|allocations:|write barriers:)" +echo "✓ CountingGC verified" +echo + +echo "=== All smoke tests passed! ===" \ No newline at end of file