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.
This commit is contained in:
10
AGENTS.md
10
AGENTS.md
@ -305,8 +305,9 @@ Notes
|
|||||||
- VM と Cranelift(JIT) は MIR14 へ移行中のため、現在は実行経路として安定していないよ(検証・実装作業の都合で壊れている場合があるにゃ)。
|
- VM と Cranelift(JIT) は MIR14 へ移行中のため、現在は実行経路として安定していないよ(検証・実装作業の都合で壊れている場合があるにゃ)。
|
||||||
- 当面の実行・配布は LLVM ラインを最優先・全力で整備する方針だよ。開発・確認は `--features llvm` を有効にして進めてね。
|
- 当面の実行・配布は LLVM ラインを最優先・全力で整備する方針だよ。開発・確認は `--features llvm` を有効にして進めてね。
|
||||||
- 推奨チェック:
|
- 推奨チェック:
|
||||||
- `env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo build --release --features llvm -j 24`
|
- LLVM は llvmlite ハーネス(Python)経由だよ。Rust inkwell は既定で不使用(legacy のみ)。
|
||||||
- `env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo check --features llvm`
|
- ビルド(ハーネス): `cargo build --release --features llvm -j 24`
|
||||||
|
- チェック: `cargo check --features llvm`
|
||||||
|
|
||||||
## Docs links(開発方針/スタイル)
|
## Docs links(開発方針/スタイル)
|
||||||
- Language statements (ASI): `docs/reference/language/statements.md`
|
- Language statements (ASI): `docs/reference/language/statements.md`
|
||||||
@ -337,7 +338,7 @@ Notes
|
|||||||
- Build (JIT/VM): `cargo build --release --features cranelift-jit`
|
- Build (JIT/VM): `cargo build --release --features cranelift-jit`
|
||||||
- Build (LLVM AOT / harness-first):
|
- Build (LLVM AOT / harness-first):
|
||||||
- `cargo build --release -p nyash-llvm-compiler` (ny-llvmc builder)
|
- `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`
|
- 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`
|
- 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`
|
- 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).
|
- Rust tests: `cargo test` (add targeted unit tests near code).
|
||||||
- Smoke scripts validate end‑to‑end AOT/JIT (`tools/llvm_smoke.sh`).
|
- 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/`.
|
- 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
|
## Commit & Pull Request Guidelines
|
||||||
- Commits: concise imperative subject; scope the change (e.g., "llvm: fix argc handling in nyrt").
|
- Commits: concise imperative subject; scope the change (e.g., "llvm: fix argc handling in nyrt").
|
||||||
|
|||||||
65
CLAUDE.md
65
CLAUDE.md
@ -2,6 +2,33 @@
|
|||||||
|
|
||||||
このファイルは最小限の入口だよ。詳細はREADMEから辿ってねにゃ😺
|
このファイルは最小限の入口だよ。詳細は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構造を使う!
|
## 🚨 重要:スモークテストはv2構造を使う!
|
||||||
- 📖 **スモークテスト完全ガイド**: [tools/smokes/README.md](tools/smokes/README.md)
|
- 📖 **スモークテスト完全ガイド**: [tools/smokes/README.md](tools/smokes/README.md)
|
||||||
- 📁 **v2詳細ドキュメント**: [tools/smokes/v2/README.md](tools/smokes/v2/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
|
||||||
|
|
||||||
# 個別スモークテスト
|
# 個別スモークテスト(フィルタ指定)
|
||||||
tools/smokes/v2/run.sh --profile quick --filter "<glob>"
|
tools/smokes/v2/run.sh --profile quick --filter "<glob>"
|
||||||
# 例: --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
|
./target/release/nyash --backend vm apps/APP/main.nyash
|
||||||
@ -29,14 +60,17 @@ tools/smokes/v2/run.sh --profile quick --filter "<glob>"
|
|||||||
# 前提: Python3 + llvmlite
|
# 前提: Python3 + llvmlite
|
||||||
# 未導入なら: pip install llvmlite
|
# 未導入なら: pip install llvmlite
|
||||||
|
|
||||||
# ビルド(LLVM_SYS_180_PREFIX不要!)
|
# 一括スモークテスト(そのまま実行)
|
||||||
cargo build --release --features llvm
|
|
||||||
|
|
||||||
# 一括スモークテスト
|
|
||||||
tools/smokes/v2/run.sh --profile integration
|
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 "<glob>"
|
tools/smokes/v2/run.sh --profile integration --filter "<glob>"
|
||||||
|
# 例: --filter "json_*" # JSON関連のみ
|
||||||
|
# 例: --filter "vm_llvm_*" # VM/LLVM比較系のみ
|
||||||
|
|
||||||
# 単発実行
|
# 単発実行
|
||||||
NYASH_LLVM_USE_HARNESS=1 ./target/release/nyash --backend llvm apps/tests/peek_expr_block.nyash
|
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 # 実行時最適化
|
./target/release/nyash --backend llvm program.nyash # 実行時最適化
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🎯 **2本柱ビルド方法** (2025-09-24更新)
|
### 🎯 **2本柱ビルド方法** (2025-09-28更新)
|
||||||
|
|
||||||
#### 🔨 **標準ビルド**(推奨)
|
#### 🔨 **標準ビルド**(推奨)
|
||||||
```bash
|
```bash
|
||||||
# 標準ビルド(2本柱対応)
|
# 標準ビルド(2本柱対応)
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
# LLVM機能付きビルド(本番用)
|
# LLVM(llvmliteハーネス)付きビルド(本番用)
|
||||||
env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo build --release --features llvm
|
cargo build --release --features llvm
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 📝 **2本柱テスト実行**
|
#### 📝 **2本柱テスト実行**
|
||||||
@ -196,9 +230,9 @@ env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo build --release --features llvm
|
|||||||
cargo build --release
|
cargo build --release
|
||||||
./target/release/nyash program.nyash
|
./target/release/nyash program.nyash
|
||||||
|
|
||||||
# 2. LLVM実行 ✅(本番・最適化用)
|
# 2. LLVM実行 ✅(本番・最適化用, llvmliteハーネス)
|
||||||
env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo build --release --features llvm
|
cargo build --release --features llvm
|
||||||
./target/release/nyash --backend llvm program.nyash
|
NYASH_LLVM_USE_HARNESS=1 ./target/release/nyash --backend llvm program.nyash
|
||||||
|
|
||||||
# 3. プラグインテスト実証済み ✅
|
# 3. プラグインテスト実証済み ✅
|
||||||
# CounterBox
|
# CounterBox
|
||||||
@ -829,7 +863,9 @@ box MyBox {
|
|||||||
- Reference: [docs/reference/](docs/reference/)
|
- 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)
|
- **MIR**: [INSTRUCTION_SET.md](docs/reference/mir/INSTRUCTION_SET.md)
|
||||||
- **API**: [boxes-system/](docs/reference/boxes-system/)
|
- **API**: [boxes-system/](docs/reference/boxes-system/)
|
||||||
- **プラグイン**: [plugin-system/](docs/reference/plugin-system/)
|
- **プラグイン**: [plugin-system/](docs/reference/plugin-system/)
|
||||||
@ -847,6 +883,7 @@ box MyBox {
|
|||||||
### 🎯 最重要ドキュメント(2つの核心)
|
### 🎯 最重要ドキュメント(2つの核心)
|
||||||
|
|
||||||
#### 🔤 言語仕様
|
#### 🔤 言語仕様
|
||||||
|
- **[クイックリファレンス](docs/reference/language/quick-reference.md)** ⭐最優先 - 1ページ実用ガイド(ASI・Truthiness・演算子・型ルール)
|
||||||
- **[構文早見表](docs/quick-reference/syntax-cheatsheet.md)** - 基本構文・よくある間違い
|
- **[構文早見表](docs/quick-reference/syntax-cheatsheet.md)** - 基本構文・よくある間違い
|
||||||
- **[完全リファレンス](docs/reference/language/LANGUAGE_REFERENCE_2025.md)** - 言語仕様詳細
|
- **[完全リファレンス](docs/reference/language/LANGUAGE_REFERENCE_2025.md)** - 言語仕様詳細
|
||||||
|
|
||||||
|
|||||||
228
CURRENT_TASK.md
228
CURRENT_TASK.md
@ -4,13 +4,34 @@ Focus
|
|||||||
- Keep VM quick green; llvmlite integration on-demand.
|
- Keep VM quick green; llvmlite integration on-demand.
|
||||||
- Using SSOT(nyash.toml + 相対using)で安定解決。
|
- Using SSOT(nyash.toml + 相対using)で安定解決。
|
||||||
- Builder/VM ガードは最小限・仕様不変(dev では診断のみ)。
|
- 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
|
Status Snapshot — 2025‑09‑27
|
||||||
- Completed
|
- 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.
|
- 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配下でも安定)
|
- Using/SSOT: JSONモジュール内部 using を相対に統一(alias配下でも安定)
|
||||||
- DebugHub: 追加ゲート `NYASH_DEBUG_SAMPLE_EVERY`(N件に1度だけ emit)。重いケースでのログ制御のため(既定OFF・ゼロコスト)。
|
- 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)
|
Decision — Variables (Option A; 2025‑09‑27)
|
||||||
- 方針: var/let は導入しない。ローカルは常に `local` で明示宣言。
|
- 方針: var/let は導入しない。ローカルは常に `local` で明示宣言。
|
||||||
@ -26,8 +47,7 @@ Decision — Variables (Option A; 2025‑09‑27)
|
|||||||
- 互換: `Main.main` が存在する場合は常にそちらを優先。両方無い場合は従来通りエラー。
|
- 互換: `Main.main` が存在する場合は常にそちらを優先。両方無い場合は従来通りエラー。
|
||||||
- オプトアウト: `NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=0|false|off` で無効化可能。
|
- オプトアウト: `NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=0|false|off` で無効化可能。
|
||||||
- Next
|
- Next
|
||||||
- Scanner.read_string_literal: 未終端で null を返すよう修正(TokenizerがERRORを積む)+小プローブ追加
|
- Heavy JSON: quick 既定ONへ再切替(LLVM 常備で段階復帰)
|
||||||
- Heavy JSON: quick 既定ONへ再切替(環境が安定したら)
|
|
||||||
- 解析ログの統一: parser/tokenizerのdevトレースは既定OFFのまま維持、必要時だけ有効化
|
- 解析ログの統一: parser/tokenizerのdevトレースは既定OFFのまま維持、必要時だけ有効化
|
||||||
- llvmlite(integration): 任意ジョブで確認(単発実行のハングはタイムアウト/リンク分離で回避)
|
- llvmlite(integration): 任意ジョブで確認(単発実行のハングはタイムアウト/リンク分離で回避)
|
||||||
|
|
||||||
@ -70,19 +90,32 @@ Guards / Policy
|
|||||||
- 既定挙動は不変(prod 用心)。
|
- 既定挙動は不変(prod 用心)。
|
||||||
- dev では診断強化(ログ/メトリクス)し、ランナー側でノイズはフィルタ。
|
- dev では診断強化(ログ/メトリクス)し、ランナー側でノイズはフィルタ。
|
||||||
|
|
||||||
|
Policy — AST Using (Status Quo)
|
||||||
|
- SSOT(nyash.toml)+AST prelude merge を維持。prod は toml 限定、dev/ci は段階的に緩和。
|
||||||
|
- 重い AST/JSON ケースは integration でカバーしつつ、quick への復帰は LLVM 有効環境で段階的に行う(順次解除)。
|
||||||
|
|
||||||
Work Queue (Next)
|
Work Queue (Next)
|
||||||
1) Scanner: 未終端文字列で必ず null を返す(Tokenizer が ERROR へ)
|
1) Scanner: 未終端文字列で必ず null を返す(Tokenizer が ERROR へ)
|
||||||
2) Heavy JSON: quick 既定ONに戻す(プローブは維持)
|
2) Heavy JSON: quick 既定ONに戻す(プローブは維持)
|
||||||
3) エラーメッセージの詳細化(expected/actual/line/column)
|
3) エラーメッセージの詳細化(expected/actual/line/column)
|
||||||
4) Ny 実行器 M2 スケルトン(JSON v0 ローダ+const/binop 等の最小実装)下書き
|
4) Ny 実行器 M2 スケルトン(JSON v0 ローダ+const/binop 等の最小実装)下書き
|
||||||
5) Parity ミニセット(VM↔llvmlite↔Ny)を用意し、差分ダッシュボード化
|
5) Parity ミニセット(VM↔llvmlite↔Ny)を用意し、差分ダッシュボード化
|
||||||
6) Router 観測ログの軽追加(dev-only, 既定OFF): class-reroute / special-reroute を DebugHub に emit(サンプル制御対応)
|
6) Router: Known/Union 方針の磨き込み(挙動不変)
|
||||||
7) LLVM ハーネスの MIR ダンプに certainty 表示(挙動不変の診断整合)
|
- 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)
|
Update — @local expansion promotion (2025‑09‑27)
|
||||||
- すべてのランナーモードに `preexpand_at_local` を適用(common/llvm/pyvm に加え vm/selfhost へも導入)。
|
- すべてのランナーモードに `preexpand_at_local` を適用(common/llvm/pyvm に加え vm/selfhost へも導入)。
|
||||||
- Docs を更新し、構文糖衣が標準で有効であることを明記。
|
- Docs を更新し、構文糖衣が標準で有効であることを明記。
|
||||||
|
|
||||||
|
Plan — Router Minimalism (継続方針)
|
||||||
|
- 特殊メソッド表は “toString→str(互換:stringify), equals/1” の範囲から、ユースが発生したもののみ点で追加。
|
||||||
|
- 既定の挙動・言語仕様は変更しない(フォールバックの拡大はしない)。
|
||||||
|
- 測定: DebugHub(resolve.*)ログと LLVM の `NYASH_LLVM_TRACE_CALLS` を併用し、Union 経路を可視化。
|
||||||
|
|
||||||
Runbook(抜粋)
|
Runbook(抜粋)
|
||||||
- VM quick: `tools/smokes/v2/run.sh --profile quick`
|
- VM quick: `tools/smokes/v2/run.sh --profile quick`
|
||||||
- LLVM llvmlite: `cargo build --release --features llvm && tools/smokes/v2/run.sh --profile integration`
|
- 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)
|
- 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.
|
- 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-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.
|
- 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
|
- File: apps/selfhost/vm/boxes/mir_vm_min.nyash
|
||||||
- Entry: apps/selfhost/vm/mir_min_entry.nyash (optional thin wrapper)
|
- 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(例示アプリ、他で十分カバー)
|
- 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)
|
- 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)
|
- 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.
|
- 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):
|
- Next (focused):
|
||||||
1) Run integration smokes for JSON heavy with DebugHub ON and collect /tmp logs
|
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_branch_phi_vm.sh — SKIP (rewrite/materialize pending)
|
||||||
- userbox_toString_mapping_vm.sh — SKIP (mapping pending)
|
- userbox_toString_mapping_vm.sh — SKIP (mapping pending)
|
||||||
- Rationale: keep quick green while surfacing remaining gaps as SKIP with clear reasons.
|
- 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)
|
Update — 2025-09-27 (Loop‑Form Scope Debug & AOT PoC — Plan)
|
||||||
- Added design doc: docs/design/loopform-scope-debug-and-aot.md
|
- 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.
|
- 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)
|
- Acceptance (Phase‑1)
|
||||||
- Debug JSONL has resolve/ssa events with region_id and choices; PASS cases unchanged (OFF)
|
- 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
|
- 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`(重複回避; 段階移行)
|
||||||
|
|||||||
@ -40,6 +40,8 @@ mir_refbarrier_unify_poc = []
|
|||||||
llvm-harness = []
|
llvm-harness = []
|
||||||
llvm-inkwell-legacy = ["dep:inkwell"]
|
llvm-inkwell-legacy = ["dep:inkwell"]
|
||||||
llvm = ["llvm-harness"]
|
llvm = ["llvm-harness"]
|
||||||
|
# Legacy AST interpreter (off by default). Gates NyashInterpreter usage in runner/tests.
|
||||||
|
interpreter-legacy = []
|
||||||
# (removed) Optional modular MIR builder feature
|
# (removed) Optional modular MIR builder feature
|
||||||
# cranelift-jit = [ # ARCHIVED: Moved to archive/jit-cranelift/ during Phase 15
|
# cranelift-jit = [ # ARCHIVED: Moved to archive/jit-cranelift/ during Phase 15
|
||||||
# "dep:cranelift-codegen",
|
# "dep:cranelift-codegen",
|
||||||
|
|||||||
14
Makefile
14
Makefile
@ -1,6 +1,7 @@
|
|||||||
# Nyash selfhosting-dev quick targets
|
# 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:
|
build:
|
||||||
cargo build --features cranelift-jit
|
cargo build --features cranelift-jit
|
||||||
@ -28,6 +29,17 @@ clean:
|
|||||||
|
|
||||||
quick: build-release smoke-selfhost
|
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:
|
fmt:
|
||||||
cargo fmt --all
|
cargo fmt --all
|
||||||
|
|
||||||
|
|||||||
73
README.ja.md
73
README.ja.md
@ -19,6 +19,23 @@ AST JSON v0(マクロ/ブリッジ): `docs/reference/ir/ast-json-v0.md`
|
|||||||
セルフホスト1枚ガイド: `docs/how-to/self-hosting.md`
|
セルフホスト1枚ガイド: `docs/how-to/self-hosting.md`
|
||||||
ExternCall(env.*)と println 正規化: `docs/reference/runtime/externcall.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>`(N件ごとに出力)
|
||||||
|
|
||||||
|
レイヤー・ガード(origin→observe→rewrite の一方向)
|
||||||
|
- スクリプト: `tools/dev/check_builder_layers.sh`
|
||||||
|
|
||||||
開発ショートカット(Operator Boxes + JSON)
|
開発ショートカット(Operator Boxes + JSON)
|
||||||
- JSON最小(Roundtrip/Nested を一発): `./tools/opbox-json.sh`
|
- JSON最小(Roundtrip/Nested を一発): `./tools/opbox-json.sh`
|
||||||
- quick 全体(軽量プリフライト+timeout 180s): `./tools/opbox-quick.sh`
|
- quick 全体(軽量プリフライト+timeout 180s): `./tools/opbox-quick.sh`
|
||||||
@ -59,7 +76,7 @@ Phase‑15(2025‑09)アップデート
|
|||||||
<a id="self-hosting"></a>
|
<a id="self-hosting"></a>
|
||||||
## 🧪 Self-Hosting(自己ホスト開発)
|
## 🧪 Self-Hosting(自己ホスト開発)
|
||||||
- ガイド: `docs/how-to/self-hosting.md`
|
- ガイド: `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`
|
- スモーク: `bash tools/jit_smoke.sh` / `bash tools/selfhost_vm_smoke.sh`
|
||||||
- Makefile: `make run-minimal`, `make smoke-selfhost`
|
- 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/インタープリタはフィーチャーで切替
|
Phase‑15(自己ホスト期): ASTインタープリタは任意featureで明示ON
|
||||||
- 既定ビルド: `--backend vm` は PyVM 実行(python3 + `tools/pyvm_runner.py` が必要)
|
- 既定ビルド: `--backend vm` は PyVM 経路(python3 + `tools/pyvm_runner.py` が必要)/Rust VM(MIR)
|
||||||
- レガシー Rust VM/インタープリターを有効化するには:
|
- レガシー AST インタープリタを有効化するには(通常は不要):
|
||||||
```bash
|
```bash
|
||||||
cargo build --release --features vm-legacy,interpreter-legacy
|
cargo build --release --features interpreter-legacy
|
||||||
```
|
```
|
||||||
以降、`--backend vm`/`--backend interpreter` が従来経路で動作します。
|
|
||||||
|
|
||||||
### 1. **インタープリターモード** (開発用)
|
### 1. **インタープリターモード** (開発用)
|
||||||
```bash
|
```bash
|
||||||
@ -178,13 +194,23 @@ cargo build --release --features cranelift-jit
|
|||||||
- 最高性能
|
- 最高性能
|
||||||
- 簡単配布
|
- 簡単配布
|
||||||
|
|
||||||
### 4. **ネイティブバイナリ(LLVM AOT)**
|
### 4. **ネイティブバイナリ(LLVM AOT, llvmliteハーネス)**
|
||||||
```bash
|
```bash
|
||||||
LLVM_SYS_180_PREFIX=$(llvm-config-18 --prefix) \
|
# ハーネス+CLI をビルド(LLVM_SYS_180_PREFIX不要)
|
||||||
cargo build --release --features llvm
|
cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm
|
||||||
NYASH_LLVM_OBJ_OUT=$PWD/nyash_llvm_temp.o \
|
|
||||||
./target/release/nyash --backend llvm program.nyash
|
# ハーネス経由で 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
|
cc nyash_llvm_temp.o -L crates/nyrt/target/release -Wl,--whole-archive -lnyrt -Wl,--no-whole-archive -lpthread -ldl -lm -o myapp
|
||||||
./myapp
|
./myapp
|
||||||
```
|
```
|
||||||
@ -195,6 +221,7 @@ tools/smoke_aot_vs_vm.sh examples/aot_min_string_len.nyash
|
|||||||
```
|
```
|
||||||
|
|
||||||
### LLVM バックエンドの補足
|
### 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`: `--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_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ベースに統一。
|
- 削除された `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}` は現在のプロジェクトルートに展開されます。
|
- `{root}` は現在のプロジェクトルートに展開されます。
|
||||||
- 現状は最小機能(OS別/依存/並列は未対応)。
|
- 現状は最小機能(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`
|
## 🧰 一発ビルド(MVP): `nyash --build`
|
||||||
|
|||||||
68
README.md
68
README.md
@ -19,7 +19,7 @@ Execution Status (Feature Additions Pause)
|
|||||||
- `--backend vm` (PyVM harness)
|
- `--backend vm` (PyVM harness)
|
||||||
- Inactive/Sealed
|
- Inactive/Sealed
|
||||||
- `--backend cranelift`, `--jit-direct` (sealed; use LLVM harness)
|
- `--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
|
Quick pointers
|
||||||
- Emit object with harness: set `NYASH_LLVM_USE_HARNESS=1` and `NYASH_LLVM_OBJ_OUT=<path>` (defaults in tools use `tmp/`).
|
- Emit object with harness: set `NYASH_LLVM_USE_HARNESS=1` and `NYASH_LLVM_OBJ_OUT=<path>` (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`.
|
Self‑hosting one‑pager: `docs/how-to/self-hosting.md`.
|
||||||
ExternCall (env.*) and println normalization: `docs/reference/runtime/externcall.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=<N>` (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)
|
Profiles (quick)
|
||||||
- `--profile dev` → Macros ON (strict), PyVM dev向け設定を適用(必要に応じて環境で上書き可)
|
- `--profile dev` → Macros ON (strict), PyVM dev向け設定を適用(必要に応じて環境で上書き可)
|
||||||
- `--profile lite` → Macros OFF の軽量実行
|
- `--profile lite` → Macros OFF の軽量実行
|
||||||
@ -76,7 +116,7 @@ Specs & Constraints
|
|||||||
<a id="self-hosting"></a>
|
<a id="self-hosting"></a>
|
||||||
## 🧪 Self‑Hosting (Dev Focus)
|
## 🧪 Self‑Hosting (Dev Focus)
|
||||||
- Guide: `docs/how-to/self-hosting.md`
|
- 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`
|
- Smokes: `bash tools/jit_smoke.sh` / `bash tools/selfhost_vm_smoke.sh`
|
||||||
- JSON (Operator Boxes, dev): `./tools/opbox-json.sh` / `./tools/opbox-quick.sh`
|
- JSON (Operator Boxes, dev): `./tools/opbox-json.sh` / `./tools/opbox-quick.sh`
|
||||||
- Makefile: `make run-minimal`, `make smoke-selfhost`
|
- Makefile: `make run-minimal`, `make smoke-selfhost`
|
||||||
@ -200,13 +240,23 @@ cargo build --release --features cranelift-jit
|
|||||||
- Maximum performance
|
- Maximum performance
|
||||||
- Easy distribution
|
- Easy distribution
|
||||||
|
|
||||||
### 4. **Native Binary (LLVM AOT)**
|
### 4. **Native Binary (LLVM AOT, llvmlite harness)**
|
||||||
```bash
|
```bash
|
||||||
LLVM_SYS_180_PREFIX=$(llvm-config-18 --prefix) \
|
# Build harness + CLI (no LLVM_SYS_180_PREFIX needed)
|
||||||
cargo build --release --features llvm
|
cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm
|
||||||
NYASH_LLVM_OBJ_OUT=$PWD/nyash_llvm_temp.o \
|
|
||||||
./target/release/nyash --backend llvm program.nyash
|
# Emit and run native executable via harness
|
||||||
# Link and run
|
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
|
cc nyash_llvm_temp.o -L crates/nyrt/target/release -Wl,--whole-archive -lnyrt -Wl,--no-whole-archive -lpthread -ldl -lm -o myapp
|
||||||
./myapp
|
./myapp
|
||||||
```
|
```
|
||||||
@ -254,7 +304,7 @@ Key options (minimal)
|
|||||||
- `--target <triple>` (only when needed)
|
- `--target <triple>` (only when needed)
|
||||||
|
|
||||||
Notes
|
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.
|
- 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).
|
- On WSL if the window doesn’t show, see `docs/guides/cranelift_aot_egui_hello.md` (Wayland→X11).
|
||||||
|
|
||||||
|
|||||||
@ -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からの完全脱却
|
Notes
|
||||||
- **Everything is Box**: 全てをNyash Boxで実装
|
- Error messages aim to include: “Error at line X, column Y: …”.
|
||||||
- **80/20ルール**: 動作優先、最適化は後
|
- Unterminated string → tokenizer emits "Unterminated string literal" (locked by quick smoke).
|
||||||
- **段階切り替え**: `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協働
|
|
||||||
|
|||||||
@ -59,9 +59,14 @@ box JsonParser {
|
|||||||
// Step 2: 構文解析
|
// Step 2: 構文解析
|
||||||
local result = me.parse_value()
|
local result = me.parse_value()
|
||||||
|
|
||||||
// Step 3: 余剰トークンチェック
|
// Step 3: 余剰トークンチェック(詳細情報付き)
|
||||||
if result != null and not me.is_at_end() {
|
if result != null and not me.is_at_end() {
|
||||||
|
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")
|
me.add_error("Unexpected tokens after JSON value")
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
apps/selfhost/vm/README.md
Normal file
14
apps/selfhost/vm/README.md
Normal file
@ -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.
|
||||||
112
apps/selfhost/vm/boxes/mir_vm_m2.nyash
Normal file
112
apps/selfhost/vm/boxes/mir_vm_m2.nyash
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,9 +7,17 @@
|
|||||||
// {"op":"ret","value":1}
|
// {"op":"ret","value":1}
|
||||||
// ]}]}]
|
// ]}]}]
|
||||||
// }
|
// }
|
||||||
// 振る舞い: 最初の const i64 の値を読み取り、print する。ret は value スロット参照を想定するが、MVPでは無視。
|
// 振る舞い:
|
||||||
|
// - M1: 最初の const i64 の値を読み取り print
|
||||||
|
// - M2: const/binop/compare/ret を最小実装(簡易スキャンで安全に解釈)
|
||||||
|
|
||||||
static box MirVmMin {
|
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) {
|
index_of_from(hay, needle, pos) {
|
||||||
if pos < 0 { pos = 0 }
|
if pos < 0 { pos = 0 }
|
||||||
@ -26,11 +34,11 @@ static box MirVmMin {
|
|||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
read_digits(json, pos) {
|
read_digits(text, pos) {
|
||||||
local out = ""
|
local out = ""
|
||||||
local i = pos
|
local i = pos
|
||||||
loop (true) {
|
loop (true) {
|
||||||
local s = json.substring(i, i+1)
|
local s = text.substring(i, i+1)
|
||||||
if s == "" { break }
|
if s == "" { break }
|
||||||
if s == "0" || s == "1" || s == "2" || s == "3" || s == "4" || s == "5" || s == "6" || s == "7" || s == "8" || s == "9" {
|
if s == "0" || s == "1" || s == "2" || s == "3" || s == "4" || s == "5" || s == "6" || s == "7" || s == "8" || s == "9" {
|
||||||
out = out + s
|
out = out + s
|
||||||
@ -63,10 +71,10 @@ static box MirVmMin {
|
|||||||
if n == 0 { return "0" }
|
if n == 0 { return "0" }
|
||||||
local v = n
|
local v = n
|
||||||
local out = ""
|
local out = ""
|
||||||
|
local digits = "0123456789"
|
||||||
loop (v > 0) {
|
loop (v > 0) {
|
||||||
local d = v % 10
|
local d = v % 10
|
||||||
local ch = "0"
|
local ch = digits.substring(d, d+1)
|
||||||
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
|
out = ch + out
|
||||||
v = v / 10
|
v = v / 10
|
||||||
}
|
}
|
||||||
@ -74,26 +82,228 @@ static box MirVmMin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MVP: 最初の const i64 の値を抽出
|
// MVP: 最初の const i64 の値を抽出
|
||||||
_extract_first_const_i64(json) {
|
_extract_first_const_i64(text) {
|
||||||
if json == null { return 0 }
|
if text == null { return 0 }
|
||||||
// "op":"const" を探す
|
// "op":"const" を探す
|
||||||
local p = json.indexOf("\"op\":\"const\"")
|
local p = text.indexOf("\"op\":\"const\"")
|
||||||
if p < 0 { return 0 }
|
if p < 0 { return 0 }
|
||||||
// そこから "\"value\":{\"type\":\"i64\",\"value\":" を探す
|
// そこから "\"value\":{\"type\":\"i64\",\"value\":" を探す
|
||||||
local key = "\"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 }
|
if q < 0 { return 0 }
|
||||||
q = q + key.length()
|
q = q + key.length()
|
||||||
// 連続する数字を読む
|
// 連続する数字を読む
|
||||||
local digits = me.read_digits(json, q)
|
local digits = me.read_digits(text, q)
|
||||||
if digits == "" { return 0 }
|
if digits == "" { return 0 }
|
||||||
return me._str_to_int(digits)
|
return me._str_to_int(digits)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 実行: 値を print し、0 を返す(MVP)。将来は exit code 連動可。
|
// --- M2 追加: 最小 MIR 実行(const/binop/compare/ret) ---
|
||||||
run(mir_json_text) {
|
_get_map(regs, key) { if regs.has(key) { return regs.get(key) } return 0 }
|
||||||
local v = me._extract_first_const_i64(mir_json_text)
|
_set_map(regs, key, val) { regs.set(key, val) }
|
||||||
print(me._int_to_str(v))
|
_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
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
32
apps/selfhost/vm/boxes/vm_kernel_box.nyash
Normal file
32
apps/selfhost/vm/boxes/vm_kernel_box.nyash
Normal file
@ -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\":[]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -7,7 +7,9 @@ static box Main {
|
|||||||
main(args) {
|
main(args) {
|
||||||
// 既定の最小 MIR(JSON v0)
|
// 既定の最小 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}]}]}]}"
|
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 } } }
|
if args != null { if args.size() > 0 { local s = args.get(0) if s != null { json = s } } }
|
||||||
return MirVmMin.run(json)
|
local v = MirVmMin._run_min(json)
|
||||||
|
print(MirVmMin._int_to_str(v))
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
docs/abi/vm-kernel.md
Normal file
47
docs/abi/vm-kernel.md
Normal file
@ -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.
|
||||||
90
docs/development/roadmap/phases/phase-15.7/README.md
Normal file
90
docs/development/roadmap/phases/phase-15.7/README.md
Normal file
@ -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下地の計画
|
||||||
@ -19,6 +19,12 @@
|
|||||||
5) LLVM 統合(任意・AOT/ハーネス)
|
5) LLVM 統合(任意・AOT/ハーネス)
|
||||||
- 実行: `tools/smokes/v2/run.sh --profile integration`
|
- 実行: `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)
|
- 期待出力: `Result: 0`(selfhost‑minimal)
|
||||||
- スモーク:全成功(非 0 は失敗)
|
- スモーク:全成功(非 0 は失敗)
|
||||||
|
|||||||
76
docs/reference/language/quick-reference.md
Normal file
76
docs/reference/language/quick-reference.md
Normal file
@ -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: <message>`
|
||||||
|
- 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.
|
||||||
@ -26,15 +26,52 @@ impl MirInterpreter {
|
|||||||
) -> Result<VMValue, VMError> {
|
) -> Result<VMValue, VMError> {
|
||||||
match callee {
|
match callee {
|
||||||
Callee::Global(func_name) => self.execute_global_function(func_name, args),
|
Callee::Global(func_name) => self.execute_global_function(func_name, args),
|
||||||
Callee::Method {
|
Callee::Method { box_name: _, method, receiver, certainty: _, } => {
|
||||||
box_name: _,
|
|
||||||
method,
|
|
||||||
receiver,
|
|
||||||
certainty: _,
|
|
||||||
} => {
|
|
||||||
if let Some(recv_id) = receiver {
|
if let Some(recv_id) = receiver {
|
||||||
let recv_val = self.reg_load(*recv_id)?;
|
let recv_val = self.reg_load(*recv_id)?;
|
||||||
let dev_trace = std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1");
|
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::<crate::boxes::array::ArrayBox>() {
|
||||||
|
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";
|
let is_kw = method == &"keyword_to_token_type";
|
||||||
if dev_trace && is_kw {
|
if dev_trace && is_kw {
|
||||||
let a0 = args.get(0).and_then(|id| self.reg_load(*id).ok());
|
let a0 = args.get(0).and_then(|id| self.reg_load(*id).ok());
|
||||||
|
|||||||
@ -62,18 +62,20 @@ fn reroute_to_correct_method(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Try mapping special methods to canonical targets (table-driven).
|
/// 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(
|
fn try_special_reroute(
|
||||||
interp: &mut MirInterpreter,
|
interp: &mut MirInterpreter,
|
||||||
recv_cls: &str,
|
recv_cls: &str,
|
||||||
parsed: &ParsedSig<'_>,
|
parsed: &ParsedSig<'_>,
|
||||||
arg_vals: Option<&[VMValue]>,
|
arg_vals: Option<&[VMValue]>,
|
||||||
) -> Option<Result<VMValue, VMError>> {
|
) -> Option<Result<VMValue, VMError>> {
|
||||||
// toString → stringify
|
// toString → str(互換: stringify)
|
||||||
if parsed.method == "toString" && parsed.arity_str == "0" {
|
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 base = recv_cls.strip_suffix("Instance").unwrap_or(recv_cls);
|
||||||
let candidates = [
|
let candidates = [
|
||||||
|
format!("{}.str/0", recv_cls),
|
||||||
|
format!("{}.str/0", base),
|
||||||
format!("{}.stringify/0", recv_cls),
|
format!("{}.stringify/0", recv_cls),
|
||||||
format!("{}.stringify/0", base),
|
format!("{}.stringify/0", base),
|
||||||
];
|
];
|
||||||
@ -91,7 +93,7 @@ fn try_special_reroute(
|
|||||||
"method": parsed.method,
|
"method": parsed.method,
|
||||||
"arity": parsed.arity_str,
|
"arity": parsed.arity_str,
|
||||||
"target": name,
|
"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));
|
return Some(interp.exec_function_inner(&f, arg_vals));
|
||||||
|
|||||||
@ -343,7 +343,9 @@ impl P2PBox {
|
|||||||
if let Ok(mut li) = last_intent.write() {
|
if let Ok(mut li) = last_intent.write() {
|
||||||
*li = Some(env.intent.get_name().to_string_box().value);
|
*li = Some(env.intent.get_name().to_string_box().value);
|
||||||
}
|
}
|
||||||
// 最小インタープリタで FunctionBox を実行
|
// 最小インタープリタで FunctionBox を実行(legacy, feature-gated)
|
||||||
|
#[cfg(feature = "interpreter-legacy")]
|
||||||
|
{
|
||||||
let mut interp = crate::interpreter::NyashInterpreter::new();
|
let mut interp = crate::interpreter::NyashInterpreter::new();
|
||||||
// キャプチャ注入
|
// キャプチャ注入
|
||||||
for (k, v) in func_clone.env.captures.iter() {
|
for (k, v) in func_clone.env.captures.iter() {
|
||||||
@ -370,6 +372,13 @@ impl P2PBox {
|
|||||||
let _ = interp.execute_statement(st);
|
let _ = interp.execute_statement(st);
|
||||||
}
|
}
|
||||||
crate::runtime::global_hooks::pop_task_scope();
|
crate::runtime::global_hooks::pop_task_scope();
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "interpreter-legacy"))]
|
||||||
|
{
|
||||||
|
if crate::config::env::cli_verbose() {
|
||||||
|
eprintln!("[warn] FunctionBox handler requires interpreter-legacy; skipped execution");
|
||||||
|
}
|
||||||
|
}
|
||||||
if once {
|
if once {
|
||||||
flag.store(false, Ordering::SeqCst);
|
flag.store(false, Ordering::SeqCst);
|
||||||
if let Ok(mut flags) = flags_arc.write() {
|
if let Ok(mut flags) = flags_arc.write() {
|
||||||
|
|||||||
@ -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
|
# 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:
|
if not use_unified:
|
||||||
# Fall back to legacy dispatching
|
# Fall back to legacy dispatching
|
||||||
return lower_legacy_call(owner, builder, mir_call, dst_vid, vmap, resolver)
|
return lower_legacy_call(owner, builder, mir_call, dst_vid, vmap, resolver)
|
||||||
|
|||||||
@ -37,6 +37,9 @@ mod plugin_sigs; // plugin signature loader
|
|||||||
mod stmts;
|
mod stmts;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod vars; // variables/scope helpers // small loop helpers (header/exit context)
|
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
|
// Unified member property kinds for computed/once/birth_once
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
@ -99,6 +102,11 @@ pub struct MirBuilder {
|
|||||||
/// Index of static methods seen during lowering: name -> [(BoxName, arity)]
|
/// Index of static methods seen during lowering: name -> [(BoxName, arity)]
|
||||||
pub(super) static_method_index: std::collections::HashMap<String, Vec<(String, usize)>>,
|
pub(super) static_method_index: std::collections::HashMap<String, Vec<(String, usize)>>,
|
||||||
|
|
||||||
|
/// Fast lookup: method+arity tail → candidate function names (e.g., ".str/0" → ["JsonNode.str/0", ...])
|
||||||
|
pub(super) method_tail_index: std::collections::HashMap<String, Vec<String>>,
|
||||||
|
/// Source size snapshot to detect when to rebuild the tail index
|
||||||
|
pub(super) method_tail_index_source_len: usize,
|
||||||
|
|
||||||
// include guards removed
|
// include guards removed
|
||||||
|
|
||||||
/// Loop context stacks for lowering break/continue inside nested control flow
|
/// Loop context stacks for lowering break/continue inside nested control flow
|
||||||
@ -169,6 +177,8 @@ impl MirBuilder {
|
|||||||
plugin_method_sigs,
|
plugin_method_sigs,
|
||||||
current_static_box: None,
|
current_static_box: None,
|
||||||
static_method_index: std::collections::HashMap::new(),
|
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_header_stack: Vec::new(),
|
||||||
loop_exit_stack: Vec::new(),
|
loop_exit_stack: Vec::new(),
|
||||||
@ -256,6 +266,56 @@ impl MirBuilder {
|
|||||||
self.debug_scope_stack.last().cloned()
|
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<String> {
|
||||||
|
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<S: AsRef<str>>(&mut self, tail: S) -> Vec<String> {
|
||||||
|
self.ensure_method_tail_index();
|
||||||
|
self.method_tail_index
|
||||||
|
.get(tail.as_ref())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Build a complete MIR module from AST
|
/// Build a complete MIR module from AST
|
||||||
pub fn build_module(&mut self, ast: ASTNode) -> Result<MirModule, String> {
|
pub fn build_module(&mut self, ast: ASTNode) -> Result<MirModule, String> {
|
||||||
@ -354,84 +414,13 @@ impl MirBuilder {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|f| f.signature.name.clone());
|
.map(|f| f.signature.name.clone());
|
||||||
let dbg_region_id = self.debug_current_region_id();
|
let dbg_region_id = self.debug_current_region_id();
|
||||||
if let Some(ref mut function) = self.current_function {
|
// P0: PHI の軽量補強と観測は、関数ブロック取得前に実施して借用競合を避ける
|
||||||
// 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 {
|
if let MirInstruction::Phi { dst, inputs } = &instruction {
|
||||||
// Propagate value_types when all inputs share the same known type
|
origin::phi::propagate_phi_meta(self, *dst, inputs);
|
||||||
let mut common_ty: Option<super::MirType> = None;
|
observe::ssa::emit_phi(self, *dst, inputs);
|
||||||
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<String> = 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<serde_json::Value> = 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(ref mut function) = self.current_function {
|
||||||
if let Some(block) = function.get_block_mut(block_id) {
|
if let Some(block) = function.get_block_mut(block_id) {
|
||||||
if utils::builder_debug_enabled() {
|
if utils::builder_debug_enabled() {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@ -602,7 +591,10 @@ impl MirBuilder {
|
|||||||
// VM will treat plain NewBox as constructed; dev verify warns if needed.
|
// VM will treat plain NewBox as constructed; dev verify warns if needed.
|
||||||
// - For builtins/plugins, keep BoxCall("birth") fallback to preserve legacy init.
|
// - For builtins/plugins, keep BoxCall("birth") fallback to preserve legacy init.
|
||||||
let is_user_box = self.user_defined_boxes.contains(&class);
|
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");
|
let birt_mid = resolve_slot_by_type_name(&class, "birth");
|
||||||
self.emit_box_or_plugin_call(
|
self.emit_box_or_plugin_call(
|
||||||
None,
|
None,
|
||||||
|
|||||||
@ -81,12 +81,117 @@ impl super::MirBuilder {
|
|||||||
return self.emit_legacy_call(dst, target, args);
|
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<String> = 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
|
// Convert CallTarget to Callee using the new module
|
||||||
let callee = call_unified::convert_target_to_callee(
|
if let CallTarget::Global(ref _n) = target { /* dev trace removed */ }
|
||||||
target,
|
// 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_origin_newbox,
|
||||||
&self.value_types,
|
&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
|
// Validate call arguments
|
||||||
call_unified::validate_call_args(&callee, &args)?;
|
call_unified::validate_call_args(&callee, &args)?;
|
||||||
@ -114,49 +219,10 @@ impl super::MirBuilder {
|
|||||||
args: Vec<ValueId>,
|
args: Vec<ValueId>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
match target {
|
match target {
|
||||||
CallTarget::Method { receiver, method, box_type } => {
|
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
|
// LEGACY PATH (after unified migration):
|
||||||
let mut is_user_box = false;
|
// Instance→Function rewrite is centralized in unified call path.
|
||||||
let mut class_name_opt: Option<String> = None;
|
// Legacy path no longer functionizes; always use Box/Plugin call here.
|
||||||
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.)
|
|
||||||
self.emit_box_or_plugin_call(dst, receiver, method, None, args, EffectMask::IO)
|
self.emit_box_or_plugin_call(dst, receiver, method, None, args, EffectMask::IO)
|
||||||
},
|
},
|
||||||
CallTarget::Constructor(box_type) => {
|
CallTarget::Constructor(box_type) => {
|
||||||
@ -400,6 +466,7 @@ impl super::MirBuilder {
|
|||||||
name: String,
|
name: String,
|
||||||
args: Vec<ASTNode>,
|
args: Vec<ASTNode>,
|
||||||
) -> Result<ValueId, String> {
|
) -> Result<ValueId, String> {
|
||||||
|
// dev trace removed
|
||||||
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
|
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(|| "<none>".to_string());
|
let cur_fun = self.current_function.as_ref().map(|f| f.signature.name.clone()).unwrap_or_else(|| "<none>".to_string());
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@ -440,19 +507,20 @@ impl super::MirBuilder {
|
|||||||
arg_values.push(self.build_expression(a)?);
|
arg_values.push(self.build_expression(a)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3.2: Use unified call for basic functions like print
|
// Special-case: global str(x) → x.str() に正規化(内部は関数へ統一される)
|
||||||
let use_unified = std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1";
|
if name == "str" && arg_values.len() == 1 {
|
||||||
|
|
||||||
if use_unified {
|
|
||||||
// New unified path - use emit_unified_call with Global target
|
|
||||||
let dst = self.value_gen.next();
|
let dst = self.value_gen.next();
|
||||||
self.emit_unified_call(
|
// Use unified method emission; downstream rewrite will functionize as needed
|
||||||
Some(dst),
|
self.emit_method_call(Some(dst), arg_values[0], "str".to_string(), vec![])?;
|
||||||
CallTarget::Global(name),
|
return Ok(dst);
|
||||||
arg_values,
|
}
|
||||||
)?;
|
|
||||||
Ok(dst)
|
// Phase 3.2: Unified call is default ON, but only use it for known builtins/externs.
|
||||||
} else {
|
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
|
// Legacy path
|
||||||
let dst = self.value_gen.next();
|
let dst = self.value_gen.next();
|
||||||
|
|
||||||
@ -461,6 +529,7 @@ impl super::MirBuilder {
|
|||||||
let callee = match self.resolve_call_target(&name) {
|
let callee = match self.resolve_call_target(&name) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
|
// dev trace removed
|
||||||
// Fallback: if exactly one static method with this name and arity is known, call it.
|
// 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) {
|
if let Some(cands) = self.static_method_index.get(&name) {
|
||||||
let mut matches: Vec<(String, usize)> = cands
|
let mut matches: Vec<(String, usize)> = cands
|
||||||
@ -517,6 +586,15 @@ impl super::MirBuilder {
|
|||||||
effects: EffectMask::READ.add(Effect::ReadHeap),
|
effects: EffectMask::READ.add(Effect::ReadHeap),
|
||||||
})?;
|
})?;
|
||||||
Ok(dst)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,10 @@ use super::extern_calls;
|
|||||||
|
|
||||||
/// Check if unified call system is enabled
|
/// Check if unified call system is enabled
|
||||||
pub fn is_unified_call_enabled() -> bool {
|
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
|
/// Convert CallTarget to Callee
|
||||||
|
|||||||
@ -15,7 +15,7 @@ impl super::MirBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3.1: Use unified call with CallTarget::Value for indirect calls
|
// 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 {
|
if use_unified {
|
||||||
// New unified path - use emit_unified_call with Value target
|
// New unified path - use emit_unified_call with Value target
|
||||||
|
|||||||
@ -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;
|
use crate::ast::ASTNode;
|
||||||
|
|
||||||
// Lifecycle routines extracted from builder.rs
|
// Lifecycle routines extracted from builder.rs
|
||||||
@ -265,6 +265,29 @@ impl super::MirBuilder {
|
|||||||
|
|
||||||
module.add_function(function);
|
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)
|
Ok(module)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,8 +91,7 @@ impl MirBuilder {
|
|||||||
method: String,
|
method: String,
|
||||||
arguments: &[ASTNode],
|
arguments: &[ASTNode],
|
||||||
) -> Result<ValueId, String> {
|
) -> Result<ValueId, String> {
|
||||||
// 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
|
let object_value = self
|
||||||
.pin_to_slot(object_value, "@recv")
|
.pin_to_slot(object_value, "@recv")
|
||||||
.unwrap_or(object_value);
|
.unwrap_or(object_value);
|
||||||
@ -125,372 +124,16 @@ impl MirBuilder {
|
|||||||
if let MirType::Box(bn) = t { class_name_opt = Some(bn.clone()); }
|
if let MirType::Box(bn) = t { class_name_opt = Some(bn.clone()); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Instance→Function rewrite (obj.m(a) → Box.m/Arity(obj,a))
|
// レガシー経路(BoxCall/Plugin)へ送る(安定優先・挙動不変)。
|
||||||
// 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<String> = 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<String> = 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<String> = 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
|
|
||||||
let result_id = self.value_gen.next();
|
let result_id = self.value_gen.next();
|
||||||
self.emit_box_or_plugin_call(
|
self.emit_box_or_plugin_call(
|
||||||
Some(result_id),
|
Some(result_id),
|
||||||
object_value,
|
object_value,
|
||||||
method,
|
method.clone(),
|
||||||
None,
|
None,
|
||||||
arg_values,
|
arg_values,
|
||||||
crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap),
|
super::EffectMask::READ.add(super::Effect::ReadHeap),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(result_id)
|
Ok(result_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/mir/builder/observe/README.md
Normal file
23
src/mir/builder/observe/README.md
Normal file
@ -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 の機能をここに持ち込まない。
|
||||||
|
|
||||||
8
src/mir/builder/observe/mod.rs
Normal file
8
src/mir/builder/observe/mod.rs
Normal file
@ -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;
|
||||||
|
|
||||||
55
src/mir/builder/observe/resolve.rs
Normal file
55
src/mir/builder/observe/resolve.rs
Normal file
@ -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<bool> = OnceLock::new();
|
||||||
|
static SAMPLE_EVERY: OnceLock<usize> = 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::<usize>().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);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/mir/builder/observe/ssa.rs
Normal file
43
src/mir/builder/observe/ssa.rs
Normal file
@ -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<serde_json::Value> = 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);
|
||||||
|
}
|
||||||
26
src/mir/builder/origin/README.md
Normal file
26
src/mir/builder/origin/README.md
Normal file
@ -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 呼び出し、命令生成。
|
||||||
|
|
||||||
25
src/mir/builder/origin/infer.rs
Normal file
25
src/mir/builder/origin/infer.rs
Normal file
@ -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<String> = 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/mir/builder/origin/mod.rs
Normal file
13
src/mir/builder/origin/mod.rs
Normal file
@ -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;
|
||||||
|
|
||||||
38
src/mir/builder/origin/phi.rs
Normal file
38
src/mir/builder/origin/phi.rs
Normal file
@ -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<MirType> = 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<String> = 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); } }
|
||||||
|
}
|
||||||
33
src/mir/builder/rewrite/README.md
Normal file
33
src/mir/builder/rewrite/README.md
Normal file
@ -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<Result<ValueId,String>>`
|
||||||
|
- `try_unique_suffix_rewrite(builder, recv, method, args) -> Option<Result<ValueId,String>>`
|
||||||
|
- `try_known_or_unique(builder, recv, class_opt, method, args) -> Option<Result<ValueId,String>>`
|
||||||
|
- `try_early_str_like(builder, recv, class_opt, method, arity) -> Option<Result<ValueId,String>>`
|
||||||
|
- `try_special_equals(builder, recv, class_opt, method, args) -> Option<Result<ValueId,String>>`
|
||||||
|
|
||||||
|
レイヤールール
|
||||||
|
- Allowed: Builder のメタ参照/関数名生成、MirInstruction の生成(関数化結果)。
|
||||||
|
- Forbidden: origin/observe のロジックを混在させない(必要時は呼び出しで連携)。
|
||||||
|
|
||||||
|
決定原則
|
||||||
|
- Known のみ関数化(`value_origin_newbox` が根拠)。
|
||||||
|
- 表示系は規範 `str` を優先、`stringify` は当面互換として許容。
|
||||||
|
- すべての決定は dev 観測(resolve.try/choose)で可視化し、挙動は不変。
|
||||||
227
src/mir/builder/rewrite/known.rs
Normal file
227
src/mir/builder/rewrite/known.rs
Normal file
@ -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<ValueId>,
|
||||||
|
) -> Option<Result<ValueId, String>> {
|
||||||
|
// 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<ValueId>,
|
||||||
|
object_value: ValueId,
|
||||||
|
cls: &str,
|
||||||
|
method: &str,
|
||||||
|
mut arg_values: Vec<ValueId>,
|
||||||
|
) -> Option<Result<ValueId, String>> {
|
||||||
|
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<ValueId>,
|
||||||
|
) -> Option<Result<ValueId, String>> {
|
||||||
|
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<String> = 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<ValueId>,
|
||||||
|
object_value: ValueId,
|
||||||
|
method: &str,
|
||||||
|
mut arg_values: Vec<ValueId>,
|
||||||
|
) -> Option<Result<ValueId, String>> {
|
||||||
|
if !rewrite_enabled() { return None; }
|
||||||
|
if builder.value_origin_newbox.get(&object_value).is_none() { return None; }
|
||||||
|
let mut cands: Vec<String> = 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<String>,
|
||||||
|
method: &str,
|
||||||
|
arg_values: Vec<ValueId>,
|
||||||
|
) -> Option<Result<ValueId, String>> {
|
||||||
|
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<ValueId>,
|
||||||
|
object_value: ValueId,
|
||||||
|
class_name_opt: &Option<String>,
|
||||||
|
method: &str,
|
||||||
|
arg_values: Vec<ValueId>,
|
||||||
|
) -> Option<Result<ValueId, String>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
10
src/mir/builder/rewrite/mod.rs
Normal file
10
src/mir/builder/rewrite/mod.rs
Normal file
@ -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;
|
||||||
|
|
||||||
217
src/mir/builder/rewrite/special.rs
Normal file
217
src/mir/builder/rewrite/special.rs
Normal file
@ -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<String>,
|
||||||
|
method: &str,
|
||||||
|
arity: usize,
|
||||||
|
) -> Option<Result<super::super::ValueId, String>> {
|
||||||
|
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<String> = 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<String>,
|
||||||
|
method: &str,
|
||||||
|
args: Vec<super::super::ValueId>,
|
||||||
|
) -> Option<Result<super::super::ValueId, String>> {
|
||||||
|
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<super::super::ValueId>,
|
||||||
|
object_value: super::super::ValueId,
|
||||||
|
class_name_opt: &Option<String>,
|
||||||
|
method: &str,
|
||||||
|
arity: usize,
|
||||||
|
) -> Option<Result<super::super::ValueId, String>> {
|
||||||
|
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<String> = 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<super::super::ValueId>,
|
||||||
|
object_value: super::super::ValueId,
|
||||||
|
class_name_opt: &Option<String>,
|
||||||
|
method: &str,
|
||||||
|
args: Vec<super::super::ValueId>,
|
||||||
|
) -> Option<Result<super::super::ValueId, String>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@ -124,7 +124,7 @@ impl super::MirBuilder {
|
|||||||
super::utils::builder_debug_log(&format!("fallback print value={}", value));
|
super::utils::builder_debug_log(&format!("fallback print value={}", value));
|
||||||
|
|
||||||
// Phase 3.2: Use unified call for print statements
|
// 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 {
|
if use_unified {
|
||||||
// New unified path - treat print as global function
|
// New unified path - treat print as global function
|
||||||
@ -185,14 +185,19 @@ impl super::MirBuilder {
|
|||||||
) -> Result<ValueId, String> {
|
) -> Result<ValueId, String> {
|
||||||
let mut last_value = None;
|
let mut last_value = None;
|
||||||
for (i, var_name) in variables.iter().enumerate() {
|
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();
|
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 {
|
} 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);
|
self.variable_map.insert(var_name.clone(), var_id);
|
||||||
last_value = Some(value_id);
|
last_value = Some(var_id);
|
||||||
}
|
}
|
||||||
Ok(last_value.unwrap_or_else(|| self.value_gen.next()))
|
Ok(last_value.unwrap_or_else(|| self.value_gen.next()))
|
||||||
}
|
}
|
||||||
@ -314,6 +319,8 @@ impl super::MirBuilder {
|
|||||||
value: ConstValue::String(me_tag),
|
value: ConstValue::String(me_tag),
|
||||||
})?;
|
})?;
|
||||||
self.variable_map.insert("me".to_string(), me_value);
|
self.variable_map.insert("me".to_string(), me_value);
|
||||||
|
// P0: Known 化 — 分かる範囲で me の起源クラスを付与(挙動不変)。
|
||||||
|
super::origin::infer::annotate_me_origin(self, me_value);
|
||||||
Ok(me_value)
|
Ok(me_value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,12 +76,8 @@ impl super::MirBuilder {
|
|||||||
args: Vec<super::ValueId>,
|
args: Vec<super::ValueId>,
|
||||||
effects: super::EffectMask,
|
effects: super::EffectMask,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Check environment variable for unified call usage
|
// Check environment variable for unified call usage, with safe overrides for core/user boxes
|
||||||
let use_unified = std::env::var("NYASH_MIR_UNIFIED_CALL")
|
let use_unified_env = super::calls::call_unified::is_unified_call_enabled();
|
||||||
.unwrap_or_else(|_| "0".to_string()) != "0";
|
|
||||||
|
|
||||||
if use_unified {
|
|
||||||
// Use unified call emission for BoxCall
|
|
||||||
// First, try to determine the box type
|
// First, try to determine the box type
|
||||||
let mut box_type: Option<String> = self.value_origin_newbox.get(&box_val).cloned();
|
let mut box_type: Option<String> = self.value_origin_newbox.get(&box_val).cloned();
|
||||||
if box_type.is_none() {
|
if box_type.is_none() {
|
||||||
@ -93,14 +89,18 @@ impl super::MirBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Prefer legacy BoxCall for core collection boxes and user instance boxes (stability first)
|
||||||
// Use emit_unified_call with Method target
|
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 {
|
let target = super::builder_calls::CallTarget::Method {
|
||||||
box_type,
|
box_type,
|
||||||
method: method.clone(),
|
method: method.clone(),
|
||||||
receiver: box_val,
|
receiver: box_val,
|
||||||
};
|
};
|
||||||
|
|
||||||
return self.emit_unified_call(dst, target, args);
|
return self.emit_unified_call(dst, target, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,5 +55,131 @@ pub(super) fn apply_cli_directives_from_source(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lint: enforce fields at top-of-box (delegated)
|
// 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -310,7 +310,10 @@ pub fn emit_mir_json_for_harness(
|
|||||||
dst, func, callee, args, effects, ..
|
dst, func, callee, args, effects, ..
|
||||||
} => {
|
} => {
|
||||||
// Phase 15.5: Unified Call support with environment variable control
|
// 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() {
|
if use_unified && callee.is_some() {
|
||||||
// v1: Unified mir_call format
|
// 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
|
// 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"
|
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 {
|
let root = if use_v1_schema {
|
||||||
create_json_v1_root(json!(funs))
|
create_json_v1_root(json!(funs))
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
#![cfg(feature = "interpreter-legacy")]
|
||||||
|
|
||||||
use super::super::NyashRunner;
|
use super::super::NyashRunner;
|
||||||
use nyash_rust::{
|
use nyash_rust::{backend::VM, interpreter::NyashInterpreter, mir::MirCompiler, parser::NyashParser};
|
||||||
backend::VM, interpreter::NyashInterpreter, mir::MirCompiler, parser::NyashParser,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl NyashRunner {
|
impl NyashRunner {
|
||||||
/// Execute benchmark mode (split)
|
/// Execute benchmark mode (split)
|
||||||
@ -241,4 +241,3 @@ impl NyashRunner {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#![cfg(feature = "vm-legacy")]
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
use super::super::NyashRunner;
|
use super::super::NyashRunner;
|
||||||
use crate::runner::json_v0_bridge;
|
use crate::runner::json_v0_bridge;
|
||||||
|
#[cfg(feature = "interpreter-legacy")]
|
||||||
use nyash_rust::{interpreter::NyashInterpreter, parser::NyashParser};
|
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 the library crate's plugin init module rather than the bin crate root
|
||||||
use crate::cli_v;
|
use crate::cli_v;
|
||||||
use crate::runner::pipeline::{resolve_using_target, suggest_in_base};
|
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
|
// (moved) suggest_in_base is now in runner/pipeline.rs
|
||||||
|
|
||||||
|
#[cfg(feature = "interpreter-legacy")]
|
||||||
impl NyashRunner {
|
impl NyashRunner {
|
||||||
// legacy run_file_legacy removed (was commented out)
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -103,7 +103,8 @@ pub fn ny_llvmc_emit_exe_lib(
|
|||||||
.arg("exe")
|
.arg("exe")
|
||||||
.arg("--out")
|
.arg("--out")
|
||||||
.arg(exe_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); } }
|
if let Some(flags) = extra_libs { if !flags.trim().is_empty() { cmd.arg("--libs").arg(flags); } }
|
||||||
let status = cmd.status().map_err(|e| {
|
let status = cmd.status().map_err(|e| {
|
||||||
let prog_path = std::path::Path::new(cmd.get_program());
|
let prog_path = std::path::Path::new(cmd.get_program());
|
||||||
@ -146,7 +147,8 @@ pub fn ny_llvmc_emit_exe_bin(
|
|||||||
.arg("exe")
|
.arg("exe")
|
||||||
.arg("--out")
|
.arg("--out")
|
||||||
.arg(exe_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); } }
|
if let Some(flags) = extra_libs { if !flags.trim().is_empty() { cmd.arg("--libs").arg(flags); } }
|
||||||
let status = cmd.status().map_err(|e| {
|
let status = cmd.status().map_err(|e| {
|
||||||
let prog_path = std::path::Path::new(cmd.get_program());
|
let prog_path = std::path::Path::new(cmd.get_program());
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
#![cfg(feature = "interpreter-legacy")]
|
||||||
|
|
||||||
use crate::interpreter::NyashInterpreter;
|
use crate::interpreter::NyashInterpreter;
|
||||||
use crate::ast::ASTNode;
|
use crate::ast::ASTNode;
|
||||||
use crate::box_trait::{NyashBox, IntegerBox};
|
use crate::box_trait::{NyashBox, IntegerBox};
|
||||||
@ -62,4 +64,3 @@ fn functionbox_call_via_field() {
|
|||||||
let ib = out.as_any().downcast_ref::<IntegerBox>().expect("integer ret");
|
let ib = out.as_any().downcast_ref::<IntegerBox>().expect("integer ret");
|
||||||
assert_eq!(ib.value, 7);
|
assert_eq!(ib.value, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
#![cfg(feature = "interpreter-legacy")]
|
||||||
|
|
||||||
use crate::interpreter::NyashInterpreter;
|
use crate::interpreter::NyashInterpreter;
|
||||||
use crate::ast::{ASTNode, LiteralValue};
|
use crate::ast::{ASTNode, LiteralValue};
|
||||||
use crate::box_trait::{NyashBox, IntegerBox};
|
use crate::box_trait::{NyashBox, IntegerBox};
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
#![cfg(feature = "interpreter-legacy")]
|
||||||
|
|
||||||
use crate::parser::NyashParser;
|
use crate::parser::NyashParser;
|
||||||
use crate::interpreter::NyashInterpreter;
|
use crate::interpreter::NyashInterpreter;
|
||||||
|
|
||||||
|
|||||||
44
tools/dev/check_builder_layers.sh
Normal file
44
tools/dev/check_builder_layers.sh
Normal file
@ -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
|
||||||
|
|
||||||
@ -11,18 +11,23 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
activate_pyvm() {
|
activate_pyvm() {
|
||||||
export NYASH_DISABLE_PLUGINS=1
|
unset NYASH_DISABLE_PLUGINS || true
|
||||||
export NYASH_VM_USE_PY=1
|
export NYASH_VM_USE_PY=1
|
||||||
export NYASH_PIPE_USE_PYVM=1
|
export NYASH_PIPE_USE_PYVM=1
|
||||||
export NYASH_NY_COMPILER_TIMEOUT_MS=${NYASH_NY_COMPILER_TIMEOUT_MS:-2000}
|
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() {
|
activate_bridge() {
|
||||||
export NYASH_DISABLE_PLUGINS=1
|
unset NYASH_DISABLE_PLUGINS || true
|
||||||
unset NYASH_VM_USE_PY || true
|
unset NYASH_VM_USE_PY || true
|
||||||
export NYASH_NY_COMPILER_TIMEOUT_MS=${NYASH_NY_COMPILER_TIMEOUT_MS:-2000}
|
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() {
|
reset_env() {
|
||||||
@ -30,6 +35,8 @@ reset_env() {
|
|||||||
unset NYASH_PIPE_USE_PYVM || true
|
unset NYASH_PIPE_USE_PYVM || true
|
||||||
unset NYASH_DISABLE_PLUGINS || true
|
unset NYASH_DISABLE_PLUGINS || true
|
||||||
unset NYASH_NY_COMPILER_TIMEOUT_MS || 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_MIR_NO_PHI || true
|
||||||
unset NYASH_VERIFY_ALLOW_NO_PHI || true
|
unset NYASH_VERIFY_ALLOW_NO_PHI || true
|
||||||
unset NYASH_LLVM_USE_HARNESS || 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_COMPARE_CALL=1
|
||||||
export NYASH_BUILDER_OPERATOR_BOX_ADD_CALL=1
|
export NYASH_BUILDER_OPERATOR_BOX_ADD_CALL=1
|
||||||
export NYASH_BUILDER_OPERATOR_BOX_ALL_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
|
echo "[dev-env] Operator Boxes (stringify/compare/add) enabled (adopt+builder-call)" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,18 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
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"
|
PY_BUILDER = ROOT / "src" / "llvm_py" / "llvm_builder.py"
|
||||||
|
|
||||||
def run_dummy(out_path: str) -> None:
|
def run_dummy(out_path: str) -> None:
|
||||||
|
|||||||
@ -30,7 +30,8 @@ export NYASH_SELFHOST_EXEC=0 # JSON v0ブリッジ無効
|
|||||||
export SMOKES_DEFAULT_TIMEOUT=30
|
export SMOKES_DEFAULT_TIMEOUT=30
|
||||||
export SMOKES_PARALLEL_TESTS=1
|
export SMOKES_PARALLEL_TESTS=1
|
||||||
export SMOKES_FAST_FAIL=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"
|
export SMOKES_LOG_LEVEL="info"
|
||||||
|
|||||||
@ -139,13 +139,13 @@ check_parity() {
|
|||||||
|
|
||||||
# LLVM(Pythonハーネス)実行
|
# LLVM(Pythonハーネス)実行
|
||||||
if [ "$program" = "-c" ]; then
|
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
|
llvm_exit=0
|
||||||
else
|
else
|
||||||
llvm_exit=$?
|
llvm_exit=$?
|
||||||
fi
|
fi
|
||||||
else
|
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
|
llvm_exit=0
|
||||||
else
|
else
|
||||||
llvm_exit=$?
|
llvm_exit=$?
|
||||||
|
|||||||
@ -165,6 +165,10 @@ run_nyash_vm() {
|
|||||||
shift
|
shift
|
||||||
local tmpfile="/tmp/nyash_test_$$.nyash"
|
local tmpfile="/tmp/nyash_test_$$.nyash"
|
||||||
echo "$code" > "$tmpfile"
|
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_VM_USE_PY="$USE_PYVM" NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 "${ENV_PREFIX[@]}" \
|
||||||
"$NYASH_BIN" --backend vm "$tmpfile" "${EXTRA_ARGS[@]}" "$@" 2>&1 | filter_noise
|
"$NYASH_BIN" --backend vm "$tmpfile" "${EXTRA_ARGS[@]}" "$@" 2>&1 | filter_noise
|
||||||
@ -187,30 +191,51 @@ run_nyash_vm() {
|
|||||||
run_nyash_llvm() {
|
run_nyash_llvm() {
|
||||||
local program="$1"
|
local program="$1"
|
||||||
shift
|
shift
|
||||||
|
# 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
|
# 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
|
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_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'"
|
log_info "Hint: build ny-llvmc + enable harness: cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
# -c オプションの場合は一時ファイル経由で実行
|
# -c オプションの場合は一時ファイル経由で実行
|
||||||
if [ "$program" = "-c" ]; then
|
if [ "$program" = "-c" ]; then
|
||||||
local code="$1"
|
local code="$1"
|
||||||
shift
|
shift
|
||||||
local tmpfile="/tmp/nyash_test_$$.nyash"
|
local tmpfile="/tmp/nyash_test_$$.nyash"
|
||||||
echo "$code" > "$tmpfile"
|
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 | \
|
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 "^\[FileBox\]" | grep -v "^Net plugin:" | grep -v "^\[.*\] Plugin" | \
|
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 '^✅ 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]}
|
local exit_code=${PIPESTATUS[0]}
|
||||||
rm -f "$tmpfile"
|
rm -f "$tmpfile"
|
||||||
return $exit_code
|
return $exit_code
|
||||||
else
|
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 | \
|
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 "^\[FileBox\]" | grep -v "^Net plugin:" | grep -v "^\[.*\] Plugin" | \
|
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 '^✅ 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]}
|
return ${PIPESTATUS[0]}
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,10 @@ TEST_DIR="/tmp/json_error_messages_ast_$$"
|
|||||||
mkdir -p "$TEST_DIR"
|
mkdir -p "$TEST_DIR"
|
||||||
cd "$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
|
cat > nyash.toml << EOF
|
||||||
[using.json_native]
|
[using.json_native]
|
||||||
path = "$NYASH_ROOT/apps/lib/json_native/"
|
path = "$NYASH_ROOT/apps/lib/json_native/"
|
||||||
|
|||||||
@ -10,6 +10,10 @@ TEST_DIR="/tmp/json_nested_ast_$$"
|
|||||||
mkdir -p "$TEST_DIR"
|
mkdir -p "$TEST_DIR"
|
||||||
cd "$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
|
cat > nyash.toml << EOF
|
||||||
[using.json_native]
|
[using.json_native]
|
||||||
path = "$NYASH_ROOT/apps/lib/json_native/"
|
path = "$NYASH_ROOT/apps/lib/json_native/"
|
||||||
|
|||||||
@ -10,6 +10,10 @@ TEST_DIR="/tmp/json_roundtrip_ast_$$"
|
|||||||
mkdir -p "$TEST_DIR"
|
mkdir -p "$TEST_DIR"
|
||||||
cd "$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
|
cat > nyash.toml << EOF
|
||||||
[using.json_native]
|
[using.json_native]
|
||||||
path = "$NYASH_ROOT/apps/lib/json_native/"
|
path = "$NYASH_ROOT/apps/lib/json_native/"
|
||||||
|
|||||||
@ -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"
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
36
tools/smokes/v2/profiles/quick/core/selfhost_mir_binop_vm.sh
Normal file
36
tools/smokes/v2/profiles/quick/core/selfhost_mir_binop_vm.sh
Normal file
@ -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(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="" local digits="0123456789" loop(v>0){ 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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -6,6 +6,9 @@ source "$(dirname "$0")/../../../lib/test_runner.sh"
|
|||||||
require_env || exit 2
|
require_env || exit 2
|
||||||
preflight_plugins || 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() {
|
setup_tmp_dir() {
|
||||||
TEST_DIR="/tmp/using_multi_prelude_$$"
|
TEST_DIR="/tmp/using_multi_prelude_$$"
|
||||||
mkdir -p "$TEST_DIR"
|
mkdir -p "$TEST_DIR"
|
||||||
|
|||||||
@ -6,6 +6,9 @@ source "$(dirname "$0")/../../../lib/test_runner.sh"
|
|||||||
require_env || exit 2
|
require_env || exit 2
|
||||||
preflight_plugins || 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() {
|
setup_tmp_dir() {
|
||||||
TEST_DIR="/tmp/using_profiles_ast_$$"
|
TEST_DIR="/tmp/using_profiles_ast_$$"
|
||||||
mkdir -p "$TEST_DIR"
|
mkdir -p "$TEST_DIR"
|
||||||
|
|||||||
@ -220,9 +220,19 @@ find_test_files() {
|
|||||||
local profile_dir="$SCRIPT_DIR/profiles/$PROFILE"
|
local profile_dir="$SCRIPT_DIR/profiles/$PROFILE"
|
||||||
local test_files=()
|
local test_files=()
|
||||||
local have_llvm=0
|
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
|
have_llvm=1
|
||||||
fi
|
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
|
if [ ! -d "$profile_dir" ]; then
|
||||||
log_error "Profile directory not found: $profile_dir"
|
log_error "Profile directory not found: $profile_dir"
|
||||||
|
|||||||
Reference in New Issue
Block a user