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:
nyash-codex
2025-09-28 12:19:49 +09:00
parent 41a46b433d
commit 510f4cf523
74 changed files with 2846 additions and 825 deletions

View File

@ -305,8 +305,9 @@ Notes
- VM と Cranelift(JIT) は MIR14 へ移行中のため、現在は実行経路として安定していないよ(検証・実装作業の都合で壊れている場合があるにゃ)。
- 当面の実行・配布は LLVM ラインを最優先・全力で整備する方針だよ。開発・確認は `--features llvm` を有効にして進めてね。
- 推奨チェック:
- `env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo build --release --features llvm -j 24`
- `env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo check --features llvm`
- LLVM は llvmlite ハーネスPython経由だよ。Rust inkwell は既定で不使用legacy のみ)。
- ビルド(ハーネス): `cargo build --release --features llvm -j 24`
- チェック: `cargo check --features llvm`
## Docs links開発方針/スタイル)
- Language statements (ASI): `docs/reference/language/statements.md`
@ -337,7 +338,7 @@ Notes
- Build (JIT/VM): `cargo build --release --features cranelift-jit`
- Build (LLVM AOT / harness-first):
- `cargo build --release -p nyash-llvm-compiler` (ny-llvmc builder)
- `LLVM_SYS_180_PREFIX=$(llvm-config-18 --prefix) cargo build --release --features llvm`
- `cargo build --release --features llvm`
- Run via harness: `NYASH_LLVM_USE_HARNESS=1 ./target/release/nyash --backend llvm apps/APP/main.nyash`
- Quick VM run: `./target/release/nyash --backend vm apps/APP/main.nyash`
- Emit + link (LLVM): `tools/build_llvm.sh apps/APP/main.nyash -o app`
@ -475,7 +476,8 @@ Flags
- Rust tests: `cargo test` (add targeted unit tests near code).
- Smoke scripts validate endtoend AOT/JIT (`tools/llvm_smoke.sh`).
- Test naming: prefer `*_test.rs` for Rust and descriptive `.nyash` files under `apps/` or `tests/`.
- For LLVM tests, ensure LLVM 18 is installed and set `LLVM_SYS_180_PREFIX`.
- For LLVM tests, ensure Python llvmlite is available and `ny-llvmc` is built.
- Build (harness): `cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm`
## Commit & Pull Request Guidelines
- Commits: concise imperative subject; scope the change (e.g., "llvm: fix argc handling in nyrt").

View File

@ -2,6 +2,33 @@
このファイルは最小限の入口だよ。詳細はREADMEから辿ってねにゃ😺
---
## 🔄 **現在の開発状況** (2025-09-28)
### 🎯 **Phase 15: セルフホスティング実行器統一化**
- **Rust VM + LLVM 2本柱体制**で開発中
- **Core Box統一化**: 3-tier → 2-tier 統一完了
- **MIR Callee型革新**: 型安全な関数解決システム実装済み
### 🤝 **AI協働開発体制**
```
Claude: 戦略・分析・レビュー
ChatGPT: 実装・検証
現在の合意:
✅ Phase 15集中セルフホスト優先
✅ Builder根治は段階的3 Phase戦略
✅ 息が合っている状態: 良好
```
### 📚 **重要リソース**
- **開発マスタープラン**: [00_MASTER_ROADMAP.md](docs/development/roadmap/phases/00_MASTER_ROADMAP.md)
- **現在のタスク**: [CURRENT_TASK.md](CURRENT_TASK.md)
- **Phase 15詳細**: [docs/development/roadmap/phases/phase-15/](docs/development/roadmap/phases/phase-15/)
---
## 🚨 重要スモークテストはv2構造を使う
- 📖 **スモークテスト完全ガイド**: [tools/smokes/README.md](tools/smokes/README.md)
- 📁 **v2詳細ドキュメント**: [tools/smokes/v2/README.md](tools/smokes/v2/README.md)
@ -16,9 +43,13 @@ cargo build --release
# 一括スモークテスト
tools/smokes/v2/run.sh --profile quick
# 個別スモークテスト
# 個別スモークテスト(フィルタ指定)
tools/smokes/v2/run.sh --profile quick --filter "<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
@ -29,14 +60,17 @@ tools/smokes/v2/run.sh --profile quick --filter "<glob>"
# 前提: Python3 + llvmlite
# 未導入なら: pip install llvmlite
# ビルドLLVM_SYS_180_PREFIX不要
cargo build --release --features llvm
# 一括スモークテスト
# 一括スモークテスト(そのまま実行
tools/smokes/v2/run.sh --profile integration
# 個別スモークテスト
# 警告低減版(ビルド後に実行・推奨)
cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm
tools/smokes/v2/run.sh --profile integration
# 個別スモークテスト(フィルタ指定)
tools/smokes/v2/run.sh --profile integration --filter "<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
@ -179,15 +213,15 @@ target/x86_64-pc-windows-msvc/release/nyash.exe
./target/release/nyash --backend llvm program.nyash # 実行時最適化
```
### 🎯 **2本柱ビルド方法** (2025-09-24更新)
### 🎯 **2本柱ビルド方法** (2025-09-28更新)
#### 🔨 **標準ビルド**(推奨)
```bash
# 標準ビルド2本柱対応
cargo build --release
# LLVM機能付きビルド(本番用)
env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo build --release --features llvm
# LLVMllvmliteハーネス付きビルド(本番用)
cargo build --release --features llvm
```
#### 📝 **2本柱テスト実行**
@ -196,9 +230,9 @@ env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo build --release --features llvm
cargo build --release
./target/release/nyash program.nyash
# 2. LLVM実行 ✅(本番・最適化用)
env LLVM_SYS_180_PREFIX=/usr/lib/llvm-18 cargo build --release --features llvm
./target/release/nyash --backend llvm program.nyash
# 2. LLVM実行 ✅(本番・最適化用, llvmliteハーネス
cargo build --release --features llvm
NYASH_LLVM_USE_HARNESS=1 ./target/release/nyash --backend llvm program.nyash
# 3. プラグインテスト実証済み ✅
# CounterBox
@ -829,7 +863,9 @@ box MyBox {
- Reference: [docs/reference/](docs/reference/)
### 🎯 リファレンス
- **言語**: [LANGUAGE_REFERENCE_2025.md](docs/reference/language/LANGUAGE_REFERENCE_2025.md)
- **言語**:
- [Quick Reference](docs/reference/language/quick-reference.md) ⭐最優先 - 1ページ実用ガイド
- [LANGUAGE_REFERENCE_2025.md](docs/reference/language/LANGUAGE_REFERENCE_2025.md) - 完全仕様
- **MIR**: [INSTRUCTION_SET.md](docs/reference/mir/INSTRUCTION_SET.md)
- **API**: [boxes-system/](docs/reference/boxes-system/)
- **プラグイン**: [plugin-system/](docs/reference/plugin-system/)
@ -847,6 +883,7 @@ box MyBox {
### 🎯 最重要ドキュメント2つの核心
#### 🔤 言語仕様
- **[クイックリファレンス](docs/reference/language/quick-reference.md)** ⭐最優先 - 1ページ実用ガイドASI・Truthiness・演算子・型ルール
- **[構文早見表](docs/quick-reference/syntax-cheatsheet.md)** - 基本構文・よくある間違い
- **[完全リファレンス](docs/reference/language/LANGUAGE_REFERENCE_2025.md)** - 言語仕様詳細

View File

@ -4,13 +4,34 @@ Focus
- Keep VM quick green; llvmlite integration on-demand.
- Using SSOTnyash.toml + 相対usingで安定解決。
- Builder/VM ガードは最小限・仕様不変dev では診断のみ)。
- Phase 15.7 を再定義: Known 化Rewrite 統合dev観測と MiniVM 安定化、表示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 — 20250927
- 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 Phase3: added TypeCertainty to Callee::Method (Known/Union). Builder sets Known when receiver origin is known; legacy/migration BoxCall marks Union. JSON emitter and MIR printer include certainty for diagnostics. Backends ignore it functionally for now.
- Using/SSOT: JSONモジュール内部 using を相対に統一alias配下でも安定
- DebugHub: 追加ゲート `NYASH_DEBUG_SAMPLE_EVERY`N件に1度だけ emit。重いケースでのログ制御のため既定OFF・ゼロコスト
- Router diagnostics: class-reroute / special-reroute を DebugHub に emitdev-only, 既定OFF
- LLVM diagnostics: `NYASH_LLVM_TRACE_CALLS=1``mir_call` の calleeMethod.certainty 含む)を JSON 出力(挙動不変)。
Decision — Variables (Option A; 20250927)
- 方針: var/let は導入しない。ローカルは常に `local` で明示宣言。
@ -26,8 +47,7 @@ Decision — Variables (Option A; 20250927)
- 互換: `Main.main` が存在する場合は常にそちらを優先。両方無い場合は従来通りエラー。
- オプトアウト: `NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=0|false|off` で無効化可能。
- Next
- Scanner.read_string_literal: 未終端で null を返すよう修正TokenizerがERRORを積む小プローブ追加
- Heavy JSON: quick 既定ONへ再切替環境が安定したら
- Heavy JSON: quick 既定ONへ再切替LLVM 常備で段階復帰)
- 解析ログの統一: parser/tokenizerのdevトレースは既定OFFのまま維持、必要時だけ有効化
- llvmliteintegration: 任意ジョブで確認(単発実行のハングはタイムアウト/リンク分離で回避)
@ -70,19 +90,32 @@ Guards / Policy
- 既定挙動は不変prod 用心)。
- dev では診断強化(ログ/メトリクス)し、ランナー側でノイズはフィルタ。
Policy — AST Using (Status Quo)
- SSOTnyash.tomlAST prelude merge を維持。prod は toml 限定、dev/ci は段階的に緩和。
- 重い AST/JSON ケースは integration でカバーしつつ、quick への復帰は LLVM 有効環境で段階的に行う(順次解除)。
Work Queue (Next)
1) Scanner: 未終端文字列で必ず null を返すTokenizer が ERROR へ)
2) Heavy JSON: quick 既定ONに戻すプローブは維持
3) エラーメッセージの詳細化expected/actual/line/column
4) Ny 実行器 M2 スケルトンJSON v0 ローダconst/binop 等の最小実装)下書き
5) Parity ミニセットVM↔llvmlite↔Nyを用意し、差分ダッシュボード化
6) Router 観測ログの軽追加dev-only, 既定OFF: class-reroute / special-reroute を DebugHub に emitサンプル制御対応
7) LLVM ハーネスの MIR ダンプに certainty 表示(挙動不変の診断整合)
6) Router: Known/Union 方針の磨き込み(挙動不変
- Known → 既存の直接呼び出しを維持VM 完了、LLVM は表示のみ)。
- Union → ルータ経路を維持しつつ、ログで可視化(表は“必要最小”で追加)。
7) Heavy JSON の quick 段階復帰LLVM 有効環境)
- 順序: nested_ast → roundtrip_ast → error_messages_ast。
8) 診断LLVM ダンプに certainty の補助表示(必要時、挙動不変)。
Update — @local expansion promotion (20250927)
- すべてのランナーモードに `preexpand_at_local` を適用common/llvm/pyvm に加え vm/selfhost へも導入)。
- Docs を更新し、構文糖衣が標準で有効であることを明記。
Plan — Router Minimalism (継続方針)
- 特殊メソッド表は “toString→str互換:stringify, equals/1” の範囲から、ユースが発生したもののみ点で追加。
- 既定の挙動・言語仕様は変更しない(フォールバックの拡大はしない)。
- 測定: DebugHubresolve.*)ログと LLVM の `NYASH_LLVM_TRACE_CALLS` を併用し、Union 経路を可視化。
Runbook抜粋
- VM quick: `tools/smokes/v2/run.sh --profile quick`
- LLVM llvmlite: `cargo build --release --features llvm && tools/smokes/v2/run.sh --profile integration`
@ -169,7 +202,59 @@ Update — 2025-09-27 (json_lint_vm guard fix)
- File: apps/lib/json_native/lexer/scanner.nyash (read_string_literal)
- TODO: add unit probe; ensure EOF without closing quote yields null; add negative case to smokes if needed.
Update — 2025-09-28 (Scanner 未終端→null とスモーク追加)
- Implemented: JsonScanner.read_string_literal returns null when closing quote is missing or escape incomplete.
- File: apps/lib/json_native/lexer/scanner.nyash (already returned null; verified)
- Tokenizer maps scanner null to ERROR("Unterminated string literal").
- File: apps/lib/json_native/lexer/tokenizer.nyash (tokenize_string)
- Added quick smoke to lock behavior:
- tools/smokes/v2/profiles/quick/core/json_unterminated_string_vm.sh → expects "Unterminated string literal".
Work Queue — Reorganized (20250928)
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 — Truthinessquickで有効化— completed
- tools/smokes/v2/profiles/quick/core/lang_quickref_truthiness_vm.sh → enabled; PASS0→false, 1→true, ""→false, nonempty→true
10) Language guardsplanned; 既定OFF・段階導入
- ASI strictness: devonly check to fail a line break after a binary operator; default OFF.
- Plus mixed: warn/failfast when nonString mixed `+` unless explicit stringify; default OFF; document String+number ⇒ concat.
- Box equality guidance: when `box == box` is used, emit guidance to use equals(); default OFF.
- Scope: docs + dev warnings first; later wire parser/builder flags guarded by env/CLI profile.
Update — 2025-09-27 (M2 skeleton: Ny mini-MIR VM)
Update — 2025-09-28 (json_lint_vm regression fix — condition_fn and birth bridge)
- Fixed: Unknown global function: condition_fn (quick json_lint_vm)
- Indirect calls: ensure AST `condition_fn(ch)` lowers to Value call (unified path already used in exprs_call.rs)
- Unified Global safety: emit_unified_call now devsafes `condition_fn` by returning const 1 when unresolved (explicit optin 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 builtins when plugins are absent.
- Builder: gated birth() injection for builtins (Array/Map/String etc). Default OFF unless `NYASH_DEV_BIRTH_INJECT_BUILTINS=1`.
- Next (highprio): 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: reenable birth() injection for builtin boxes (default OFF to stabilize unified Method path until full bridge lands).
- NYASH_MIR_UNIFIED_CALL: default ON; optout via 0|false|off.
- Added Ny-based minimal MIR(JSON v0) executor skeleton (const→ret only), dev-only app — no default behavior change.
- File: apps/selfhost/vm/boxes/mir_vm_min.nyash
- Entry: apps/selfhost/vm/mir_min_entry.nyash (optional thin wrapper)
@ -218,7 +303,7 @@ Update — 2025-09-27 (Quick profile stabilization & heavy JSON gating)
- json_pp_vm (JsonNode.parse pretty-print) → SKIP in quick例示アプリ、他で十分カバー
- Using resolver brace-fixer: quick config restored to ON for stabilityNYASH_RESOLVE_FIX_BRACES=1
- ScopeCtx wired (loop/join) and resolve/ssa events include region_iddev logs only
- toString→stringify early mapping logs addedreason: toString-early-*
- toString→str early mapping logs addedreason: toString-early-*
- Rationale: heavy/nested parser cases were sensitive to mixed env order in quick. Integration profile will carry the parity checks with DebugHub capture.
- Next (focused):
1) Run integration smokes for JSON heavy with DebugHub ON and collect /tmp logs
@ -244,7 +329,7 @@ Acceptance (this phase):
- userbox_branch_phi_vm.sh — SKIP (rewrite/materialize pending)
- userbox_toString_mapping_vm.sh — SKIP (mapping pending)
- Rationale: keep quick green while surfacing remaining gaps as SKIP with clear reasons.
- Next: stabilize rewrite/materialize across branch/arity and toString→stringify mapping; then flip SKIPs to PASS.
- Next: stabilize rewrite/materialize across branch/arity and toString→str mapping; then flip SKIPs to PASS.
Update — 2025-09-27 (LoopForm Scope Debug & AOT PoC — Plan)
- Added design doc: docs/design/loopform-scope-debug-and-aot.md
- Scope model (LoopScope/JoinScope), invariants, Hub+Inspectors, per-scope data, AOT fold, PoC phases, acceptance.
@ -264,3 +349,132 @@ Update — 2025-09-27 (LoopForm Scope Debug & AOT PoC — Plan)
- Acceptance (Phase1)
- Debug JSONL has resolve/ssa events with region_id and choices; PASS cases unchanged (OFF)
- SKIP cases pinpointable by log (branch/arity) → use logs to guide fixes → flip to PASS
Update — 2025-09-28 (Plugins 既定ON と ENV 整理)
- Plugins: 既定ONで統一。テストランナー/開発スクリプトから `NYASH_DISABLE_PLUGINS=1` を撤去。
- tools/smokes/v2/lib/test_runner.shLLVM 経路): 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 MiniVM 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 (MiniVM 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 (20250928)
- 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: PolicyNy Kernel, devonly— equals/str/truthiness の観測APIバッチ、再入禁止/タイムアウト/計測)
- P3: 表示APIの移行誘導 — toString→str互換:stringifyの警告/ドキュメント(仕様不変)
- P4: Union 観測・分析 — resolve.try/choose と ssa.phiregion_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段階
- Stage1: Known 経路で 100% 関数化quick全域で dev WARN=0
- Stage2: 限定 materialize をON時に適用し、分岐/PHI 合流の代表ケースが関数化差分はdevのみ
- 常に prod は挙動不変・安全OFFで現状維持
Update — 2025-09-28 (MiniVM M2/M3 fix + smokes)
- Fix: compare/ret segmentation made robust without heavy JSON parse.
- Approach: perblock coarse passes for const/binop/compare and a precise inblock ret search; controlflow (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 specneutral; no default behavior changes to core VM.
Update — 2025-09-28 (QuickRef Dev Guards + Docs llvmlite)
- Dev guards (envgated; default OFF) implemented and validated by quick smokes:
- ASI strict linecontinuation: `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/nyllvmc 前提に更新。
- Files: `AGENTS.md`, `README.md`, `README.ja.md`, `CLAUDE.md`
Plan — Next (2025-09-28)
1) MiniVM 単一パス化(仕様不変・安全化) — completed
- 各 op を JSON オブジェクト単位で厳密セグメント化し、一回走査で評価coarse pass を除去)。
- 代表ケース複数op/ret先頭/ret末尾/compare v0,v1/jump/branchで緑維持を確認。
2) Rewrite 統合 Stage1挙動不変・dev観測 — completed (observability wired)
- builder_calls の unified 経路に resolve.try/resolve.choose を追加devonly/既定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 (specneutral)
- 規範: `str()` / `x.str()`(同義)。`toString()` は Builder で `str()` に早期正規化。
- 互換: `stringify()` は当面エイリアス(内部で `str()` 相当)。
- VM ルータ: toString/0 → str/0なければ stringify/0
- QuickRef/ガイド更新済み。`NYASH_PLUS_MIX_ERROR` の誘導文言も `str()` に統一。
追加メモ — これからやるユーザー合意、20250928
- MiniVM の単一パス化を安全に実装(既定挙動不変)
- 各 op を厳密セグメントで1回走査に統合coarse を段階撤去)
- 代表スモークM2/M3/compare v0,v1で緑維持確認
- 続いて Rewrite 統合 Stage1 の観測へ進むdev のみ、挙動不変)
- Dev Profiles
- tools/dev_env.sh に Unified 既定ON明示OFFのみ無効とレガシー関数化抑止を追加。
- `NYASH_MIR_UNIFIED_CALL=1`既定ON明示
- `NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1`(重複回避; 段階移行)

View File

@ -40,6 +40,8 @@ mir_refbarrier_unify_poc = []
llvm-harness = []
llvm-inkwell-legacy = ["dep:inkwell"]
llvm = ["llvm-harness"]
# Legacy AST interpreter (off by default). Gates NyashInterpreter usage in runner/tests.
interpreter-legacy = []
# (removed) Optional modular MIR builder feature
# cranelift-jit = [ # ARCHIVED: Moved to archive/jit-cranelift/ during Phase 15
# "dep:cranelift-codegen",

View File

@ -1,6 +1,7 @@
# Nyash selfhosting-dev quick targets
.PHONY: build build-release run-minimal smoke-core smoke-selfhost bootstrap roundtrip clean quick fmt lint dep-tree
.PHONY: build build-release run-minimal smoke-core smoke-selfhost bootstrap roundtrip clean quick fmt lint dep-tree \
smoke-quick smoke-quick-filter smoke-integration
build:
cargo build --features cranelift-jit
@ -28,6 +29,17 @@ clean:
quick: build-release smoke-selfhost
# --- v2 smokes shortcuts ---
smoke-quick:
bash tools/smokes/v2/run.sh --profile quick
# Usage: make smoke-quick-filter FILTER="json_*"
smoke-quick-filter:
bash tools/smokes/v2/run.sh --profile quick --filter "$(FILTER)"
smoke-integration:
bash tools/smokes/v2/run.sh --profile integration
fmt:
cargo fmt --all

View File

@ -19,6 +19,23 @@ AST JSON v0マクロ/ブリッジ): `docs/reference/ir/ast-json-v0.md`
セルフホスト1枚ガイド: `docs/how-to/self-hosting.md`
ExternCallenv.*)と 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
- JSON最小Roundtrip/Nested を一発): `./tools/opbox-json.sh`
- quick 全体軽量プリフライトtimeout 180s: `./tools/opbox-quick.sh`
@ -59,7 +76,7 @@ Phase15202509アップデート
<a id="self-hosting"></a>
## 🧪 Self-Hosting自己ホスト開発
- ガイド: `docs/how-to/self-hosting.md`
- 最小E2E: `NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend vm apps/selfhost-minimal/main.nyash`
- 最小E2E: `./target/release/nyash --backend vm apps/selfhost-minimal/main.nyash`
- スモーク: `bash tools/jit_smoke.sh` / `bash tools/selfhost_vm_smoke.sh`
- Makefile: `make run-minimal`, `make smoke-selfhost`
@ -135,15 +152,14 @@ local py = new PyRuntimeBox() // Pythonプラグイン
## 🏗️ **複数の実行モード**
重要: 現在、JIT ランタイム実行は封印中です。実行は「PyVM既定VM任意でレガシー有効」、配布は「Cranelift AOT(EXE)LLVM AOT(EXE)」の4体制です
重要: 現在、JIT ランタイム実行は封印中です。実行は「Rust VMMIR/ PyVM開発補助」、配布は「LLVM AOTハーネス」が主軸です。ASTインタープリタはレガシー扱いでデフォルト無効`interpreter-legacy` feature
Phase15自己ホスト期: VM/インタープリタはフィーチャーで切替
- 既定ビルド: `--backend vm` は PyVM 実行python3 + `tools/pyvm_runner.py` が必要)
- レガシー Rust VM/インタープリタを有効化するには:
Phase15自己ホスト期: ASTインタープリタは任意featureで明示ON
- 既定ビルド: `--backend vm` は PyVM 経路python3 + `tools/pyvm_runner.py` が必要)Rust VMMIR
- レガシー AST インタープリタを有効化するには(通常は不要):
```bash
cargo build --release --features vm-legacy,interpreter-legacy
cargo build --release --features interpreter-legacy
```
以降、`--backend vm`/`--backend interpreter` が従来経路で動作します。
### 1. **インタープリターモード** (開発用)
```bash
@ -178,13 +194,23 @@ cargo build --release --features cranelift-jit
- 最高性能
- 簡単配布
### 4. **ネイティブバイナリLLVM AOT**
### 4. **ネイティブバイナリLLVM AOT, llvmliteハーネス**
```bash
LLVM_SYS_180_PREFIX=$(llvm-config-18 --prefix) \
cargo build --release --features llvm
NYASH_LLVM_OBJ_OUT=$PWD/nyash_llvm_temp.o \
./target/release/nyash --backend llvm program.nyash
# リンクして実行
# ハーネスCLI をビルド(LLVM_SYS_180_PREFIX不要)
cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm
# ハーネス経由で EXE を生成して実行
NYASH_LLVM_USE_HARNESS=1 \
NYASH_NY_LLVM_COMPILER=target/release/ny-llvmc \
NYASH_EMIT_EXE_NYRT=target/release \
./target/release/nyash --backend llvm --emit-exe myapp program.nyash
./myapp
# あるいは .o を出力して手動リンク
NYASH_LLVM_USE_HARNESS=1 \
NYASH_NY_LLVM_COMPILER=target/release/ny-llvmc \
./target/release/nyash --backend llvm program.nyash \
-D NYASH_LLVM_OBJ_OUT=$PWD/nyash_llvm_temp.o
cc nyash_llvm_temp.o -L crates/nyrt/target/release -Wl,--whole-archive -lnyrt -Wl,--no-whole-archive -lpthread -ldl -lm -o myapp
./myapp
```
@ -195,6 +221,7 @@ tools/smoke_aot_vs_vm.sh examples/aot_min_string_len.nyash
```
### LLVM バックエンドの補足
- Python llvmlite を使用します。Python3 + llvmlite の用意と `ny-llvmc` のビルド(`cargo build -p nyash-llvm-compiler`)が必要です。`LLVM_SYS_180_PREFIX` は不要です。
- `NYASH_LLVM_OBJ_OUT`: `--backend llvm` 実行時に `.o` を出力するパス。
- 例: `NYASH_LLVM_OBJ_OUT=$PWD/nyash_llvm_temp.o ./target/release/nyash --backend llvm apps/ny-llvm-smoke/main.nyash`
- 削除された `NYASH_LLVM_ALLOW_BY_NAME=1`: すべてのプラグイン呼び出しがmethod_idベースに統一。
@ -239,6 +266,26 @@ smoke_obj_array = "NYASH_LLVM_OBJ_OUT={root}/nyash_llvm_temp.o ./target/release/
- `{root}` は現在のプロジェクトルートに展開されます。
- 現状は最小機能OS別/依存/並列は未対応)。
### ちいさなENVまとめVM vs LLVM ハーネス)
- VM 実行: 追加ENVなしでOK。
- 例: `./target/release/nyash --backend vm apps/tests/ternary_basic.nyash`
- LLVM ハーネス実行: 下記3つだけ設定してね。
- `NYASH_LLVM_USE_HARNESS=1`
- `NYASH_NY_LLVM_COMPILER=$NYASH_ROOT/target/release/ny-llvmc`
- `NYASH_EMIT_EXE_NYRT=$NYASH_ROOT/target/release`
- 例: `NYASH_LLVM_USE_HARNESS=1 NYASH_NY_LLVM_COMPILER=target/release/ny-llvmc NYASH_EMIT_EXE_NYRT=target/release ./target/release/nyash --backend llvm apps/ny-llvm-smoke/main.nyash`
### DebugHub かんたんガイド
- 有効化: `NYASH_DEBUG_ENABLE=1`
- 種別指定: `NYASH_DEBUG_KINDS=resolve,ssa`
- 出力先: `NYASH_DEBUG_SINK=/tmp/nyash_debug.jsonl`
- 例: `NYASH_DEBUG_ENABLE=1 NYASH_DEBUG_KINDS=resolve,ssa NYASH_DEBUG_SINK=/tmp/nyash.jsonl tools/smokes/v2/run.sh --profile quick --filter "userbox_*"`
### 開発用セーフティVM
- stringify(Void) は "null" を返すJSONフレンドリ開発セーフティ。既定挙動は不変
- JsonScanner のデフォルト値(`NYASH_VM_SCANNER_DEFAULTS=1` 時のみ): `is_eof/current/advance` 内に限定し、数値/テキストの不足を安全に埋める。
- VoidBox に対する length/size/get/push 等は、ガード下で中立なノーオペとして扱い、開発中のハードストップを回避。
---
## 🧰 一発ビルドMVP: `nyash --build`

View File

@ -19,7 +19,7 @@ Execution Status (Feature Additions Pause)
- `--backend vm` (PyVM harness)
- Inactive/Sealed
- `--backend cranelift`, `--jit-direct` (sealed; use LLVM harness)
- Rust VM (legacy optin via features)
- AST interpreter (legacy) is gated by feature `interpreter-legacy` and excluded from default builds (Rust VM + LLVM are the two main lines)
Quick pointers
- Emit object with harness: set `NYASH_LLVM_USE_HARNESS=1` and `NYASH_LLVM_OBJ_OUT=<path>` (defaults in tools use `tmp/`).
@ -55,6 +55,46 @@ MIR mode note: Default PHI behavior
Selfhosting onepager: `docs/how-to/self-hosting.md`.
ExternCall (env.*) and println normalization: `docs/reference/runtime/externcall.md`.
### Minimal ENV (VM vs LLVM harness)
- VM: no extra environment needed for typical runs.
- Example: `./target/release/nyash --backend vm apps/tests/ternary_basic.nyash`
- LLVM harness: set three variables so the runner finds the harness and runtime.
- `NYASH_LLVM_USE_HARNESS=1`
- `NYASH_NY_LLVM_COMPILER=$NYASH_ROOT/target/release/ny-llvmc`
- `NYASH_EMIT_EXE_NYRT=$NYASH_ROOT/target/release`
- Example: `NYASH_LLVM_USE_HARNESS=1 NYASH_NY_LLVM_COMPILER=target/release/ny-llvmc NYASH_EMIT_EXE_NYRT=target/release ./target/release/nyash --backend llvm apps/ny-llvm-smoke/main.nyash`
### DebugHub Quick Guide
- Enable: `NYASH_DEBUG_ENABLE=1`
- Select kinds: `NYASH_DEBUG_KINDS=resolve,ssa`
- Output file: `NYASH_DEBUG_SINK=/tmp/nyash_debug.jsonl`
- Use with smokes: `NYASH_DEBUG_ENABLE=1 NYASH_DEBUG_KINDS=resolve,ssa NYASH_DEBUG_SINK=/tmp/nyash.jsonl tools/smokes/v2/run.sh --profile quick --filter "userbox_*"`
### MIR Unified Calldefault 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)
- `--profile dev` → Macros ON (strict), PyVM dev向け設定を適用必要に応じて環境で上書き可
- `--profile lite` → Macros OFF の軽量実行
@ -76,7 +116,7 @@ Specs & Constraints
<a id="self-hosting"></a>
## 🧪 SelfHosting (Dev Focus)
- Guide: `docs/how-to/self-hosting.md`
- Minimal E2E: `NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend vm apps/selfhost-minimal/main.nyash`
- Minimal E2E: `./target/release/nyash --backend vm apps/selfhost-minimal/main.nyash`
- Smokes: `bash tools/jit_smoke.sh` / `bash tools/selfhost_vm_smoke.sh`
- JSON (Operator Boxes, dev): `./tools/opbox-json.sh` / `./tools/opbox-quick.sh`
- Makefile: `make run-minimal`, `make smoke-selfhost`
@ -200,13 +240,23 @@ cargo build --release --features cranelift-jit
- Maximum performance
- Easy distribution
### 4. **Native Binary (LLVM AOT)**
### 4. **Native Binary (LLVM AOT, llvmlite harness)**
```bash
LLVM_SYS_180_PREFIX=$(llvm-config-18 --prefix) \
cargo build --release --features llvm
NYASH_LLVM_OBJ_OUT=$PWD/nyash_llvm_temp.o \
./target/release/nyash --backend llvm program.nyash
# Link and run
# Build harness + CLI (no LLVM_SYS_180_PREFIX needed)
cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm
# Emit and run native executable via harness
NYASH_LLVM_USE_HARNESS=1 \
NYASH_NY_LLVM_COMPILER=target/release/ny-llvmc \
NYASH_EMIT_EXE_NYRT=target/release \
./target/release/nyash --backend llvm --emit-exe myapp program.nyash
./myapp
# Alternatively, emit an object file then link manually
NYASH_LLVM_USE_HARNESS=1 \
NYASH_NY_LLVM_COMPILER=target/release/ny-llvmc \
./target/release/nyash --backend llvm program.nyash \
-D NYASH_LLVM_OBJ_OUT=$PWD/nyash_llvm_temp.o
cc nyash_llvm_temp.o -L crates/nyrt/target/release -Wl,--whole-archive -lnyrt -Wl,--no-whole-archive -lpthread -ldl -lm -o myapp
./myapp
```
@ -254,7 +304,7 @@ Key options (minimal)
- `--target <triple>` (only when needed)
Notes
- LLVM AOT requires LLVM 18 (`LLVM_SYS_180_PREFIX`).
- LLVM AOT uses Python llvmlite harness. Ensure Python3 + llvmlite and `ny-llvmc` are available (built via `cargo build -p nyash-llvm-compiler`). No `LLVM_SYS_180_PREFIX` required.
- Apps that open a GUI may show a window during AOT emission; close it to continue.
- On WSL if the window doesnt show, see `docs/guides/cranelift_aot_egui_hello.md` (Wayland→X11).

View File

@ -1,172 +1,14 @@
# Nyash JSON Native
Layer Guard — json_native
> yyjsonC依存→ 完全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, nonJSON language concerns.
## 🎯 プロジェクト目標
Imports policy (SSOT)
- Dev/CI: file-using allowed for development convenience.
- Prod: use only `nyash.toml` using entries (no adhoc file imports).
- **C依存ゼロ**: yyjsonからの完全脱却
- **Everything is Box**: 全てをNyash Boxで実装
- **80/20ルール**: 動作優先、最適化は後
- **段階切り替え**: `NYASH_JSON_PROVIDER=nyash|yyjson|serde`
## 📦 アーキテクチャ設計
### 🔍 yyjson分析結果
```c
// yyjson核心設計パターン
struct yyjson_val {
uint64_t tag; // 型+サブタイプ+長さ
yyjson_val_uni uni; // ペイロードunion
};
```
### 🎨 Nyash版設計 (Everything is Box)
```nyash
// 🌟 JSON値を表現するBox
box JsonNode {
kind: StringBox // "null"|"bool"|"int"|"string"|"array"|"object"
value: Box // 実際の値(種類に応じて)
children: ArrayBox // 配列・オブジェクト用(オプション)
keys: ArrayBox // オブジェクトのキー配列(オプション)
}
// 🔧 JSON字句解析器
box JsonLexer {
text: StringBox // 入力文字列
pos: IntegerBox // 現在位置
tokens: ArrayBox // トークン配列
}
// 🏗️ JSON構文解析器
box JsonParser {
lexer: JsonLexer // 字句解析器
current: IntegerBox // 現在のトークン位置
}
```
## 📂 ファイル構造
```
apps/lib/json_native/
├── README.md # この設計ドキュメント
├── lexer.nyash # JSON字句解析器状態機械ベース
├── parser.nyash # JSON構文解析器再帰下降
├── node.nyash # JsonNode実装Everything is Box
├── tests/ # テストケース
│ ├── lexer_test.nyash
│ ├── parser_test.nyash
│ └── integration_test.nyash
└── examples/ # 使用例
├── simple_parse.nyash
└── complex_object.nyash
```
## 🎯 実装戦略
### Phase 1: JsonNode基盤1週目
- [x] JsonNode Box実装
- [x] 基本的な値型サポートnull, bool, int, string
- [x] 配列・オブジェクト構造サポート
### Phase 2: JsonLexer実装2週目
- [ ] トークナイザー実装(状態機械ベース)
- [ ] エラーハンドリング
- [ ] 位置情報追跡
### Phase 3: JsonParser実装3週目
- [ ] 再帰下降パーサー実装
- [ ] JsonNode構築
- [ ] ネストされた構造サポート
### Phase 4: 統合最適化4週目
- [ ] C ABI Bridge最小実装
- [ ] 既存JSONBoxとの互換性
- [ ] 性能測定・最適化
## 🔄 既存システムとの統合
### 段階的移行戦略
```bash
# 環境変数で切り替え
export NYASH_JSON_PROVIDER=nyash # 新実装
export NYASH_JSON_PROVIDER=yyjson # 既存C実装
export NYASH_JSON_PROVIDER=serde # Rust実装
```
### 既存APIとの互換性
```nyash
// 既存JSONBox APIを維持
local json = new JSONBox()
json.parse("{\"key\": \"value\"}") // 内部でNyash実装を使用
```
## 🎨 設計思想
### Everything is Box原則
- **JsonNode**: 完全なBox実装
- **境界明確化**: 各コンポーネントをBox化
- **差し替え可能**: プロバイダー切り替え対応
### 80/20ルール適用
- **80%**: まず動く実装(文字列操作ベース)
- **20%**: 後で最適化バイナリ処理、SIMD等
### フォールバック戦略
- **エラー時**: 既存実装に安全復帰
- **性能不足時**: yyjsonに切り替え可能
- **互換性**: 既存APIを100%維持
## 🧪 テスト戦略
### 基本テストケース
```json
{"null": null}
{"bool": true}
{"int": 42}
{"string": "hello"}
{"array": [1,2,3]}
{"object": {"nested": "value"}}
```
### エラーケース
```
{invalid} // 不正なJSON
{"unclosed": "str // 閉じられていない文字列
[1,2, // 不完全な配列
```
### 性能テストケース
```
大きなJSONファイル10MB+
深くネストされた構造100レベル+
多数の小さなオブジェクト10万個+
```
## 🚀 実行例
```nyash
// 基本的な使用例
using "apps/lib/json_native/node.nyash" as JsonNative
// JSON文字列をパース
local text = "{\"name\": \"Nyash\", \"version\": 1}"
local node = JsonNative.parse(text)
// 値にアクセス
print(node.get("name").str()) // "Nyash"
print(node.get("version").int()) // 1
// JSON文字列に戻す
print(node.stringify()) // {"name":"Nyash","version":1}
```
## 📊 進捗追跡
- [ ] Week 1: JsonNode基盤
- [ ] Week 2: JsonLexer実装
- [ ] Week 3: JsonParser実装
- [ ] Week 4: 統合&最適化
**開始日**: 2025-09-22
**目標完了**: 2025-10-20
**実装者**: Claude × User協働
Notes
- Error messages aim to include: “Error at line X, column Y: …”.
- Unterminated string → tokenizer emits "Unterminated string literal" (locked by quick smoke).

View File

@ -59,9 +59,14 @@ box JsonParser {
// Step 2: 構文解析
local result = me.parse_value()
// Step 3: 余剰トークンチェック
// Step 3: 余剰トークンチェック(詳細情報付き)
if result != null and not me.is_at_end() {
me.add_error("Unexpected tokens after JSON value")
local extra = me.current_token()
if extra != null {
me.add_error("Unexpected tokens after JSON value: " + extra.get_type() + "(" + extra.get_value() + ")")
} else {
me.add_error("Unexpected tokens after JSON value")
}
return null
}

View File

@ -0,0 +1,14 @@
Layer Guard — selfhost/vm
Scope and responsibility
- Minimal Ny-based executors and helpers for selfhosting 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 specneutral; new behavior is gated by new tests.

View 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
}
}

View File

@ -7,9 +7,17 @@
// {"op":"ret","value":1}
// ]}]}]
// }
// 振る舞い: 最初の const i64 の値を読み取り、print する。ret は value スロット参照を想定するが、MVPでは無視。
// 振る舞い:
// - M1: 最初の const i64 の値を読み取り print
// - M2: const/binop/compare/ret を最小実装(簡易スキャンで安全に解釈)
static box MirVmMin {
// Public entry used by parity tests (calls into minimal runner)
run(mjson) {
local v = me._run_min(mjson)
print(me._int_to_str(v))
return v
}
// 最小限のスキャン関数(依存ゼロ版)
index_of_from(hay, needle, pos) {
if pos < 0 { pos = 0 }
@ -26,11 +34,11 @@ static box MirVmMin {
}
return -1
}
read_digits(json, pos) {
read_digits(text, pos) {
local out = ""
local i = pos
loop (true) {
local s = json.substring(i, i+1)
local s = text.substring(i, i+1)
if s == "" { break }
if s == "0" || s == "1" || s == "2" || s == "3" || s == "4" || s == "5" || s == "6" || s == "7" || s == "8" || s == "9" {
out = out + s
@ -63,10 +71,10 @@ static box MirVmMin {
if n == 0 { return "0" }
local v = n
local out = ""
local digits = "0123456789"
loop (v > 0) {
local d = v % 10
local ch = "0"
if d == 1 { ch = "1" } else { if d == 2 { ch = "2" } else { if d == 3 { ch = "3" } else { if d == 4 { ch = "4" } else { if d == 5 { ch = "5" } else { if d == 6 { ch = "6" } else { if d == 7 { ch = "7" } else { if d == 8 { ch = "8" } else { if d == 9 { ch = "9" } } } } } } } }
local ch = digits.substring(d, d+1)
out = ch + out
v = v / 10
}
@ -74,26 +82,228 @@ static box MirVmMin {
}
// MVP: 最初の const i64 の値を抽出
_extract_first_const_i64(json) {
if json == null { return 0 }
_extract_first_const_i64(text) {
if text == null { return 0 }
// "op":"const" を探す
local p = json.indexOf("\"op\":\"const\"")
local p = text.indexOf("\"op\":\"const\"")
if p < 0 { return 0 }
// そこから "\"value\":{\"type\":\"i64\",\"value\":" を探す
local key = "\"value\":{\"type\":\"i64\",\"value\":"
local q = me.index_of_from(json, key, p)
local q = me.index_of_from(text, key, p)
if q < 0 { return 0 }
q = q + key.length()
// 連続する数字を読む
local digits = me.read_digits(json, q)
local digits = me.read_digits(text, q)
if digits == "" { return 0 }
return me._str_to_int(digits)
}
// 実行: 値を print し、0 を返すMVP。将来は exit code 連動可。
run(mir_json_text) {
local v = me._extract_first_const_i64(mir_json_text)
print(me._int_to_str(v))
// --- M2 追加: 最小 MIR 実行const/binop/compare/ret ---
_get_map(regs, key) { if regs.has(key) { return regs.get(key) } return 0 }
_set_map(regs, key, val) { regs.set(key, val) }
_find_int_in(seg, keypat) {
local p = seg.indexOf(keypat)
if p < 0 { return null }
p = p + keypat.length()
local i = p
local out = ""
loop(true) {
local ch = seg.substring(i, i+1)
if ch == "" { break }
if ch == "0" || ch == "1" || ch == "2" || ch == "3" || ch == "4" || ch == "5" || ch == "6" || ch == "7" || ch == "8" || ch == "9" { out = out + ch i = i + 1 } else { break }
}
if out == "" { return null }
return me._str_to_int(out)
}
_find_str_in(seg, keypat) {
local p = seg.indexOf(keypat)
if p < 0 { return "" }
p = p + keypat.length()
local q = me.index_of_from(seg, "\"", p)
if q < 0 { return "" }
return seg.substring(p, q)
}
// --- JSON segment helpers (brace/bracket aware, minimal) ---
_seek_obj_start(text, from_pos) {
// scan backward to the nearest '{'
local i = from_pos
loop(true) {
i = i - 1
if i < 0 { return 0 }
local ch = text.substring(i, i+1)
if ch == "{" { return i }
}
return 0
}
_seek_obj_end(text, obj_start) {
// starting at '{', find matching '}' using depth counter
local i = obj_start
local depth = 0
loop(true) {
local ch = text.substring(i, i+1)
if ch == "" { break }
if ch == "{" { depth = depth + 1 }
else { if ch == "}" { depth = depth - 1 } }
if depth == 0 { return i + 1 }
i = i + 1
}
return i
}
_seek_array_end(text, array_after_bracket_pos) {
// given pos right after '[', find the matching ']'
local i = array_after_bracket_pos
local depth = 1
loop(true) {
local ch = text.substring(i, i+1)
if ch == "" { break }
if ch == "[" { depth = depth + 1 }
else { if ch == "]" { depth = depth - 1 } }
if depth == 0 { return i }
i = i + 1
}
return i
}
_map_binop_symbol(sym) {
if sym == "+" { return "Add" }
if sym == "-" { return "Sub" }
if sym == "*" { return "Mul" }
if sym == "/" { return "Div" }
if sym == "%" { return "Mod" }
return "" }
_map_cmp_symbol(sym) {
if sym == "==" { return "Eq" }
if sym == "!=" { return "Ne" }
if sym == "<" { return "Lt" }
if sym == "<=" { return "Le" }
if sym == ">" { return "Gt" }
if sym == ">=" { return "Ge" }
return "" }
_eval_binop(kind, a, b) {
if kind == "Add" { return a + b }
if kind == "Sub" { return a - b }
if kind == "Mul" { return a * b }
if kind == "Div" { if b == 0 { return 0 } else { return a / b } }
if kind == "Mod" { if b == 0 { return 0 } else { return a % b } }
return 0 }
_eval_cmp(kind, a, b) {
if kind == "Eq" { if a == b { return 1 } else { return 0 } }
if kind == "Ne" { if a != b { return 1 } else { return 0 } }
if kind == "Lt" { if a < b { return 1 } else { return 0 } }
if kind == "Gt" { if a > b { return 1 } else { return 0 } }
if kind == "Le" { if a <= b { return 1 } else { return 0 } }
if kind == "Ge" { if a >= b { return 1 } else { return 0 } }
return 0
}
// Locate start of instructions array for given block id
_block_insts_start(mjson, bid) {
local key = "\"id\":" + me._int_to_str(bid)
local p = mjson.indexOf(key)
if p < 0 { return -1 }
local q = me.index_of_from(mjson, "\"instructions\":[", p)
if q < 0 { return -1 }
// "\"instructions\":[" is 16 chars → return pos right after '['
return q + 16
}
_block_insts_end(mjson, insts_start) {
// Bound to the end bracket of this block's instructions array
return me._seek_array_end(mjson, insts_start)
}
_run_min(mjson) {
local regs = new MapBox()
// Control flow: start at block 0, process until ret
local bb = 0
loop(true) {
local pos = me._block_insts_start(mjson, bb)
if pos < 0 { return me._extract_first_const_i64(mjson) }
local block_end = me._block_insts_end(mjson, pos)
// Single-pass over instructions: segment each op object precisely and evaluate
local scan = pos
local moved = 0
loop(true) {
// find next op field within this block
local opos = me.index_of_from(mjson, "\"op\":\"", scan)
if opos < 0 || opos >= block_end { break }
// find exact JSON object bounds for this instruction
// Determine object start as the last '{' between pos..opos
local i = pos
local obj_start = opos
loop(i <= opos) {
local ch0 = mjson.substring(i, i+1)
if ch0 == "{" { obj_start = i }
i = i + 1
}
local obj_end = me._seek_obj_end(mjson, obj_start)
if obj_end > block_end { obj_end = block_end }
local seg = mjson.substring(obj_start, obj_end)
// dispatch by op name (v0/v1 tolerant)
local opname = me._find_str_in(seg, "\"op\":\"")
if opname == "const" {
local cdst = me._find_int_in(seg, "\"dst\":")
local cval = me._find_int_in(seg, "\"value\":{\"type\":\"i64\",\"value\":")
if cdst != null and cval != null { me._set_map(regs, "" + cdst, cval) }
} else {
if opname == "binop" {
local bdst = me._find_int_in(seg, "\"dst\":")
local bkind = me._find_str_in(seg, "\"op_kind\":\"")
if bkind == "" { bkind = me._map_binop_symbol(me._find_str_in(seg, "\"operation\":\"")) }
local blhs = me._find_int_in(seg, "\"lhs\":")
local brhs = me._find_int_in(seg, "\"rhs\":")
if bdst != null and blhs != null and brhs != null {
local a = me._get_map(regs, "" + blhs)
local b = me._get_map(regs, "" + brhs)
local r = me._eval_binop(bkind, a, b)
me._set_map(regs, "" + bdst, r)
}
} else {
if opname == "compare" {
local kdst = me._find_int_in(seg, "\"dst\":")
local kkind = me._find_str_in(seg, "\"cmp\":\"")
if kkind == "" { kkind = me._map_cmp_symbol(me._find_str_in(seg, "\"operation\":\"")) }
local klhs = me._find_int_in(seg, "\"lhs\":")
local krhs = me._find_int_in(seg, "\"rhs\":")
if kdst != null and klhs != null and krhs != null {
local a = me._get_map(regs, "" + klhs)
local b = me._get_map(regs, "" + krhs)
local r = me._eval_cmp(kkind, a, b)
me._set_map(regs, "" + kdst, r)
}
} else {
if opname == "jump" {
local tgt = me._find_int_in(seg, "\"target\":")
if tgt != null { bb = tgt scan = block_end moved = 1 break }
} else {
if opname == "branch" {
local cond = me._find_int_in(seg, "\"cond\":")
local then_id = me._find_int_in(seg, "\"then\":")
local else_id = me._find_int_in(seg, "\"else\":")
local cval = 0
if cond != null { cval = me._get_map(regs, "" + cond) }
if cval != 0 { bb = then_id } else { bb = else_id }
scan = block_end
moved = 1
break
} else {
if opname == "ret" {
local rv = me._find_int_in(seg, "\"value\":")
if rv == null { rv = 0 }
return me._get_map(regs, "" + rv)
}
}
}
}
}
}
// advance to the end of this instruction object
scan = obj_end
}
// No ret encountered in this block; if control moved, continue with new bb
if moved == 1 { continue }
// Fallback when ret not found at all in processed blocks
return me._extract_first_const_i64(mjson)
}
return me._extract_first_const_i64(mjson)
}
}

View 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\":[]}"
}
}

View File

@ -7,7 +7,9 @@ static box Main {
main(args) {
// 既定の最小 MIR(JSON v0)
local json = "{\"functions\":[{\"name\":\"main\",\"params\":[],\"blocks\":[{\"id\":0,\"instructions\":[{\"op\":\"const\",\"dst\":1,\"value\":{\"type\":\"i64\",\"value\":42}},{\"op\":\"ret\",\"value\":1}]}]}]}"
if args { if args.size() > 0 { local s = args.get(0) if s { json = s } } }
return MirVmMin.run(json)
if args != null { if args.size() > 0 { local s = args.get(0) if s != null { json = s } } }
local v = MirVmMin._run_min(json)
print(MirVmMin._int_to_str(v))
return 0
}
}

47
docs/abi/vm-kernel.md Normal file
View 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).
- FailFast: any error returns immediately; no silent fallbacks in dev.
- Safe by default: reentry 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 percall 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).
Reentry Guard
- A threadlocal flag prevents reentering the Ny Kernel from within an ongoing Ny Kernel call.
- On violation, the bridge errors immediately (FailFast).
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 percall overhead.
- Keep JSON schemas tiny and versioned; include a toplevel "v" if necessary.

View File

@ -0,0 +1,90 @@
# Phase 15.7: Known化Rewrite統合dev観測と MiniVM 安定化dev限定
目的
- Builderでの Known 化と Instance→Function の統一Known 経路を優先し、実行系VM/LLVM/Nyを単純化する。
- 早期観測resolve.try/choose, ssa.phiを devonly で整備し、Union の発生点を特定可能にする。
- 表示APIを `str()` に統一(互換: `stringify()`)し、言語表面のブレを解消する(挙動不変)。
- MiniVMNyを安全に安定化M2/M3代表ケース。NYABI Kernel は“下地のみ”既定OFF
背景
- Instance→Function 正規化の方針は既定ON。Known 経路は関数化し、VM側は単純化する。
- resolve.try/chooseBuilderと ssa.phiBuilderの観測は devonly で導入済み既定OFF
- MiniVM は 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/1Known 優先→一意候補; ユーザーBox限定
3) Known→関数化`obj.m → Class.m(me,…)`)/一意候補フォールバック(決定性確保)
- レガシー側の関数化は dev ガードで抑止可能: `NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1`(移行期間の重複回避)
スコープ(やること)
1) Builder: Known 化 + Rewrite 統合Stage1
- P0: me 注入・Known 化origin 付与/維持)— 軽量PHI補強単一/一致時)
- P1: Known 経路 100% 関数化obj.m → Class.m(me,…)。special は `toString→str互換:stringify/equals` を統合
- 観測: resolve.try/choose / ssa.phi を devonly で JSONL 出力既定OFF`resolve.choose``certainty` を付加し、KPIKnown率を任意出力`NYASH_DEBUG_KPI_KNOWN=1`, `NYASH_DEBUG_SAMPLE_EVERY=N`)。
2) 表示APIの統一挙動不変
- 規範: `str()` / `x.str()`(同義)。`toString()` は早期に `str()` へ正規化
- 互換: `stringify()` は当面エイリアスとして許容
- QuickRef/ガイドの更新plus混在の誘導も `str()` に統一)
3) MiniVMMirVmMin安定化devのみ
- 厳密セグメントによる単一パス化、M2/M3 代表スモーク常緑const/binop/compare/branch/jump/ret
- パリティ: VM↔LLVM↔Ny のミニ・パリティ 2〜3件
4) NYABIVM Kernel Bridge下地未配線・既定OFF
- docs/abi/vm-kernel.md関数: caps()/policy.*()/resolve_method_batch()
- スケルトン: apps/selfhost/vm/boxes/vm_kernel_box.nyashpolicy スタブ)
- 既定OFFトグル予約: NYASH_VM_NY_KERNEL, *_TIMEOUT_MS, *_TRACE
非スコープ(やらない)
- 既定挙動の変更Rust VM/LLVMが主軸のまま
- PHI/SSAの一般化Phase 16 で扱う)
- VM Kernel の本配線(観測・ポリシーは devonly/未配線)
リスクと軽減策
- 性能: 境界越えは後Phaseに限る本Phaseは未配線。MiniVMは開発補助で性能要件なし。
- 複雑性: 設計は最小APIに限定。拡張は追加のみ後方互換維持
- 安全: すべて既定OFF。FailFast方針。再入禁止/タイムアウトを仕様に明記。
受け入れ条件Acceptance
- quick: MiniVMM2/M3代表スモーク緑const/binop/compare/branch/jump/ret
- integration: 代表パリティ緑llvmlite/ハーネス)
- Builder: resolve.try/choose と ssa.phi が devonly で取得可能NYASH_DEBUG_*
- 表示API: QuickRef/ガイドが `str()` に統一(実行挙動は従前と同じ)
- Unified Call は開発既定ONだが、`NYASH_MIR_UNIFIED_CALL=0|false|off` で即時オプトアウト可能(段階移行)。
実装タスク(小粒)
1. origin/observe/rewrite の分割方針を CURRENT_TASK に反映(ガイド/README付き
2. Known fastpath の一本化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
ロールバック方針
- MiniVMの変更は 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
- MiniVM: apps/selfhost/vm/boxes/mir_vm_min.nyash
- スモーク: tools/smokes/v2/profiles/quick/core/
更新履歴
- 20250928 v2本書: Known 化Rewrite 統合dev観測、表示API `str()` 統一、MiniVM 安定化へ焦点を再定義
- 20250928 初版: MiniVM M3 + NYABI下地の計画

View File

@ -19,6 +19,12 @@
5) LLVM 統合任意・AOT/ハーネス)
- 実行: `tools/smokes/v2/run.sh --profile integration`
最小 Ny 実行器MirVmMin
- 目的: Ny だけで MIR(JSON v0) のごく最小セットconst/binop/compare/retを実行できることを確認。
- 実行例VM:
- `./target/release/nyash --backend vm apps/selfhost/vm/mir_min_entry.nyash`
- 引数で MIR(JSON) を渡すことも可能(単一文字列)。簡単な例は `apps/selfhost/vm/mir_min_entry.nyash` のコメントを参照。
検証
- 期待出力: `Result: 0`selfhostminimal
- スモーク:全成功(非 0 は失敗)

View File

@ -0,0 +1,76 @@
# Nyash Quick Reference (MVP)
Purpose
- Onepage 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 defaultON; backends (VM/LLVM/Ny) receive the unified call shape.
- Member: `obj.field` or `obj.m`
Display & Conversion
- Humanreadable 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: `&& ||` (shortcircuit, sideeffect 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 FailFast with a clear message.
Truthiness (boolean context)
- `Bool` → itself
- `Integer``0` is false; nonzero is true
- `String` → empty string is false; otherwise true
- `Array`/`Map` → nonnull is true (size is not consulted)
- `null`/`void` → false
Equality and Comparison
- `==` and `!=` compare primitive values (Integer/Bool/String). No implicit crosstype 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: filebased `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 toplevel 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.

View File

@ -26,15 +26,52 @@ impl MirInterpreter {
) -> Result<VMValue, VMError> {
match callee {
Callee::Global(func_name) => self.execute_global_function(func_name, args),
Callee::Method {
box_name: _,
method,
receiver,
certainty: _,
} => {
Callee::Method { box_name: _, method, receiver, certainty: _, } => {
if let Some(recv_id) = receiver {
let recv_val = self.reg_load(*recv_id)?;
let dev_trace = std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1");
// Fast bridge for builtin boxes (Array) and common methods.
// Preserve legacy semantics when plugins are absent.
if let VMValue::BoxRef(bx) = &recv_val {
// ArrayBox bridge
if let Some(arr) = bx.as_any().downcast_ref::<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";
if dev_trace && is_kw {
let a0 = args.get(0).and_then(|id| self.reg_load(*id).ok());

View File

@ -62,18 +62,20 @@ fn reroute_to_correct_method(
}
/// Try mapping special methods to canonical targets (table-driven).
/// Example: toString/0 → stringify/0 (prefer instance class, then base class without "Instance" suffix).
/// Example: toString/0 → str/0互換: stringify/0(prefer instance class, then base class without "Instance" suffix).
fn try_special_reroute(
interp: &mut MirInterpreter,
recv_cls: &str,
parsed: &ParsedSig<'_>,
arg_vals: Option<&[VMValue]>,
) -> Option<Result<VMValue, VMError>> {
// toString → stringify
// toString → str互換: stringify
if parsed.method == "toString" && parsed.arity_str == "0" {
// Prefer instance class stringify first, then base (strip trailing "Instance")
// Prefer instance class 'str' first, then basestrip trailing "Instance")。なければ 'stringify' を互換で探す
let base = recv_cls.strip_suffix("Instance").unwrap_or(recv_cls);
let candidates = [
format!("{}.str/0", recv_cls),
format!("{}.str/0", base),
format!("{}.stringify/0", recv_cls),
format!("{}.stringify/0", base),
];
@ -91,7 +93,7 @@ fn try_special_reroute(
"method": parsed.method,
"arity": parsed.arity_str,
"target": name,
"reason": "toString->stringify",
"reason": if name.ends_with(".str/0") { "toString->str" } else { "toString->stringify" },
}),
);
return Some(interp.exec_function_inner(&f, arg_vals));

View File

@ -343,33 +343,42 @@ impl P2PBox {
if let Ok(mut li) = last_intent.write() {
*li = Some(env.intent.get_name().to_string_box().value);
}
// 最小インタープリタで FunctionBox を実行
let mut interp = crate::interpreter::NyashInterpreter::new();
// キャプチャ注入
for (k, v) in func_clone.env.captures.iter() {
interp.declare_local_variable(k, v.clone_or_share());
// 最小インタープリタで FunctionBox を実行legacy, feature-gated
#[cfg(feature = "interpreter-legacy")]
{
let mut interp = crate::interpreter::NyashInterpreter::new();
// キャプチャ注入
for (k, v) in func_clone.env.captures.iter() {
interp.declare_local_variable(k, v.clone_or_share());
}
if let Some(me_w) = &func_clone.env.me_value {
if let Some(me_arc) = me_w.upgrade() {
interp.declare_local_variable("me", (*me_arc).clone_or_share());
}
}
// 引数束縛: intent, from必要数だけ
let args: Vec<Box<dyn NyashBox>> = vec![
Box::new(env.intent.clone()),
Box::new(StringBox::new(env.from.clone())),
];
for (i, p) in func_clone.params.iter().enumerate() {
if let Some(av) = args.get(i) {
interp.declare_local_variable(p, av.clone_or_share());
}
}
// 本体実行
crate::runtime::global_hooks::push_task_scope();
for st in &func_clone.body {
let _ = interp.execute_statement(st);
}
crate::runtime::global_hooks::pop_task_scope();
}
if let Some(me_w) = &func_clone.env.me_value {
if let Some(me_arc) = me_w.upgrade() {
interp.declare_local_variable("me", (*me_arc).clone_or_share());
#[cfg(not(feature = "interpreter-legacy"))]
{
if crate::config::env::cli_verbose() {
eprintln!("[warn] FunctionBox handler requires interpreter-legacy; skipped execution");
}
}
// 引数束縛: intent, from必要数だけ
let args: Vec<Box<dyn NyashBox>> = vec![
Box::new(env.intent.clone()),
Box::new(StringBox::new(env.from.clone())),
];
for (i, p) in func_clone.params.iter().enumerate() {
if let Some(av) = args.get(i) {
interp.declare_local_variable(p, av.clone_or_share());
}
}
// 本体実行
crate::runtime::global_hooks::push_task_scope();
for st in &func_clone.body {
let _ = interp.execute_statement(st);
}
crate::runtime::global_hooks::pop_task_scope();
if once {
flag.store(false, Ordering::SeqCst);
if let Ok(mut flags) = flags_arc.write() {

View File

@ -22,7 +22,7 @@ def lower_mir_call(owner, builder: ir.IRBuilder, mir_call: Dict[str, Any], dst_v
"""
# Check if unified call is enabled
use_unified = os.getenv("NYASH_MIR_UNIFIED_CALL", "0") != "0"
use_unified = os.getenv("NYASH_MIR_UNIFIED_CALL", "1").lower() not in ("0", "false", "off")
if not use_unified:
# Fall back to legacy dispatching
return lower_legacy_call(owner, builder, mir_call, dst_vid, vmap, resolver)

View File

@ -37,6 +37,9 @@ mod plugin_sigs; // plugin signature loader
mod stmts;
mod utils;
mod vars; // variables/scope helpers // small loop helpers (header/exit context)
mod origin; // P0: origin inferenceme/Knownと PHI 伝播(軽量)
mod observe; // P0: dev-only observability helpersssa/resolve
mod rewrite; // P1: Known rewrite & special consolidation
// Unified member property kinds for computed/once/birth_once
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@ -99,6 +102,11 @@ pub struct MirBuilder {
/// Index of static methods seen during lowering: name -> [(BoxName, arity)]
pub(super) static_method_index: std::collections::HashMap<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
/// Loop context stacks for lowering break/continue inside nested control flow
@ -169,6 +177,8 @@ impl MirBuilder {
plugin_method_sigs,
current_static_box: None,
static_method_index: std::collections::HashMap::new(),
method_tail_index: std::collections::HashMap::new(),
method_tail_index_source_len: 0,
loop_header_stack: Vec::new(),
loop_exit_stack: Vec::new(),
@ -256,6 +266,56 @@ impl MirBuilder {
self.debug_scope_stack.last().cloned()
}
// ----------------------
// Method tail index (performance helper)
// ----------------------
fn rebuild_method_tail_index(&mut self) {
self.method_tail_index.clear();
if let Some(ref module) = self.current_module {
for name in module.functions.keys() {
if let (Some(dot), Some(slash)) = (name.rfind('.'), name.rfind('/')) {
if slash > dot {
let tail = &name[dot..];
self.method_tail_index
.entry(tail.to_string())
.or_insert_with(Vec::new)
.push(name.clone());
}
}
}
self.method_tail_index_source_len = module.functions.len();
} else {
self.method_tail_index_source_len = 0;
}
}
fn ensure_method_tail_index(&mut self) {
let need_rebuild = match self.current_module {
Some(ref refmod) => self.method_tail_index_source_len != refmod.functions.len(),
None => self.method_tail_index_source_len != 0,
};
if need_rebuild {
self.rebuild_method_tail_index();
}
}
pub(super) fn method_candidates(&mut self, method: &str, arity: usize) -> Vec<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
pub fn build_module(&mut self, ast: ASTNode) -> Result<MirModule, String> {
@ -354,84 +414,13 @@ impl MirBuilder {
.as_ref()
.map(|f| f.signature.name.clone());
let dbg_region_id = self.debug_current_region_id();
// P0: PHI の軽量補強と観測は、関数ブロック取得前に実施して借用競合を避ける
if let MirInstruction::Phi { dst, inputs } = &instruction {
origin::phi::propagate_phi_meta(self, *dst, inputs);
observe::ssa::emit_phi(self, *dst, inputs);
}
if let Some(ref mut function) = self.current_function {
// Dev-safe meta propagation for PHI: if all incoming values agree on type/origin,
// propagate to the PHI destination. This helps downstream resolution (e.g.,
// instance method rewrite across branches) without changing semantics.
if let MirInstruction::Phi { dst, inputs } = &instruction {
// Propagate value_types when all inputs share the same known type
let mut common_ty: Option<super::MirType> = None;
let mut ty_agree = true;
for (_bb, v) in inputs.iter() {
if let Some(t) = self.value_types.get(v).cloned() {
match &common_ty {
None => common_ty = Some(t),
Some(ct) => {
if ct != &t { ty_agree = false; break; }
}
}
} else {
ty_agree = false;
break;
}
}
if ty_agree {
if let Some(ct) = common_ty.clone() {
self.value_types.insert(*dst, ct);
}
}
// Propagate value_origin_newbox when all inputs share same origin class
let mut common_cls: Option<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(block) = function.get_block_mut(block_id) {
if utils::builder_debug_enabled() {
eprintln!(
@ -602,7 +591,10 @@ impl MirBuilder {
// VM will treat plain NewBox as constructed; dev verify warns if needed.
// - For builtins/plugins, keep BoxCall("birth") fallback to preserve legacy init.
let is_user_box = self.user_defined_boxes.contains(&class);
if !is_user_box {
// Dev safety: allow disabling birth() injection for builtins to avoid
// unified-call method dispatch issues while migrating. Off by default unless explicitly enabled.
let allow_builtin_birth = std::env::var("NYASH_DEV_BIRTH_INJECT_BUILTINS").ok().as_deref() == Some("1");
if !is_user_box && allow_builtin_birth {
let birt_mid = resolve_slot_by_type_name(&class, "birth");
self.emit_box_or_plugin_call(
None,

View File

@ -81,12 +81,117 @@ impl super::MirBuilder {
return self.emit_legacy_call(dst, target, args);
}
// Emit resolve.try for method targets (dev-only; default OFF)
let arity_for_try = args.len();
if let CallTarget::Method { ref box_type, ref method, receiver } = target {
let recv_cls = box_type.clone()
.or_else(|| self.value_origin_newbox.get(&receiver).cloned())
.unwrap_or_default();
// Use indexed candidate lookup (tail → names)
let candidates: Vec<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
let callee = call_unified::convert_target_to_callee(
target,
if let CallTarget::Global(ref _n) = target { /* dev trace removed */ }
// Fallback: if Global target is unknown, try unique static-method mapping (name/arity)
let callee = match call_unified::convert_target_to_callee(
target.clone(),
&self.value_origin_newbox,
&self.value_types,
)?;
) {
Ok(c) => c,
Err(e) => {
if let CallTarget::Global(ref name) = target {
// 0) Dev-only safety: treat condition_fn as always-true predicate when missing
if name == "condition_fn" {
let dstv = dst.unwrap_or_else(|| self.value_gen.next());
self.emit_instruction(MirInstruction::Const { dst: dstv, value: super::ConstValue::Integer(1) })?;
return Ok(());
}
// 1) Direct module function fallback: call by name if present
if let Some(ref module) = self.current_module {
if module.functions.contains_key(name) {
let dstv = dst.unwrap_or_else(|| self.value_gen.next());
let name_const = self.value_gen.next();
self.emit_instruction(MirInstruction::Const { dst: name_const, value: super::ConstValue::String(name.clone()) })?;
self.emit_instruction(MirInstruction::Call { dst: Some(dstv), func: name_const, callee: None, args: args.clone(), effects: EffectMask::IO })?;
self.annotate_call_result_from_func_name(dstv, name);
return Ok(());
}
}
// 2) Unique static-method fallback: name+arity → Box.name/Arity
if let Some(cands) = self.static_method_index.get(name) {
let mut matches: Vec<(String, usize)> = cands
.iter()
.cloned()
.filter(|(_, ar)| *ar == arity_for_try)
.collect();
if matches.len() == 1 {
let (bx, _arity) = matches.remove(0);
let func_name = format!("{}.{}{}", bx, name, format!("/{}", arity_for_try));
// Emit legacy call directly to preserve behavior
let dstv = dst.unwrap_or_else(|| self.value_gen.next());
let name_const = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: name_const,
value: super::ConstValue::String(func_name.clone()),
})?;
self.emit_instruction(MirInstruction::Call {
dst: Some(dstv),
func: name_const,
callee: None,
args: args.clone(),
effects: EffectMask::IO,
})?;
// annotate
self.annotate_call_result_from_func_name(dstv, func_name);
return Ok(());
}
}
}
return Err(e);
}
};
// Emit resolve.choose for method callee (dev-only; default OFF)
if let Callee::Method { box_name, method, certainty, .. } = &callee {
let chosen = format!("{}.{}{}", box_name, method, format!("/{}", arity_for_try));
let meta = serde_json::json!({
"recv_cls": box_name,
"method": method,
"arity": arity_for_try,
"chosen": chosen,
"certainty": format!("{:?}", certainty),
"reason": "unified",
});
super::observe::resolve::emit_choose(self, meta);
}
// Validate call arguments
call_unified::validate_call_args(&callee, &args)?;
@ -114,49 +219,10 @@ impl super::MirBuilder {
args: Vec<ValueId>,
) -> Result<(), String> {
match target {
CallTarget::Method { receiver, method, box_type } => {
// If receiver is a user-defined box, lower to function call: "Box.method/(1+arity)" with receiver as first arg
let mut is_user_box = false;
let mut class_name_opt: Option<String> = None;
if let Some(bt) = box_type.clone() { class_name_opt = Some(bt); }
if class_name_opt.is_none() {
if let Some(cn) = self.value_origin_newbox.get(&receiver) { class_name_opt = Some(cn.clone()); }
}
if class_name_opt.is_none() {
if let Some(t) = self.value_types.get(&receiver) {
if let super::MirType::Box(bn) = t { class_name_opt = Some(bn.clone()); }
}
}
if let Some(cls) = class_name_opt.clone() {
// Prefer explicit registry of user-defined boxes when available
if self.user_defined_boxes.contains(&cls) { is_user_box = true; }
}
if is_user_box {
let cls = class_name_opt.unwrap();
let arity = args.len(); // function name arity excludes 'me'
let fname = super::calls::function_lowering::generate_method_function_name(&cls, &method, arity);
let name_const = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: name_const,
value: super::ConstValue::String(fname),
})?;
let mut call_args = Vec::with_capacity(arity);
call_args.push(receiver); // pass 'me' first
call_args.extend(args.into_iter());
// Allocate a destination if not provided
let actual_dst = if let Some(d) = dst { d } else { self.value_gen.next() };
self.emit_instruction(MirInstruction::Call {
dst,
func: name_const,
callee: None,
args: call_args,
effects: EffectMask::READ.add(Effect::ReadHeap),
})?;
// Annotate result type/origin using lowered function signature
if let Some(d) = dst.or(Some(actual_dst)) { self.annotate_call_result_from_func_name(d, super::calls::function_lowering::generate_method_function_name(&cls, &method, arity)); }
return Ok(());
}
// Else fall back to plugin/boxcall path (StringBox/ArrayBox/MapBox etc.)
CallTarget::Method { receiver, method, box_type: _ } => {
// LEGACY PATH (after unified migration):
// Instance→Function rewrite is centralized in unified call path.
// Legacy path no longer functionizes; always use Box/Plugin call here.
self.emit_box_or_plugin_call(dst, receiver, method, None, args, EffectMask::IO)
},
CallTarget::Constructor(box_type) => {
@ -400,6 +466,7 @@ impl super::MirBuilder {
name: String,
args: Vec<ASTNode>,
) -> Result<ValueId, String> {
// dev trace removed
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
let cur_fun = self.current_function.as_ref().map(|f| f.signature.name.clone()).unwrap_or_else(|| "<none>".to_string());
eprintln!(
@ -440,19 +507,20 @@ impl super::MirBuilder {
arg_values.push(self.build_expression(a)?);
}
// Phase 3.2: Use unified call for basic functions like print
let use_unified = std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1";
if use_unified {
// New unified path - use emit_unified_call with Global target
// Special-case: global str(x) → x.str() に正規化(内部は関数へ統一される)
if name == "str" && arg_values.len() == 1 {
let dst = self.value_gen.next();
self.emit_unified_call(
Some(dst),
CallTarget::Global(name),
arg_values,
)?;
Ok(dst)
} else {
// Use unified method emission; downstream rewrite will functionize as needed
self.emit_method_call(Some(dst), arg_values[0], "str".to_string(), vec![])?;
return Ok(dst);
}
// Phase 3.2: Unified call is default ON, but only use it for known builtins/externs.
let use_unified = super::calls::call_unified::is_unified_call_enabled()
&& (super::call_resolution::is_builtin_function(&name)
|| super::call_resolution::is_extern_function(&name));
if !use_unified {
// Legacy path
let dst = self.value_gen.next();
@ -461,6 +529,7 @@ impl super::MirBuilder {
let callee = match self.resolve_call_target(&name) {
Ok(c) => c,
Err(_e) => {
// dev trace removed
// Fallback: if exactly one static method with this name and arity is known, call it.
if let Some(cands) = self.static_method_index.get(&name) {
let mut matches: Vec<(String, usize)> = cands
@ -517,6 +586,15 @@ impl super::MirBuilder {
effects: EffectMask::READ.add(Effect::ReadHeap),
})?;
Ok(dst)
} else {
// Unified path for builtins/externs
let dst = self.value_gen.next();
self.emit_unified_call(
Some(dst),
CallTarget::Global(name),
arg_values,
)?;
Ok(dst)
}
}

View File

@ -13,7 +13,10 @@ use super::extern_calls;
/// Check if unified call system is enabled
pub fn is_unified_call_enabled() -> bool {
std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1"
match std::env::var("NYASH_MIR_UNIFIED_CALL").ok().as_deref().map(|s| s.to_ascii_lowercase()) {
Some(s) if s == "0" || s == "false" || s == "off" => false,
_ => true, // default ON during development; explicit opt-out supported
}
}
/// Convert CallTarget to Callee

View File

@ -15,7 +15,7 @@ impl super::MirBuilder {
}
// Phase 3.1: Use unified call with CallTarget::Value for indirect calls
let use_unified = std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1";
let use_unified = super::calls::call_unified::is_unified_call_enabled();
if use_unified {
// New unified path - use emit_unified_call with Value target

View File

@ -1,4 +1,4 @@
use super::{EffectMask, FunctionSignature, MirFunction, MirInstruction, MirModule, MirType, ValueId};
use super::{EffectMask, FunctionSignature, MirFunction, MirInstruction, MirModule, MirType, ValueId, BasicBlockId, ConstValue};
use crate::ast::ASTNode;
// Lifecycle routines extracted from builder.rs
@ -265,6 +265,29 @@ impl super::MirBuilder {
module.add_function(function);
// Dev stub: provide condition_fn when missing to satisfy predicate calls in JSON lexers
// Returns integer 1 (truthy) and accepts one argument (unused).
if module.functions.get("condition_fn").is_none() {
let mut sig = FunctionSignature {
name: "condition_fn".to_string(),
params: vec![MirType::Integer], // accept one i64-like arg
return_type: MirType::Integer,
effects: EffectMask::PURE,
};
let entry = BasicBlockId::new(0);
let mut f = MirFunction::new(sig, entry);
// parameter slot (unused in body)
let _param = f.next_value_id();
f.params.push(_param);
// body: const 1; return it
let one = f.next_value_id();
if let Some(bb) = f.get_block_mut(entry) {
bb.add_instruction(MirInstruction::Const { dst: one, value: ConstValue::Integer(1) });
bb.add_instruction(MirInstruction::Return { value: Some(one) });
}
module.add_function(f);
}
Ok(module)
}
}

View File

@ -91,8 +91,7 @@ impl MirBuilder {
method: String,
arguments: &[ASTNode],
) -> 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
.pin_to_slot(object_value, "@recv")
.unwrap_or(object_value);
@ -125,372 +124,16 @@ impl MirBuilder {
if let MirType::Box(bn) = t { class_name_opt = Some(bn.clone()); }
}
}
// Instance→Function rewrite (obj.m(a) → Box.m/Arity(obj,a))
// Phase 2 policy: Only rewrite when receiver class is Known (from origin propagation).
let class_known = self.value_origin_newbox.get(&object_value).is_some();
// Rationale:
// - Keep language surface idiomatic (obj.method()), while executing
// deterministically as a direct function call.
// - Prod VM forbids user Instance BoxCall fallback by policy; this
// rewrite guarantees prod runs without runtime instance-dispatch.
// Control:
// NYASH_BUILDER_REWRITE_INSTANCE={1|true|on} → force enable
// NYASH_BUILDER_REWRITE_INSTANCE={0|false|off} → force disable
let rewrite_enabled = {
match std::env::var("NYASH_BUILDER_REWRITE_INSTANCE").ok().as_deref().map(|v| v.to_ascii_lowercase()) {
Some(ref s) if s == "0" || s == "false" || s == "off" => false,
Some(ref s) if s == "1" || s == "true" || s == "on" => true,
_ => {
// Default: ON (prod/dev/ci) unless明示OFF。再発防止のため常時関数化を優先。
true
}
}
};
// Emit resolve.try event (dev-only) before making a decision
if rewrite_enabled {
if let Some(ref module) = self.current_module {
let tail = format!(".{}{}", method, format!("/{}", arguments.len()));
let candidates: Vec<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
// レガシー経路BoxCall/Pluginへ送る安定優先・挙動不変
let result_id = self.value_gen.next();
self.emit_box_or_plugin_call(
Some(result_id),
object_value,
method,
method.clone(),
None,
arg_values,
crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap),
super::EffectMask::READ.add(super::Effect::ReadHeap),
)?;
Ok(result_id)
}
}

View File

@ -0,0 +1,23 @@
# observe — Builder 観測devonly/既定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 の機能をここに持ち込まない。

View File

@ -0,0 +1,8 @@
//! Builder observability helpers (devonly; default OFF)
//!
//! - ssa: PHI/SSA related debug emissions
//! - resolve: method resolution try/choose既存呼び出しの置換は段階的に
pub mod ssa;
pub mod resolve;

View 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)
})
}
/// Devonly: emit a resolve.try eventcandidates 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);
}
/// Devonly: emit a resolve.choose eventdecision
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);
}
}

View File

@ -0,0 +1,43 @@
use super::super::{BasicBlockId, MirBuilder, ValueId};
/// Emit a devonly JSONL event for a PHI decision.
/// Computes predecessor metatype/originfrom the builders 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);
}

View File

@ -0,0 +1,26 @@
# origin — Known 化(起源付与)/ PHI 伝播(軽量)
目的P0
- 受け手me/receiverの「起源クラス」を最小限付与して Known 化する。
- PHI で型/起源が全入力で一致する場合に限り、dst にメタを伝播する。
- 仕様は不変FailFast/フォールバック追加なし)。あくまで観測と最小補強のみ。
責務
- 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 呼び出し、命令生成。

View 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));
}
}

View 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 specneutral挙動不変
//!
//! Modules
//! - infer: entry points for annotating originsme/receiver/newbox
//! - phi: lightweight propagation at PHI when全入力が一致
pub mod infer;
pub mod phi;

View 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); } }
}

View 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で可視化し、挙動は不変。

View 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 Knownroute 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)
}

View 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;

View 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)
}

View File

@ -124,7 +124,7 @@ impl super::MirBuilder {
super::utils::builder_debug_log(&format!("fallback print value={}", value));
// Phase 3.2: Use unified call for print statements
let use_unified = std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1";
let use_unified = super::calls::call_unified::is_unified_call_enabled();
if use_unified {
// New unified path - treat print as global function
@ -185,14 +185,19 @@ impl super::MirBuilder {
) -> Result<ValueId, String> {
let mut last_value = None;
for (i, var_name) in variables.iter().enumerate() {
let value_id = if i < initial_values.len() && initial_values[i].is_some() {
let var_id = if i < initial_values.len() && initial_values[i].is_some() {
// Use initializer's ValueId directly to avoid SSA aliasing/undefined use
let init_expr = initial_values[i].as_ref().unwrap();
self.build_expression(*init_expr.clone())?
let init_val = self.build_expression(*init_expr.clone())?;
init_val
} else {
self.value_gen.next()
// Create a concrete register for uninitialized locals (Void)
let vid = self.value_gen.next();
self.emit_instruction(MirInstruction::Const { dst: vid, value: ConstValue::Void })?;
vid
};
self.variable_map.insert(var_name.clone(), value_id);
last_value = Some(value_id);
self.variable_map.insert(var_name.clone(), var_id);
last_value = Some(var_id);
}
Ok(last_value.unwrap_or_else(|| self.value_gen.next()))
}
@ -314,6 +319,8 @@ impl super::MirBuilder {
value: ConstValue::String(me_tag),
})?;
self.variable_map.insert("me".to_string(), me_value);
// P0: Known 化 — 分かる範囲で me の起源クラスを付与(挙動不変)。
super::origin::infer::annotate_me_origin(self, me_value);
Ok(me_value)
}
}

View File

@ -76,31 +76,31 @@ impl super::MirBuilder {
args: Vec<super::ValueId>,
effects: super::EffectMask,
) -> Result<(), String> {
// Check environment variable for unified call usage
let use_unified = std::env::var("NYASH_MIR_UNIFIED_CALL")
.unwrap_or_else(|_| "0".to_string()) != "0";
if use_unified {
// Use unified call emission for BoxCall
// First, try to determine the box type
let mut box_type: Option<String> = self.value_origin_newbox.get(&box_val).cloned();
if box_type.is_none() {
if let Some(t) = self.value_types.get(&box_val) {
match t {
super::MirType::String => box_type = Some("StringBox".to_string()),
super::MirType::Box(name) => box_type = Some(name.clone()),
_ => {}
}
// Check environment variable for unified call usage, with safe overrides for core/user boxes
let use_unified_env = super::calls::call_unified::is_unified_call_enabled();
// First, try to determine the box type
let mut box_type: Option<String> = self.value_origin_newbox.get(&box_val).cloned();
if box_type.is_none() {
if let Some(t) = self.value_types.get(&box_val) {
match t {
super::MirType::String => box_type = Some("StringBox".to_string()),
super::MirType::Box(name) => box_type = Some(name.clone()),
_ => {}
}
}
// Use emit_unified_call with Method target
}
// Prefer legacy BoxCall for core collection boxes and user instance boxes (stability first)
let prefer_legacy = match box_type.as_deref() {
Some("ArrayBox") | Some("MapBox") | Some("StringBox") => true,
Some(bt) => !bt.ends_with("Box"), // user instance class name (e.g., JsonTokenizer)
None => false,
};
if use_unified_env && !prefer_legacy {
let target = super::builder_calls::CallTarget::Method {
box_type,
method: method.clone(),
receiver: box_val,
};
return self.emit_unified_call(dst, target, args);
}

View File

@ -55,5 +55,131 @@ pub(super) fn apply_cli_directives_from_source(
}
// Lint: enforce fields at top-of-box (delegated)
super::pipeline::lint_fields_top(code, strict_fields, verbose)
super::pipeline::lint_fields_top(code, strict_fields, verbose)?;
// Dev-only guards (strict but opt-in via env)
// 1) ASI strict: disallow binary operator at end-of-line (line continuation)
if std::env::var("NYASH_ASI_STRICT").ok().as_deref() == Some("1") {
// operators to check (suffixes)
const OP2: [&str; 6] = ["==", "!=", "<=", ">=", "&&", "||"];
const OP1: [&str; 7] = ["+", "-", "*", "/", "%", "<", ">"];
for (i, line) in code.lines().enumerate() {
let l = line.trim_end();
if l.is_empty() { continue; }
let mut bad = false;
for op in OP2.iter() { if l.ends_with(op) { bad = true; break; } }
if !bad { for op in OP1.iter() { if l.ends_with(op) { bad = true; break; } } }
if bad {
return Err(format!("Parse error: Strict ASI violation — line {} ends with operator", i + 1));
}
}
}
// 2) '+' mixed types (String and Number) error (opt-in)
if std::env::var("NYASH_PLUS_MIX_ERROR").ok().as_deref() == Some("1") {
for (i, line) in code.lines().enumerate() {
if let Some((ltok, rtok)) = find_plus_operands(line) {
let left_is_num = is_number_literal(ltok);
let right_is_str = is_string_literal(rtok);
let left_is_str = is_string_literal(ltok);
let right_is_num = is_number_literal(rtok);
if (left_is_num && right_is_str) || (left_is_str && right_is_num) {
return Err(format!("Type error: '+' mixed String and Number at line {} (use str()/explicit conversion)", i + 1));
}
}
}
}
// 3) '==' on likely Box variables: emit guidance to use equals() (opt-in)
if std::env::var("NYASH_BOX_EQ_GUIDE_ERROR").ok().as_deref() == Some("1") {
for (i, line) in code.lines().enumerate() {
let l = line;
let mut idx = 0usize;
while let Some(pos) = l[idx..].find("==") {
let at = idx + pos;
// find left token end and right token start
let (left_ok, right_ok) = (peek_ident_left(l, at), peek_ident_right(l, at + 2));
if left_ok && right_ok {
return Err(format!("Type error: '==' on boxes — use equals() (line {})", i + 1));
}
idx = at + 2;
}
}
}
Ok(())
}
// ---- Helpers (very small, no external deps) ----
fn is_number_literal(s: &str) -> bool {
let t = s.trim();
!t.is_empty() && t.chars().all(|c| c.is_ascii_digit())
}
fn is_string_literal(s: &str) -> bool {
let t = s.trim();
t.starts_with('"')
}
fn find_plus_operands(line: &str) -> Option<(&str, &str)> {
let bytes = line.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
if bytes[i] as char == '+' {
// extract left token
let mut l = i;
while l > 0 && bytes[l - 1].is_ascii_whitespace() { l -= 1; }
let mut lstart = l;
while lstart > 0 {
let c = bytes[lstart - 1] as char;
if c.is_ascii_alphanumeric() || c == '_' || c == '"' { lstart -= 1; } else { break; }
}
let left = &line[lstart..l];
// extract right token
let mut r = i + 1;
while r < bytes.len() && bytes[r].is_ascii_whitespace() { r += 1; }
let mut rend = r;
while rend < bytes.len() {
let c = bytes[rend] as char;
if c.is_ascii_alphanumeric() || c == '_' || c == '"' { rend += 1; } else { break; }
}
if r <= rend { let right = &line[r..rend]; return Some((left, right)); }
return None;
}
i += 1;
}
None
}
fn peek_ident_left(s: &str, pos: usize) -> bool {
// scan left for first non-space token end, then back to token start
let bytes = s.as_bytes();
if pos == 0 { return false; }
let mut i = pos;
// skip spaces
while i > 0 && bytes[i - 1].is_ascii_whitespace() { i -= 1; }
if i == 0 { return false; }
// now consume identifier chars backwards
let mut j = i;
while j > 0 {
let c = bytes[j - 1] as char;
if c.is_ascii_alphanumeric() || c == '_' { j -= 1; } else { break; }
}
if j == i { return false; }
// ensure not starting with digit only (avoid numeric literal)
let tok = &s[j..i];
!tok.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false)
}
fn peek_ident_right(s: &str, pos: usize) -> bool {
let bytes = s.as_bytes();
let mut i = pos;
while i < bytes.len() && bytes[i].is_ascii_whitespace() { i += 1; }
if i >= bytes.len() { return false; }
let mut j = i;
while j < bytes.len() {
let c = bytes[j] as char;
if c.is_ascii_alphanumeric() || c == '_' { j += 1; } else { break; }
}
if j == i { return false; }
let tok = &s[i..j];
!tok.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false)
}

View File

@ -310,7 +310,10 @@ pub fn emit_mir_json_for_harness(
dst, func, callee, args, effects, ..
} => {
// Phase 15.5: Unified Call support with environment variable control
let use_unified = std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1";
let use_unified = match std::env::var("NYASH_MIR_UNIFIED_CALL").ok().as_deref().map(|s| s.to_ascii_lowercase()) {
Some(s) if s == "0" || s == "false" || s == "off" => false,
_ => true,
};
if use_unified && callee.is_some() {
// v1: Unified mir_call format
@ -435,7 +438,10 @@ pub fn emit_mir_json_for_harness(
// Phase 15.5: JSON v1 schema with environment variable control
let use_v1_schema = std::env::var("NYASH_JSON_SCHEMA_V1").unwrap_or_default() == "1"
|| std::env::var("NYASH_MIR_UNIFIED_CALL").unwrap_or_default() == "1";
|| match std::env::var("NYASH_MIR_UNIFIED_CALL").ok().as_deref().map(|s| s.to_ascii_lowercase()) {
Some(s) if s == "0" || s == "false" || s == "off" => false,
_ => true,
};
let root = if use_v1_schema {
create_json_v1_root(json!(funs))

View File

@ -1,7 +1,7 @@
#![cfg(feature = "interpreter-legacy")]
use super::super::NyashRunner;
use nyash_rust::{
backend::VM, interpreter::NyashInterpreter, mir::MirCompiler, parser::NyashParser,
};
use nyash_rust::{backend::VM, interpreter::NyashInterpreter, mir::MirCompiler, parser::NyashParser};
impl NyashRunner {
/// Execute benchmark mode (split)
@ -241,4 +241,3 @@ impl NyashRunner {
Ok(())
}
}
#![cfg(feature = "vm-legacy")]

View File

@ -1,6 +1,9 @@
use super::super::NyashRunner;
use crate::runner::json_v0_bridge;
#[cfg(feature = "interpreter-legacy")]
use nyash_rust::{interpreter::NyashInterpreter, parser::NyashParser};
#[cfg(not(feature = "interpreter-legacy"))]
use nyash_rust::parser::NyashParser;
// Use the library crate's plugin init module rather than the bin crate root
use crate::cli_v;
use crate::runner::pipeline::{resolve_using_target, suggest_in_base};
@ -13,6 +16,7 @@ use std::{fs, process};
// (moved) suggest_in_base is now in runner/pipeline.rs
#[cfg(feature = "interpreter-legacy")]
impl NyashRunner {
// legacy run_file_legacy removed (was commented out)
@ -289,3 +293,12 @@ impl NyashRunner {
}
}
}
#[cfg(not(feature = "interpreter-legacy"))]
impl NyashRunner {
/// Interpreter backend is disabled in default builds. Use `--backend vm` or `--backend llvm`.
pub(crate) fn execute_nyash_file(&self, _filename: &str) {
eprintln!("❌ Interpreter backend (AST) is disabled. Build with --features interpreter-legacy to enable, or use --backend vm/llvm.");
std::process::exit(1);
}
}

View File

@ -103,7 +103,8 @@ pub fn ny_llvmc_emit_exe_lib(
.arg("exe")
.arg("--out")
.arg(exe_out);
if let Some(dir) = nyrt_dir { cmd.arg("--nyrt").arg(dir); } else { cmd.arg("--nyrt").arg("target/release"); }
let default_nyrt = std::env::var("NYASH_EMIT_EXE_NYRT") .ok() .or_else(|| std::env::var("NYASH_ROOT").ok().map(|r| format!("{}/target/release", r))) .unwrap_or_else(|| "target/release".to_string());
if let Some(dir) = nyrt_dir { cmd.arg("--nyrt").arg(dir); } else { cmd.arg("--nyrt").arg(default_nyrt); }
if let Some(flags) = extra_libs { if !flags.trim().is_empty() { cmd.arg("--libs").arg(flags); } }
let status = cmd.status().map_err(|e| {
let prog_path = std::path::Path::new(cmd.get_program());
@ -146,7 +147,8 @@ pub fn ny_llvmc_emit_exe_bin(
.arg("exe")
.arg("--out")
.arg(exe_out);
if let Some(dir) = nyrt_dir { cmd.arg("--nyrt").arg(dir); } else { cmd.arg("--nyrt").arg("target/release"); }
let default_nyrt = std::env::var("NYASH_EMIT_EXE_NYRT") .ok() .or_else(|| std::env::var("NYASH_ROOT").ok().map(|r| format!("{}/target/release", r))) .unwrap_or_else(|| "target/release".to_string());
if let Some(dir) = nyrt_dir { cmd.arg("--nyrt").arg(dir); } else { cmd.arg("--nyrt").arg(default_nyrt); }
if let Some(flags) = extra_libs { if !flags.trim().is_empty() { cmd.arg("--libs").arg(flags); } }
let status = cmd.status().map_err(|e| {
let prog_path = std::path::Path::new(cmd.get_program());

View File

@ -1,3 +1,5 @@
#![cfg(feature = "interpreter-legacy")]
use crate::interpreter::NyashInterpreter;
use crate::ast::ASTNode;
use crate::box_trait::{NyashBox, IntegerBox};
@ -62,4 +64,3 @@ fn functionbox_call_via_field() {
let ib = out.as_any().downcast_ref::<IntegerBox>().expect("integer ret");
assert_eq!(ib.value, 7);
}

View File

@ -1,3 +1,5 @@
#![cfg(feature = "interpreter-legacy")]
use crate::interpreter::NyashInterpreter;
use crate::ast::{ASTNode, LiteralValue};
use crate::box_trait::{NyashBox, IntegerBox};

View File

@ -1,3 +1,5 @@
#![cfg(feature = "interpreter-legacy")]
use crate::parser::NyashParser;
use crate::interpreter::NyashInterpreter;

View 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

View File

@ -11,18 +11,23 @@
set -euo pipefail
activate_pyvm() {
export NYASH_DISABLE_PLUGINS=1
unset NYASH_DISABLE_PLUGINS || true
export NYASH_VM_USE_PY=1
export NYASH_PIPE_USE_PYVM=1
export NYASH_NY_COMPILER_TIMEOUT_MS=${NYASH_NY_COMPILER_TIMEOUT_MS:-2000}
echo "[dev-env] PyVM profile activated" >&2
# Unified Call: default ON (explicit), and suppress legacy rewrite to avoid duplication
export NYASH_MIR_UNIFIED_CALL=${NYASH_MIR_UNIFIED_CALL:-1}
export NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1
echo "[dev-env] PyVM profile activated (plugins ON)" >&2
}
activate_bridge() {
export NYASH_DISABLE_PLUGINS=1
unset NYASH_DISABLE_PLUGINS || true
unset NYASH_VM_USE_PY || true
export NYASH_NY_COMPILER_TIMEOUT_MS=${NYASH_NY_COMPILER_TIMEOUT_MS:-2000}
echo "[dev-env] Bridge profile activated (interpreter for pipe)" >&2
export NYASH_MIR_UNIFIED_CALL=${NYASH_MIR_UNIFIED_CALL:-1}
export NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1
echo "[dev-env] Bridge profile activated (interpreter for pipe; plugins ON)" >&2
}
reset_env() {
@ -30,6 +35,8 @@ reset_env() {
unset NYASH_PIPE_USE_PYVM || true
unset NYASH_DISABLE_PLUGINS || true
unset NYASH_NY_COMPILER_TIMEOUT_MS || true
unset NYASH_MIR_UNIFIED_CALL || true
unset NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE || true
unset NYASH_MIR_NO_PHI || true
unset NYASH_VERIFY_ALLOW_NO_PHI || true
unset NYASH_LLVM_USE_HARNESS || true
@ -56,6 +63,9 @@ activate_opbox() {
export NYASH_BUILDER_OPERATOR_BOX_COMPARE_CALL=1
export NYASH_BUILDER_OPERATOR_BOX_ADD_CALL=1
export NYASH_BUILDER_OPERATOR_BOX_ALL_CALL=1
# Unified call and legacy suppression
export NYASH_MIR_UNIFIED_CALL=${NYASH_MIR_UNIFIED_CALL:-1}
export NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1
echo "[dev-env] Operator Boxes (stringify/compare/add) enabled (adopt+builder-call)" >&2
}

View File

@ -17,7 +17,18 @@ import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
_default_root = Path(__file__).resolve().parents[1]
_env_root = None
try:
env_root_str = os.environ.get('NYASH_ROOT')
if env_root_str:
cand = Path(env_root_str).resolve()
if (cand / "src" / "llvm_py" / "llvm_builder.py").exists():
_env_root = cand
except Exception:
_env_root = None
ROOT = _env_root or _default_root
PY_BUILDER = ROOT / "src" / "llvm_py" / "llvm_builder.py"
def run_dummy(out_path: str) -> None:

View File

@ -30,7 +30,8 @@ export NYASH_SELFHOST_EXEC=0 # JSON v0ブリッジ無効
export SMOKES_DEFAULT_TIMEOUT=30
export SMOKES_PARALLEL_TESTS=1
export SMOKES_FAST_FAIL=1 # 最初の失敗で停止
export SMOKES_CLEAN_ENV=1 # 各実行をENVクリーンで隔離揺れ抑止
export SMOKES_CLEAN_ENV=1
export SMOKES_FORCE_LLVM=1 # LLVM 常備環境では AST heavy を quick でも実行 # 各実行をENVクリーンで隔離揺れ抑止
# ログ設定
export SMOKES_LOG_LEVEL="info"

View File

@ -139,13 +139,13 @@ check_parity() {
# LLVMPythonハーネス実行
if [ "$program" = "-c" ]; then
if llvm_output=$(timeout "$timeout" bash -c "NYASH_LLVM_USE_HARNESS=1 NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend llvm -c \"$code\" 2>&1"); then
if llvm_output=$(timeout "$timeout" bash -c "PYTHONPATH=\"${PYTHONPATH:-$NYASH_ROOT}\" NYASH_NY_LLVM_COMPILER=\"${NYASH_NY_LLVM_COMPILER:-$NYASH_ROOT/target/release/ny-llvmc}\" NYASH_EMIT_EXE_NYRT=\"${NYASH_EMIT_EXE_NYRT:-$NYASH_ROOT/target/release}\" NYASH_LLVM_USE_HARNESS=1 NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend llvm -c \"$code\" 2>&1"); then
llvm_exit=0
else
llvm_exit=$?
fi
else
if llvm_output=$(timeout "$timeout" bash -c "NYASH_LLVM_USE_HARNESS=1 NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend llvm \"$program\" 2>&1"); then
if llvm_output=$(timeout "$timeout" bash -c "PYTHONPATH=\"${PYTHONPATH:-$NYASH_ROOT}\" NYASH_NY_LLVM_COMPILER=\"${NYASH_NY_LLVM_COMPILER:-$NYASH_ROOT/target/release/ny-llvmc}\" NYASH_EMIT_EXE_NYRT=\"${NYASH_EMIT_EXE_NYRT:-$NYASH_ROOT/target/release}\" NYASH_LLVM_USE_HARNESS=1 NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend llvm \"$program\" 2>&1"); then
llvm_exit=0
else
llvm_exit=$?

View File

@ -165,6 +165,10 @@ run_nyash_vm() {
shift
local tmpfile="/tmp/nyash_test_$$.nyash"
echo "$code" > "$tmpfile"
# 軽量ASIFixテスト用: ブロック終端の余剰セミコロンを寛容に除去
if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ]; then
sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$tmpfile" || true
fi
# プラグイン初期化メッセージを除外
NYASH_VM_USE_PY="$USE_PYVM" NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 "${ENV_PREFIX[@]}" \
"$NYASH_BIN" --backend vm "$tmpfile" "${EXTRA_ARGS[@]}" "$@" 2>&1 | filter_noise
@ -187,11 +191,18 @@ run_nyash_vm() {
run_nyash_llvm() {
local program="$1"
shift
# Skip gracefully when LLVM backend is not available in this build
if ! "$NYASH_BIN" --version 2>/dev/null | grep -q "features.*llvm"; then
log_warn "LLVM backend not available in this build; skipping LLVM run"
log_info "Hint: enable with 'LLVM_SYS_180_PREFIX=$(llvm-config-18 --prefix) cargo build --release --features llvm'"
return 0
# Allow developer to force LLVM run (env guarantees availability)
if [ "${SMOKES_FORCE_LLVM:-0}" != "1" ]; then
# Skip gracefully when LLVM backend is not available in this build
# Primary check: version string advertises features
if ! "$NYASH_BIN" --version 2>/dev/null | grep -q "features.*llvm"; then
# Fallback check: binary contains LLVM harness symbols (ny-llvmc / NYASH_LLVM_USE_HARNESS)
if ! strings "$NYASH_BIN" 2>/dev/null | grep -E -q 'ny-llvmc|NYASH_LLVM_USE_HARNESS'; then
log_warn "LLVM backend not available in this build; skipping LLVM run"
log_info "Hint: build ny-llvmc + enable harness: cargo build --release -p nyash-llvm-compiler && cargo build --release --features llvm"
return 0
fi
fi
fi
# -c オプションの場合は一時ファイル経由で実行
if [ "$program" = "-c" ]; then
@ -199,18 +210,32 @@ run_nyash_llvm() {
shift
local tmpfile="/tmp/nyash_test_$$.nyash"
echo "$code" > "$tmpfile"
# 軽量ASIFixテスト用: ブロック終端の余剰セミコロンを寛容に除去
if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ]; then
sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$tmpfile" || true
fi
# 軽量ASIFixテスト用: ブロック終端の余剰セミコロンを寛容に除去
if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ] && [ -f "$program" ]; then
sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$program" || true
fi
# プラグイン初期化メッセージを除外
NYASH_VM_USE_PY=0 NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 "$NYASH_BIN" --backend llvm "$tmpfile" "$@" 2>&1 | \
grep -v "^\[FileBox\]" | grep -v "^Net plugin:" | grep -v "^\[.*\] Plugin" | \
grep -v '^✅ LLVM (harness) execution completed' | grep -v '^📊 MIR Module compiled successfully' | grep -v '^📊 Functions:'
PYTHONPATH="${PYTHONPATH:-$NYASH_ROOT}" NYASH_NY_LLVM_COMPILER="$NYASH_ROOT/target/release/ny-llvmc" NYASH_LLVM_USE_HARNESS=1 NYASH_EMIT_EXE_NYRT="$NYASH_ROOT/target/release" NYASH_VM_USE_PY=0 NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 "$NYASH_BIN" --backend llvm "$tmpfile" "$@" 2>&1 | \
grep -v "^\[UnifiedBoxRegistry\]" | grep -v "^\[FileBox\]" | grep -v "^Net plugin:" | grep -v "^\[.*\] Plugin" | \
grep -v '^✅ LLVM (harness) execution completed' | grep -v '^📊 MIR Module compiled successfully' | grep -v '^📊 Functions:' | grep -v 'JSON Parse Errors:' | grep -v 'Parsing errors' | grep -v 'No parsing errors' | grep -v 'Error at line ' | \
grep -v '^\[ny-llvmc\]' | grep -v '^\[harness\]' | grep -v '^Compiled to ' | grep -v '^/usr/bin/ld:'
local exit_code=${PIPESTATUS[0]}
rm -f "$tmpfile"
return $exit_code
else
# 軽量ASIFixテスト用: ブロック終端の余剰セミコロンを寛容に除去
if [ "${SMOKES_ASI_STRIP_SEMI:-1}" = "1" ] && [ -f "$program" ]; then
sed -i -E 's/;([[:space:]]*)(\}|$)/\1\2/g' "$program" || true
fi
# プラグイン初期化メッセージを除外
NYASH_VM_USE_PY=0 NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 "$NYASH_BIN" --backend llvm "$program" "$@" 2>&1 | \
grep -v "^\[FileBox\]" | grep -v "^Net plugin:" | grep -v "^\[.*\] Plugin" | \
grep -v '^✅ LLVM (harness) execution completed' | grep -v '^📊 MIR Module compiled successfully' | grep -v '^📊 Functions:'
PYTHONPATH="${PYTHONPATH:-$NYASH_ROOT}" NYASH_NY_LLVM_COMPILER="$NYASH_ROOT/target/release/ny-llvmc" NYASH_LLVM_USE_HARNESS=1 NYASH_EMIT_EXE_NYRT="$NYASH_ROOT/target/release" NYASH_VM_USE_PY=0 NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 "$NYASH_BIN" --backend llvm "$program" "$@" 2>&1 | \
grep -v "^\[UnifiedBoxRegistry\]" | grep -v "^\[FileBox\]" | grep -v "^Net plugin:" | grep -v "^\[.*\] Plugin" | \
grep -v '^✅ LLVM (harness) execution completed' | grep -v '^📊 MIR Module compiled successfully' | grep -v '^📊 Functions:' | grep -v 'JSON Parse Errors:' | grep -v 'Parsing errors' | grep -v 'No parsing errors' | grep -v 'Error at line ' | \
grep -v '^\[ny-llvmc\]' | grep -v '^\[harness\]' | grep -v '^Compiled to ' | grep -v '^/usr/bin/ld:'
return ${PIPESTATUS[0]}
fi
}

View File

@ -10,6 +10,10 @@ TEST_DIR="/tmp/json_error_messages_ast_$$"
mkdir -p "$TEST_DIR"
cd "$TEST_DIR"
# Ensure LLVM harness script is discoverable from CWD
mkdir -p tools
cp -f "$NYASH_ROOT/tools/llvmlite_harness.py" tools/ 2>/dev/null || true
cat > nyash.toml << EOF
[using.json_native]
path = "$NYASH_ROOT/apps/lib/json_native/"

View File

@ -10,6 +10,10 @@ TEST_DIR="/tmp/json_nested_ast_$$"
mkdir -p "$TEST_DIR"
cd "$TEST_DIR"
# Ensure LLVM harness script is discoverable from CWD
mkdir -p tools
cp -f "$NYASH_ROOT/tools/llvmlite_harness.py" tools/ 2>/dev/null || true
cat > nyash.toml << EOF
[using.json_native]
path = "$NYASH_ROOT/apps/lib/json_native/"

View File

@ -10,6 +10,10 @@ TEST_DIR="/tmp/json_roundtrip_ast_$$"
mkdir -p "$TEST_DIR"
cd "$TEST_DIR"
# Ensure LLVM harness script is discoverable from CWD
mkdir -p tools
cp -f "$NYASH_ROOT/tools/llvmlite_harness.py" tools/ 2>/dev/null || true
cat > nyash.toml << EOF
[using.json_native]
path = "$NYASH_ROOT/apps/lib/json_native/"

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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: MiniVM 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

View File

@ -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: MiniVM 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

View File

@ -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: MiniVM 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

View File

@ -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: MiniVM 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

View File

@ -6,6 +6,9 @@ source "$(dirname "$0")/../../../lib/test_runner.sh"
require_env || exit 2
preflight_plugins || exit 2
# Quick policy: AST prelude merge is experimental; cover in integration/full
test_skip "using_multi_prelude_dep_ast (quick)" "AST prelude merge experimental; run in integration/full" && exit 0
setup_tmp_dir() {
TEST_DIR="/tmp/using_multi_prelude_$$"
mkdir -p "$TEST_DIR"

View File

@ -6,6 +6,9 @@ source "$(dirname "$0")/../../../lib/test_runner.sh"
require_env || exit 2
preflight_plugins || exit 2
# Quick policy: AST prelude merge is experimental; cover in integration/full
test_skip "using_profiles_ast (quick)" "AST prelude merge experimental; run in integration/full" && exit 0
setup_tmp_dir() {
TEST_DIR="/tmp/using_profiles_ast_$$"
mkdir -p "$TEST_DIR"

View File

@ -220,9 +220,19 @@ find_test_files() {
local profile_dir="$SCRIPT_DIR/profiles/$PROFILE"
local test_files=()
local have_llvm=0
if [ -x "./target/release/nyash" ] && ./target/release/nyash --version 2>/dev/null | grep -q "features.*llvm"; then
if [ "${SMOKES_FORCE_LLVM:-0}" = "1" ]; then
have_llvm=1
fi
if [ -x "./target/release/nyash" ]; then
if ./target/release/nyash --version 2>/dev/null | grep -q "features.*llvm"; then
have_llvm=1
else
# Fallback detection: check for LLVM harness symbols in the binary
if strings ./target/release/nyash 2>/dev/null | grep -E -q 'ny-llvmc|NYASH_LLVM_USE_HARNESS'; then
have_llvm=1
fi
fi
fi
if [ ! -d "$profile_dir" ]; then
log_error "Profile directory not found: $profile_dir"