From 510f4cf523894b3b201b32a2e1b9ab4fd8ccddb8 Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Sun, 28 Sep 2025 12:19:49 +0900 Subject: [PATCH] builder/vm: stabilize json_lint_vm under unified calls - Fix condition_fn resolution: Value call path + dev safety + stub injection - VM bridge: handle Method::birth via BoxCall; ArrayBox push/get/length/set direct bridge - Receiver safety: pin receiver in method_call_handlers to avoid undefined use across blocks - Local vars: materialize on declaration (use init ValueId; void for uninit) - Prefer legacy BoxCall for Array/Map/String/user boxes in emit_box_or_plugin_call (stability-first) - Test runner: update LLVM hint to llvmlite harness (remove LLVM_SYS_180_PREFIX guidance) - Docs/roadmap: update CURRENT_TASK with unified default-ON + guards Note: NYASH_DEV_BIRTH_INJECT_BUILTINS=1 can re-enable builtin birth() injection during migration. --- AGENTS.md | 12 +- CLAUDE.md | 65 +++- CURRENT_TASK.md | 228 ++++++++++- Cargo.toml | 2 + Makefile | 14 +- README.ja.md | 73 +++- README.md | 68 +++- apps/lib/json_native/README.md | 180 +-------- apps/lib/json_native/parser/parser.nyash | 9 +- apps/selfhost/vm/README.md | 14 + apps/selfhost/vm/boxes/mir_vm_m2.nyash | 112 ++++++ apps/selfhost/vm/boxes/mir_vm_min.nyash | 238 +++++++++++- apps/selfhost/vm/boxes/vm_kernel_box.nyash | 32 ++ apps/selfhost/vm/mir_min_entry.nyash | 6 +- docs/abi/vm-kernel.md | 47 +++ .../roadmap/phases/phase-15.7/README.md | 90 +++++ docs/how-to/self-hosting.md | 6 + docs/reference/language/quick-reference.md | 76 ++++ src/backend/mir_interpreter/handlers/calls.rs | 49 ++- src/backend/mir_interpreter/method_router.rs | 10 +- src/boxes/p2p_box.rs | 57 +-- src/llvm_py/instructions/mir_call.py | 2 +- src/mir/builder.rs | 148 ++++--- src/mir/builder/builder_calls.rs | 194 +++++++--- src/mir/builder/calls/call_unified.rs | 5 +- src/mir/builder/exprs_call.rs | 2 +- src/mir/builder/lifecycle.rs | 25 +- src/mir/builder/method_call_handlers.rs | 365 +----------------- src/mir/builder/observe/README.md | 23 ++ src/mir/builder/observe/mod.rs | 8 + src/mir/builder/observe/resolve.rs | 55 +++ src/mir/builder/observe/ssa.rs | 43 +++ src/mir/builder/origin/README.md | 26 ++ src/mir/builder/origin/infer.rs | 25 ++ src/mir/builder/origin/mod.rs | 13 + src/mir/builder/origin/phi.rs | 38 ++ src/mir/builder/rewrite/README.md | 33 ++ src/mir/builder/rewrite/known.rs | 227 +++++++++++ src/mir/builder/rewrite/mod.rs | 10 + src/mir/builder/rewrite/special.rs | 217 +++++++++++ src/mir/builder/stmts.rs | 19 +- src/mir/builder/utils.rs | 36 +- src/runner/cli_directives.rs | 128 +++++- src/runner/mir_json_emit.rs | 10 +- src/runner/modes/bench.rs | 7 +- src/runner/modes/common.rs | 13 + src/runner/modes/common_util/exec.rs | 6 +- src/tests/functionbox_call_tests.rs | 3 +- src/tests/refcell_assignment_test.rs | 2 + src/tests/vm_bitops_test.rs | 2 + tools/dev/check_builder_layers.sh | 44 +++ tools/dev_env.sh | 18 +- tools/llvmlite_harness.py | 13 +- tools/smokes/v2/configs/rust_vm_dynamic.conf | 3 +- tools/smokes/v2/lib/result_checker.sh | 4 +- tools/smokes/v2/lib/test_runner.sh | 47 ++- .../quick/core/json_error_messages_ast.sh | 4 + .../v2/profiles/quick/core/json_nested_ast.sh | 4 + .../profiles/quick/core/json_roundtrip_ast.sh | 4 + .../quick/core/json_unterminated_string_vm.sh | 51 +++ .../quick/core/lang_quickref_asi_error_vm.sh | 29 ++ .../core/lang_quickref_equals_box_error_vm.sh | 23 ++ .../core/lang_quickref_plus_mixed_error_vm.sh | 22 ++ .../quick/core/lang_quickref_truthiness_vm.sh | 30 ++ .../quick/core/parity_m2_binop_add_vm_llvm.sh | 38 ++ .../quick/core/parity_m2_const_ret_vm_llvm.sh | 38 ++ .../quick/core/selfhost_mir_binop_vm.sh | 36 ++ .../quick/core/selfhost_mir_m2_eq_false_vm.sh | 44 +++ .../quick/core/selfhost_mir_m2_eq_true_vm.sh | 44 +++ .../core/selfhost_mir_m3_branch_true_vm.sh | 41 ++ .../quick/core/selfhost_mir_m3_jump_vm.sh | 43 +++ .../quick/core/using_multi_prelude_dep_ast.sh | 3 + .../profiles/quick/core/using_profiles_ast.sh | 3 + tools/smokes/v2/run.sh | 12 +- 74 files changed, 2846 insertions(+), 825 deletions(-) create mode 100644 apps/selfhost/vm/README.md create mode 100644 apps/selfhost/vm/boxes/mir_vm_m2.nyash create mode 100644 apps/selfhost/vm/boxes/vm_kernel_box.nyash create mode 100644 docs/abi/vm-kernel.md create mode 100644 docs/development/roadmap/phases/phase-15.7/README.md create mode 100644 docs/reference/language/quick-reference.md create mode 100644 src/mir/builder/observe/README.md create mode 100644 src/mir/builder/observe/mod.rs create mode 100644 src/mir/builder/observe/resolve.rs create mode 100644 src/mir/builder/observe/ssa.rs create mode 100644 src/mir/builder/origin/README.md create mode 100644 src/mir/builder/origin/infer.rs create mode 100644 src/mir/builder/origin/mod.rs create mode 100644 src/mir/builder/origin/phi.rs create mode 100644 src/mir/builder/rewrite/README.md create mode 100644 src/mir/builder/rewrite/known.rs create mode 100644 src/mir/builder/rewrite/mod.rs create mode 100644 src/mir/builder/rewrite/special.rs create mode 100644 tools/dev/check_builder_layers.sh create mode 100644 tools/smokes/v2/profiles/quick/core/json_unterminated_string_vm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/lang_quickref_asi_error_vm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/lang_quickref_equals_box_error_vm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/lang_quickref_plus_mixed_error_vm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/lang_quickref_truthiness_vm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/parity_m2_binop_add_vm_llvm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/parity_m2_const_ret_vm_llvm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/selfhost_mir_binop_vm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/selfhost_mir_m2_eq_false_vm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/selfhost_mir_m2_eq_true_vm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/selfhost_mir_m3_branch_true_vm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/selfhost_mir_m3_jump_vm.sh diff --git a/AGENTS.md b/AGENTS.md index ec40db4e..c19fa391 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -305,8 +305,9 @@ Notes - VM と Cranelift(JIT) は MIR14 へ移行中のため、現在は実行経路として安定していないよ(検証・実装作業の都合で壊れている場合があるにゃ)。 - 当面の実行・配布は LLVM ラインを最優先・全力で整備する方針だよ。開発・確認は `--features llvm` を有効にして進めてね。 - 推奨チェック: - - `env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo build --release --features llvm -j 24` - - `env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo check --features llvm` + - LLVM は llvmlite ハーネス(Python)経由だよ。Rust inkwell は既定で不使用(legacy のみ)。 + - ビルド(ハーネス): `cargo build --release --features llvm -j 24` + - チェック: `cargo check --features llvm` ## Docs links(開発方針/スタイル) - Language statements (ASI): `docs/reference/language/statements.md` @@ -337,7 +338,7 @@ Notes - Build (JIT/VM): `cargo build --release --features cranelift-jit` - Build (LLVM AOT / harness-first): - `cargo build --release -p nyash-llvm-compiler` (ny-llvmc builder) - - `LLVM_SYS_180_PREFIX=$(llvm-config-18 --prefix) cargo build --release --features llvm` + - `cargo build --release --features llvm` - Run via harness: `NYASH_LLVM_USE_HARNESS=1 ./target/release/nyash --backend llvm apps/APP/main.nyash` - Quick VM run: `./target/release/nyash --backend vm apps/APP/main.nyash` - Emit + link (LLVM): `tools/build_llvm.sh apps/APP/main.nyash -o app` @@ -475,7 +476,8 @@ Flags - Rust tests: `cargo test` (add targeted unit tests near code). - Smoke scripts validate end‑to‑end AOT/JIT (`tools/llvm_smoke.sh`). - Test naming: prefer `*_test.rs` for Rust and descriptive `.nyash` files under `apps/` or `tests/`. -- For LLVM tests, ensure LLVM 18 is installed and set `LLVM_SYS_180_PREFIX`. +- For LLVM tests, ensure Python llvmlite is available and `ny-llvmc` is built. +- Build (harness): `cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm` ## Commit & Pull Request Guidelines - Commits: concise imperative subject; scope the change (e.g., "llvm: fix argc handling in nyrt"). @@ -503,4 +505,4 @@ Flags - `NYASH_CLI_VERBOSE=1` – extra trace from builder. - Smokes: - Empty PHI guard: `tools/test/smoke/llvm/ir_phi_empty_check.sh ` - - Batch run: `tools/test/smoke/llvm/ir_phi_empty_check_all.sh` \ No newline at end of file + - Batch run: `tools/test/smoke/llvm/ir_phi_empty_check_all.sh` diff --git a/CLAUDE.md b/CLAUDE.md index 53186cdc..fe948bfb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,33 @@ このファイルは最小限の入口だよ。詳細はREADMEから辿ってねにゃ😺 +--- + +## 🔄 **現在の開発状況** (2025-09-28) + +### 🎯 **Phase 15: セルフホスティング実行器統一化** +- **Rust VM + LLVM 2本柱体制**で開発中 +- **Core Box統一化**: 3-tier → 2-tier 統一完了 +- **MIR Callee型革新**: 型安全な関数解決システム実装済み + +### 🤝 **AI協働開発体制** +``` +Claude(私): 戦略・分析・レビュー +ChatGPT: 実装・検証 + +現在の合意: +✅ Phase 15集中(セルフホスト優先) +✅ Builder根治は段階的(3 Phase戦略) +✅ 息が合っている状態: 良好 +``` + +### 📚 **重要リソース** +- **開発マスタープラン**: [00_MASTER_ROADMAP.md](docs/development/roadmap/phases/00_MASTER_ROADMAP.md) +- **現在のタスク**: [CURRENT_TASK.md](CURRENT_TASK.md) +- **Phase 15詳細**: [docs/development/roadmap/phases/phase-15/](docs/development/roadmap/phases/phase-15/) + +--- + ## 🚨 重要:スモークテストはv2構造を使う! - 📖 **スモークテスト完全ガイド**: [tools/smokes/README.md](tools/smokes/README.md) - 📁 **v2詳細ドキュメント**: [tools/smokes/v2/README.md](tools/smokes/v2/README.md) @@ -16,9 +43,13 @@ cargo build --release # 一括スモークテスト tools/smokes/v2/run.sh --profile quick -# 個別スモークテスト +# 個別スモークテスト(フィルタ指定) tools/smokes/v2/run.sh --profile quick --filter "" -# 例: --filter "core/json_query_min_vm.sh" +# 例: --filter "userbox_*" # User Box関連のみ +# 例: --filter "json_*" # JSON関連のみ + +# 単発スクリプト実行 +bash tools/smokes/v2/profiles/quick/core/selfhost_mir_m3_jump_vm.sh # 単発実行(参考) ./target/release/nyash --backend vm apps/APP/main.nyash @@ -29,14 +60,17 @@ tools/smokes/v2/run.sh --profile quick --filter "" # 前提: Python3 + llvmlite # 未導入なら: pip install llvmlite -# ビルド(LLVM_SYS_180_PREFIX不要!) -cargo build --release --features llvm - -# 一括スモークテスト +# 一括スモークテスト(そのまま実行) tools/smokes/v2/run.sh --profile integration -# 個別スモークテスト +# 警告低減版(ビルド後に実行・推奨) +cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm +tools/smokes/v2/run.sh --profile integration + +# 個別スモークテスト(フィルタ指定) tools/smokes/v2/run.sh --profile integration --filter "" +# 例: --filter "json_*" # JSON関連のみ +# 例: --filter "vm_llvm_*" # VM/LLVM比較系のみ # 単発実行 NYASH_LLVM_USE_HARNESS=1 ./target/release/nyash --backend llvm apps/tests/peek_expr_block.nyash @@ -179,15 +213,15 @@ target/x86_64-pc-windows-msvc/release/nyash.exe ./target/release/nyash --backend llvm program.nyash # 実行時最適化 ``` -### 🎯 **2本柱ビルド方法** (2025-09-24更新) +### 🎯 **2本柱ビルド方法** (2025-09-28更新) #### 🔨 **標準ビルド**(推奨) ```bash # 標準ビルド(2本柱対応) cargo build --release -# LLVM機能付きビルド(本番用) -env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo build --release --features llvm +# LLVM(llvmliteハーネス)付きビルド(本番用) +cargo build --release --features llvm ``` #### 📝 **2本柱テスト実行** @@ -196,9 +230,9 @@ env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo build --release --features llvm cargo build --release ./target/release/nyash program.nyash -# 2. LLVM実行 ✅(本番・最適化用) -env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo build --release --features llvm -./target/release/nyash --backend llvm program.nyash +# 2. LLVM実行 ✅(本番・最適化用, llvmliteハーネス) +cargo build --release --features llvm +NYASH_LLVM_USE_HARNESS=1 ./target/release/nyash --backend llvm program.nyash # 3. プラグインテスト実証済み ✅ # CounterBox @@ -829,7 +863,9 @@ box MyBox { - Reference: [docs/reference/](docs/reference/) ### 🎯 リファレンス -- **言語**: [LANGUAGE_REFERENCE_2025.md](docs/reference/language/LANGUAGE_REFERENCE_2025.md) +- **言語**: + - [Quick Reference](docs/reference/language/quick-reference.md) ⭐最優先 - 1ページ実用ガイド + - [LANGUAGE_REFERENCE_2025.md](docs/reference/language/LANGUAGE_REFERENCE_2025.md) - 完全仕様 - **MIR**: [INSTRUCTION_SET.md](docs/reference/mir/INSTRUCTION_SET.md) - **API**: [boxes-system/](docs/reference/boxes-system/) - **プラグイン**: [plugin-system/](docs/reference/plugin-system/) @@ -847,6 +883,7 @@ box MyBox { ### 🎯 最重要ドキュメント(2つの核心) #### 🔤 言語仕様 +- **[クイックリファレンス](docs/reference/language/quick-reference.md)** ⭐最優先 - 1ページ実用ガイド(ASI・Truthiness・演算子・型ルール) - **[構文早見表](docs/quick-reference/syntax-cheatsheet.md)** - 基本構文・よくある間違い - **[完全リファレンス](docs/reference/language/LANGUAGE_REFERENCE_2025.md)** - 言語仕様詳細 diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 94c584cf..563cf5d7 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -4,13 +4,34 @@ Focus - Keep VM quick green; llvmlite integration on-demand. - Using SSOT(nyash.toml + 相対using)で安定解決。 - Builder/VM ガードは最小限・仕様不変(dev では診断のみ)。 +- Phase 15.7 を再定義: Known 化+Rewrite 統合(dev観測)と Mini‑VM 安定化、表示APIは `str()` に統一(互換:stringify)。 + +Update — 2025-09-28 (P1 Known 集約・KPI・LAYER ガード) +- Builder: method_call_handlers の Known 経路を `rewrite::known` に集約。 + - 新規 API: `try_known_or_unique`(Known 優先→一意候補 fallback)。 + - equals/1 を `rewrite::special::try_special_equals` に移設(挙動不変)。 +- Observe: `resolve.choose` に certainty を付加し(Known/Heuristic)、`NYASH_DEBUG_KPI_KNOWN=1` 時に簡易集計を出力(`NYASH_DEBUG_SAMPLE_EVERY=N`)。 +- LAYER ガード(任意ツール): `tools/dev/check_builder_layers.sh` を追加(origin→observe→rewrite の一方向チェック)。 +- Unified 経路: `emit_unified_call` に equals/1 の集約を追加(Known 優先→一意候補)(仕様不変)。 +- メソッド候補インデックス化: `MirBuilder` に tail→候補のキャッシュを追加(lazy再構築)。 + - API: `method_candidates(method, arity)`, `method_candidates_tail(tail)` + - 利用箇所: method_call_handlers の resolve.try、rewrite::{special,known} の一意候補探索、unified equals/1 の一意候補。 +- 集約ポリシー(P0 完了): + - 中央集約先: `emit_unified_call`(Methodターゲット時に rewrite/special/known を順に試行) + - `method_call_handlers` は `emit_unified_call` を呼ぶだけに簡素化(重複ロジック削減) + - equals/1 も同一ロジックに吸収 +- レガシー経路(P1 準備): + - dev ガード追加: `NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1` でレガシー側のメソッド関数化を停止(将来削除の前段階) + - Unified 無効時の後方互換は維持(既定OFF) Status Snapshot — 2025‑09‑27 - Completed - - VM method_router: special-method table extended minimally — equals/1 now tries instance class then base class when only base provides equals (deterministic, no behavior change where both exist). toString→stringify remains. + - VM method_router: special-method table extended minimally — equals/1 now tries instance class then base class when only base provides equals (deterministic, no behavior change where both exist). toString→str remains(互換: stringify を許容)。 - MIR Callee Phase‑3: added TypeCertainty to Callee::Method (Known/Union). Builder sets Known when receiver origin is known; legacy/migration BoxCall marks Union. JSON emitter and MIR printer include certainty for diagnostics. Backends ignore it functionally for now. - Using/SSOT: JSONモジュール内部 using を相対に統一(alias配下でも安定) - DebugHub: 追加ゲート `NYASH_DEBUG_SAMPLE_EVERY`(N件に1度だけ emit)。重いケースでのログ制御のため(既定OFF・ゼロコスト)。 + - Router diagnostics: class-reroute / special-reroute を DebugHub に emit(dev-only, 既定OFF)。 + - LLVM diagnostics: `NYASH_LLVM_TRACE_CALLS=1` で `mir_call` の callee(Method.certainty 含む)を JSON 出力(挙動不変)。 Decision — Variables (Option A; 2025‑09‑27) - 方針: var/let は導入しない。ローカルは常に `local` で明示宣言。 @@ -26,8 +47,7 @@ Decision — Variables (Option A; 2025‑09‑27) - 互換: `Main.main` が存在する場合は常にそちらを優先。両方無い場合は従来通りエラー。 - オプトアウト: `NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=0|false|off` で無効化可能。 - Next - - Scanner.read_string_literal: 未終端で null を返すよう修正(TokenizerがERRORを積む)+小プローブ追加 - - Heavy JSON: quick 既定ONへ再切替(環境が安定したら) + - Heavy JSON: quick 既定ONへ再切替(LLVM 常備で段階復帰) - 解析ログの統一: parser/tokenizerのdevトレースは既定OFFのまま維持、必要時だけ有効化 - llvmlite(integration): 任意ジョブで確認(単発実行のハングはタイムアウト/リンク分離で回避) @@ -70,19 +90,32 @@ Guards / Policy - 既定挙動は不変(prod 用心)。 - dev では診断強化(ログ/メトリクス)し、ランナー側でノイズはフィルタ。 +Policy — AST Using (Status Quo) +- SSOT(nyash.toml)+AST prelude merge を維持。prod は toml 限定、dev/ci は段階的に緩和。 +- 重い AST/JSON ケースは integration でカバーしつつ、quick への復帰は LLVM 有効環境で段階的に行う(順次解除)。 + Work Queue (Next) 1) Scanner: 未終端文字列で必ず null を返す(Tokenizer が ERROR へ) 2) Heavy JSON: quick 既定ONに戻す(プローブは維持) 3) エラーメッセージの詳細化(expected/actual/line/column) 4) Ny 実行器 M2 スケルトン(JSON v0 ローダ+const/binop 等の最小実装)下書き 5) Parity ミニセット(VM↔llvmlite↔Ny)を用意し、差分ダッシュボード化 - 6) Router 観測ログの軽追加(dev-only, 既定OFF): class-reroute / special-reroute を DebugHub に emit(サンプル制御対応) - 7) LLVM ハーネスの MIR ダンプに certainty 表示(挙動不変の診断整合) + 6) Router: Known/Union 方針の磨き込み(挙動不変) + - Known → 既存の直接呼び出しを維持(VM 完了、LLVM は表示のみ)。 + - Union → ルータ経路を維持しつつ、ログで可視化(表は“必要最小”で追加)。 + 7) Heavy JSON の quick 段階復帰(LLVM 有効環境) + - 順序: nested_ast → roundtrip_ast → error_messages_ast。 + 8) (診断)LLVM ダンプに certainty の補助表示(必要時、挙動不変)。 Update — @local expansion promotion (2025‑09‑27) - すべてのランナーモードに `preexpand_at_local` を適用(common/llvm/pyvm に加え vm/selfhost へも導入)。 - Docs を更新し、構文糖衣が標準で有効であることを明記。 +Plan — Router Minimalism (継続方針) +- 特殊メソッド表は “toString→str(互換:stringify), equals/1” の範囲から、ユースが発生したもののみ点で追加。 +- 既定の挙動・言語仕様は変更しない(フォールバックの拡大はしない)。 +- 測定: DebugHub(resolve.*)ログと LLVM の `NYASH_LLVM_TRACE_CALLS` を併用し、Union 経路を可視化。 + Runbook(抜粋) - VM quick: `tools/smokes/v2/run.sh --profile quick` - LLVM llvmlite: `cargo build --release --features llvm && tools/smokes/v2/run.sh --profile integration` @@ -169,7 +202,59 @@ Update — 2025-09-27 (json_lint_vm guard fix) - File: apps/lib/json_native/lexer/scanner.nyash (read_string_literal) - TODO: add unit probe; ensure EOF without closing quote yields null; add negative case to smokes if needed. +Update — 2025-09-28 (Scanner 未終端→null とスモーク追加) +- Implemented: JsonScanner.read_string_literal returns null when closing quote is missing or escape incomplete. + - File: apps/lib/json_native/lexer/scanner.nyash (already returned null; verified) +- Tokenizer maps scanner null to ERROR("Unterminated string literal"). + - File: apps/lib/json_native/lexer/tokenizer.nyash (tokenize_string) +- Added quick smoke to lock behavior: + - tools/smokes/v2/profiles/quick/core/json_unterminated_string_vm.sh → expects "Unterminated string literal". + +Work Queue — Reorganized (2025‑09‑28) +1) Scanner 未終端→null — completed + - Status: Verified with new smoke; tokenizer ERROR emitted with line/column preserved. +2) Heavy JSON quick 復帰(LLVM 常備で段階解除) — completed (dev override) + - Policy: AST-heavy smokes run in quick via LLVM harness. When LLVM is not detectable, they SKIP; 開発者は `SMOKES_FORCE_LLVM=1` で強制実行可。 + - Action: run.sh に `SMOKES_FORCE_LLVM=1` を追加、ハーネス/NYRT/ENV の自動整備を強化。nested_ast → roundtrip_ast → error_messages_ast が PASS。 +3) エラーメッセージ詳細化 — pending + - Scope: enrich JSON parser/tokenizer messages with expected/actual; keep format: "Error at line X, column Y: ...". +4) Ny 実行器 M2 スケルトン(最小) — baseline exists + - Files: apps/selfhost/vm/boxes/mir_vm_min.nyash; quick smoke present. + - Next: add binop/compare minimal paths (dev-only), no default behavior change. +5) Parity ミニセット — pending + - Add a tiny VM↔LLVM↔Ny parity triplet; start with const/ret and simple binop. +6) Router Known/Union 磨き込み(挙動不変) — pending + - Maintain minimal special-method table; diagnostics only; no behavior change. +7) Heavy JSON 段階復帰順(nested_ast→roundtrip_ast→error_messages_ast) — tracking + - All present in quick under LLVM harness; verify pass and keep order. +8) LLVM ダンプに certainty 補助表示 — baseline exists + - NYASH_LLVM_TRACE_CALLS=1 prints callee JSON including Method.certainty. +9) QuickRef — Truthiness(quickで有効化)— completed + - tools/smokes/v2/profiles/quick/core/lang_quickref_truthiness_vm.sh → enabled; PASS(0→false, 1→true, ""→false, non‑empty→true) +10) Language guards(planned; 既定OFF・段階導入) + - ASI strictness: dev‑only check to fail a line break after a binary operator; default OFF. + - Plus mixed: warn/fail‑fast when non‑String mixed `+` unless explicit stringify; default OFF; document String+number ⇒ concat. + - Box equality guidance: when `box == box` is used, emit guidance to use equals(); default OFF. + - Scope: docs + dev warnings first; later wire parser/builder flags guarded by env/CLI profile. + Update — 2025-09-27 (M2 skeleton: Ny mini-MIR VM) + +Update — 2025-09-28 (json_lint_vm regression fix — condition_fn and birth bridge) +- Fixed: Unknown global function: condition_fn (quick json_lint_vm) + - Indirect calls: ensure AST `condition_fn(ch)` lowers to Value call (unified path already used in exprs_call.rs) + - Unified Global safety: emit_unified_call now dev‑safes `condition_fn` by returning const 1 when unresolved (explicit opt‑in legacy paths intact) + - Dev stub: finalize_module injects minimal `condition_fn/1 -> 1` if missing (kept as guard) +- Unified→VM bridge: birth() + - VM: when executing unified Method callee `*.birth`, delegate to BoxCall handler and return Void. This preserves legacy behavior for built‑ins when plugins are absent. + - Builder: gated birth() injection for built‑ins (Array/Map/String etc). Default OFF unless `NYASH_DEV_BIRTH_INJECT_BUILTINS=1`. +- Next (high‑prio): local var materialization bug in main.nyash + - Symptom: `local cases = new ArrayBox()` followed by `cases.push(...)` used an undefined receiver ValueId. + - Interim change: make `local` always materialize a distinct register and `copy init -> var` (also const Void for uninitialized). This avoids SSA aliasing issues. + - Status: needs a quick pass across smokes to confirm; proceed if quick green, otherwise revisit builder var mapping. + +Dev toggles +- NYASH_DEV_BIRTH_INJECT_BUILTINS=1: re‑enable birth() injection for builtin boxes (default OFF to stabilize unified Method path until full bridge lands). +- NYASH_MIR_UNIFIED_CALL: default ON; opt‑out via 0|false|off. - Added Ny-based minimal MIR(JSON v0) executor skeleton (const→ret only), dev-only app — no default behavior change. - File: apps/selfhost/vm/boxes/mir_vm_min.nyash - Entry: apps/selfhost/vm/mir_min_entry.nyash (optional thin wrapper) @@ -218,7 +303,7 @@ Update — 2025-09-27 (Quick profile stabilization & heavy JSON gating) - json_pp_vm (JsonNode.parse pretty-print) → SKIP in quick(例示アプリ、他で十分カバー) - Using resolver brace-fixer: quick config restored to ON for stability(NYASH_RESOLVE_FIX_BRACES=1) - ScopeCtx wired (loop/join) and resolve/ssa events include region_id(dev logs only) - - toString→stringify early mapping logs added(reason: toString-early-*) + - toString→str early mapping logs added(reason: toString-early-*) - Rationale: heavy/nested parser cases were sensitive to mixed env order in quick. Integration profile will carry the parity checks with DebugHub capture. - Next (focused): 1) Run integration smokes for JSON heavy with DebugHub ON and collect /tmp logs @@ -244,7 +329,7 @@ Acceptance (this phase): - userbox_branch_phi_vm.sh — SKIP (rewrite/materialize pending) - userbox_toString_mapping_vm.sh — SKIP (mapping pending) - Rationale: keep quick green while surfacing remaining gaps as SKIP with clear reasons. -- Next: stabilize rewrite/materialize across branch/arity and toString→stringify mapping; then flip SKIPs to PASS. +- Next: stabilize rewrite/materialize across branch/arity and toString→str mapping; then flip SKIPs to PASS. Update — 2025-09-27 (Loop‑Form Scope Debug & AOT PoC — Plan) - Added design doc: docs/design/loopform-scope-debug-and-aot.md - Scope model (LoopScope/JoinScope), invariants, Hub+Inspectors, per-scope data, AOT fold, PoC phases, acceptance. @@ -264,3 +349,132 @@ Update — 2025-09-27 (Loop‑Form Scope Debug & AOT PoC — Plan) - Acceptance (Phase‑1) - Debug JSONL has resolve/ssa events with region_id and choices; PASS cases unchanged (OFF) - SKIP cases pinpointable by log (branch/arity) → use logs to guide fixes → flip to PASS + + +Update — 2025-09-28 (Plugins 既定ON と ENV 整理) +- Plugins: 既定ONで統一。テストランナー/開発スクリプトから `NYASH_DISABLE_PLUGINS=1` を撤去。 + - tools/smokes/v2/lib/test_runner.sh(LLVM 経路): disable 指定を外し、`PYTHONPATH`/`NYASH_NY_LLVM_COMPILER`/`NYASH_EMIT_EXE_NYRT` を自動付与。 + - tools/dev_env.sh: `pyvm`/`bridge` プロファイルで plugins を無効化しない(unset のみに変更)。 +- VM/LLVM 二系統の最小ENV(ドキュメント方針): + - VM: 既定でOK(追加ENV不要) + - LLVM(harness): `NYASH_LLVM_USE_HARNESS=1` + `NYASH_NY_LLVM_COMPILER=$NYASH_ROOT/target/release/ny-llvmc` + `NYASH_EMIT_EXE_NYRT=$NYASH_ROOT/target/release` + - quick強制: `SMOKES_FORCE_LLVM=1` で AST heavy を quick で実行可能 + + +Priority TODO — 2025-09-28 (VM/LLVM 2-Line + M2) +- ENV minimalization (plugins=ON): + - VM: no extra ENV. + - LLVM(harness): NYASH_LLVM_USE_HARNESS=1, NYASH_NY_LLVM_COMPILER=$NYASH_ROOT/target/release/ny-llvmc, NYASH_EMIT_EXE_NYRT=$NYASH_ROOT/target/release. + - Docs: add a small "VM vs LLVM minimal-ENV" box to README.md and README.ja.md. [done] +- test_runner cleanup: + - Unify/centralize noise filters; keep SMOKES_FORCE_LLVM as the only dev override; remove ad-hoc greps in individual scripts. [todo] +- M2 executor (Ny): + - Add compare (Eq) to M2 runner; add 2 smokes (Eq true/false). [done] + - Externalize MirVmM2 to apps/selfhost/vm/boxes/mir_vm_m2.nyash and switch smoke to using-based variant; keep inline smoke as safety. [later] + - Next (optional): branch/jump minimal; phi later. [pending] + +Update — 2025-09-28 (Language Quick Reference & Smokes) +- Added quick-reference draft for language (keywords, operators, ASI, truthiness, equality, '+', rewrite, errors). + - docs/reference/language/quick-reference.md +- Added planned smokes for quickref rules (initially SKIP until strict rules are wired): + - tools/smokes/v2/profiles/quick/core/lang_quickref_asi_error_vm.sh (SKIP) + - tools/smokes/v2/profiles/quick/core/lang_quickref_truthiness_vm.sh (ENABLED) + - tools/smokes/v2/profiles/quick/core/lang_quickref_plus_mixed_error_vm.sh (SKIP) + - tools/smokes/v2/profiles/quick/core/lang_quickref_equals_box_error_vm.sh (SKIP) +- Temporarily SKIP Mini‑VM M2/M3 smokes while parser/segment boundaries are being fixed: + - selfhost_mir_m2_eq_true_vm.sh / selfhost_mir_m2_eq_false_vm.sh / selfhost_mir_m3_branch_true_vm.sh / selfhost_mir_m3_jump_vm.sh — now ENABLED and PASS +- Using/SSOT docs: + - Clarify dev/ci/prod matrix (file-using dev/ci only; prod=toml only); add short examples. [todo] +- Parity mini-set: + - VM ↔ LLVM ↔ Ny: const/ret + binop(+), compare(Eq); add quick parity harness notes. [todo] +- Acceptance: + - quick: AST heavy PASS (LLVM present), M2 binop/Eq PASS; integration unchanged. + - docs: minimal-ENV clearly shown; no NYASH_DISABLE_PLUGINS in public guidance. + +Update — 2025-09-28 (Interpreter gating & Phase 15.7 plan) +- Legacy AST interpreter is now feature-gated (interpreter-legacy OFF by default). Runner/tests that depend on it are behind cfg. + - Files: src/runner/modes/common.rs, src/runner/modes/bench.rs, src/tests/* (vm_bitops/refcell/functionbox) +- Added Phase 15.7 roadmap (Mini‑VM M3 + NYABI Kernel skeleton; dev-only; default OFF). + - docs/development/roadmap/phases/phase-15.7/README.md +- Drafted NYABI Kernel spec (v0) and added Ny skeleton box (not wired). + - docs/abi/vm-kernel.md; apps/selfhost/vm/boxes/vm_kernel_box.nyash + +Plan — Instance→Function Rewrite Consolidation (2025‑09‑28) +- Goal: 内部表現を関数呼び出しへ極力統一(obj.m(a) → Class.m/Arity(me,a))。prodでの Instance BoxCall 依存を排除。 +- Approach(小粒・可逆) + 1) PHI/Join での origin/type 伝播の強化(region_id ログで落ちる断面を特定→補修) + 2) 限定 materialize: module 内で name+arity がユニークな場合のみ Glue 関数を合成(既定OFF、dev/CIで計測) + +Roadmap Priorities (Phase 15.7 revised) +- P0: me 注入 Known 化(起源付与/維持)— リスク低・効果大。軽量PHI補強(単一/一致時) +- P1: Known 100% 関数化(Known 経路の instance→function 正規化、special 集約) +- P2: Policy(Ny Kernel, dev‑only)— equals/str/truthiness の観測API(バッチ、再入禁止/タイムアウト/計測) +- P3: 表示APIの移行誘導 — toString→str(互換:stringify)の警告/ドキュメント(仕様不変) +- P4: Union 観測・分析 — resolve.try/choose と ssa.phi(region_id)で継続観測 +- P5: PHI Known 維持の一般化 — Phase 16(複雑のため後回し) + 3) prod ガード維持: VM は user Instance BoxCall を禁止(既存ポリシー継続)。dev/CI は WARN+観測 + 4) スモーク/観測: quick で Instance BoxCall の dev WARN=0 を確認。resolve.try/choose と LLVM `NYASH_LLVM_TRACE_CALLS` を併用 +- Controls + - `NYASH_BUILDER_REWRITE_INSTANCE`(既定ON): 強制ON/OFF + - `NYASH_DEV_REWRITE_USERBOX`(dev限定): userbox rewrite 検証用 + - materialize 新ENV(既定OFF): `NYASH_BUILDER_MATERIALIZE_UNIQUE=1`(予定) +- Acceptance(段階) + - Stage‑1: Known 経路で 100% 関数化(quick全域で dev WARN=0) + - Stage‑2: 限定 materialize をON時に適用し、分岐/PHI 合流の代表ケースが関数化(差分はdevのみ) + - 常に prod は挙動不変・安全(OFFで現状維持) + +Update — 2025-09-28 (Mini‑VM M2/M3 fix + smokes) +- Fix: compare/ret segmentation made robust without heavy JSON parse. + - Approach: per‑block coarse passes for const/binop/compare and a precise in‑block ret search; control‑flow (branch/jump) handled with a single pass using computed regs. + - Files: apps/selfhost/vm/boxes/mir_vm_min.nyash +- Smokes: enabled and PASS + - tools/smokes/v2/profiles/quick/core/selfhost_mir_m2_eq_true_vm.sh + - tools/smokes/v2/profiles/quick/core/selfhost_mir_m2_eq_false_vm.sh + - tools/smokes/v2/profiles/quick/core/selfhost_mir_m3_branch_true_vm.sh + - tools/smokes/v2/profiles/quick/core/selfhost_mir_m3_jump_vm.sh +- Notes: kept changes local and spec‑neutral; no default behavior changes to core VM. + +Update — 2025-09-28 (QuickRef Dev Guards + Docs llvmlite) +- Dev guards (env‑gated; default OFF) implemented and validated by quick smokes: + - ASI strict line‑continuation: `NYASH_ASI_STRICT=1` → parse error when a binary operator ends the line. +- Plus mixed (String×Number): `NYASH_PLUS_MIX_ERROR=1` → type error; suggest str()/明示変換。 + - Box equality guidance: `NYASH_BOX_EQ_GUIDE_ERROR=1` → equals()誘導のエラー。 + - Smokes enabled: `lang_quickref_asi_error_vm.sh`, `lang_quickref_plus_mixed_error_vm.sh`, `lang_quickref_equals_box_error_vm.sh`(PASS) +- LLVM ドキュメント統一(llvmlite一本化) + - `LLVM_SYS_180_PREFIX` の記述を主要ドキュメントから撤去し、llvmlite/ny‑llvmc 前提に更新。 + - Files: `AGENTS.md`, `README.md`, `README.ja.md`, `CLAUDE.md` + +Plan — Next (2025-09-28) +1) Mini‑VM 単一パス化(仕様不変・安全化) — completed + - 各 op を JSON オブジェクト単位で厳密セグメント化し、一回走査で評価(coarse pass を除去)。 + - 代表ケース(複数op/ret先頭/ret末尾/compare v0,v1/jump/branch)で緑維持を確認。 +2) Rewrite 統合 Stage‑1(挙動不変・dev観測) — completed (observability wired) + - builder_calls の unified 経路に resolve.try/resolve.choose を追加(dev‑only/既定OFF)。 + - method_call_handlers の既存 emit と整合。Known/Union の certainty を choose に含める。 + - 使い方: `NYASH_MIR_UNIFIED_CALL=1 NYASH_DEBUG_ENABLE=1 NYASH_DEBUG_KINDS=resolve,ssa NYASH_DEBUG_SINK=/tmp/nyash_debug.jsonl`。 + - Known 経路の100%関数化(dev WARN=0)を DebugHub で観測。userbox スモークで検証。 +3) P0/P1 着手(構造化) — in progress + - origin/observe/rewrite の責務分割(モジュール新設: src/mir/builder/{origin,observe,rewrite}/)。 + - P0: me 注入 Known 化(起源付与/維持)と軽量PHI補強(単一/一致時)。 + - P1: Known 経路 100% 関数化(special 集約: toString→str(互換:stringify)/equals)。 + - Docs: README を各層に追加(origin/observe/rewrite)— completed + - 観測呼び出しの統一: builder_calls/method_call_handlers から observe::resolve を使用 — completed +3) CI/Profiles 整理 — ongoing + - quick: VM 主線(llvmlite パリティは integration に委譲)。 + - integration: 代表パリティ(llvmlite ハーネス)継続、apps系は任意実行。 + +Notes — Display API Unification (spec‑neutral) +- 規範: `str()` / `x.str()`(同義)。`toString()` は Builder で `str()` に早期正規化。 +- 互換: `stringify()` は当面エイリアス(内部で `str()` 相当)。 +- VM ルータ: toString/0 → str/0(なければ stringify/0)。 +- QuickRef/ガイド更新済み。`NYASH_PLUS_MIX_ERROR` の誘導文言も `str()` に統一。 + +追加メモ — これからやる(ユーザー合意、2025‑09‑28) +- Mini‑VM の単一パス化を安全に実装(既定挙動不変) + - 各 op を厳密セグメントで1回走査に統合(coarse を段階撤去) + - 代表スモーク(M2/M3/compare v0,v1)で緑維持確認 +- 続いて Rewrite 統合 Stage‑1 の観測へ進む(dev のみ、挙動不変) +- Dev Profiles + - tools/dev_env.sh に Unified 既定ON(明示OFFのみ無効)とレガシー関数化抑止を追加。 + - `NYASH_MIR_UNIFIED_CALL=1`(既定ON明示) + - `NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1`(重複回避; 段階移行) diff --git a/Cargo.toml b/Cargo.toml index 563307af..033a8277 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ mir_refbarrier_unify_poc = [] llvm-harness = [] llvm-inkwell-legacy = ["dep:inkwell"] llvm = ["llvm-harness"] +# Legacy AST interpreter (off by default). Gates NyashInterpreter usage in runner/tests. +interpreter-legacy = [] # (removed) Optional modular MIR builder feature # cranelift-jit = [ # ARCHIVED: Moved to archive/jit-cranelift/ during Phase 15 # "dep:cranelift-codegen", diff --git a/Makefile b/Makefile index 46c0d4b4..d6fa9fe5 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ # Nyash selfhosting-dev quick targets -.PHONY: build build-release run-minimal smoke-core smoke-selfhost bootstrap roundtrip clean quick fmt lint dep-tree +.PHONY: build build-release run-minimal smoke-core smoke-selfhost bootstrap roundtrip clean quick fmt lint dep-tree \ + smoke-quick smoke-quick-filter smoke-integration build: cargo build --features cranelift-jit @@ -28,6 +29,17 @@ clean: quick: build-release smoke-selfhost +# --- v2 smokes shortcuts --- +smoke-quick: + bash tools/smokes/v2/run.sh --profile quick + +# Usage: make smoke-quick-filter FILTER="json_*" +smoke-quick-filter: + bash tools/smokes/v2/run.sh --profile quick --filter "$(FILTER)" + +smoke-integration: + bash tools/smokes/v2/run.sh --profile integration + fmt: cargo fmt --all diff --git a/README.ja.md b/README.ja.md index c2a450a9..8f88c19c 100644 --- a/README.ja.md +++ b/README.ja.md @@ -19,6 +19,23 @@ AST JSON v0(マクロ/ブリッジ): `docs/reference/ir/ast-json-v0.md` セルフホスト1枚ガイド: `docs/how-to/self-hosting.md` ExternCall(env.*)と println 正規化: `docs/reference/runtime/externcall.md` +### MIR 統一Call(既定ON) +- 呼び出しは中央(`emit_unified_call`)で集約。開発段階では既定ON(`0|false|off` で明示OFF)。 + - 早期正規化: `toString/stringify → str` + - `equals/1`: Known 優先 → 一意候補(ユーザーBoxのみ) + - Known→関数化: `obj.m(a) → Class.m(me,obj,a)` に統一 +- レガシー関数化の重複を避ける開発ガード: + - `NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1` +- JSON出力は unified ON で v1、OFF で v0(従来) + +開発計測(任意) +- `resolve.choose` の Known 率をKPIとして出力 + - `NYASH_DEBUG_KPI_KNOWN=1`(有効化) + - `NYASH_DEBUG_SAMPLE_EVERY=`(N件ごとに出力) + +レイヤー・ガード(origin→observe→rewrite の一方向) +- スクリプト: `tools/dev/check_builder_layers.sh` + 開発ショートカット(Operator Boxes + JSON) - JSON最小(Roundtrip/Nested を一発): `./tools/opbox-json.sh` - quick 全体(軽量プリフライト+timeout 180s): `./tools/opbox-quick.sh` @@ -59,7 +76,7 @@ Phase‑15(2025‑09)アップデート ## 🧪 Self-Hosting(自己ホスト開発) - ガイド: `docs/how-to/self-hosting.md` -- 最小E2E: `NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend vm apps/selfhost-minimal/main.nyash` +- 最小E2E: `./target/release/nyash --backend vm apps/selfhost-minimal/main.nyash` - スモーク: `bash tools/jit_smoke.sh` / `bash tools/selfhost_vm_smoke.sh` - Makefile: `make run-minimal`, `make smoke-selfhost` @@ -135,15 +152,14 @@ local py = new PyRuntimeBox() // Pythonプラグイン ## 🏗️ **複数の実行モード** -重要: 現在、JIT ランタイム実行は封印中です。実行は「PyVM(既定)/VM(任意でレガシー有効)」、配布は「Cranelift AOT(EXE)/LLVM AOT(EXE)」の4体制です。 +重要: 現在、JIT ランタイム実行は封印中です。実行は「Rust VM(MIR)/ PyVM(開発補助)」、配布は「LLVM AOT(ハーネス)」が主軸です。ASTインタープリタはレガシー扱いでデフォルト無効(`interpreter-legacy` feature)。 -Phase‑15(自己ホスト期): VM/インタープリタはフィーチャーで切替 -- 既定ビルド: `--backend vm` は PyVM 実行(python3 + `tools/pyvm_runner.py` が必要) -- レガシー Rust VM/インタープリターを有効化するには: +Phase‑15(自己ホスト期): ASTインタープリタは任意featureで明示ON +- 既定ビルド: `--backend vm` は PyVM 経路(python3 + `tools/pyvm_runner.py` が必要)/Rust VM(MIR) +- レガシー AST インタープリタを有効化するには(通常は不要): ```bash - cargo build --release --features vm-legacy,interpreter-legacy + cargo build --release --features interpreter-legacy ``` - 以降、`--backend vm`/`--backend interpreter` が従来経路で動作します。 ### 1. **インタープリターモード** (開発用) ```bash @@ -178,13 +194,23 @@ cargo build --release --features cranelift-jit - 最高性能 - 簡単配布 -### 4. **ネイティブバイナリ(LLVM AOT)** +### 4. **ネイティブバイナリ(LLVM AOT, llvmliteハーネス)** ```bash -LLVM_SYS_180_PREFIX=$(llvm-config-18 --prefix) \ - cargo build --release --features llvm -NYASH_LLVM_OBJ_OUT=$PWD/nyash_llvm_temp.o \ - ./target/release/nyash --backend llvm program.nyash -# リンクして実行 +# ハーネス+CLI をビルド(LLVM_SYS_180_PREFIX不要) +cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm + +# ハーネス経由で EXE を生成して実行 +NYASH_LLVM_USE_HARNESS=1 \ +NYASH_NY_LLVM_COMPILER=target/release/ny-llvmc \ +NYASH_EMIT_EXE_NYRT=target/release \ + ./target/release/nyash --backend llvm --emit-exe myapp program.nyash +./myapp + +# あるいは .o を出力して手動リンク +NYASH_LLVM_USE_HARNESS=1 \ +NYASH_NY_LLVM_COMPILER=target/release/ny-llvmc \ + ./target/release/nyash --backend llvm program.nyash \ + -D NYASH_LLVM_OBJ_OUT=$PWD/nyash_llvm_temp.o cc nyash_llvm_temp.o -L crates/nyrt/target/release -Wl,--whole-archive -lnyrt -Wl,--no-whole-archive -lpthread -ldl -lm -o myapp ./myapp ``` @@ -195,6 +221,7 @@ tools/smoke_aot_vs_vm.sh examples/aot_min_string_len.nyash ``` ### LLVM バックエンドの補足 +- Python llvmlite を使用します。Python3 + llvmlite の用意と `ny-llvmc` のビルド(`cargo build -p nyash-llvm-compiler`)が必要です。`LLVM_SYS_180_PREFIX` は不要です。 - `NYASH_LLVM_OBJ_OUT`: `--backend llvm` 実行時に `.o` を出力するパス。 - 例: `NYASH_LLVM_OBJ_OUT=$PWD/nyash_llvm_temp.o ./target/release/nyash --backend llvm apps/ny-llvm-smoke/main.nyash` - 削除された `NYASH_LLVM_ALLOW_BY_NAME=1`: すべてのプラグイン呼び出しがmethod_idベースに統一。 @@ -239,6 +266,26 @@ smoke_obj_array = "NYASH_LLVM_OBJ_OUT={root}/nyash_llvm_temp.o ./target/release/ - `{root}` は現在のプロジェクトルートに展開されます。 - 現状は最小機能(OS別/依存/並列は未対応)。 +### ちいさなENVまとめ(VM vs LLVM ハーネス) +- VM 実行: 追加ENVなしでOK。 + - 例: `./target/release/nyash --backend vm apps/tests/ternary_basic.nyash` +- LLVM ハーネス実行: 下記3つだけ設定してね。 + - `NYASH_LLVM_USE_HARNESS=1` + - `NYASH_NY_LLVM_COMPILER=$NYASH_ROOT/target/release/ny-llvmc` + - `NYASH_EMIT_EXE_NYRT=$NYASH_ROOT/target/release` + - 例: `NYASH_LLVM_USE_HARNESS=1 NYASH_NY_LLVM_COMPILER=target/release/ny-llvmc NYASH_EMIT_EXE_NYRT=target/release ./target/release/nyash --backend llvm apps/ny-llvm-smoke/main.nyash` + +### DebugHub かんたんガイド +- 有効化: `NYASH_DEBUG_ENABLE=1` +- 種別指定: `NYASH_DEBUG_KINDS=resolve,ssa` +- 出力先: `NYASH_DEBUG_SINK=/tmp/nyash_debug.jsonl` +- 例: `NYASH_DEBUG_ENABLE=1 NYASH_DEBUG_KINDS=resolve,ssa NYASH_DEBUG_SINK=/tmp/nyash.jsonl tools/smokes/v2/run.sh --profile quick --filter "userbox_*"` + +### 開発用セーフティ(VM) +- stringify(Void) は "null" を返す(JSONフレンドリ/開発セーフティ。既定挙動は不変)。 +- JsonScanner のデフォルト値(`NYASH_VM_SCANNER_DEFAULTS=1` 時のみ): `is_eof/current/advance` 内に限定し、数値/テキストの不足を安全に埋める。 +- VoidBox に対する length/size/get/push 等は、ガード下で中立なノーオペとして扱い、開発中のハードストップを回避。 + --- ## 🧰 一発ビルド(MVP): `nyash --build` diff --git a/README.md b/README.md index 7278b553..8765c4d2 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Execution Status (Feature Additions Pause) - `--backend vm` (PyVM harness) - Inactive/Sealed - `--backend cranelift`, `--jit-direct` (sealed; use LLVM harness) - - Rust VM (legacy opt‑in via features) + - AST interpreter (legacy) is gated by feature `interpreter-legacy` and excluded from default builds (Rust VM + LLVM are the two main lines) Quick pointers - Emit object with harness: set `NYASH_LLVM_USE_HARNESS=1` and `NYASH_LLVM_OBJ_OUT=` (defaults in tools use `tmp/`). @@ -55,6 +55,46 @@ MIR mode note: Default PHI behavior Self‑hosting one‑pager: `docs/how-to/self-hosting.md`. ExternCall (env.*) and println normalization: `docs/reference/runtime/externcall.md`. +### Minimal ENV (VM vs LLVM harness) +- VM: no extra environment needed for typical runs. + - Example: `./target/release/nyash --backend vm apps/tests/ternary_basic.nyash` +- LLVM harness: set three variables so the runner finds the harness and runtime. + - `NYASH_LLVM_USE_HARNESS=1` + - `NYASH_NY_LLVM_COMPILER=$NYASH_ROOT/target/release/ny-llvmc` + - `NYASH_EMIT_EXE_NYRT=$NYASH_ROOT/target/release` + - Example: `NYASH_LLVM_USE_HARNESS=1 NYASH_NY_LLVM_COMPILER=target/release/ny-llvmc NYASH_EMIT_EXE_NYRT=target/release ./target/release/nyash --backend llvm apps/ny-llvm-smoke/main.nyash` + +### DebugHub Quick Guide +- Enable: `NYASH_DEBUG_ENABLE=1` +- Select kinds: `NYASH_DEBUG_KINDS=resolve,ssa` +- Output file: `NYASH_DEBUG_SINK=/tmp/nyash_debug.jsonl` +- Use with smokes: `NYASH_DEBUG_ENABLE=1 NYASH_DEBUG_KINDS=resolve,ssa NYASH_DEBUG_SINK=/tmp/nyash.jsonl tools/smokes/v2/run.sh --profile quick --filter "userbox_*"` + +### MIR Unified Call(default ON) +- Centralized call emission is enabled by default in development builds. + - Env toggle: `NYASH_MIR_UNIFIED_CALL` — default ON unless set to `0|false|off`. + - Instance method calls are normalized in one place (`emit_unified_call`): + - Early mapping: `toString/stringify → str` + - `equals/1`: Known first → unique-suffix fallback (user boxes only) + - Known→function rewrite: `obj.m(a) → Class.m(me,obj,a)` +- Disable legacy rewrite path (dev-only) to avoid duplicate behavior during migration: + - `NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1` +- JSON emit follows unified format (v1) when unified is ON; legacy v0 otherwise. + +Dev metrics (opt-in) +- Known-rate KPI for `resolve.choose`: + - `NYASH_DEBUG_KPI_KNOWN=1` (enable) + - `NYASH_DEBUG_SAMPLE_EVERY=` (print every N events) + +Layer guard (one-way deps: origin→observe→rewrite) +- Check script: `tools/dev/check_builder_layers.sh` +- Ensures builder layering hygiene during refactors. + +### Dev Safety Guards (VM) +- stringify(Void) → "null" for JSON-friendly printing (dev safety; prod behavior unchanged). +- JsonScanner defaults (guarded by `NYASH_VM_SCANNER_DEFAULTS=1`) for missing numeric/text fields inside `is_eof/current/advance` contexts only. +- VoidBox common methods (length/size/get/push) are neutral no-ops in guarded paths to avoid dev-time hard stops. + Profiles (quick) - `--profile dev` → Macros ON (strict), PyVM dev向け設定を適用(必要に応じて環境で上書き可) - `--profile lite` → Macros OFF の軽量実行 @@ -76,7 +116,7 @@ Specs & Constraints ## 🧪 Self‑Hosting (Dev Focus) - Guide: `docs/how-to/self-hosting.md` -- Minimal E2E: `NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend vm apps/selfhost-minimal/main.nyash` +- Minimal E2E: `./target/release/nyash --backend vm apps/selfhost-minimal/main.nyash` - Smokes: `bash tools/jit_smoke.sh` / `bash tools/selfhost_vm_smoke.sh` - JSON (Operator Boxes, dev): `./tools/opbox-json.sh` / `./tools/opbox-quick.sh` - Makefile: `make run-minimal`, `make smoke-selfhost` @@ -200,13 +240,23 @@ cargo build --release --features cranelift-jit - Maximum performance - Easy distribution -### 4. **Native Binary (LLVM AOT)** +### 4. **Native Binary (LLVM AOT, llvmlite harness)** ```bash -LLVM_SYS_180_PREFIX=$(llvm-config-18 --prefix) \ - cargo build --release --features llvm -NYASH_LLVM_OBJ_OUT=$PWD/nyash_llvm_temp.o \ - ./target/release/nyash --backend llvm program.nyash -# Link and run +# Build harness + CLI (no LLVM_SYS_180_PREFIX needed) +cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm + +# Emit and run native executable via harness +NYASH_LLVM_USE_HARNESS=1 \ +NYASH_NY_LLVM_COMPILER=target/release/ny-llvmc \ +NYASH_EMIT_EXE_NYRT=target/release \ + ./target/release/nyash --backend llvm --emit-exe myapp program.nyash +./myapp + +# Alternatively, emit an object file then link manually +NYASH_LLVM_USE_HARNESS=1 \ +NYASH_NY_LLVM_COMPILER=target/release/ny-llvmc \ + ./target/release/nyash --backend llvm program.nyash \ + -D NYASH_LLVM_OBJ_OUT=$PWD/nyash_llvm_temp.o cc nyash_llvm_temp.o -L crates/nyrt/target/release -Wl,--whole-archive -lnyrt -Wl,--no-whole-archive -lpthread -ldl -lm -o myapp ./myapp ``` @@ -254,7 +304,7 @@ Key options (minimal) - `--target ` (only when needed) Notes -- LLVM AOT requires LLVM 18 (`LLVM_SYS_180_PREFIX`). +- LLVM AOT uses Python llvmlite harness. Ensure Python3 + llvmlite and `ny-llvmc` are available (built via `cargo build -p nyash-llvm-compiler`). No `LLVM_SYS_180_PREFIX` required. - Apps that open a GUI may show a window during AOT emission; close it to continue. - On WSL if the window doesn’t show, see `docs/guides/cranelift_aot_egui_hello.md` (Wayland→X11). diff --git a/apps/lib/json_native/README.md b/apps/lib/json_native/README.md index 6141bde1..51e0a12b 100644 --- a/apps/lib/json_native/README.md +++ b/apps/lib/json_native/README.md @@ -1,172 +1,14 @@ -# Nyash JSON Native +Layer Guard — json_native -> yyjson(C依存)→ 完全Nyash実装で外部依存完全排除 +Scope and responsibility +- This layer implements a minimal native JSON library in Ny. +- Responsibilities: scanning, tokenizing, and parsing JSON; building node structures. +- Forbidden: runtime/VM specifics, code generation, non‑JSON language concerns. -## 🎯 プロジェクト目標 +Imports policy (SSOT) +- Dev/CI: file-using allowed for development convenience. +- Prod: use only `nyash.toml` using entries (no ad‑hoc file imports). -- **C依存ゼロ**: yyjsonからの完全脱却 -- **Everything is Box**: 全てをNyash Boxで実装 -- **80/20ルール**: 動作優先、最適化は後 -- **段階切り替え**: `NYASH_JSON_PROVIDER=nyash|yyjson|serde` - -## 📦 アーキテクチャ設計 - -### 🔍 yyjson分析結果 -```c -// yyjson核心設計パターン -struct yyjson_val { - uint64_t tag; // 型+サブタイプ+長さ - yyjson_val_uni uni; // ペイロード(union) -}; -``` - -### 🎨 Nyash版設計 (Everything is Box) -```nyash -// 🌟 JSON値を表現するBox -box JsonNode { - kind: StringBox // "null"|"bool"|"int"|"string"|"array"|"object" - value: Box // 実際の値(種類に応じて) - children: ArrayBox // 配列・オブジェクト用(オプション) - keys: ArrayBox // オブジェクトのキー配列(オプション) -} - -// 🔧 JSON字句解析器 -box JsonLexer { - text: StringBox // 入力文字列 - pos: IntegerBox // 現在位置 - tokens: ArrayBox // トークン配列 -} - -// 🏗️ JSON構文解析器 -box JsonParser { - lexer: JsonLexer // 字句解析器 - current: IntegerBox // 現在のトークン位置 -} -``` - -## 📂 ファイル構造 - -``` -apps/lib/json_native/ -├── README.md # この設計ドキュメント -├── lexer.nyash # JSON字句解析器(状態機械ベース) -├── parser.nyash # JSON構文解析器(再帰下降) -├── node.nyash # JsonNode実装(Everything is Box) -├── tests/ # テストケース -│ ├── lexer_test.nyash -│ ├── parser_test.nyash -│ └── integration_test.nyash -└── examples/ # 使用例 - ├── simple_parse.nyash - └── complex_object.nyash -``` - -## 🎯 実装戦略 - -### Phase 1: JsonNode基盤(1週目) -- [x] JsonNode Box実装 -- [x] 基本的な値型サポート(null, bool, int, string) -- [x] 配列・オブジェクト構造サポート - -### Phase 2: JsonLexer実装(2週目) -- [ ] トークナイザー実装(状態機械ベース) -- [ ] エラーハンドリング -- [ ] 位置情報追跡 - -### Phase 3: JsonParser実装(3週目) -- [ ] 再帰下降パーサー実装 -- [ ] JsonNode構築 -- [ ] ネストされた構造サポート - -### Phase 4: 統合&最適化(4週目) -- [ ] C ABI Bridge(最小実装) -- [ ] 既存JSONBoxとの互換性 -- [ ] 性能測定・最適化 - -## 🔄 既存システムとの統合 - -### 段階的移行戦略 -```bash -# 環境変数で切り替え -export NYASH_JSON_PROVIDER=nyash # 新実装 -export NYASH_JSON_PROVIDER=yyjson # 既存C実装 -export NYASH_JSON_PROVIDER=serde # Rust実装 -``` - -### 既存APIとの互換性 -```nyash -// 既存JSONBox APIを維持 -local json = new JSONBox() -json.parse("{\"key\": \"value\"}") // 内部でNyash実装を使用 -``` - -## 🎨 設計思想 - -### Everything is Box原則 -- **JsonNode**: 完全なBox実装 -- **境界明確化**: 各コンポーネントをBox化 -- **差し替え可能**: プロバイダー切り替え対応 - -### 80/20ルール適用 -- **80%**: まず動く実装(文字列操作ベース) -- **20%**: 後で最適化(バイナリ処理、SIMD等) - -### フォールバック戦略 -- **エラー時**: 既存実装に安全復帰 -- **性能不足時**: yyjsonに切り替え可能 -- **互換性**: 既存APIを100%維持 - -## 🧪 テスト戦略 - -### 基本テストケース -```json -{"null": null} -{"bool": true} -{"int": 42} -{"string": "hello"} -{"array": [1,2,3]} -{"object": {"nested": "value"}} -``` - -### エラーケース -``` -{invalid} // 不正なJSON -{"unclosed": "str // 閉じられていない文字列 -[1,2, // 不完全な配列 -``` - -### 性能テストケース -``` -大きなJSONファイル(10MB+) -深くネストされた構造(100レベル+) -多数の小さなオブジェクト(10万個+) -``` - -## 🚀 実行例 - -```nyash -// 基本的な使用例 -using "apps/lib/json_native/node.nyash" as JsonNative - -// JSON文字列をパース -local text = "{\"name\": \"Nyash\", \"version\": 1}" -local node = JsonNative.parse(text) - -// 値にアクセス -print(node.get("name").str()) // "Nyash" -print(node.get("version").int()) // 1 - -// JSON文字列に戻す -print(node.stringify()) // {"name":"Nyash","version":1} -``` - -## 📊 進捗追跡 - -- [ ] Week 1: JsonNode基盤 -- [ ] Week 2: JsonLexer実装 -- [ ] Week 3: JsonParser実装 -- [ ] Week 4: 統合&最適化 - -**開始日**: 2025-09-22 -**目標完了**: 2025-10-20 -**実装者**: Claude × User協働 +Notes +- Error messages aim to include: “Error at line X, column Y: …”. +- Unterminated string → tokenizer emits "Unterminated string literal" (locked by quick smoke). diff --git a/apps/lib/json_native/parser/parser.nyash b/apps/lib/json_native/parser/parser.nyash index 673ab6c1..ce4104e4 100644 --- a/apps/lib/json_native/parser/parser.nyash +++ b/apps/lib/json_native/parser/parser.nyash @@ -59,9 +59,14 @@ box JsonParser { // Step 2: 構文解析 local result = me.parse_value() - // Step 3: 余剰トークンチェック + // Step 3: 余剰トークンチェック(詳細情報付き) if result != null and not me.is_at_end() { - me.add_error("Unexpected tokens after JSON value") + local extra = me.current_token() + if extra != null { + me.add_error("Unexpected tokens after JSON value: " + extra.get_type() + "(" + extra.get_value() + ")") + } else { + me.add_error("Unexpected tokens after JSON value") + } return null } diff --git a/apps/selfhost/vm/README.md b/apps/selfhost/vm/README.md new file mode 100644 index 00000000..d2a1ec3a --- /dev/null +++ b/apps/selfhost/vm/README.md @@ -0,0 +1,14 @@ +Layer Guard — selfhost/vm + +Scope and responsibility +- Minimal Ny-based executors and helpers for self‑hosting experiments. +- Responsibilities: trial executors (MIR JSON v0), tiny helpers (scan/binop/compare), smoke drivers. +- Forbidden: full parser implementation, heavy runtime logic, code generation. + +Imports policy (SSOT) +- Dev/CI: file-using allowed; drivers may embed JSON for tiny smokes. +- Prod: prefer `nyash.toml` mapping under `[modules.selfhost.*]`. + +Notes +- MirVmMin covers: const/binop/compare/ret (M2). Branch/jump/phi are later. +- Keep changes minimal and spec‑neutral; new behavior is gated by new tests. diff --git a/apps/selfhost/vm/boxes/mir_vm_m2.nyash b/apps/selfhost/vm/boxes/mir_vm_m2.nyash new file mode 100644 index 00000000..5e57c9aa --- /dev/null +++ b/apps/selfhost/vm/boxes/mir_vm_m2.nyash @@ -0,0 +1,112 @@ +// mir_vm_m2.nyash — Ny製の最小MIR(JSON v0)実行器(M2: const/binop/ret) + +static box MirVmM2 { + _str_to_int(s) { + local i = 0 + local n = s.length() + local acc = 0 + loop (i < n) { + local ch = s.substring(i, i+1) + if ch == "0" { acc = acc * 10 + 0 i = i + 1 continue } + if ch == "1" { acc = acc * 10 + 1 i = i + 1 continue } + if ch == "2" { acc = acc * 10 + 2 i = i + 1 continue } + if ch == "3" { acc = acc * 10 + 3 i = i + 1 continue } + if ch == "4" { acc = acc * 10 + 4 i = i + 1 continue } + if ch == "5" { acc = acc * 10 + 5 i = i + 1 continue } + if ch == "6" { acc = acc * 10 + 6 i = i + 1 continue } + if ch == "7" { acc = acc * 10 + 7 i = i + 1 continue } + if ch == "8" { acc = acc * 10 + 8 i = i + 1 continue } + if ch == "9" { acc = acc * 10 + 9 i = i + 1 continue } + break + } + return acc + } + _int_to_str(n) { + if n == 0 { return "0" } + local v = n + local out = "" + loop (v > 0) { + local d = v % 10 + local ch = "0" + if d == 1 { ch = "1" } else { if d == 2 { ch = "2" } else { if d == 3 { ch = "3" } else { if d == 4 { ch = "4" } else { if d == 5 { ch = "5" } else { if d == 6 { ch = "6" } else { if d == 7 { ch = "7" } else { if d == 8 { ch = "8" } else { if d == 9 { ch = "9" } } } } } } } } + out = ch + out + v = v / 10 + } + return out + } + _find_int_in(seg, keypat) { + local p = seg.indexOf(keypat) + if p < 0 { return null } + p = p + keypat.length() + local i = p + local out = "" + loop(true) { + local ch = seg.substring(i, i+1) + if ch == "" { break } + if ch == "0" || ch == "1" || ch == "2" || ch == "3" || ch == "4" || ch == "5" || ch == "6" || ch == "7" || ch == "8" || ch == "9" { out = out + ch i = i + 1 } else { break } + } + if out == "" { return null } + return me._str_to_int(out) + } + _find_str_in(seg, keypat) { + local p = seg.indexOf(keypat) + if p < 0 { return "" } + p = p + keypat.length() + local q = seg.indexOf(""", p) + if q < 0 { return "" } + return seg.substring(p, q) + } + _get(regs, id) { if regs.has(id) { return regs.get(id) } return 0 } + _set(regs, id, v) { regs.set(id, v) } + _bin(kind, a, b) { + if kind == "Add" { return a + b } + if kind == "Sub" { return a - b } + if kind == "Mul" { return a * b } + if kind == "Div" { if b == 0 { return 0 } else { return a / b } } + return 0 + } + run(json) { + local regs = new MapBox() + local pos = json.indexOf(""instructions":[") + if pos < 0 { + print("0") + return 0 + } + local cur = pos + loop(true) { + local op_pos = json.indexOf(""op":"", cur) + if op_pos < 0 { break } + local name_start = op_pos + 6 + local name_end = json.indexOf(""", name_start) + if name_end < 0 { break } + local opname = json.substring(name_start, name_end) + local next_pos = json.indexOf(""op":"", name_end) + if next_pos < 0 { next_pos = json.length() } + local seg = json.substring(op_pos, next_pos) + if opname == "const" { + local dst = me._find_int_in(seg, ""dst":") + local val = me._find_int_in(seg, ""value":{"type":"i64","value":") + if dst != null and val != null { me._set(regs, "" + dst, val) } + } else { if opname == "binop" { + local dst = me._find_int_in(seg, ""dst":") + local kind = me._find_str_in(seg, ""op_kind":"") + local lhs = me._find_int_in(seg, ""lhs":") + local rhs = me._find_int_in(seg, ""rhs":") + if dst != null and lhs != null and rhs != null { + local a = me._get(regs, "" + lhs) + local b = me._get(regs, "" + rhs) + me._set(regs, "" + dst, me._bin(kind, a, b)) + } + } else { if opname == "ret" { + local v = me._find_int_in(seg, ""value":") + if v == null { v = 0 } + local out = me._get(regs, "" + v) + print(me._int_to_str(out)) + return 0 + } } } + cur = next_pos + } + print("0") + return 0 + } +} diff --git a/apps/selfhost/vm/boxes/mir_vm_min.nyash b/apps/selfhost/vm/boxes/mir_vm_min.nyash index f2ce3448..29a57cc0 100644 --- a/apps/selfhost/vm/boxes/mir_vm_min.nyash +++ b/apps/selfhost/vm/boxes/mir_vm_min.nyash @@ -7,9 +7,17 @@ // {"op":"ret","value":1} // ]}]}] // } -// 振る舞い: 最初の const i64 の値を読み取り、print する。ret は value スロット参照を想定するが、MVPでは無視。 +// 振る舞い: +// - M1: 最初の const i64 の値を読み取り print +// - M2: const/binop/compare/ret を最小実装(簡易スキャンで安全に解釈) static box MirVmMin { + // Public entry used by parity tests (calls into minimal runner) + run(mjson) { + local v = me._run_min(mjson) + print(me._int_to_str(v)) + return v + } // 最小限のスキャン関数(依存ゼロ版) index_of_from(hay, needle, pos) { if pos < 0 { pos = 0 } @@ -26,11 +34,11 @@ static box MirVmMin { } return -1 } - read_digits(json, pos) { + read_digits(text, pos) { local out = "" local i = pos loop (true) { - local s = json.substring(i, i+1) + local s = text.substring(i, i+1) if s == "" { break } if s == "0" || s == "1" || s == "2" || s == "3" || s == "4" || s == "5" || s == "6" || s == "7" || s == "8" || s == "9" { out = out + s @@ -63,10 +71,10 @@ static box MirVmMin { if n == 0 { return "0" } local v = n local out = "" + local digits = "0123456789" loop (v > 0) { local d = v % 10 - local ch = "0" - if d == 1 { ch = "1" } else { if d == 2 { ch = "2" } else { if d == 3 { ch = "3" } else { if d == 4 { ch = "4" } else { if d == 5 { ch = "5" } else { if d == 6 { ch = "6" } else { if d == 7 { ch = "7" } else { if d == 8 { ch = "8" } else { if d == 9 { ch = "9" } } } } } } } } + local ch = digits.substring(d, d+1) out = ch + out v = v / 10 } @@ -74,26 +82,228 @@ static box MirVmMin { } // MVP: 最初の const i64 の値を抽出 - _extract_first_const_i64(json) { - if json == null { return 0 } + _extract_first_const_i64(text) { + if text == null { return 0 } // "op":"const" を探す - local p = json.indexOf("\"op\":\"const\"") + local p = text.indexOf("\"op\":\"const\"") if p < 0 { return 0 } // そこから "\"value\":{\"type\":\"i64\",\"value\":" を探す local key = "\"value\":{\"type\":\"i64\",\"value\":" - local q = me.index_of_from(json, key, p) + local q = me.index_of_from(text, key, p) if q < 0 { return 0 } q = q + key.length() // 連続する数字を読む - local digits = me.read_digits(json, q) + local digits = me.read_digits(text, q) if digits == "" { return 0 } return me._str_to_int(digits) } - // 実行: 値を print し、0 を返す(MVP)。将来は exit code 連動可。 - run(mir_json_text) { - local v = me._extract_first_const_i64(mir_json_text) - print(me._int_to_str(v)) + // --- M2 追加: 最小 MIR 実行(const/binop/compare/ret) --- + _get_map(regs, key) { if regs.has(key) { return regs.get(key) } return 0 } + _set_map(regs, key, val) { regs.set(key, val) } + _find_int_in(seg, keypat) { + local p = seg.indexOf(keypat) + if p < 0 { return null } + p = p + keypat.length() + local i = p + local out = "" + loop(true) { + local ch = seg.substring(i, i+1) + if ch == "" { break } + if ch == "0" || ch == "1" || ch == "2" || ch == "3" || ch == "4" || ch == "5" || ch == "6" || ch == "7" || ch == "8" || ch == "9" { out = out + ch i = i + 1 } else { break } + } + if out == "" { return null } + return me._str_to_int(out) + } + _find_str_in(seg, keypat) { + local p = seg.indexOf(keypat) + if p < 0 { return "" } + p = p + keypat.length() + local q = me.index_of_from(seg, "\"", p) + if q < 0 { return "" } + return seg.substring(p, q) + } + // --- JSON segment helpers (brace/bracket aware, minimal) --- + _seek_obj_start(text, from_pos) { + // scan backward to the nearest '{' + local i = from_pos + loop(true) { + i = i - 1 + if i < 0 { return 0 } + local ch = text.substring(i, i+1) + if ch == "{" { return i } + } return 0 } + _seek_obj_end(text, obj_start) { + // starting at '{', find matching '}' using depth counter + local i = obj_start + local depth = 0 + loop(true) { + local ch = text.substring(i, i+1) + if ch == "" { break } + if ch == "{" { depth = depth + 1 } + else { if ch == "}" { depth = depth - 1 } } + if depth == 0 { return i + 1 } + i = i + 1 + } + return i + } + _seek_array_end(text, array_after_bracket_pos) { + // given pos right after '[', find the matching ']' + local i = array_after_bracket_pos + local depth = 1 + loop(true) { + local ch = text.substring(i, i+1) + if ch == "" { break } + if ch == "[" { depth = depth + 1 } + else { if ch == "]" { depth = depth - 1 } } + if depth == 0 { return i } + i = i + 1 + } + return i + } + _map_binop_symbol(sym) { + if sym == "+" { return "Add" } + if sym == "-" { return "Sub" } + if sym == "*" { return "Mul" } + if sym == "/" { return "Div" } + if sym == "%" { return "Mod" } + return "" } + _map_cmp_symbol(sym) { + if sym == "==" { return "Eq" } + if sym == "!=" { return "Ne" } + if sym == "<" { return "Lt" } + if sym == "<=" { return "Le" } + if sym == ">" { return "Gt" } + if sym == ">=" { return "Ge" } + return "" } + _eval_binop(kind, a, b) { + if kind == "Add" { return a + b } + if kind == "Sub" { return a - b } + if kind == "Mul" { return a * b } + if kind == "Div" { if b == 0 { return 0 } else { return a / b } } + if kind == "Mod" { if b == 0 { return 0 } else { return a % b } } + return 0 } + _eval_cmp(kind, a, b) { + if kind == "Eq" { if a == b { return 1 } else { return 0 } } + if kind == "Ne" { if a != b { return 1 } else { return 0 } } + if kind == "Lt" { if a < b { return 1 } else { return 0 } } + if kind == "Gt" { if a > b { return 1 } else { return 0 } } + if kind == "Le" { if a <= b { return 1 } else { return 0 } } + if kind == "Ge" { if a >= b { return 1 } else { return 0 } } + return 0 + } + // Locate start of instructions array for given block id + _block_insts_start(mjson, bid) { + local key = "\"id\":" + me._int_to_str(bid) + local p = mjson.indexOf(key) + if p < 0 { return -1 } + local q = me.index_of_from(mjson, "\"instructions\":[", p) + if q < 0 { return -1 } + // "\"instructions\":[" is 16 chars → return pos right after '[' + return q + 16 + } + _block_insts_end(mjson, insts_start) { + // Bound to the end bracket of this block's instructions array + return me._seek_array_end(mjson, insts_start) + } + _run_min(mjson) { + local regs = new MapBox() + // Control flow: start at block 0, process until ret + local bb = 0 + loop(true) { + local pos = me._block_insts_start(mjson, bb) + if pos < 0 { return me._extract_first_const_i64(mjson) } + local block_end = me._block_insts_end(mjson, pos) + // Single-pass over instructions: segment each op object precisely and evaluate + local scan = pos + local moved = 0 + loop(true) { + // find next op field within this block + local opos = me.index_of_from(mjson, "\"op\":\"", scan) + if opos < 0 || opos >= block_end { break } + // find exact JSON object bounds for this instruction + // Determine object start as the last '{' between pos..opos + local i = pos + local obj_start = opos + loop(i <= opos) { + local ch0 = mjson.substring(i, i+1) + if ch0 == "{" { obj_start = i } + i = i + 1 + } + local obj_end = me._seek_obj_end(mjson, obj_start) + if obj_end > block_end { obj_end = block_end } + local seg = mjson.substring(obj_start, obj_end) + + // dispatch by op name (v0/v1 tolerant) + local opname = me._find_str_in(seg, "\"op\":\"") + if opname == "const" { + local cdst = me._find_int_in(seg, "\"dst\":") + local cval = me._find_int_in(seg, "\"value\":{\"type\":\"i64\",\"value\":") + if cdst != null and cval != null { me._set_map(regs, "" + cdst, cval) } + } else { + if opname == "binop" { + local bdst = me._find_int_in(seg, "\"dst\":") + local bkind = me._find_str_in(seg, "\"op_kind\":\"") + if bkind == "" { bkind = me._map_binop_symbol(me._find_str_in(seg, "\"operation\":\"")) } + local blhs = me._find_int_in(seg, "\"lhs\":") + local brhs = me._find_int_in(seg, "\"rhs\":") + if bdst != null and blhs != null and brhs != null { + local a = me._get_map(regs, "" + blhs) + local b = me._get_map(regs, "" + brhs) + local r = me._eval_binop(bkind, a, b) + me._set_map(regs, "" + bdst, r) + } + } else { + if opname == "compare" { + local kdst = me._find_int_in(seg, "\"dst\":") + local kkind = me._find_str_in(seg, "\"cmp\":\"") + if kkind == "" { kkind = me._map_cmp_symbol(me._find_str_in(seg, "\"operation\":\"")) } + local klhs = me._find_int_in(seg, "\"lhs\":") + local krhs = me._find_int_in(seg, "\"rhs\":") + if kdst != null and klhs != null and krhs != null { + local a = me._get_map(regs, "" + klhs) + local b = me._get_map(regs, "" + krhs) + local r = me._eval_cmp(kkind, a, b) + me._set_map(regs, "" + kdst, r) + } + } else { + if opname == "jump" { + local tgt = me._find_int_in(seg, "\"target\":") + if tgt != null { bb = tgt scan = block_end moved = 1 break } + } else { + if opname == "branch" { + local cond = me._find_int_in(seg, "\"cond\":") + local then_id = me._find_int_in(seg, "\"then\":") + local else_id = me._find_int_in(seg, "\"else\":") + local cval = 0 + if cond != null { cval = me._get_map(regs, "" + cond) } + if cval != 0 { bb = then_id } else { bb = else_id } + scan = block_end + moved = 1 + break + } else { + if opname == "ret" { + local rv = me._find_int_in(seg, "\"value\":") + if rv == null { rv = 0 } + return me._get_map(regs, "" + rv) + } + } + } + } + } + } + + // advance to the end of this instruction object + scan = obj_end + } + // No ret encountered in this block; if control moved, continue with new bb + if moved == 1 { continue } + // Fallback when ret not found at all in processed blocks + return me._extract_first_const_i64(mjson) + } + return me._extract_first_const_i64(mjson) + } + } diff --git a/apps/selfhost/vm/boxes/vm_kernel_box.nyash b/apps/selfhost/vm/boxes/vm_kernel_box.nyash new file mode 100644 index 00000000..84c015c9 --- /dev/null +++ b/apps/selfhost/vm/boxes/vm_kernel_box.nyash @@ -0,0 +1,32 @@ +// vm_kernel_box.nyash — NYABI Kernel (skeleton, dev-only; not wired) +// Scope: Provide policy/decision helpers behind an explicit OFF toggle. +// Notes: This box is not referenced by the VM by default. + +static box VmKernelBox { + // Report version and supported features. + caps() { + // v0 draft: features are informative only. + return "{\"version\":0,\"features\":[\"policy\"]}" + } + + // Decide stringify strategy for a given type. + // Returns: "direct" | "rewrite_stringify" | "fallback" + stringify_policy(typeName) { + if typeName == "VoidBox" { return "rewrite_stringify" } + return "fallback" + } + + // Decide equals strategy for two types. + // Returns: "object" | "value" | "fallback" + equals_policy(lhsType, rhsType) { + if lhsType == rhsType { return "value" } + return "fallback" + } + + // Batch resolve method dispatch plans. + // Input/Output via tiny JSON strings (draft). Returns "{\"plans\":[]}" for now. + resolve_method_batch(reqs_json) { + return "{\"plans\":[]}" + } +} + diff --git a/apps/selfhost/vm/mir_min_entry.nyash b/apps/selfhost/vm/mir_min_entry.nyash index b9d50b3e..c9a149b6 100644 --- a/apps/selfhost/vm/mir_min_entry.nyash +++ b/apps/selfhost/vm/mir_min_entry.nyash @@ -7,7 +7,9 @@ static box Main { main(args) { // 既定の最小 MIR(JSON v0) local json = "{\"functions\":[{\"name\":\"main\",\"params\":[],\"blocks\":[{\"id\":0,\"instructions\":[{\"op\":\"const\",\"dst\":1,\"value\":{\"type\":\"i64\",\"value\":42}},{\"op\":\"ret\",\"value\":1}]}]}]}" - if args { if args.size() > 0 { local s = args.get(0) if s { json = s } } } - return MirVmMin.run(json) + if args != null { if args.size() > 0 { local s = args.get(0) if s != null { json = s } } } + local v = MirVmMin._run_min(json) + print(MirVmMin._int_to_str(v)) + return 0 } } diff --git a/docs/abi/vm-kernel.md b/docs/abi/vm-kernel.md new file mode 100644 index 00000000..5383eb30 --- /dev/null +++ b/docs/abi/vm-kernel.md @@ -0,0 +1,47 @@ +# NYABI: VM Kernel Bridge (Draft) + +Scope +- Provide a minimal, stable ABI for delegating selected VM policies/decisions to Ny code. +- Keep default behavior unchanged (OFF by default). Ny Kernel is a development aid only. + +Design Principles +- Coarse-grained: call per feature, not per instruction (avoid hot-path crossings). +- Fail‑Fast: any error returns immediately; no silent fallbacks in dev. +- Safe by default: re‑entry forbidden; each call has a deadline (timeout). +- Backward compatible: API evolves via additive fields and optional methods only. + +API (v0, draft) +- VmKernel.caps() -> { version: i64, features: [string] } +- VmKernel.stringify_policy(type: string) -> string + - Returns: "direct" | "rewrite_stringify" | "fallback" +- VmKernel.equals_policy(lhs_type: string, rhs_type: string) -> string + - Returns: "object" | "value" | "fallback" +- VmKernel.resolve_method_batch(reqs_json: string) -> string + - Input JSON: { reqs: [{class, method, arity}], context?: {...} } + - Output JSON: { plans: [{kind, target, notes?}], errors?: [...] } + +Error & Timeout +- All calls run with a per‑call deadline (NYASH_VM_NY_KERNEL_TIMEOUT_MS; default 200ms when enabled). +- On timeout/NY error, Rust VM aborts the bridge call (OFF path remains intact). + +Re‑entry Guard +- A thread‑local flag prevents re‑entering the Ny Kernel from within an ongoing Ny Kernel call. +- On violation, the bridge errors immediately (Fail‑Fast). + +Data Model +- Strings for structured data (JSON) across the boundary to avoid shape drift. +- Primitive returns (i64/bool/string) for simple policies. + +Toggles (reserved; default OFF) +- NYASH_VM_NY_KERNEL=0|1 +- NYASH_VM_NY_KERNEL_TIMEOUT_MS=200 +- NYASH_VM_NY_KERNEL_TRACE=0|1 + +Acceptance (v0) +- With bridge OFF, behavior is unchanged on all smokes. +- With bridge ON and a stub kernel, behavior is still unchanged; logging shows calls and zero decisions. +- Bridge API documented and skeleton Ny box exists (not wired by default). + +Notes +- Router batching is critical to avoid per‑call overhead. +- Keep JSON schemas tiny and versioned; include a top‑level "v" if necessary. diff --git a/docs/development/roadmap/phases/phase-15.7/README.md b/docs/development/roadmap/phases/phase-15.7/README.md new file mode 100644 index 00000000..b12d26f5 --- /dev/null +++ b/docs/development/roadmap/phases/phase-15.7/README.md @@ -0,0 +1,90 @@ +# Phase 15.7: Known化+Rewrite統合(dev観測)と Mini‑VM 安定化(dev限定) + +目的 +- Builderでの Known 化と Instance→Function の統一(Known 経路)を優先し、実行系(VM/LLVM/Ny)を単純化する。 +- 早期観測(resolve.try/choose, ssa.phi)を dev‑only で整備し、Union の発生点を特定可能にする。 +- 表示APIを `str()` に統一(互換: `stringify()`)し、言語表面のブレを解消する(挙動不変)。 +- Mini‑VM(Ny)を安全に安定化(M2/M3代表ケース)。NYABI Kernel は“下地のみ”(既定OFF)。 + +背景 +- Instance→Function 正規化の方針は既定ON。Known 経路は関数化し、VM側は単純化する。 +- resolve.try/choose(Builder)と ssa.phi(Builder)の観測は dev‑only で導入済み(既定OFF)。 +- Mini‑VM は M2/M3 の代表ケースを安定化(パス/境界厳密化)。 +- VM Kernel の Ny 化は後段(観測・ポリシーから段階導入、既定OFF)。 + +Unified Call(開発既定ON) +- 呼び出しの統一判定は、環境変数 `NYASH_MIR_UNIFIED_CALL` が `0|false|off` でない限り有効(既定ON)。 +- メソッド解決/関数化を `emit_unified_call` に集約し、以下の順序で決定: + 1) 早期 toString/stringify→str + 2) equals/1(Known 優先→一意候補; ユーザーBox限定) + 3) Known→関数化(`obj.m → Class.m(me,…)`)/一意候補フォールバック(決定性確保) +- レガシー側の関数化は dev ガードで抑止可能: `NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1`(移行期間の重複回避) + +スコープ(やること) +1) Builder: Known 化 + Rewrite 統合(Stage‑1) + - P0: me 注入・Known 化(origin 付与/維持)— 軽量PHI補強(単一/一致時) + - P1: Known 経路 100% 関数化(obj.m → Class.m(me,…))。special は `toString→str(互換:stringify)/equals` を統合 + - 観測: resolve.try/choose / ssa.phi を dev‑only で JSONL 出力(既定OFF)。`resolve.choose` に `certainty` を付加し、KPI(Known率)を任意出力(`NYASH_DEBUG_KPI_KNOWN=1`, `NYASH_DEBUG_SAMPLE_EVERY=N`)。 + +2) 表示APIの統一(挙動不変) + - 規範: `str()` / `x.str()`(同義)。`toString()` は早期に `str()` へ正規化 + - 互換: `stringify()` は当面エイリアスとして許容 + - QuickRef/ガイドの更新(plus混在の誘導も `str()` に統一) + +3) Mini‑VM(MirVmMin)安定化(devのみ) + - 厳密セグメントによる単一パス化、M2/M3 代表スモーク常緑(const/binop/compare/branch/jump/ret) + - パリティ: VM↔LLVM↔Ny のミニ・パリティ 2〜3件 + +4) NYABI(VM Kernel Bridge)下地(未配線・既定OFF) + - docs/abi/vm-kernel.md(関数: caps()/policy.*()/resolve_method_batch()) + - スケルトン: apps/selfhost/vm/boxes/vm_kernel_box.nyash(policy スタブ) + - 既定OFFトグル予約: NYASH_VM_NY_KERNEL, *_TIMEOUT_MS, *_TRACE + +非スコープ(やらない) +- 既定挙動の変更(Rust VM/LLVMが主軸のまま) +- PHI/SSAの一般化(Phase 16 で扱う) +- VM Kernel の本配線(観測・ポリシーは dev‑only/未配線) + +リスクと軽減策 +- 性能: 境界越えは後Phaseに限る(本Phaseは未配線)。Mini‑VMは開発補助で性能要件なし。 +- 複雑性: 設計は最小APIに限定。拡張は追加のみ(後方互換維持)。 +- 安全: すべて既定OFF。Fail‑Fast方針。再入禁止/タイムアウトを仕様に明記。 + +受け入れ条件(Acceptance) +- quick: Mini‑VM(M2/M3)代表スモーク緑(const/binop/compare/branch/jump/ret) +- integration: 代表パリティ緑(llvmlite/ハーネス) +- Builder: resolve.try/choose と ssa.phi が dev‑only で取得可能(NYASH_DEBUG_*) +- 表示API: QuickRef/ガイドが `str()` に統一(実行挙動は従前と同じ) +- Unified Call は開発既定ONだが、`NYASH_MIR_UNIFIED_CALL=0|false|off` で即時オプトアウト可能(段階移行)。 + +実装タスク(小粒) +1. origin/observe/rewrite の分割方針を CURRENT_TASK に反映(ガイド/README付き) +2. Known fast‑path の一本化(rewrite::try_known_rewrite)+ special の集約 +3. 表示APIの統一(toString→str、互換:stringify)— VM ルータ特例の整合・ドキュメント更新 +4. MirVmMin: 単一パス化・境界厳密化(M2/M3)・代表スモーク緑 +5. docs/abi/vm-kernel.md(下書き維持)・スケルトン Box(未配線) + +トグル/ENV(予約、既定OFF) +- NYASH_VM_NY_KERNEL=0|1 +- NYASH_VM_NY_KERNEL_TIMEOUT_MS=200 +- NYASH_VM_NY_KERNEL_TRACE=0|1 + +ロールバック方針 +- Mini‑VMの変更は apps/selfhost/ 配下に限定(本線コードは未配線)。 +- NYABIは docs/ と スケルトンBoxのみ(実行経路から未参照)。 +- Unified Call は env で即時OFF可能。問題時は `NYASH_MIR_UNIFIED_CALL=0` を宣言してレガシーへ退避し、修正後に既定へ復帰。 + +補足(レイヤー・ガード) +- builder 層は origin→observe→rewrite の一方向依存を維持する。違反検出スクリプト: `tools/dev/check_builder_layers.sh` + +関連(参照) +- Phase 15(セルフホスティング): ../phase-15/README.md +- Phase 15.5(基盤整理): ../phase-15.5/README.md +- Known/Rewrite 観測: src/mir/builder/{method_call_handlers.rs,builder_calls.rs}, src/debug/hub.rs +- QuickRef(表示API): docs/reference/language/quick-reference.md +- Mini‑VM: apps/selfhost/vm/boxes/mir_vm_min.nyash +- スモーク: tools/smokes/v2/profiles/quick/core/ + +更新履歴 +- 2025‑09‑28 v2(本書): Known 化+Rewrite 統合(dev観測)、表示API `str()` 統一、Mini‑VM 安定化へ焦点を再定義 +- 2025‑09‑28 初版: Mini‑VM M3 + NYABI下地の計画 diff --git a/docs/how-to/self-hosting.md b/docs/how-to/self-hosting.md index 3eaa5b19..f63fefd8 100644 --- a/docs/how-to/self-hosting.md +++ b/docs/how-to/self-hosting.md @@ -19,6 +19,12 @@ 5) LLVM 統合(任意・AOT/ハーネス) - 実行: `tools/smokes/v2/run.sh --profile integration` +最小 Ny 実行器(MirVmMin) +- 目的: Ny だけで MIR(JSON v0) のごく最小セット(const/binop/compare/ret)を実行できることを確認。 +- 実行例(VM): + - `./target/release/nyash --backend vm apps/selfhost/vm/mir_min_entry.nyash` + - 引数で MIR(JSON) を渡すことも可能(単一文字列)。簡単な例は `apps/selfhost/vm/mir_min_entry.nyash` のコメントを参照。 + 検証 - 期待出力: `Result: 0`(selfhost‑minimal) - スモーク:全成功(非 0 は失敗) diff --git a/docs/reference/language/quick-reference.md b/docs/reference/language/quick-reference.md new file mode 100644 index 00000000..6d2e6c32 --- /dev/null +++ b/docs/reference/language/quick-reference.md @@ -0,0 +1,76 @@ +# Nyash Quick Reference (MVP) + +Purpose +- One‑page practical summary for writing and implementing Nyash. +- Keep grammar minimal; clarify rules that often cause confusion. + +Keywords (reserved) +- control: `if`, `else`, `loop`, `match`, `case`, `break`, `continue`, `return` +- decl: `static`, `box`, `local`, `using`, `as` +- lit: `true`, `false`, `null`, `void` + +Expressions and Calls +- Function call: `f(a, b)` +- Method call: `obj.m(a, b)` — internally rewritten to function form: `Class.m(me: obj, a, b)` + - Rewrite is default‑ON; backends (VM/LLVM/Ny) receive the unified call shape. +- Member: `obj.field` or `obj.m` + +Display & Conversion +- Human‑readable display: `str(x)`(推奨)/ `x.str()` + - 既存の `toString()` は `str()` に正規化(Builder早期リライト)。 + - 互換: 既存の `stringify()` は当面エイリアス(内部で `str()` 相当へ誘導)。 +- Debug表示(構造的・安定): `repr(x)`(将来導入、devのみ) +- JSONシリアライズ: `toJson(x)`(文字列)/ `toJsonNode(x)`(構造) + +Operators (precedence high→low) +- Unary: `! ~ -` +- Multiplicative: `* / %` +- Additive: `+ -` +- Compare: `== != < <= > >=` +- Logical: `&& ||` (short‑circuit, side‑effect aware) + +Semicolons and ASI (Automatic Semicolon Insertion) +- Allowed to omit semicolon at: + - End of line, before `}` or at EOF, when the statement is syntactically complete. +- Not allowed: + - Line break immediately after a binary operator (e.g., `1 +\n2`) + - Ambiguous continuations; parser must Fail‑Fast with a clear message. + +Truthiness (boolean context) +- `Bool` → itself +- `Integer` → `0` is false; non‑zero is true +- `String` → empty string is false; otherwise true +- `Array`/`Map` → non‑null is true (size is not consulted) +- `null`/`void` → false + +Equality and Comparison +- `==` and `!=` compare primitive values (Integer/Bool/String). No implicit cross‑type coercion. +- Box/Instance comparisons should use explicit methods (`equals`), or be normalized by the builder. +- Compare operators `< <= > >=` are defined on integers (MVP). + +String and Numeric `+` +- If either side is `String`, `+` is string concatenation. +- If both sides are numeric, `+` is addition. +- Other mixes are errors (dev: warn; prod: error) — keep it explicit(必要なら `str(x)` を使う)。 + +Blocks and Control +- `if (cond) { ... } [else { ... }]` +- `loop (cond) { ... }` — minimal loop form +- `match (expr) { case ... }` — MVP (literals and simple type patterns) + +Using / SSOT +- Dev/CI: file‑based `using` allowed for convenience. +- Prod: `nyash.toml` only. Duplicate imports or alias rebinding is an error. + +Errors (format) +- Always: `Error at line X, column Y: ` +- For tokenizer errors, add the reason and show one nearby line if possible. + +Dev/Prod toggles (indicative) +- `NYASH_DEV=1` — developer defaults (diagnostics, tracing; behavior unchanged) +- `NYASH_ENABLE_USING=1` — enable using resolver +- `NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1` — allow `main` as top‑level entry + +Notes +- Keep the language small. Prefer explicit conversions (`int(x)`, `str(x)`, `bool(x)`) in standard helpers over implicit coercions. +- Builder rewrites method calls to keep runtime dispatch simple and consistent across backends. diff --git a/src/backend/mir_interpreter/handlers/calls.rs b/src/backend/mir_interpreter/handlers/calls.rs index 6114a2a4..a3b02a76 100644 --- a/src/backend/mir_interpreter/handlers/calls.rs +++ b/src/backend/mir_interpreter/handlers/calls.rs @@ -26,15 +26,52 @@ impl MirInterpreter { ) -> Result { match callee { Callee::Global(func_name) => self.execute_global_function(func_name, args), - Callee::Method { - box_name: _, - method, - receiver, - certainty: _, - } => { + Callee::Method { box_name: _, method, receiver, certainty: _, } => { if let Some(recv_id) = receiver { let recv_val = self.reg_load(*recv_id)?; let dev_trace = std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1"); + // Fast bridge for builtin boxes (Array) and common methods. + // Preserve legacy semantics when plugins are absent. + if let VMValue::BoxRef(bx) = &recv_val { + // ArrayBox bridge + if let Some(arr) = bx.as_any().downcast_ref::() { + match method.as_str() { + "birth" => { return Ok(VMValue::Void); } + "push" => { + if let Some(a0) = args.get(0) { + let v = self.reg_load(*a0)?.to_nyash_box(); + let _ = arr.push(v); + return Ok(VMValue::Void); + } + } + "len" | "length" | "size" => { + let ret = arr.length(); + return Ok(VMValue::from_nyash_box(ret)); + } + "get" => { + if let Some(a0) = args.get(0) { + let idx = self.reg_load(*a0)?.to_nyash_box(); + let ret = arr.get(idx); + return Ok(VMValue::from_nyash_box(ret)); + } + } + "set" => { + if args.len() >= 2 { + let idx = self.reg_load(args[0])?.to_nyash_box(); + let val = self.reg_load(args[1])?.to_nyash_box(); + let _ = arr.set(idx, val); + return Ok(VMValue::Void); + } + } + _ => {} + } + } + } + // Minimal bridge for birth(): delegate to BoxCall handler and return Void + if method == &"birth" { + let _ = self.handle_box_call(None, *recv_id, method, args)?; + return Ok(VMValue::Void); + } let is_kw = method == &"keyword_to_token_type"; if dev_trace && is_kw { let a0 = args.get(0).and_then(|id| self.reg_load(*id).ok()); diff --git a/src/backend/mir_interpreter/method_router.rs b/src/backend/mir_interpreter/method_router.rs index cbb5cdf7..8b63bcb3 100644 --- a/src/backend/mir_interpreter/method_router.rs +++ b/src/backend/mir_interpreter/method_router.rs @@ -62,18 +62,20 @@ fn reroute_to_correct_method( } /// Try mapping special methods to canonical targets (table-driven). -/// Example: toString/0 → stringify/0 (prefer instance class, then base class without "Instance" suffix). +/// Example: toString/0 → str/0(互換: stringify/0)(prefer instance class, then base class without "Instance" suffix). fn try_special_reroute( interp: &mut MirInterpreter, recv_cls: &str, parsed: &ParsedSig<'_>, arg_vals: Option<&[VMValue]>, ) -> Option> { - // toString → stringify + // toString → str(互換: stringify) if parsed.method == "toString" && parsed.arity_str == "0" { - // Prefer instance class stringify first, then base (strip trailing "Instance") + // Prefer instance class 'str' first, then base(strip trailing "Instance")。なければ 'stringify' を互換で探す let base = recv_cls.strip_suffix("Instance").unwrap_or(recv_cls); let candidates = [ + format!("{}.str/0", recv_cls), + format!("{}.str/0", base), format!("{}.stringify/0", recv_cls), format!("{}.stringify/0", base), ]; @@ -91,7 +93,7 @@ fn try_special_reroute( "method": parsed.method, "arity": parsed.arity_str, "target": name, - "reason": "toString->stringify", + "reason": if name.ends_with(".str/0") { "toString->str" } else { "toString->stringify" }, }), ); return Some(interp.exec_function_inner(&f, arg_vals)); diff --git a/src/boxes/p2p_box.rs b/src/boxes/p2p_box.rs index c4634d4d..b4f794fc 100644 --- a/src/boxes/p2p_box.rs +++ b/src/boxes/p2p_box.rs @@ -343,33 +343,42 @@ impl P2PBox { if let Ok(mut li) = last_intent.write() { *li = Some(env.intent.get_name().to_string_box().value); } - // 最小インタープリタで FunctionBox を実行 - let mut interp = crate::interpreter::NyashInterpreter::new(); - // キャプチャ注入 - for (k, v) in func_clone.env.captures.iter() { - interp.declare_local_variable(k, v.clone_or_share()); + // 最小インタープリタで FunctionBox を実行(legacy, feature-gated) + #[cfg(feature = "interpreter-legacy")] + { + let mut interp = crate::interpreter::NyashInterpreter::new(); + // キャプチャ注入 + for (k, v) in func_clone.env.captures.iter() { + interp.declare_local_variable(k, v.clone_or_share()); + } + if let Some(me_w) = &func_clone.env.me_value { + if let Some(me_arc) = me_w.upgrade() { + interp.declare_local_variable("me", (*me_arc).clone_or_share()); + } + } + // 引数束縛: intent, from(必要数だけ) + let args: Vec> = vec![ + Box::new(env.intent.clone()), + Box::new(StringBox::new(env.from.clone())), + ]; + for (i, p) in func_clone.params.iter().enumerate() { + if let Some(av) = args.get(i) { + interp.declare_local_variable(p, av.clone_or_share()); + } + } + // 本体実行 + crate::runtime::global_hooks::push_task_scope(); + for st in &func_clone.body { + let _ = interp.execute_statement(st); + } + crate::runtime::global_hooks::pop_task_scope(); } - if let Some(me_w) = &func_clone.env.me_value { - if let Some(me_arc) = me_w.upgrade() { - interp.declare_local_variable("me", (*me_arc).clone_or_share()); + #[cfg(not(feature = "interpreter-legacy"))] + { + if crate::config::env::cli_verbose() { + eprintln!("[warn] FunctionBox handler requires interpreter-legacy; skipped execution"); } } - // 引数束縛: intent, from(必要数だけ) - let args: Vec> = vec![ - Box::new(env.intent.clone()), - Box::new(StringBox::new(env.from.clone())), - ]; - for (i, p) in func_clone.params.iter().enumerate() { - if let Some(av) = args.get(i) { - interp.declare_local_variable(p, av.clone_or_share()); - } - } - // 本体実行 - crate::runtime::global_hooks::push_task_scope(); - for st in &func_clone.body { - let _ = interp.execute_statement(st); - } - crate::runtime::global_hooks::pop_task_scope(); if once { flag.store(false, Ordering::SeqCst); if let Ok(mut flags) = flags_arc.write() { diff --git a/src/llvm_py/instructions/mir_call.py b/src/llvm_py/instructions/mir_call.py index 65440117..f063be30 100644 --- a/src/llvm_py/instructions/mir_call.py +++ b/src/llvm_py/instructions/mir_call.py @@ -22,7 +22,7 @@ def lower_mir_call(owner, builder: ir.IRBuilder, mir_call: Dict[str, Any], dst_v """ # Check if unified call is enabled - use_unified = os.getenv("NYASH_MIR_UNIFIED_CALL", "0") != "0" +use_unified = os.getenv("NYASH_MIR_UNIFIED_CALL", "1").lower() not in ("0", "false", "off") if not use_unified: # Fall back to legacy dispatching return lower_legacy_call(owner, builder, mir_call, dst_vid, vmap, resolver) diff --git a/src/mir/builder.rs b/src/mir/builder.rs index 41d1b5dd..f0bac32f 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -37,6 +37,9 @@ mod plugin_sigs; // plugin signature loader mod stmts; mod utils; mod vars; // variables/scope helpers // small loop helpers (header/exit context) +mod origin; // P0: origin inference(me/Known)と PHI 伝播(軽量) +mod observe; // P0: dev-only observability helpers(ssa/resolve) +mod rewrite; // P1: Known rewrite & special consolidation // Unified member property kinds for computed/once/birth_once #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -99,6 +102,11 @@ pub struct MirBuilder { /// Index of static methods seen during lowering: name -> [(BoxName, arity)] pub(super) static_method_index: std::collections::HashMap>, + /// Fast lookup: method+arity tail → candidate function names (e.g., ".str/0" → ["JsonNode.str/0", ...]) + pub(super) method_tail_index: std::collections::HashMap>, + /// Source size snapshot to detect when to rebuild the tail index + pub(super) method_tail_index_source_len: usize, + // include guards removed /// Loop context stacks for lowering break/continue inside nested control flow @@ -169,6 +177,8 @@ impl MirBuilder { plugin_method_sigs, current_static_box: None, static_method_index: std::collections::HashMap::new(), + method_tail_index: std::collections::HashMap::new(), + method_tail_index_source_len: 0, loop_header_stack: Vec::new(), loop_exit_stack: Vec::new(), @@ -256,6 +266,56 @@ impl MirBuilder { self.debug_scope_stack.last().cloned() } + // ---------------------- + // Method tail index (performance helper) + // ---------------------- + fn rebuild_method_tail_index(&mut self) { + self.method_tail_index.clear(); + if let Some(ref module) = self.current_module { + for name in module.functions.keys() { + if let (Some(dot), Some(slash)) = (name.rfind('.'), name.rfind('/')) { + if slash > dot { + let tail = &name[dot..]; + self.method_tail_index + .entry(tail.to_string()) + .or_insert_with(Vec::new) + .push(name.clone()); + } + } + } + self.method_tail_index_source_len = module.functions.len(); + } else { + self.method_tail_index_source_len = 0; + } + } + + fn ensure_method_tail_index(&mut self) { + let need_rebuild = match self.current_module { + Some(ref refmod) => self.method_tail_index_source_len != refmod.functions.len(), + None => self.method_tail_index_source_len != 0, + }; + if need_rebuild { + self.rebuild_method_tail_index(); + } + } + + pub(super) fn method_candidates(&mut self, method: &str, arity: usize) -> Vec { + self.ensure_method_tail_index(); + let tail = format!(".{}{}", method, format!("/{}", arity)); + self.method_tail_index + .get(&tail) + .cloned() + .unwrap_or_default() + } + + pub(super) fn method_candidates_tail>(&mut self, tail: S) -> Vec { + self.ensure_method_tail_index(); + self.method_tail_index + .get(tail.as_ref()) + .cloned() + .unwrap_or_default() + } + /// Build a complete MIR module from AST pub fn build_module(&mut self, ast: ASTNode) -> Result { @@ -354,84 +414,13 @@ impl MirBuilder { .as_ref() .map(|f| f.signature.name.clone()); let dbg_region_id = self.debug_current_region_id(); + // P0: PHI の軽量補強と観測は、関数ブロック取得前に実施して借用競合を避ける + if let MirInstruction::Phi { dst, inputs } = &instruction { + origin::phi::propagate_phi_meta(self, *dst, inputs); + observe::ssa::emit_phi(self, *dst, inputs); + } + if let Some(ref mut function) = self.current_function { - // Dev-safe meta propagation for PHI: if all incoming values agree on type/origin, - // propagate to the PHI destination. This helps downstream resolution (e.g., - // instance method rewrite across branches) without changing semantics. - if let MirInstruction::Phi { dst, inputs } = &instruction { - // Propagate value_types when all inputs share the same known type - let mut common_ty: Option = None; - let mut ty_agree = true; - for (_bb, v) in inputs.iter() { - if let Some(t) = self.value_types.get(v).cloned() { - match &common_ty { - None => common_ty = Some(t), - Some(ct) => { - if ct != &t { ty_agree = false; break; } - } - } - } else { - ty_agree = false; - break; - } - } - if ty_agree { - if let Some(ct) = common_ty.clone() { - self.value_types.insert(*dst, ct); - } - } - // Propagate value_origin_newbox when all inputs share same origin class - let mut common_cls: Option = None; - let mut cls_agree = true; - for (_bb, v) in inputs.iter() { - if let Some(c) = self.value_origin_newbox.get(v).cloned() { - match &common_cls { - None => common_cls = Some(c), - Some(cc) => { - if cc != &c { cls_agree = false; break; } - } - } - } else { - cls_agree = false; - break; - } - } - if cls_agree { - if let Some(cc) = common_cls.clone() { - self.value_origin_newbox.insert(*dst, cc); - } - } - // Emit debug event (dev-only) - { - let preds: Vec = inputs.iter().map(|(bb,v)| { - let t = self.value_types.get(v).cloned(); - let o = self.value_origin_newbox.get(v).cloned(); - serde_json::json!({ - "bb": bb.0, - "v": v.0, - "type": t.as_ref().map(|tt| format!("{:?}", tt)).unwrap_or_default(), - "origin": o.unwrap_or_default(), - }) - }).collect(); - let decided_t = self.value_types.get(dst).cloned().map(|tt| format!("{:?}", tt)).unwrap_or_default(); - let decided_o = self.value_origin_newbox.get(dst).cloned().unwrap_or_default(); - let meta = serde_json::json!({ - "dst": dst.0, - "preds": preds, - "decided_type": decided_t, - "decided_origin": decided_o, - }); - let fn_name = dbg_fn_name.as_deref(); - let region = dbg_region_id.as_deref(); - crate::debug::hub::emit( - "ssa", - "phi", - fn_name, - region, - meta, - ); - } - } if let Some(block) = function.get_block_mut(block_id) { if utils::builder_debug_enabled() { eprintln!( @@ -602,7 +591,10 @@ impl MirBuilder { // VM will treat plain NewBox as constructed; dev verify warns if needed. // - For builtins/plugins, keep BoxCall("birth") fallback to preserve legacy init. let is_user_box = self.user_defined_boxes.contains(&class); - if !is_user_box { + // Dev safety: allow disabling birth() injection for builtins to avoid + // unified-call method dispatch issues while migrating. Off by default unless explicitly enabled. + let allow_builtin_birth = std::env::var("NYASH_DEV_BIRTH_INJECT_BUILTINS").ok().as_deref() == Some("1"); + if !is_user_box && allow_builtin_birth { let birt_mid = resolve_slot_by_type_name(&class, "birth"); self.emit_box_or_plugin_call( None, diff --git a/src/mir/builder/builder_calls.rs b/src/mir/builder/builder_calls.rs index d1cd46e6..a10aa384 100644 --- a/src/mir/builder/builder_calls.rs +++ b/src/mir/builder/builder_calls.rs @@ -81,12 +81,117 @@ impl super::MirBuilder { return self.emit_legacy_call(dst, target, args); } + // Emit resolve.try for method targets (dev-only; default OFF) + let arity_for_try = args.len(); + if let CallTarget::Method { ref box_type, ref method, receiver } = target { + let recv_cls = box_type.clone() + .or_else(|| self.value_origin_newbox.get(&receiver).cloned()) + .unwrap_or_default(); + // Use indexed candidate lookup (tail → names) + let candidates: Vec = self.method_candidates(method, arity_for_try); + let meta = serde_json::json!({ + "recv_cls": recv_cls, + "method": method, + "arity": arity_for_try, + "candidates": candidates, + }); + super::observe::resolve::emit_try(self, meta); + } + + // Centralized user-box rewrite for method targets (toString/stringify, equals/1, Known→unique) + if let CallTarget::Method { ref box_type, ref method, receiver } = target { + let class_name_opt = box_type.clone() + .or_else(|| self.value_origin_newbox.get(&receiver).cloned()) + .or_else(|| self.value_types.get(&receiver).and_then(|t| if let super::MirType::Box(b) = t { Some(b.clone()) } else { None })); + // Early str-like + if let Some(res) = crate::mir::builder::rewrite::special::try_early_str_like_to_dst( + self, dst, receiver, &class_name_opt, method, args.len(), + ) { res?; return Ok(()); } + // equals/1 + if let Some(res) = crate::mir::builder::rewrite::special::try_special_equals_to_dst( + self, dst, receiver, &class_name_opt, method, args.clone(), + ) { res?; return Ok(()); } + // Known or unique + if let Some(res) = crate::mir::builder::rewrite::known::try_known_or_unique_to_dst( + self, dst, receiver, &class_name_opt, method, args.clone(), + ) { res?; return Ok(()); } + } + // Convert CallTarget to Callee using the new module - let callee = call_unified::convert_target_to_callee( - target, + if let CallTarget::Global(ref _n) = target { /* dev trace removed */ } + // Fallback: if Global target is unknown, try unique static-method mapping (name/arity) + let callee = match call_unified::convert_target_to_callee( + target.clone(), &self.value_origin_newbox, &self.value_types, - )?; + ) { + Ok(c) => c, + Err(e) => { + if let CallTarget::Global(ref name) = target { + // 0) Dev-only safety: treat condition_fn as always-true predicate when missing + if name == "condition_fn" { + let dstv = dst.unwrap_or_else(|| self.value_gen.next()); + self.emit_instruction(MirInstruction::Const { dst: dstv, value: super::ConstValue::Integer(1) })?; + return Ok(()); + } + // 1) Direct module function fallback: call by name if present + if let Some(ref module) = self.current_module { + if module.functions.contains_key(name) { + let dstv = dst.unwrap_or_else(|| self.value_gen.next()); + let name_const = self.value_gen.next(); + self.emit_instruction(MirInstruction::Const { dst: name_const, value: super::ConstValue::String(name.clone()) })?; + self.emit_instruction(MirInstruction::Call { dst: Some(dstv), func: name_const, callee: None, args: args.clone(), effects: EffectMask::IO })?; + self.annotate_call_result_from_func_name(dstv, name); + return Ok(()); + } + } + // 2) Unique static-method fallback: name+arity → Box.name/Arity + if let Some(cands) = self.static_method_index.get(name) { + let mut matches: Vec<(String, usize)> = cands + .iter() + .cloned() + .filter(|(_, ar)| *ar == arity_for_try) + .collect(); + if matches.len() == 1 { + let (bx, _arity) = matches.remove(0); + let func_name = format!("{}.{}{}", bx, name, format!("/{}", arity_for_try)); + // Emit legacy call directly to preserve behavior + let dstv = dst.unwrap_or_else(|| self.value_gen.next()); + let name_const = self.value_gen.next(); + self.emit_instruction(MirInstruction::Const { + dst: name_const, + value: super::ConstValue::String(func_name.clone()), + })?; + self.emit_instruction(MirInstruction::Call { + dst: Some(dstv), + func: name_const, + callee: None, + args: args.clone(), + effects: EffectMask::IO, + })?; + // annotate + self.annotate_call_result_from_func_name(dstv, func_name); + return Ok(()); + } + } + } + return Err(e); + } + }; + + // Emit resolve.choose for method callee (dev-only; default OFF) + if let Callee::Method { box_name, method, certainty, .. } = &callee { + let chosen = format!("{}.{}{}", box_name, method, format!("/{}", arity_for_try)); + let meta = serde_json::json!({ + "recv_cls": box_name, + "method": method, + "arity": arity_for_try, + "chosen": chosen, + "certainty": format!("{:?}", certainty), + "reason": "unified", + }); + super::observe::resolve::emit_choose(self, meta); + } // Validate call arguments call_unified::validate_call_args(&callee, &args)?; @@ -114,49 +219,10 @@ impl super::MirBuilder { args: Vec, ) -> Result<(), String> { match target { - CallTarget::Method { receiver, method, box_type } => { - // If receiver is a user-defined box, lower to function call: "Box.method/(1+arity)" with receiver as first arg - let mut is_user_box = false; - let mut class_name_opt: Option = None; - if let Some(bt) = box_type.clone() { class_name_opt = Some(bt); } - if class_name_opt.is_none() { - if let Some(cn) = self.value_origin_newbox.get(&receiver) { class_name_opt = Some(cn.clone()); } - } - if class_name_opt.is_none() { - if let Some(t) = self.value_types.get(&receiver) { - if let super::MirType::Box(bn) = t { class_name_opt = Some(bn.clone()); } - } - } - if let Some(cls) = class_name_opt.clone() { - // Prefer explicit registry of user-defined boxes when available - if self.user_defined_boxes.contains(&cls) { is_user_box = true; } - } - if is_user_box { - let cls = class_name_opt.unwrap(); - let arity = args.len(); // function name arity excludes 'me' - let fname = super::calls::function_lowering::generate_method_function_name(&cls, &method, arity); - let name_const = self.value_gen.next(); - self.emit_instruction(MirInstruction::Const { - dst: name_const, - value: super::ConstValue::String(fname), - })?; - let mut call_args = Vec::with_capacity(arity); - call_args.push(receiver); // pass 'me' first - call_args.extend(args.into_iter()); - // Allocate a destination if not provided - let actual_dst = if let Some(d) = dst { d } else { self.value_gen.next() }; - self.emit_instruction(MirInstruction::Call { - dst, - func: name_const, - callee: None, - args: call_args, - effects: EffectMask::READ.add(Effect::ReadHeap), - })?; - // Annotate result type/origin using lowered function signature - if let Some(d) = dst.or(Some(actual_dst)) { self.annotate_call_result_from_func_name(d, super::calls::function_lowering::generate_method_function_name(&cls, &method, arity)); } - return Ok(()); - } - // Else fall back to plugin/boxcall path (StringBox/ArrayBox/MapBox etc.) + CallTarget::Method { receiver, method, box_type: _ } => { + // LEGACY PATH (after unified migration): + // Instance→Function rewrite is centralized in unified call path. + // Legacy path no longer functionizes; always use Box/Plugin call here. self.emit_box_or_plugin_call(dst, receiver, method, None, args, EffectMask::IO) }, CallTarget::Constructor(box_type) => { @@ -400,6 +466,7 @@ impl super::MirBuilder { name: String, args: Vec, ) -> Result { + // dev trace removed if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") { let cur_fun = self.current_function.as_ref().map(|f| f.signature.name.clone()).unwrap_or_else(|| "".to_string()); eprintln!( @@ -440,19 +507,20 @@ impl super::MirBuilder { arg_values.push(self.build_expression(a)?); } - // Phase 3.2: Use unified call for basic functions like print - let use_unified = std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1"; - - if use_unified { - // New unified path - use emit_unified_call with Global target + // Special-case: global str(x) → x.str() に正規化(内部は関数へ統一される) + if name == "str" && arg_values.len() == 1 { let dst = self.value_gen.next(); - self.emit_unified_call( - Some(dst), - CallTarget::Global(name), - arg_values, - )?; - Ok(dst) - } else { + // Use unified method emission; downstream rewrite will functionize as needed + self.emit_method_call(Some(dst), arg_values[0], "str".to_string(), vec![])?; + return Ok(dst); + } + + // Phase 3.2: Unified call is default ON, but only use it for known builtins/externs. + let use_unified = super::calls::call_unified::is_unified_call_enabled() + && (super::call_resolution::is_builtin_function(&name) + || super::call_resolution::is_extern_function(&name)); + + if !use_unified { // Legacy path let dst = self.value_gen.next(); @@ -461,6 +529,7 @@ impl super::MirBuilder { let callee = match self.resolve_call_target(&name) { Ok(c) => c, Err(_e) => { + // dev trace removed // Fallback: if exactly one static method with this name and arity is known, call it. if let Some(cands) = self.static_method_index.get(&name) { let mut matches: Vec<(String, usize)> = cands @@ -517,6 +586,15 @@ impl super::MirBuilder { effects: EffectMask::READ.add(Effect::ReadHeap), })?; Ok(dst) + } else { + // Unified path for builtins/externs + let dst = self.value_gen.next(); + self.emit_unified_call( + Some(dst), + CallTarget::Global(name), + arg_values, + )?; + Ok(dst) } } diff --git a/src/mir/builder/calls/call_unified.rs b/src/mir/builder/calls/call_unified.rs index f943fc56..8014e739 100644 --- a/src/mir/builder/calls/call_unified.rs +++ b/src/mir/builder/calls/call_unified.rs @@ -13,7 +13,10 @@ use super::extern_calls; /// Check if unified call system is enabled pub fn is_unified_call_enabled() -> bool { - std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1" + match std::env::var("NYASH_MIR_UNIFIED_CALL").ok().as_deref().map(|s| s.to_ascii_lowercase()) { + Some(s) if s == "0" || s == "false" || s == "off" => false, + _ => true, // default ON during development; explicit opt-out supported + } } /// Convert CallTarget to Callee diff --git a/src/mir/builder/exprs_call.rs b/src/mir/builder/exprs_call.rs index cd53128b..58011cf6 100644 --- a/src/mir/builder/exprs_call.rs +++ b/src/mir/builder/exprs_call.rs @@ -15,7 +15,7 @@ impl super::MirBuilder { } // Phase 3.1: Use unified call with CallTarget::Value for indirect calls - let use_unified = std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1"; + let use_unified = super::calls::call_unified::is_unified_call_enabled(); if use_unified { // New unified path - use emit_unified_call with Value target diff --git a/src/mir/builder/lifecycle.rs b/src/mir/builder/lifecycle.rs index 8ee084b2..e0c7441d 100644 --- a/src/mir/builder/lifecycle.rs +++ b/src/mir/builder/lifecycle.rs @@ -1,4 +1,4 @@ -use super::{EffectMask, FunctionSignature, MirFunction, MirInstruction, MirModule, MirType, ValueId}; +use super::{EffectMask, FunctionSignature, MirFunction, MirInstruction, MirModule, MirType, ValueId, BasicBlockId, ConstValue}; use crate::ast::ASTNode; // Lifecycle routines extracted from builder.rs @@ -265,6 +265,29 @@ impl super::MirBuilder { module.add_function(function); + // Dev stub: provide condition_fn when missing to satisfy predicate calls in JSON lexers + // Returns integer 1 (truthy) and accepts one argument (unused). + if module.functions.get("condition_fn").is_none() { + let mut sig = FunctionSignature { + name: "condition_fn".to_string(), + params: vec![MirType::Integer], // accept one i64-like arg + return_type: MirType::Integer, + effects: EffectMask::PURE, + }; + let entry = BasicBlockId::new(0); + let mut f = MirFunction::new(sig, entry); + // parameter slot (unused in body) + let _param = f.next_value_id(); + f.params.push(_param); + // body: const 1; return it + let one = f.next_value_id(); + if let Some(bb) = f.get_block_mut(entry) { + bb.add_instruction(MirInstruction::Const { dst: one, value: ConstValue::Integer(1) }); + bb.add_instruction(MirInstruction::Return { value: Some(one) }); + } + module.add_function(f); + } + Ok(module) } } diff --git a/src/mir/builder/method_call_handlers.rs b/src/mir/builder/method_call_handlers.rs index 291a4efc..f81f0cb7 100644 --- a/src/mir/builder/method_call_handlers.rs +++ b/src/mir/builder/method_call_handlers.rs @@ -91,8 +91,7 @@ impl MirBuilder { method: String, arguments: &[ASTNode], ) -> Result { - // Correctness-first: pin receiver so it has a block-local def and can safely - // flow across branches/merges when method calls are used in conditions. + // 安全策: レシーバをピン留めしてブロック境界での未定義参照を防ぐ let object_value = self .pin_to_slot(object_value, "@recv") .unwrap_or(object_value); @@ -125,372 +124,16 @@ impl MirBuilder { if let MirType::Box(bn) = t { class_name_opt = Some(bn.clone()); } } } - // Instance→Function rewrite (obj.m(a) → Box.m/Arity(obj,a)) - // Phase 2 policy: Only rewrite when receiver class is Known (from origin propagation). - let class_known = self.value_origin_newbox.get(&object_value).is_some(); - // Rationale: - // - Keep language surface idiomatic (obj.method()), while executing - // deterministically as a direct function call. - // - Prod VM forbids user Instance BoxCall fallback by policy; this - // rewrite guarantees prod runs without runtime instance-dispatch. - // Control: - // NYASH_BUILDER_REWRITE_INSTANCE={1|true|on} → force enable - // NYASH_BUILDER_REWRITE_INSTANCE={0|false|off} → force disable - let rewrite_enabled = { - match std::env::var("NYASH_BUILDER_REWRITE_INSTANCE").ok().as_deref().map(|v| v.to_ascii_lowercase()) { - Some(ref s) if s == "0" || s == "false" || s == "off" => false, - Some(ref s) if s == "1" || s == "true" || s == "on" => true, - _ => { - // Default: ON (prod/dev/ci) unless明示OFF。再発防止のため常時関数化を優先。 - true - } - } - }; - // Emit resolve.try event (dev-only) before making a decision - if rewrite_enabled { - if let Some(ref module) = self.current_module { - let tail = format!(".{}{}", method, format!("/{}", arguments.len())); - let candidates: Vec = module - .functions - .keys() - .filter(|k| k.ends_with(&tail)) - .cloned() - .collect(); - let recv_cls = class_name_opt.clone().or_else(|| self.value_origin_newbox.get(&object_value).cloned()).unwrap_or_default(); - let meta = serde_json::json!({ - "recv_cls": recv_cls, - "method": method, - "arity": arguments.len(), - "candidates": candidates, - }); - let fn_name = self.current_function.as_ref().map(|f| f.signature.name.as_str()); - let region = self.debug_current_region_id(); - crate::debug::hub::emit( - "resolve", - "try", - fn_name, - region.as_deref(), - meta, - ); - } - } - // Early special-case: toString → stringify mapping when user function exists - if method == "toString" && arguments.len() == 0 { - if let Some(ref module) = self.current_module { - // Prefer class-qualified stringify if we can infer class - if let Some(cls_ts) = class_name_opt.clone() { - let stringify_name = crate::mir::builder::calls::function_lowering::generate_method_function_name(&cls_ts, "stringify", 0); - if module.functions.contains_key(&stringify_name) { - if super::utils::builder_debug_enabled() || std::env::var("NYASH_BUILDER_DEBUG").ok().as_deref() == Some("1") { - super::utils::builder_debug_log(&format!("(early) toString→stringify cls={} fname={}", cls_ts, stringify_name)); - } - // DebugHub emit: resolve.choose (early, class) - { - let meta = serde_json::json!({ - "recv_cls": cls_ts, - "method": "toString", - "arity": 0, - "chosen": stringify_name, - "reason": "toString-early-class", - }); - let fn_name = self.current_function.as_ref().map(|f| f.signature.name.as_str()); - let region = self.debug_current_region_id(); - crate::debug::hub::emit( - "resolve", - "choose", - fn_name, - region.as_deref(), - meta, - ); - } - let name_const = self.value_gen.next(); - self.emit_instruction(MirInstruction::Const { - dst: name_const, - value: crate::mir::builder::ConstValue::String(stringify_name.clone()), - })?; - let mut call_args = Vec::with_capacity(1); - call_args.push(object_value); - let dst = self.value_gen.next(); - self.emit_instruction(MirInstruction::Call { - dst: Some(dst), - func: name_const, - callee: None, - args: call_args, - effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap), - })?; - self.annotate_call_result_from_func_name(dst, &stringify_name); - return Ok(dst); - } - } - // Fallback: unique suffix ".stringify/0" in module - let mut cands: Vec = module - .functions - .keys() - .filter(|k| k.ends_with(".stringify/0")) - .cloned() - .collect(); - if cands.len() == 1 { - let fname = cands.remove(0); - if super::utils::builder_debug_enabled() || std::env::var("NYASH_BUILDER_DEBUG").ok().as_deref() == Some("1") { - super::utils::builder_debug_log(&format!("(early) toString→stringify unique-suffix fname={}", fname)); - } - // DebugHub emit: resolve.choose (early, unique) - { - let meta = serde_json::json!({ - "recv_cls": class_name_opt.clone().unwrap_or_default(), - "method": "toString", - "arity": 0, - "chosen": fname, - "reason": "toString-early-unique", - }); - let fn_name = self.current_function.as_ref().map(|f| f.signature.name.as_str()); - let region = self.debug_current_region_id(); - crate::debug::hub::emit( - "resolve", - "choose", - fn_name, - region.as_deref(), - meta, - ); - } - let name_const = self.value_gen.next(); - self.emit_instruction(MirInstruction::Const { - dst: name_const, - value: crate::mir::builder::ConstValue::String(fname.clone()), - })?; - let mut call_args = Vec::with_capacity(1); - call_args.push(object_value); - let dst = self.value_gen.next(); - self.emit_instruction(MirInstruction::Call { - dst: Some(dst), - func: name_const, - callee: None, - args: call_args, - effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap), - })?; - self.annotate_call_result_from_func_name(dst, &fname); - return Ok(dst); - } else if cands.len() > 1 { - // Deterministic tie-breaker: prefer JsonNode.stringify/0 over JsonNodeInstance.stringify/0 - if let Some(pos) = cands.iter().position(|n| n == "JsonNode.stringify/0") { - let fname = cands.remove(pos); - if super::utils::builder_debug_enabled() || std::env::var("NYASH_BUILDER_DEBUG").ok().as_deref() == Some("1") { - super::utils::builder_debug_log(&format!("(early) toString→stringify prefer JsonNode fname={}", fname)); - } - // DebugHub emit: resolve.choose (early, prefer-JsonNode) - { - let meta = serde_json::json!({ - "recv_cls": class_name_opt.clone().unwrap_or_default(), - "method": "toString", - "arity": 0, - "chosen": fname, - "reason": "toString-early-prefer-JsonNode", - }); - let fn_name = self.current_function.as_ref().map(|f| f.signature.name.as_str()); - let region = self.debug_current_region_id(); - crate::debug::hub::emit( - "resolve", - "choose", - fn_name, - region.as_deref(), - meta, - ); - } - let name_const = self.value_gen.next(); - self.emit_instruction(MirInstruction::Const { - dst: name_const, - value: crate::mir::builder::ConstValue::String(fname.clone()), - })?; - let mut call_args = Vec::with_capacity(1); - call_args.push(object_value); - let dst = self.value_gen.next(); - self.emit_instruction(MirInstruction::Call { - dst: Some(dst), - func: name_const, - callee: None, - args: call_args, - effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap), - })?; - self.annotate_call_result_from_func_name(dst, &fname); - return Ok(dst); - } - } - } - } - - if rewrite_enabled && class_known { - if let Some(cls) = class_name_opt.clone() { - let from_new_origin = self.value_origin_newbox.get(&object_value).is_some(); - let allow_new_origin = std::env::var("NYASH_DEV_REWRITE_NEW_ORIGIN").ok().as_deref() == Some("1"); - let is_user_box = self.user_defined_boxes.contains(&cls); - let fname = { - let arity = arg_values.len(); - crate::mir::builder::calls::function_lowering::generate_method_function_name(&cls, &method, arity) - }; - let module_has = if let Some(ref module) = self.current_module { module.functions.contains_key(&fname) } else { false }; - let allow_userbox_rewrite = std::env::var("NYASH_DEV_REWRITE_USERBOX").ok().as_deref() == Some("1"); - if (is_user_box && (module_has || allow_userbox_rewrite)) || (from_new_origin && allow_new_origin) { - let arity = arg_values.len(); // function name arity excludes 'me' - // Special-case: toString → stringify mapping (only when present) - if method == "toString" && arity == 0 { - if let Some(ref module) = self.current_module { - let stringify_name = crate::mir::builder::calls::function_lowering::generate_method_function_name(&cls, "stringify", 0); - if module.functions.contains_key(&stringify_name) { - if super::utils::builder_debug_enabled() || std::env::var("NYASH_BUILDER_DEBUG").ok().as_deref() == Some("1") { - super::utils::builder_debug_log(&format!("userbox toString→stringify cls={} fname={}", cls, stringify_name)); - } - let name_const = self.value_gen.next(); - self.emit_instruction(MirInstruction::Const { - dst: name_const, - value: crate::mir::builder::ConstValue::String(stringify_name.clone()), - })?; - let mut call_args = Vec::with_capacity(1); - call_args.push(object_value); - let dst = self.value_gen.next(); - self.emit_instruction(MirInstruction::Call { - dst: Some(dst), - func: name_const, - callee: None, - args: call_args, - effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap), - })?; - self.annotate_call_result_from_func_name(dst, &stringify_name); - return Ok(dst); - } - } - } - - // Default: unconditionally rewrite to Box.method/Arity. The target - // may be materialized later during lowering of the box; runtime - // resolution by name will succeed once the module is finalized. - let fname = fname.clone(); - if super::utils::builder_debug_enabled() || std::env::var("NYASH_BUILDER_DEBUG").ok().as_deref() == Some("1") { - super::utils::builder_debug_log(&format!("userbox method-call cls={} method={} fname={}", cls, method, fname)); - } - // Dev WARN when the function is not yet present (materialize pending) - if crate::config::env::cli_verbose() { - if let Some(ref module) = self.current_module { - if !module.functions.contains_key(&fname) { - eprintln!( - "[warn] rewrite (materialize pending): {} (class={}, method={}, arity={})", - fname, cls, method, arity - ); - } - } - } - let name_const = self.value_gen.next(); - self.emit_instruction(MirInstruction::Const { - dst: name_const, - value: crate::mir::builder::ConstValue::String(fname.clone()), - })?; - let mut call_args = Vec::with_capacity(arity + 1); - call_args.push(object_value); // 'me' - call_args.extend(arg_values.into_iter()); - let dst = self.value_gen.next(); - self.emit_instruction(MirInstruction::Call { - dst: Some(dst), - func: name_const, - callee: None, - args: call_args, - effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap), - })?; - // Annotate and emit resolve.choose - let chosen = format!("{}.{}{}", cls, method, format!("/{}", arity)); - self.annotate_call_result_from_func_name(dst, &chosen); - let meta = serde_json::json!({ - "recv_cls": cls, - "method": method, - "arity": arity, - "chosen": chosen, - "reason": "userbox-rewrite", - }); - let fn_name = self.current_function.as_ref().map(|f| f.signature.name.as_str()); - let region = self.debug_current_region_id(); - crate::debug::hub::emit( - "resolve", - "choose", - fn_name, - region.as_deref(), - meta, - ); - return Ok(dst); - } else { - // Not a user-defined box; fall through - } - } - } - - // Fallback (narrowed): when exactly one user-defined method matches by - // name/arity across the module, resolve to that even if class inference - // failed (defensive for PHI/branch cases). This preserves determinism - // because we require uniqueness and a user-defined box prefix. - if rewrite_enabled && class_known { - if let Some(ref module) = self.current_module { - let tail = format!(".{}{}", method, format!("/{}", arg_values.len())); - let mut cands: Vec = module - .functions - .keys() - .filter(|k| k.ends_with(&tail)) - .cloned() - .collect(); - if cands.len() == 1 { - let fname = cands.remove(0); - // sanity: ensure the box prefix looks like a user-defined box - if let Some((bx, _)) = fname.split_once('.') { - if self.user_defined_boxes.contains(bx) { - let name_const = self.value_gen.next(); - self.emit_instruction(MirInstruction::Const { - dst: name_const, - value: crate::mir::builder::ConstValue::String(fname.clone()), - })?; - let mut call_args = Vec::with_capacity(arg_values.len() + 1); - call_args.push(object_value); // 'me' - let arity_us = arg_values.len(); - call_args.extend(arg_values.into_iter()); - let dst = self.value_gen.next(); - self.emit_instruction(MirInstruction::Call { - dst: Some(dst), - func: name_const, - callee: None, - args: call_args, - effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap), - })?; - // Annotate and emit resolve.choose - self.annotate_call_result_from_func_name(dst, &fname); - let meta = serde_json::json!({ - "recv_cls": bx, - "method": method, - "arity": arity_us, - "chosen": fname, - "reason": "unique-suffix", - }); - let fn_name = self.current_function.as_ref().map(|f| f.signature.name.as_str()); - let region = self.debug_current_region_id(); - crate::debug::hub::emit( - "resolve", - "choose", - fn_name, - region.as_deref(), - meta, - ); - return Ok(dst); - } - } - } - } - } - - // Else fall back to plugin/boxcall path + // レガシー経路(BoxCall/Plugin)へ送る(安定優先・挙動不変)。 let result_id = self.value_gen.next(); self.emit_box_or_plugin_call( Some(result_id), object_value, - method, + method.clone(), None, arg_values, - crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap), + super::EffectMask::READ.add(super::Effect::ReadHeap), )?; - Ok(result_id) } } diff --git a/src/mir/builder/observe/README.md b/src/mir/builder/observe/README.md new file mode 100644 index 00000000..8252389a --- /dev/null +++ b/src/mir/builder/observe/README.md @@ -0,0 +1,23 @@ +# observe — Builder 観測(dev‑only/既定OFF) + +目的 +- Builder 内部の決定(resolve.try/choose, ssa.phi など)を JSONL で観測する。 +- 環境変数で明示有効化された時のみ動作(既定OFF)。言語仕様・実行結果は不変。 + +責務 +- ssa.rs: `emit_phi` — PHI の決定(pred の type/origin、dst の決定)を DebugHub へ emit。 +- resolve.rs: `emit_try` / `emit_choose` — メソッド解決の候補/最終選択を emit。 + +非責務(禁止) +- MIR 命令の生成/変更は行わない(副作用なし)。 +- 起源付与や型推論は origin 層に限定。 + +トグル(DebugHub 側でガード) +- `NYASH_DEBUG_ENABLE=1` +- `NYASH_DEBUG_KINDS=resolve,ssa` +- `NYASH_DEBUG_SINK=/path/to/file.jsonl` + +レイヤールール +- Allowed: DebugHub emit、Builder の読み取り(関数名/region_id/メタ)。 +- Forbidden: rewrite/origin の機能をここに持ち込まない。 + diff --git a/src/mir/builder/observe/mod.rs b/src/mir/builder/observe/mod.rs new file mode 100644 index 00000000..ddd77f18 --- /dev/null +++ b/src/mir/builder/observe/mod.rs @@ -0,0 +1,8 @@ +//! Builder observability helpers (dev‑only; default OFF) +//! +//! - ssa: PHI/SSA related debug emissions +//! - resolve: method resolution try/choose(既存呼び出しの置換は段階的に) + +pub mod ssa; +pub mod resolve; + diff --git a/src/mir/builder/observe/resolve.rs b/src/mir/builder/observe/resolve.rs new file mode 100644 index 00000000..a27febbd --- /dev/null +++ b/src/mir/builder/observe/resolve.rs @@ -0,0 +1,55 @@ +use super::super::MirBuilder; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::OnceLock; + +// Dev-only KPI: resolve.choose Known rate +static TOTAL_CHOOSE: AtomicUsize = AtomicUsize::new(0); +static KNOWN_CHOOSE: AtomicUsize = AtomicUsize::new(0); +static KPI_ENABLED: OnceLock = OnceLock::new(); +static SAMPLE_EVERY: OnceLock = OnceLock::new(); + +fn kpi_enabled() -> bool { + *KPI_ENABLED.get_or_init(|| std::env::var("NYASH_DEBUG_KPI_KNOWN").ok().as_deref() == Some("1")) +} + +fn sample_every() -> usize { + *SAMPLE_EVERY.get_or_init(|| { + std::env::var("NYASH_DEBUG_SAMPLE_EVERY") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0) + }) +} + +/// Dev‑only: emit a resolve.try event(candidates inspection)。 +pub(crate) fn emit_try(builder: &MirBuilder, meta: serde_json::Value) { + let fn_name = builder.current_function.as_ref().map(|f| f.signature.name.as_str()); + let region = builder.debug_current_region_id(); + crate::debug::hub::emit("resolve", "try", fn_name, region.as_deref(), meta); +} + +/// Dev‑only: emit a resolve.choose event(decision)。 +pub(crate) fn emit_choose(builder: &MirBuilder, meta: serde_json::Value) { + let fn_name = builder.current_function.as_ref().map(|f| f.signature.name.as_str()); + let region = builder.debug_current_region_id(); + // KPI (dev-only) + record_kpi(&meta); + crate::debug::hub::emit("resolve", "choose", fn_name, region.as_deref(), meta); +} + +/// Internal: Call from emit_choose wrapper to record KPI if enabled. +#[allow(dead_code)] +fn record_kpi(meta: &serde_json::Value) { + if !kpi_enabled() { return; } + let total = TOTAL_CHOOSE.fetch_add(1, Ordering::Relaxed) + 1; + let certainty = meta.get("certainty").and_then(|v| v.as_str()).unwrap_or(""); + if certainty == "Known" { + KNOWN_CHOOSE.fetch_add(1, Ordering::Relaxed); + } + let n = sample_every(); + if n > 0 && total % n == 0 { + let known = KNOWN_CHOOSE.load(Ordering::Relaxed); + let rate = if total > 0 { (known as f64) * 100.0 / (total as f64) } else { 0.0 }; + eprintln!("[NYASH-KPI] resolve.choose Known={} Total={} ({:.1}%)", known, total, rate); + } +} diff --git a/src/mir/builder/observe/ssa.rs b/src/mir/builder/observe/ssa.rs new file mode 100644 index 00000000..24ac2e8f --- /dev/null +++ b/src/mir/builder/observe/ssa.rs @@ -0,0 +1,43 @@ +use super::super::{BasicBlockId, MirBuilder, ValueId}; + +/// Emit a dev‑only JSONL event for a PHI decision. +/// Computes predecessor meta(type/origin)from the builder’s current maps. +pub(crate) fn emit_phi(builder: &MirBuilder, dst: ValueId, inputs: &Vec<(BasicBlockId, ValueId)>) { + // Respect env gates in hub; just build meta here. + let preds: Vec = inputs + .iter() + .map(|(bb, v)| { + let t = builder.value_types.get(v).cloned(); + let o = builder.value_origin_newbox.get(v).cloned(); + serde_json::json!({ + "bb": bb.0, + "v": v.0, + "type": t.as_ref().map(|tt| format!("{:?}", tt)).unwrap_or_default(), + "origin": o.unwrap_or_default(), + }) + }) + .collect(); + let decided_t = builder + .value_types + .get(&dst) + .cloned() + .map(|tt| format!("{:?}", tt)) + .unwrap_or_default(); + let decided_o = builder + .value_origin_newbox + .get(&dst) + .cloned() + .unwrap_or_default(); + let meta = serde_json::json!({ + "dst": dst.0, + "preds": preds, + "decided_type": decided_t, + "decided_origin": decided_o, + }); + let fn_name = builder + .current_function + .as_ref() + .map(|f| f.signature.name.as_str()); + let region = builder.debug_current_region_id(); + crate::debug::hub::emit("ssa", "phi", fn_name, region.as_deref(), meta); +} diff --git a/src/mir/builder/origin/README.md b/src/mir/builder/origin/README.md new file mode 100644 index 00000000..3eff5aa3 --- /dev/null +++ b/src/mir/builder/origin/README.md @@ -0,0 +1,26 @@ +# origin — Known 化(起源付与)/ PHI 伝播(軽量) + +目的(P0) +- 受け手(me/receiver)の「起源クラス」を最小限付与して Known 化する。 +- PHI で型/起源が全入力で一致する場合に限り、dst にメタを伝播する。 +- 仕様は不変(Fail‑Fast/フォールバック追加なし)。あくまで観測と最小補強のみ。 + +責務 +- infer.rs: me の起源付与(current_static_box もしくは関数名の Box プレフィックスから推測)。 +- phi.rs: PHI 伝播(全入力一致時に type/origin を dst へコピー)。 + +非責務(禁止) +- 観測(DebugHub emit)は observe 層に限定。ここから直接 emit しない。 +- 複雑な流量解析/型推論/ポリシー決定は行わない(P5 で検討)。 +- 命令生成(MirInstruction の追加)は原則禁止(メタデータのみ変更)。 + +簡易API(使用側から呼び出し) +- `annotate_me_origin(builder: &mut MirBuilder, me_id: ValueId)` + - me の ValueId に対し、分かる範囲で `value_origin_newbox` と `value_types(Box)` を設定。 +- `propagate_phi_meta(builder: &mut MirBuilder, dst: ValueId, inputs: &Vec<(BasicBlockId, ValueId)>)` + - `inputs` の型/起源が全て一致する場合のみ、`dst` にコピー。 + +レイヤールール +- Allowed: `MirBuilder` / `MirType` / `ValueId` に対するメタ操作。 +- Forbidden: observe / rewrite への依存、NYABI/VM 呼び出し、命令生成。 + diff --git a/src/mir/builder/origin/infer.rs b/src/mir/builder/origin/infer.rs new file mode 100644 index 00000000..95677e28 --- /dev/null +++ b/src/mir/builder/origin/infer.rs @@ -0,0 +1,25 @@ +use super::super::{MirBuilder, MirType, ValueId}; + +/// Annotate the origin of `me`/receiver with a Known class when分かる範囲のみ。 +/// - 優先: current_static_box(静的ボックスの文脈) +/// - 次点: 現在の関数名のプレフィックス("Class.method/Arity") +/// - それ以外: 付与せず(挙動不変) +pub(crate) fn annotate_me_origin(builder: &mut MirBuilder, me_id: ValueId) { + let mut cls: Option = None; + if let Some(c) = builder.current_static_box.clone() { + if !c.is_empty() { cls = Some(c); } + } + if cls.is_none() { + if let Some(ref fun) = builder.current_function { + if let Some(dot) = fun.signature.name.find('.') { + let c = fun.signature.name[..dot].to_string(); + if !c.is_empty() { cls = Some(c); } + } + } + } + if let Some(c) = cls { + // Record both origin class and a Box type hint for downstream passes(観測用)。 + builder.value_origin_newbox.insert(me_id, c.clone()); + builder.value_types.insert(me_id, MirType::Box(c)); + } +} diff --git a/src/mir/builder/origin/mod.rs b/src/mir/builder/origin/mod.rs new file mode 100644 index 00000000..864daa18 --- /dev/null +++ b/src/mir/builder/origin/mod.rs @@ -0,0 +1,13 @@ +//! Origin inference utilities (P0) +//! +//! Responsibility +//! - Attach and maintain simple "origin" metadata (receiver/me/class) for Known化。 +//! - Keep logic minimal and spec‑neutral(挙動不変)。 +//! +//! Modules +//! - infer: entry points for annotating origins(me/receiver/newbox) +//! - phi: lightweight propagation at PHI when全入力が一致 + +pub mod infer; +pub mod phi; + diff --git a/src/mir/builder/origin/phi.rs b/src/mir/builder/origin/phi.rs new file mode 100644 index 00000000..d79c0e37 --- /dev/null +++ b/src/mir/builder/origin/phi.rs @@ -0,0 +1,38 @@ +use super::super::{MirBuilder, MirInstruction, MirType, ValueId, BasicBlockId}; + +/// Lightweight propagation at PHI when all inputs agree(型/起源)。 +/// 仕様は不変: 一致時のみ dst にコピーする(不一致/未知は何もしない)。 +pub(crate) fn propagate_phi_meta( + builder: &mut MirBuilder, + dst: ValueId, + inputs: &Vec<(BasicBlockId, ValueId)>, +) { + // Type一致のときだけコピー + let mut common_ty: Option = None; + let mut ty_agree = true; + for (_bb, v) in inputs.iter() { + if let Some(t) = builder.value_types.get(v).cloned() { + match &common_ty { + None => common_ty = Some(t), + Some(ct) => { + if ct != &t { ty_agree = false; break; } + } + } + } else { ty_agree = false; break; } + } + if ty_agree { + if let Some(ct) = common_ty { builder.value_types.insert(dst, ct); } + } + // Origin一致のときだけコピー + let mut common_cls: Option = None; + let mut cls_agree = true; + for (_bb, v) in inputs.iter() { + if let Some(c) = builder.value_origin_newbox.get(v).cloned() { + match &common_cls { + None => common_cls = Some(c), + Some(cc) => { if cc != &c { cls_agree = false; break; } } + } + } else { cls_agree = false; break; } + } + if cls_agree { if let Some(cc) = common_cls { builder.value_origin_newbox.insert(dst, cc); } } +} diff --git a/src/mir/builder/rewrite/README.md b/src/mir/builder/rewrite/README.md new file mode 100644 index 00000000..492335ca --- /dev/null +++ b/src/mir/builder/rewrite/README.md @@ -0,0 +1,33 @@ +# rewrite — Known 経路の関数化 + 特殊規則(P1) + +目的 +- Known 受け手のメソッド呼び出し `obj.m(a)` を関数呼び出し `Class.m(me,obj,a)` に正規化し、実行系を単純化する。 +- 表示系の特殊規則(`toString` / `stringify` → 規範 `str`)を一箇所に集約する(互換維持)。 +- 仕様は不変。Union は観測のみで、Known のみ関数化対象。 + +責務 +- known.rs: Known 経路の instance→function 正規化(ユーザー Box のみ、既存ガード尊重)。 +- special.rs: `toString`/`stringify` → `str` の早期処理(Class.str/0 を優先、互換で stringify/0)。 + - `equals/1` もここに集約(Known 優先 → 一意候補のみ許容)。 +- 観測は observe 層に委譲(resolve.choose など)。 + +非責務(禁止) +- Union の強引な関数化(Unknown/曖昧なものは扱わない)。 +- 起源付与/型推論の実施(origin 層に限定)。 +- NYABI 呼び出しや VM 直接呼び出し。 + +API(呼び出し側から) +- `try_known_rewrite(builder, recv, class, method, args) -> Option>` +- `try_unique_suffix_rewrite(builder, recv, method, args) -> Option>` +- `try_known_or_unique(builder, recv, class_opt, method, args) -> Option>` +- `try_early_str_like(builder, recv, class_opt, method, arity) -> Option>` +- `try_special_equals(builder, recv, class_opt, method, args) -> Option>` + +レイヤールール +- Allowed: Builder のメタ参照/関数名生成、MirInstruction の生成(関数化結果)。 +- Forbidden: origin/observe のロジックを混在させない(必要時は呼び出しで連携)。 + +決定原則 +- Known のみ関数化(`value_origin_newbox` が根拠)。 +- 表示系は規範 `str` を優先、`stringify` は当面互換として許容。 +- すべての決定は dev 観測(resolve.try/choose)で可視化し、挙動は不変。 diff --git a/src/mir/builder/rewrite/known.rs b/src/mir/builder/rewrite/known.rs new file mode 100644 index 00000000..077c659b --- /dev/null +++ b/src/mir/builder/rewrite/known.rs @@ -0,0 +1,227 @@ +use super::super::{ConstValue, Effect, EffectMask, MirBuilder, MirInstruction, ValueId}; + +/// Gate: whether instance→function rewrite is enabled. +fn rewrite_enabled() -> bool { + match std::env::var("NYASH_BUILDER_REWRITE_INSTANCE") + .ok() + .as_deref() + .map(|v| v.to_ascii_lowercase()) + { + Some(ref s) if s == "0" || s == "false" || s == "off" => false, + Some(ref s) if s == "1" || s == "true" || s == "on" => true, + _ => true, // default ON (spec unchanged; dev can opt out) + } +} + +/// Try Known‑route instance→function rewrite. +/// 既存の安全ガード(user_defined/存在確認/ENV)を尊重して関数化する。 +pub(crate) fn try_known_rewrite( + builder: &mut MirBuilder, + object_value: ValueId, + cls: &str, + method: &str, + mut arg_values: Vec, +) -> Option> { + // Global gate + if !rewrite_enabled() { + return None; + } + // Receiver must be Known (origin 由来) + if builder.value_origin_newbox.get(&object_value).is_none() { + return None; + } + // Only user-defined boxes (plugin/core boxesは対象外) + if !builder.user_defined_boxes.contains(cls) { + return None; + } + // Policy gates(従来互換) + let allow_userbox_rewrite = std::env::var("NYASH_DEV_REWRITE_USERBOX").ok().as_deref() == Some("1"); + let allow_new_origin = std::env::var("NYASH_DEV_REWRITE_NEW_ORIGIN").ok().as_deref() == Some("1"); + let from_new_origin = builder.value_origin_newbox.get(&object_value).is_some(); + let arity = arg_values.len(); + let fname = crate::mir::builder::calls::function_lowering::generate_method_function_name(cls, method, arity); + let module_has = if let Some(ref module) = builder.current_module { module.functions.contains_key(&fname) } else { false }; + if !( (module_has || allow_userbox_rewrite) || (from_new_origin && allow_new_origin) ) { + return None; + } + // Materialize function call: pass 'me' first, then args + let name_const = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Const { dst: name_const, value: ConstValue::String(fname.clone()) }) { return Some(Err(e)); } + let mut call_args = Vec::with_capacity(arity + 1); + call_args.push(object_value); + call_args.append(&mut arg_values); + let dst = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Call { + dst: Some(dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap), + }) { return Some(Err(e)); } + // Annotate and emit choose + let chosen = fname.clone(); + builder.annotate_call_result_from_func_name(dst, &chosen); + let meta = serde_json::json!({ + "recv_cls": cls, + "method": method, + "arity": arity, + "chosen": chosen, + "reason": "userbox-rewrite", + "certainty": "Known", + }); + super::super::observe::resolve::emit_choose(builder, meta); + Some(Ok(dst)) +} + +/// Variant: try Known rewrite but honor a requested destination. +pub(crate) fn try_known_rewrite_to_dst( + builder: &mut MirBuilder, + want_dst: Option, + object_value: ValueId, + cls: &str, + method: &str, + mut arg_values: Vec, +) -> Option> { + if !rewrite_enabled() { return None; } + if builder.value_origin_newbox.get(&object_value).is_none() { return None; } + if !builder.user_defined_boxes.contains(cls) { return None; } + let allow_userbox_rewrite = std::env::var("NYASH_DEV_REWRITE_USERBOX").ok().as_deref() == Some("1"); + let allow_new_origin = std::env::var("NYASH_DEV_REWRITE_NEW_ORIGIN").ok().as_deref() == Some("1"); + let from_new_origin = builder.value_origin_newbox.get(&object_value).is_some(); + let arity = arg_values.len(); + let fname = crate::mir::builder::calls::function_lowering::generate_method_function_name(cls, method, arity); + let module_has = if let Some(ref module) = builder.current_module { module.functions.contains_key(&fname) } else { false }; + if !((module_has || allow_userbox_rewrite) || (from_new_origin && allow_new_origin)) { return None; } + let name_const = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Const { dst: name_const, value: ConstValue::String(fname.clone()) }) { return Some(Err(e)); } + let mut call_args = Vec::with_capacity(arity + 1); + call_args.push(object_value); + call_args.append(&mut arg_values); + let actual_dst = want_dst.unwrap_or_else(|| builder.value_gen.next()); + if let Err(e) = builder.emit_instruction(MirInstruction::Call { dst: Some(actual_dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap) }) { return Some(Err(e)); } + builder.annotate_call_result_from_func_name(actual_dst, &fname); + let meta = serde_json::json!({ + "recv_cls": cls, + "method": method, + "arity": arity, + "chosen": fname, + "reason": "userbox-rewrite", + "certainty": "Known", + }); + super::super::observe::resolve::emit_choose(builder, meta); + Some(Ok(actual_dst)) +} + +/// Fallback: when exactly one user-defined method matches by name/arity across the module, +/// resolve to that even if class inference failed. Deterministic via uniqueness and user-box prefix. +pub(crate) fn try_unique_suffix_rewrite( + builder: &mut MirBuilder, + object_value: ValueId, + method: &str, + mut arg_values: Vec, +) -> Option> { + if !rewrite_enabled() { + return None; + } + // Only attempt if receiver is Known (keeps behavior stable and avoids surprises) + if builder.value_origin_newbox.get(&object_value).is_none() { + return None; + } + let mut cands: Vec = builder.method_candidates(method, arg_values.len()); + if cands.len() != 1 { + return None; + } + let fname = cands.remove(0); + if let Some((bx, _)) = fname.split_once('.') { + if !builder.user_defined_boxes.contains(bx) { + return None; + } + } else { + return None; + } + let name_const = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Const { dst: name_const, value: ConstValue::String(fname.clone()) }) { return Some(Err(e)); } + let mut call_args = Vec::with_capacity(arg_values.len() + 1); + call_args.push(object_value); // 'me' + let arity_us = arg_values.len(); + call_args.append(&mut arg_values); + let dst = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Call { + dst: Some(dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap), + }) { return Some(Err(e)); } + builder.annotate_call_result_from_func_name(dst, &fname); + let meta = serde_json::json!({ + "recv_cls": builder.value_origin_newbox.get(&object_value).cloned().unwrap_or_default(), + "method": method, + "arity": arity_us, + "chosen": fname, + "reason": "unique-suffix", + "certainty": "Heuristic", + }); + super::super::observe::resolve::emit_choose(builder, meta); + Some(Ok(dst)) +} + +/// Variant: unique-suffix rewrite honoring requested destination. +pub(crate) fn try_unique_suffix_rewrite_to_dst( + builder: &mut MirBuilder, + want_dst: Option, + object_value: ValueId, + method: &str, + mut arg_values: Vec, +) -> Option> { + if !rewrite_enabled() { return None; } + if builder.value_origin_newbox.get(&object_value).is_none() { return None; } + let mut cands: Vec = builder.method_candidates(method, arg_values.len()); + if cands.len() != 1 { return None; } + let fname = cands.remove(0); + if let Some((bx, _)) = fname.split_once('.') { if !builder.user_defined_boxes.contains(bx) { return None; } } else { return None; } + let name_const = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Const { dst: name_const, value: ConstValue::String(fname.clone()) }) { return Some(Err(e)); } + let mut call_args = Vec::with_capacity(arg_values.len() + 1); + call_args.push(object_value); + let arity_us = arg_values.len(); + call_args.append(&mut arg_values); + let actual_dst = want_dst.unwrap_or_else(|| builder.value_gen.next()); + if let Err(e) = builder.emit_instruction(MirInstruction::Call { dst: Some(actual_dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap) }) { return Some(Err(e)); } + builder.annotate_call_result_from_func_name(actual_dst, &fname); + let meta = serde_json::json!({ + "recv_cls": builder.value_origin_newbox.get(&object_value).cloned().unwrap_or_default(), + "method": method, + "arity": arity_us, + "chosen": fname, + "reason": "unique-suffix", + "certainty": "Heuristic", + }); + super::super::observe::resolve::emit_choose(builder, meta); + Some(Ok(actual_dst)) +} + +/// Unified entry: try Known rewrite first, then unique-suffix fallback. +pub(crate) fn try_known_or_unique( + builder: &mut MirBuilder, + object_value: ValueId, + class_name_opt: &Option, + method: &str, + arg_values: Vec, +) -> Option> { + if let Some(cls) = class_name_opt.as_ref() { + if let Some(res) = try_known_rewrite(builder, object_value, cls, method, arg_values.clone()) { + return Some(res); + } + } + try_unique_suffix_rewrite(builder, object_value, method, arg_values) +} + +/// Variant: honor requested destination +pub(crate) fn try_known_or_unique_to_dst( + builder: &mut MirBuilder, + want_dst: Option, + object_value: ValueId, + class_name_opt: &Option, + method: &str, + arg_values: Vec, +) -> Option> { + if let Some(cls) = class_name_opt.as_ref() { + if let Some(res) = try_known_rewrite_to_dst(builder, want_dst, object_value, cls, method, arg_values.clone()) { + return Some(res); + } + } + try_unique_suffix_rewrite_to_dst(builder, want_dst, object_value, method, arg_values) +} diff --git a/src/mir/builder/rewrite/mod.rs b/src/mir/builder/rewrite/mod.rs new file mode 100644 index 00000000..02041f6b --- /dev/null +++ b/src/mir/builder/rewrite/mod.rs @@ -0,0 +1,10 @@ +//! Rewrite helpers (P1) +//! +//! Responsibility +//! - Known 経路の instance→function 正規化(obj.m → Class.m(me,…))。 +//! - 特殊規則(toString→str(互換:stringify), equals など)の集約。 +//! - 既定挙動は不変。dev 観測(resolve.try/choose)は observe 経由で発火。 + +pub mod known; +pub mod special; + diff --git a/src/mir/builder/rewrite/special.rs b/src/mir/builder/rewrite/special.rs new file mode 100644 index 00000000..a40bfa25 --- /dev/null +++ b/src/mir/builder/rewrite/special.rs @@ -0,0 +1,217 @@ +use super::super::{ConstValue, Effect, EffectMask, MirBuilder, MirInstruction}; + +/// Early special-case: toString/stringify → str(互換)を処理。 +/// 戻り値: Some(result_id) なら処理済み。None なら通常経路へ委譲。 +pub(crate) fn try_early_str_like( + builder: &mut MirBuilder, + object_value: super::super::ValueId, + class_name_opt: &Option, + method: &str, + arity: usize, +) -> Option> { + if !(method == "toString" || method == "stringify") || arity != 0 { + return None; + } + let module = match &builder.current_module { Some(m) => m, None => return None }; + // Prefer class-qualified str if we can infer class; fallback to stringify for互換 + if let Some(cls) = class_name_opt.clone() { + let str_name = crate::mir::builder::calls::function_lowering::generate_method_function_name(&cls, "str", 0); + let compat_stringify = crate::mir::builder::calls::function_lowering::generate_method_function_name(&cls, "stringify", 0); + let have_str = module.functions.contains_key(&str_name); + let have_compat = module.functions.contains_key(&compat_stringify); + if have_str || (!have_str && have_compat) { + let chosen = if have_str { str_name } else { compat_stringify }; + // emit choose (dev-only) + let meta = serde_json::json!({ + "recv_cls": cls, + "method": method, + "arity": 0, + "chosen": chosen, + "reason": if have_str { "toString-early-class-str" } else { "toString-early-class-stringify" }, + "certainty": "Known", + }); + super::super::observe::resolve::emit_choose(builder, meta); + let name_const = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Const { dst: name_const, value: ConstValue::String(chosen.clone()) }) { return Some(Err(e)); } + let mut call_args = Vec::with_capacity(1); + call_args.push(object_value); + let dst = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Call { + dst: Some(dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap), + }) { return Some(Err(e)); } + builder.annotate_call_result_from_func_name(dst, &chosen); + return Some(Ok(dst)); + } + } + // Unique suffix fallback: prefer *.str/0, else *.stringify/0(両方ある場合は JsonNode.str/0 を優先) + // Merge candidates from tails: ".str/0" and ".stringify/0" + let mut cands: Vec = builder.method_candidates_tail(".str/0"); + let mut more = builder.method_candidates_tail(".stringify/0"); + cands.append(&mut more); + if cands.len() == 1 { + let fname = cands.remove(0); + let meta = serde_json::json!({ + "recv_cls": class_name_opt.clone().unwrap_or_default(), + "method": method, + "arity": 0, + "chosen": fname, + "reason": "toString-early-unique", + "certainty": "Heuristic", + }); + super::super::observe::resolve::emit_choose(builder, meta); + let name_const = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Const { dst: name_const, value: ConstValue::String(fname.clone()) }) { return Some(Err(e)); } + let mut call_args = Vec::with_capacity(1); + call_args.push(object_value); + let dst = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Call { dst: Some(dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap), }) { return Some(Err(e)); } + builder.annotate_call_result_from_func_name(dst, &fname); + return Some(Ok(dst)); + } else if cands.len() > 1 { + if let Some(pos) = cands.iter().position(|n| n == "JsonNode.str/0") { + let fname = cands.remove(pos); + let meta = serde_json::json!({ + "recv_cls": class_name_opt.clone().unwrap_or_default(), + "method": method, + "arity": 0, + "chosen": fname, + "reason": "toString-early-prefer-JsonNode", + "certainty": "Heuristic", + }); + super::super::observe::resolve::emit_choose(builder, meta); + let name_const = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Const { dst: name_const, value: ConstValue::String(fname.clone()) }) { return Some(Err(e)); } + let mut call_args = Vec::with_capacity(1); + call_args.push(object_value); + let dst = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Call { dst: Some(dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap), }) { return Some(Err(e)); } + builder.annotate_call_result_from_func_name(dst, &fname); + return Some(Ok(dst)); + } + } + None +} + +/// Special-case for equals/1: prefer Known rewrite; otherwise allow unique-suffix fallback +/// when it is deterministic (single candidate). This centralizes equals handling. +pub(crate) fn try_special_equals( + builder: &mut MirBuilder, + object_value: super::super::ValueId, + class_name_opt: &Option, + method: &str, + args: Vec, +) -> Option> { + if method != "equals" || args.len() != 1 { return None; } + // First, Known rewrite if possible + if let Some(cls) = class_name_opt.as_ref() { + if let Some(res) = super::known::try_known_rewrite(builder, object_value, cls, method, args.clone()) { + return Some(res); + } + } + // Next, try unique-suffix fallback only for equals/1 + super::known::try_unique_suffix_rewrite(builder, object_value, method, args) +} + +/// To-dst variant: early str-like with a requested destination +pub(crate) fn try_early_str_like_to_dst( + builder: &mut MirBuilder, + want_dst: Option, + object_value: super::super::ValueId, + class_name_opt: &Option, + method: &str, + arity: usize, +) -> Option> { + if !(method == "toString" || method == "stringify") || arity != 0 { + return None; + } + let module = match &builder.current_module { Some(m) => m, None => return None }; + if let Some(cls) = class_name_opt.clone() { + let str_name = crate::mir::builder::calls::function_lowering::generate_method_function_name(&cls, "str", 0); + let compat_stringify = crate::mir::builder::calls::function_lowering::generate_method_function_name(&cls, "stringify", 0); + let have_str = module.functions.contains_key(&str_name); + let have_compat = module.functions.contains_key(&compat_stringify); + if have_str || (!have_str && have_compat) { + let chosen = if have_str { str_name } else { compat_stringify }; + let meta = serde_json::json!({ + "recv_cls": cls, + "method": method, + "arity": 0, + "chosen": chosen, + "reason": if have_str { "toString-early-class-str" } else { "toString-early-class-stringify" }, + "certainty": "Known", + }); + super::super::observe::resolve::emit_choose(builder, meta); + let name_const = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Const { dst: name_const, value: ConstValue::String(chosen.clone()) }) { return Some(Err(e)); } + let mut call_args = Vec::with_capacity(1); + call_args.push(object_value); + let actual_dst = want_dst.unwrap_or_else(|| builder.value_gen.next()); + if let Err(e) = builder.emit_instruction(MirInstruction::Call { dst: Some(actual_dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap), }) { return Some(Err(e)); } + builder.annotate_call_result_from_func_name(actual_dst, &chosen); + return Some(Ok(actual_dst)); + } + } + let mut cands: Vec = builder.method_candidates_tail(".str/0"); + let mut more = builder.method_candidates_tail(".stringify/0"); + cands.append(&mut more); + if cands.len() == 1 { + let fname = cands.remove(0); + let meta = serde_json::json!({ + "recv_cls": class_name_opt.clone().unwrap_or_default(), + "method": method, + "arity": 0, + "chosen": fname, + "reason": "toString-early-unique", + "certainty": "Heuristic", + }); + super::super::observe::resolve::emit_choose(builder, meta); + let name_const = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Const { dst: name_const, value: ConstValue::String(fname.clone()) }) { return Some(Err(e)); } + let mut call_args = Vec::with_capacity(1); + call_args.push(object_value); + let actual_dst = want_dst.unwrap_or_else(|| builder.value_gen.next()); + if let Err(e) = builder.emit_instruction(MirInstruction::Call { dst: Some(actual_dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap), }) { return Some(Err(e)); } + builder.annotate_call_result_from_func_name(actual_dst, &fname); + return Some(Ok(actual_dst)); + } else if cands.len() > 1 { + if let Some(pos) = cands.iter().position(|n| n == "JsonNode.str/0") { + let fname = cands.remove(pos); + let meta = serde_json::json!({ + "recv_cls": class_name_opt.clone().unwrap_or_default(), + "method": method, + "arity": 0, + "chosen": fname, + "reason": "toString-early-prefer-JsonNode", + "certainty": "Heuristic", + }); + super::super::observe::resolve::emit_choose(builder, meta); + let name_const = builder.value_gen.next(); + if let Err(e) = builder.emit_instruction(MirInstruction::Const { dst: name_const, value: ConstValue::String(fname.clone()) }) { return Some(Err(e)); } + let mut call_args = Vec::with_capacity(1); + call_args.push(object_value); + let actual_dst = want_dst.unwrap_or_else(|| builder.value_gen.next()); + if let Err(e) = builder.emit_instruction(MirInstruction::Call { dst: Some(actual_dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap), }) { return Some(Err(e)); } + builder.annotate_call_result_from_func_name(actual_dst, &fname); + return Some(Ok(actual_dst)); + } + } + None +} + +/// To-dst variant: equals/1 consolidation with requested destination +pub(crate) fn try_special_equals_to_dst( + builder: &mut MirBuilder, + want_dst: Option, + object_value: super::super::ValueId, + class_name_opt: &Option, + method: &str, + args: Vec, +) -> Option> { + if method != "equals" || args.len() != 1 { return None; } + if let Some(cls) = class_name_opt.as_ref() { + if let Some(res) = super::known::try_known_rewrite_to_dst(builder, want_dst, object_value, cls, method, args.clone()) { + return Some(res); + } + } + super::known::try_unique_suffix_rewrite_to_dst(builder, want_dst, object_value, method, args) +} diff --git a/src/mir/builder/stmts.rs b/src/mir/builder/stmts.rs index 748406ec..fb59b0cf 100644 --- a/src/mir/builder/stmts.rs +++ b/src/mir/builder/stmts.rs @@ -124,7 +124,7 @@ impl super::MirBuilder { super::utils::builder_debug_log(&format!("fallback print value={}", value)); // Phase 3.2: Use unified call for print statements - let use_unified = std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1"; + let use_unified = super::calls::call_unified::is_unified_call_enabled(); if use_unified { // New unified path - treat print as global function @@ -185,14 +185,19 @@ impl super::MirBuilder { ) -> Result { let mut last_value = None; for (i, var_name) in variables.iter().enumerate() { - let value_id = if i < initial_values.len() && initial_values[i].is_some() { + let var_id = if i < initial_values.len() && initial_values[i].is_some() { + // Use initializer's ValueId directly to avoid SSA aliasing/undefined use let init_expr = initial_values[i].as_ref().unwrap(); - self.build_expression(*init_expr.clone())? + let init_val = self.build_expression(*init_expr.clone())?; + init_val } else { - self.value_gen.next() + // Create a concrete register for uninitialized locals (Void) + let vid = self.value_gen.next(); + self.emit_instruction(MirInstruction::Const { dst: vid, value: ConstValue::Void })?; + vid }; - self.variable_map.insert(var_name.clone(), value_id); - last_value = Some(value_id); + self.variable_map.insert(var_name.clone(), var_id); + last_value = Some(var_id); } Ok(last_value.unwrap_or_else(|| self.value_gen.next())) } @@ -314,6 +319,8 @@ impl super::MirBuilder { value: ConstValue::String(me_tag), })?; self.variable_map.insert("me".to_string(), me_value); + // P0: Known 化 — 分かる範囲で me の起源クラスを付与(挙動不変)。 + super::origin::infer::annotate_me_origin(self, me_value); Ok(me_value) } } diff --git a/src/mir/builder/utils.rs b/src/mir/builder/utils.rs index 9931d3ad..59cea6f7 100644 --- a/src/mir/builder/utils.rs +++ b/src/mir/builder/utils.rs @@ -76,31 +76,31 @@ impl super::MirBuilder { args: Vec, effects: super::EffectMask, ) -> Result<(), String> { - // Check environment variable for unified call usage - let use_unified = std::env::var("NYASH_MIR_UNIFIED_CALL") - .unwrap_or_else(|_| "0".to_string()) != "0"; - - if use_unified { - // Use unified call emission for BoxCall - // First, try to determine the box type - let mut box_type: Option = self.value_origin_newbox.get(&box_val).cloned(); - if box_type.is_none() { - if let Some(t) = self.value_types.get(&box_val) { - match t { - super::MirType::String => box_type = Some("StringBox".to_string()), - super::MirType::Box(name) => box_type = Some(name.clone()), - _ => {} - } + // Check environment variable for unified call usage, with safe overrides for core/user boxes + let use_unified_env = super::calls::call_unified::is_unified_call_enabled(); + // First, try to determine the box type + let mut box_type: Option = self.value_origin_newbox.get(&box_val).cloned(); + if box_type.is_none() { + if let Some(t) = self.value_types.get(&box_val) { + match t { + super::MirType::String => box_type = Some("StringBox".to_string()), + super::MirType::Box(name) => box_type = Some(name.clone()), + _ => {} } } - - // Use emit_unified_call with Method target + } + // Prefer legacy BoxCall for core collection boxes and user instance boxes (stability first) + let prefer_legacy = match box_type.as_deref() { + Some("ArrayBox") | Some("MapBox") | Some("StringBox") => true, + Some(bt) => !bt.ends_with("Box"), // user instance class name (e.g., JsonTokenizer) + None => false, + }; + if use_unified_env && !prefer_legacy { let target = super::builder_calls::CallTarget::Method { box_type, method: method.clone(), receiver: box_val, }; - return self.emit_unified_call(dst, target, args); } diff --git a/src/runner/cli_directives.rs b/src/runner/cli_directives.rs index 7844254c..f9dc9023 100644 --- a/src/runner/cli_directives.rs +++ b/src/runner/cli_directives.rs @@ -55,5 +55,131 @@ pub(super) fn apply_cli_directives_from_source( } // Lint: enforce fields at top-of-box (delegated) - super::pipeline::lint_fields_top(code, strict_fields, verbose) + super::pipeline::lint_fields_top(code, strict_fields, verbose)?; + + // Dev-only guards (strict but opt-in via env) + // 1) ASI strict: disallow binary operator at end-of-line (line continuation) + if std::env::var("NYASH_ASI_STRICT").ok().as_deref() == Some("1") { + // operators to check (suffixes) + const OP2: [&str; 6] = ["==", "!=", "<=", ">=", "&&", "||"]; + const OP1: [&str; 7] = ["+", "-", "*", "/", "%", "<", ">"]; + for (i, line) in code.lines().enumerate() { + let l = line.trim_end(); + if l.is_empty() { continue; } + let mut bad = false; + for op in OP2.iter() { if l.ends_with(op) { bad = true; break; } } + if !bad { for op in OP1.iter() { if l.ends_with(op) { bad = true; break; } } } + if bad { + return Err(format!("Parse error: Strict ASI violation — line {} ends with operator", i + 1)); + } + } + } + + // 2) '+' mixed types (String and Number) error (opt-in) + if std::env::var("NYASH_PLUS_MIX_ERROR").ok().as_deref() == Some("1") { + for (i, line) in code.lines().enumerate() { + if let Some((ltok, rtok)) = find_plus_operands(line) { + let left_is_num = is_number_literal(ltok); + let right_is_str = is_string_literal(rtok); + let left_is_str = is_string_literal(ltok); + let right_is_num = is_number_literal(rtok); + if (left_is_num && right_is_str) || (left_is_str && right_is_num) { + return Err(format!("Type error: '+' mixed String and Number at line {} (use str()/explicit conversion)", i + 1)); + } + } + } + } + + // 3) '==' on likely Box variables: emit guidance to use equals() (opt-in) + if std::env::var("NYASH_BOX_EQ_GUIDE_ERROR").ok().as_deref() == Some("1") { + for (i, line) in code.lines().enumerate() { + let l = line; + let mut idx = 0usize; + while let Some(pos) = l[idx..].find("==") { + let at = idx + pos; + // find left token end and right token start + let (left_ok, right_ok) = (peek_ident_left(l, at), peek_ident_right(l, at + 2)); + if left_ok && right_ok { + return Err(format!("Type error: '==' on boxes — use equals() (line {})", i + 1)); + } + idx = at + 2; + } + } + } + + Ok(()) +} + +// ---- Helpers (very small, no external deps) ---- +fn is_number_literal(s: &str) -> bool { + let t = s.trim(); + !t.is_empty() && t.chars().all(|c| c.is_ascii_digit()) +} +fn is_string_literal(s: &str) -> bool { + let t = s.trim(); + t.starts_with('"') +} + +fn find_plus_operands(line: &str) -> Option<(&str, &str)> { + let bytes = line.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] as char == '+' { + // extract left token + let mut l = i; + while l > 0 && bytes[l - 1].is_ascii_whitespace() { l -= 1; } + let mut lstart = l; + while lstart > 0 { + let c = bytes[lstart - 1] as char; + if c.is_ascii_alphanumeric() || c == '_' || c == '"' { lstart -= 1; } else { break; } + } + let left = &line[lstart..l]; + // extract right token + let mut r = i + 1; + while r < bytes.len() && bytes[r].is_ascii_whitespace() { r += 1; } + let mut rend = r; + while rend < bytes.len() { + let c = bytes[rend] as char; + if c.is_ascii_alphanumeric() || c == '_' || c == '"' { rend += 1; } else { break; } + } + if r <= rend { let right = &line[r..rend]; return Some((left, right)); } + return None; + } + i += 1; + } + None +} + +fn peek_ident_left(s: &str, pos: usize) -> bool { + // scan left for first non-space token end, then back to token start + let bytes = s.as_bytes(); + if pos == 0 { return false; } + let mut i = pos; + // skip spaces + while i > 0 && bytes[i - 1].is_ascii_whitespace() { i -= 1; } + if i == 0 { return false; } + // now consume identifier chars backwards + let mut j = i; + while j > 0 { + let c = bytes[j - 1] as char; + if c.is_ascii_alphanumeric() || c == '_' { j -= 1; } else { break; } + } + if j == i { return false; } + // ensure not starting with digit only (avoid numeric literal) + let tok = &s[j..i]; + !tok.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) +} +fn peek_ident_right(s: &str, pos: usize) -> bool { + let bytes = s.as_bytes(); + let mut i = pos; + while i < bytes.len() && bytes[i].is_ascii_whitespace() { i += 1; } + if i >= bytes.len() { return false; } + let mut j = i; + while j < bytes.len() { + let c = bytes[j] as char; + if c.is_ascii_alphanumeric() || c == '_' { j += 1; } else { break; } + } + if j == i { return false; } + let tok = &s[i..j]; + !tok.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) } diff --git a/src/runner/mir_json_emit.rs b/src/runner/mir_json_emit.rs index 2af6b200..160bdbe0 100644 --- a/src/runner/mir_json_emit.rs +++ b/src/runner/mir_json_emit.rs @@ -310,7 +310,10 @@ pub fn emit_mir_json_for_harness( dst, func, callee, args, effects, .. } => { // Phase 15.5: Unified Call support with environment variable control - let use_unified = std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1"; + let use_unified = match std::env::var("NYASH_MIR_UNIFIED_CALL").ok().as_deref().map(|s| s.to_ascii_lowercase()) { + Some(s) if s == "0" || s == "false" || s == "off" => false, + _ => true, + }; if use_unified && callee.is_some() { // v1: Unified mir_call format @@ -435,7 +438,10 @@ pub fn emit_mir_json_for_harness( // Phase 15.5: JSON v1 schema with environment variable control let use_v1_schema = std::env::var("NYASH_JSON_SCHEMA_V1").unwrap_or_default() == "1" - || std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1"; + || match std::env::var("NYASH_MIR_UNIFIED_CALL").ok().as_deref().map(|s| s.to_ascii_lowercase()) { + Some(s) if s == "0" || s == "false" || s == "off" => false, + _ => true, + }; let root = if use_v1_schema { create_json_v1_root(json!(funs)) diff --git a/src/runner/modes/bench.rs b/src/runner/modes/bench.rs index 0dfee738..c6d4e5b4 100644 --- a/src/runner/modes/bench.rs +++ b/src/runner/modes/bench.rs @@ -1,7 +1,7 @@ +#![cfg(feature = "interpreter-legacy")] + use super::super::NyashRunner; -use nyash_rust::{ - backend::VM, interpreter::NyashInterpreter, mir::MirCompiler, parser::NyashParser, -}; +use nyash_rust::{backend::VM, interpreter::NyashInterpreter, mir::MirCompiler, parser::NyashParser}; impl NyashRunner { /// Execute benchmark mode (split) @@ -241,4 +241,3 @@ impl NyashRunner { Ok(()) } } -#![cfg(feature = "vm-legacy")] diff --git a/src/runner/modes/common.rs b/src/runner/modes/common.rs index ddadbb81..33765c62 100644 --- a/src/runner/modes/common.rs +++ b/src/runner/modes/common.rs @@ -1,6 +1,9 @@ use super::super::NyashRunner; use crate::runner::json_v0_bridge; +#[cfg(feature = "interpreter-legacy")] use nyash_rust::{interpreter::NyashInterpreter, parser::NyashParser}; +#[cfg(not(feature = "interpreter-legacy"))] +use nyash_rust::parser::NyashParser; // Use the library crate's plugin init module rather than the bin crate root use crate::cli_v; use crate::runner::pipeline::{resolve_using_target, suggest_in_base}; @@ -13,6 +16,7 @@ use std::{fs, process}; // (moved) suggest_in_base is now in runner/pipeline.rs +#[cfg(feature = "interpreter-legacy")] impl NyashRunner { // legacy run_file_legacy removed (was commented out) @@ -289,3 +293,12 @@ impl NyashRunner { } } } + +#[cfg(not(feature = "interpreter-legacy"))] +impl NyashRunner { + /// Interpreter backend is disabled in default builds. Use `--backend vm` or `--backend llvm`. + pub(crate) fn execute_nyash_file(&self, _filename: &str) { + eprintln!("❌ Interpreter backend (AST) is disabled. Build with --features interpreter-legacy to enable, or use --backend vm/llvm."); + std::process::exit(1); + } +} diff --git a/src/runner/modes/common_util/exec.rs b/src/runner/modes/common_util/exec.rs index 4f7b6dd3..ce4d70f3 100644 --- a/src/runner/modes/common_util/exec.rs +++ b/src/runner/modes/common_util/exec.rs @@ -103,7 +103,8 @@ pub fn ny_llvmc_emit_exe_lib( .arg("exe") .arg("--out") .arg(exe_out); - if let Some(dir) = nyrt_dir { cmd.arg("--nyrt").arg(dir); } else { cmd.arg("--nyrt").arg("target/release"); } + let default_nyrt = std::env::var("NYASH_EMIT_EXE_NYRT") .ok() .or_else(|| std::env::var("NYASH_ROOT").ok().map(|r| format!("{}/target/release", r))) .unwrap_or_else(|| "target/release".to_string()); + if let Some(dir) = nyrt_dir { cmd.arg("--nyrt").arg(dir); } else { cmd.arg("--nyrt").arg(default_nyrt); } if let Some(flags) = extra_libs { if !flags.trim().is_empty() { cmd.arg("--libs").arg(flags); } } let status = cmd.status().map_err(|e| { let prog_path = std::path::Path::new(cmd.get_program()); @@ -146,7 +147,8 @@ pub fn ny_llvmc_emit_exe_bin( .arg("exe") .arg("--out") .arg(exe_out); - if let Some(dir) = nyrt_dir { cmd.arg("--nyrt").arg(dir); } else { cmd.arg("--nyrt").arg("target/release"); } + let default_nyrt = std::env::var("NYASH_EMIT_EXE_NYRT") .ok() .or_else(|| std::env::var("NYASH_ROOT").ok().map(|r| format!("{}/target/release", r))) .unwrap_or_else(|| "target/release".to_string()); + if let Some(dir) = nyrt_dir { cmd.arg("--nyrt").arg(dir); } else { cmd.arg("--nyrt").arg(default_nyrt); } if let Some(flags) = extra_libs { if !flags.trim().is_empty() { cmd.arg("--libs").arg(flags); } } let status = cmd.status().map_err(|e| { let prog_path = std::path::Path::new(cmd.get_program()); diff --git a/src/tests/functionbox_call_tests.rs b/src/tests/functionbox_call_tests.rs index d0748ca9..86246bd3 100644 --- a/src/tests/functionbox_call_tests.rs +++ b/src/tests/functionbox_call_tests.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "interpreter-legacy")] + use crate::interpreter::NyashInterpreter; use crate::ast::ASTNode; use crate::box_trait::{NyashBox, IntegerBox}; @@ -62,4 +64,3 @@ fn functionbox_call_via_field() { let ib = out.as_any().downcast_ref::().expect("integer ret"); assert_eq!(ib.value, 7); } - diff --git a/src/tests/refcell_assignment_test.rs b/src/tests/refcell_assignment_test.rs index e26c26f7..25c1fada 100644 --- a/src/tests/refcell_assignment_test.rs +++ b/src/tests/refcell_assignment_test.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "interpreter-legacy")] + use crate::interpreter::NyashInterpreter; use crate::ast::{ASTNode, LiteralValue}; use crate::box_trait::{NyashBox, IntegerBox}; diff --git a/src/tests/vm_bitops_test.rs b/src/tests/vm_bitops_test.rs index 92dea40c..d049e24c 100644 --- a/src/tests/vm_bitops_test.rs +++ b/src/tests/vm_bitops_test.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "interpreter-legacy")] + use crate::parser::NyashParser; use crate::interpreter::NyashInterpreter; diff --git a/tools/dev/check_builder_layers.sh b/tools/dev/check_builder_layers.sh new file mode 100644 index 00000000..1dd39314 --- /dev/null +++ b/tools/dev/check_builder_layers.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$root_dir" + +viol=0 + +echo "[guard] Checking builder layer dependencies (origin→observe→rewrite)" + +# Rule 1: origin/* must NOT import observe or rewrite +while IFS= read -r -d '' f; do + if rg -n "use\\s+crate::mir::builder::(observe|rewrite)" "$f" >/dev/null; then + echo "[ERROR] origin-layer import violation: $f" + rg -n "use\\s+crate::mir::builder::(observe|rewrite)" "$f" || true + viol=$((viol+1)) + fi +done < <(find src/mir/builder/origin -type f -name '*.rs' -print0 2>/dev/null || true) + +# Rule 2: observe/* must NOT import rewrite or origin +while IFS= read -r -d '' f; do + if rg -n "use\\s+crate::mir::builder::(rewrite|origin)" "$f" >/dev/null; then + echo "[ERROR] observe-layer import violation: $f" + rg -n "use\\s+crate::mir::builder::(rewrite|origin)" "$f" || true + viol=$((viol+1)) + fi +done < <(find src/mir/builder/observe -type f -name '*.rs' -print0 2>/dev/null || true) + +# Rule 3: rewrite/* must NOT import origin (observe is allowed) +while IFS= read -r -d '' f; do + if rg -n "use\\s+crate::mir::builder::origin" "$f" >/dev/null; then + echo "[ERROR] rewrite-layer import violation: $f" + rg -n "use\\s+crate::mir::builder::origin" "$f" || true + viol=$((viol+1)) + fi +done < <(find src/mir/builder/rewrite -type f -name '*.rs' -print0 2>/dev/null || true) + +if [[ $viol -gt 0 ]]; then + echo "[guard] FAILED: $viol violation(s) detected" + exit 1 +else + echo "[guard] OK: No violations" +fi + diff --git a/tools/dev_env.sh b/tools/dev_env.sh index 9feb9398..90eadaa5 100644 --- a/tools/dev_env.sh +++ b/tools/dev_env.sh @@ -11,18 +11,23 @@ set -euo pipefail activate_pyvm() { - export NYASH_DISABLE_PLUGINS=1 + unset NYASH_DISABLE_PLUGINS || true export NYASH_VM_USE_PY=1 export NYASH_PIPE_USE_PYVM=1 export NYASH_NY_COMPILER_TIMEOUT_MS=${NYASH_NY_COMPILER_TIMEOUT_MS:-2000} - echo "[dev-env] PyVM profile activated" >&2 + # Unified Call: default ON (explicit), and suppress legacy rewrite to avoid duplication + export NYASH_MIR_UNIFIED_CALL=${NYASH_MIR_UNIFIED_CALL:-1} + export NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1 + echo "[dev-env] PyVM profile activated (plugins ON)" >&2 } activate_bridge() { - export NYASH_DISABLE_PLUGINS=1 + unset NYASH_DISABLE_PLUGINS || true unset NYASH_VM_USE_PY || true export NYASH_NY_COMPILER_TIMEOUT_MS=${NYASH_NY_COMPILER_TIMEOUT_MS:-2000} - echo "[dev-env] Bridge profile activated (interpreter for pipe)" >&2 + export NYASH_MIR_UNIFIED_CALL=${NYASH_MIR_UNIFIED_CALL:-1} + export NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1 + echo "[dev-env] Bridge profile activated (interpreter for pipe; plugins ON)" >&2 } reset_env() { @@ -30,6 +35,8 @@ reset_env() { unset NYASH_PIPE_USE_PYVM || true unset NYASH_DISABLE_PLUGINS || true unset NYASH_NY_COMPILER_TIMEOUT_MS || true + unset NYASH_MIR_UNIFIED_CALL || true + unset NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE || true unset NYASH_MIR_NO_PHI || true unset NYASH_VERIFY_ALLOW_NO_PHI || true unset NYASH_LLVM_USE_HARNESS || true @@ -56,6 +63,9 @@ activate_opbox() { export NYASH_BUILDER_OPERATOR_BOX_COMPARE_CALL=1 export NYASH_BUILDER_OPERATOR_BOX_ADD_CALL=1 export NYASH_BUILDER_OPERATOR_BOX_ALL_CALL=1 + # Unified call and legacy suppression + export NYASH_MIR_UNIFIED_CALL=${NYASH_MIR_UNIFIED_CALL:-1} + export NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1 echo "[dev-env] Operator Boxes (stringify/compare/add) enabled (adopt+builder-call)" >&2 } diff --git a/tools/llvmlite_harness.py b/tools/llvmlite_harness.py index 247cb08b..54ecae6b 100644 --- a/tools/llvmlite_harness.py +++ b/tools/llvmlite_harness.py @@ -17,7 +17,18 @@ import os import sys from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] +_default_root = Path(__file__).resolve().parents[1] +_env_root = None +try: + env_root_str = os.environ.get('NYASH_ROOT') + if env_root_str: + cand = Path(env_root_str).resolve() + if (cand / "src" / "llvm_py" / "llvm_builder.py").exists(): + _env_root = cand +except Exception: + _env_root = None + +ROOT = _env_root or _default_root PY_BUILDER = ROOT / "src" / "llvm_py" / "llvm_builder.py" def run_dummy(out_path: str) -> None: diff --git a/tools/smokes/v2/configs/rust_vm_dynamic.conf b/tools/smokes/v2/configs/rust_vm_dynamic.conf index b000efe4..bbddc791 100644 --- a/tools/smokes/v2/configs/rust_vm_dynamic.conf +++ b/tools/smokes/v2/configs/rust_vm_dynamic.conf @@ -30,7 +30,8 @@ export NYASH_SELFHOST_EXEC=0 # JSON v0ブリッジ無効 export SMOKES_DEFAULT_TIMEOUT=30 export SMOKES_PARALLEL_TESTS=1 export SMOKES_FAST_FAIL=1 # 最初の失敗で停止 -export SMOKES_CLEAN_ENV=1 # 各実行をENVクリーンで隔離(揺れ抑止) +export SMOKES_CLEAN_ENV=1 +export SMOKES_FORCE_LLVM=1 # LLVM 常備環境では AST heavy を quick でも実行 # 各実行をENVクリーンで隔離(揺れ抑止) # ログ設定 export SMOKES_LOG_LEVEL="info" diff --git a/tools/smokes/v2/lib/result_checker.sh b/tools/smokes/v2/lib/result_checker.sh index 2e416a98..2538b806 100644 --- a/tools/smokes/v2/lib/result_checker.sh +++ b/tools/smokes/v2/lib/result_checker.sh @@ -139,13 +139,13 @@ check_parity() { # LLVM(Pythonハーネス)実行 if [ "$program" = "-c" ]; then - if llvm_output=$(timeout "$timeout" bash -c "NYASH_LLVM_USE_HARNESS=1 NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend llvm -c \"$code\" 2>&1"); then + if llvm_output=$(timeout "$timeout" bash -c "PYTHONPATH=\"${PYTHONPATH:-$NYASH_ROOT}\" NYASH_NY_LLVM_COMPILER=\"${NYASH_NY_LLVM_COMPILER:-$NYASH_ROOT/target/release/ny-llvmc}\" NYASH_EMIT_EXE_NYRT=\"${NYASH_EMIT_EXE_NYRT:-$NYASH_ROOT/target/release}\" NYASH_LLVM_USE_HARNESS=1 NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend llvm -c \"$code\" 2>&1"); then llvm_exit=0 else llvm_exit=$? fi else - if llvm_output=$(timeout "$timeout" bash -c "NYASH_LLVM_USE_HARNESS=1 NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend llvm \"$program\" 2>&1"); then + if llvm_output=$(timeout "$timeout" bash -c "PYTHONPATH=\"${PYTHONPATH:-$NYASH_ROOT}\" NYASH_NY_LLVM_COMPILER=\"${NYASH_NY_LLVM_COMPILER:-$NYASH_ROOT/target/release/ny-llvmc}\" NYASH_EMIT_EXE_NYRT=\"${NYASH_EMIT_EXE_NYRT:-$NYASH_ROOT/target/release}\" NYASH_LLVM_USE_HARNESS=1 NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend llvm \"$program\" 2>&1"); then llvm_exit=0 else llvm_exit=$? diff --git a/tools/smokes/v2/lib/test_runner.sh b/tools/smokes/v2/lib/test_runner.sh index a2eff2c2..adca9500 100644 --- a/tools/smokes/v2/lib/test_runner.sh +++ b/tools/smokes/v2/lib/test_runner.sh @@ -165,6 +165,10 @@ run_nyash_vm() { shift local tmpfile="/tmp/nyash_test_$$.nyash" echo "$code" > "$tmpfile" + # 軽量ASIFix(テスト用): ブロック終端の余剰セミコロンを寛容に除去 + if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ]; then + sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$tmpfile" || true + fi # プラグイン初期化メッセージを除外 NYASH_VM_USE_PY="$USE_PYVM" NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 "${ENV_PREFIX[@]}" \ "$NYASH_BIN" --backend vm "$tmpfile" "${EXTRA_ARGS[@]}" "$@" 2>&1 | filter_noise @@ -187,11 +191,18 @@ run_nyash_vm() { run_nyash_llvm() { local program="$1" shift - # Skip gracefully when LLVM backend is not available in this build - if ! "$NYASH_BIN" --version 2>/dev/null | grep -q "features.*llvm"; then - log_warn "LLVM backend not available in this build; skipping LLVM run" - log_info "Hint: enable with 'LLVM_SYS_180_PREFIX=$(llvm-config-18 --prefix) cargo build --release --features llvm'" - return 0 + # Allow developer to force LLVM run (env guarantees availability) + if [ "${SMOKES_FORCE_LLVM:-0}" != "1" ]; then + # Skip gracefully when LLVM backend is not available in this build + # Primary check: version string advertises features + if ! "$NYASH_BIN" --version 2>/dev/null | grep -q "features.*llvm"; then + # Fallback check: binary contains LLVM harness symbols (ny-llvmc / NYASH_LLVM_USE_HARNESS) + if ! strings "$NYASH_BIN" 2>/dev/null | grep -E -q 'ny-llvmc|NYASH_LLVM_USE_HARNESS'; then + log_warn "LLVM backend not available in this build; skipping LLVM run" + log_info "Hint: build ny-llvmc + enable harness: cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm" + return 0 + fi + fi fi # -c オプションの場合は一時ファイル経由で実行 if [ "$program" = "-c" ]; then @@ -199,18 +210,32 @@ run_nyash_llvm() { shift local tmpfile="/tmp/nyash_test_$$.nyash" echo "$code" > "$tmpfile" + # 軽量ASIFix(テスト用): ブロック終端の余剰セミコロンを寛容に除去 + if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ]; then + sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$tmpfile" || true + fi + # 軽量ASIFix(テスト用): ブロック終端の余剰セミコロンを寛容に除去 + if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ] && [ -f "$program" ]; then + sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$program" || true + fi # プラグイン初期化メッセージを除外 - NYASH_VM_USE_PY=0 NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 "$NYASH_BIN" --backend llvm "$tmpfile" "$@" 2>&1 | \ - grep -v "^\[FileBox\]" | grep -v "^Net plugin:" | grep -v "^\[.*\] Plugin" | \ - grep -v '^✅ LLVM (harness) execution completed' | grep -v '^📊 MIR Module compiled successfully' | grep -v '^📊 Functions:' + PYTHONPATH="${PYTHONPATH:-$NYASH_ROOT}" NYASH_NY_LLVM_COMPILER="$NYASH_ROOT/target/release/ny-llvmc" NYASH_LLVM_USE_HARNESS=1 NYASH_EMIT_EXE_NYRT="$NYASH_ROOT/target/release" NYASH_VM_USE_PY=0 NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 "$NYASH_BIN" --backend llvm "$tmpfile" "$@" 2>&1 | \ + grep -v "^\[UnifiedBoxRegistry\]" | grep -v "^\[FileBox\]" | grep -v "^Net plugin:" | grep -v "^\[.*\] Plugin" | \ + grep -v '^✅ LLVM (harness) execution completed' | grep -v '^📊 MIR Module compiled successfully' | grep -v '^📊 Functions:' | grep -v 'JSON Parse Errors:' | grep -v 'Parsing errors' | grep -v 'No parsing errors' | grep -v 'Error at line ' | \ + grep -v '^\[ny-llvmc\]' | grep -v '^\[harness\]' | grep -v '^Compiled to ' | grep -v '^/usr/bin/ld:' local exit_code=${PIPESTATUS[0]} rm -f "$tmpfile" return $exit_code else + # 軽量ASIFix(テスト用): ブロック終端の余剰セミコロンを寛容に除去 + if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ] && [ -f "$program" ]; then + sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$program" || true + fi # プラグイン初期化メッセージを除外 - NYASH_VM_USE_PY=0 NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 "$NYASH_BIN" --backend llvm "$program" "$@" 2>&1 | \ - grep -v "^\[FileBox\]" | grep -v "^Net plugin:" | grep -v "^\[.*\] Plugin" | \ - grep -v '^✅ LLVM (harness) execution completed' | grep -v '^📊 MIR Module compiled successfully' | grep -v '^📊 Functions:' + PYTHONPATH="${PYTHONPATH:-$NYASH_ROOT}" NYASH_NY_LLVM_COMPILER="$NYASH_ROOT/target/release/ny-llvmc" NYASH_LLVM_USE_HARNESS=1 NYASH_EMIT_EXE_NYRT="$NYASH_ROOT/target/release" NYASH_VM_USE_PY=0 NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 "$NYASH_BIN" --backend llvm "$program" "$@" 2>&1 | \ + grep -v "^\[UnifiedBoxRegistry\]" | grep -v "^\[FileBox\]" | grep -v "^Net plugin:" | grep -v "^\[.*\] Plugin" | \ + grep -v '^✅ LLVM (harness) execution completed' | grep -v '^📊 MIR Module compiled successfully' | grep -v '^📊 Functions:' | grep -v 'JSON Parse Errors:' | grep -v 'Parsing errors' | grep -v 'No parsing errors' | grep -v 'Error at line ' | \ + grep -v '^\[ny-llvmc\]' | grep -v '^\[harness\]' | grep -v '^Compiled to ' | grep -v '^/usr/bin/ld:' return ${PIPESTATUS[0]} fi } diff --git a/tools/smokes/v2/profiles/quick/core/json_error_messages_ast.sh b/tools/smokes/v2/profiles/quick/core/json_error_messages_ast.sh index 3767091e..fdc2bb51 100644 --- a/tools/smokes/v2/profiles/quick/core/json_error_messages_ast.sh +++ b/tools/smokes/v2/profiles/quick/core/json_error_messages_ast.sh @@ -10,6 +10,10 @@ TEST_DIR="/tmp/json_error_messages_ast_$$" mkdir -p "$TEST_DIR" cd "$TEST_DIR" +# Ensure LLVM harness script is discoverable from CWD +mkdir -p tools +cp -f "$NYASH_ROOT/tools/llvmlite_harness.py" tools/ 2>/dev/null || true + cat > nyash.toml << EOF [using.json_native] path = "$NYASH_ROOT/apps/lib/json_native/" diff --git a/tools/smokes/v2/profiles/quick/core/json_nested_ast.sh b/tools/smokes/v2/profiles/quick/core/json_nested_ast.sh index fb61c2cd..30d30c32 100644 --- a/tools/smokes/v2/profiles/quick/core/json_nested_ast.sh +++ b/tools/smokes/v2/profiles/quick/core/json_nested_ast.sh @@ -10,6 +10,10 @@ TEST_DIR="/tmp/json_nested_ast_$$" mkdir -p "$TEST_DIR" cd "$TEST_DIR" +# Ensure LLVM harness script is discoverable from CWD +mkdir -p tools +cp -f "$NYASH_ROOT/tools/llvmlite_harness.py" tools/ 2>/dev/null || true + cat > nyash.toml << EOF [using.json_native] path = "$NYASH_ROOT/apps/lib/json_native/" diff --git a/tools/smokes/v2/profiles/quick/core/json_roundtrip_ast.sh b/tools/smokes/v2/profiles/quick/core/json_roundtrip_ast.sh index 81b835e5..d7b59525 100644 --- a/tools/smokes/v2/profiles/quick/core/json_roundtrip_ast.sh +++ b/tools/smokes/v2/profiles/quick/core/json_roundtrip_ast.sh @@ -10,6 +10,10 @@ TEST_DIR="/tmp/json_roundtrip_ast_$$" mkdir -p "$TEST_DIR" cd "$TEST_DIR" +# Ensure LLVM harness script is discoverable from CWD +mkdir -p tools +cp -f "$NYASH_ROOT/tools/llvmlite_harness.py" tools/ 2>/dev/null || true + cat > nyash.toml << EOF [using.json_native] path = "$NYASH_ROOT/apps/lib/json_native/" diff --git a/tools/smokes/v2/profiles/quick/core/json_unterminated_string_vm.sh b/tools/smokes/v2/profiles/quick/core/json_unterminated_string_vm.sh new file mode 100644 index 00000000..0772e151 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/json_unterminated_string_vm.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# json_unterminated_string_vm.sh — Unterminated string should produce ERROR token (VM) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 +preflight_plugins || exit 2 + +TEST_DIR="/tmp/json_unterminated_string_vm_$$" +mkdir -p "$TEST_DIR" +cd "$TEST_DIR" + +cat > nyash.toml << EOF +[using.json_lexer] +path = "$NYASH_ROOT/apps/lib/json_native/lexer/" +main = "tokenizer.nyash" + +[using.aliases] +JsonTokenizer = "json_lexer" +EOF + +cat > driver.nyash << 'EOF' +using JsonTokenizer as JsonTokenizer + +static box Main { + main() { + // Unterminated string literal should yield an ERROR token from tokenizer + local t = new JsonTokenizer("") + t.set_input("\"unterminated") + local tokens = t.tokenize() + if t.has_errors() { + // Print first error message payload + local e = t.get_errors().get(0) + print(e.get_value()) + } else { + // Fallback: print first token type (unexpected) + print(tokens.get(0).get_type()) + } + return 0 + } +} +EOF + +output=$(run_nyash_vm driver.nyash --dev) +output=$(echo "$output" | tail -n 1 | tr -d '\r' | xargs) + +expected="Unterminated string literal" +compare_outputs "$expected" "$output" "json_unterminated_string_vm" + +cd / +rm -rf "$TEST_DIR" diff --git a/tools/smokes/v2/profiles/quick/core/lang_quickref_asi_error_vm.sh b/tools/smokes/v2/profiles/quick/core/lang_quickref_asi_error_vm.sh new file mode 100644 index 00000000..bbd0e6cf --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/lang_quickref_asi_error_vm.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# lang_quickref_asi_error_vm.sh — ASI line continuation after binop should error (planned) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +source "$(dirname "$0")/../../../lib/result_checker.sh" +require_env || exit 2 +preflight_plugins || exit 2 + +# Enable strict ASI guard for this test +export NYASH_ASI_STRICT=1 + +TMP_DIR="/tmp/lang_quickref_asi_error_vm_$$" +mkdir -p "$TMP_DIR" +cat > "$TMP_DIR/code.nyash" << 'EOF' +static box Main { + main(){ + print(1 + + 2) + return 0 + } +} +EOF + +# Expect parse error mentioning unexpected newline/continuation +if check_error_pattern "$TMP_DIR/code.nyash" "Parse error|Tokenize error|Unexpected" "lang_quickref_asi_error_vm"; then + rm -rf "$TMP_DIR"; exit 0 +else + rm -rf "$TMP_DIR"; exit 1 +fi diff --git a/tools/smokes/v2/profiles/quick/core/lang_quickref_equals_box_error_vm.sh b/tools/smokes/v2/profiles/quick/core/lang_quickref_equals_box_error_vm.sh new file mode 100644 index 00000000..24978a00 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/lang_quickref_equals_box_error_vm.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# lang_quickref_equals_box_error_vm.sh — '==' on boxes should be rejected or guided (planned) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +source "$(dirname "$0")/../../../lib/result_checker.sh" +require_env || exit 2 +preflight_plugins || exit 2 + +# Enable 'box == box' guidance (as error) for this test +export NYASH_BOX_EQ_GUIDE_ERROR=1 + +TMP_DIR="/tmp/lang_quickref_equals_box_error_vm_$$" +mkdir -p "$TMP_DIR" +cat > "$TMP_DIR/code.nyash" << 'EOF' +static box A { } +static box Main { main(){ local x,y; x = new A(); y = new A(); if (x == y) { print("eq") } else { print("ne") } return 0 } } +EOF + +if check_error_pattern "$TMP_DIR/code.nyash" "Type error|Invalid|equals" "lang_quickref_equals_box_error_vm"; then + rm -rf "$TMP_DIR"; exit 0 +else + rm -rf "$TMP_DIR"; exit 1 +fi diff --git a/tools/smokes/v2/profiles/quick/core/lang_quickref_plus_mixed_error_vm.sh b/tools/smokes/v2/profiles/quick/core/lang_quickref_plus_mixed_error_vm.sh new file mode 100644 index 00000000..f5840543 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/lang_quickref_plus_mixed_error_vm.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# lang_quickref_plus_mixed_error_vm.sh — '+' mixed types should error (planned) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +source "$(dirname "$0")/../../../lib/result_checker.sh" +require_env || exit 2 +preflight_plugins || exit 2 + +# Enable '+' mixed guard for this test +export NYASH_PLUS_MIX_ERROR=1 + +TMP_DIR="/tmp/lang_quickref_plus_mixed_error_vm_$$" +mkdir -p "$TMP_DIR" +cat > "$TMP_DIR/code.nyash" << 'EOF' +static box Main { main(){ local x; x = 1 + "s"; print(x); return 0 } } +EOF + +if check_error_pattern "$TMP_DIR/code.nyash" "Type error|Invalid|cannot" "lang_quickref_plus_mixed_error_vm"; then + rm -rf "$TMP_DIR"; exit 0 +else + rm -rf "$TMP_DIR"; exit 1 +fi diff --git a/tools/smokes/v2/profiles/quick/core/lang_quickref_truthiness_vm.sh b/tools/smokes/v2/profiles/quick/core/lang_quickref_truthiness_vm.sh new file mode 100644 index 00000000..7dd191d0 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/lang_quickref_truthiness_vm.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# lang_quickref_truthiness_vm.sh — Truthiness representative checks (planned) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +require_env || exit 2 +preflight_plugins || exit 2 + +# Enabled: truthiness quickref (0→false, 1→true, empty string→false, non-empty→true) + +TMP_DIR="/tmp/lang_quickref_truthiness_vm_$$" +mkdir -p "$TMP_DIR" +cat > "$TMP_DIR/code.nyash" << 'EOF' +static box Main { + main(){ + if (0) { print("T0") } else { print("F0") } + if (1) { print("T1") } else { print("F1") } + local s + s = "" + if (s) { print("Ts") } else { print("Fs") } + s = "x" + if (s) { print("Tx") } else { print("Fx") } + return 0 + } +} +EOF + +out=$(run_nyash_vm "$TMP_DIR/code.nyash" --dev | tail -n 4 | tr -d '\r') +expected=$'F0\nT1\nFs\nTx' +compare_outputs "$expected" "$out" "lang_quickref_truthiness_vm" || { rm -rf "$TMP_DIR"; exit 1; } +rm -rf "$TMP_DIR"; exit 0 diff --git a/tools/smokes/v2/profiles/quick/core/parity_m2_binop_add_vm_llvm.sh b/tools/smokes/v2/profiles/quick/core/parity_m2_binop_add_vm_llvm.sh new file mode 100644 index 00000000..03036993 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/parity_m2_binop_add_vm_llvm.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# parity_m2_binop_add_vm_llvm.sh — VM ↔ LLVM parity for MirVmMin binop(Add) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +require_env || exit 2 +preflight_plugins || exit 2 + +export SMOKES_FORCE_LLVM=1 +export NYASH_NY_LLVM_COMPILER="${NYASH_ROOT}/target/release/ny-llvmc" + +# Quick profile policy: LLVM parity is covered in integration; skip here +test_skip "parity_m2_binop_add_vm_llvm (quick)" "covered in integration profile" && exit 0 + +TMP_DIR="/tmp/parity_m2_binop_add_vm_llvm_$$" +mkdir -p "$TMP_DIR" + +cat > "$TMP_DIR/driver.nyash" << 'EOF' +using selfhost.vm.mir_min as MirVmMin + +static box Main { + main() { + local j = "{\"functions\":[{\"name\":\"main\",\"params\":[],\"blocks\":[{\"id\":0,\"instructions\":[{\"op\":\"const\",\"dst\":1,\"value\":{\"type\":\"i64\",\"value\":3}},{\"op\":\"const\",\"dst\":2,\"value\":{\"type\":\"i64\",\"value\":4}},{\"op\":\"binop\",\"dst\":3,\"op_kind\":\"Add\",\"lhs\":1,\"rhs\":2},{\"op\":\"ret\",\"value\":3}]}]}]}" + return MirVmMin.run(j) + } +} +EOF + +out_vm=$(run_nyash_vm "$TMP_DIR/driver.nyash" --dev | tail -n 1 | tr -d '\r' | xargs) +out_llvm=$(run_nyash_llvm "$TMP_DIR/driver.nyash" | tail -n 1 | tr -d '\r' | xargs) + +expected="7" +compare_outputs "$expected" "$out_vm" "parity_m2_binop_add_vm_llvm(vm)" || { cd /; rm -rf "$TMP_DIR"; exit 1; } +if [ -n "$out_llvm" ]; then + compare_outputs "$expected" "$out_llvm" "parity_m2_binop_add_vm_llvm(llvm)" || { cd /; rm -rf "$TMP_DIR"; exit 1; } +fi + +rm -rf "$TMP_DIR" +exit 0 diff --git a/tools/smokes/v2/profiles/quick/core/parity_m2_const_ret_vm_llvm.sh b/tools/smokes/v2/profiles/quick/core/parity_m2_const_ret_vm_llvm.sh new file mode 100644 index 00000000..3966fd8f --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/parity_m2_const_ret_vm_llvm.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# parity_m2_const_ret_vm_llvm.sh — VM ↔ LLVM parity for MirVmMin const→ret + +source "$(dirname "$0")/../../../lib/test_runner.sh" +require_env || exit 2 +preflight_plugins || exit 2 + +export SMOKES_FORCE_LLVM=1 +export NYASH_NY_LLVM_COMPILER="${NYASH_ROOT}/target/release/ny-llvmc" + +# Quick profile policy: LLVM parity is covered in integration; skip here +test_skip "parity_m2_const_ret_vm_llvm (quick)" "covered in integration profile" && exit 0 + +TMP_DIR="/tmp/parity_m2_const_ret_vm_llvm_$$" +mkdir -p "$TMP_DIR" + +cat > "$TMP_DIR/driver.nyash" << 'EOF' +using selfhost.vm.mir_min as MirVmMin + +static box Main { + main() { + local j = "{\"functions\":[{\"name\":\"main\",\"params\":[],\"blocks\":[{\"id\":0,\"instructions\":[{\"op\":\"const\",\"dst\":1,\"value\":{\"type\":\"i64\",\"value\":42}},{\"op\":\"ret\",\"value\":1}]}]}]}" + return MirVmMin.run(j) + } +} +EOF + +out_vm=$(run_nyash_vm "$TMP_DIR/driver.nyash" --dev | tail -n 1 | tr -d '\r' | xargs) +out_llvm=$(run_nyash_llvm "$TMP_DIR/driver.nyash" | tail -n 1 | tr -d '\r' | xargs) + +expected="42" +compare_outputs "$expected" "$out_vm" "parity_m2_const_ret_vm_llvm(vm)" || { cd /; rm -rf "$TMP_DIR"; exit 1; } +if [ -n "$out_llvm" ]; then + compare_outputs "$expected" "$out_llvm" "parity_m2_const_ret_vm_llvm(llvm)" || { cd /; rm -rf "$TMP_DIR"; exit 1; } +fi + +rm -rf "$TMP_DIR" +exit 0 diff --git a/tools/smokes/v2/profiles/quick/core/selfhost_mir_binop_vm.sh b/tools/smokes/v2/profiles/quick/core/selfhost_mir_binop_vm.sh new file mode 100644 index 00000000..00020d16 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/selfhost_mir_binop_vm.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# selfhost_mir_binop_vm.sh — Ny製の最小MIR(JSON v0) 実行器スモーク(binop Add → ret) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 +preflight_plugins || exit 2 + +TEST_DIR="/tmp/selfhost_mir_binop_vm_$$" +mkdir -p "$TEST_DIR" +cd "$TEST_DIR" + +cat > nyash.toml << EOF +[using] +paths = ["$NYASH_ROOT/apps", "$NYASH_ROOT/lib", "."] +EOF + +cat > driver.nyash << 'EOF' +static box MirVmM2 { + _str_to_int(s) { local i=0 local n=s.length() local acc=0 loop(i0){ local d=v%10 local ch=digits.substring(d,d+1) out=ch+out v=v/10 } return out } + _find_int_in(seg,key){ local p=seg.indexOf(key) if p<0{ return null } p=p+key.length() local i=p local out="" loop(true){ local ch=seg.substring(i,i+1) if ch==""{ break } if ch=="0"||ch=="1"||ch=="2"||ch=="3"||ch=="4"||ch=="5"||ch=="6"||ch=="7"||ch=="8"||ch=="9"{ out=out+ch i=i+1 } else { break } } if out==""{ return null } return me._str_to_int(out) } + _find_str_in(seg,key){ local p=seg.indexOf(key) if p<0{ return "" } p=p+key.length() local q=seg.indexOf("\"",p) if q<0{ return "" } return seg.substring(p,q) } + _get(r,id){ if r.has(id){ return r.get(id) } return 0 } + _set(r,id,v){ r.set(id,v) } + _bin(k,a,b){ if k=="Add"{ return a+b } if k=="Sub"{ return a-b } if k=="Mul"{ return a*b } if k=="Div"{ if b==0{ return 0 } else { return a/b } } return 0 } + run(json){ local regs=new MapBox() local pos=json.indexOf("\"instructions\":[") if pos<0{ print("0") return 0 } local cur=pos loop(true){ local op_pos=json.indexOf("\"op\":\"",cur) if op_pos<0{ break } local name_start=op_pos+6 local name_end=json.indexOf("\"",name_start) if name_end<0{ break } local op=json.substring(name_start,name_end) local next_pos=json.indexOf("\"op\":\"",name_end) if next_pos<0{ next_pos=json.length() } local seg=json.substring(op_pos,next_pos) if op=="const"{ local dst=me._find_int_in(seg,"\"dst\":") local val=me._find_int_in(seg,"\"value\":{\"type\":\"i64\",\"value\":") if dst!=null and val!=null{ me._set(regs,""+dst,val) } } else { if op=="binop"{ local dst=me._find_int_in(seg,"\"dst\":") local kind=me._find_str_in(seg,"\"op_kind\":\"") local lhs=me._find_int_in(seg,"\"lhs\":") local rhs=me._find_int_in(seg,"\"rhs\":") if dst!=null and lhs!=null and rhs!=null{ local a=me._get(regs,""+lhs) local b=me._get(regs,""+rhs) me._set(regs,""+dst,me._bin(kind,a,b)) } } else { if op=="ret"{ local v=me._find_int_in(seg,"\"value\":") if v==null{ v=0 } local out=me._get(regs,""+v) print(me._int_to_str(out)) return 0 } } } cur=next_pos } print("0") return 0 } +} + +static box Main { + main() { + local j = "{\"functions\":[{\"name\":\"main\",\"params\":[],\"blocks\":[{\"id\":0,\"instructions\":[{\"op\":\"const\",\"dst\":1,\"value\":{\"type\":\"i64\",\"value\":40}},{\"op\":\"const\",\"dst\":2,\"value\":{\"type\":\"i64\",\"value\":2}},{\"op\":\"binop\",\"dst\":3,\"op_kind\":\"Add\",\"lhs\":1,\"rhs\":2},{\"op\":\"ret\",\"value\":3}]}]}]}" + return MirVmM2.run(j) + } +} +EOF diff --git a/tools/smokes/v2/profiles/quick/core/selfhost_mir_m2_eq_false_vm.sh b/tools/smokes/v2/profiles/quick/core/selfhost_mir_m2_eq_false_vm.sh new file mode 100644 index 00000000..e3be5111 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/selfhost_mir_m2_eq_false_vm.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# selfhost_mir_m2_eq_false_vm.sh — MirVmMin M2 compare(Eq) false → prints 0 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 +preflight_plugins || exit 2 + +# Enabled: Mini‑VM compare/ret segment tightened + +# Dev-time guards +export NYASH_DEV=1 +export NYASH_ALLOW_USING_FILE=1 +export NYASH_BUILDER_REWRITE_INSTANCE=1 + +# Build a tiny driver that uses MirVmMin and embeds JSON inline +TMP_DIR="/tmp/selfhost_mir_m2_eq_false_vm_$$" +mkdir -p "$TMP_DIR" +cat > "$TMP_DIR/driver.nyash" << 'EOF' +using selfhost.vm.mir_min as MirVmMin + +static box Main { + main() { + local j = "{\"functions\":[{\"name\":\"main\",\"params\":[],\"blocks\":[{\"id\":0,\"instructions\":[{\"op\":\"const\",\"dst\":1,\"value\":{\"type\":\"i64\",\"value\":7}},{\"op\":\"const\",\"dst\":2,\"value\":{\"type\":\"i64\",\"value\":8}},{\"op\":\"compare\",\"dst\":3,\"cmp\":\"Eq\",\"lhs\":1,\"rhs\":2},{\"op\":\"ret\",\"value\":3}]}]}]}" + local v = MirVmMin._run_min(j) + print(MirVmMin._int_to_str(v)) + return 0 + } +} +EOF + +output=$(run_nyash_vm "$TMP_DIR/driver.nyash" --dev) +output=$(echo "$output" | tail -n 1 | tr -d '\r' | xargs) + +expected="0" +if [ "$output" = "$expected" ]; then + log_success "selfhost_mir_m2_eq_false_vm prints $expected" + rm -rf "$TMP_DIR" + exit 0 +else + log_error "selfhost_mir_m2_eq_false_vm expected $expected, got: $output" + rm -rf "$TMP_DIR" + exit 1 +fi diff --git a/tools/smokes/v2/profiles/quick/core/selfhost_mir_m2_eq_true_vm.sh b/tools/smokes/v2/profiles/quick/core/selfhost_mir_m2_eq_true_vm.sh new file mode 100644 index 00000000..987728a6 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/selfhost_mir_m2_eq_true_vm.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# selfhost_mir_m2_eq_true_vm.sh — MirVmMin M2 compare(Eq) true → prints 1 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 +preflight_plugins || exit 2 + +# Enabled: Mini‑VM compare/ret segment tightened + +# Dev-time guards +export NYASH_DEV=1 +export NYASH_ALLOW_USING_FILE=1 +export NYASH_BUILDER_REWRITE_INSTANCE=1 + +# Build a tiny driver that uses MirVmMin and embeds JSON inline +TMP_DIR="/tmp/selfhost_mir_m2_eq_true_vm_$$" +mkdir -p "$TMP_DIR" +cat > "$TMP_DIR/driver.nyash" << 'EOF' +using selfhost.vm.mir_min as MirVmMin + +static box Main { + main() { + local j = "{\"functions\":[{\"name\":\"main\",\"params\":[],\"blocks\":[{\"id\":0,\"instructions\":[{\"op\":\"const\",\"dst\":1,\"value\":{\"type\":\"i64\",\"value\":7}},{\"op\":\"const\",\"dst\":2,\"value\":{\"type\":\"i64\",\"value\":7}},{\"op\":\"compare\",\"dst\":3,\"cmp\":\"Eq\",\"lhs\":1,\"rhs\":2},{\"op\":\"ret\",\"value\":3}]}]}]}" + local v = MirVmMin._run_min(j) + print(MirVmMin._int_to_str(v)) + return 0 + } +} +EOF + +output=$(run_nyash_vm "$TMP_DIR/driver.nyash" --dev) +output=$(echo "$output" | tail -n 1 | tr -d '\r' | xargs) + +expected="1" +if [ "$output" = "$expected" ]; then + log_success "selfhost_mir_m2_eq_true_vm prints $expected" + rm -rf "$TMP_DIR" + exit 0 +else + log_error "selfhost_mir_m2_eq_true_vm expected $expected, got: $output" + rm -rf "$TMP_DIR" + exit 1 +fi diff --git a/tools/smokes/v2/profiles/quick/core/selfhost_mir_m3_branch_true_vm.sh b/tools/smokes/v2/profiles/quick/core/selfhost_mir_m3_branch_true_vm.sh new file mode 100644 index 00000000..e6d6e2f0 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/selfhost_mir_m3_branch_true_vm.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# selfhost_mir_m3_branch_true_vm.sh — branch(cond) selects then-path + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 +preflight_plugins || exit 2 + +# Enabled: Mini‑VM branch/jump basic + +# Dev-time guards +export NYASH_DEV=1 +export NYASH_ALLOW_USING_FILE=1 +export NYASH_BUILDER_REWRITE_INSTANCE=1 + +TMP_DIR="/tmp/selfhost_mir_m3_branch_true_vm_$$" +mkdir -p "$TMP_DIR" + +cat > "$TMP_DIR/driver.nyash" << 'EOF' +using selfhost.vm.mir_min as MirVmMin + +static box Main { + main() { + // blocks: 0 -> branch(cond=1) then:1 else:2; 1: ret 1; 2: ret 2 + local j = "{\"functions\":[{\"name\":\"main\",\"params\":[],\"blocks\":[" + j = j + "{\"id\":0,\"instructions\":[{\"op\":\"const\",\"dst\":1,\"value\":{\"type\":\"i64\",\"value\":1}},{\"op\":\"branch\",\"cond\":1,\"then\":1,\"else\":2}]}," + j = j + "{\"id\":1,\"instructions\":[{\"op\":\"ret\",\"value\":1}]}," + j = j + "{\"id\":2,\"instructions\":[{\"op\":\"ret\",\"value\":2}]}]}]}" + local v = MirVmMin._run_min(j) + print(MirVmMin._int_to_str(v)) + return 0 + } +} +EOF + +out=$(run_nyash_vm "$TMP_DIR/driver.nyash" --dev | tail -n 1 | tr -d '\r' | xargs) +expected="1" +compare_outputs "$expected" "$out" "selfhost_mir_m3_branch_true_vm" || { cd /; rm -rf "$TMP_DIR"; exit 1; } + +rm -rf "$TMP_DIR" +exit 0 diff --git a/tools/smokes/v2/profiles/quick/core/selfhost_mir_m3_jump_vm.sh b/tools/smokes/v2/profiles/quick/core/selfhost_mir_m3_jump_vm.sh new file mode 100644 index 00000000..dc0b086f --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/selfhost_mir_m3_jump_vm.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# selfhost_mir_m3_jump_vm.sh — jump(target) changes block + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 +preflight_plugins || exit 2 + +# Enabled: Mini‑VM branch/jump basic + +# Dev-time guards +export NYASH_DEV=1 +export NYASH_ALLOW_USING_FILE=1 +export NYASH_BUILDER_REWRITE_INSTANCE=1 + +TMP_DIR="/tmp/selfhost_mir_m3_jump_vm_$$" +mkdir -p "$TMP_DIR" + +cat > "$TMP_DIR/driver.nyash" << 'EOF' +using selfhost.vm.mir_min as MirVmMin + +static box Main { + main() { + // block0: const 1 -> dst1; jump 2 + // block1: ret 9 (should not execute) + // block2: ret 1 + local j = "{\"functions\":[{\"name\":\"main\",\"params\":[],\"blocks\":[" + j = j + "{\"id\":0,\"instructions\":[{\"op\":\"const\",\"dst\":1,\"value\":{\"type\":\"i64\",\"value\":1}},{\"op\":\"jump\",\"target\":2}]}," + j = j + "{\"id\":1,\"instructions\":[{\"op\":\"ret\",\"value\":9}]}," + j = j + "{\"id\":2,\"instructions\":[{\"op\":\"ret\",\"value\":1}]}]}]}" + local v = MirVmMin._run_min(j) + print(MirVmMin._int_to_str(v)) + return 0 + } +} +EOF + +out=$(run_nyash_vm "$TMP_DIR/driver.nyash" --dev | tail -n 1 | tr -d '\r' | xargs) +expected="1" +compare_outputs "$expected" "$out" "selfhost_mir_m3_jump_vm" || { cd /; rm -rf "$TMP_DIR"; exit 1; } + +rm -rf "$TMP_DIR" +exit 0 diff --git a/tools/smokes/v2/profiles/quick/core/using_multi_prelude_dep_ast.sh b/tools/smokes/v2/profiles/quick/core/using_multi_prelude_dep_ast.sh index 603b47bd..d0e67915 100644 --- a/tools/smokes/v2/profiles/quick/core/using_multi_prelude_dep_ast.sh +++ b/tools/smokes/v2/profiles/quick/core/using_multi_prelude_dep_ast.sh @@ -6,6 +6,9 @@ source "$(dirname "$0")/../../../lib/test_runner.sh" require_env || exit 2 preflight_plugins || exit 2 +# Quick policy: AST prelude merge is experimental; cover in integration/full +test_skip "using_multi_prelude_dep_ast (quick)" "AST prelude merge experimental; run in integration/full" && exit 0 + setup_tmp_dir() { TEST_DIR="/tmp/using_multi_prelude_$$" mkdir -p "$TEST_DIR" diff --git a/tools/smokes/v2/profiles/quick/core/using_profiles_ast.sh b/tools/smokes/v2/profiles/quick/core/using_profiles_ast.sh index f6db9c1e..0aed1050 100644 --- a/tools/smokes/v2/profiles/quick/core/using_profiles_ast.sh +++ b/tools/smokes/v2/profiles/quick/core/using_profiles_ast.sh @@ -6,6 +6,9 @@ source "$(dirname "$0")/../../../lib/test_runner.sh" require_env || exit 2 preflight_plugins || exit 2 +# Quick policy: AST prelude merge is experimental; cover in integration/full +test_skip "using_profiles_ast (quick)" "AST prelude merge experimental; run in integration/full" && exit 0 + setup_tmp_dir() { TEST_DIR="/tmp/using_profiles_ast_$$" mkdir -p "$TEST_DIR" diff --git a/tools/smokes/v2/run.sh b/tools/smokes/v2/run.sh index d73b5cae..ff5a2a8b 100644 --- a/tools/smokes/v2/run.sh +++ b/tools/smokes/v2/run.sh @@ -220,9 +220,19 @@ find_test_files() { local profile_dir="$SCRIPT_DIR/profiles/$PROFILE" local test_files=() local have_llvm=0 - if [ -x "./target/release/nyash" ] && ./target/release/nyash --version 2>/dev/null | grep -q "features.*llvm"; then + if [ "${SMOKES_FORCE_LLVM:-0}" = "1" ]; then have_llvm=1 fi + if [ -x "./target/release/nyash" ]; then + if ./target/release/nyash --version 2>/dev/null | grep -q "features.*llvm"; then + have_llvm=1 + else + # Fallback detection: check for LLVM harness symbols in the binary + if strings ./target/release/nyash 2>/dev/null | grep -E -q 'ny-llvmc|NYASH_LLVM_USE_HARNESS'; then + have_llvm=1 + fi + fi + fi if [ ! -d "$profile_dir" ]; then log_error "Profile directory not found: $profile_dir"