Phase 21.7 normalization: optimization pre-work + bench harness expansion

- Add opt-in optimizations (defaults OFF)
  - Ret purity verifier: NYASH_VERIFY_RET_PURITY=1
  - strlen FAST enhancement for const handles
  - FAST_INT gate for same-BB SSA optimization
  - length cache for string literals in llvmlite
- Expand bench harness (tools/perf/microbench.sh)
  - Add branch/call/stringchain/arraymap/chip8/kilo cases
  - Auto-calculate ratio vs C reference
  - Document in benchmarks/README.md
- Compiler health improvements
  - Unify PHI insertion to insert_phi_at_head()
  - Add NYASH_LLVM_SKIP_BUILD=1 for build reuse
- Runtime & safety enhancements
  - Clarify Rust/Hako ownership boundaries
  - Strengthen receiver localization (LocalSSA/pin/after-PHIs)
  - Stop excessive PluginInvoke→BoxCall rewrites
- Update CURRENT_TASK.md, docs, and canaries

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-13 16:40:58 +09:00
parent 9e2fa1e36e
commit dda65b94b7
160 changed files with 6773 additions and 1692 deletions

View File

@ -1,5 +1,173 @@
# Current Task — Phase 21.7Normalization & Unification: Methodize Static Boxes # Current Task — Phase 21.7Normalization & Unification: Methodize Static Boxes
Update (2025-11-12 — Optimization pre-work and bench harness)
- Implemented (optin, defaults OFF)
- Ret block purity verifier: NYASH_VERIFY_RET_PURITY=1 → Return直前の副作用命令をFailFastConst/Copy/Phi/Nopのみ許可。構造純化の安全弁として維持。
- strlen FAST 強化: newbox(StringBox,const) だけでなく const ハンドルからの length() も即値畳み込みNYASH_LLVM_FAST=1時
- Kernel hint: nyrt_string_length に #[inline(always)] を付与AOT呼び出しのオーバーヘッド抑制
- FAST_INT ゲート: NYASH_LLVM_FAST_INT=1 で BinOp/Compare が同一ブロックSSAvmapを優先利用し、resolver/PHIの局所化コストを回避文字列連結は除外
- length キャッシュllvmlite 最低限): NYASH_LLVM_FAST=1 時、文字列リテラル由来ハンドルに対する length()/len の i64 即値を resolver.length_cache に格納し、ループ内で再利用。
- Added
- Bench harness 拡張tools/perf/microbench.sh: branch/call/stringchain/arraymap/chip8/kilo を追加。各ケースは C 参照付きで ratio を自動算出。
- Bench doc: benchmarks/README.md に一覧・実行例を追加。
- Preliminary resultsLLVM/EXE, ノイズ抑制: NYASH_SKIP_TOML_ENV=1 NYASH_DISABLE_PLUGINS=1
- 良好: call / stringchain / kilo → ratio < 100%Nyash側が軽い
- 要改善: branch / arraymap / chip8 ratio 200%Cの約1/2速度
Compiler healthE0308対応
- PHI 挿入のコールサイトを監査し`insert_phi_at_head(&mut MirFunction, ...)` へ統一`current_function.as_mut()` 経由で渡す)。`cargo check` は通過済み
- `tools/ny_mir_builder.sh --emit exe` 実行時にワークスペース再ビルドが走る環境では必要に応じて `NYASH_LLVM_SKIP_BUILD=1` を指定し既存の `ny-llvmc` / `nyash_kernel` ビルド成果物を用いる運用を併記
- Next actionsAOT集中; 既定OFFのまま段階適用
1) ループ不変の簡易ホイスティングstrlen/比較/分母mod など
2) FAST_INT の適用範囲精緻化不要cast/extendの抑制CFG保存のまま
3) array/map のホットパス検討AOT経路のみ診断OFF
4) 再ベンチbranch/arraymap/chip8 を再測)、目標: ratio 125%Cの8割
5) 既定OFFで挙動不変Ret純化ガード緑維持小差分リバーシブルを堅持
Toggles再掲
- NYASH_LLVM_FAST=1 strlen FAST + literal length キャッシュ
- NYASH_LLVM_FAST_INT=1 i64 ホットパス同一BB SSA 優先
- NYASH_VERIFY_RET_PURITY=1 Ret 純化ガード開発ガード
- NYASH_LLVM_SKIP_BUILD=1 ny_mir_builder.sh の再ビルド抑止既存バイナリ再利用
- NYASH_MIR_DEV_IDEMP=1 MIR normalize/dev 用の再実行安定化idempotenceメタ既定OFF診断専用
- HAKO_MIR_NORMALIZE_PRINT=1 AotPrep(.hako) 側で printexterncall(env.console.log) を正規化CFG不変既定OFF
- HAKO_MIR_NORMALIZE_REF=1 AotPrep(.hako) 側で ref_get/ref_setboxcall(getField/setField) を正規化CFG不変既定OFF
- HAKO_SILENT_TAGS=0|1 v2ランナーのタグ静音トグル1=静音既定、0=生ログ)
Wrapup (this session)
- Rust/Hako ownership clarifieddocs/development/normalization/ownership.md)。Hako=意味論正規化、Rust=構造/安全。
- Runtime autoload for using.dylibguarded Runner 初期化に集約箱名は nyash_box.toml から推論して登録
- Builder/VM safety: 受信ローカライズを強化LocalSSA/pin/afterPHIs/tail)。未定義受信は同一BBの直近 NewBox<Box> で構造回復dev 安全弁)に限定。
- PluginInvoke→BoxCall の過剰な書換えを停止(意味論は Hako 側の methodize に集約)。
- カナリア:
- phase217_methodize_canary.sh → PASSrc=5
- phase217_methodize_json_canary.sh → v1 + mir_call presentMethod が優先、Global は経過容認)。
- phase217_methodize_json_strict.sh を追加selfhostonly 仕上げ用ゲート、現状は StageB child パース修正が必要)。
## Phase 21.5 — Optimization Checklist進行管理
- [x] パス分割AotPrep: StrlenFold / LoopHoist / ConstDedup / CollectionsHot / BinopCSE
- [x] CollectionsHot: Array/Map の boxcall→externcall既定OFF・トグル
- [x] Map key モード導入(`NYASH_AOT_MAP_KEY_MODE={h|i64|hh|auto}`
- [x] LoopHoist v1: compare/mod/div 右辺 const の前出し(参照 const を先頭へ)
- [x] BinopCSE v1: `+/*` の同一式を再利用、`copy` で伝播
- [x] VERIFY ガード常時ON`NYASH_VERIFY_RET_PURITY=1`)で測定
- [x] ベンチ新規(`linidx`/`maplin`)追加・実測を README に反映
- [x] LoopHoist v2: `+/*` 右項 const の連鎖前出し、fixpoint≤3周の安全適用定数 binop 折畳みhoist 実装)
- [x] BinopCSE v2: 線形 `i*n` の共通化強化copy 連鎖の正規化・左右交換可の正規化)
- [x] CollectionsHot v2: Array/Map index/key の式キー共有(同一ブロック短窓で get/set の a0 を統一)
- [x] Map auto 精緻化: `_is_const_or_linear` の再帰判定強化copy 追跡div/rem の定数片を線形扱い)
- [x] Rust normalize.rs の借用修正E0499/E0502解消 dev idempotence メタ追加既定OFF
- [x] 正規化の .hako 化新箱でモジュール化NormalizePrint / NormalizeRef を AotPrep から opt-in で呼び出し
- [ ] Idempotence: 置換済みマークdevメタで再実行の結果不変を担保
- [ ] ベンチ更新: `arraymap`/`matmul`/`sieve`/`linidx`/`maplin` EXE 比率 ≤ 125%arraymap/matmul優先
### 21.5 引き継ぎメモ(ベンチ&観測)
- 新規トグル/修正
- `NYASH_LLVM_DUMP_MIR_IN=/tmp/xxx.json`AOT 入力MIRダンプ
- dump_mir.sh: provider/selfhost-first + min fallback で実MIRダンプを安定化
- microbench: `PERF_USE_PROVIDER=1` で jsonfrag 強制を解除実MIRでの突合せ用
- ベンチ現状EXE, C=100%
- arraymap: 150300%(分散あり) / matmul: 300% / sieve: 200% / linidx: 100% / maplin: 200%
- 次のアクション
1) arraymap/matmul の実MIRprovider or `NYASH_LLVM_DUMP_MIR_IN`)で index SSA 共有の崩れを特定
2) CollectionsHot v2.1: 同一ブロック短窓 get→set で a0 の一致を強制(式キー統一の取りこぼし追加)
3) BinopCSE 小窓 fixpoint 拡張(隣接ブロックまでの `i*n+k` 共有; 制御不変・既定OFF
4) 再ベンチarraymap/matmul/maplin`benchmarks/README.md` に 日付/トグル/ratio ≤ 125% を追記
5) provider emit の失敗パターンを継続調査emit ラッパの fallback/guard を追加調整)
Decisions
- 既定挙動は変更しないdev では methodize トグルON、CI/ユーザ既定は従来維持)。
- Rust 層は正規化を持たず、構造のみSSA/PHI/LocalSSA/安全弁。Hako が mir_call(Method) のSSOT。
Rust normalize.rs sunset plan記録
- 目的: normalize.rs の責務を .hako 側へ移し、Rust 側は既定OFF→撤去へ。
- 段階:
- 1) dev カナリア期間は `HAKO_MIR_NORMALIZE_*`PRINT/REF/ARRAYを opt-in で運用既定OFF
- 2) AotPrep 経由の normalize が十分に安定後、dev/quick で既定ONへ昇格CI は段階移行)。
- 3) Rust 側 normalize は env で明示 ON のみ許可(互換ガードを残す)。
- 4) 既定ON 状態で 2 週安定を確認後、Rust normalize のビルド配線を外し、最終撤去。
- 付随: 新規/更新トグルは env 一覧へ追記し、昇格時にデフォルト化→古いトグルは段階的に非推奨化とする。
Followups (carried to backlog)
- StageB child の "Unexpected token FN" 修正selfhostfirst/strict ゲート用)。
- Provider 出力の callee を Method へ寄せ、Global 経過容認を段階縮小。
- core_bridge の methodize ブリッジは bringup 用。Hako 既定化後に撤去。
# Next Active Task — Phase 21.16PreOptimization / 最適化前フェーズへ戻す)
Intent
- 正規化の骨格が揃った段階で、最適化前の安定化に戻す(意味論は不変、構造/観測の強化)。
Scope当面の作業
1) Optimizer 前段の安定化(構造のみ)
- normalize_legacy/ref_field_access は構造変換の範囲に留める(意味論を変えない)。
- NYASH_MIR_DISABLE_OPT/HAKO_MIR_DISABLE_OPT で最適化全休のゲートを尊重(既存)。
2) Baseline/観測
- quick/integration の rc/出力をゴールデン更新mir_call 既定はまだOFF
- crate_exec タイムボックスを EXE テストへ段階横展開。
- プラグイン parity の軽量観測autoload ガード有効時)。
3) テスト整備
- 受信未定義の負テスト追加Builder で防止。dev 安全弁を将来的にOFFにできる状態を作る。
- v1 + unified の canary は dev のみ strict を維持CI は現状どおり)。
Exit criteria21.16
- Optimizer OFF でも quick/integration/plugins が安定緑。
- 受信ローカライズの穴が負テストで検知可能dev 安全弁に依存しない)。
- v1/unified は dev プロファイルで常時確認可能(既存トグルで担保)。
Handoff Summary (ready for restart)
- Delegate v1 fixed (providerfirst): tools/hakorune_emit_mir.sh now prefers env.mirbuilder.emit to emit MIR(JSON v1). Legacy CLI converter kept as fallback with NYASH_JSON_SCHEMA_V1=1/NYASH_MIR_UNIFIED_CALL=1.
- Selfhostfirst stabilized for mini cases: HAKO_SELFHOST_TRY_MIN=1 in dev profile; min runner (BuilderRunnerMinBox.run) is used when builder fails.
- Methodize canaries:
- phase217_methodize_canary.sh → compilerun rc=5 PASSsemantics
- phase217_methodize_json_canary.sh → schema_version present + mir_call presentMethod preferred; Global tolerated for now
- Dev onegun toggles (enable_mirbuilder_dev_env.sh): HAKO_STAGEB_FUNC_SCAN=1, HAKO_MIR_BUILDER_FUNCS=1, HAKO_MIR_BUILDER_CALL_RESOLVE=1, NYASH_JSON_SCHEMA_V1=1, NYASH_MIR_UNIFIED_CALL=1, HAKO_SELFHOST_TRY_MIN=1.
Next Steps (postrestart)
1) Selfhostfirst child parse fixStageB: resolve the known “Unexpected token FN” in compiler_stageb chain; target [builder/selfhostfirst:ok] without min fallback.
2) Provider output callee finishing: prefer Method callee so JSON canary asserts Method strictly (now Global allowed temporarily).
3) Consolidate remaining legacy converter usage to provider where possible; keep defaults unchanged.
4) Docs: 21.7 checklist updated; core_bridge methodize remains diagnosticonly (OFF) with a removal plan once Hako methodize is default.
# New Active Task — Phase 21.6DualEmit Parity & Cline Readiness
Intent
- ProviderRustと SelfhostHakoで同一の MIR(JSON) を出力できることを確認し、生成時間を計測・比較する。AOTnyllvmc/crateも O0 既定で安定させる。
Whats done (this session)
- Perf ツール追加:
- MIR emit bench: `tools/perf/bench_hakorune_emit_mir.sh`
- AOT bench: `tools/perf/bench_ny_mir_builder.sh`
- 構造比較: `tools/perf/compare_mir_json.sh`
- Dual emit + 比較 + ベンチ: `tools/perf/dual_emit_compare.sh`
- Docs 整備:
- `docs/guides/perf/benchmarks.md`(手順/トグル)
- README/README.ja にパフォーマンス導線を追記
- `docs/ENV_VARS.md` にベンチ関連のトグルを追加
- 小最適化(意味論不変):
- Builder の不要 clone / finalize 二重呼び出し削減
- VM console 出力の to_string の枝刈りString/StringBox/Null 先出し)
- VM 受信未定義の負テストを 3 本追加Array/Map/String。failfast を監視
Known issue / Action
- Provider 経路tools/hakorune_emit_mir.sh の providerfirstが一部環境で `Program→MIR delegate failed` になることがある。
- 原因: StageB 実行の stdout にログが混在し JSON 抽出が空になるケース。
- 対策(次実装): ラッパを tmp 経由の CLI 変換(`--program-json-to-mir`へフォールバックさせる安全化。selfhostfirst も明示トグルで維持。
Plan (next after restart)
1) tools/hakorune_emit_mir.sh を安全化StageB→tmp→CLI 変換フォールバック。stderr/JSON 分離を強化
2) 代表 4 ケースjson_pp / json_query_min / json_lint / json_queryで dualemit を 5 回実行し、21.6 表に p50/Parity を記入
3) Parity 不一致はカテゴリ化callee 種別/順序/PHI/メタして修正元を特定provider か selfhost
4) AOTnyllvmcO0 の 3 回ベンチを取得、必要なら O1 スポットも取得
Tracking
- 記録台帳: `docs/development/roadmap/phases/phase-21.6/README.md` の表へ随時追記(チェックボックス方式)
Phase 21.6 wrap-up Phase 21.6 wrap-up
- Parser(StageB) ループJSONの保守フォールバックで壊れ形のみ復元正常形は素通り - Parser(StageB) ループJSONの保守フォールバックで壊れ形のみ復元正常形は素通り
- 代表E2Ereturn/binop/loop/callPASScall は関数化: "Main.add" → Global 関数呼び出し) - 代表E2Ereturn/binop/loop/callPASScall は関数化: "Main.add" → Global 関数呼び出し)
@ -135,6 +303,41 @@ Notesライン確認
- VM runtime counters`NYASH_VM_STATS=1` - VM runtime counters`NYASH_VM_STATS=1`
- microbench `--exe` 実装ビルド1回→EXEをRUNS回実行。現状、loop/strlen の一般形は crate 未対応で EXE 失敗 → 上記 1)/2) で解消予定。 - microbench `--exe` 実装ビルド1回→EXEをRUNS回実行。現状、loop/strlen の一般形は crate 未対応で EXE 失敗 → 上記 1)/2) で解消予定。
20251112 UpdatesPHI grouping fix, retafter guards, provider hooks
- LLVM Python backendllvmlite harness/crate 経由)
- Return 合成 PHI を常にブロック先頭に配置PHI グルーピング違反の根治)。
- lowering 入口boxcall/mir_call/safepointで terminator 後の誤挿入を防止継続BBへ移動
- builder 側でブロック命令列は最初の terminatorret/branch/jumpで打ち切る。
- JsonFrag 正規化・純化dev
- purify=1 で newbox/boxcall/externcall/mir_call を除去、ret 以降を打ち切り。
- provider-first フックdev
- HAKO_MIR_NORMALIZE_PROVIDER=1 で provider 出力 MIR にも正規化を適用可能。
- HAKO_MIR_BUILDER_LOOP_FORCE_JSONFRAG=1 を provider-first にも適用(最小ループ MIR を直返し)。
Verified
- selfhost-first + JsonFrag 正規化purify: llvmlite=OK / nyllvmc=OKEXE生成
- provider-first + FORCE_JSONFRAG: nyllvmc=OKEXE生成
Open
- provider-first の通常出力に対する純化強化ret ブロックの副作用禁止を徹底)。
- strlen(FAST) の適用強化resolver マーク補強 → IR に nyrt_string_length を反映)。
- Rust→.hako Normalize 移行計画SelfHost First / Freeze準拠
- 方針
- 正規化/置換系は Rust 側から .hakoAotPrep/MirBuilder 直下の新規箱)へ段階移行する。
- Rust 層は最小シーム+安全ガード(互換維持・借用修正・診断)に限定。既定挙動は不変。
- 現状
- 実装済みoptin、既定OFF: NormalizePrintBoxprint→externcall、NormalizeRefBoxref_get/ref_set→boxcall
- Rust 側 normalize.rs は借用修正のみ。dev 用 idempotence トグルNYASH_MIR_DEV_IDEMP=1を追加既定OFF
- これから(段階)
1) .hako 正規化のカナリア整備出力形状トークン検証VM/EXE RC パリティ)。
2) Rust 側の同等パスは「既定OFF」を明文化し、.hako 側が有効な場合は二重適用を避ける運用に統一(最終的に Rust 側の normalize はOFF運用
3) 代表ケースが緑になった段階で、.hako 側をデフォルト化dev/bench ラッパでON
4) 安定期間後、Rust 側の当該パスを撤去(環境変数は deprecate → 警告 → 削除の順)。
- 環境変数の扱い(増えたトグルの将来)
- いまは optin既定OFF。デフォルト化タイミングで .hako 側トグルはON に寄せ、Rust 側の旧トグルは非推奨化。
- 最終的に Rust 側トグルは削除(ドキュメントも更新)。.hako 側は必要最小のみ残す。
- Next (actionable) - Next (actionable)
- Implement loop JSONFrag purification (no MapBox/newbox) and add canary - Implement loop JSONFrag purification (no MapBox/newbox) and add canary
- Add strlen FAST lowering + `nyrt_string_length` (kernel) + EXE canary - Add strlen FAST lowering + `nyrt_string_length` (kernel) + EXE canary

View File

@ -32,6 +32,12 @@ ExternCallenv.*)と println 正規化: `docs/reference/runtime/externcall.m
- `NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1` - `NYASH_DEV_DISABLE_LEGACY_METHOD_REWRITE=1`
- JSON出力は unified ON で v1、OFF で v0従来 - JSON出力は unified ON で v1、OFF で v0従来
呼び出し実行VM側の既定
- VM は `Callee` 種別でルーティングGlobal/Method/Extern/…)。
- callee なしのレガシー byname 呼び出しは廃止。Builder は常に `Callee` を付与してね(付与がない場合は FailFast
- Extern の実体ディスパッチSSOT`src/backend/mir_interpreter/handlers/calls/externs.rs` に集約。`env.get` などの Global からもここに委譲する方針だよ。
- arity サフィックスの正規化: `env.get/1` のような表記も受理し、`env.get` に正規化してからディスパッチするよ。
開発計測(任意) 開発計測(任意)
- `resolve.choose` の Known 率をKPIとして出力 - `resolve.choose` の Known 率をKPIとして出力
- `NYASH_DEBUG_KPI_KNOWN=1`(有効化) - `NYASH_DEBUG_KPI_KNOWN=1`(有効化)
@ -52,6 +58,18 @@ ExternCallenv.*)と println 正規化: `docs/reference/runtime/externcall.m
- 例: `using: duplicate import of '<canon_path>' at file.hako:12 (previous alias 'X' first seen at line 5)` - 例: `using: duplicate import of '<canon_path>' at file.hako:12 (previous alias 'X' first seen at line 5)`
- 重複を削除/統合して解消してください。 - 重複を削除/統合して解消してください。
### パフォーマンスMIR & AOT
- MIR 生成ベンチStageB → MIR(JSON)
- `tools/perf/bench_hakorune_emit_mir.sh apps/examples/json_query/main.hako 5`
- 環境トグル: `HAKO_USING_RESOLVER_FIRST=1`, `HAKO_SELFHOST_BUILDER_FIRST=1`
- MIR(JSON) → obj/exe ベンチny-llvmc/crate
- `NYASH_LLVM_BACKEND=crate tools/perf/bench_ny_mir_builder.sh /tmp/program.mir.json 3`
- 任意: `HAKO_LLVM_OPT_LEVEL=1`(既定 O0
- MIR(JSON) 構造比較
- `tools/perf/compare_mir_json.sh out_before.json out_after.json`
詳細: `docs/guides/perf/benchmarks.md`
Phase15202509アップデート Phase15202509アップデート
- LLVM は nyllvmcクレート backendが主線。llvmlite は内部ハーネスとして nyllvmc から呼び出されます(利用者は nyllvmc/スクリプトを使えばOK - LLVM は nyllvmcクレート backendが主線。llvmlite は内部ハーネスとして nyllvmc から呼び出されます(利用者は nyllvmc/スクリプトを使えばOK
- パーサの改行処理は TokenCursor に統一中(`NYASH_PARSER_TOKEN_CURSOR=1`)。 - パーサの改行処理は TokenCursor に統一中(`NYASH_PARSER_TOKEN_CURSOR=1`)。

View File

@ -16,6 +16,17 @@ Notes
- The wrapper runs StageB with `NYASH_JSON_ONLY=1` to keep the output clean (no `RC:` lines). - The wrapper runs StageB with `NYASH_JSON_ONLY=1` to keep the output clean (no `RC:` lines).
- When the Hako MirBuilder fails (e.g., under development), it automatically falls back to `--program-json-to-mir` (no behavior change by default). - When the Hako MirBuilder fails (e.g., under development), it automatically falls back to `--program-json-to-mir` (no behavior change by default).
[Performance quickstart]
- MIR emit bench (StageB → MIR(JSON))
- `tools/perf/bench_hakorune_emit_mir.sh apps/examples/json_query/main.hako 5`
- Env toggles: `HAKO_USING_RESOLVER_FIRST=1`, `HAKO_SELFHOST_BUILDER_FIRST=1`
- MIR(JSON) → obj/exe benchny-llvmc/crate
- `NYASH_LLVM_BACKEND=crate tools/perf/bench_ny_mir_builder.sh /tmp/program.mir.json 3`
- Optional: `HAKO_LLVM_OPT_LEVEL=1`default O0
- MIR(JSON) structural compare
- `tools/perf/compare_mir_json.sh out_before.json out_after.json`
See also: docs/guides/perf/benchmarks.md
*[🇯🇵 日本語版はこちら / Japanese Version](README.ja.md)* *[🇯🇵 日本語版はこちら / Japanese Version](README.ja.md)*
[![Selfhost Minimal](https://github.com/moe-charm/nyash/actions/workflows/selfhost-minimal.yml/badge.svg?branch=selfhosting-dev)](https://github.com/moe-charm/nyash/actions/workflows/selfhost-minimal.yml) [![Selfhost Minimal](https://github.com/moe-charm/nyash/actions/workflows/selfhost-minimal.yml/badge.svg?branch=selfhosting-dev)](https://github.com/moe-charm/nyash/actions/workflows/selfhost-minimal.yml)
@ -30,6 +41,13 @@ Notes
Architecture notes Architecture notes
- Runtime rings (ring0/ring1/ring2) and provider policy: see `docs/architecture/RINGS.md`. - Runtime rings (ring0/ring1/ring2) and provider policy: see `docs/architecture/RINGS.md`.
Call system (unified by default)
- Builder emits `Call { callee: Callee }` whenever possible; the VM routes by callee kind (Global/Method/Extern/... ).
- Legacy byname callscallee なし)は廃止。必ず Builder が `Callee` を付与する(付与されない場合は FailFast
- Extern dispatch SSOT lives in `src/backend/mir_interpreter/handlers/calls/externs.rs`. Global externlike names should delegate there (e.g., `env.get`).
- Extern arity normalization: names with arity suffix (e.g., `env.get/1`) are accepted and normalized to their base form (`env.get`).
See environment knobs and policies in `docs/ENV_VARS.md`.
Execution Status (Feature Additions Pause) Execution Status (Feature Additions Pause)
- Active - Active
- `--backend llvm` (ny-llvmc crate backend; llvmlite harness is internal) — AOT object/EXE line - `--backend llvm` (ny-llvmc crate backend; llvmlite harness is internal) — AOT object/EXE line

View File

@ -67,7 +67,7 @@ static box Main {
} }
// Evaluate a simple JSON path by slicing JSON text directly (no full parse) // Evaluate a simple JSON path by slicing JSON text directly (no full parse)
// Returns a JSON substring for the value or null if not found // Returns a JSON substring for the value or the string "null" if not found
eval_path_text(json_text, path) { eval_path_text(json_text, path) {
local DEBUG = 0 // set to 1 for ad-hoc debug local DEBUG = 0 // set to 1 for ad-hoc debug
local cur_text = json_text local cur_text = json_text
@ -87,11 +87,11 @@ static box Main {
} }
local key = path.substring(start, i) local key = path.substring(start, i)
if DEBUG == 1 { print("[dbg] key=" + key) } if DEBUG == 1 { print("[dbg] key=" + key) }
if key.length() == 0 { return null } if key.length() == 0 { return "null" }
// Get value text directly; then reset window to that text // Get value text directly; then reset window to that text
local next_text = this.object_get_text(cur_text, 0, cur_text.length(), key) local next_text = this.object_get_text(cur_text, 0, cur_text.length(), key)
if DEBUG == 1 { if next_text == null { print("[dbg] obj miss") } else { print("[dbg] obj hit len=" + next_text.length()) } } if DEBUG == 1 { if next_text == null { print("[dbg] obj miss") } else { print("[dbg] obj hit len=" + next_text.length()) } }
if next_text == null { return null } if next_text == null { return "null" }
cur_text = next_text cur_text = next_text
} else { } else {
if ch == "[" { if ch == "[" {
@ -102,16 +102,16 @@ static box Main {
loop(i < path.length() && this.is_digit(path.substring(i, i + 1))) { i = i + 1 } loop(i < path.length() && this.is_digit(path.substring(i, i + 1))) { i = i + 1 }
local idx_str = path.substring(start, i) local idx_str = path.substring(start, i)
if DEBUG == 1 { print("[dbg] idx_str=" + idx_str + ", next=" + path.substring(i, i + 1)) } if DEBUG == 1 { print("[dbg] idx_str=" + idx_str + ", next=" + path.substring(i, i + 1)) }
if i >= path.length() || path.substring(i, i + 1) != "]" { return null } if i >= path.length() || path.substring(i, i + 1) != "]" { return "null" }
i = i + 1 // skip ']' i = i + 1 // skip ']'
local idx = this.parse_int(idx_str) local idx = this.parse_int(idx_str)
if DEBUG == 1 { print("[dbg] idx=" + idx) } if DEBUG == 1 { print("[dbg] idx=" + idx) }
local next_text = this.array_get_text(cur_text, 0, cur_text.length(), idx) local next_text = this.array_get_text(cur_text, 0, cur_text.length(), idx)
if DEBUG == 1 { if next_text == null { print("[dbg] arr miss idx=" + idx_str) } else { print("[dbg] arr hit len=" + next_text.length()) } } if DEBUG == 1 { if next_text == null { print("[dbg] arr miss idx=" + idx_str) } else { print("[dbg] arr hit len=" + next_text.length()) } }
if next_text == null { return null } if next_text == null { return "null" }
cur_text = next_text cur_text = next_text
} else { } else {
return null return "null"
} }
} }
} }

80
benchmarks/README.md Normal file
View File

@ -0,0 +1,80 @@
# Hakorune Benchmarks
This repository now bundles a light micro/synthetic benchmark suite to keep an eye on low-level optimizations. All cases are emitted on the fly by `tools/perf/microbench.sh` so they do not interfere with normal apps, but the generated Nyash code mirrors patterns we see in real programs.
## Included cases
| Case | Notes |
|-------------|-----------------------------------------------------------------------|
| `loop` | Plain integer accumulation |
| `strlen` | String length in tight loop (`nyrt_string_length` path) |
| `box` | StringBox allocation/free |
| `branch` | Dense conditional tree (modulo & arithmetic) |
| `call` | Helper function dispatch (mix/twist) |
| `stringchain`| Substring concatenation + length accumulation |
| `arraymap` | ArrayBox + MapBox churn |
| `chip8` | Simplified CHIP-8 style fetch/decode loop (derived from apps/chip8) |
| `kilo` | Text-buffer edits/search (inspired by enhanced_kilo_editor) |
| `sieve` | Integer-heavy Sieve of Eratosthenes (prime count) |
| `matmul` | NxN integer matrix multiply (3 nested loops) |
| `linidx` | Linear index array ops: idx=i*cols+j (CSE/hoist検証) |
| `maplin` | Integer-key Map get/set with linear keys (auto key heuristics) |
Each case has a matching C reference, so the script reports both absolute time and the Hakorune/C ratio. Scenario-style cases (`chip8`, `kilo`) still keep all logic deterministic and print-free to make timings stable.
## Notes
- `tools/perf/dump_mir.sh` can optionally write the MIR(JSON) for a given `.hako` and print a block/op histogram. It tries the normal provider path first and falls back to the minimal `jsonfrag` version (while-form) when needed, so you can inspect both the structural skeleton and the full lowering.
- Current baseline observations (LLVM/EXE, `NYASH_SKIP_TOML_ENV=1 NYASH_DISABLE_PLUGINS=1`): `call`, `stringchain`, and `kilo` already beat the C reference (ratio < 100%), while `branch`, `arraymap`, and `chip8` remain near 200%—they are targets for the upcoming hoisting/array-map hot-path work.
## Latest fast-path measurements
The following numbers were recorded on 2025-11-12 with the opt-in work enabled:
```bash
export NYASH_SKIP_TOML_ENV=1 NYASH_DISABLE_PLUGINS=1 \
NYASH_LLVM_SKIP_BUILD=1 NYASH_LLVM_FAST=1 NYASH_LLVM_FAST_INT=1 \
NYASH_MIR_LOOP_HOIST=1 NYASH_AOT_COLLECTIONS_HOT=1
tools/perf/microbench.sh --case <case> --backend llvm --exe --runs 3
```
Goal: bring the Hakorune EXE ratio to 125% of the reference C time (roughly C's 80%).
Current effort: keep baking new hoist/CSE patterns so `arraymap`, `matmul`, and `maplin` fall below that bar while `sieve` and the other cases stay stable.
| Case | EXE ratio (C=100%) | Notes |
|-----------|--------------------|-------|
| `branch` | 75.00% | 目標達成(≤125%)。 |
| `arraymap` | 150.00% | Array/Map hot-path + binop CSE をさらに磨いて 125% を目指す |
| `chip8` | 25.00% | 十分速いFAST_INT/hoist が効いている |
| `kilo` | 0.21% (N=200,000) | EXE モード既定で N 200k に自動調整C 参照の方が重い構成のため比率は極小 |
| `sieve` | 200.00% | `NYASH_VERIFY_RET_PURITY=1` ON での測定auto キー判定がまだ保守的 |
| `matmul` | 300.00% | まだ 3 重ループの Array/Map get/set が支配自動 CSE auto map key を詰める予定 |
| `linidx` | 100.00% | Linear index case is at parity; hoist + CSE already helps share SSA. |
| `maplin` | 200.00% | auto Map キー判定linear/const拡張 _h を選択しやすくなった さらに詰める余地あり |
### Notes
- You can rerun any case with the command above; `NYASH_LLVM_SKIP_BUILD=1` keeps repeated ny-llvmc builds cheap once the binaries are ready.
- `kilo` C 参照側が重く既定 N=5,000,000 だと長時間化するためEXE モードかつ N 未指定では既定 N 200,000 に自動調整するようにしました`tools/perf/microbench.sh`)。必要なら `--n <value>` で上書きしてください
- `lang/src/llvm_ir/boxes/aot_prep/README.md` StrlenFold / LoopHoist / ConstDedup / CollectionsHot のパス一覧をまとめNYASH_AOT_MAP_KEY_MODE={h|i64|hh|auto} の切り替えも説明しています今後は hoist/collections の強化で arraymap/matmul/maplin/sieve 125% 以内に引き下げる施策を続けます
- 代表測定では `NYASH_VERIFY_RET_PURITY=1` を有効化しReturn 直前の副作用を Fail-Fast で検出しながら回していますごく軽微なハンドルboxcallの変化が 23× に跳ねることがある点をご留意ください)。
## Running
Build `hakorune` in release mode first:
```bash
cargo build --release --bin hakorune
```
Then pick a case:
```bash
# LLVM EXE vs C (default n/runs are tuned per case)
tools/perf/microbench.sh --case chip8 --exe --n 200000 --runs 3
# VM vs C for a string-heavy micro
tools/perf/microbench.sh --case stringchain --backend vm --n 100000 --runs 3
```
The script takes care of generating temporary Nyash/C sources, building the C baseline, piping through the MIR builder (with FAST toggles enabled), and printing averages + ratios. Set `NYASH_SKIP_TOML_ENV=1` / `NYASH_DISABLE_PLUGINS=1` before calling the script if you want a fully clean execution environment.

864
check.err Normal file
View File

@ -0,0 +1,864 @@
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /home/tomoaki/git/hakorune-selfhost/plugins/nyash-console-plugin/Cargo.toml
workspace: /home/tomoaki/git/hakorune-selfhost/Cargo.toml
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /home/tomoaki/git/hakorune-selfhost/plugins/nyash-counter-plugin/Cargo.toml
workspace: /home/tomoaki/git/hakorune-selfhost/Cargo.toml
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /home/tomoaki/git/hakorune-selfhost/plugins/nyash-filebox-plugin/Cargo.toml
workspace: /home/tomoaki/git/hakorune-selfhost/Cargo.toml
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /home/tomoaki/git/hakorune-selfhost/plugins/nyash-json-plugin/Cargo.toml
workspace: /home/tomoaki/git/hakorune-selfhost/Cargo.toml
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /home/tomoaki/git/hakorune-selfhost/plugins/nyash-math-plugin/Cargo.toml
workspace: /home/tomoaki/git/hakorune-selfhost/Cargo.toml
warning: /home/tomoaki/git/hakorune-selfhost/Cargo.toml: file `/home/tomoaki/git/hakorune-selfhost/src/main.rs` found to be present in multiple build targets:
* `bin` target `hakorune`
* `bin` target `nyash`
warning: unused doc comment
--> src/box_operators/static_ops.rs:14:1
|
14 | / /// Static numeric operations for IntegerBox
15 | | ///
16 | | /// Generates implementations for: Add, Sub, Mul, Div with zero-division error handling
| |_--------------------------------------------------------------------------------------^
| |
| rustdoc does not generate documentation for macro invocations
|
= help: to document an item produced by a macro, the macro must produce the documentation as part of its expansion
= note: `#[warn(unused_doc_comments)]` on by default
warning: unused doc comment
--> src/box_operators/static_ops.rs:19:1
|
19 | / /// Static numeric operations for FloatBox
20 | | ///
21 | | /// Generates implementations for: Add, Sub, Mul, Div with zero-division error handling
| |_--------------------------------------------------------------------------------------^
| |
| rustdoc does not generate documentation for macro invocations
|
= help: to document an item produced by a macro, the macro must produce the documentation as part of its expansion
warning: unused import: `std::any::Any`
--> src/boxes/arithmetic/modulo_box.rs:6:5
|
6 | use std::any::Any;
| ^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: unused import: `super::library`
--> src/runtime/plugin_loader_v2/enabled/loader/config.rs:1:5
|
1 | use super::library;
| ^^^^^^^^^^^^^^
warning: unused import: `super::specs`
--> src/runtime/plugin_loader_v2/enabled/loader/singletons.rs:1:5
|
1 | use super::specs;
| ^^^^^^^^^^^^
warning: unused import: `PluginBoxV2`
--> src/runtime/plugin_loader_v2/enabled/loader/mod.rs:9:55
|
9 | use super::types::{LoadedPluginV2, PluginBoxMetadata, PluginBoxV2, PluginHandleInner};
| ^^^^^^^^^^^
warning: unused import: `BidError`
--> src/runtime/plugin_loader_v2/enabled/loader/mod.rs:10:18
|
10 | use crate::bid::{BidError, BidResult};
| ^^^^^^^^
warning: unused import: `std::collections::HashMap`
--> src/runtime/plugin_loader_v2/enabled/method_resolver.rs:8:5
|
8 | use std::collections::HashMap;
| ^^^^^^^^^^^^^^^^^^^^^^^^^
warning: unused import: `FactoryPolicy`
--> src/runtime/unified_registry.rs:11:46
|
11 | use crate::box_factory::{UnifiedBoxRegistry, FactoryPolicy};
| ^^^^^^^^^^^^^
warning: unexpected `cfg` condition value: `cranelift-jit`
--> src/runner/dispatch.rs:159:15
|
159 | #[cfg(feature = "cranelift-jit")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: expected values for `feature` are: `all-examples`, `aot-plan-import`, `builtin-core`, `builtin-filebox`, `c-core`, `cli`, `default`, `dynamic-file`, `e2e`, `gui`, `gui-examples`, `interpreter-legacy`, `jit-direct-only`, `llvm`, `llvm-harness`, `llvm-inkwell-legacy`, `mir_refbarrier_unify_poc`, `mir_typeop_poc`, `phi-legacy`, `plugins`, `plugins-only`, `tlv-shim`, `vm-legacy`, and `wasm-backend`
= help: consider adding `cranelift-jit` as a feature in `Cargo.toml`
= note: see <https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html> for more information about checking conditional configuration
= note: `#[warn(unexpected_cfgs)]` on by default
warning: unexpected `cfg` condition value: `cranelift-jit`
--> src/runner/dispatch.rs:164:19
|
164 | #[cfg(not(feature = "cranelift-jit"))]
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: expected values for `feature` are: `all-examples`, `aot-plan-import`, `builtin-core`, `builtin-filebox`, `c-core`, `cli`, `default`, `dynamic-file`, `e2e`, `gui`, `gui-examples`, `interpreter-legacy`, `jit-direct-only`, `llvm`, `llvm-harness`, `llvm-inkwell-legacy`, `mir_refbarrier_unify_poc`, `mir_typeop_poc`, `phi-legacy`, `plugins`, `plugins-only`, `tlv-shim`, `vm-legacy`, and `wasm-backend`
= help: consider adding `cranelift-jit` as a feature in `Cargo.toml`
= note: see <https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html> for more information about checking conditional configuration
warning: unexpected `cfg` condition value: `cranelift-jit`
--> src/runner/dispatch.rs:190:15
|
190 | #[cfg(feature = "cranelift-jit")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: expected values for `feature` are: `all-examples`, `aot-plan-import`, `builtin-core`, `builtin-filebox`, `c-core`, `cli`, `default`, `dynamic-file`, `e2e`, `gui`, `gui-examples`, `interpreter-legacy`, `jit-direct-only`, `llvm`, `llvm-harness`, `llvm-inkwell-legacy`, `mir_refbarrier_unify_poc`, `mir_typeop_poc`, `phi-legacy`, `plugins`, `plugins-only`, `tlv-shim`, `vm-legacy`, and `wasm-backend`
= help: consider adding `cranelift-jit` as a feature in `Cargo.toml`
= note: see <https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html> for more information about checking conditional configuration
warning: unused import: `BinaryOp`
--> src/runner/json_v0_bridge/lowering.rs:5:35
|
5 | MirPrinter, MirType, ValueId, BinaryOp,
| ^^^^^^^^
warning: unused import: `MirInstruction`
--> src/runner/json_v0_bridge/lowering/if_else.rs:2:45
|
2 | use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
| ^^^^^^^^^^^^^^
warning: unused import: `BinaryOp`
--> src/runner/json_v0_bridge/lowering/expr.rs:6:19
|
6 | BasicBlockId, BinaryOp, ConstValue, EffectMask, MirFunction, MirInstruction, ValueId,
| ^^^^^^^^
warning: unused import: `MirInstruction`
--> src/runner/json_v0_bridge/lowering/ternary.rs:8:45
|
8 | use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
| ^^^^^^^^^^^^^^
warning: unused import: `MirInstruction`
--> src/runner/json_v0_bridge/lowering/throw_ctx.rs:1:45
|
1 | use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
| ^^^^^^^^^^^^^^
warning: unused import: `NyashBox`
--> src/runner/modes/vm.rs:420:40
|
420 | use crate::box_trait::{NyashBox, IntegerBox, BoolBox};
| ^^^^^^^^
warning: unused import: `NyashBox`
--> src/runner/modes/vm_fallback.rs:324:40
|
324 | use crate::box_trait::{NyashBox, IntegerBox, BoolBox};
| ^^^^^^^^
warning: unused import: `crate::runner::child_env`
--> src/runner/pipe_io.rs:13:5
|
13 | use crate::runner::child_env;
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: unused import: `std::io::Write`
--> src/host_providers/mir_builder.rs:4:5
|
4 | use std::io::Write;
| ^^^^^^^^^^^^^^
warning: unused import: `crate::box_trait::NyashBox`
--> src/backend/mir_interpreter/handlers/boxes_object_fields.rs:2:5
|
2 | use crate::box_trait::NyashBox;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: unused import: `crate::box_trait::NyashBox`
--> src/backend/mir_interpreter/handlers/boxes_string.rs:2:5
|
2 | use crate::box_trait::NyashBox;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: variable does not need to be mutable
--> src/parser/statements/mod.rs:134:13
|
134 | let mut looks_like_method_head = |this: &Self| -> bool {
| ----^^^^^^^^^^^^^^^^^^^^^^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default
warning: unused variable: `program_ast`
--> src/mir/builder/decls.rs:43:25
|
43 | let program_ast = ASTNode::Program {
| ^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_program_ast`
|
= note: `#[warn(unused_variables)]` on by default
warning: unused variable: `type_id`
--> src/mir/builder/decls.rs:118:13
|
118 | let type_id = crate::mir::builder::emission::constant::emit_string(self, format!("__box_type_{}", name));
| ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_type_id`
warning: unreachable pattern
--> src/mir/builder/exprs.rs:330:13
|
10 | ASTNode::Program { statements, .. } => {
| ----------------------------------- matches all the relevant values
...
330 | ASTNode::Program { statements, .. } => self.cf_block(statements.clone()),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no value can reach this
|
= note: `#[warn(unreachable_patterns)]` on by default
warning: unreachable pattern
--> src/mir/builder/exprs.rs:333:13
|
14 | ASTNode::Print { expression, .. } => {
| --------------------------------- matches all the relevant values
...
333 | ASTNode::Print { expression, .. } => self.build_print_statement(*expression.clone()),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no value can reach this
warning: unreachable pattern
--> src/mir/builder/exprs.rs:335:13
|
17 | / ASTNode::If {
18 | | condition,
19 | | then_body,
20 | | else_body,
21 | | ..
22 | | } => {
| |_____________- matches all the relevant values
...
335 | / ASTNode::If {
336 | | condition,
337 | | then_body,
338 | | else_body,
339 | | ..
340 | | } => {
| |_____________^ no value can reach this
warning: unreachable pattern
--> src/mir/builder/exprs.rs:359:13
|
34 | ASTNode::Loop { condition, body, .. } => {
| ------------------------------------- matches all the relevant values
...
359 | / ASTNode::Loop {
360 | | condition, body, ..
361 | | } => self.cf_loop(*condition.clone(), body.clone()),
| |_____________^ no value can reach this
warning: unreachable pattern
--> src/mir/builder/exprs.rs:363:13
|
41 | / ASTNode::TryCatch {
42 | | try_body,
43 | | catch_clauses,
44 | | finally_body,
45 | | ..
46 | | } => self.cf_try_catch(try_body, catch_clauses, finally_body),
| |_____________- matches all the relevant values
...
363 | / ASTNode::TryCatch {
364 | | try_body,
365 | | catch_clauses,
366 | | finally_body,
367 | | ..
368 | | } => self.cf_try_catch(
| |_____________^ no value can reach this
warning: unreachable pattern
--> src/mir/builder/exprs.rs:374:13
|
47 | ASTNode::Throw { expression, .. } => self.cf_throw(*expression),
| --------------------------------- matches all the relevant values
...
374 | ASTNode::Throw { expression, .. } => self.cf_throw(*expression.clone()),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no value can reach this
warning: unused variable: `else_block`
--> src/mir/builder/phi.rs:15:9
|
15 | else_block: super::BasicBlockId,
| ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_else_block`
warning: unused variable: `then_block`
--> src/mir/builder/phi.rs:107:9
|
107 | then_block: BasicBlockId,
| ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_then_block`
warning: unused variable: `else_block`
--> src/mir/builder/phi.rs:108:9
|
108 | else_block: BasicBlockId,
| ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_else_block`
warning: unused variable: `func`
--> src/mir/builder/lifecycle.rs:241:60
|
241 | ... MirInstruction::Call { func, .. } => {
| ^^^^-
| |
| help: try removing the field
warning: variable does not need to be mutable
--> src/mir/builder/lifecycle.rs:271:17
|
271 | let mut sig = FunctionSignature {
| ----^^^
| |
| help: remove this `mut`
warning: unused variable: `name_const`
--> src/mir/builder/rewrite/known.rs:186:9
|
186 | let name_const = match crate::mir::builder::name_const::make_name_const_result(builder, &fname) {
| ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_name_const`
warning: unused variable: `name_const`
--> src/mir/builder/rewrite/special.rs:154:17
|
154 | let name_const = match crate::mir::builder::name_const::make_name_const_result(builder, &chosen) {
| ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_name_const`
warning: unused variable: `name_const`
--> src/mir/builder/rewrite/special.rs:185:13
|
185 | let name_const = match crate::mir::builder::name_const::make_name_const_result(builder, &fname) {
| ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_name_const`
warning: unused variable: `name_const`
--> src/mir/builder/rewrite/special.rs:212:17
|
212 | let name_const = match crate::mir::builder::name_const::make_name_const_result(builder, &fname) {
| ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_name_const`
warning: unused variable: `dbg_fn_name`
--> src/mir/builder.rs:424:13
|
424 | let dbg_fn_name = self
| ^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_dbg_fn_name`
warning: unused variable: `dbg_region_id`
--> src/mir/builder.rs:428:13
|
428 | let dbg_region_id = self.debug_current_region_id();
| ^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_dbg_region_id`
warning: unused variable: `else_block`
--> src/mir/phi_core/if_phi.rs:148:5
|
148 | else_block: crate::mir::BasicBlockId,
| ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_else_block`
warning: variable does not need to be mutable
--> src/mir/phi_core/loop_phi.rs:185:13
|
185 | let mut inc = IncompletePhi {
| ----^^^
| |
| help: remove this `mut`
warning: unused variable: `static_box_name`
--> src/backend/mir_interpreter/exec.rs:23:13
|
23 | let static_box_name = self.is_static_box_method(&func.signature.name);
| ^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_static_box_name`
warning: variable does not need to be mutable
--> src/backend/mir_interpreter/handlers/boxes_instance.rs:105:17
|
105 | let mut cands: Vec<String> = this
| ----^^^^^
| |
| help: remove this `mut`
warning: unreachable pattern
--> src/backend/mir_interpreter/handlers/externals.rs:138:13
|
30 | ("env", "get") => {
| -------------- matches all the relevant values
...
138 | ("env", "get") => {
| ^^^^^^^^^^^^^^ no value can reach this
warning: unused variable: `source`
--> src/benchmarks.rs:48:23
|
48 | if let Ok(source) = fs::read_to_string(file_path) {
| ^^^^^^ help: if this is intentional, prefix it with an underscore: `_source`
warning: variable does not need to be mutable
--> src/benchmarks.rs:36:13
|
36 | let mut results = Vec::new();
| ----^^^^^^^
| |
| help: remove this `mut`
warning: unused variable: `lib`
--> src/runtime/plugin_loader_v2/enabled/method_resolver.rs:40:19
|
40 | for ((lib, bt), spec) in map.iter() {
| ^^^ help: if this is intentional, prefix it with an underscore: `_lib`
warning: unnecessary `unsafe` block
--> src/runtime/plugin_loader_v2/enabled/method_resolver.rs:50:39
|
50 | ... let mid = unsafe { res_fn(cstr.as_ptr()) };
| ^^^^^^ unnecessary `unsafe` block
|
= note: `#[warn(unused_unsafe)]` on by default
warning: variable does not need to be mutable
--> src/runner/json_v0_bridge/lowering/ternary.rs:38:9
|
38 | let mut inputs = vec![(tend, tval), (eend, eval)];
| ----^^^^^^
| |
| help: remove this `mut`
warning: unreachable pattern
--> src/runner/json_v1_bridge.rs:512:29
|
368 | ... "Constructor" => {
| ------------- matches all the relevant values
...
512 | ... "Constructor" => {
| ^^^^^^^^^^^^^ no value can reach this
warning: unused variable: `filename`
--> src/runner/modes/vm_fallback.rs:349:44
|
349 | fn execute_vm_fallback_from_ast(&self, filename: &str, ast: nyash_rust::ast::ASTNode) {
| ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_filename`
warning: unused variable: `ast_on`
--> src/runner/modes/common_util/resolve/strip.rs:444:9
|
444 | let ast_on = crate::config::env::env_bool("NYASH_USING_AST");
| ^^^^^^ help: if this is intentional, prefix it with an underscore: `_ast_on`
warning: unused variable: `using_resolver`
--> src/runner/modes/common_util/resolve/prelude_manager.rs:126:17
|
126 | let using_resolver = UsingResolutionBox::new(&self.runner, path)?;
| ^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_using_resolver`
warning: unused variable: `filename`
--> src/runner/modes/common_util/resolve/prelude_manager.rs:114:9
|
114 | filename: &str,
| ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_filename`
warning: unused variable: `filename`
--> src/runner/modes/common_util/resolve/selfhost_pipeline.rs:132:9
|
132 | filename: &str,
| ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_filename`
warning: unused variable: `code`
--> src/runner/modes/common_util/resolve/selfhost_pipeline.rs:182:9
|
182 | code: &str,
| ^^^^ help: if this is intentional, prefix it with an underscore: `_code`
warning: unused variable: `filename`
--> src/runner/modes/common_util/resolve/selfhost_pipeline.rs:183:9
|
183 | filename: &str,
| ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_filename`
warning: variable does not need to be mutable
--> src/runner/modes/common_util/plugin_guard.rs:25:9
|
25 | let mut v = vec![
| ----^
| |
| help: remove this `mut`
warning: variable does not need to be mutable
--> src/runner/pipe_io.rs:23:13
|
23 | let mut json = if let Some(path) = &groups.parser.json_file {
| ----^^^^
| |
| help: remove this `mut`
warning: unused variable: `code`
--> src/runner/plugins.rs:61:40
|
61 | ... Ok(code) => {
| ^^^^ help: if this is intentional, prefix it with an underscore: `_code`
warning: value assigned to `out` is never read
--> src/runner/hv1_inline.rs:132:25
|
132 | let mut out = 0i64;
| ^^^
|
= help: maybe it is overwritten before being read?
= note: `#[warn(unused_assignments)]` on by default
warning: variable does not need to be mutable
--> src/host_providers/mir_builder.rs:49:38
|
49 | Ok(JsonValue::Object(mut m)) => {
| ----^
| |
| help: remove this `mut`
warning: methods `peek`, `peek_nth`, `get_mode`, and `set_mode` are never used
--> src/parser/cursor.rs:46:12
|
23 | impl<'a> TokenCursor<'a> {
| ------------------------ methods in this implementation
...
46 | pub fn peek(&self) -> &Token {
| ^^^^
...
55 | pub fn peek_nth(&self, n: usize) -> &Token {
| ^^^^^^^^
...
223 | pub fn get_mode(&self) -> NewlineMode {
| ^^^^^^^^
...
228 | pub fn set_mode(&mut self, mode: NewlineMode) {
| ^^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: methods `err_unexpected` and `expect_identifier` are never used
--> src/parser/statements/helpers.rs:62:19
|
18 | impl NyashParser {
| ---------------- methods in this implementation
...
62 | pub(super) fn err_unexpected<S: Into<String>>(&self, expected: S) -> ParseError {
| ^^^^^^^^^^^^^^
...
71 | pub(super) fn expect_identifier(&mut self, what: &str) -> Result<String, ParseError> {
| ^^^^^^^^^^^^^^^^^
warning: function `is_env_interface` is never used
--> src/mir/builder/calls/extern_calls.rs:152:8
|
152 | pub fn is_env_interface(name: &str) -> bool {
| ^^^^^^^^^^^^^^^^
warning: function `is_numeric_value` is never used
--> src/mir/builder/calls/special_handlers.rs:61:8
|
61 | pub fn is_numeric_value(node: &ASTNode) -> bool {
| ^^^^^^^^^^^^^^^^
warning: function `extract_numeric_type` is never used
--> src/mir/builder/calls/special_handlers.rs:73:8
|
73 | pub fn extract_numeric_type(node: &ASTNode) -> Option<MirType> {
| ^^^^^^^^^^^^^^^^^^^^
warning: function `make_function_name_with_arity` is never used
--> src/mir/builder/calls/special_handlers.rs:120:8
|
120 | pub fn make_function_name_with_arity(base_name: &str, arity: usize) -> String {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `is_reserved_function` is never used
--> src/mir/builder/calls/special_handlers.rs:125:8
|
125 | pub fn is_reserved_function(name: &str) -> bool {
| ^^^^^^^^^^^^^^^^^^^^
warning: function `suggest_alternative_for_reserved` is never used
--> src/mir/builder/calls/special_handlers.rs:133:8
|
133 | pub fn suggest_alternative_for_reserved(name: &str) -> String {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `generate_static_method_function_name` is never used
--> src/mir/builder/calls/function_lowering.rs:88:8
|
88 | pub fn generate_static_method_function_name(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `needs_void_termination` is never used
--> src/mir/builder/calls/function_lowering.rs:97:8
|
97 | pub fn needs_void_termination(returns_value: bool, is_terminated: bool) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: function `create_method_parameter_mapping` is never used
--> src/mir/builder/calls/function_lowering.rs:103:8
|
103 | pub fn create_method_parameter_mapping(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `create_static_parameter_mapping` is never used
--> src/mir/builder/calls/function_lowering.rs:121:8
|
121 | pub fn create_static_parameter_mapping(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `method_likely_returns_value` is never used
--> src/mir/builder/calls/function_lowering.rs:138:8
|
138 | pub fn method_likely_returns_value(method_name: &str) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `is_commonly_shadowed_method` is never used
--> src/mir/builder/call_resolution.rs:49:8
|
49 | pub fn is_commonly_shadowed_method(method: &str) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `generate_self_recursion_warning` is never used
--> src/mir/builder/call_resolution.rs:60:8
|
60 | pub fn generate_self_recursion_warning(box_name: &str, method: &str) -> String {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: associated function `is_typeop_method` is never used
--> src/mir/builder/method_call_handlers.rs:64:19
|
11 | impl MirBuilder {
| --------------- associated function in this implementation
...
64 | pub(super) fn is_typeop_method(method: &str, arguments: &[ASTNode]) -> Option<String> {
| ^^^^^^^^^^^^^^^^
warning: method `materialize_local` is never used
--> src/mir/builder/utils.rs:297:19
|
106 | impl super::MirBuilder {
| ---------------------- method in this implementation
...
297 | pub(crate) fn materialize_local(&mut self, v: super::ValueId) -> Result<super::ValueId, String> {
| ^^^^^^^^^^^^^^^^^
warning: function `try_known_rewrite` is never used
--> src/mir/builder/rewrite/known.rs:22:15
|
22 | pub(crate) fn try_known_rewrite(
| ^^^^^^^^^^^^^^^^^
warning: function `try_unique_suffix_rewrite` is never used
--> src/mir/builder/rewrite/known.rs:122:15
|
122 | pub(crate) fn try_unique_suffix_rewrite(
| ^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `try_known_or_unique` is never used
--> src/mir/builder/rewrite/known.rs:215:15
|
215 | pub(crate) fn try_known_or_unique(
| ^^^^^^^^^^^^^^^^^^^
warning: function `try_early_str_like` is never used
--> src/mir/builder/rewrite/special.rs:5:15
|
5 | pub(crate) fn try_early_str_like(
| ^^^^^^^^^^^^^^^^^^
warning: function `try_special_equals` is never used
--> src/mir/builder/rewrite/special.rs:107:15
|
107 | pub(crate) fn try_special_equals(
| ^^^^^^^^^^^^^^^^^^
warning: function `propagate_with_override` is never used
--> src/mir/builder/metadata/propagate.rs:21:8
|
21 | pub fn propagate_with_override(builder: &mut MirBuilder, dst: ValueId, ty: MirType) {
| ^^^^^^^^^^^^^^^^^^^^^^^
warning: function `emit_eq_to` is never used
--> src/mir/builder/emission/compare.rs:20:8
|
20 | pub fn emit_eq_to(b: &mut MirBuilder, dst: ValueId, lhs: ValueId, rhs: ValueId) -> Result<(), String> {
| ^^^^^^^^^^
warning: function `emit_ne_to` is never used
--> src/mir/builder/emission/compare.rs:25:8
|
25 | pub fn emit_ne_to(b: &mut MirBuilder, dst: ValueId, lhs: ValueId, rhs: ValueId) -> Result<(), String> {
| ^^^^^^^^^^
warning: function `set_type` is never used
--> src/mir/builder/types/annotation.rs:8:8
|
8 | pub fn set_type(builder: &mut MirBuilder, dst: ValueId, ty: MirType) {
| ^^^^^^^^
warning: methods `try_handle_map_box`, `try_handle_string_box`, and `try_handle_array_box` are never used
--> src/backend/mir_interpreter/handlers/boxes.rs:281:8
|
4 | impl MirInterpreter {
| ------------------- methods in this implementation
...
281 | fn try_handle_map_box(
| ^^^^^^^^^^^^^^^^^^
...
292 | fn try_handle_string_box(
| ^^^^^^^^^^^^^^^^^^^^^
...
303 | fn try_handle_array_box(
| ^^^^^^^^^^^^^^^^^^^^
warning: associated function `ensure_mir_json_version_field` is never used
--> src/backend/mir_interpreter/handlers/externals.rs:6:8
|
4 | impl MirInterpreter {
| ------------------- associated function in this implementation
5 | #[inline]
6 | fn ensure_mir_json_version_field(s: &str) -> String {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: method `write_box_result` is never used
--> src/backend/mir_interpreter/utils/destination_helpers.rs:16:19
|
9 | impl MirInterpreter {
| ------------------- method in this implementation
...
16 | pub(crate) fn write_box_result(
| ^^^^^^^^^^^^^^^^
warning: methods `validate_args_range` and `validate_args_min` are never used
--> src/backend/mir_interpreter/utils/arg_validation.rs:47:19
|
8 | impl MirInterpreter {
| ------------------- methods in this implementation
...
47 | pub(crate) fn validate_args_range(
| ^^^^^^^^^^^^^^^^^^^
...
74 | pub(crate) fn validate_args_min(
| ^^^^^^^^^^^^^^^^^
warning: method `convert_to_box` is never used
--> src/backend/mir_interpreter/utils/receiver_helpers.rs:18:19
|
9 | impl MirInterpreter {
| ------------------- method in this implementation
...
18 | pub(crate) fn convert_to_box(
| ^^^^^^^^^^^^^^
warning: associated functions `type_mismatch`, `out_of_bounds`, `receiver_type_error`, `arg_count_min`, and `from_error` are never used
--> src/backend/mir_interpreter/utils/error_helpers.rs:43:12
|
16 | impl ErrorBuilder {
| ----------------- associated functions in this implementation
...
43 | pub fn type_mismatch(method: &str, expected: &str, actual: &str) -> VMError {
| ^^^^^^^^^^^^^
...
60 | pub fn out_of_bounds(method: &str, index: usize, len: usize) -> VMError {
| ^^^^^^^^^^^^^
...
96 | pub fn receiver_type_error(expected: &str) -> VMError {
| ^^^^^^^^^^^^^^^^^^^
...
126 | pub fn arg_count_min(method: &str, min: usize, actual: usize) -> VMError {
| ^^^^^^^^^^^^^
...
156 | pub fn from_error(operation: &str, error: &dyn std::error::Error) -> VMError {
| ^^^^^^^^^^
warning: methods `err_type_mismatch` and `err_out_of_bounds` are never used
--> src/backend/mir_interpreter/utils/error_helpers.rs:181:19
|
162 | impl super::super::MirInterpreter {
| --------------------------------- methods in this implementation
...
181 | pub(crate) fn err_type_mismatch(&self, method: &str, expected: &str, actual: &str) -> VMError {
| ^^^^^^^^^^^^^^^^^
...
192 | pub(crate) fn err_out_of_bounds(&self, method: &str, index: usize, len: usize) -> VMError {
| ^^^^^^^^^^^^^^^^^
warning: methods `load_as_int`, `load_as_bool`, and `load_args_as_values` are never used
--> src/backend/mir_interpreter/utils/conversion_helpers.rs:45:19
|
9 | impl MirInterpreter {
| ------------------- methods in this implementation
...
45 | pub(crate) fn load_as_int(&mut self, vid: ValueId) -> Result<i64, VMError> {
| ^^^^^^^^^^^
...
74 | pub(crate) fn load_as_bool(&mut self, vid: ValueId) -> Result<bool, VMError> {
| ^^^^^^^^^^^^
...
115 | pub(crate) fn load_args_as_values(
| ^^^^^^^^^^^^^^^^^^^
warning: field `returns_result` is never read
--> src/runtime/plugin_loader_v2/enabled/loader/specs.rs:21:16
|
19 | pub(crate) struct MethodSpec {
| ---------- field in this struct
20 | pub(crate) method_id: u32,
21 | pub(crate) returns_result: bool,
| ^^^^^^^^^^^^^^
|
= note: `MethodSpec` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis
warning: function `is_special_method` is never used
--> src/runtime/plugin_loader_v2/enabled/method_resolver.rs:133:15
|
133 | pub(super) fn is_special_method(method_name: &str) -> bool {
| ^^^^^^^^^^^^^^^^^
warning: function `get_special_method_id` is never used
--> src/runtime/plugin_loader_v2/enabled/method_resolver.rs:138:15
|
138 | pub(super) fn get_special_method_id(method_name: &str) -> Option<u32> {
| ^^^^^^^^^^^^^^^^^^^^^
warning: function `post_run_exit_if_oob_strict_triggered` is never used
--> src/runner/child_env.rs:11:8
|
11 | pub fn post_run_exit_if_oob_strict_triggered() -> ! {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `demo_interpreter_system` is never used
--> src/runner/demos.rs:93:15
|
93 | pub(super) fn demo_interpreter_system() {
| ^^^^^^^^^^^^^^^^^^^^^^^
warning: method `execute_vm_fallback_from_ast` is never used
--> src/runner/modes/vm_fallback.rs:349:8
|
347 | impl NyashRunner {
| ---------------- method in this implementation
348 | /// Small helper to continue fallback execution once AST is prepared
349 | fn execute_vm_fallback_from_ast(&self, filename: &str, ast: nyash_rust::ast::ASTNode) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: field `filename_canon` is never read
--> src/runner/modes/common_util/resolve/using_resolution.rs:17:5
|
13 | pub struct UsingResolutionBox<'a> {
| ------------------ field in this struct
...
17 | filename_canon: Option<PathBuf>,
| ^^^^^^^^^^^^^^
warning: function `suggest_in_base` is never used
--> src/runner/pipeline.rs:87:15
|
87 | pub(super) fn suggest_in_base(base: &str, leaf: &str, out: &mut Vec<String>) {
| ^^^^^^^^^^^^^^^
warning: `nyash-rust` (lib) generated 108 warnings (run `cargo fix --lib -p nyash-rust` to apply 26 suggestions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s

View File

@ -131,6 +131,12 @@ fn main() -> Result<()> {
bail!("input JSON not found: {}", input_path.display()); bail!("input JSON not found: {}", input_path.display());
} }
// Optional: dump incoming MIR JSON for diagnostics (AotPrep 後の入力を観測)
if let Ok(dump_path) = env::var("NYASH_LLVM_DUMP_MIR_IN") {
let _ = std::fs::copy(&input_path, &dump_path);
eprintln!("[ny-llvmc] dumped MIR input to {}", dump_path);
}
// Optional: preflight shape/hints (best-effort; no behavior change) // Optional: preflight shape/hints (best-effort; no behavior change)
if let Ok(s) = std::fs::read_to_string(&input_path) { if let Ok(s) = std::fs::read_to_string(&input_path) {
if let Ok(val) = serde_json::from_str::<JsonValue>(&s) { if let Ok(val) = serde_json::from_str::<JsonValue>(&s) {
@ -379,9 +385,26 @@ fn link_executable(
cmd.arg(tok); cmd.arg(tok);
} }
} }
let status = cmd.status().context("failed to invoke system linker")?; // Run linker and capture diagnostics for better error reporting
if !status.success() { let output = cmd
bail!("linker exited with status: {:?}", status.code()); .output()
.with_context(|| format!("failed to invoke system linker: {}", linker))?;
if !output.status.success() {
eprintln!("[ny-llvmc/link] command: {}", linker);
// Show args (for debugging)
// Note: std::process::Command doesn't expose argv back; re-emit essential parts
eprintln!(
"[ny-llvmc/link] args: -o {} {} -Wl,--whole-archive {} -Wl,--no-whole-archive -ldl -lpthread -lm {}",
out_exe.display(),
obj.display(),
libnyrt.display(),
extra_libs.unwrap_or("")
);
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
eprintln!("[ny-llvmc/link:stdout]\n{}", stdout);
eprintln!("[ny-llvmc/link:stderr]\n{}", stderr);
bail!("linker exited with status: {:?}", output.status.code());
} }
Ok(()) Ok(())
} }

View File

@ -38,6 +38,7 @@ pub extern "C" fn nyash_string_len_h(handle: i64) -> i64 {
// FAST-path helper: compute string length from raw pointer (i8*) with mode (reserved) // FAST-path helper: compute string length from raw pointer (i8*) with mode (reserved)
// Exported as both legacy name (nyash.string.length_si) and neutral name (nyrt_string_length) // Exported as both legacy name (nyash.string.length_si) and neutral name (nyrt_string_length)
#[inline(always)]
#[export_name = "nyrt_string_length"] #[export_name = "nyrt_string_length"]
pub extern "C" fn nyrt_string_length(ptr: *const i8, _mode: i64) -> i64 { pub extern "C" fn nyrt_string_length(ptr: *const i8, _mode: i64) -> i64 {
use std::ffi::CStr; use std::ffi::CStr;

View File

@ -1,231 +1,126 @@
# Environment Variables — Quick Reference (Phase 22.1) ENV Variables Index (Core, Builder, Smokes)
This document lists the environment flags introduced or used by the Phase 22.1 work. Defaults are OFF and behavior remains unchanged unless noted. Purpose: quick reference for toggles used frequently during development and in smokes. Defaults aim to be safe and failfast.
- NYASH_JSON_ONLY=0|1 Runtime/VM
- Quiet JSON pipelines: suppresses `RC:` and routine logs on stdout (diagnostics still go to stderr). - NYASH_FAIL_FAST=0
- Used by StageB → Program(JSON) emit to keep the output clean for downstream processing. - Global relaxation switch; when set to 0/false, paired legacy guards may allow otherwise failfast paths.
- Use only for diagnosis; keep OFF in CI.
- HAKO_USING_SSOT=0|1 - NYASH_LEGACY_FIELDS_ENABLE=1
- Enables the SSOT resolver gate in the runner pipeline. - Materialize legacy InstanceBox shared fields map for compatibility with older code paths.
- When ON, resolution first consults the SSOT bridge (modules-only MVP). If not resolved, it falls back to the existing resolver. - Default: OFF. Dev/compat only; safe to keep disabled in CI and normal runs.
- Trace: set `NYASH_RESOLVE_TRACE=1` to see `[using/ssot]` tags.
- HAKO_USING_SSOT_HAKO=0|1 - NYASH_JSON_ONLY=1
- Optional: within the SSOT bridge, call the Hako box `UsingResolveSSOTBox.resolve(name, ctx)` via the VM. - When tools/wrappers set this, StageB and related emitters print only JSON to stdout. Auxiliary logs should be routed to stderr.
- MVP passes `{ modules, using_paths, cwd }` in `ctx` (modules is consulted). IO is not performed in the box. - Smokes expect singleline JSON on stdout for Program(JSON) and MIR(JSON) producers.
- Requires `nyash` binary present; guard remains OFF by default.
Relative inference (SSOT) - NYASH_ENABLE_USING=1, HAKO_ENABLE_USING=1
- Default OFF: `HAKO_USING_SSOT_RELATIVE=1` enables a minimal relative candidate synthesis (cwd → using_paths). When multiple candidates exist and `NYASH_USING_STRICT=1`, resolution delegates to legacy resolver (behavior unchanged). - Enable using/alias resolution in dev/VM runs. Smokes set both for symmetry.
- Ambiguous list size: `HAKO_USING_SSOT_RELATIVE_AMBIG_FIRST_N=<N>` customizes how many candidates are shown in trace (default 3, bounded 110).
Notes on SSOT ctx (expansion plan) Extern/Providers
- The bridge constructs a context with: - Arity suffix normalization
- `modules` (Map<String,String>) — exact name → path mapping - The VM accepts extern names with arity suffixes and normalizes them before dispatch:
- `using_paths` (Array<String>) — resolution bases (MVP: hint only) - Examples: `env.get/1``env.get`, `hostbridge.extern_invoke/3``hostbridge.extern_invoke`.
- `cwd` (String) — callers directory (MVP: hint only) - Implemented in `src/backend/mir_interpreter/handlers/calls/externs.rs` and `handlers/externals.rs`.
- Hako box will progressively leverage `using_paths`/`cwd` for relative inference (planned; defaults remain unchanged until enabled).
- HAKO_TLV_SHIM=0|1 Plugins / Autoload
- Enables an identity TLV roundtrip at the end of argument encoding for plugin calls. - NYASH_USING_DYLIB_AUTOLOAD=1
- Requires building with `--features tlv-shim` to link the optional crate `nyash-tlv`. - Enable autoload of `[using.<name>]` entries with `kind="dylib"` from `nyash.toml`.
- Default OFF; when OFF, the buffer is returned unchanged. - The runner loads the specified shared libraries at startup and registers providers for the boxes declared in each plugins `nyash_box.toml`.
- Default: OFF. Guarded to avoid surprises; respects `NYASH_DISABLE_PLUGINS=1`.
- tlv-shim (Cargo feature) Parser/StageB
- `cargo build --features tlv-shim` links the optional `nyash-tlv` crate. - HAKO_PARSER_STAGE3=1, NYASH_PARSER_STAGE3=1
- Without this feature, `HAKO_TLV_SHIM=1` has no effect and the original path is used. - Accept Stage3 syntax (Break/Continue/Try/Catch, etc.). Enabled in smokes for StageB runs.
TLV shim diagnostics - HAKO_STAGEB_FUNC_SCAN=1
- HAKO_TLV_SHIM_TRACE=0|1 - Devonly: inject a `defs` array into Program(JSON) with scanned method definitions for `box Main`.
- When 1 (with `tlv-shim` feature), emit a concise trace tag `[tlv/shim:<Box>.<method>]` for shimmed calls.
- Default: minimal noise`MapBox.set` のみ)。詳細な対象はフィルタで拡張。
- HAKO_TLV_SHIM_FILTER=<CommaSeparatedList>
- Filter which calls are traced例: `MapBox.set,ArrayBox.push`)。`<Box>.<method>` または `method` のみで一致。
- 未設定時は最小(`MapBox.set` のみ)。
- HAKO_TLV_SHIM_TRACE_DETAIL=0|1
- When 1, emits `[tlv/shim:detail argc=N]`.
Examples (TLV trace) Selfhost builders and wrappers
- `HAKO_TLV_SHIM=1 HAKO_TLV_SHIM_TRACE=1 HAKO_TLV_SHIM_FILTER=ArrayBox.push` - HAKO_SELFHOST_BUILDER_FIRST=1
- `HAKO_TLV_SHIM=1 HAKO_TLV_SHIM_TRACE=1 HAKO_TLV_SHIM_FILTER=MapBox.get` - Prefer the Hako MirBuilder path first; wrappers fall back to Rust CLI builder on failure to keep runs green.
- `HAKO_TLV_SHIM=1 HAKO_TLV_SHIM_TRACE=1 HAKO_TLV_SHIM_FILTER=ArrayBox.push,MapBox.get` (複数指定)
- `HAKO_SHOW_CALL_LOGS=1 HAKO_CALL_TRACE=1 HAKO_CALL_TRACE_FILTER=env.console.log`テストランナーのフィルタ無効extern だけ観測)
Core Thinning I (Phase 22.2) — Plugin C wrapper (design hook) - NYASH_LLVM_USE_HARNESS=1
- HAKO_PLUGIN_LOADER_C_WRAP=0|1 - Enable LLVM harness mode (nyllvmc crate backend). Used by builder scripts for EXE/OBJ emission.
- When 1, emits a design-stage tag `[cwrap:invoke:<Box>.<method>]` at the plugin invocation site and then proceeds with the normal path.
- Default OFF; no behavior change.
- HAKO_PLUGIN_LOADER_C_WRAP_FILTER=<CommaSeparatedList>
- Filter for cwrap tags例: `MapBox.set,ArrayBox.push`)。`<Box>.<method>` または `method` のみで一致。
Core Thinning I (Phase 22.2) — C-core probe (design hook) Smokes
- HAKO_C_CORE_ENABLE=0|1 - SMOKES_DEFAULT_TIMEOUT
- When 1, emits a tag `[c-core:invoke:<Box>.<method>]` and (when built with feature `c-core`) calls a thin C probe (`nyash-c-core`) before proceeding with the normal path. - Pertest timeout (seconds) used by `tools/smokes/v2/run.sh --timeout` or auto profile defaults. Quick profile defaults to ~15s.
- Default OFF; behavior unchanged. - Some tests wrap heavy steps (e.g., running a built EXE) with a shorter internal `timeout` to convert hangs into SKIP.
- HAKO_C_CORE_TARGETS=<CommaSeparatedList> - HAKO_BUILD_TIMEOUT, HAKO_EXE_TIMEOUT
- Targets to probe例: `MapBox.set,ArrayBox.push`)。未設定時は `MapBox.set` のみ。 - Internal timeouts (seconds) used by several phase2100 cratebackend tests to bound nyllvmc build/link and EXE execution steps under quick.
- Build note: enable C-core with `cargo build --release -p nyash-rust --features c-core`. - Defaults: `HAKO_BUILD_TIMEOUT=10`, `HAKO_EXE_TIMEOUT=5`.
- Examples:
- `HAKO_C_CORE_ENABLE=1 HAKO_C_CORE_TARGETS=ArrayBox.push`
- `HAKO_C_CORE_ENABLE=1 HAKO_C_CORE_TARGETS=ArrayBox.len,ArrayBox.length`
Related toggles used by smokes/tools (for parity with runner/test wrappers):
Call/route unified trace (optional)
- HAKO_CALL_TRACE=0|1
- When ON, emits `[call:<target>.<method>]` for both plugin calls and extern calls.
- Default OFF; logs go to stderr.
- HAKO_CALL_TRACE_FILTER=<CommaSeparatedList>
- Restrict `[call:]` logs to specific targets.
- Matches `<target>.<method>` or bare `method`.
- Examples:
- `HAKO_CALL_TRACE_FILTER=MapBox.set` (method-only)
- `HAKO_CALL_TRACE_FILTER=env.console.log,MapBox.set` (mix target+method)
- HAKO_SHOW_CALL_LOGS=0|1
- When 1, test runner disables its default log filter so `[call:]` traces appear in output.
- NYASH_PARSER_STAGE3=1, HAKO_PARSER_STAGE3=1, NYASH_PARSER_ALLOW_SEMICOLON=1
- NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1
- NYASH_DISABLE_NY_COMPILER=1, HAKO_DISABLE_NY_COMPILER=1
LLVM backend selector (builder wrapper)
- NYASH_LLVM_BACKEND=auto|crate|llvmlite|native
- Selects the backend used by `tools/ny_mir_builder.sh` for `--emit obj|exe`.
- Default: `auto`(優先順位: `crate`ny-llvmc が存在すれば既定)→ `native`llc がある場合)/`llvmlite` は明示指定時のみ)。
- `crate`: `./target/release/ny-llvmc` を使用(`cargo build -p nyash-llvm-compiler --release`。推奨の本線EXE/AOT
- `llvmlite`: Python ハーネス `tools/llvmlite_harness.py`(内部用途。外部から使う場合は明示指定)
- `native`: 将来の Hakonative builder 用に予約。
- `--emit exe` のリンク追加ライブラリは `HAKO_AOT_LDFLAGS` で渡す(例: `-static`。nyllvmc は `--libs` 経由で受理。
- 備考: crate 経路では `ny_main` の戻り値i64がプロセスの終了コードに反映rc mapping
- NYASH_LLVM_NATIVE_TRACE=0|1
- When 1, dumps the native IR to stderr for debugging.
- Used with `native` backend to inspect generated LLVM IR before optimization.
- Default OFF; only active when NYASH_LLVM_BACKEND=native is set.
- HAKO_LLVM_CANARY_NORMALIZE=0|1
- 開発/カナリア専用の正規化スイッチ。`1` のとき、最小の JSON 形状差(`schema_version=1``"1.0"``blocks.inst``instructions``const``ty/value` 包装)を自動補正してからビルドします。
- 既定は `0`(無効)。既存ツールの挙動は変わりません。`NYASH_CLI_VERBOSE=1` のとき形状ヒントを `[ny-llvmc/hint]` で出力します。
Name mapping note (EXE link convenience)
- nyash.console.* は C リンク時にシンボル名 `nyash_console_*` に正規化される(ドット→アンダースコア)。
- 例: `externcall nyash.console.log(i8*)` → C シンボル `nyash_console_log`
- 最小 C ランタイムPhase 22.3)の `nyash-kernel-min-c``nyash_console_log(char*)` を提供(設計段階)。
Kernel Minimal C Runtime (Phase 22.3 — design)
- NYASH_KERNEL_C_MIN=0|1
- Reserved toggle for enabling the minimal C runtime shimsdesignstage; defaults OFF
- Build: `cargo build --release -p nyash-kernel-min-c`not linked by default
ENV consolidation (aliases)
- NY compiler path
- Primary: `NYASH_USE_NY_COMPILER=0|1`
- Accepted aliases (deprecated; prints a onetime warning):
- `NYASH_DISABLE_NY_COMPILER=1` → equivalent to `NYASH_USE_NY_COMPILER=0`
- `HAKO_DISABLE_NY_COMPILER=1` → equivalent to `NYASH_USE_NY_COMPILER=0`
- LLVM opt level
- Primary: `NYASH_LLVM_OPT_LEVEL`
- Accepted alias (deprecated; onetime warning): `HAKO_LLVM_OPT_LEVEL`
- GateC (Core direct route)
- Primary: `NYASH_GATE_C_CORE`
- Accepted alias (deprecated; onetime warning): `HAKO_GATE_C_CORE`
Notes Notes
- Primary keys are preferred and will be kept. Aliases remain accepted for a grace period and emit a concise deprecation line once per process. - Keep default behavior unchanged for users. Use these toggles in development and CI wrappers only.
NYASH_FAIL_FAST - Avoid enabling legacy paths except for targeted diagnosis. The unified call system is the default in both builder and VM.
- Type: 0|1 (default: 1)
- Purpose: Global FailFast policy for runtime fallbacks in Rust layer. When 1, prohibits silent or alternateroute fallbacks and panics with a stable tag (e.g., [failfast/provider/filebox:*], [failfast/ssot/*]). Set to 0 temporarily during bringup or canaries that rely on legacy routes.
Hakorune StageB (include policy) Using/Resolver
- VM backend currently does not support `include` statements in Hako execution. StageB focuses on producing Program(JSON v0) (oneline) and avoids includes. - HAKO_USING_RESOLVER_FIRST=1
- Program(JSON) → MIR(JSON) uses the Rust GateC path by default. Hako MirBuilder is optin and introduced gradually (registry/internal toggles). - Try the SSOT using resolver (`using::resolver::resolve_using_target_common`) first in the runner pipeline.
- On failure, the pipeline falls back to the existing runner logic (aliases/builtins/plugins/relative search).
- Default: OFF. Use to validate resolverfirst migration without changing defaults.
MirBuilder toggles (optin) Builder/Emit (Selfhost)
- `HAKO_MIR_BUILDER_INTERNAL=0|1` (default: 1) - HAKO_SELFHOST_BUILDER_FIRST=1
- Enable internal lowers (Return/If/Compare/BinOp/Method minimal). When 0, only delegate path is used (if enabled). - Prefer Hako MirBuilder path first; delegates to provider/legacy on failure. Used by `tools/hakorune_emit_mir.sh` and bench scripts.
- `HAKO_MIR_BUILDER_REGISTRY=0|1` (default: 1) - HAKO_MIR_BUILDER_BOX=hako.mir.builder|min
- Enable registrydriven lowering (pattern names like `if.compare.intint`, `return.binop.intint`, `return.method.arraymap`). - Choose selfhost builder box (full or minimal runner).
- `HAKO_MIR_BUILDER_DELEGATE=0|1` (default: 0) - HAKO_SELFHOST_TRACE=1
- Delegate to Runner (`--program-json-to-mir`) instead of internal lowers. Useful for bringup; keep OFF for selfhost canaries. - Print additional traces during MIR emit bench/wrappers.
- `HAKO_MIR_BUILDER_SKIP_LOOPS=0|1` (default: 0)
- Skip heavy loop lowers (`lower_loop_sum_bc`, `lower_loop_count_param`, `lower_loop_simple`) when 1. Diagnostic/bringup aid; no behavior change when unset.
- `HAKO_MIR_BUILDER_REGISTRY_ONLY=<name>` (optional)
- Restrict registry dispatch to a single pattern name (e.g., `return.method.arraymap`) for diagnostics.
MirBuilder (min) alias — bringup helper - HAKO_MIR_BUILDER_LOOP_FORCE_JSONFRAG=1
- Module alias: `hako.mir.builder.min``lang/src/mir/builder/MirBuilderMinBox.hako` - Force the selfhost builder (and wrappers) to emit a minimal, pure controlflow MIR(JSON) for loop cases (const/phi/compare/branch/binop/jump/ret)。
- Minimal entry that only loads lightweight lowers (e.g., `return.method.arraymap`, `return.int`). - Dev専用。purify/normalize と併用すると ret ブロックに副作用命令を混入させない形で AOT/EXE 検証がしやすくなる。
- Tags: emits `[mirbuilder/min:<pattern>]` on success. Default behavior unchanged; use explicitly in tests/tools.
FileBox provider policydev overrides - HAKO_MIR_BUILDER_JSONFRAG_NORMALIZE=1, HAKO_MIR_BUILDER_JSONFRAG_PURIFY=1
- Baseline: FileBox は ring1 常備coreroとして登録されます。プラグイン未配置でも panic 経路にはならず、FailFast が ON の場合は明示タグで失敗します(例: `[failfast/provider/filebox:*]`)。 - JsonFrag の正規化と純化を有効化する。purify=1 のとき newbox/boxcall/externcall/mir_call を除去し、ret 以降の命令を打ち切る(構造純化)。
- `NYASH_FILEBOX_MODE=auto|core-ro|plugin-only` — provider selection modedefault auto; with plugins disabled, resolves to corero
- `NYASH_FILEBOX_ALLOW_FALLBACK=0|1` — When 1, allows corero fallback even if FailFast is ON (use sparingly; defaults OFF).
- JSON pipelines: `NYASH_JSON_ONLY=1` also permits corero fallback under FailFast (quiet structured I/O).
Examples (FailFast tags and safe overrides) Provider path (delegate)
- Default (FailFast=1): corero フォールバックを禁止(プロバイダ未設定時に失敗) - HAKO_MIR_NORMALIZE_PROVIDER=1
- 例外ログ: `[failfast/provider/filebox:auto-fallback-blocked]` - ProviderRust出力の MIR(JSON) に対して、Hako の JsonFrag 正規化パスを適用するtools/hakorune_emit_mir.sh 内部)。
- 実行例: - 互換維持のため既定はOFF。Box 系で ret ブロックに副作用命令が残るようなケースの暫定純化に利用できる。
```sh
# will fail fast without plugins
./target/release/hakorune --backend vm apps/tests/filebox_sample.hako
```
- Bringup局所的に許可: `NYASH_FILEBOX_ALLOW_FALLBACK=1` で corero を許可
- 実行例:
```sh
NYASH_FILEBOX_ALLOW_FALLBACK=1 ./target/release/hakorune --backend vm apps/tests/filebox_sample.hako
```
- JSON パイプ(静音): `NYASH_JSON_ONLY=1` を併用して整形済み JSON のみを stdout に出す
- 実行例:
```sh
NYASH_JSON_ONLY=1 NYASH_FILEBOX_ALLOW_FALLBACK=1 ./target/release/hakorune --backend vm apps/tests/emit_program_json.hako
```
Provider policy共通 - NYASH_LLVM_FAST_INT=1
- `HAKO_PROVIDER_POLICY=strict-plugin-first|safe-core-first|static-preferred` - Opt-in toggle for integer hot paths in LLVM MIR/IR. When `1`, `binop``compare` は同一ブロックの `vmap` 定義を最優先し、resolver/PHI を経由しない i64 経路を選びます。ループや分岐で i64 が綺麗に残るときだけ ON にして、CI 等では unset のまま。
- Auto モード時の選択ポリシーを制御(既定: `strict-plugin-first`)。
- `safe-core-first`/`static-preferred` は ring1静的/coreroを優先、利用不可時のみプラグインにフォールバック。
- Box個別のモード指定例: FileBox
- `<BOX>_MODE=auto|ring1|plugin-only`(例: `NYASH_FILEBOX_MODE`
- `<BOX>_ALLOW_FALLBACK=0|1`FailFast 例外。局所許可)
- 既存の FileBox 変数はそのまま有効。共通パターンとして今後は他Boxにも拡張予定。
Selfhostfirst wrapper toggles (StageB → MirBuilder) - NYASH_MIR_DEV_IDEMP=1
- `HAKO_SELFHOST_BUILDER_FIRST=0|1` (default: 0) - Dev-only: Enable idempotence markers inside MIR normalize passes. Each pass stamps `pass:function` in the module metadata after rewriting a function once, and subsequent optimizer runs skip reprocessing that function for the same pass. 仕様不変既定OFF。診断時に複数回最適化を呼んでも差分が出ないことを保証するための保険だよ。
- When 1, tools like `tools/hakorune_emit_mir.sh` try StageB → MirBuilder(Hako) first, and only fall back to the Rust delegate when necessary. - NYASH_LLVM_FAST=1
- `HAKO_SELFHOST_NO_DELEGATE=0|1` (default: 0) - Enables NyRT-based FAST helpers (string length via `nyrt_string_length`, pointer re-use, etc.) and now caches literal-backed `length/len` results so loops reuse the same constant value instead of re-calling the helper. Default OFF.
- When 1, forbids delegate fallback in the wrapper. If the Hako MirBuilder fails, the wrapper fails fast (useful to validate the selfhost path).
VM/AOT fast-path toggles (bench/dev only)
- NYASH_VM_FAST=0|1
- Enables small VM micro-optimizations used in micro-benchmarks (no behavior changes in normal runs):
- new StringBox("const") fast path (registry bypass when safe)
- StringBox.length/size returning i64 directly (avoid boxing)
- Default OFF.
- NYASH_LLVM_FAST=0|1
- Enables AOT lowering tweaks in ny-llvmc (opt-in, benches only):
- StringBox.length/size lowered to extern helper returning i64 (no boxing)
- Default OFF. Guarded in ny-llvmc; legacy lowering remains default.
MirBuilder toggles (trace and JsonFrag) - NYASH_MIR_LOOP_HOIST=1
- `HAKO_MIR_BUILDER_TRACE=0|1` (default: 0) - AOT 前準備AotPrepBoxでの軽ホイスティングを有効化。固定文字列の `length/len` を即値に置換JSON 書換え)する。制御フローは変更しない。既定 OFF。
- When 1, emits concise builder tags like `[mirbuilder/internal/*]` from Hako lowers. Errors remain visible regardless; this flag controls informational tags.
- `HAKO_MIR_BUILDER_LOOP_JSONFRAG=0|1` (default: 0)
- Enable JsonFrag minimal MIR assembly in loop adapters/lowers (structure observation only).
- `HAKO_MIR_BUILDER_JSONFRAG_NORMALIZE=0|1` (default: 0)
- Normalize JsonFrag output for stable canaries (const grouping, phi placement, numeric canonicalization).
- `HAKO_MIR_BUILDER_LOOP_RETURN=string|map` (default: string)
- Controls the return type of the loop adapter JsonFrag path. `string` returns MIR(JSON) text; `map` returns a `MapBox` like `{ mir: <string> }`.
Provider diagnostics - NYASH_AOT_COLLECTIONS_HOT=1
- `HAKO_PROVIDER_TRACE=0|1` (default: 0) - AOT 前準備AotPrepBoxで Array/Map の `boxcall``externcall``nyash.array.*` / `nyash.map.*`のホットパスに張り替える。AOT 専用の最短経路で、診断を省いてオーバーヘッドを抑える。既定 OFF。
- When 1, forces provider selection tags like `[provider/select:FileBox ring=1 src=static]` to appear even under `NYASH_JSON_ONLY=1`.
EXE argv bridge (crate backend) - HAKO_MIR_NORMALIZE_PRINT=1
- NYASH_EXE_ARGV=0|1 - AotPrep の正規化パス(.hakoを有効化して、`print` 命令を `externcall env.console.log(value)` に書き換えるCFG 不変)。既定 OFF。
- When 1, the ny-llvmc entry wrapper calls `nyash.env.argv_get()` to obtain an `ArrayBox` of process arguments and passes it to `Main.main(args)`.
- Default OFF互換維持。OFF の場合は空の `ArrayBox` が渡されます。 - HAKO_MIR_NORMALIZE_REF=1
- VM 実行は従来どおり環境変数経由(`NYASH_SCRIPT_ARGS_HEX_JSON` / `NYASH_SCRIPT_ARGS_JSON` / `NYASH_ARGV`)で受け取り、`Main.main(args)` に配列を渡します。 - AotPrep の正規化パス(.hakoを有効化して、`ref_get/ref_set``boxcall getField/setField` に書き換えるCFG 不変、best-effort。既定 OFF
- MIR optimizer dev gate
- NYASH_MIR_DISABLE_OPT=0|1 (alias: HAKO_MIR_DISABLE_OPT) - HAKO_MIR_NORMALIZE_ARRAY=1
- When 1, disables all MIR optimizer passes (diagnostics only). Default OFF. - AotPrep の正規化パス(.hakoを有効化して、`array_get/array_set`(および一部の `map_get/map_set`)を `boxcall get/set` に書き換えるCFG 不変、best-effort。CollectionsHot の前処理として有効。既定 OFF
- 用途: 最適化の誤変換切り分けや、退行時の一時回避(既定不変)。
- NYASH_AOT_INDEX_WINDOW=1
- AotPrep(CollectionsHot 内部) の index 共有をブロック境界をまたいだ短窓で行う実験的トグル(デフォルト OFF
- 窓はバイト数で固定2048。誤共有のリスクがあるため初期はベンチ限定で使用。
- NYASH_VERIFY_RET_PURITY=1
- Ret ブロック純化の Fail-Fast トグル。Return の直前に `Const``Copy``Phi``Nop` 以外があると MIR/VM 実行が失敗するようにする開発ガード。既定 OFF。
- tools/perf/dump_mir.sh
- MIR(JSON) を provider-first → jsonfrag フォールバックで吐く小さな wrapper。`--mode provider` で通常 builder 主導、`--mode jsonfrag` で最小化ループを強制。内部でブロックごとの op カウンターを出力するので、branch/map/kilo などのホットパスをすばやく可視化できるよ。
AOT/LLVM (ny-llvmc)
- HAKO_LLVM_OPT_LEVEL=0|1
- ny-llvmc optimization level (default 0/O0). Bench scripts keep O0 unless overridden.
- NYASH_LLVM_DUMP_MIR_IN=/path/to/out.json
- ny-llvmc が受け取る MIR(JSON) をそのまま保存する開発用トグル。AotPrep 適用後の実入力を観測するのに使う。既定 OFF。
Bench helpers
- PERF_USE_PROVIDER=1
- `tools/perf/microbench.sh` で provider/selfhost-first の MIR emit を優先jsonfrag 強制を解除)。環境により provider 失敗時は自動で最小ループにフォールバック。

View File

@ -0,0 +1,45 @@
Legacy ByName Call Removal Plan
Context
- Unified call system is default. VM routes by `Callee` kind (Global/Method/Extern/…)
- Legacy byname resolution (callee=None → string name lookup) is gated by env and OFF by default.
- Goal: remove legacy code path once parity is proven at integration/full levels.
Guards / Current Behavior
- 旧 byname 経路は削除済み。`NYASH_VM_ALLOW_LEGACY` は廃止。
- 既定: `callee=None` 生成時は FailFastBuilder で Callee を必ず付与)。
Scope of Work
1) Identify callers that could still rely on legacy resolution
- Builder emissions that may omit `callee` (should be none after Phase 1 refactor)
- Any VM entry points or helpers constructing byname calls dynamically
- Older tests or tools expecting byname lookups
2) Ensure unified path covers them
- For Global user functions: attach `Callee::Global("Module.func")`
- For dynamic/indirect calls: mark `Callee::Value` and keep VM restrictions explicit
- For externlike names: delegate via handlers/calls/externs.rs (SSOT)
- Normalize arity (`foo/1``foo`) via `utils::normalize_arity_suffix`
3) Strengthen tests
- Smokes/quick: unified pathbyname 不可)を既に強制
- Integration/full: ユーザー関数呼び出し/extern 名正規化のカナリアを追加
- 旧ENV依存がないことを確認`NYASH_VM_ALLOW_LEGACY` は削除済)
4) Remove legacy code path
- Delete `src/backend/mir_interpreter/handlers/calls/legacy.rs` and its resolver module
- Drop env guard checks referring to legacy path (keep error message clarity)
- Update READMEs to remove temporary notes
Readiness Criteria (before removal)
- quick/integration/full all green with legacy OFF
- No `callee=None` emission in builder (verified by grepping MIR JSON / code review)
- Extern SSOT accepts aritysuffixed names; Global externlike names delegate properly
Rollback Plan
- Keep a small revertable commit boundary for legacy deletion
- If issues appear, reintroduce legacy.rs with the same path under a devguard until fixed
CrossReferences
- handlers/calls/README.md (SSOT and boundaries)
- docs/ENV_VARS.md (env guards and policies)

View File

@ -0,0 +1,43 @@
Normalization Ownership — Rust vs Hakorune
Goal
- Prevent double-normalization and keep a clear Single Source of Truth (SSOT) for where each rewrite lives.
Ownership
- Hakorune layer (Hako scripts)
- Methodization: Global("Box.m/N") → mir_call(Method) への変換。
- Name/arity canonicalizationBox.method/N
- Function defs scan/injectHAKO_STAGEB_FUNC_SCAN, HAKO_MIR_BUILDER_FUNCS
- Emit JSON v1 + unified mir_callNYASH_JSON_SCHEMA_V1=1, NYASH_MIR_UNIFIED_CALL=1
- 可視化タグ/診断の出力dev のみ)
- Rust layer
- Structural/correctness: SSA/PHI、受信側ローカライズLocalSSA/Copy/pin
- Legacy JSON v0 → minimal bridgingjson_v0_bridge 内での Callee 補完など)。
- 互換/安全弁: 未定義受信の構造的回復同一BB直近 NewBoxなど、dev ガード付きの最小範囲。
- Optimizer は構造・副作用ベースの最適化に限定(意味論の再書換えはしない)。
Guards and Toggles
- Hakodev 推奨セット)
- HAKO_STAGEB_FUNC_SCAN=1
- HAKO_MIR_BUILDER_FUNCS=1
- HAKO_MIR_BUILDER_CALL_RESOLVE=1
- HAKO_MIR_BUILDER_METHODIZE=1methodize が v1+unified 出力へ寄与)
- NYASH_JSON_SCHEMA_V1=1, NYASH_MIR_UNIFIED_CALL=1
- Rustbridge/診断)
- HAKO_BRIDGE_METHODIZE=1 は bring-up 用の補助。Hako 既定化後は OFF撤去予定
- mir_plugin_invoke/plugin_only は A/B 評価・診断用途。既定 OFF。
Rules of Engagement
- v1 + unified を Hako で生成した場合、Rust 側での methodize/再書換えは行わない(構造のみ)。
- json_v0_bridge は v0 入力に対する互換のために限定運用。v1 既定化が進めば縮退する。
- dev の安全弁(未定義受信の構造回復など)は、テストが十分になり次第 OFF/撤去する。
Testing
- Canary
- tools/dev/phase217_methodize_canary.shrc=5
- tools/dev/phase217_methodize_json_canary.shschema_version + mir_call present、Method優先
- tools/dev/phase216_chain_canary_call.shrc=5
- 失敗時は Hako 側methodize→ Rust 側(構造) の順で原因を特定する。

View File

@ -1,31 +1,33 @@
# Phase 21.5 — Optimization (ny-llvm crate line) # Phase 21.5 — Optimization (AotPrepFirst)
Scope 目的
- Optimize hot paths for the crate (ny-llvmc) line using Hakorune scripts only. - .hako 側AotPrepで前処理最適化構造のみを行い、LLVM/AOT に渡すIRを軽量にする。
- Preserve default behavior; all risky changes behind dev toggles. - 既定は挙動不変optin。Return 純化ガードで安全性を担保。
- Measure EXE runtime (build once, run many) to avoid toolchain overhead noise.
Targets (initial) チェックリスト
- loop: integer accumulations (no I/O) - [x] パス分割StrlenFold / LoopHoist / ConstDedup / CollectionsHot / BinopCSE
- strlen: FAST=1 path (pointer → nyrt_string_length) - [x] CollectionsHotArray/Map導入既定OFF
- box: construct/destroy minimal boxes (String/Integer) - [x] Map key モード `NYASH_AOT_MAP_KEY_MODE={h|i64|hh|auto}`
- [x] LoopHoist v1 / BinopCSE v1最小
- [x] ベンチ `linidx`/`maplin` 追加
- [ ] LoopHoist v2+/* 右項 const の連鎖前出し/fixpoint
- [ ] BinopCSE v2線形 `i*n` 共通化の強化)
- [ ] CollectionsHot v2array index の共通SSA利用
- [ ] Map auto 精緻化_is_const_or_linear の再帰判定)
- [ ] Idempotence置換済みタグで再実行時も不変
- [ ] `arraymap`/`matmul` ≤ 125%C基準
Methodology トグル
- Build once via ny-llvmc; time execution only (`--exe` mode). - `NYASH_MIR_LOOP_HOIST=1` … StrlenFold/LoopHoist/ConstDedup/BinopCSE を有効化
- Runs: 35; report median and average (target ≥ 100ms per run). - `NYASH_AOT_COLLECTIONS_HOT=1` … CollectionsHotArray/Map
- Observe NYASH_VM_STATS=1 (inst/compare/branch) where relevant to correlate structure and runtime. - `NYASH_AOT_MAP_KEY_MODE``h|i64|hh|auto`(推奨: `auto`
- `NYASH_VERIFY_RET_PURITY=1` … Return 純化ガード開発時ON
Commands (examples)
- tools/perf/phase215/bench_loop.sh --runs 5
- tools/perf/phase215/bench_strlen.sh --runs 5 --fast 1
- tools/perf/phase215/run_all.sh --runs 5 --timeout 120
Dev Toggles (keep OFF by default)
- NYASH_LLVM_FAST=1 (strlen FAST)
- HAKO_MIR_BUILDER_JSONFRAG_NORMALIZE=1 (normalize)
- HAKO_MIR_BUILDER_NORMALIZE_TAG=1 (tag, test-only)
Exit Criteria
- Representative microbenches stable (≤ 5% variance) and ≥ 80% of C baselines.
- No regression in EXE canaries (loop/print/strlen FAST) and VM parity canaries.
ベンチ(例)
```bash
export NYASH_SKIP_TOML_ENV=1 NYASH_DISABLE_PLUGINS=1 \
NYASH_LLVM_SKIP_BUILD=1 NYASH_LLVM_FAST=1 NYASH_LLVM_FAST_INT=1 \
NYASH_MIR_LOOP_HOIST=1 NYASH_AOT_COLLECTIONS_HOT=1 NYASH_VERIFY_RET_PURITY=1
for c in arraymap matmul sieve linidx maplin; do \
tools/perf/microbench.sh --case $c --exe --runs 3; echo; done
```

View File

@ -36,7 +36,13 @@ Canaries
- tools/dev/stageb_loop_json_canary.sh — Program(JSON) shape for loop(i<n){i=i+1} - tools/dev/stageb_loop_json_canary.sh — Program(JSON) shape for loop(i<n){i=i+1}
- tools/dev/phase216_chain_canary.sh endtoend EXE rc=10 for minimal loop - tools/dev/phase216_chain_canary.sh endtoend EXE rc=10 for minimal loop
Provider Path Notes (Dev)
- Optional normalization for provider output is available via `HAKO_MIR_NORMALIZE_PROVIDER=1`.
- This applies the same JsonFrag normalizer/purifier to MIR(JSON) emitted by the Rust Provider path.
- Keep defaults unchanged; use only during bringup to eliminate retafter effects.
- `HAKO_MIR_BUILDER_LOOP_FORCE_JSONFRAG=1` now shortcircuits both selfhostfirst and providerfirst wrappers to emit a minimal, pure controlflow MIR suitable for EXE build sanity.
- Default OFF; intended for small canaries and performance harness bringup.
Removal Plan for temporary parser fallback Removal Plan for temporary parser fallback
- Once VM/gpos interaction is fixed and parser emits correct loop JSON without guards, - Once VM/gpos interaction is fixed and parser emits correct loop JSON without guards,
remove the conservative fallback in ParserControlBox.parse_loop. remove the conservative fallback in ParserControlBox.parse_loop.

View File

@ -0,0 +1,51 @@
# Phase 21.6 — DualEmit Parity & Cline Readiness
Goal: Produce identical MIR(JSON) from both provider (Rust) and selfhost (Hako) builder paths, measure generation cost, and keep AOT (nyllvmc) fast/green. All work is semanticspreserving; defaults remain unchanged.
## Checklists
- [ ] Dualemit parity on representative apps (MIR(JSON) normalized SHA1 equal)
- [ ] Resolverfirst ON passes quick/integration
- [ ] Selfhostfirst fallback ok (provider/legacy on failure)
- [ ] AOT obj/exe via nyllvmc (crate backend) green
- [ ] Docs updated (bench guides, env vars, quick recipes)
## Scripts
- Dual emit + compare + bench: `tools/perf/dual_emit_compare.sh <input.hako> [rounds]`
- MIR emit bench: `tools/perf/bench_hakorune_emit_mir.sh <input.hako> [rounds]`
- AOT bench: `tools/perf/bench_ny_mir_builder.sh <mir.json> [rounds]`
- MIR diff: `tools/perf/compare_mir_json.sh <a.json> <b.json>`
## Env Knobs
- `HAKO_USING_RESOLVER_FIRST=1` (resolverfirst)
- `HAKO_SELFHOST_BUILDER_FIRST=1` (selfhost→provider→legacy)
- `HAKO_MIR_BUILDER_BOX=hako.mir.builder|min`
- `NYASH_LLVM_BACKEND=crate`nyllvmc
- `HAKO_LLVM_OPT_LEVEL=0|1`AOT O0 既定)
## Benchmarks — Tracking
Record normalized parity and generation times here (edit in place).
Legend: SHA1 = normalized JSON digest; Parity=Yes when SHA1 equal; Times are medians unless noted.
| Benchmark (Hako) | Resolver | Parity | Provider p50 (ms) | Selfhost p50 (ms) | Notes |
|------------------------------------------|----------|--------|-------------------|-------------------|-------|
| apps/examples/json_query/main.hako | off/on | | | | |
| apps/examples/json_pp/main.hako | off/on | | | | |
| apps/examples/json_lint/main.hako | off/on | | | | |
| apps/examples/json_query_min/main.hako | off/on | | | | |
How to fill:
1) Run `tools/perf/dual_emit_compare.sh <file> 5`
2) Copy p50s from the summary lines and mark Parity based on `compare_mir_json.sh` output.
3) Note any diffs (callee kinds/order/phi/meta) in Notes.
## Next Steps
- [ ] If parity holds on the above set, extend to apps/tests subset
- [ ] If diffs remain, categorize and align either provider or selfhost output
- [ ] Keep AOT line green under `HAKO_LLVM_OPT_LEVEL=0` and optional `=1` spot checks

View File

@ -9,12 +9,15 @@ Targets (must be green)
Canaries Canaries
- tools/dev/phase216_chain_canary_call.sh — remains PASS when OFF, PASS when ON - tools/dev/phase216_chain_canary_call.sh — remains PASS when OFF, PASS when ON
- Add: tools/dev/phase217_methodize_canary.sh (dev) — asserts method callee usage in MIR or IR tags - tools/dev/phase217_methodize_canary.sh (dev) — compile-run rc=5セマンティクス
- tools/dev/phase217_methodize_json_canary.sh (dev) — v1 rootschema_version+ mir_call presentMethodが望ましい、Globalは経過容認
Toggles Toggles
- HAKO_MIR_BUILDER_METHODIZE=1 (new) - HAKO_MIR_BUILDER_METHODIZE=1 (new)
- HAKO_STAGEB_FUNC_SCAN=1 / HAKO_MIR_BUILDER_FUNCS=1 / HAKO_MIR_BUILDER_CALL_RESOLVE=1 (existing) - HAKO_STAGEB_FUNC_SCAN=1 / HAKO_MIR_BUILDER_FUNCS=1 / HAKO_MIR_BUILDER_CALL_RESOLVE=1 (existing)
- 一軍devプロファイルで既定ON: 上記3つ + NYASH_JSON_SCHEMA_V1=1 + NYASH_MIR_UNIFIED_CALL=1
- 診断既定OFF: HAKO_BRIDGE_METHODIZE=1core_bridge側の補助。Hako既定化後に撤去
Rollback Rollback
- Disable HAKO_MIR_BUILDER_METHODIZE; remove methodization rewrite; keep Global path active. - Disable HAKO_MIR_BUILDER_METHODIZE; remove methodization rewrite; keep Global path active.
- core_bridgeの methodize ブリッジは Hako側が既定化され次第、撤去タグ: [bridge/methodize:*] を一時観測可能にして差分検知)

View File

@ -0,0 +1,67 @@
# Benchmarking MIR Generation & AOT (Quickstart)
This guide shows how to measure Hakorune's MIR emit (StageB → MIR(JSON)) and AOT build (MIR(JSON) → obj/exe) without llvmlite. All commands are semanticspreserving and keep defaults conservative (failfast, O0).
## Prerequisites
- Build binaries once (release):
- `cargo build --release`
- `cargo build --release -p nyash-llvm-compiler` (ny-llvmc)
- `cargo build --release -p nyash_kernel` (NyRT static runtime)
## Scripts
### 1) MIR emit bench (StageB → MIR(JSON))
- Script: `tools/perf/bench_hakorune_emit_mir.sh`
- Usage: `tools/perf/bench_hakorune_emit_mir.sh <input.hako> [rounds]`
- Output CSV: `round,ms,size,sha1` (sha1 is normalized JSON digest; identical = structure equal)
- Useful env toggles:
- `HAKO_USING_RESOLVER_FIRST=1` (resolverfirst)
- `HAKO_SELFHOST_BUILDER_FIRST=1` (selfhost builder → provider fallback)
- `HAKO_MIR_BUILDER_BOX=hako.mir.builder|min` (builder selector)
- `HAKO_SELFHOST_TRACE=1` (stderr trace)
Example:
```
tools/perf/bench_hakorune_emit_mir.sh apps/examples/json_query/main.hako 5
```
### 2) MIR(JSON) → obj/exe benchny-llvmc / crate backend
- Script: `tools/perf/bench_ny_mir_builder.sh`
- Usage: `tools/perf/bench_ny_mir_builder.sh <mir.json> [rounds]`
- Output CSV: `kind,round,ms`kind = obj|exe
- Useful env toggles:
- `NYASH_LLVM_BACKEND=crate`既定。ny-llvmc を使う)
- `HAKO_LLVM_OPT_LEVEL=0|1`(既定は 0O0
Example:
```
tools/perf/bench_ny_mir_builder.sh /path/to/out.json 3
```
### 3) MIR(JSON) 構造比較
- Script: `tools/perf/compare_mir_json.sh`
- Usage: `tools/perf/compare_mir_json.sh <a.json> <b.json>`
- 出力: サイズと正規化 SHA1、差分jq -S 利用時は整形差分)。
## Typical Workflow
1) Emit MIR(JSON)
- `tools/hakorune_emit_mir.sh apps/APP/main.hako out.json`
2) Measure MIR emit time
- `HAKO_USING_RESOLVER_FIRST=1 tools/perf/bench_hakorune_emit_mir.sh apps/APP/main.hako 5`
3) Measure AOTobj/exe
- `NYASH_LLVM_BACKEND=crate tools/perf/bench_ny_mir_builder.sh out.json 3`
4) Compare MIR outputs across toggles/branches
- `tools/perf/compare_mir_json.sh out_before.json out_after.json`
## Notes
- All benches are besteffort micromeasurements; run multiple rounds and compare medians.
- Keep defaults strict: resolver/selfhost togglesは明示時のみON。AOTは O0 既定(`HAKO_LLVM_OPT_LEVEL` で上げられます)。

View File

@ -0,0 +1,12 @@
Plugins Autoload Sample
How to run (from repo root):
- Build core: `cargo build --release`
- Enable autoload and run:
`NYASH_USING_DYLIB_AUTOLOAD=1 ./target/release/hakorune examples/plugins/autoload_sample/main.hako`
Notes
- The plugin path in nyash.toml points to `plugins/nyash-counter-plugin/*`. Ensure the shared library exists.
- If missing, build plugins with `cargo build --release -p nyash-counter-plugin`.
- Autoload is guarded; respects `NYASH_DISABLE_PLUGINS=1`.

View File

@ -0,0 +1,13 @@
using counter
static box Main {
main() {
// Minimal plugin usage example (CounterBox from nyash-counter-plugin)
local c = new CounterBox()
c.inc()
c.inc()
print("Counter value: " + c.get())
return 0
}
}

View File

@ -0,0 +1,9 @@
[using.counter]
kind = "dylib"
# Adjust the path if running from another directory
path = "../../plugins/nyash-counter-plugin/libnyash_counter_plugin.so"
bid = "CounterBox"
[using]
paths = ["lib"]

View File

@ -7,6 +7,25 @@
using selfhost.shared.mir.io as MirIoBox using selfhost.shared.mir.io as MirIoBox
using selfhost.shared.common.string_helpers as StringHelpers using selfhost.shared.common.string_helpers as StringHelpers
using selfhost.shared.json.utils.json_frag as JsonFragBox
// Modular normalizers (opt-in, default OFF)
using selfhost.llvm.ir.normalize.print as NormalizePrintBox
using selfhost.llvm.ir.normalize.ref as NormalizeRefBox
using selfhost.llvm.ir.normalize.array_legacy as NormalizeArrayLegacyBox
// Externalized core passes (Stage-1: wrappers; impl move is incremental)
using selfhost.llvm.ir.aot_prep.passes.strlen as AotPrepStrlenBox
using selfhost.llvm.ir.aot_prep.passes.loop_hoist as AotPrepLoopHoistBox
using selfhost.llvm.ir.aot_prep.passes.const_dedup as AotPrepConstDedupBox
using selfhost.llvm.ir.aot_prep.passes.binop_cse as AotPrepBinopCSEBox
using selfhost.llvm.ir.aot_prep.passes.collections_hot as AotPrepCollectionsHotBox
using selfhost.llvm.ir.aot_prep.passes.fold_const_ret as AotPrepFoldConstRetBox
// Pass modules (logical分割; 将来はファイル分割へ移行)
static box AotPrepPass_Strlen { run(json) { return AotPrepStrlenBox.run(json) } }
static box AotPrepPass_LoopHoist { run(json) { return AotPrepLoopHoistBox.run(json) } }
static box AotPrepPass_ConstDedup { run(json) { return AotPrepConstDedupBox.run(json) } }
static box AotPrepPass_CollectionsHot { run(json) { return AotPrepCollectionsHotBox.run(json) } }
static box AotPrepPass_BinopCSE { run(json) { return AotPrepBinopCSEBox.run(json) } }
static box AotPrepBox { static box AotPrepBox {
// AotPrepBox.prep // AotPrepBox.prep
@ -30,6 +49,40 @@ static box AotPrepBox {
local out = AotPrepBox._try_fold_const_binop_ret(canon) local out = AotPrepBox._try_fold_const_binop_ret(canon)
if !out { out = canon } if !out { out = canon }
// Phase3 (opt-in): 軽いホイスティング/ホットパス
// - ループ内の固定文字列 length/len の即値化(実質的な前計算)
// - Array/Map の get/set/push/len を AOT 向け externcall に張替え(最短経路)
// 既定はOFF。開発時のみ明示フラグで有効化する。
local do_hoist = (env.get("NYASH_MIR_LOOP_HOIST") == "1") || (env.get("NYASH_LLVM_FAST") == "1")
if do_hoist {
local h = AotPrepPass_Strlen.run(out)
if h != null && h != "" { out = h }
local l = AotPrepPass_LoopHoist.run(out)
if l != null && l != "" { out = l }
local d = AotPrepPass_ConstDedup.run(out)
if d != null && d != "" { out = d }
local c = AotPrepPass_BinopCSE.run(out)
if c != null && c != "" { out = c }
}
if env.get("NYASH_AOT_COLLECTIONS_HOT") == "1" {
local r = AotPrepPass_CollectionsHot.run(out)
if r != null && r != "" { out = r }
}
// Phase3.5 (opt-in): 正規化Rust normalize.rs からの移行先既定OFF
if env.get("HAKO_MIR_NORMALIZE_PRINT") == "1" {
local np = NormalizePrintBox.run(out)
if np != null && np != "" { out = np }
}
if env.get("HAKO_MIR_NORMALIZE_REF") == "1" {
local nr = NormalizeRefBox.run(out)
if nr != null && nr != "" { out = nr }
}
if env.get("HAKO_MIR_NORMALIZE_ARRAY") == "1" {
local na = NormalizeArrayLegacyBox.run(out)
if na != null && na != "" { out = na }
}
// Decide output path // Decide output path
local out_path = json_in_path + ".prep.json" local out_path = json_in_path + ".prep.json"
fb.open(out_path, "w") fb.open(out_path, "w")
@ -38,6 +91,225 @@ static box AotPrepBox {
return out_path return out_path
} }
_is_const_or_linear(json, vid) {
if vid == "" { return false }
return AotPrepBox._linear_expr(json, vid, 0)
}
_is_const_vid(json, vid) {
if vid == "" { return false }
local needle = "\"dst\":" + vid
local pos = JsonFragBox.index_of_from(json, needle, 0)
while pos >= 0 {
local start = json.lastIndexOf("{", pos)
if start < 0 { break }
local end = AotPrepBox._seek_object_end(json, start)
if end < 0 { break }
local inst = json.substring(start, end+1)
if inst.indexOf("\"op\":\"const\"") >= 0 { return true }
if inst.indexOf("\"op\":\"copy\"") >= 0 {
// copy 連鎖を辿って const を認定
local ksrc = inst.indexOf("\"src\":")
if ksrc >= 0 {
local svid = StringHelpers.read_digits(inst, ksrc + 6)
if svid != "" { return AotPrepBox._is_const_vid(json, svid) }
}
}
pos = JsonFragBox.index_of_from(json, needle, pos + 1)
}
return false
}
_linear_expr(json, vid, depth) {
if vid == "" { return false }
if depth > 10 { return false }
if AotPrepBox._is_const_vid(json, vid) { return true }
local needle = "\"dst\":" + vid
local pos = JsonFragBox.index_of_from(json, needle, 0)
while pos >= 0 {
local start = json.lastIndexOf("{", pos)
if start < 0 { break }
local end = AotPrepBox._seek_object_end(json, start)
if end < 0 { break }
local inst = json.substring(start, end+1)
// copy は線形性をそのまま伝播
if inst.indexOf("\"op\":\"copy\"") >= 0 {
local ksrc = inst.indexOf("\"src\":")
if ksrc >= 0 {
local svid = StringHelpers.read_digits(inst, ksrc + 6)
if svid != "" { return AotPrepBox._linear_expr(json, svid, depth + 1) }
}
}
if inst.indexOf("\"op\":\"binop\"") >= 0 {
local lhs_pos = inst.indexOf("\"lhs\":")
local rhs_pos = inst.indexOf("\"rhs\":")
local op_key = inst.indexOf("\"operation\":\"")
if lhs_pos >= 0 && rhs_pos >= 0 && op_key >= 0 {
local lhs = StringHelpers.read_digits(inst, lhs_pos + 6)
local rhs = StringHelpers.read_digits(inst, rhs_pos + 6)
local op = JsonFragBox.read_string_after(inst, op_key + 13)
if lhs != "" && rhs != "" && (op == "+" || op == "-" || op == "*" || op == "add" || op == "sub" || op == "mul") {
if AotPrepBox._is_const_vid(json, lhs) && AotPrepBox._linear_expr(json, rhs, depth + 1) { return true }
if AotPrepBox._is_const_vid(json, rhs) && AotPrepBox._linear_expr(json, lhs, depth + 1) { return true }
}
// allow div/rem with const operand as linear for heuristics
if lhs != "" && rhs != "" && (op == "/" || op == "div" || op == "sdiv" || op == "%" || op == "rem" || op == "srem") {
// only accept when one side is a const and the other side is linear
if (op == "/" || op == "div" || op == "sdiv") {
if AotPrepBox._linear_expr(json, lhs, depth + 1) && AotPrepBox._is_const_vid(json, rhs) { return true }
if AotPrepBox._is_const_vid(json, lhs) && AotPrepBox._linear_expr(json, rhs, depth + 1) { return true }
} else {
// rem: treat similarly (mod by const)
if AotPrepBox._linear_expr(json, lhs, depth + 1) && AotPrepBox._is_const_vid(json, rhs) { return true }
}
}
}
}
pos = JsonFragBox.index_of_from(json, needle, pos + 1)
}
return false
}
// ループ内 mod/div/compare の定数をブロック先頭へ hoistJSON文字列ベース、構造は変えない
local read_field = fun(text, key) {
local needle = "\"" + key + "\":\""
local idx = text.indexOf(needle)
if idx < 0 { return "" }
return JsonFragBox.read_string_after(text, idx + needle.length())
}
local read_digits_field = fun(text, key) {
local needle = "\"" + key + "\":"
local idx = text.indexOf(needle)
if idx < 0 { return "" }
return StringHelpers.read_digits(text, idx + needle.length())
}
local read_const_value = fun(text) {
local needle = "\"value\":{\"type\":\"i64\",\"value\":"
local idx = text.indexOf(needle)
if idx < 0 { return "" }
return StringHelpers.read_digits(text, idx + needle.length())
}
local ref_fields = ["lhs", "rhs", "cond", "target"]
loop(true) {
local key = "\"instructions\":["
local kinst = out.indexOf(key, pos)
if kinst < 0 { break }
local lb = out.indexOf("[", kinst)
if lb < 0 { break }
local rb = JsonFragBox._seek_array_end(out, lb)
if rb < 0 { break }
local body = out.substring(lb+1, rb)
local insts = build_items(body)
local const_defs = {}
local const_vals = {}
for inst in insts {
local op = read_field(inst, "op")
if op == "const" {
local dst = read_digits_field(inst, "dst")
local val = read_const_value(inst)
if dst != "" && val != "" {
const_defs[dst] = inst
const_vals[dst] = val
}
}
}
local folded = true
while folded {
folded = false
for inst in insts {
local op = read_field(inst, "op")
if op != "binop" { continue }
local dst = read_digits_field(inst, "dst")
if dst == "" || const_vals.contains(dst) { continue }
local lhs = read_digits_field(inst, "lhs")
local rhs = read_digits_field(inst, "rhs")
local operation = read_field(inst, "operation")
if lhs == "" || rhs == "" || operation == "" { continue }
local lhs_val = const_vals.contains(lhs) ? const_vals[lhs] : ""
local rhs_val = const_vals.contains(rhs) ? const_vals[rhs] : ""
if lhs_val == "" || rhs_val == "" { continue }
local computed = AotPrepBox._evaluate_binop_constant(operation, lhs_val, rhs_val)
if computed == "" { continue }
const_defs[dst] = inst
const_vals[dst] = computed
folded = true
}
}
local needed = {}
for inst in insts {
local op = read_field(inst, "op")
if op == "const" { continue }
for field in ref_fields {
local ref = read_digits_field(inst, field)
if ref != "" && const_defs.contains(ref) {
needed[ref] = true
}
}
}
if needed.size() == 0 {
pos = rb + 1
continue
}
local hoist_items = []
local keep_items = []
for inst in insts {
local dst = read_digits_field(inst, "dst")
if dst != "" && needed.contains(dst) && const_defs.contains(dst) {
hoist_items.push(inst)
continue
}
keep_items.push(inst)
}
if hoist_items.size() == 0 {
pos = rb + 1
continue
}
local merged = ""
local first = 1
local append_item = fun(item) {
if first == 0 { merged = merged + "," }
merged = merged + item
first = 0
}
for item in hoist_items { append_item(item) }
for item in keep_items { append_item(item) }
out = out.substring(0, lb+1) + merged + out.substring(rb, out.length())
pos = lb + merged.length() + 1
}
return out
}
_evaluate_binop_constant(operation, lhs_val, rhs_val) {
if operation == "" { return "" }
local li = StringHelpers.to_i64(lhs_val)
local ri = StringHelpers.to_i64(rhs_val)
if li == null || ri == null { return "" }
local res = null
if operation == "add" || operation == "+" {
res = li + ri
} else if operation == "sub" || operation == "-" {
res = li - ri
} else if operation == "mul" || operation == "*" {
res = li * ri
} else if operation == "sdiv" || operation == "div" || operation == "/" {
if ri == 0 { return "" }
res = li / ri
} else if operation == "srem" || operation == "rem" || operation == "%" {
if ri == 0 { return "" }
res = li % ri
} else {
return ""
}
return StringHelpers.int_to_str(res)
}
// 内部: 最小の安全畳み込みJSON文字列ベース // 内部: 最小の安全畳み込みJSON文字列ベース
_try_fold_const_binop_ret(json) { _try_fold_const_binop_ret(json) {
if !json { return null } if !json { return null }
@ -159,4 +431,67 @@ static box AotPrepBox {
} }
return null return null
} }
// 軽ホイスティング1: 固定文字列の length/len を即値に置換JSON文字列書換、構造変更なし
// Array/Map hot path: boxcall を externcall に張替えAOT専用・診断OFFの最短経路
// JSON 内のオブジェクト { ... } の終了位置を探索(入れ子/文字列・エスケープ対応)
_seek_object_end(s, start) {
if s == null { return -1 }
if start < 0 || start >= s.length() { return -1 }
if s.substring(start, start+1) != "{" { return -1 }
local i = start
local depth = 0
local in_str = 0
local esc = 0
loop (i < s.length()) {
local ch = s.substring(i, i+1)
if in_str == 1 {
if esc == 1 { esc = 0 }
else if ch == "\\" { esc = 1 }
else if ch == "\"" { in_str = 0 }
} else {
if ch == "\"" { in_str = 1 }
else if ch == "{" { depth = depth + 1 }
else if ch == "}" {
depth = depth - 1
if depth == 0 { return i }
}
}
i = i + 1
}
return -1
}
// AotPrepBox.run_json — E2E (no FileBox). Input: JSON text; Output: JSON text after opt-in passes.
run_json(src_json) {
if src_json == null { return null }
// Phase1: 文字列正規化(安定化)
local out = MirIoBox.normalize("" + src_json)
// Phase2: 安全な単一ブロック const/binop(+,-,*)/ret の畳み込み(最小実装)
local tmp = AotPrepBox._try_fold_const_binop_ret(out)
if tmp != null && tmp != "" { out = tmp }
// Phase3: 軽いホイスティング/ホットパスoptin
local do_hoist = (env.get("NYASH_MIR_LOOP_HOIST") == "1") || (env.get("NYASH_LLVM_FAST") == "1")
if do_hoist {
local h = AotPrepPass_Strlen.run(out); if h != null && h != "" { out = h }
local l = AotPrepPass_LoopHoist.run(out); if l != null && l != "" { out = l }
local d = AotPrepPass_ConstDedup.run(out); if d != null && d != "" { out = d }
local c = AotPrepPass_BinopCSE.run(out); if c != null && c != "" { out = c }
}
if env.get("NYASH_AOT_COLLECTIONS_HOT") == "1" {
local r = AotPrepPass_CollectionsHot.run(out); if r != null && r != "" { out = r }
}
if env.get("HAKO_MIR_NORMALIZE_PRINT") == "1" {
local np = NormalizePrintBox.run(out); if np != null && np != "" { out = np }
}
if env.get("HAKO_MIR_NORMALIZE_REF") == "1" {
local nr = NormalizeRefBox.run(out); if nr != null && nr != "" { out = nr }
}
if env.get("HAKO_MIR_NORMALIZE_ARRAY") == "1" {
local na = NormalizeArrayLegacyBox.run(out); if na != null && na != "" { out = na }
}
return out
}
} }

View File

@ -0,0 +1,40 @@
# AotPrep Passes (PreAOT Normalization)
目的: MIR(JSON v0) を安全な範囲で前処理し、LLVM/AOT に渡す前の負荷を軽減します。
方針
- 既定は挙動不変・最小。すべて optin の ENV トグルで有効化します。
- バックエンド非依存の“構造正規化”のみを行い、意味論を変えません。
- 将来的にファイル分割passes/*)へ移行できるよう、パスの役割を明確化します。
主なパス(現在)
- StrlenFold`NYASH_LLVM_FAST=1` or `NYASH_MIR_LOOP_HOIST=1`
- 固定文字列由来の `length/len` を i64 即値に置換
- LoopHoist`NYASH_MIR_LOOP_HOIST=1`
- mod/div/compare の右辺にぶら下がる定数 `const` をブロック先頭へ移動
- 同一ブロック i64 の `const` をデデュープ
- CollectionsHot`NYASH_AOT_COLLECTIONS_HOT=1`
- Array/Map の単純 `boxcall``externcall``nyash.array.*` / `nyash.map.*`)へ張替え
- Map キー戦略は `NYASH_AOT_MAP_KEY_MODE={h|i64|hh|auto}`(既定: `h`/`i64`
- BinopCSE`NYASH_MIR_LOOP_HOIST=1`
- 同一 binop加算/乗算を含むを1回だけ発行し、`copy` で再利用することで線形インデックス(例: `i*n+k`)の再計算を抑制
- `LoopHoist` と組み合わせることで `const` + `binop` の共通部分を前出しする汎用CSEになっている
ENV トグル一覧
- `NYASH_MIR_LOOP_HOIST=1` … StrlenFold + LoopHoist + ConstDedup を有効化
- `NYASH_AOT_COLLECTIONS_HOT=1` … Array/Map hot-path 置換を有効化
- `NYASH_AOT_MAP_KEY_MODE``h`/`i64`(既定), `hh`, `auto`(将来拡張)
- `auto` モードでは `_is_const_or_linear` で args を走査し、単純な i64 定数/線形表現と判定できる場合に `nyash.map.*_h` を選ぶ(それ以外は `*_hh`)。
- `NYASH_VERIFY_RET_PURITY=1` … Return 直前の副作用を Fail-Fast で検出。ベンチはこのトグルをオンにした状態で回しています。
使い方(例)
```
export NYASH_SKIP_TOML_ENV=1 NYASH_DISABLE_PLUGINS=1 \
NYASH_LLVM_SKIP_BUILD=1 NYASH_LLVM_FAST=1 NYASH_LLVM_FAST_INT=1 \
NYASH_MIR_LOOP_HOIST=1 NYASH_AOT_COLLECTIONS_HOT=1
tools/perf/microbench.sh --case arraymap --exe --runs 3
```
注意
- すべて optin。CI/既定ユーザ挙動は従来どおりです。
- Return 純化ガード(`NYASH_VERIFY_RET_PURITY=1`)と併用してください。

View File

@ -0,0 +1,114 @@
// AotPrepHelpers — shared helpers for AotPrep passes (const/linear checks)
using selfhost.shared.json.utils.json_frag as JsonFragBox
using selfhost.shared.common.string_helpers as StringHelpers
static box AotPrepHelpers {
// Public: Determine if `vid` is a constant or linear i64 expression in JSON(MIR v0)
is_const_or_linear(json, vid) {
if vid == "" { return false }
return me._linear_expr(json, vid, 0)
}
// Public: Is `vid` defined by a const (following copy chains)
is_const_vid(json, vid) {
if vid == "" { return false }
local needle = "\"dst\":" + vid
local pos = JsonFragBox.index_of_from(json, needle, 0)
while pos >= 0 {
local start = json.lastIndexOf("{", pos)
if start < 0 { break }
local end = me._seek_object_end(json, start)
if end < 0 { break }
local inst = json.substring(start, end+1)
if inst.indexOf("\"op\":\"const\"") >= 0 { return true }
if inst.indexOf("\"op\":\"copy\"") >= 0 {
// Follow copy chains to the source
local ksrc = inst.indexOf("\"src\":")
if ksrc >= 0 {
local svid = StringHelpers.read_digits(inst, ksrc + 6)
if svid != "" { return me.is_const_vid(json, svid) }
}
}
pos = JsonFragBox.index_of_from(json, needle, pos + 1)
}
return false
}
// Internal: linear expression checker with small recursion bound
_linear_expr(json, vid, depth) {
if vid == "" { return false }
if depth > 10 { return false }
if me.is_const_vid(json, vid) { return true }
local needle = "\"dst\":" + vid
local pos = JsonFragBox.index_of_from(json, needle, 0)
while pos >= 0 {
local start = json.lastIndexOf("{", pos)
if start < 0 { break }
local end = me._seek_object_end(json, start)
if end < 0 { break }
local inst = json.substring(start, end+1)
// copy: propagate linearity
if inst.indexOf("\"op\":\"copy\"") >= 0 {
local ksrc = inst.indexOf("\"src\":")
if ksrc >= 0 {
local svid = StringHelpers.read_digits(inst, ksrc + 6)
if svid != "" { return me._linear_expr(json, svid, depth + 1) }
}
}
if inst.indexOf("\"op\":\"binop\"") >= 0 {
local lhs_pos = inst.indexOf("\"lhs\":")
local rhs_pos = inst.indexOf("\"rhs\":")
local op_key = inst.indexOf("\"operation\":\"")
if lhs_pos >= 0 && rhs_pos >= 0 && op_key >= 0 {
local lhs = StringHelpers.read_digits(inst, lhs_pos + 6)
local rhs = StringHelpers.read_digits(inst, rhs_pos + 6)
local op = JsonFragBox.read_string_after(inst, op_key + 13)
// Treat +,-,* with one const and one linear as linear
if lhs != "" && rhs != "" && (op == "+" || op == "-" || op == "*" || op == "add" || op == "sub" || op == "mul") {
if me.is_const_vid(json, lhs) && me._linear_expr(json, rhs, depth + 1) { return true }
if me.is_const_vid(json, rhs) && me._linear_expr(json, lhs, depth + 1) { return true }
}
// Heuristic: allow div/rem with a const side as linear
if lhs != "" && rhs != "" && (op == "/" || op == "div" || op == "sdiv" || op == "%" || op == "rem" || op == "srem") {
// div: either linear/const or const/linear
if (op == "/" || op == "div" || op == "sdiv") {
if me._linear_expr(json, lhs, depth + 1) && me.is_const_vid(json, rhs) { return true }
if me.is_const_vid(json, lhs) && me._linear_expr(json, rhs, depth + 1) { return true }
} else {
// rem: only accept linear % const (mod by const)
if me._linear_expr(json, lhs, depth + 1) && me.is_const_vid(json, rhs) { return true }
}
}
}
}
pos = JsonFragBox.index_of_from(json, needle, pos + 1)
}
return false
}
// Minimal object end seeker (duplicated here to avoid box coupling)
_seek_object_end(s, start) {
if s == null { return -1 }
if start < 0 || start >= s.length() { return -1 }
if s.substring(start, start+1) != "{" { return -1 }
local i = start
local depth = 0
local in_str = 0
local esc = 0
loop (i < s.length()) {
local ch = s.substring(i, i+1)
if in_str == 1 {
if esc == 1 { esc = 0 }
else if ch == "\\" { esc = 1 }
else if ch == "\"" { in_str = 0 }
} else {
if ch == "\"" { in_str = 1 }
else if ch == "{" { depth = depth + 1 }
else if ch == "}" { depth = depth - 1 if depth == 0 { return i } }
}
i = i + 1
}
return -1
}
}

View File

@ -0,0 +1,141 @@
// AotPrepBinopCSEBox — common subexpression elimination for binops (text-level)
using selfhost.shared.json.utils.json_frag as JsonFragBox
using selfhost.shared.common.string_helpers as StringHelpers
static box AotPrepBinopCSEBox {
run(json) {
if json == null { return null }
local pos = 0
local out = json
local read_field = fun(text, key) {
local needle = "\"" + key + "\":\""
local idx = text.indexOf(needle)
if idx < 0 { return "" }
return JsonFragBox.read_string_after(text, idx + needle.length())
}
local read_digits_field = fun(text, key) {
local needle = "\"" + key + "\":"
local idx = text.indexOf(needle)
if idx < 0 { return "" }
return StringHelpers.read_digits(text, idx + needle.length())
}
loop(true) {
local key = "\"instructions\":["
local kinst = out.indexOf(key, pos)
if kinst < 0 { break }
local lb = out.indexOf("[", kinst)
if lb < 0 { break }
local rb = JsonFragBox._seek_array_end(out, lb)
if rb < 0 { break }
local body = out.substring(lb+1, rb)
local insts = []
local i = 0
loop(true) {
local os = body.indexOf("{", i)
if os < 0 { break }
local oe = me._seek_object_end(body, os)
if oe < 0 { break }
insts.push(body.substring(os, oe+1))
i = oe + 1
}
local copy_src = {}
local new_body = ""
local first = 1
local append_item = fun(item) {
if first == 0 { new_body = new_body + "," }
new_body = new_body + item
first = 0
}
for inst in insts {
local op = read_field(inst, "op")
if op == "copy" {
local dst = read_digits_field(inst, "dst")
local src = read_digits_field(inst, "src")
if dst != "" && src != "" {
copy_src[dst] = src
}
}
}
local resolve_copy = fun(vid) {
local current = vid
local depth = 0
loop(true) {
if current == "" { break }
if !copy_src.contains(current) { break }
current = copy_src[current]
depth = depth + 1
if depth >= 12 { break }
}
return current
}
local canon_binop = fun(op, lhs, rhs) {
if lhs == "" || rhs == "" { return "" }
local key_lhs = resolve_copy(lhs)
local key_rhs = resolve_copy(rhs)
if key_lhs == "" || key_rhs == "" { return "" }
if op == "+" || op == "add" || op == "*" || op == "mul" {
local li = StringHelpers.to_i64(key_lhs)
local ri = StringHelpers.to_i64(key_rhs)
if li != null && ri != null && ri < li {
local tmp = key_lhs
key_lhs = key_rhs
key_rhs = tmp
}
}
return op + ":" + key_lhs + ":" + key_rhs
}
for inst in insts {
local op = read_field(inst, "op")
if op == "binop" {
local operation = read_field(inst, "operation")
local lhs = read_digits_field(inst, "lhs")
local rhs = read_digits_field(inst, "rhs")
local key = canon_binop(operation, lhs, rhs)
if key != "" && seen.contains(key) {
local dst = read_digits_field(inst, "dst")
if dst != "" {
append_item("{\"op\":\"copy\",\"dst\":" + dst + ",\"src\":" + seen[key] + "}")
continue
}
} else if key != "" {
local dst = read_digits_field(inst, "dst")
if dst != "" {
seen[key] = dst
}
}
}
append_item(inst)
}
out = out.substring(0, lb+1) + new_body + out.substring(rb, out.length())
pos = lb + new_body.length() + 1
}
return out
}
_seek_object_end(s, start) {
if s == null { return -1 }
if start < 0 || start >= s.length() { return -1 }
if s.substring(start, start+1) != "{" { return -1 }
local i = start
local depth = 0
local in_str = 0
local esc = 0
loop (i < s.length()) {
local ch = s.substring(i, i+1)
if in_str == 1 {
if esc == 1 { esc = 0 }
else if ch == "\\" { esc = 1 }
else if ch == "\"" { in_str = 0 }
} else {
if ch == "\"" { in_str = 1 }
else if ch == "{" { depth = depth + 1 }
else if ch == "}" {
depth = depth - 1
if depth == 0 { return i }
}
}
i = i + 1
}
return -1
}
}

View File

@ -0,0 +1,226 @@
// AotPrepCollectionsHotBox — rewrite Array/Map boxcall to externcall hot paths (AOT-only)
using selfhost.shared.json.utils.json_frag as JsonFragBox
using selfhost.shared.common.string_helpers as StringHelpers
using selfhost.llvm.ir.aot_prep.helpers.common as AotPrepHelpers // for is_const_or_linear
static box AotPrepCollectionsHotBox {
run(json) {
if json == null { return null }
local arr_recv = {}
local map_recv = {}
local copy_src = {}
{
local pos = 0
loop(true){
local k = JsonFragBox.index_of_from(json, "\"op\":\"newbox\"", pos)
if k < 0 { break }
local kdst = JsonFragBox.index_of_from(json, "\"dst\":", k)
local dsts = StringHelpers.read_digits(json, kdst+6)
if dsts != "" {
if JsonFragBox.index_of_from(json, "\"type\":\"ArrayBox\"", k) >= 0 { arr_recv[dsts] = 1 }
if JsonFragBox.index_of_from(json, "\"type\":\"MapBox\"", k) >= 0 { map_recv[dsts] = 1 }
}
pos = k + 1
}
pos = 0
loop(true){
local k = JsonFragBox.index_of_from(json, "\"op\":\"copy\"", pos)
if k < 0 { break }
local kdst = JsonFragBox.index_of_from(json, "\"dst\":", k)
local ksrc = JsonFragBox.index_of_from(json, "\"src\":", k)
if kdst >= 0 && ksrc >= 0 {
local dsts = StringHelpers.read_digits(json, kdst+6)
local srcs = StringHelpers.read_digits(json, ksrc+6)
if dsts != "" && srcs != "" { copy_src[dsts] = srcs }
}
pos = k + 1
}
}
if arr_recv.size() == 0 && map_recv.size() == 0 { return json }
local out = json
local key_mode = env.get("NYASH_AOT_MAP_KEY_MODE")
local find_block_span = fun(text, p) {
local key = "\"instructions\":["
local start = text.lastIndexOf(key, p)
if start < 0 { return [-1, -1] }
local lb = text.indexOf("[", start)
if lb < 0 { return [-1, -1] }
local rb = JsonFragBox._seek_array_end(text, lb)
if rb < 0 { return [-1, -1] }
return [lb, rb]
}
local resolve_copy = fun(vid) {
local cur = vid
local depth = 0
loop(true){ if cur == "" { break } if !copy_src.contains(cur) { break } cur = copy_src[cur]; depth = depth + 1; if depth >= 12 { break } }
return cur
}
local expr_key_in_block = fun(text, block_lb, k, vid) {
if vid == "" { return "" }
local needle = "\"dst\":" + vid
local pos = JsonFragBox.index_of_from(text, needle, block_lb)
local last = -1
while pos >= 0 && pos < k { last = pos; pos = JsonFragBox.index_of_from(text, needle, pos + 1) }
if last < 0 { return "" }
local os = text.lastIndexOf("{", last)
if os < 0 { return "" }
local oe = me._seek_object_end(text, os)
if oe < 0 || oe >= k { return "" }
local inst = text.substring(os, oe+1)
if inst.indexOf("\"op\":\"const\"") >= 0 {
local kval = inst.indexOf("\"value\":{\"type\":\"i64\",\"value\":")
local val = (kval>=0 ? StringHelpers.read_digits(inst, kval+30) : "")
if val != "" { return "const:" + val }
}
if inst.indexOf("\"op\":\"copy\"") >= 0 {
local ksrc = inst.indexOf("\"src\":")
local svid = (ksrc>=0 ? StringHelpers.read_digits(inst, ksrc+6) : "")
if svid != "" { return "copy:" + resolve_copy(svid) }
}
if inst.indexOf("\"op\":\"binop\"") >= 0 {
local lhs_pos = inst.indexOf("\"lhs\":")
local rhs_pos = inst.indexOf("\"rhs\":")
local op_key = inst.indexOf("\"operation\":\"")
if lhs_pos >= 0 && rhs_pos >= 0 && op_key >= 0 {
local lhs = StringHelpers.read_digits(inst, lhs_pos + 6)
local rhs = StringHelpers.read_digits(inst, rhs_pos + 6)
local op = JsonFragBox.read_string_after(inst, op_key + 13)
lhs = resolve_copy(lhs); rhs = resolve_copy(rhs)
if op == "+" || op == "add" || op == "*" || op == "mul" {
local li = StringHelpers.to_i64(lhs)
local ri = StringHelpers.to_i64(rhs)
if li != null && ri != null && ri < li { local tmp = lhs; lhs = rhs; rhs = tmp }
}
return "binop:" + op + ":" + lhs + ":" + rhs
}
}
return ""
}
// helper: find last set(box,a0,*) for same receiver inside block
local find_last_set_index_in_block = fun(text, block_lb, k, recv_vid) {
if recv_vid == "" { return "" }
// naive scan: look back in block slice for boxcall set with same box
local slice = text.substring(block_lb, k)
local p = slice.lastIndexOf("\"op\":\"boxcall\"")
while p >= 0 {
local abs = block_lb + p
local os = text.lastIndexOf("{", abs)
if os < 0 { break }
local oe = me._seek_object_end(text, os)
if oe < 0 || oe >= k { break }
local inst = text.substring(os, oe+1)
if inst.indexOf("\"method\":\"set\"") >= 0 {
local kbox = inst.indexOf("\"box\":")
local bid = (kbox>=0 ? StringHelpers.read_digits(inst, kbox+6) : "")
if bid == recv_vid {
local kargs = inst.indexOf("\"args\":[")
if kargs >= 0 {
local idx = StringHelpers.read_digits(inst, kargs+8)
if idx != "" { return idx }
}
}
}
// search previous occurrence
p = slice.lastIndexOf("\"op\":\"boxcall\"", p-1)
}
return ""
}
local pos2 = 0
local seen_key_vid = {}
loop(true){
local k = JsonFragBox.index_of_from(out, "\"op\":\"boxcall\"", pos2)
if k < 0 { break }
local kdst = JsonFragBox.index_of_from(out, "\"dst\":", k)
local dvid = (kdst>=0 ? StringHelpers.read_digits(out, kdst+6) : "")
local kbox = JsonFragBox.index_of_from(out, "\"box\":", k)
if kbox < 0 { pos2 = k + 1; continue }
local bvid = StringHelpers.read_digits(out, kbox+6)
if bvid != "" { bvid = resolve_copy(bvid) }
local mend = JsonFragBox.index_of_from(out, "\"method\":\"", k)
if mend < 0 { pos2 = k + 1; continue }
local mname = JsonFragBox.read_string_after(out, mend+10)
local is_arr = arr_recv.contains(bvid)
local is_map = map_recv.contains(bvid)
if !(is_arr || is_map) { pos2 = k + 1; continue }
local kargs = JsonFragBox.index_of_from(out, "\"args\":[", k)
local a0 = ""; local a1 = ""
if kargs >= 0 {
a0 = StringHelpers.read_digits(out, kargs+8)
local sep = out.indexOf(",", kargs)
if sep > 0 { a1 = StringHelpers.read_digits(out, sep+1) }
}
if a0 != "" { a0 = resolve_copy(a0) }
do {
local span = find_block_span(out, k)
local lb = span[0]; local rb = span[1]
if lb >= 0 && rb >= 0 {
local key = expr_key_in_block(out, lb, k, a0)
if key != "" {
local sc_key = (is_map ? "map:" : "arr:") + bvid + "|" + key
if seen_key_vid.contains(sc_key) { a0 = seen_key_vid[sc_key] } else { seen_key_vid[sc_key] = a0 }
} else if is_arr && mname == "get" {
// Safe set->get index reuse inside same block
local prev_idx = find_last_set_index_in_block(out, lb, k, bvid)
if prev_idx != "" { a0 = prev_idx }
}
}
} while(false)
local func = ""; local args = ""
if is_arr {
if mname == "get" { func = "nyash.array.get_h"; args = bvid + "," + a0 }
else if mname == "set" { func = "nyash.array.set_h"; args = bvid + "," + a0 + "," + a1 }
else if mname == "push" { func = "nyash.array.push_h"; args = bvid + "," + a0 }
else if (mname == "len" || mname == "length" || mname == "size") { func = "nyash.array.len_h"; args = bvid }
} else if is_map {
local mode = key_mode
local use_hh = (mode == "hh")
if mode == "auto" { use_hh = !AotPrepHelpers.is_const_or_linear(out, a0) }
if use_hh {
if mname == "get" { func = "nyash.map.get_hh"; args = bvid + "," + a0 }
else if mname == "set" { func = "nyash.map.set_hh"; args = bvid + "," + a0 + "," + a1 }
else if mname == "has" { func = "nyash.map.has_hh"; args = bvid + "," + a0 }
else if (mname == "len" || mname == "length" || mname == "size") { func = "nyash.map.size_h"; args = bvid }
} else {
if mname == "get" { func = "nyash.map.get_h"; args = bvid + "," + a0 }
else if mname == "set" { func = "nyash.map.set_h"; args = bvid + "," + a0 + "," + a1 }
else if mname == "has" { func = "nyash.map.has_h"; args = bvid + "," + a0 }
else if (mname == "len" || mname == "length" || mname == "size") { func = "nyash.map.size_h"; args = bvid }
}
}
if func == "" { pos2 = k + 1; continue }
local obj_start = out.lastIndexOf("{", k)
if obj_start < 0 { pos2 = k + 1; continue }
local obj_end = me._seek_object_end(out, obj_start)
if obj_end < 0 { pos2 = k + 1; continue }
local dst_part = (dvid != "" ? ("\"dst\":" + dvid + ",") : "")
local repl = "{" + dst_part + "\"op\":\"externcall\",\"func\":\"" + func + "\",\"args\":[" + args + "]}"
out = out.substring(0, obj_start) + repl + out.substring(obj_end+1, out.length())
pos2 = obj_start + repl.length()
}
return out
}
_seek_object_end(s, start) {
if s == null { return -1 }
if start < 0 || start >= s.length() { return -1 }
if s.substring(start, start+1) != "{" { return -1 }
local i = start
local depth = 0
local in_str = 0
local esc = 0
loop (i < s.length()) {
local ch = s.substring(i, i+1)
if in_str == 1 {
if esc == 1 { esc = 0 }
else if ch == "\\" { esc = 1 }
else if ch == "\"" { in_str = 0 }
} else {
if ch == "\"" { in_str = 1 }
else if ch == "{" { depth = depth + 1 }
else if ch == "}" { depth = depth - 1 if depth == 0 { return i } }
}
i = i + 1
}
return -1
}
}

View File

@ -0,0 +1,87 @@
using selfhost.shared.json.utils.json_frag as JsonFragBox
using selfhost.shared.common.string_helpers as StringHelpers
static box AotPrepConstDedupBox {
run(json) {
if json == null { return null }
local pos = 0
local out = json
loop(true) {
local kinst = JsonFragBox.index_of_from(out, "\"instructions\"", pos)
if kinst < 0 { break }
local lb = out.indexOf("[", kinst)
if lb < 0 { pos = kinst + 1; continue }
local rb = JsonFragBox._seek_array_end(out, lb)
if rb < 0 { pos = kinst + 1; continue }
local body = out.substring(lb+1, rb)
local i = 0
local new_body = ""
local first_vid_by_value = {}
loop(i < body.length()) {
local os = body.indexOf("{", i)
if os < 0 {
new_body = new_body + body.substring(i, body.length())
break
}
new_body = new_body + body.substring(i, os)
local oe = me._seek_object_end(body, os)
if oe < 0 {
new_body = new_body + body.substring(os, body.length())
break
}
local obj = body.substring(os, oe+1)
if obj.indexOf("\"op\":\"const\"") >= 0 && obj.indexOf("\"type\":\"i64\"") >= 0 {
local kdst = obj.indexOf("\"dst\":")
local dsts = (kdst>=0 ? StringHelpers.read_digits(obj, kdst+6) : "")
local kval = obj.indexOf("\"value\":{\"type\":\"i64\",\"value\":")
local vals = (kval>=0 ? StringHelpers.read_digits(obj, kval+30) : "")
if dsts != "" && vals != "" {
if first_vid_by_value.contains(vals) {
local src = first_vid_by_value[vals]
local repl = "{\"op\":\"copy\",\"dst\":" + dsts + ",\"src\":" + src + "}"
new_body = new_body + repl
} else {
first_vid_by_value[vals] = dsts
new_body = new_body + obj
}
} else {
new_body = new_body + obj
}
} else {
new_body = new_body + obj
}
i = oe + 1
}
out = out.substring(0, lb+1) + new_body + out.substring(rb, out.length())
pos = rb + 1
}
return out
}
_seek_object_end(s, start) {
if s == null { return -1 }
if start < 0 || start >= s.length() { return -1 }
if s.substring(start, start+1) != "{" { return -1 }
local i = start
local depth = 0
local in_str = 0
local esc = 0
loop (i < s.length()) {
local ch = s.substring(i, i+1)
if in_str == 1 {
if esc == 1 { esc = 0 }
else if ch == "\\" { esc = 1 }
else if ch == "\"" { in_str = 0 }
} else {
if ch == "\"" { in_str = 1 }
else if ch == "{" { depth = depth + 1 }
else if ch == "}" {
depth = depth - 1
if depth == 0 { return i }
}
}
i = i + 1
}
return -1
}
}

View File

@ -0,0 +1,4 @@
// AotPrepFoldConstRetBox — Fold simple const/binop/ret single-block cases
using selfhost.llvm.ir.aot_prep as AotPrepBox
static box AotPrepFoldConstRetBox { run(json) { return AotPrepBox._try_fold_const_binop_ret(json) } }

View File

@ -0,0 +1,137 @@
// AotPrepLoopHoistBox — Hoist loop-local consts (binop/div/rem/compare) to block head
using selfhost.shared.json.utils.json_frag as JsonFragBox
using selfhost.shared.common.string_helpers as StringHelpers
using selfhost.llvm.ir.aot_prep as AotPrepBox // for _evaluate_binop_constant
static box AotPrepLoopHoistBox {
run(json) {
if json == null { return null }
local out = json
local pos = 0
local build_items = fun(body) {
local items = []
local i = 0
loop(true) {
local os = body.indexOf("{", i)
if os < 0 { break }
local oe = me._seek_object_end(body, os)
if oe < 0 { break }
items.push(body.substring(os, oe+1))
i = oe + 1
}
return items
}
local read_field = fun(text, key) {
local needle = "\"" + key + "\":\""
local idx = text.indexOf(needle)
if idx < 0 { return "" }
return JsonFragBox.read_string_after(text, idx + needle.length())
}
local read_digits_field = fun(text, key) {
local needle = "\"" + key + "\":"
local idx = text.indexOf(needle)
if idx < 0 { return "" }
return StringHelpers.read_digits(text, idx + needle.length())
}
local read_const_value = fun(text) {
local needle = "\"value\":{\"type\":\"i64\",\"value\":"
local idx = text.indexOf(needle)
if idx < 0 { return "" }
return StringHelpers.read_digits(text, idx + needle.length())
}
local ref_fields = ["lhs", "rhs", "cond", "target"]
loop(true) {
local key = "\"instructions\":["
local kinst = out.indexOf(key, pos)
if kinst < 0 { break }
local lb = out.indexOf("[", kinst)
if lb < 0 { break }
local rb = JsonFragBox._seek_array_end(out, lb)
if rb < 0 { break }
local body = out.substring(lb+1, rb)
local insts = build_items(body)
local const_defs = {}
local const_vals = {}
for inst in insts {
local op = read_field(inst, "op")
if op == "const" {
local dst = read_digits_field(inst, "dst")
local val = read_const_value(inst)
if dst != "" && val != "" { const_defs[dst] = inst; const_vals[dst] = val }
}
}
local folded = true
while folded {
folded = false
for inst in insts {
local op = read_field(inst, "op")
if op != "binop" { continue }
local dst = read_digits_field(inst, "dst")
if dst == "" || const_vals.contains(dst) { continue }
local lhs = read_digits_field(inst, "lhs")
local rhs = read_digits_field(inst, "rhs")
local operation = read_field(inst, "operation")
if lhs == "" || rhs == "" || operation == "" { continue }
local lhs_val = const_vals.contains(lhs) ? const_vals[lhs] : ""
local rhs_val = const_vals.contains(rhs) ? const_vals[rhs] : ""
if lhs_val == "" || rhs_val == "" { continue }
local computed = AotPrepBox._evaluate_binop_constant(operation, lhs_val, rhs_val)
if computed == "" { continue }
const_defs[dst] = inst
const_vals[dst] = computed
folded = true
}
}
local needed = {}
for inst in insts {
local op = read_field(inst, "op")
if op == "const" { continue }
for field in ref_fields {
local ref = read_digits_field(inst, field)
if ref != "" && const_defs.contains(ref) { needed[ref] = true }
}
}
if needed.size() == 0 { pos = rb + 1; continue }
local hoist_items = []
local keep_items = []
for inst in insts {
local dst = read_digits_field(inst, "dst")
if dst != "" && needed.contains(dst) && const_defs.contains(dst) { hoist_items.push(inst); continue }
keep_items.push(inst)
}
if hoist_items.size() == 0 { pos = rb + 1; continue }
local merged = ""
local first = 1
local append_item = fun(item) { if first == 0 { merged = merged + "," } merged = merged + item; first = 0 }
for item in hoist_items { append_item(item) }
for item in keep_items { append_item(item) }
out = out.substring(0, lb+1) + merged + out.substring(rb, out.length())
pos = lb + merged.length() + 1
}
return out
}
_seek_object_end(s, start) {
if s == null { return -1 }
if start < 0 || start >= s.length() { return -1 }
if s.substring(start, start+1) != "{" { return -1 }
local i = start
local depth = 0
local in_str = 0
local esc = 0
loop (i < s.length()) {
local ch = s.substring(i, i+1)
if in_str == 1 {
if esc == 1 { esc = 0 }
else if ch == "\\" { esc = 1 }
else if ch == "\"" { in_str = 0 }
} else {
if ch == "\"" { in_str = 1 }
else if ch == "{" { depth = depth + 1 }
else if ch == "}" { depth = depth - 1 if depth == 0 { return i } }
}
i = i + 1
}
return -1
}
}

View File

@ -0,0 +1,114 @@
// AotPrepStrlenBox — fold length/len for known StringBox receivers (JSON text)
using selfhost.shared.json.utils.json_frag as JsonFragBox
using selfhost.shared.common.string_helpers as StringHelpers
static box AotPrepStrlenBox {
run(json) {
if json == null { return null }
// 1) const string: vid -> byte length
local map_len = {}
{
local pos = 0
loop(true){
local k = JsonFragBox.index_of_from(json, "\"op\":\"const\"", pos)
if k < 0 { break }
local kdst = JsonFragBox.index_of_from(json, "\"dst\":", k)
local kid = StringHelpers.read_digits(json, kdst+6)
if kid != "" {
local ktp = JsonFragBox.index_of_from(json, "\"type\":", k)
if ktp >= 0 {
local is_str = (JsonFragBox.index_of_from(json, "\"type\":\"string\"", ktp) >= 0)
local is_h_str = (JsonFragBox.index_of_from(json, "\"box_type\":\"StringBox\"", ktp) >= 0)
if is_str || is_h_str {
local kv = JsonFragBox.index_of_from(json, "\"value\":\"", ktp)
if kv >= 0 {
local lit = JsonFragBox.read_string_after(json, kv+8)
if lit != null {
local blen = (""+lit).length()
map_len[StringHelpers.int_to_str(StringHelpers.to_i64(kid))] = blen
}
}
}
}
}
pos = k + 1
}
}
if map_len.size() == 0 { return json }
// 2) newbox(StringBox, <const_str_vid>): dst_vid -> byte length
local recv_len = {}
{
local pos2 = 0
loop(true){
local k = JsonFragBox.index_of_from(json, "\"op\":\"newbox\"", pos2)
if k < 0 { break }
local kty = JsonFragBox.index_of_from(json, "\"type\":\"StringBox\"", k)
if kty >= 0 {
local kdst = JsonFragBox.index_of_from(json, "\"dst\":", k)
local dsts = StringHelpers.read_digits(json, kdst+6)
local kargs = JsonFragBox.index_of_from(json, "\"args\":[", k)
if dsts != "" && kargs >= 0 {
local avid = StringHelpers.read_digits(json, kargs+8)
if avid != "" && map_len.contains(avid) {
recv_len[dsts] = map_len[avid]
}
}
}
pos2 = k + 1
}
}
if recv_len.size() == 0 { return json }
// 3) boxcall length/len → const i64
local out = json
local pos3 = 0
loop(true){
local k = JsonFragBox.index_of_from(out, "\"op\":\"boxcall\"", pos3)
if k < 0 { break }
local mend = JsonFragBox.index_of_from(out, "\"method\":\"", k)
if mend < 0 { pos3 = k + 1; continue }
local mname = JsonFragBox.read_string_after(out, mend+10)
if !(mname == "length" || mname == "len") { pos3 = k + 1; continue }
local kbox = JsonFragBox.index_of_from(out, "\"box\":", k)
if kbox < 0 { pos3 = k + 1; continue }
local bvid = StringHelpers.read_digits(out, kbox+6)
if bvid == "" || !recv_len.contains(bvid) { pos3 = k + 1; continue }
local kdst = JsonFragBox.index_of_from(out, "\"dst\":", k)
if kdst < 0 { pos3 = k + 1; continue }
local dvid = StringHelpers.read_digits(out, kdst+6)
if dvid == "" { pos3 = k + 1; continue }
local obj_start = out.lastIndexOf("{", k)
if obj_start < 0 { pos3 = k + 1; continue }
local obj_end = me._seek_object_end(out, obj_start)
if obj_end < 0 { pos3 = k + 1; continue }
local blen = recv_len[bvid]
local repl = "{\"op\":\"const\",\"dst\":" + dvid + ",\"value\":{\"type\":\"i64\",\"value\":" + StringHelpers.int_to_str(blen) + "}}"
out = out.substring(0, obj_start) + repl + out.substring(obj_end+1, out.length())
pos3 = obj_start + repl.length()
}
return out
}
_seek_object_end(s, start) {
if s == null { return -1 }
if start < 0 || start >= s.length() { return -1 }
if s.substring(start, start+1) != "{" { return -1 }
local i = start
local depth = 0
local in_str = 0
local esc = 0
loop (i < s.length()) {
local ch = s.substring(i, i+1)
if in_str == 1 {
if esc == 1 { esc = 0 }
else if ch == "\\" { esc = 1 }
else if ch == "\"" { in_str = 0 }
} else {
if ch == "\"" { in_str = 1 }
else if ch == "{" { depth = depth + 1 }
else if ch == "}" { depth = depth - 1 if depth == 0 { return i } }
}
i = i + 1
}
return -1
}
}

View File

@ -0,0 +1,17 @@
Normalize Passes (AOT Prep)
Purpose
- Provide modular, opt-in JSON v0 normalizers to keep Rust normalize.rs minimal and move pre-ny-llvmc shaping into .hako.
Passes (initial)
- NormalizePrintBox: rewrite `op: print` to `externcall env.console.log(value)`.
- NormalizeRefBox: rewrite `ref_get/ref_set` to `boxcall getField/setField` with a string field operand (best-effort; no CFG change).
Toggles (default OFF)
- HAKO_MIR_NORMALIZE_PRINT=1
- HAKO_MIR_NORMALIZE_REF=1
Notes
- These operate on MIR(JSON v0) text. They are simple, conservative string transforms built on JsonFrag utilities.
- Keep effects and CFG unchanged. For ref_set, inserting a barrier is not enforced here; AOT will later select the shortest extern hot-path.

View File

@ -0,0 +1,129 @@
// NormalizeArrayLegacyBox — rewrite array_get/array_set → boxcall get/set
using selfhost.shared.json.utils.json_frag as JsonFragBox
using selfhost.shared.common.string_helpers as StringHelpers
static box NormalizeArrayLegacyBox {
_seek_object_end(s, start) {
if s == null { return -1 }
if start < 0 || start >= s.length() { return -1 }
if s.substring(start, start+1) != "{" { return -1 }
local i = start
local depth = 0
local in_str = 0
local esc = 0
loop (i < s.length()) {
local ch = s.substring(i, i+1)
if in_str == 1 {
if esc == 1 { esc = 0 }
else if ch == "\\" { esc = 1 }
else if ch == "\"" { in_str = 0 }
} else {
if ch == "\"" { in_str = 1 }
else if ch == "{" { depth = depth + 1 }
else if ch == "}" { depth = depth - 1 if depth == 0 { return i } }
}
i = i + 1
}
return -1
}
run(json) {
if json == null { return null }
local out = json
// array_get → boxcall(get)
local pos = 0
loop(true) {
local k = JsonFragBox.index_of_from(out, "\"op\":\"array_get\"", pos)
if k < 0 { break }
local obj_start = out.lastIndexOf("{", k)
if obj_start < 0 { pos = k + 1; continue }
local obj_end = me._seek_object_end(out, obj_start)
if obj_end < 0 { pos = k + 1; continue }
local kdst = JsonFragBox.index_of_from(out, "\"dst\":", obj_start)
local dsts = (kdst>=0 && kdst<obj_end ? StringHelpers.read_digits(out, kdst+6) : "")
local ka = JsonFragBox.index_of_from(out, "\"array\":", obj_start)
local arr = (ka>=0 && ka<obj_end ? StringHelpers.read_digits(out, ka+8) : "")
local ki = JsonFragBox.index_of_from(out, "\"index\":", obj_start)
local idx = (ki>=0 && ki<obj_end ? StringHelpers.read_digits(out, ki+8) : "")
if dsts != "" && arr != "" && idx != "" {
local dst_part = "\"dst\":" + dsts + ","
local repl = "{" + dst_part + "\"op\":\"boxcall\",\"box\":" + arr + ",\"method\":\"get\",\"args\":[" + idx + "]}"
out = out.substring(0, obj_start) + repl + out.substring(obj_end+1, out.length())
pos = obj_start + repl.length()
} else {
pos = k + 1
}
}
// array_set → boxcall(set)
pos = 0
loop(true) {
local k = JsonFragBox.index_of_from(out, "\"op\":\"array_set\"", pos)
if k < 0 { break }
local obj_start = out.lastIndexOf("{", k)
if obj_start < 0 { pos = k + 1; continue }
local obj_end = me._seek_object_end(out, obj_start)
if obj_end < 0 { pos = k + 1; continue }
local ka = JsonFragBox.index_of_from(out, "\"array\":", obj_start)
local arr = (ka>=0 && ka<obj_end ? StringHelpers.read_digits(out, ka+8) : "")
local ki = JsonFragBox.index_of_from(out, "\"index\":", obj_start)
local idx = (ki>=0 && ki<obj_end ? StringHelpers.read_digits(out, ki+8) : "")
local kv = JsonFragBox.index_of_from(out, "\"value\":", obj_start)
local val = (kv>=0 && kv<obj_end ? StringHelpers.read_digits(out, kv+8) : "")
if arr != "" && idx != "" && val != "" {
local repl = "{\"op\":\"boxcall\",\"box\":" + arr + ",\"method\":\"set\",\"args\":[" + idx + "," + val + "]}"
out = out.substring(0, obj_start) + repl + out.substring(obj_end+1, out.length())
pos = obj_start + repl.length()
} else {
pos = k + 1
}
}
// Optional: map_get/map_set → boxcall(get/set) if present in legacy JSON
pos = 0
loop(true) {
local k = JsonFragBox.index_of_from(out, "\"op\":\"map_get\"", pos)
if k < 0 { break }
local obj_start = out.lastIndexOf("{", k)
if obj_start < 0 { pos = k + 1; continue }
local obj_end = me._seek_object_end(out, obj_start)
if obj_end < 0 { pos = k + 1; continue }
local km = JsonFragBox.index_of_from(out, "\"map\":", obj_start)
local mv = (km>=0 && km<obj_end ? StringHelpers.read_digits(out, km+6) : "")
local kk = JsonFragBox.index_of_from(out, "\"key\":", obj_start)
local kv = (kk>=0 && kk<obj_end ? StringHelpers.read_digits(out, kk+6) : "")
local kdst = JsonFragBox.index_of_from(out, "\"dst\":", obj_start)
local dsts = (kdst>=0 && kdst<obj_end ? StringHelpers.read_digits(out, kdst+6) : "")
if mv != "" && kv != "" && dsts != "" {
local dst_part = "\"dst\":" + dsts + ","
local repl = "{" + dst_part + "\"op\":\"boxcall\",\"box\":" + mv + ",\"method\":\"get\",\"args\":[" + kv + "]}"
out = out.substring(0, obj_start) + repl + out.substring(obj_end+1, out.length())
pos = obj_start + repl.length()
} else { pos = k + 1 }
}
pos = 0
loop(true) {
local k = JsonFragBox.index_of_from(out, "\"op\":\"map_set\"", pos)
if k < 0 { break }
local obj_start = out.lastIndexOf("{", k)
if obj_start < 0 { pos = k + 1; continue }
local obj_end = me._seek_object_end(out, obj_start)
if obj_end < 0 { pos = k + 1; continue }
local km = JsonFragBox.index_of_from(out, "\"map\":", obj_start)
local mv = (km>=0 && km<obj_end ? StringHelpers.read_digits(out, km+6) : "")
local kk = JsonFragBox.index_of_from(out, "\"key\":", obj_start)
local kv = (kk>=0 && kk<obj_end ? StringHelpers.read_digits(out, kk+6) : "")
local kval = JsonFragBox.index_of_from(out, "\"value\":", obj_start)
local vv = (kval>=0 && kval<obj_end ? StringHelpers.read_digits(out, kval+8) : "")
if mv != "" && kv != "" && vv != "" {
local repl = "{\"op\":\"boxcall\",\"box\":" + mv + ",\"method\":\"set\",\"args\":[" + kv + "," + vv + "]}"
out = out.substring(0, obj_start) + repl + out.substring(obj_end+1, out.length())
pos = obj_start + repl.length()
} else { pos = k + 1 }
}
return out
}
}

View File

@ -0,0 +1,55 @@
// NormalizePrintBox — rewrite print → externcall(env.console.log)
using selfhost.shared.json.utils.json_frag as JsonFragBox
using selfhost.shared.common.string_helpers as StringHelpers
static box NormalizePrintBox {
_seek_object_end(s, start) {
if s == null { return -1 }
if start < 0 || start >= s.length() { return -1 }
if s.substring(start, start+1) != "{" { return -1 }
local i = start
local depth = 0
local in_str = 0
local esc = 0
loop (i < s.length()) {
local ch = s.substring(i, i+1)
if in_str == 1 {
if esc == 1 { esc = 0 }
else if ch == "\\" { esc = 1 }
else if ch == "\"" { in_str = 0 }
} else {
if ch == "\"" { in_str = 1 }
else if ch == "{" { depth = depth + 1 }
else if ch == "}" { depth = depth - 1 if depth == 0 { return i } }
}
i = i + 1
}
return -1
}
run(json) {
if json == null { return null }
local out = json
local pos = 0
loop(true) {
local k = JsonFragBox.index_of_from(out, "\"op\":\"print\"", pos)
if k < 0 { break }
// Find enclosing object span
local obj_start = out.lastIndexOf("{", k)
if obj_start < 0 { pos = k + 1; continue }
local obj_end = me._seek_object_end(out, obj_start)
if obj_end < 0 { pos = k + 1; continue }
// Extract value register
local kval = JsonFragBox.index_of_from(out, "\"value\":", obj_start)
local vreg = (kval>=0 && kval<obj_end ? StringHelpers.read_digits(out, kval+8) : "")
// Build externcall
local dst_part = "" // print has no dst
local args = (vreg != "" ? vreg : "")
local repl = "{" + dst_part + "\"op\":\"externcall\",\"func\":\"env.console.log\",\"args\":[" + args + "]}"
out = out.substring(0, obj_start) + repl + out.substring(obj_end+1, out.length())
pos = obj_start + repl.length()
}
return out
}
}

View File

@ -0,0 +1,92 @@
// NormalizeRefBox — rewrite ref_get/ref_set → boxcall getField/setField
using selfhost.shared.json.utils.json_frag as JsonFragBox
using selfhost.shared.common.string_helpers as StringHelpers
static box NormalizeRefBox {
_seek_object_end(s, start) {
if s == null { return -1 }
if start < 0 || start >= s.length() { return -1 }
if s.substring(start, start+1) != "{" { return -1 }
local i = start
local depth = 0
local in_str = 0
local esc = 0
loop (i < s.length()) {
local ch = s.substring(i, i+1)
if in_str == 1 {
if esc == 1 { esc = 0 }
else if ch == "\\" { esc = 1 }
else if ch == "\"" { in_str = 0 }
} else {
if ch == "\"" { in_str = 1 }
else if ch == "{" { depth = depth + 1 }
else if ch == "}" { depth = depth - 1 if depth == 0 { return i } }
}
i = i + 1
}
return -1
}
run(json) {
if json == null { return null }
local out = json
local pos = 0
loop(true) {
// ref_get
local k = JsonFragBox.index_of_from(out, "\"op\":\"ref_get\"", pos)
if k < 0 { break }
local obj_start = out.lastIndexOf("{", k)
if obj_start < 0 { pos = k + 1; continue }
local obj_end = me._seek_object_end(out, obj_start)
if obj_end < 0 { pos = k + 1; continue }
local kdst = JsonFragBox.index_of_from(out, "\"dst\":", obj_start)
local dsts = (kdst>=0 && kdst<obj_end ? StringHelpers.read_digits(out, kdst+6) : "")
local kref = JsonFragBox.index_of_from(out, "\"reference\":", obj_start)
local refv = (kref>=0 && kref<obj_end ? StringHelpers.read_digits(out, kref+12) : "")
// field is JSON string; read raw token between quotes after "field":"
local kf = JsonFragBox.index_of_from(out, "\"field\":\"", obj_start)
local field = (kf>=0 && kf<obj_end ? JsonFragBox.read_string_after(out, kf+9) : "")
if dsts != "" && refv != "" && field != "" {
// emit: const field + boxcall getField(field)
local c = "{\"op\":\"const\",\"dst\":__FID__,\"value\":{\"type\":\"string\",\"value\":\"__FLD__\"}}"
// allocate a temporary dst id for field: reuse dst+1000000 to avoid collision (text-level best effort)
local fid = dsts + "000000"
c = StringHelpers.replace_all(c, "__FID__", fid)
c = StringHelpers.replace_all(c, "__FLD__", field)
local repl = c + ",{" + (dsts!=""?"\"dst\":"+dsts+",":"") + "\"op\":\"boxcall\",\"box\":" + refv + ",\"method\":\"getField\",\"args\":[" + fid + "]}"
out = out.substring(0, obj_start) + repl + out.substring(obj_end+1, out.length())
pos = obj_start + repl.length()
} else {
pos = k + 1
}
}
// ref_set
pos = 0
loop(true) {
local k = JsonFragBox.index_of_from(out, "\"op\":\"ref_set\"", pos)
if k < 0 { break }
local obj_start = out.lastIndexOf("{", k)
if obj_start < 0 { pos = k + 1; continue }
local obj_end = me._seek_object_end(out, obj_start)
if obj_end < 0 { pos = k + 1; continue }
local kref = JsonFragBox.index_of_from(out, "\"reference\":", obj_start)
local refv = (kref>=0 && kref<obj_end ? StringHelpers.read_digits(out, kref+12) : "")
local kval = JsonFragBox.index_of_from(out, "\"value\":", obj_start)
local valv = (kval>=0 && kval<obj_end ? StringHelpers.read_digits(out, kval+8) : "")
local kf = JsonFragBox.index_of_from(out, "\"field\":\"", obj_start)
local field = (kf>=0 && kf<obj_end ? JsonFragBox.read_string_after(out, kf+9) : "")
if refv != "" && valv != "" && field != "" {
local c = "{\"op\":\"const\",\"dst\":__FID__,\"value\":{\"type\":\"string\",\"value\":\"__FLD__\"}}"
local fid = refv + "000000"
c = StringHelpers.replace_all(c, "__FID__", fid)
c = StringHelpers.replace_all(c, "__FLD__", field)
local repl = c + ",{" + "\"op\":\"boxcall\",\"box\":" + refv + ",\"method\":\"setField\",\"args\":[" + fid + "," + valv + "]}"
out = out.substring(0, obj_start) + repl + out.substring(obj_end+1, out.length())
pos = obj_start + repl.length()
} else {
pos = k + 1
}
}
return out
}
}

View File

@ -12,3 +12,14 @@ boxes = [
"LLVMV0BuilderBox", "LLVMV0BuilderBox",
"LLVMAotFacadeBox", "LLVMAotFacadeBox",
] ]
normalize.print = "boxes/normalize/normalize_print.hako"
normalize.ref = "boxes/normalize/normalize_ref.hako"
normalize.array_legacy = "boxes/normalize/normalize_array_legacy.hako"
aot_prep = "boxes/aot_prep.hako"
aot_prep.passes.strlen = "boxes/aot_prep/passes/strlen.hako"
aot_prep.passes.loop_hoist = "boxes/aot_prep/passes/loop_hoist.hako"
aot_prep.passes.const_dedup = "boxes/aot_prep/passes/const_dedup.hako"
aot_prep.passes.binop_cse = "boxes/aot_prep/passes/binop_cse.hako"
aot_prep.passes.collections_hot = "boxes/aot_prep/passes/collections_hot.hako"
aot_prep.passes.fold_const_ret = "boxes/aot_prep/passes/fold_const_ret.hako"
aot_prep.helpers.common = "boxes/aot_prep/helpers/common.hako"

View File

@ -64,6 +64,11 @@ static box MirBuilderBox {
if m == null { return null } if m == null { return null }
// Inject function definitions if available // Inject function definitions if available
local result = FuncLoweringBox.inject_funcs(m, func_defs_mir) local result = FuncLoweringBox.inject_funcs(m, func_defs_mir)
// Optional methodize (dev): rewrite legacy Call -> unified mir_call(Method)
if env.get("HAKO_MIR_BUILDER_METHODIZE") == "1" {
result = FuncLoweringBox.methodize_calls_in_mir(result)
}
// Optional normalize (dev)
local nv = env.get("HAKO_MIR_BUILDER_JSONFRAG_NORMALIZE") local nv = env.get("HAKO_MIR_BUILDER_JSONFRAG_NORMALIZE")
if nv != null && ("" + nv) == "1" { return NormBox.normalize_all(result) } if nv != null && ("" + nv) == "1" { return NormBox.normalize_all(result) }
return result return result
@ -413,8 +418,15 @@ static box MirBuilderBox {
} }
} }
} }
// Unsupported internal shape → FailFast and return null // Unsupported internal shape → try defs fallback (dev: HAKO_MIR_BUILDER_FUNCS=1)
print("[mirbuilder/internal/unsupported] only Return(Int|Binary(Int,Int)) supported in internal mode") print("[mirbuilder/internal/unsupported] only Return(Int|Binary(Int,Int)) supported in internal mode")
if func_defs_mir != null && func_defs_mir != "" {
// Assemble a minimal module from lowered defs
local defs = func_defs_mir
if defs.substring(0,1) == "," { defs = defs.substring(1, defs.length()) }
local mod = "{\\\"functions\\\":[" + defs + "]}"
return norm_if(mod)
}
return null return null
} }
} }

View File

@ -220,6 +220,89 @@ static box FuncLoweringBox {
return func_defs_mir return func_defs_mir
} }
// Dev-only: rewrite legacy Call into unified mir_call(Method) when possible
// Heuristic: find call {func:K,args:[..],dst:R} and match preceding const {dst:K,value:"Box.method/N"}
// Receiverは省略VM 側で静的シングルトンを補完。LLVM 経路はPoCの範囲外。
method methodize_calls_in_mir(mir_json) {
if env.get("HAKO_MIR_BUILDER_METHODIZE") != "1" { return mir_json }
if mir_json == null { return mir_json }
local s = "" + mir_json
local out = ""
local pos = 0
loop(pos < s.length()) {
local k_call = JsonFragBox.index_of_from(s, "\"op\":\"call\"", pos)
if k_call < 0 {
out.append(s.substring(pos, s.length()))
break
}
// copy up to call
out = out + s.substring(pos, k_call)
// parse func register
local k_func = JsonFragBox.index_of_from(s, "\"func\":", k_call)
local k_dst_call = JsonFragBox.index_of_from(s, "\"dst\":", k_call)
if k_func < 0 || k_dst_call < 0 { out = out + "\"op\":\"call\"" pos = k_call + 14 continue }
local func_reg = JsonFragBox.read_int_after(s, k_func + 7)
local call_dst = JsonFragBox.read_int_after(s, k_dst_call + 6)
if func_reg == null || call_dst == null { out = out + "\"op\":\"call\"" pos = k_call + 14 continue }
// find args list for splice
local k_args = JsonFragBox.index_of_from(s, "\"args\":[", k_call)
if k_args < 0 { out = out + "\"op\":\"call\"" pos = k_call + 14 continue }
local args_start = k_args + 7
// find closing ] for args
local args_end = -1 { local j = args_start local depth = 0 loop(j < s.length()) { local ch = s.substring(j,j+1) if ch == "[" { depth = depth + 1 } if ch == "]" { if depth == 0 { args_end = j break } else { depth = depth - 1 } } j = j + 1 } }
if args_end < 0 { out = out + "\"op\":\"call\"" pos = k_call + 14 continue }
local args_text = s.substring(args_start + 1, args_end)
// scan from start to k_call for const dst==func_reg and a string value
local scan = 0
local last_val = null
loop(scan < k_call) {
local k_dst = JsonFragBox.index_of_from(s, "\"dst\":", scan)
if k_dst < 0 || k_dst >= k_call { break }
local dst_id = JsonFragBox.read_int_after(s, k_dst + 6)
if dst_id != null && JsonFragBox._str_to_int("" + dst_id) == JsonFragBox._str_to_int("" + func_reg) {
// try to read value string after this position — prefer '"value":"' literal (skip nested type.value)
local k_val = JsonFragBox.index_of_from(s, "\"value\\\":\\\"", k_dst)
if k_val > 0 && k_val < k_call {
// Read string directly after '"value":"'
local name = JsonFragBox.read_string_after(s, k_val + 9)
if name != null {
if name != null && name.contains(".") {
last_val = name
}
} else {
// fallback: try generic '"value":' then read next quoted string
local k_val2 = JsonFragBox.index_of_from(s, "\"value\":", k_dst)
if k_val2 > 0 && k_val2 < k_call {
local k_q2 = JsonFragBox.index_of_from(s, "\"", k_val2 + 7)
if k_q2 > 0 && k_q2 < k_call {
local name2 = JsonFragBox.read_string_after(s, k_q2)
if name2 != null && name2.contains(".") { last_val = name2 }
}
}
}
}
}
scan = k_dst + 6
}
if last_val == null {
// no match; keep original token and advance
out = out + "\"op\":\"call\""
pos = k_call + 14
continue
}
// split Box.method[/N]
local dot = JsonFragBox.index_of_from(last_val, ".", 0)
local slash = JsonFragBox.index_of_from(last_val, "/", 0)
local bname = (dot >= 0 ? last_val.substring(0, dot) : last_val)
local mname = (dot >= 0 ? last_val.substring(dot + 1, (slash >= 0 ? slash : last_val.length())) : last_val)
// emit methodized mir_call head
out = out + ("\"op\":\"mir_call\",\"dst\":" + call_dst + ",\"mir_call\":{\"callee\":{\"type\":\"Method\",\"box_name\":\"" + bname + "\",\"method\":\"" + mname + "\"},\"args\":[" + args_text + "],\"effects\":[]}")
// advance pos to after this call instruction object key we handled (skip minimal)
pos = args_end
}
return out
}
// Lower function body to MIR (minimal support) // Lower function body to MIR (minimal support)
// Supports: Return(Int), Return(Binary(+|-|*|/, Int|Var, Int|Var)), Return(Call) // Supports: Return(Int), Return(Binary(+|-|*|/, Int|Var, Int|Var)), Return(Call)
method _lower_func_body(func_name, box_name, params_arr, body_json, func_map) { method _lower_func_body(func_name, box_name, params_arr, body_json, func_map) {
@ -252,7 +335,8 @@ static box FuncLoweringBox {
if val != null { if val != null {
// Build params JSON array // Build params JSON array
local params_json = me._build_params_json(params_arr) local params_json = me._build_params_json(params_arr)
local mir = "{\\\"name\\\":\\\"" + box_name + "." + func_name + "\\\",\\\"params\\\":" + params_json + ",\\\"locals\\\":[],\\\"blocks\\\":[{\\\"id\\\":0,\\\"instructions\\\":[{\\\"op\\\":\\\"const\\\",\\\"dst\\\":1,\\\"value\\\":{\\\"type\\\":\\\"i64\\\",\\\"value\\\":" + val + "}},{\\\"op\\\":\\\"ret\\\",\\\"value\\\":1}]}]}" local arity = JsonFragBox._str_to_int("" + params_arr.length())
local mir = "{\\\"name\\\":\\\"" + box_name + "." + func_name + "/" + arity + "\\\",\\\"params\\\":" + params_json + ",\\\"locals\\\":[],\\\"blocks\\\":[{\\\"id\\\":0,\\\"instructions\\\":[{\\\"op\\\":\\\"const\\\",\\\"dst\\\":1,\\\"value\\\":{\\\"type\\\":\\\"i64\\\",\\\"value\\\":" + val + "}},{\\\"op\\\":\\\"ret\\\",\\\"value\\\":1}]}]}"
return mir return mir
} }
} }
@ -361,7 +445,8 @@ static box FuncLoweringBox {
// Build params JSON array // Build params JSON array
local params_json = me._build_params_json(params_arr) local params_json = me._build_params_json(params_arr)
local mir = "{\\\"name\\\":\\\"" + box_name + "." + func_name + "\\\",\\\"params\\\":" + params_json + ",\\\"locals\\\":[],\\\"blocks\\\":[{\\\"id\\\":0,\\\"instructions\\\":[" + insts + "]}]}" local arity = JsonFragBox._str_to_int("" + params_arr.length())
local mir = "{\\\"name\\\":\\\"" + box_name + "." + func_name + "/" + arity + "\\\",\\\"params\\\":" + params_json + ",\\\"locals\\\":[],\\\"blocks\\\":[{\\\"id\\\":0,\\\"instructions\\\":[" + insts + "]}]}"
return mir return mir
} }
@ -431,12 +516,7 @@ static box FuncLoweringBox {
} }
} }
// Load const for function name // Build args (materialize immediates/params into registers)
insts = "{\\\"op\\\":\\\"const\\\",\\\"dst\\\":" + next_reg + ",\\\"value\\\":{\\\"type\\\":\\\"string\\\",\\\"value\\\":\\\"" + resolved_name + "\\\"}}"
local func_reg = next_reg
next_reg = next_reg + 1
// Load args
local arg_regs = new ArrayBox() local arg_regs = new ArrayBox()
{ {
local ai = 0 local ai = 0
@ -472,17 +552,52 @@ static box FuncLoweringBox {
} }
} }
// call instruction (using func_reg for function name) // Decide emission form: Global call (legacy) or unified Method (dev: methodize)
insts = insts + ",{\\\"op\\\":\\\"call\\\",\\\"func\\\":" + func_reg + ",\\\"args\\\":[" + args_list + "],\\\"dst\\\":" + next_reg + "}"
local result_reg = next_reg local result_reg = next_reg
{
local dm = env.get("HAKO_MIR_BUILDER_METHODIZE")
local do_methodize = 0
if dm != null && ("" + dm) == "1" { do_methodize = 1 }
if do_methodize == 1 && resolved_name.contains(".") {
// Methodize PoC: rewrite Global("Box.method") -> mir_call Method(box_name, method, receiver: omitted)
// Note: receiver is omitted here; VM will materialize a static singleton when callee.type==Method
// LLVM path may treat args[0] as receiver; PoC canary targets VM backend.
local dot_pos = JsonFragBox.index_of_from(resolved_name, ".", 0)
local slash_pos = JsonFragBox.index_of_from(resolved_name, "/", 0)
if dot_pos >= 0 {
local box_part = resolved_name.substring(0, dot_pos)
local meth_part = resolved_name.substring(dot_pos + 1, (slash_pos >= 0 ? slash_pos : resolved_name.length()))
insts = insts + "," + (
"{\\\"op\\\":\\\"mir_call\\\",\\\"dst\\\":" + result_reg + ",\\\"mir_call\\\":{" +
"\\\"callee\\\":{\\\"type\\\":\\\"Method\\\",\\\"box_name\\\":\\\"" + box_part + "\\\",\\\"method\\\":\\\"" + meth_part + "\\\"}," +
"\\\"args\\\":[" + args_list + "],\\\"effects\\\":[]}}"
)
next_reg = next_reg + 1 next_reg = next_reg + 1
} else {
// Fallback to legacy call if split failed
do_methodize = 0
}
}
if do_methodize != 1 {
// Legacy: const func name with arity and call
local call_arity = JsonFragBox._str_to_int("" + args_arr.length())
local resolved_with_arity = resolved_name + "/" + call_arity
insts = insts + ",{\\\"op\\\":\\\"const\\\",\\\"dst\\\":" + result_reg + ",\\\"value\\\":{\\\"type\\\":\\\"string\\\",\\\"value\\\":\\\"" + resolved_with_arity + "\\\"}}"
local func_reg = result_reg
result_reg = result_reg + 1
next_reg = result_reg + 0
insts = insts + ",{\\\"op\\\":\\\"call\\\",\\\"func\\\":" + func_reg + ",\\\"args\\\":[" + args_list + "],\\\"dst\\\":" + result_reg + "}"
next_reg = next_reg + 1
}
}
// ret // ret
insts = insts + ",{\\\"op\\\":\\\"ret\\\",\\\"value\\\":" + result_reg + "}" insts = insts + ",{\\\"op\\\":\\\"ret\\\",\\\"value\\\":" + result_reg + "}"
// Build params JSON array // Build params JSON array
local params_json = me._build_params_json(params_arr) local params_json = me._build_params_json(params_arr)
local mir = "{\\\"name\\\":\\\"" + box_name + "." + func_name + "\\\",\\\"params\\\":" + params_json + ",\\\"locals\\\":[],\\\"blocks\\\":[{\\\"id\\\":0,\\\"instructions\\\":[" + insts + "]}]}" local arity = JsonFragBox._str_to_int("" + params_arr.length())
local mir = "{\\\"name\\\":\\\"" + box_name + "." + func_name + "/" + arity + "\\\",\\\"params\\\":" + params_json + ",\\\"locals\\\":[],\\\"blocks\\\":[{\\\"id\\\":0,\\\"instructions\\\":[" + insts + "]}]}"
// Debug log // Debug log
if env.get("HAKO_MIR_BUILDER_DEBUG") == "1" { if env.get("HAKO_MIR_BUILDER_DEBUG") == "1" {

View File

@ -41,6 +41,7 @@ static box JsonFragNormalizerBox {
local seen = new MapBox() // signature -> 1 local seen = new MapBox() // signature -> 1
// Dev-only: purify flag to drop builder-side allocations (e.g., newbox) from JSONFrag output // Dev-only: purify flag to drop builder-side allocations (e.g., newbox) from JSONFrag output
local purify = 0; { local pv = env.get("HAKO_MIR_BUILDER_JSONFRAG_PURIFY"); if pv != null { local pvs = "" + pv; if pvs == "1" { purify = 1 } } } local purify = 0; { local pv = env.get("HAKO_MIR_BUILDER_JSONFRAG_PURIFY"); if pv != null { local pvs = "" + pv; if pvs == "1" { purify = 1 } } }
local seen_ret = 0
loop(i < n) { loop(i < n) {
guard = guard + 1 guard = guard + 1
if guard > 4096 { break } if guard > 4096 { break }
@ -55,7 +56,11 @@ static box JsonFragNormalizerBox {
// classify by op // classify by op
local op = JsonFragBox.get_str(obj, "op") local op = JsonFragBox.get_str(obj, "op")
if purify == 1 { if purify == 1 {
// Drop builder-side allocations and side-effect calls for JsonFrag purify
if op == "newbox" { continue } if op == "newbox" { continue }
if op == "boxcall" { continue }
if op == "externcall" { continue }
if op == "mir_call" { continue }
} }
if op == "phi" { if op == "phi" {
phi_list.push(obj) phi_list.push(obj)
@ -74,7 +79,9 @@ static box JsonFragNormalizerBox {
} }
if op == "ret" { if op == "ret" {
others_list.push(me._ret_ensure_value(obj)) others_list.push(me._ret_ensure_value(obj))
continue // cut: do not allow any instruction after ret in this block
seen_ret = 1
break
} }
others_list.push(obj) others_list.push(obj)
} }

View File

@ -166,6 +166,9 @@ path = "lang/src/shared/common/string_helpers.hako"
"selfhost.shared.mir.builder" = "lang/src/shared/mir/block_builder_box.hako" "selfhost.shared.mir.builder" = "lang/src/shared/mir/block_builder_box.hako"
"selfhost.shared.mir.io" = "lang/src/shared/mir/mir_io_box.hako" "selfhost.shared.mir.io" = "lang/src/shared/mir/mir_io_box.hako"
"selfhost.shared.mir.json_emit" = "lang/src/shared/mir/json_emit_box.hako" "selfhost.shared.mir.json_emit" = "lang/src/shared/mir/json_emit_box.hako"
"selfhost.llvm.ir.normalize.print" = "lang/src/llvm_ir/boxes/normalize/normalize_print.hako"
"selfhost.llvm.ir.normalize.ref" = "lang/src/llvm_ir/boxes/normalize/normalize_ref.hako"
"selfhost.llvm.ir.normalize.array_legacy" = "lang/src/llvm_ir/boxes/normalize/normalize_array_legacy.hako"
"selfhost.vm.entry" = "lang/src/vm/boxes/mini_vm_entry.hako" "selfhost.vm.entry" = "lang/src/vm/boxes/mini_vm_entry.hako"
"selfhost.vm.mir_min" = "lang/src/vm/boxes/mir_vm_min.hako" "selfhost.vm.mir_min" = "lang/src/vm/boxes/mir_vm_min.hako"
"selfhost.vm.core" = "lang/src/vm/boxes/mini_vm_core.hako" "selfhost.vm.core" = "lang/src/vm/boxes/mini_vm_core.hako"

View File

@ -1,5 +1,4 @@
use super::*; use super::*;
use super::super::utils::*;
impl MirInterpreter { impl MirInterpreter {
pub(super) fn handle_const(&mut self, dst: ValueId, value: &ConstValue) -> Result<(), VMError> { pub(super) fn handle_const(&mut self, dst: ValueId, value: &ConstValue) -> Result<(), VMError> {

View File

@ -1,5 +1,4 @@
use super::*; use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox; use crate::box_trait::NyashBox;
impl MirInterpreter { impl MirInterpreter {

View File

@ -1,5 +1,4 @@
use super::*; use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox; use crate::box_trait::NyashBox;
pub(super) fn try_handle_array_box( pub(super) fn try_handle_array_box(

View File

@ -1,5 +1,4 @@
use super::*; use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox; use crate::box_trait::NyashBox;
pub(super) fn try_handle_instance_box( pub(super) fn try_handle_instance_box(

View File

@ -1,5 +1,4 @@
use super::*; use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox; use crate::box_trait::NyashBox;
pub(super) fn try_handle_map_box( pub(super) fn try_handle_map_box(

View File

@ -1,5 +1,4 @@
use super::*; use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox; use crate::box_trait::NyashBox;
pub(super) fn try_handle_object_fields( pub(super) fn try_handle_object_fields(

View File

@ -1,5 +1,4 @@
use super::*; use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox; use crate::box_trait::NyashBox;
pub(super) fn invoke_plugin_box( pub(super) fn invoke_plugin_box(

View File

@ -1,5 +1,4 @@
use super::*; use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox; use crate::box_trait::NyashBox;
pub(super) fn try_handle_string_box( pub(super) fn try_handle_string_box(

View File

@ -1,89 +0,0 @@
// Unique-tail function name resolution
// Extracted from execute_legacy_call to keep handlers lean
use std::collections::HashMap;
use super::MirFunction;
/// Resolve function name using unique-tail matching algorithm.
///
/// Resolution strategy:
/// 1. Fast path: exact match on raw name
/// 2. Normalize with arity: "base/N"
/// 3. Unique-tail matching: find candidates ending with ".method/N"
/// 4. Same-box preference: prioritize functions in the same box as current_function
///
/// Returns resolved function name, or None if resolution fails.
pub(super) fn resolve_function_name(
raw: &str,
arity: usize,
functions: &HashMap<String, MirFunction>,
current_function: Option<&str>,
) -> Option<String> {
// Fast path: exact match
if functions.contains_key(raw) {
return Some(raw.to_string());
}
// Robust normalization for names like "Box.method/Arity" or just "method"
let (base, ar_from_raw) = if let Some((b, a)) = raw.rsplit_once('/') {
(b.to_string(), a.parse::<usize>().ok())
} else {
(raw.to_string(), None)
};
let want_arity = ar_from_raw.unwrap_or(arity);
// Try exact canonical form: "base/arity"
let exact = format!("{}/{}", base, want_arity);
if functions.contains_key(&exact) {
return Some(exact);
}
// Split base into optional box and method name
let (maybe_box, method) = if let Some((bx, m)) = base.split_once('.') {
(Some(bx.to_string()), m.to_string())
} else {
(None, base.clone())
};
// Collect candidates that end with ".method/arity"
let mut cands: Vec<String> = Vec::new();
let tail = format!(".{}{}", method, format!("/{}", want_arity));
for k in functions.keys() {
if k.ends_with(&tail) {
if let Some(ref bx) = maybe_box {
if k.starts_with(&format!("{}.", bx)) {
cands.push(k.clone());
}
} else {
cands.push(k.clone());
}
}
}
if cands.len() > 1 {
// Prefer same-box candidate based on current function's box
if let Some(cur) = current_function {
let cur_box = cur.split('.').next().unwrap_or("");
let scoped: Vec<String> = cands
.iter()
.filter(|k| k.starts_with(&format!("{}.", cur_box)))
.cloned()
.collect();
if scoped.len() == 1 {
cands = scoped;
}
}
}
match cands.len() {
0 => None,
1 => Some(cands.into_iter().next().unwrap()),
_ => {
// Multiple candidates: sort and pick first (deterministic fallback)
let mut c = cands;
c.sort();
Some(c.into_iter().next().unwrap())
}
}
}

View File

@ -1,873 +0,0 @@
use super::*;
use super::super::utils::*;
impl MirInterpreter {
pub(super) fn handle_call(
&mut self,
dst: Option<ValueId>,
func: ValueId,
callee: Option<&Callee>,
args: &[ValueId],
) -> Result<(), VMError> {
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
match callee {
Some(Callee::Global(n)) => eprintln!("[hb:path] call Callee::Global {} argc={}", n, args.len()),
Some(Callee::Method{ box_name, method, ..}) => eprintln!("[hb:path] call Callee::Method {}.{} argc={}", box_name, method, args.len()),
Some(Callee::Constructor{ box_type }) => eprintln!("[hb:path] call Callee::Constructor {} argc={}", box_type, args.len()),
Some(Callee::Closure{ .. }) => eprintln!("[hb:path] call Callee::Closure argc={}", args.len()),
Some(Callee::Value(_)) => eprintln!("[hb:path] call Callee::Value argc={}", args.len()),
Some(Callee::Extern(n)) => eprintln!("[hb:path] call Callee::Extern {} argc={}", n, args.len()),
None => eprintln!("[hb:path] call Legacy func_id={:?} argc={}", func, args.len()),
}
}
// SSOT fast-path: route hostbridge.extern_invoke to extern dispatcher regardless of resolution form
if let Some(Callee::Global(func_name)) = callee {
if func_name == "hostbridge.extern_invoke" || func_name.starts_with("hostbridge.extern_invoke/") {
let v = self.execute_extern_function("hostbridge.extern_invoke", args)?;
self.write_result(dst, v);
return Ok(());
}
}
let call_result = if let Some(callee_type) = callee {
self.execute_callee_call(callee_type, args)?
} else {
self.execute_legacy_call(func, args)?
};
self.write_result(dst, call_result);
Ok(())
}
pub(super) fn execute_callee_call(
&mut self,
callee: &Callee,
args: &[ValueId],
) -> Result<VMValue, VMError> {
match callee {
Callee::Global(func_name) => {
// Phase 21.2: Dev by-name bridge removed - all adapter functions now in .hako
self.execute_global_function(func_name, args)
}
Callee::Method { box_name: _, method, receiver, certainty: _, } => {
if let Some(recv_id) = receiver {
// Primary: load receiver by id. Dev fallback: if undefined and env allows,
// use args[0] as a surrogate receiver (builder localization gap workaround).
let recv_val = match self.reg_load(*recv_id) {
Ok(v) => v,
Err(e) => {
let tolerate = std::env::var("NYASH_VM_RECV_ARG_FALLBACK").ok().as_deref() == Some("1")
|| std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1");
if tolerate {
if let Some(a0) = args.get(0) {
self.reg_load(*a0)?
} else {
return Err(e);
}
} else {
return Err(e);
}
}
};
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.load_as_box(*a0)?;
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.load_as_box(*a0)?;
let ret = arr.get(idx);
return Ok(VMValue::from_nyash_box(ret));
}
}
"set" => {
if args.len() >= 2 {
let idx = self.load_as_box(args[0])?;
let val = self.load_as_box(args[1])?;
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());
eprintln!("[vm-trace] mcall {} argv0={:?}", method, a0);
}
let out = self.execute_method_call(&recv_val, method, args)?;
if dev_trace && is_kw {
eprintln!("[vm-trace] mret {} -> {:?}", method, out);
}
Ok(out)
} else {
Err(self.err_with_context("Method call", &format!("missing receiver for {}", method)))
}
}
Callee::Constructor { box_type } => Err(self.err_unsupported(&format!("Constructor calls for {}", box_type))),
Callee::Closure { .. } => Err(self.err_unsupported("Closure creation in VM")),
Callee::Value(func_val_id) => {
let _func_val = self.reg_load(*func_val_id)?;
Err(self.err_unsupported("First-class function calls in VM"))
}
Callee::Extern(extern_name) => self.execute_extern_function(extern_name, args),
}
}
pub(super) fn execute_legacy_call(
&mut self,
func_id: ValueId,
args: &[ValueId],
) -> Result<VMValue, VMError> {
let name_val = self.reg_load(func_id)?;
let raw = match name_val {
VMValue::String(ref s) => s.clone(),
other => other.to_string(),
};
if std::env::var("HAKO_DEBUG_LEGACY_CALL").ok().as_deref() == Some("1") {
eprintln!("[vm-debug] legacy-call raw='{}' argc={}", raw, args.len());
}
// Minimal builtin bridge: support print-like globals in legacy form
// Accept: "print", "nyash.console.log", "env.console.log", "nyash.builtin.print"
match raw.as_str() {
"print" | "nyash.console.log" | "env.console.log" | "nyash.builtin.print" => {
if let Some(a0) = args.get(0) {
let v = self.reg_load(*a0)?;
println!("{}", v.to_string());
} else {
println!("");
}
return Ok(VMValue::Void);
}
name if name == "hostbridge.extern_invoke" || name.starts_with("hostbridge.extern_invoke/") =>
// SSOT: always delegate to extern dispatcher
{ return self.execute_extern_function("hostbridge.extern_invoke", args); }
name if name == "env.get" || name.starts_with("env.get/") || name.contains("env.get") => {
return self.execute_extern_function("env.get", args);
}
// Minimal bridge for Hako static provider: HakoruneExternProviderBox.get(name, arg)
// Purpose: allow -c/inline alias path to tolerate static box calls without unresolved errors.
// Behavior: when HAKO_V1_EXTERN_PROVIDER_C_ABI=1, emit stable tag; always return empty string.
name if name == "HakoruneExternProviderBox.get"
|| name.starts_with("HakoruneExternProviderBox.get/")
|| name.contains("HakoruneExternProviderBox.get") =>
{
// Read provider name from first arg if available
let mut prov: Option<String> = None;
if let Some(a0) = args.get(0) {
if let Ok(v) = self.reg_load(*a0) { prov = Some(v.to_string()); }
}
if std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() == Some("1") {
if let Some(p) = prov.as_deref() {
match p {
"env.mirbuilder.emit" => eprintln!("[extern/c-abi:mirbuilder.emit]"),
"env.codegen.emit_object" => eprintln!("[extern/c-abi:codegen.emit_object]"),
_ => {}
}
}
}
return Ok(VMValue::String(String::new()));
}
// Phase 21.2: Dev bridge removed - all adapter functions now resolved via .hako implementation
// MirCallV1HandlerBox.handle, JsonFragBox._str_to_int, AbiAdapterRegistryBox.*
// are now implemented in lang/src/vm/ and compiled via text-merge
_ => {}
}
// Resolve function name using unique-tail matching
let fname = call_resolution::resolve_function_name(
&raw,
args.len(),
&self.functions,
self.cur_fn.as_deref(),
)
.ok_or_else(|| {
ErrorBuilder::with_context("call", &format!("unresolved: '{}' (arity={})", raw, args.len()))
})?;
if std::env::var("NYASH_VM_CALL_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm] legacy-call resolved '{}' -> '{}'", raw, fname);
}
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
eprintln!("[hb:resolved:calls] fname='{}'", fname);
}
let callee =
self.functions.get(&fname).cloned().ok_or_else(|| {
self.err_with_context("function call", &format!("function not found: {}", fname))
})?;
// SSOT: delegate hostbridge.extern_invoke to the extern dispatcher early,
// avoiding duplicated legacy handling below. This ensures C-API link path
// is consistently covered by extern_provider_dispatch.
if fname == "hostbridge.extern_invoke" || fname.starts_with("hostbridge.extern_invoke/") {
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
eprintln!("[hb:delegate:calls] hostbridge.extern_invoke -> execute_extern_function (early)");
}
return self.execute_extern_function("hostbridge.extern_invoke", args);
}
let mut argv: Vec<VMValue> = Vec::new();
for a in args {
argv.push(self.reg_load(*a)?);
}
let dev_trace = std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1");
let is_kw = fname.ends_with("JsonTokenizer.keyword_to_token_type/1");
let is_sc_ident = fname.ends_with("JsonScanner.read_identifier/0");
let is_sc_current = fname.ends_with("JsonScanner.current/0");
let is_tok_kw = fname.ends_with("JsonTokenizer.tokenize_keyword/0");
let is_tok_struct = fname.ends_with("JsonTokenizer.create_structural_token/2");
if dev_trace && (is_kw || is_sc_ident || is_sc_current || is_tok_kw || is_tok_struct) {
if let Some(a0) = argv.get(0) {
eprintln!("[vm-trace] call {} argv0={:?}", fname, a0);
} else {
eprintln!("[vm-trace] call {}", fname);
}
}
// Dev trace: emit a synthetic "call" event for global function calls
// so operator boxes (e.g., CompareOperator.apply/3) are observable with
// argument kinds. This produces a JSON line on stderr, filtered by
// NYASH_BOX_TRACE_FILTER like other box traces.
if Self::box_trace_enabled() {
// Render class/method from canonical fname like "Class.method/Arity"
let (class_name, method_name) = if let Some((cls, rest)) = fname.split_once('.') {
let method = rest.split('/').next().unwrap_or(rest);
(cls.to_string(), method.to_string())
} else {
("<global>".to_string(), fname.split('/').next().unwrap_or(&fname).to_string())
};
// Simple filter match (local copy to avoid private helper)
let filt_ok = match std::env::var("NYASH_BOX_TRACE_FILTER").ok() {
Some(filt) => {
let want = filt.trim();
if want.is_empty() { true } else {
want.split(|c: char| c == ',' || c.is_whitespace())
.map(|t| t.trim())
.filter(|t| !t.is_empty())
.any(|t| class_name.contains(t))
}
}
None => true,
};
if filt_ok {
// Optionally include argument kinds for targeted debugging.
let with_args = std::env::var("NYASH_OP_TRACE_ARGS").ok().as_deref() == Some("1")
|| class_name == "CompareOperator";
if with_args {
// local JSON string escaper (subset)
let mut esc = |s: &str| {
let mut out = String::with_capacity(s.len() + 8);
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c.is_control() => out.push(' '),
c => out.push(c),
}
}
out
};
let mut kinds: Vec<String> = Vec::with_capacity(argv.len());
let mut nullish: Vec<String> = Vec::with_capacity(argv.len());
for v in &argv {
let k = match v {
VMValue::Integer(_) => "Integer".to_string(),
VMValue::Float(_) => "Float".to_string(),
VMValue::Bool(_) => "Bool".to_string(),
VMValue::String(_) => "String".to_string(),
VMValue::Void => "Void".to_string(),
VMValue::Future(_) => "Future".to_string(),
VMValue::BoxRef(b) => {
// Prefer InstanceBox.class_name when available
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
format!("BoxRef:{}", inst.class_name)
} else {
format!("BoxRef:{}", b.type_name())
}
}
};
kinds.push(k);
// nullish tag (env-gated): "null" | "missing" | "void" | ""
if crate::config::env::null_missing_box_enabled() {
let tag = match v {
VMValue::Void => "void",
VMValue::BoxRef(b) => {
if b.as_any().downcast_ref::<crate::boxes::null_box::NullBox>().is_some() { "null" }
else if b.as_any().downcast_ref::<crate::boxes::missing_box::MissingBox>().is_some() { "missing" }
else if b.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() { "void" }
else { "" }
}
_ => "",
};
nullish.push(tag.to_string());
}
}
let args_json = kinds
.into_iter()
.map(|s| format!("\"{}\"", esc(&s)))
.collect::<Vec<_>>()
.join(",");
let nullish_json = if crate::config::env::null_missing_box_enabled() {
let arr = nullish
.into_iter()
.map(|s| format!("\"{}\"", esc(&s)))
.collect::<Vec<_>>()
.join(",");
Some(arr)
} else { None };
// For CompareOperator, include op string value if present in argv[0]
let cur_fn = self
.cur_fn
.as_deref()
.map(|s| esc(s))
.unwrap_or_else(|| String::from("") );
if class_name == "CompareOperator" && !argv.is_empty() {
let op_str = match &argv[0] {
VMValue::String(s) => esc(s),
_ => String::from("")
};
if let Some(nj) = nullish_json {
eprintln!(
"{{\"ev\":\"call\",\"class\":\"{}\",\"method\":\"{}\",\"argc\":{},\"fn\":\"{}\",\"op\":\"{}\",\"argk\":[{}],\"nullish\":[{}]}}",
esc(&class_name), esc(&method_name), argv.len(), cur_fn, op_str, args_json, nj
);
} else {
eprintln!(
"{{\"ev\":\"call\",\"class\":\"{}\",\"method\":\"{}\",\"argc\":{},\"fn\":\"{}\",\"op\":\"{}\",\"argk\":[{}]}}",
esc(&class_name), esc(&method_name), argv.len(), cur_fn, op_str, args_json
);
}
} else {
if let Some(nj) = nullish_json {
eprintln!(
"{{\"ev\":\"call\",\"class\":\"{}\",\"method\":\"{}\",\"argc\":{},\"fn\":\"{}\",\"argk\":[{}],\"nullish\":[{}]}}",
esc(&class_name), esc(&method_name), argv.len(), cur_fn, args_json, nj
);
} else {
eprintln!(
"{{\"ev\":\"call\",\"class\":\"{}\",\"method\":\"{}\",\"argc\":{},\"fn\":\"{}\",\"argk\":[{}]}}",
esc(&class_name), esc(&method_name), argv.len(), cur_fn, args_json
);
}
}
} else {
self.box_trace_emit_call(&class_name, &method_name, argv.len());
}
}
}
let out = self.exec_function_inner(&callee, Some(&argv))?;
if dev_trace && (is_kw || is_sc_ident || is_sc_current || is_tok_kw || is_tok_struct) {
eprintln!("[vm-trace] ret {} -> {:?}", fname, out);
}
Ok(out)
}
fn execute_global_function(
&mut self,
func_name: &str,
args: &[ValueId],
) -> Result<VMValue, VMError> {
match func_name {
name if name == "env.get" || name.starts_with("env.get/") => {
// Route env.get global to extern handler
return self.execute_extern_function("env.get", args);
}
name if name == "hostbridge.extern_invoke" || name.starts_with("hostbridge.extern_invoke/") => {
// SSOT: delegate to extern dispatcher (provider). Keep legacy block unreachable.
return self.execute_extern_function("hostbridge.extern_invoke", args);
// Treat as extern_invoke in legacy/global-resolved form
if args.len() < 3 {
return Err(self.err_arg_count("hostbridge.extern_invoke", 3, args.len()));
}
let name = self.reg_load(args[0])?.to_string();
let method = self.reg_load(args[1])?.to_string();
let v = self.reg_load(args[2])?;
let mut first_arg_str: Option<String> = None;
match v {
VMValue::BoxRef(b) => {
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
let idx: Box<dyn crate::box_trait::NyashBox> =
Box::new(crate::box_trait::IntegerBox::new(0));
let elem = ab.get(idx);
first_arg_str = Some(elem.to_string_box().value);
} else {
first_arg_str = Some(b.to_string_box().value);
}
}
_ => first_arg_str = Some(v.to_string()),
}
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
eprintln!("[hb:dispatch:calls] {} {}", name, method);
}
match (name.as_str(), method.as_str()) {
("env.mirbuilder", "emit") => {
if let Some(s) = first_arg_str {
match crate::host_providers::mir_builder::program_json_to_mir_json(&s) {
Ok(out) => Ok(VMValue::String(out)),
Err(e) => Err(self.err_with_context("env.mirbuilder.emit", &e.to_string())),
}
} else {
Err(self.err_invalid("extern_invoke env.mirbuilder.emit expects 1 arg"))
}
}
("env.codegen", "emit_object") => {
if let Some(s) = first_arg_str {
let opts = crate::host_providers::llvm_codegen::Opts {
out: None,
nyrt: std::env::var("NYASH_EMIT_EXE_NYRT").ok().map(std::path::PathBuf::from),
opt_level: std::env::var("HAKO_LLVM_OPT_LEVEL").ok().or(Some("0".to_string())),
timeout_ms: None,
};
match crate::host_providers::llvm_codegen::mir_json_to_object(&s, opts) {
Ok(p) => Ok(VMValue::String(p.to_string_lossy().into_owned())),
Err(e) => Err(self.err_with_context("env.codegen.emit_object", &e.to_string())),
}
} else {
Err(self.err_invalid("extern_invoke env.codegen.emit_object expects 1 arg"))
}
}
("env.codegen", "link_object") => {
// C-API route only; here args[2] is already loaded into `v`
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") ||
std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() != Some("1") {
return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
}
let (obj_path, exe_out) = match v {
VMValue::BoxRef(b) => {
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0));
let elem0 = ab.get(idx0).to_string_box().value;
let mut exe: Option<String> = None;
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1));
let e1 = ab.get(idx1).to_string_box().value;
if !e1.is_empty() { exe = Some(e1); }
(elem0, exe)
} else { (b.to_string_box().value, None) }
}
_ => (v.to_string(), None),
};
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
let obj = std::path::PathBuf::from(obj_path);
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) {
Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
Err(e) => Err(self.err_with_context("env.codegen.link_object", &e.to_string()))
}
}
("env.codegen", "link_object") => {
// C-API route only; args[2] is expected to be an ArrayBox [obj_path, exe_out?]
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") ||
std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() != Some("1") {
return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
}
if args.len() < 3 { return Err(self.err_arg_count("env.codegen.link_object", 3, args.len())); }
let v = self.reg_load(args[2])?;
let (obj_path, exe_out) = match v {
VMValue::BoxRef(b) => {
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0));
let elem0 = ab.get(idx0).to_string_box().value;
let mut exe: Option<String> = None;
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1));
let e1 = ab.get(idx1).to_string_box().value;
if !e1.is_empty() { exe = Some(e1); }
(elem0, exe)
} else { (b.to_string_box().value, None) }
}
_ => (v.to_string(), None),
};
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
let obj = std::path::PathBuf::from(obj_path);
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) {
Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
Err(e) => Err(self.err_with_context("env.codegen.link_object", &e.to_string()))
}
}
("env.codegen", "link_object") => {
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") ||
std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() != Some("1") {
return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
}
// Extract from args[2] (ArrayBox): [obj_path, exe_out?]
let v = self.reg_load(args[2])?;
let (obj_path, exe_out) = match v {
VMValue::BoxRef(b) => {
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0));
let elem0 = ab.get(idx0).to_string_box().value;
let mut exe: Option<String> = None;
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1));
let e1 = ab.get(idx1).to_string_box().value;
if !e1.is_empty() { exe = Some(e1); }
(elem0, exe)
} else { (b.to_string_box().value, None) }
}
_ => (v.to_string(), None),
};
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
let obj = std::path::PathBuf::from(obj_path);
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) {
Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
Err(e) => Err(self.err_with_context("env.codegen.link_object", &e.to_string()))
}
}
("env.codegen", "link_object") => {
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") ||
std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() != Some("1") {
return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
}
// Here args[2] is already loaded into `v`; parse ArrayBox [obj, exe?]
let (obj_path, exe_out) = match v {
VMValue::BoxRef(b) => {
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0));
let elem0 = ab.get(idx0).to_string_box().value;
let mut exe: Option<String> = None;
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1));
let e1 = ab.get(idx1).to_string_box().value;
if !e1.is_empty() { exe = Some(e1); }
(elem0, exe)
} else { (b.to_string_box().value, None) }
}
_ => (v.to_string(), None),
};
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
let obj = std::path::PathBuf::from(obj_path);
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) {
Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
Err(e) => Err(self.err_with_context("env.codegen.link_object", &e.to_string()))
}
}
_ => {
// Last-chance bridge: some older dispatch paths may fall through here
if name == "env.codegen" && method == "link_object" {
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") ||
std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() != Some("1") {
return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
}
// Expect args[2] as ArrayBox [obj, exe?]
if args.len() >= 3 {
let v = self.reg_load(args[2])?;
let (obj_path, exe_out) = match v {
VMValue::BoxRef(b) => {
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
let i0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0));
let s0 = ab.get(i0).to_string_box().value;
let mut e: Option<String> = None;
let i1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1));
let s1 = ab.get(i1).to_string_box().value;
if !s1.is_empty() { e = Some(s1); }
(s0, e)
} else { (b.to_string_box().value, None) }
}
_ => (v.to_string(), None),
};
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
let obj = std::path::PathBuf::from(obj_path);
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) {
Ok(()) => return Ok(VMValue::String(exe.to_string_lossy().into_owned())),
Err(e) => return Err(self.err_with_context("env.codegen.link_object", &e.to_string())),
}
}
}
// As a final safety, try provider dispatcher again
if let Some(res) = self.extern_provider_dispatch("hostbridge.extern_invoke", args) {
return res;
}
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
eprintln!("[hb:unsupported:calls] {}.{}", name, method);
}
Err(self.err_invalid(format!(
"hostbridge.extern_invoke unsupported for {}.{} [calls]",
name, method
)))
},
}
}
"nyash.builtin.print" | "print" | "nyash.console.log" => {
if let Some(arg_id) = args.get(0) {
let val = self.reg_load(*arg_id)?;
// Dev-only: print trace (kind/class) before actual print
if Self::print_trace_enabled() { self.print_trace_emit(&val); }
// Dev observe: Null/Missing boxes quick normalization (no behavior change to prod)
if let VMValue::BoxRef(bx) = &val {
// NullBox → always print as null (stable)
if bx.as_any().downcast_ref::<crate::boxes::null_box::NullBox>().is_some() {
println!("null");
return Ok(VMValue::Void);
}
// MissingBox → default prints as null; when flag ON, show (missing)
if bx.as_any().downcast_ref::<crate::boxes::missing_box::MissingBox>().is_some() {
if crate::config::env::null_missing_box_enabled() {
println!("(missing)");
} else {
println!("null");
}
return Ok(VMValue::Void);
}
}
// Dev: treat VM Void and BoxRef(VoidBox) as JSON null for print
match &val {
VMValue::Void => {
println!("null");
return Ok(VMValue::Void);
}
VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
println!("null");
return Ok(VMValue::Void);
}
}
_ => {}
}
// Print raw strings directly (avoid double quoting via StringifyOperator)
match &val {
VMValue::String(s) => { println!("{}", s); return Ok(VMValue::Void); }
VMValue::BoxRef(bx) => {
if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
println!("{}", sb.value);
return Ok(VMValue::Void);
}
}
_ => {}
}
// Operator Box (Stringify) dev flag gated
if std::env::var("NYASH_OPERATOR_BOX_STRINGIFY").ok().as_deref() == Some("1") {
if let Some(op) = self.functions.get("StringifyOperator.apply/1").cloned() {
let out = self.exec_function_inner(&op, Some(&[val.clone()]))?;
println!("{}", out.to_string());
} else {
println!("{}", val.to_string());
}
} else {
println!("{}", val.to_string());
}
}
Ok(VMValue::Void)
}
"nyash.builtin.error" => {
if let Some(arg_id) = args.get(0) {
let val = self.reg_load(*arg_id)?;
eprintln!("Error: {}", val.to_string());
}
Ok(VMValue::Void)
}
_ => Err(self.err_with_context("global function", &format!("Unknown: {}", func_name))),
}
}
fn execute_method_call(
&mut self,
receiver: &VMValue,
method: &str,
args: &[ValueId],
) -> Result<VMValue, VMError> {
match receiver {
VMValue::String(s) => match method {
"length" => Ok(VMValue::Integer(s.len() as i64)),
"concat" => {
if let Some(arg_id) = args.get(0) {
let arg_val = self.reg_load(*arg_id)?;
let new_str = format!("{}{}", s, arg_val.to_string());
Ok(VMValue::String(new_str))
} else {
Err(self.err_invalid("concat requires 1 argument"))
}
}
"replace" => {
if args.len() == 2 {
let old = self.reg_load(args[0])?.to_string();
let new = self.reg_load(args[1])?.to_string();
Ok(VMValue::String(s.replace(&old, &new)))
} else {
Err(self.err_invalid("replace requires 2 arguments"))
}
}
"indexOf" => {
if let Some(arg_id) = args.get(0) {
let needle = self.reg_load(*arg_id)?.to_string();
let idx = s.find(&needle).map(|i| i as i64).unwrap_or(-1);
Ok(VMValue::Integer(idx))
} else {
Err(self.err_invalid("indexOf requires 1 argument"))
}
}
"lastIndexOf" => {
if let Some(arg_id) = args.get(0) {
let needle = self.reg_load(*arg_id)?.to_string();
let idx = s.rfind(&needle).map(|i| i as i64).unwrap_or(-1);
Ok(VMValue::Integer(idx))
} else {
Err(self.err_invalid("lastIndexOf requires 1 argument"))
}
}
"substring" => {
let start = if let Some(a0) = args.get(0) {
self.reg_load(*a0)?.as_integer().unwrap_or(0)
} else { 0 };
let end = if let Some(a1) = args.get(1) {
self.reg_load(*a1)?.as_integer().unwrap_or(s.len() as i64)
} else { s.len() as i64 };
let len = s.len() as i64;
let i0 = start.max(0).min(len) as usize;
let i1 = end.max(0).min(len) as usize;
if i0 > i1 { return Ok(VMValue::String(String::new())); }
// Note: operating on bytes; Nyash strings are UTF8, but tests are ASCII only here
let bytes = s.as_bytes();
let sub = String::from_utf8(bytes[i0..i1].to_vec()).unwrap_or_default();
Ok(VMValue::String(sub))
}
_ => Err(self.err_method_not_found("String", method)),
},
VMValue::BoxRef(box_ref) => {
// Try builtin StringBox first
if let Some(string_box) = box_ref
.as_any()
.downcast_ref::<crate::boxes::string_box::StringBox>()
{
match method {
"lastIndexOf" => {
if let Some(arg_id) = args.get(0) {
let needle = self.reg_load(*arg_id)?.to_string();
let result_box = string_box.lastIndexOf(&needle);
Ok(VMValue::from_nyash_box(result_box))
} else {
Err(self.err_invalid("lastIndexOf requires 1 argument"))
}
}
"indexOf" | "find" => {
if let Some(arg_id) = args.get(0) {
let needle = self.reg_load(*arg_id)?.to_string();
let result_box = string_box.find(&needle);
Ok(VMValue::from_nyash_box(result_box))
} else {
Err(self.err_invalid("indexOf/find requires 1 argument"))
}
}
_ => Err(self.err_method_not_found("StringBox", method)),
}
} else if let Some(p) = box_ref
.as_any()
.downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>()
{
let host = crate::runtime::plugin_loader_unified::get_global_plugin_host();
let host = host.read().unwrap();
let argv = self.load_args_as_boxes(args)?;
match host.invoke_instance_method(
&p.box_type,
method,
p.inner.instance_id,
&argv,
) {
Ok(Some(ret)) => Ok(VMValue::from_nyash_box(ret)),
Ok(None) => Ok(VMValue::Void),
Err(e) => Err(self.err_with_context(
&format!("Plugin method {}.{}", p.box_type, method),
&format!("{:?}", e)
)),
}
} else {
Err(self.err_method_not_found(&box_ref.type_name(), method))
}
}
_ => Err(self.err_with_context("method call", &format!("{} not supported on {:?}", method, receiver))),
}
}
// Fallback: user-defined function dispatch for Global calls
// If none of the above extern/provider/global bridges matched,
// try to resolve and execute a user function present in this module.
{
// Use unique-tail resolver against snapshot of self.functions
let fname = call_resolution::resolve_function_name(
func_name,
args.len(),
&self.functions,
self.cur_fn.as_deref(),
);
if let Some(fname) = fname {
if let Some(func) = self.functions.get(&fname).cloned() {
// Load arguments and execute
let mut argv: Vec<VMValue> = Vec::new();
for a in args { argv.push(self.reg_load(*a)?); }
if std::env::var("NYASH_VM_CALL_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm] global-call resolved '{}' -> '{}'", func_name, fname);
}
return self.exec_function_inner(&func, Some(&argv));
}
}
}
fn execute_extern_function(
&mut self,
extern_name: &str,
args: &[ValueId],
) -> Result<VMValue, VMError> {
if let Some(res) = self.extern_provider_dispatch(extern_name, args) {
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
eprintln!("[hb:dispatch:calls] provider {}", extern_name);
}
return res;
}
match extern_name {
// Minimal console externs
"nyash.console.log" | "env.console.log" | "print" | "nyash.builtin.print" => {
if let Some(arg_id) = args.get(0) {
let v = self.reg_load(*arg_id)?;
println!("{}", v.to_string());
} else {
println!("");
}
Ok(VMValue::Void)
}
// Direct provider calls (bypass hostbridge.extern_invoke)
// Above provider covers env.* family; keep legacy fallbacks below
"exit" => {
let code = if let Some(arg_id) = args.get(0) {
self.reg_load(*arg_id)?.as_integer().unwrap_or(0)
} else {
0
};
std::process::exit(code as i32);
}
"panic" => {
let msg = if let Some(arg_id) = args.get(0) {
self.reg_load(*arg_id)?.to_string()
} else {
"VM panic".to_string()
};
panic!("{}", msg);
}
"hostbridge.extern_invoke" => Err(self.err_invalid("hostbridge.extern_invoke should be routed via extern_provider_dispatch")),
_ => Err(self.err_with_context("extern function", &format!("Unknown: {}", extern_name))),
}
}
}

View File

@ -0,0 +1,13 @@
Layer Guard — handlers/calls
Scope
- Route MIR Call by callee kind (Global/Method/Extern/Legacy only).
- Keep legacy resolver isolated for phased removal.
Allowed
- Use `super::*` and `super::super::utils::*` helpers (e.g., `normalize_arity_suffix`).
Forbidden
- Direct provider/registry imports from runtime or plugins.
- Code generation or MIR building is out of scope.

View File

@ -0,0 +1,27 @@
# handlers/calls — Call Handling Layer
Purpose: isolate call handling by callee kind for the MIR interpreter.
- In scope: routing `handle_call`, percallee executors, legacy name resolution.
- Out of scope: arithmetic/box handlers, memory ops, extern provider registry.
Do not import sibling handler modules directly from here (keep boundaries tight).
Use `super::*` only and call through the interpreter methods.
Files
- `mod.rs`: entry point and callee routing
- `global.rs`: global function calls (Callee::Global)
- `method.rs`: instance/static method calls (Callee::Method)
- `externs.rs`: extern calls (Callee::Extern)
Removal status (Phase 2 complete)
- Unified callee path is default。byname 旧経路は削除済み(`legacy.rs`/`resolution.rs`)。
- `callee=None` の呼び出しは「厳密一致のモジュール関数名」以外は FailFastビルダーで Callee を付与してね)。
Extern SSOT
- `externs.rs` is the runtime SSOT for provider dispatch. Global calls that are extern-like should delegate here (e.g., `env.get`).
- Arity suffix normalization: names like `env.get/1` are accepted and normalized to `env.get` before dispatch (both in Global and ExternCall paths).
Layer Guard
- Scope: call routing only (Global/Method/Extern/Legacy isolation). Do not import provider registries or runtime plugins directly from here.
- Use helpers under `super::super::utils::*` for shared concerns (e.g., `normalize_arity_suffix`).

View File

@ -0,0 +1,63 @@
use super::*;
impl MirInterpreter {
pub(super) fn execute_extern_function(
&mut self,
extern_name: &str,
args: &[ValueId],
) -> Result<VMValue, VMError> {
// Normalize arity suffix (e.g., "env.get/1" -> "env.get")
let base = super::super::utils::normalize_arity_suffix(extern_name);
if let Some(res) = self.extern_provider_dispatch(base, args) {
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
eprintln!("[hb:dispatch:calls] provider {}", base);
}
return res;
}
match base {
// Minimal console externs
"nyash.console.log" | "env.console.log" | "print" | "nyash.builtin.print" => {
if let Some(arg_id) = args.get(0) {
let v = self.reg_load(*arg_id)?;
match &v {
VMValue::Void => println!("null"),
VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
println!("null");
} else if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
println!("{}", sb.value);
} else {
println!("{}", v.to_string());
}
}
VMValue::String(s) => println!("{}", s),
_ => println!("{}", v.to_string()),
}
} else {
println!("");
}
Ok(VMValue::Void)
}
// Direct provider calls (bypass hostbridge.extern_invoke)
// Above provider covers env.* family; keep legacy fallbacks below
"exit" => {
let code = if let Some(arg_id) = args.get(0) {
self.reg_load(*arg_id)?.as_integer().unwrap_or(0)
} else {
0
};
std::process::exit(code as i32);
}
"panic" => {
let msg = if let Some(arg_id) = args.get(0) {
self.reg_load(*arg_id)?.to_string()
} else {
"VM panic".to_string()
};
panic!("{}", msg);
}
"hostbridge.extern_invoke" => Err(self.err_invalid("hostbridge.extern_invoke should be routed via extern_provider_dispatch")),
_ => Err(self.err_with_context("extern function", &format!("Unknown: {}", extern_name))),
}
}
}

View File

@ -0,0 +1,115 @@
use super::*;
impl MirInterpreter {
pub(super) fn execute_global_function(
&mut self,
func_name: &str,
args: &[ValueId],
) -> Result<VMValue, VMError> {
// Normalize arity suffix for extern-like dispatch, but keep original name
// for module-local function table lookup (functions may carry arity suffix).
let base = super::super::utils::normalize_arity_suffix(func_name);
// Module-local/global function: execute by function table if present (use original name)
if let Some(func) = self.functions.get(func_name).cloned() {
let mut argv: Vec<VMValue> = Vec::with_capacity(args.len());
for a in args { argv.push(self.reg_load(*a)?); }
return self.exec_function_inner(&func, Some(&argv));
}
match base {
// Console-like globals
"print" | "nyash.builtin.print" => {
// Reuse extern handler for consistency with other console names
return self.execute_extern_function("print", args);
}
"error" => {
if let Some(arg_id) = args.get(0) {
let val = self.reg_load(*arg_id)?;
eprintln!("Error: {}", val.to_string());
}
return Ok(VMValue::Void);
}
"panic" => {
return self.execute_extern_function("panic", args);
}
"exit" => {
return self.execute_extern_function("exit", args);
}
"env.get" => {
// Route env.get global to extern handler
return self.execute_extern_function("env.get", args);
}
"hostbridge.extern_invoke" => {
// SSOT: delegate to extern dispatcher (provider)
return self.execute_extern_function("hostbridge.extern_invoke", args);
}
// LLVM harness providers (direct)
"env.mirbuilder.emit" | "env.mirbuilder.emit/1" => {
if let Some(a0) = args.get(0) {
let s = self.reg_load(*a0)?.to_string();
match crate::host_providers::mir_builder::program_json_to_mir_json(&s) {
Ok(out) => Ok(VMValue::String(out)),
Err(e) => Err(self.err_with_context("env.mirbuilder.emit", &e.to_string())),
}
} else {
Err(self.err_invalid("env.mirbuilder.emit expects 1 arg"))
}
}
"env.codegen.emit_object" | "env.codegen.emit_object/1" => {
if let Some(a0) = args.get(0) {
let s = self.reg_load(*a0)?.to_string();
let opts = crate::host_providers::llvm_codegen::Opts {
out: None,
nyrt: std::env::var("NYASH_EMIT_EXE_NYRT").ok().map(std::path::PathBuf::from),
opt_level: std::env::var("HAKO_LLVM_OPT_LEVEL").ok().or(Some("0".to_string())),
timeout_ms: None,
};
match crate::host_providers::llvm_codegen::mir_json_to_object(&s, opts) {
Ok(p) => Ok(VMValue::String(p.to_string_lossy().into_owned())),
Err(e) => Err(self.err_with_context("env.codegen.emit_object", &e.to_string())),
}
} else {
Err(self.err_invalid("env.codegen.emit_object expects 1 arg"))
}
}
"env.codegen.link_object" | "env.codegen.link_object/3" => {
// C-API route only; args[2] is expected to be an ArrayBox [obj_path, exe_out?]
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") ||
std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() != Some("1") {
return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
}
if args.len() < 3 { return Err(self.err_arg_count("env.codegen.link_object", 3, args.len())); }
let v = self.reg_load(args[2])?;
let (obj_path, exe_out) = match v {
VMValue::BoxRef(b) => {
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0));
let elem0 = ab.get(idx0).to_string_box().value;
let mut exe: Option<String> = None;
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1));
let e1 = ab.get(idx1).to_string_box().value;
if !e1.is_empty() { exe = Some(e1); }
(elem0, exe)
} else { (b.to_string_box().value, None) }
}
_ => (v.to_string(), None),
};
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
let obj = std::path::PathBuf::from(obj_path);
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) {
Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
Err(e) => Err(self.err_with_context("env.codegen.link_object", &e.to_string()))
}
}
"nyash.builtin.error" => {
if let Some(arg_id) = args.get(0) {
let val = self.reg_load(*arg_id)?;
eprintln!("Error: {}", val.to_string());
}
Ok(VMValue::Void)
}
_ => Err(self.err_with_context("global function", &format!("Unknown: {}", func_name))),
}
}
}

View File

@ -0,0 +1,234 @@
use super::*;
impl MirInterpreter {
pub(super) fn execute_method_callee(
&mut self,
box_name: &str,
method: &str,
receiver: &Option<ValueId>,
args: &[ValueId],
) -> Result<VMValue, VMError> {
if let Some(recv_id) = receiver {
// Primary: load receiver by id. If undefined due to builder localization gap,
// try to auto-locate the most recent `NewBox <box_name>` in the current block
// (same fn/last_block) and use its dst as the receiver. This is a structural
// recovery, not a by-name fallback, and keeps semantics stable for plugin boxes.
let recv_val = match self.reg_load(*recv_id) {
Ok(v) => v,
Err(e) => {
// Attempt structured autoscan for receiver in current block
if let (Some(cur_fn), Some(bb)) = (self.cur_fn.clone(), self.last_block) {
if let Some(func) = self.functions.get(&cur_fn) {
if let Some(block) = func.blocks.get(&bb) {
let mut last_recv: Option<ValueId> = None;
for inst in &block.instructions {
if let crate::mir::MirInstruction::NewBox { dst, box_type, .. } = inst {
if box_type == box_name { last_recv = Some(*dst); }
}
}
if let Some(rid) = last_recv {
if let Ok(v) = self.reg_load(rid) { v } else { return Err(e); }
} else {
// Dev fallback (guarded): use args[0] as surrogate receiver if explicitly allowed
let tolerate = std::env::var("NYASH_VM_RECV_ARG_FALLBACK").ok().as_deref() == Some("1")
|| std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1");
if tolerate { if let Some(a0) = args.get(0) { self.reg_load(*a0)? } else { return Err(e); } }
else { return Err(e); }
}
} else {
return Err(e);
}
} else {
return Err(e);
}
} else {
// Dev fallback (guarded): use args[0] as surrogate receiver if explicitly allowed
let tolerate = std::env::var("NYASH_VM_RECV_ARG_FALLBACK").ok().as_deref() == Some("1")
|| std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1");
if tolerate { if let Some(a0) = args.get(0) { self.reg_load(*a0)? } else { return Err(e); } }
else { return Err(e); }
}
}
};
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 {
"birth" => { return Ok(VMValue::Void); }
"push" => {
if let Some(a0) = args.get(0) {
let v = self.load_as_box(*a0)?;
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.load_as_box(*a0)?;
let ret = arr.get(idx);
return Ok(VMValue::from_nyash_box(ret));
}
}
"set" => {
if args.len() >= 2 {
let idx = self.load_as_box(args[0])?;
let val = self.load_as_box(args[1])?;
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.to_string(), 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());
eprintln!("[vm-trace] mcall {} argv0={:?}", method, a0);
}
let out = self.execute_method_call(&recv_val, method, args)?;
if dev_trace && is_kw {
eprintln!("[vm-trace] mret {} -> {:?}", method, out);
}
Ok(out)
} else {
// Receiver not provided: try static singleton instance for the box (methodize PoC fallback)
if self.static_box_decls.contains_key(box_name) {
let instance = self.ensure_static_box_instance(box_name)?;
let recv_val = VMValue::from_nyash_box(Box::new(instance.clone()));
return self.execute_method_call(&recv_val, method, args);
}
Err(self.err_with_context("Method call", &format!("missing receiver for {}", method)))
}
}
fn execute_method_call(
&mut self,
receiver: &VMValue,
method: &str,
args: &[ValueId],
) -> Result<VMValue, VMError> {
match receiver {
VMValue::String(s) => match method {
"length" => Ok(VMValue::Integer(s.len() as i64)),
"concat" => {
if let Some(arg_id) = args.get(0) {
let arg_val = self.reg_load(*arg_id)?;
let new_str = format!("{}{}", s, arg_val.to_string());
Ok(VMValue::String(new_str))
} else {
Err(self.err_invalid("concat requires 1 argument"))
}
}
"replace" => {
if args.len() == 2 {
let old = self.reg_load(args[0])?.to_string();
let new = self.reg_load(args[1])?.to_string();
Ok(VMValue::String(s.replace(&old, &new)))
} else {
Err(self.err_invalid("replace requires 2 arguments"))
}
}
"indexOf" => {
if let Some(arg_id) = args.get(0) {
let needle = self.reg_load(*arg_id)?.to_string();
let idx = s.find(&needle).map(|i| i as i64).unwrap_or(-1);
Ok(VMValue::Integer(idx))
} else {
Err(self.err_invalid("indexOf requires 1 argument"))
}
}
"lastIndexOf" => {
if let Some(arg_id) = args.get(0) {
let needle = self.reg_load(*arg_id)?.to_string();
let idx = s.rfind(&needle).map(|i| i as i64).unwrap_or(-1);
Ok(VMValue::Integer(idx))
} else {
Err(self.err_invalid("lastIndexOf requires 1 argument"))
}
}
"substring" => {
let start = if let Some(a0) = args.get(0) {
self.reg_load(*a0)?.as_integer().unwrap_or(0)
} else { 0 };
let end = if let Some(a1) = args.get(1) {
self.reg_load(*a1)?.as_integer().unwrap_or(s.len() as i64)
} else { s.len() as i64 };
let len = s.len() as i64;
let i0 = start.max(0).min(len) as usize;
let i1 = end.max(0).min(len) as usize;
if i0 > i1 { return Ok(VMValue::String(String::new())); }
// Note: operating on bytes; Nyash strings are UTF8, but tests are ASCII only here
let bytes = s.as_bytes();
let sub = String::from_utf8(bytes[i0..i1].to_vec()).unwrap_or_default();
Ok(VMValue::String(sub))
}
_ => Err(self.err_method_not_found("String", method)),
},
VMValue::BoxRef(box_ref) => {
// Try builtin StringBox first
if let Some(string_box) = box_ref
.as_any()
.downcast_ref::<crate::boxes::string_box::StringBox>()
{
match method {
"lastIndexOf" => {
if let Some(arg_id) = args.get(0) {
let needle = self.reg_load(*arg_id)?.to_string();
let result_box = string_box.lastIndexOf(&needle);
Ok(VMValue::from_nyash_box(result_box))
} else {
Err(self.err_invalid("lastIndexOf requires 1 argument"))
}
}
"indexOf" | "find" => {
if let Some(arg_id) = args.get(0) {
let needle = self.reg_load(*arg_id)?.to_string();
let result_box = string_box.find(&needle);
Ok(VMValue::from_nyash_box(result_box))
} else {
Err(self.err_invalid("indexOf/find requires 1 argument"))
}
}
_ => Err(self.err_method_not_found("StringBox", method)),
}
} else if let Some(p) = box_ref
.as_any()
.downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>()
{
let host = crate::runtime::plugin_loader_unified::get_global_plugin_host();
let host = host.read().unwrap();
let argv = self.load_args_as_boxes(args)?;
match host.invoke_instance_method(
&p.box_type,
method,
p.inner.instance_id,
&argv,
) {
Ok(Some(ret)) => Ok(VMValue::from_nyash_box(ret)),
Ok(None) => Ok(VMValue::Void),
Err(e) => Err(self.err_with_context(
&format!("Plugin method {}.{}", p.box_type, method),
&format!("{:?}", e)
)),
}
} else {
Err(self.err_method_not_found(&box_ref.type_name(), method))
}
}
_ => Err(self.err_with_context("method call", &format!("{} not supported on {:?}", method, receiver))),
}
}
}

View File

@ -0,0 +1,83 @@
//! Call handling (split from handlers/calls.rs)
//! - Route by Callee kind
//! - Keep legacy path isolated for phased removal
use super::*;
mod global;
mod method;
mod externs;
// legacy by-name resolver has been removed (Phase 2 complete)
impl MirInterpreter {
pub(super) fn handle_call(
&mut self,
dst: Option<ValueId>,
func: ValueId,
callee: Option<&Callee>,
args: &[ValueId],
) -> Result<(), VMError> {
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
match callee {
Some(Callee::Global(n)) => eprintln!("[hb:path] call Callee::Global {} argc={}", n, args.len()),
Some(Callee::Method{ box_name, method, ..}) => eprintln!("[hb:path] call Callee::Method {}.{} argc={}", box_name, method, args.len()),
Some(Callee::Constructor{ box_type }) => eprintln!("[hb:path] call Callee::Constructor {} argc={}", box_type, args.len()),
Some(Callee::Closure{ .. }) => eprintln!("[hb:path] call Callee::Closure argc={}", args.len()),
Some(Callee::Value(_)) => eprintln!("[hb:path] call Callee::Value argc={}", args.len()),
Some(Callee::Extern(n)) => eprintln!("[hb:path] call Callee::Extern {} argc={}", n, args.len()),
None => eprintln!("[hb:path] call Legacy func_id={:?} argc={}", func, args.len()),
}
}
// SSOT fast-path: route hostbridge.extern_invoke to extern dispatcher regardless of resolution form
if let Some(Callee::Global(func_name)) = callee {
if func_name == "hostbridge.extern_invoke" || func_name.starts_with("hostbridge.extern_invoke/") {
let v = self.execute_extern_function("hostbridge.extern_invoke", args)?;
self.write_result(dst, v);
return Ok(());
}
}
let call_result = if let Some(callee_type) = callee {
self.execute_callee_call(callee_type, args)?
} else {
// Fast path: allow exact module function calls without legacy resolver.
let name_val = self.reg_load(func)?;
if let VMValue::String(ref s) = name_val {
if let Some(f) = self.functions.get(s).cloned() {
let mut argv: Vec<VMValue> = Vec::with_capacity(args.len());
for a in args { argv.push(self.reg_load(*a)?); }
self.exec_function_inner(&f, Some(&argv))?
} else {
return Err(self.err_with_context("call", &format!(
"unknown function '{}' (by-name calls unsupported). attach Callee in builder or define the function",
s
)));
}
} else {
return Err(self.err_with_context("call", "by-name calls unsupported without Callee attachment"));
}
};
self.write_result(dst, call_result);
Ok(())
}
pub(super) fn execute_callee_call(
&mut self,
callee: &Callee,
args: &[ValueId],
) -> Result<VMValue, VMError> {
match callee {
Callee::Global(func_name) => self.execute_global_function(func_name, args),
Callee::Method { box_name, method, receiver, certainty: _ } => {
self.execute_method_callee(box_name, method, receiver, args)
}
Callee::Constructor { box_type } => Err(self.err_unsupported(&format!("Constructor calls for {}", box_type))),
Callee::Closure { .. } => Err(self.err_unsupported("Closure creation in VM")),
Callee::Value(func_val_id) => {
let _ = self.reg_load(*func_val_id)?;
Err(self.err_unsupported("First-class function calls in VM"))
}
Callee::Extern(extern_name) => self.execute_extern_function(extern_name, args),
}
}
}

View File

@ -1,5 +1,5 @@
use super::*; use super::*;
use super::super::utils::*; use crate::backend::mir_interpreter::utils::error_helpers::ErrorBuilder;
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
impl MirInterpreter { impl MirInterpreter {
@ -56,18 +56,81 @@ impl MirInterpreter {
match extern_name { match extern_name {
// Console family (minimal) // Console family (minimal)
"nyash.console.log" | "env.console.log" | "print" | "nyash.builtin.print" => { "nyash.console.log" | "env.console.log" | "print" | "nyash.builtin.print" => {
let s = if let Some(a0) = args.get(0) { self.reg_load(*a0).ok() } else { None }; if let Some(a0) = args.get(0) {
if let Some(v) = s { println!("{}", v.to_string()); } else { println!(""); } let v = self.reg_load(*a0).ok();
if let Some(v) = v {
match &v {
VMValue::Void => println!("null"),
VMValue::String(s) => println!("{}", s),
VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
println!("null");
} else if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
println!("{}", sb.value);
} else {
println!("{}", v.to_string());
}
}
_ => println!("{}", v.to_string()),
}
} else {
println!("");
}
} else {
println!("");
}
Some(Ok(VMValue::Void)) Some(Ok(VMValue::Void))
} }
"env.console.warn" | "nyash.console.warn" => { "env.console.warn" | "nyash.console.warn" => {
let s = if let Some(a0) = args.get(0) { self.reg_load(*a0).ok() } else { None }; if let Some(a0) = args.get(0) {
if let Some(v) = s { eprintln!("[warn] {}", v.to_string()); } else { eprintln!("[warn]"); } let v = self.reg_load(*a0).ok();
if let Some(v) = v {
match &v {
VMValue::Void => eprintln!("[warn] null"),
VMValue::String(s) => eprintln!("[warn] {}", s),
VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
eprintln!("[warn] null");
} else if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
eprintln!("[warn] {}", sb.value);
} else {
eprintln!("[warn] {}", v.to_string());
}
}
_ => eprintln!("[warn] {}", v.to_string()),
}
} else {
eprintln!("[warn]");
}
} else {
eprintln!("[warn]");
}
Some(Ok(VMValue::Void)) Some(Ok(VMValue::Void))
} }
"env.console.error" | "nyash.console.error" => { "env.console.error" | "nyash.console.error" => {
let s = if let Some(a0) = args.get(0) { self.reg_load(*a0).ok() } else { None }; if let Some(a0) = args.get(0) {
if let Some(v) = s { eprintln!("[error] {}", v.to_string()); } else { eprintln!("[error]"); } let v = self.reg_load(*a0).ok();
if let Some(v) = v {
match &v {
VMValue::Void => eprintln!("[error] null"),
VMValue::String(s) => eprintln!("[error] {}", s),
VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
eprintln!("[error] null");
} else if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
eprintln!("[error] {}", sb.value);
} else {
eprintln!("[error] {}", v.to_string());
}
}
_ => eprintln!("[error] {}", v.to_string()),
}
} else {
eprintln!("[error]");
}
} else {
eprintln!("[error]");
}
Some(Ok(VMValue::Void)) Some(Ok(VMValue::Void))
} }
// Extern providers (env.mirbuilder / env.codegen) // Extern providers (env.mirbuilder / env.codegen)

View File

@ -1,6 +1,5 @@
use super::*; use super::*;
use super::super::utils::*; use serde_json::Value as JsonValue;
use serde_json::{Value as JsonValue, Map as JsonMap};
impl MirInterpreter { impl MirInterpreter {
#[inline] #[inline]
@ -25,7 +24,9 @@ impl MirInterpreter {
method: &str, method: &str,
args: &[ValueId], args: &[ValueId],
) -> Result<(), VMError> { ) -> Result<(), VMError> {
match (iface, method) { // Normalize method arity suffix (e.g., get/1 -> get)
let mbase = super::super::utils::normalize_arity_suffix(method);
match (iface, mbase) {
("env", "get") => { ("env", "get") => {
if let Some(a0) = args.get(0) { if let Some(a0) = args.get(0) {
let key = self.reg_load(*a0)?.to_string(); let key = self.reg_load(*a0)?.to_string();

View File

@ -1,5 +1,4 @@
use super::*; use super::*;
use super::super::utils::*;
impl MirInterpreter { impl MirInterpreter {
pub(super) fn handle_ref_set( pub(super) fn handle_ref_set(

View File

@ -1,5 +1,4 @@
use super::*; use super::*;
use super::super::utils::*;
impl MirInterpreter { impl MirInterpreter {
pub(super) fn handle_debug(&mut self, message: &str, value: ValueId) -> Result<(), VMError> { pub(super) fn handle_debug(&mut self, message: &str, value: ValueId) -> Result<(), VMError> {

View File

@ -20,7 +20,6 @@ mod boxes_object_fields;
mod boxes_instance; mod boxes_instance;
mod boxes_plugin; mod boxes_plugin;
mod boxes_void_guards; mod boxes_void_guards;
mod call_resolution;
mod calls; mod calls;
mod externals; mod externals;
mod extern_provider; mod extern_provider;

View File

@ -5,11 +5,8 @@ pub mod arg_validation;
pub mod receiver_helpers; pub mod receiver_helpers;
pub mod error_helpers; pub mod error_helpers;
pub mod conversion_helpers; pub mod conversion_helpers;
pub mod naming;
// Phase 21.2: adapter_dev removed - all adapter functions now in .hako implementation // Phase 21.2: adapter_dev removed - all adapter functions now in .hako implementation
// Re-export for convenience // Selective re-export (only naming is widely used via utils::normalize_arity_suffix)
pub use destination_helpers::*; pub use naming::*;
pub use arg_validation::*;
pub use receiver_helpers::*;
pub use error_helpers::*;
pub use conversion_helpers::*;

View File

@ -0,0 +1,15 @@
//! Naming helpers shared in MIR Interpreter handlers.
/// Normalize an optional arity suffix from a function/method name.
/// Examples:
/// - "env.get/1" -> "env.get"
/// - "hostbridge.extern_invoke/3" -> "hostbridge.extern_invoke"
/// - "print" -> "print"
#[inline]
pub fn normalize_arity_suffix(name: &str) -> &str {
match name.split_once('/') {
Some((base, _)) => base,
None => name,
}
}

View File

@ -133,6 +133,10 @@ pub fn verify_allow_no_phi() -> bool {
/// and the merge block itself must not introduce a self-copy to the merged destination. /// and the merge block itself must not introduce a self-copy to the merged destination.
pub fn verify_edge_copy_strict() -> bool { env_bool("NYASH_VERIFY_EDGE_COPY_STRICT") } pub fn verify_edge_copy_strict() -> bool { env_bool("NYASH_VERIFY_EDGE_COPY_STRICT") }
/// Enforce purity of return blocks: no side-effecting instructions allowed before Return
/// Default: OFF. Enable with NYASH_VERIFY_RET_PURITY=1 in dev/profiling sessions.
pub fn verify_ret_purity() -> bool { env_bool("NYASH_VERIFY_RET_PURITY") }
// ---- LLVM harness toggle (llvmlite) ---- // ---- LLVM harness toggle (llvmlite) ----
pub fn llvm_use_harness() -> bool { pub fn llvm_use_harness() -> bool {
// Phase 15: デフォルトONLLVMバックエンドはPythonハーネス使用 // Phase 15: デフォルトONLLVMバックエンドはPythonハーネス使用
@ -170,6 +174,8 @@ pub fn env_bool_default(key: &str, default: bool) -> bool {
/// Set NYASH_FAIL_FAST=0 to temporarily allow legacy fallbacks during bring-up. /// Set NYASH_FAIL_FAST=0 to temporarily allow legacy fallbacks during bring-up.
pub fn fail_fast() -> bool { env_bool_default("NYASH_FAIL_FAST", true) } pub fn fail_fast() -> bool { env_bool_default("NYASH_FAIL_FAST", true) }
// VM legacy by-name call fallback was removed (Phase 2 complete).
// ---- Phase 11.8 MIR cleanup toggles ---- // ---- Phase 11.8 MIR cleanup toggles ----
/// Core-13 minimal MIR mode toggle /// Core-13 minimal MIR mode toggle
/// Default: ON (unless explicitly disabled with NYASH_MIR_CORE13=0) /// Default: ON (unless explicitly disabled with NYASH_MIR_CORE13=0)
@ -226,6 +232,11 @@ pub fn opt_diag_fail() -> bool {
std::env::var("NYASH_OPT_DIAG_FAIL").is_ok() std::env::var("NYASH_OPT_DIAG_FAIL").is_ok()
} }
// ---- Legacy compatibility (dev-only) ----
/// Enable legacy InstanceBox fields (SharedNyashBox map) for compatibility.
/// Default: OFF. Set NYASH_LEGACY_FIELDS_ENABLE=1 to materialize and use legacy fields.
pub fn legacy_fields_enable() -> bool { env_bool("NYASH_LEGACY_FIELDS_ENABLE") }
// ---- GC/Runtime tracing (execution-affecting visibility) ---- // ---- GC/Runtime tracing (execution-affecting visibility) ----
pub fn gc_trace() -> bool { env_bool("NYASH_GC_TRACE") } pub fn gc_trace() -> bool { env_bool("NYASH_GC_TRACE") }
pub fn gc_barrier_trace() -> bool { env_bool("NYASH_GC_BARRIER_TRACE") } pub fn gc_barrier_trace() -> bool { env_bool("NYASH_GC_BARRIER_TRACE") }

View File

@ -99,6 +99,7 @@ impl InstanceBox {
); );
} }
let legacy_enabled = crate::config::env::legacy_fields_enable();
Self { Self {
class_name, class_name,
fields_ng: Arc::new(Mutex::new(field_map)), fields_ng: Arc::new(Mutex::new(field_map)),
@ -106,8 +107,8 @@ impl InstanceBox {
inner_content: None, // ユーザー定義は内包Boxなし inner_content: None, // ユーザー定義は内包Boxなし
base: BoxBase::new(), base: BoxBase::new(),
finalized: Arc::new(Mutex::new(false)), finalized: Arc::new(Mutex::new(false)),
// レガシー互換フィールド // レガシー互換フィールド既定OFF
fields: Some(Arc::new(Mutex::new(legacy_field_map))), fields: if legacy_enabled { Some(Arc::new(Mutex::new(legacy_field_map))) } else { None },
init_field_order: fields, init_field_order: fields,
weak_fields_union: std::collections::HashSet::new(), weak_fields_union: std::collections::HashSet::new(),
in_finalization: Arc::new(Mutex::new(false)), in_finalization: Arc::new(Mutex::new(false)),
@ -233,7 +234,8 @@ impl InstanceBox {
fields.lock().unwrap().insert(field_name, arc_box); fields.lock().unwrap().insert(field_name, arc_box);
Ok(()) Ok(())
} else { } else {
Err("Legacy fields not initialized".to_string()) // 既定OFFのため何もしない互換不要。必要なら NYASH_LEGACY_FIELDS_ENABLE=1 を設定。
Ok(())
} }
} }
@ -270,7 +272,12 @@ impl InstanceBox {
Ok(()) Ok(())
} else { } else {
Err("Legacy fields not initialized".to_string()) // 既定OFF: fields_ng のみ更新Null 占位。整合を壊さないようにOK返す。
self.fields_ng
.lock()
.unwrap()
.insert(field_name.to_string(), NyashValue::Null);
Ok(())
} }
} }

View File

@ -62,6 +62,14 @@ def lower_function(builder, func_data: Dict[str, Any]):
builder.resolver.string_literals.clear() builder.resolver.string_literals.clear()
if hasattr(builder.resolver, 'string_ptrs'): if hasattr(builder.resolver, 'string_ptrs'):
builder.resolver.string_ptrs.clear() builder.resolver.string_ptrs.clear()
if hasattr(builder.resolver, 'length_cache'):
builder.resolver.length_cache.clear()
# Also clear newbox→string-arg hints per function to avoid leakage
try:
if hasattr(builder.resolver, 'newbox_string_args') and isinstance(builder.resolver.newbox_string_args, dict):
builder.resolver.newbox_string_args.clear()
except Exception:
pass
except Exception: except Exception:
pass pass

View File

@ -6,8 +6,30 @@ Handles +, -, *, /, %, &, |, ^, <<, >>
import llvmlite.ir as ir import llvmlite.ir as ir
from typing import Dict, Optional, Any from typing import Dict, Optional, Any
from utils.values import resolve_i64_strict from utils.values import resolve_i64_strict
import os
from .compare import lower_compare from .compare import lower_compare
import llvmlite.ir as ir
def _canonicalize_i64(builder: ir.IRBuilder, value, vid, vmap: Dict[int, ir.Value], hint: str):
"""Normalize integers/pointers to i64 and cache per value id for FAST_INT paths."""
if value is None:
return None
target = ir.IntType(64)
try:
vtype = value.type
except Exception:
vtype = None
if isinstance(vtype, ir.PointerType):
value = builder.ptrtoint(value, target, name=f"{hint}_p2i_{vid}")
elif isinstance(vtype, ir.IntType):
width = vtype.width
if width < 64:
value = builder.zext(value, target, name=f"{hint}_zext_{vid}")
elif width > 64:
value = builder.trunc(value, target, name=f"{hint}_trunc_{vid}")
if isinstance(vid, int):
vmap[vid] = value
return value
def lower_binop( def lower_binop(
builder: ir.IRBuilder, builder: ir.IRBuilder,
@ -37,10 +59,20 @@ def lower_binop(
vmap: Value map vmap: Value map
current_block: Current basic block current_block: Current basic block
""" """
# Resolve operands as i64 (using resolver when available) # Resolve operands as i64
# For now, simple vmap lookup fast_int = os.environ.get('NYASH_LLVM_FAST_INT') == '1'
lhs_val = None
rhs_val = None
if fast_int:
# Prefer same-block SSA directly to avoid resolver/PHI materialization cost in hot loops
lhs_val = vmap.get(lhs)
rhs_val = vmap.get(rhs)
if lhs_val is None:
lhs_val = resolve_i64_strict(resolver, lhs, current_block, preds, block_end_values, vmap, bb_map) lhs_val = resolve_i64_strict(resolver, lhs, current_block, preds, block_end_values, vmap, bb_map)
if rhs_val is None:
rhs_val = resolve_i64_strict(resolver, rhs, current_block, preds, block_end_values, vmap, bb_map) rhs_val = resolve_i64_strict(resolver, rhs, current_block, preds, block_end_values, vmap, bb_map)
lhs_val = _canonicalize_i64(builder, lhs_val, lhs, vmap, "bin_lhs")
rhs_val = _canonicalize_i64(builder, rhs_val, rhs, vmap, "bin_rhs")
if lhs_val is None: if lhs_val is None:
lhs_val = ir.Constant(ir.IntType(64), 0) lhs_val = ir.Constant(ir.IntType(64), 0)
if rhs_val is None: if rhs_val is None:

View File

@ -51,6 +51,14 @@ def lower_boxcall(
bb_map=None, bb_map=None,
ctx: Optional[Any] = None, ctx: Optional[Any] = None,
) -> None: ) -> None:
# Guard against emitting after a terminator: create continuation block if needed.
try:
if builder.block is not None and getattr(builder.block, 'terminator', None) is not None:
func = builder.block.parent
cont = func.append_basic_block(name=f"cont_bb_{builder.block.name}")
builder.position_at_end(cont)
except Exception:
pass
""" """
Lower MIR BoxCall instruction Lower MIR BoxCall instruction
@ -124,6 +132,56 @@ def lower_boxcall(
fast_on = os.environ.get('NYASH_LLVM_FAST') == '1' fast_on = os.environ.get('NYASH_LLVM_FAST') == '1'
except Exception: except Exception:
fast_on = False fast_on = False
def _cache_len(val):
if not fast_on or resolver is None or dst_vid is None or box_vid is None:
return
cache = getattr(resolver, 'length_cache', None)
if cache is None:
return
try:
cache[int(box_vid)] = val
except Exception:
pass
if fast_on and resolver is not None and dst_vid is not None and box_vid is not None:
cache = getattr(resolver, 'length_cache', None)
if cache is not None:
try:
cached = cache.get(int(box_vid))
except Exception:
cached = None
if cached is not None:
vmap[dst_vid] = cached
return
# Ultra-fast: literal length folding when receiver originates from a string literal.
# Check resolver.newbox_string_args[recv] -> arg_vid -> resolver.string_literals[arg_vid]
if fast_on and dst_vid is not None and resolver is not None:
try:
arg_vid = None
if hasattr(resolver, 'newbox_string_args'):
arg_vid = resolver.newbox_string_args.get(int(box_vid))
# Case A: newbox(StringBox, const)
if arg_vid is not None and hasattr(resolver, 'string_literals'):
lit = resolver.string_literals.get(int(arg_vid))
if isinstance(lit, str):
# Mode: bytes or code points
use_cp = os.environ.get('NYASH_STR_CP') == '1'
n = len(lit) if use_cp else len(lit.encode('utf-8'))
const_len = ir.Constant(ir.IntType(64), n)
vmap[dst_vid] = const_len
_cache_len(const_len)
return
# Case B: receiver itself is a literal-backed handle (const string)
if hasattr(resolver, 'string_literals'):
lit2 = resolver.string_literals.get(int(box_vid))
if isinstance(lit2, str):
use_cp = os.environ.get('NYASH_STR_CP') == '1'
n2 = len(lit2) if use_cp else len(lit2.encode('utf-8'))
const_len2 = ir.Constant(ir.IntType(64), n2)
vmap[dst_vid] = const_len2
_cache_len(const_len2)
return
except Exception:
pass
if fast_on and resolver is not None and hasattr(resolver, 'string_ptrs'): if fast_on and resolver is not None and hasattr(resolver, 'string_ptrs'):
try: try:
ptr = resolver.string_ptrs.get(int(box_vid)) ptr = resolver.string_ptrs.get(int(box_vid))

View File

@ -6,9 +6,31 @@ Handles comparison operations (<, >, <=, >=, ==, !=)
import llvmlite.ir as ir import llvmlite.ir as ir
from typing import Dict, Optional, Any from typing import Dict, Optional, Any
from utils.values import resolve_i64_strict from utils.values import resolve_i64_strict
import os
from .externcall import lower_externcall from .externcall import lower_externcall
from trace import values as trace_values from trace import values as trace_values
def _canonicalize_i64(builder: ir.IRBuilder, value, vid, vmap: Dict[int, ir.Value], hint: str):
if value is None:
return None
target = ir.IntType(64)
try:
vtype = value.type
except Exception:
vtype = None
if isinstance(vtype, ir.PointerType):
value = builder.ptrtoint(value, target, name=f"{hint}_p2i_{vid}")
elif isinstance(vtype, ir.IntType):
width = vtype.width
if width < 64:
value = builder.zext(value, target, name=f"{hint}_zext_{vid}")
elif width > 64:
value = builder.trunc(value, target, name=f"{hint}_trunc_{vid}")
if isinstance(vid, int):
vmap[vid] = value
return value
def lower_compare( def lower_compare(
builder: ir.IRBuilder, builder: ir.IRBuilder,
op: str, op: str,
@ -50,8 +72,18 @@ def lower_compare(
pass pass
# Get operands # Get operands
# Prefer same-block SSA from vmap; fallback to resolver for cross-block dominance # Prefer same-block SSA from vmap; fallback to resolver for cross-block dominance
fast_int = os.environ.get('NYASH_LLVM_FAST_INT') == '1'
lhs_val = None
rhs_val = None
if fast_int:
lhs_val = vmap.get(lhs)
rhs_val = vmap.get(rhs)
if lhs_val is None:
lhs_val = resolve_i64_strict(resolver, lhs, current_block, preds, block_end_values, vmap, bb_map) lhs_val = resolve_i64_strict(resolver, lhs, current_block, preds, block_end_values, vmap, bb_map)
if rhs_val is None:
rhs_val = resolve_i64_strict(resolver, rhs, current_block, preds, block_end_values, vmap, bb_map) rhs_val = resolve_i64_strict(resolver, rhs, current_block, preds, block_end_values, vmap, bb_map)
lhs_val = _canonicalize_i64(builder, lhs_val, lhs, vmap, "cmp_lhs")
rhs_val = _canonicalize_i64(builder, rhs_val, rhs, vmap, "cmp_rhs")
i64 = ir.IntType(64) i64 = ir.IntType(64)
i8p = ir.IntType(8).as_pointer() i8p = ir.IntType(8).as_pointer()

View File

@ -21,6 +21,15 @@ def lower_mir_call(owner, builder: ir.IRBuilder, mir_call: Dict[str, Any], dst_v
- resolver: Value resolver instance - resolver: Value resolver instance
""" """
# Guard: avoid emitting after a terminator; if current block is closed, create continuation.
try:
if builder.block is not None and getattr(builder.block, 'terminator', None) is not None:
func = builder.block.parent
cont = func.append_basic_block(name=f"cont_bb_{builder.block.name}")
builder.position_at_end(cont)
except Exception:
pass
# Check if unified call is enabled # Check if unified call is enabled
use_unified = os.getenv("NYASH_MIR_UNIFIED_CALL", "1").lower() not in ("0", "false", "off") use_unified = os.getenv("NYASH_MIR_UNIFIED_CALL", "1").lower() not in ("0", "false", "off")
if not use_unified: if not use_unified:

View File

@ -87,6 +87,12 @@ def lower_newbox(
resolver.newbox_string_args = {} resolver.newbox_string_args = {}
# Map the resulting box handle to the string argument # Map the resulting box handle to the string argument
resolver.newbox_string_args[dst_vid] = args[0] resolver.newbox_string_args[dst_vid] = args[0]
# Hint downstream passes that this dst is string-ish
if hasattr(resolver, 'mark_string'):
try:
resolver.mark_string(int(dst_vid))
except Exception:
resolver.mark_string(dst_vid)
except Exception: except Exception:
pass # Silently ignore failures pass # Silently ignore failures

View File

@ -5,6 +5,11 @@ Handles void and value returns
import llvmlite.ir as ir import llvmlite.ir as ir
from typing import Dict, Optional, Any from typing import Dict, Optional, Any
try:
# Create PHIs at block head to satisfy LLVM invariant
from ..phi_wiring.wiring import phi_at_block_head as _phi_at_block_head
except Exception:
_phi_at_block_head = None
def lower_return( def lower_return(
builder: ir.IRBuilder, builder: ir.IRBuilder,
@ -115,6 +120,7 @@ def lower_return(
zero_like = (str(ret_val) == str(ir.Constant(return_type, 0.0))) zero_like = (str(ret_val) == str(ir.Constant(return_type, 0.0)))
elif isinstance(return_type, ir.PointerType): elif isinstance(return_type, ir.PointerType):
zero_like = (str(ret_val) == str(ir.Constant(return_type, None))) zero_like = (str(ret_val) == str(ir.Constant(return_type, None)))
# Synthesize a PHI for return at the BLOCK HEAD (grouped), not inline.
if zero_like and preds is not None and block_end_values is not None and bb_map is not None and isinstance(value_id, int): if zero_like and preds is not None and block_end_values is not None and bb_map is not None and isinstance(value_id, int):
# Derive current block id from name like 'bb3' # Derive current block id from name like 'bb3'
cur_bid = None cur_bid = None
@ -125,8 +131,8 @@ def lower_return(
if cur_bid is not None: if cur_bid is not None:
incoming = [] incoming = []
for p in preds.get(cur_bid, []): for p in preds.get(cur_bid, []):
# Skip self-loop if p == cur_bid:
if p == cur_bid: continue continue
v = None v = None
try: try:
v = block_end_values.get(p, {}).get(value_id) v = block_end_values.get(p, {}).get(value_id)
@ -138,6 +144,16 @@ def lower_return(
if bblk is not None: if bblk is not None:
incoming.append((v, bblk)) incoming.append((v, bblk))
if incoming: if incoming:
if _phi_at_block_head is not None:
phi = _phi_at_block_head(builder.block, return_type, name=f"ret_phi_{value_id}")
else:
# Fallback: create PHI at block head using a temporary builder
try:
_b = ir.IRBuilder(builder.block)
_b.position_at_start(builder.block)
phi = _b.phi(return_type, name=f"ret_phi_{value_id}")
except Exception:
# As a last resort, create via current builder (may still succeed)
phi = builder.phi(return_type, name=f"ret_phi_{value_id}") phi = builder.phi(return_type, name=f"ret_phi_{value_id}")
for (v, bblk) in incoming: for (v, bblk) in incoming:
phi.add_incoming(v, bblk) phi.add_incoming(v, bblk)
@ -162,4 +178,5 @@ def lower_return(
# Zero extend # Zero extend
ret_val = builder.zext(ret_val, return_type) ret_val = builder.zext(ret_val, return_type)
# Emit return; no further instructions should be emitted in this block
builder.ret(ret_val) builder.ret(ret_val)

View File

@ -121,5 +121,13 @@ def insert_automatic_safepoint(
func_type = ir.FunctionType(void, []) func_type = ir.FunctionType(void, [])
check_func = ir.Function(module, func_type, name="ny_check_safepoint") check_func = ir.Function(module, func_type, name="ny_check_safepoint")
# Guard: do not insert into a terminated block; create continuation if needed
try:
if builder.block is not None and getattr(builder.block, 'terminator', None) is not None:
func = builder.block.parent
cont = func.append_basic_block(name=f"cont_bb_{builder.block.name}")
builder.position_at_end(cont)
except Exception:
pass
# Insert safepoint check # Insert safepoint check
builder.call(check_func, [], name=f"safepoint_{location}") builder.call(check_func, [], name=f"safepoint_{location}")

View File

@ -575,12 +575,27 @@ class NyashLLVMBuilder:
# to avoid divergence between two implementations. # to avoid divergence between two implementations.
def _lower_instruction_list(self, builder: ir.IRBuilder, insts: List[Dict[str, Any]], func: ir.Function): def _lower_instruction_list(self, builder: ir.IRBuilder, insts: List[Dict[str, Any]], func: ir.Function):
"""Lower a flat list of instructions using current builder and function.""" """Lower a flat list of instructions using current builder and function.
for sub in insts: Structural guard: truncate at first terminator (ret/branch/jump) to keep IR valid.
# If current block already has a terminator, create a continuation block """
if builder.block.terminator is not None: # Sanitize: stop at first terminator in the MIR list
cont = func.append_basic_block(name=f"cont_bb_{builder.block.name}") effective: List[Dict[str, Any]] = []
builder.position_at_end(cont) try:
for it in insts:
op = (it or {}).get('op')
effective.append(it)
if op in ('ret', 'branch', 'jump'):
break
except Exception:
effective = list(insts)
for sub in effective:
# If current block already has a terminator, stop lowering further instructions
# to keep LLVM IR structurally valid. Any residuals should be split upstream.
try:
if builder.block is not None and builder.block.terminator is not None:
break
except Exception:
pass
self.lower_instruction(builder, sub, func) self.lower_instruction(builder, sub, func)
def finalize_phis(self): def finalize_phis(self):

View File

@ -47,6 +47,8 @@ class Resolver:
# Track value-ids that are known to represent string handles (i64) # Track value-ids that are known to represent string handles (i64)
# This is a best-effort tag used to decide '+' as string concat when both sides are i64. # This is a best-effort tag used to decide '+' as string concat when both sides are i64.
self.string_ids: set[int] = set() self.string_ids: set[int] = set()
# Cache for repeated string length queries when immutably known
self.length_cache: Dict[int, ir.Value] = {}
# Type shortcuts # Type shortcuts
self.i64 = ir.IntType(64) self.i64 = ir.IntType(64)

View File

@ -0,0 +1,49 @@
#!/usr/bin/env python3
import os
import unittest
import llvmlite.ir as ir
from src.llvm_py.llvm_builder import NyashLLVMBuilder
class TestStrlenFast(unittest.TestCase):
def setUp(self):
# Ensure FAST toggle is ON for this test
os.environ['NYASH_LLVM_FAST'] = '1'
def tearDown(self):
os.environ.pop('NYASH_LLVM_FAST', None)
def test_newbox_string_length_fast_lowering(self):
# Minimal MIR JSON v0-like: const "hello" → newbox StringBox(arg) → boxcall length
mir = {
"functions": [
{
"name": "main",
"params": [],
"blocks": [
{
"id": 1,
"instructions": [
{"op": "const", "dst": 10, "value": {"type": "string", "value": "hello"}},
{"op": "newbox", "dst": 20, "box_type": "StringBox", "args": [10]},
{"op": "boxcall", "dst": 30, "box_val": 20, "method": "length", "args": []},
{"op": "ret", "value": 30},
]
}
]
}
]
}
b = NyashLLVMBuilder()
ir_txt = b.build_from_mir(mir)
# Must reference the neutral kernel symbol for FAST strlen
self.assertIn('declare i64 @nyrt_string_length(i8*, i64)', ir_txt or '')
# And a call site should be present
self.assertIn('call i64 @nyrt_string_length', ir_txt)
if __name__ == '__main__':
unittest.main()

View File

@ -5,7 +5,7 @@
*/ */
use super::{EffectMask, MirInstruction, ValueId}; use super::{EffectMask, MirInstruction, ValueId};
use std::collections::{HashMap, HashSet}; use std::collections::HashSet;
use std::fmt; use std::fmt;
/// Unique identifier for basic blocks within a function /// Unique identifier for basic blocks within a function

View File

@ -442,6 +442,12 @@ impl MirBuilder {
}; };
if let Some(block) = function.get_block_mut(block_id) { if let Some(block) = function.get_block_mut(block_id) {
// Invariant: Call must always carry a Callee (unified path).
if let MirInstruction::Call { callee, .. } = &instruction {
if callee.is_none() {
return Err("builder invariant violated: MirInstruction::Call.callee must be Some (unified call)".into());
}
}
if utils::builder_debug_enabled() { if utils::builder_debug_enabled() {
eprintln!( eprintln!(
"[BUILDER] emit @bb{} -> {}", "[BUILDER] emit @bb{} -> {}",

View File

@ -1,9 +1,8 @@
// Extracted call-related builders from builder.rs to keep files lean // Extracted call-related builders from builder.rs to keep files lean
use super::{Effect, EffectMask, FunctionSignature, MirInstruction, MirType, ValueId}; use super::{Effect, EffectMask, MirInstruction, MirType, ValueId};
use crate::ast::{ASTNode, LiteralValue, MethodCallExpr}; use crate::ast::{ASTNode, LiteralValue};
use crate::mir::definitions::call_unified::{Callee, CallFlags, MirCall}; use crate::mir::definitions::call_unified::Callee;
use crate::mir::TypeOpKind; use crate::mir::TypeOpKind;
use super::call_resolution;
// Import from new modules // Import from new modules
use super::calls::*; use super::calls::*;
@ -116,7 +115,13 @@ impl super::MirBuilder {
Ok(v) => v, Ok(v) => v,
Err(e) => return Err(e), Err(e) => return Err(e),
}; };
self.emit_instruction(MirInstruction::Call { dst: Some(dstv), func: name_const, callee: None, args: args.clone(), effects: EffectMask::IO })?; self.emit_instruction(MirInstruction::Call {
dst: Some(dstv),
func: name_const,
callee: Some(Callee::Global(name.clone())),
args: args.clone(),
effects: EffectMask::IO,
})?;
self.annotate_call_result_from_func_name(dstv, name); self.annotate_call_result_from_func_name(dstv, name);
return Ok(()); return Ok(());
} }
@ -140,7 +145,7 @@ impl super::MirBuilder {
self.emit_instruction(MirInstruction::Call { self.emit_instruction(MirInstruction::Call {
dst: Some(dstv), dst: Some(dstv),
func: name_const, func: name_const,
callee: None, callee: Some(Callee::Global(func_name.clone())),
args: args.clone(), args: args.clone(),
effects: EffectMask::IO, effects: EffectMask::IO,
})?; })?;
@ -231,20 +236,15 @@ impl super::MirBuilder {
// Finalize operands in current block (EmitGuardBox wrapper) // Finalize operands in current block (EmitGuardBox wrapper)
let mut callee = callee; let mut callee = callee;
let mut args_local: Vec<ValueId> = args.clone(); let mut args_local: Vec<ValueId> = args;
crate::mir::builder::emit_guard::finalize_call_operands(self, &mut callee, &mut args_local); crate::mir::builder::emit_guard::finalize_call_operands(self, &mut callee, &mut args_local);
// Create MirCall instruction using the new module (pure data composition) // Create MirCall instruction using the new module (pure data composition)
let mir_call = call_unified::create_mir_call(dst, callee.clone(), args_local.clone()); let mir_call = call_unified::create_mir_call(dst, callee.clone(), args_local.clone());
// Final last-chance LocalSSA just before emission in case any block switch happened
let mut callee2 = callee.clone();
let mut args2 = args_local.clone();
crate::mir::builder::emit_guard::finalize_call_operands(self, &mut callee2, &mut args2);
// Dev trace: show final callee/recv right before emission (guarded) // Dev trace: show final callee/recv right before emission (guarded)
if std::env::var("NYASH_LOCAL_SSA_TRACE").ok().as_deref() == Some("1") || super::utils::builder_debug_enabled() { if std::env::var("NYASH_LOCAL_SSA_TRACE").ok().as_deref() == Some("1") || super::utils::builder_debug_enabled() {
if let Callee::Method { method, receiver, box_name, .. } = &callee2 { if let Callee::Method { method, receiver, box_name, .. } = &callee {
if let Some(r) = receiver { if let Some(r) = receiver {
eprintln!("[vm-call-final] bb={:?} method={} recv=%{} class={}", eprintln!("[vm-call-final] bb={:?} method={} recv=%{} class={}",
self.current_block, method, r.0, box_name); self.current_block, method, r.0, box_name);
@ -252,38 +252,13 @@ impl super::MirBuilder {
} }
} }
// Final forced in-block materialization for receiver just-before emission
// Rationale: ensure a Copy(def) immediately precedes the Call in the same block
let callee2 = match callee2 {
Callee::Method { box_name, method, receiver: Some(r), certainty } => {
if super::utils::builder_debug_enabled() || std::env::var("NYASH_LOCAL_SSA_TRACE").ok().as_deref() == Some("1") {
eprintln!("[call-forced-copy] bb={:?} recv=%{} -> (copy)", self.current_block, r.0);
}
// Insert immediately after PHIs to guarantee position and dominance
let r_forced = crate::mir::builder::schedule::block::BlockScheduleBox::ensure_after_phis_copy(self, r)?;
if super::utils::builder_debug_enabled() || std::env::var("NYASH_LOCAL_SSA_TRACE").ok().as_deref() == Some("1") {
eprintln!("[call-forced-copy] bb={:?} inserted Copy %{} := %{} (after PHIs)", self.current_block, r_forced.0, r.0);
}
Callee::Method { box_name, method, receiver: Some(r_forced), certainty }
}
other => other,
};
// Optional last-chance: emit a tail copy right before we emit Call (keeps order stable)
let callee2 = match callee2 {
Callee::Method { box_name, method, receiver: Some(r), certainty } => {
let r_tail = crate::mir::builder::schedule::block::BlockScheduleBox::emit_before_call_copy(self, r)?;
Callee::Method { box_name, method, receiver: Some(r_tail), certainty }
}
other => other,
};
// For Phase 2: Convert to legacy Call instruction with new callee field (use finalized operands) // For Phase 2: Convert to legacy Call instruction with new callee field (use finalized operands)
let legacy_call = MirInstruction::Call { let legacy_call = MirInstruction::Call {
dst: mir_call.dst, dst: mir_call.dst,
func: ValueId::new(0), // Dummy value for legacy compatibility func: ValueId::new(0), // Dummy value for legacy compatibility
callee: Some(callee2), callee: Some(callee),
args: args2, args: args_local,
effects: mir_call.effects, effects: mir_call.effects,
}; };
@ -345,7 +320,7 @@ impl super::MirBuilder {
self.emit_instruction(MirInstruction::Call { self.emit_instruction(MirInstruction::Call {
dst: Some(actual_dst), dst: Some(actual_dst),
func: name_const, func: name_const,
callee: None, // Legacy mode callee: Some(Callee::Global(name.clone())),
args, args,
effects: EffectMask::IO, effects: EffectMask::IO,
})?; })?;
@ -359,7 +334,7 @@ impl super::MirBuilder {
self.emit_instruction(MirInstruction::Call { self.emit_instruction(MirInstruction::Call {
dst, dst,
func: func_val, func: func_val,
callee: None, // Legacy mode callee: Some(Callee::Value(func_val)),
args, args,
effects: EffectMask::IO, effects: EffectMask::IO,
}) })
@ -524,7 +499,7 @@ impl super::MirBuilder {
if let Err(e) = self.emit_instruction(MirInstruction::Call { if let Err(e) = self.emit_instruction(MirInstruction::Call {
dst: Some(result_id), dst: Some(result_id),
func: fun_val, func: fun_val,
callee: None, // Legacy math function - use old resolution callee: Some(Callee::Global(fun_name.clone())),
args: arg_values, args: arg_values,
effects: EffectMask::READ.add(Effect::ReadHeap) effects: EffectMask::READ.add(Effect::ReadHeap)
}) { return Some(Err(e)); } }) { return Some(Err(e)); }
@ -626,8 +601,8 @@ impl super::MirBuilder {
let (bx, _arity) = matches.remove(0); let (bx, _arity) = matches.remove(0);
let dst = self.value_gen.next(); let dst = self.value_gen.next();
let func_name = format!("{}.{}{}", bx, name, format!("/{}", arg_values.len())); let func_name = format!("{}.{}{}", bx, name, format!("/{}", arg_values.len()));
// Emit legacy global call to the lowered static method function // Emit unified global call to the lowered static method function
self.emit_legacy_call(Some(dst), CallTarget::Global(func_name), arg_values)?; self.emit_unified_call(Some(dst), CallTarget::Global(func_name), arg_values)?;
return Ok(dst); return Ok(dst);
} }
} }
@ -739,18 +714,12 @@ impl super::MirBuilder {
call_args.push(me_id); call_args.push(me_id);
call_args.extend(arg_values.into_iter()); call_args.extend(arg_values.into_iter());
let dst = self.value_gen.next(); let dst = self.value_gen.next();
// Emit function name via NameConstBox // Emit as unified global call to lowered function
let c = match crate::mir::builder::name_const::make_name_const_result(self, &fname) { self.emit_unified_call(
Ok(v) => v, Some(dst),
Err(e) => return Err(e), CallTarget::Global(fname.clone()),
}; call_args,
self.emit_instruction(MirInstruction::Call { )?;
dst: Some(dst),
func: c,
callee: None,
args: call_args,
effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap),
})?;
self.annotate_call_result_from_func_name(dst, &fname); self.annotate_call_result_from_func_name(dst, &fname);
return Ok(dst); return Ok(dst);
} }

View File

@ -0,0 +1,17 @@
# MIR Builder — Calls SSOT
Scope
- This directory is the single source of truth for call shaping in the builder.
- Responsibilities: target resolution, extern mapping, method lookup, flags/effects, MIR emission.
Out of scope
- Runtime dispatch details (VM/LLVM) and legacy by-name resolution. The VM keeps a legacy resolver only behind a dev flag for bring-up.
Contract
- Builder must populate `MirInstruction::Call` with a concrete `Callee` whenever possible.
- Arity and canonical names are normalized here so runtimes can be simple routers.
Phase-3 alignment
- VM call resolver is treated as legacy-only. Default runtime disables by-name fallback.
- Extern interface normalization aligns with `handlers/calls/externs.rs` (runtime SSOT for extern dispatch).

View File

@ -28,13 +28,14 @@ pub fn convert_target_to_callee(
) -> Result<Callee, String> { ) -> Result<Callee, String> {
match target { match target {
CallTarget::Global(name) => { CallTarget::Global(name) => {
// Check if it's a built-in function // Prefer explicit categories; otherwise treat as module-global function
if method_resolution::is_builtin_function(&name) { if method_resolution::is_builtin_function(&name) {
Ok(Callee::Global(name)) Ok(Callee::Global(name))
} else if method_resolution::is_extern_function(&name) { } else if method_resolution::is_extern_function(&name) {
Ok(Callee::Extern(name)) Ok(Callee::Extern(name))
} else { } else {
Err(format!("Unknown global function: {}", name)) // Module-local or static lowered function (e.g., "Box.method/N")
Ok(Callee::Global(name))
} }
}, },

View File

@ -7,7 +7,6 @@
// Core types // Core types
pub mod call_target; pub mod call_target;
pub use call_target::CallTarget;
// Resolution system // Resolution system
pub mod method_resolution; pub mod method_resolution;
@ -27,43 +26,5 @@ pub mod call_unified;
// Call result annotation // Call result annotation
pub mod annotation; pub mod annotation;
// Re-export commonly used items // Re-exports were removed to reduce unused-import warnings.
pub use method_resolution::{ // Use module-qualified paths (e.g., special_handlers::parse_type_name_to_mir) instead.
resolve_call_target,
is_builtin_function,
is_extern_function,
has_method,
};
pub use extern_calls::{
get_env_method_spec,
parse_extern_name,
is_env_interface,
compute_extern_effects,
};
pub use special_handlers::{
is_math_function,
is_typeop_method,
extract_string_literal,
parse_type_name_to_mir,
contains_value_return,
make_function_name_with_arity,
};
pub use function_lowering::{
prepare_method_signature,
prepare_static_method_signature,
generate_method_function_name,
generate_static_method_function_name,
wrap_in_program,
};
pub use call_unified::{
is_unified_call_enabled,
convert_target_to_callee,
compute_call_effects,
create_call_flags,
create_mir_call,
validate_call_args,
};

View File

@ -1,5 +1,5 @@
//! Control-flow entrypoints (if/loop/try/throw) centralized here. //! Control-flow entrypoints (if/loop/try/throw) centralized here.
use super::{ConstValue, Effect, EffectMask, MirInstruction, ValueId}; use super::{Effect, EffectMask, MirInstruction, ValueId};
use crate::ast::ASTNode; use crate::ast::ASTNode;
impl super::MirBuilder { impl super::MirBuilder {

View File

@ -1,5 +1,5 @@
// Declarations lowering: static boxes and box declarations // Declarations lowering: static boxes and box declarations
use super::{ConstValue, MirInstruction, ValueId}; use super::{MirInstruction, ValueId};
use crate::ast::ASTNode; use crate::ast::ASTNode;
use crate::mir::slot_registry::{get_or_assign_type_id, reserve_method_slot}; use crate::mir::slot_registry::{get_or_assign_type_id, reserve_method_slot};
use serde_json; use serde_json;

View File

@ -4,11 +4,24 @@ use crate::mir::ValueId;
/// Finalize call operands (receiver/args) using LocalSSA; thin wrapper to centralize usage. /// Finalize call operands (receiver/args) using LocalSSA; thin wrapper to centralize usage.
pub fn finalize_call_operands(builder: &mut MirBuilder, callee: &mut Callee, args: &mut Vec<ValueId>) { pub fn finalize_call_operands(builder: &mut MirBuilder, callee: &mut Callee, args: &mut Vec<ValueId>) {
// Step 1: LocalSSA materialization for receiver/args
crate::mir::builder::ssa::local::finalize_callee_and_args(builder, callee, args); crate::mir::builder::ssa::local::finalize_callee_and_args(builder, callee, args);
// Step 2: Stabilize receiver placement within the current block
// Ensure a Copy right after PHIs and a tail Copy just before Call emission, so that
// dominance/order invariants are satisfied without duplicating logic at call sites.
if let Callee::Method { box_name, method, receiver: Some(r0), certainty } = callee.clone() {
if let Ok(r_after) = crate::mir::builder::schedule::block::BlockScheduleBox::ensure_after_phis_copy(builder, r0) {
if let Ok(r_tail) = crate::mir::builder::schedule::block::BlockScheduleBox::emit_before_call_copy(builder, r_after) {
*callee = Callee::Method { box_name, method, receiver: Some(r_tail), certainty };
} else {
*callee = Callee::Method { box_name, method, receiver: Some(r_after), certainty };
}
}
}
} }
/// Verify block schedule invariants after emitting a call (dev-only WARNs inside). /// Verify block schedule invariants after emitting a call (dev-only WARNs inside).
pub fn verify_after_call(builder: &mut MirBuilder) { pub fn verify_after_call(builder: &mut MirBuilder) {
crate::mir::builder::schedule::block::BlockScheduleBox::verify_order(builder); crate::mir::builder::schedule::block::BlockScheduleBox::verify_order(builder);
} }

View File

@ -1,5 +1,5 @@
// Expression lowering split from builder.rs to keep files lean // Expression lowering split from builder.rs to keep files lean
use super::{ConstValue, MirInstruction, ValueId}; use super::{MirInstruction, ValueId};
use crate::ast::{ASTNode, AssignStmt, ReturnStmt, BinaryExpr, CallExpr, MethodCallExpr, FieldAccessExpr}; use crate::ast::{ASTNode, AssignStmt, ReturnStmt, BinaryExpr, CallExpr, MethodCallExpr, FieldAccessExpr};
impl super::MirBuilder { impl super::MirBuilder {

View File

@ -27,12 +27,12 @@ impl super::MirBuilder {
)?; )?;
Ok(dst) Ok(dst)
} else { } else {
// Legacy path - keep for compatibility // Unified-off path: still encode callee as Value to avoid by-name resolution
let dst = self.value_gen.next(); let dst = self.value_gen.next();
self.emit_instruction(super::MirInstruction::Call { self.emit_instruction(super::MirInstruction::Call {
dst: Some(dst), dst: Some(dst),
func: callee_id, func: callee_id,
callee: None, // Legacy call expression - use old resolution callee: Some(crate::mir::definitions::call_unified::Callee::Value(callee_id)),
args: arg_ids, args: arg_ids,
effects: super::EffectMask::PURE, effects: super::EffectMask::PURE,
})?; })?;

View File

@ -1,5 +1,5 @@
// Field access and assignment lowering // Field access and assignment lowering
use super::{ConstValue, EffectMask, MirInstruction, ValueId}; use super::{EffectMask, MirInstruction, ValueId};
use crate::ast::ASTNode; use crate::ast::ASTNode;
use crate::mir::slot_registry; use crate::mir::slot_registry;

View File

@ -1,4 +1,4 @@
use super::{ConstValue, MirBuilder, MirInstruction, ValueId}; use super::{MirBuilder, ValueId};
use crate::mir::loop_api::LoopBuilderApi; // for current_block() use crate::mir::loop_api::LoopBuilderApi; // for current_block()
use crate::ast::{ASTNode, BinaryOperator}; use crate::ast::{ASTNode, BinaryOperator};

View File

@ -1,4 +1,4 @@
use super::{EffectMask, FunctionSignature, MirFunction, MirInstruction, MirModule, MirType, ValueId, BasicBlockId, ConstValue}; use super::{EffectMask, FunctionSignature, MirFunction, MirInstruction, MirModule, MirType, ValueId, BasicBlockId};
use crate::ast::ASTNode; use crate::ast::ASTNode;
// Lifecycle routines extracted from builder.rs // Lifecycle routines extracted from builder.rs

View File

@ -6,7 +6,7 @@
use crate::ast::ASTNode; use crate::ast::ASTNode;
use crate::mir::builder::{MirBuilder, ValueId}; use crate::mir::builder::{MirBuilder, ValueId};
use crate::mir::builder::builder_calls::CallTarget; use crate::mir::builder::builder_calls::CallTarget;
use crate::mir::{MirInstruction, TypeOpKind, MirType}; use crate::mir::{MirInstruction, TypeOpKind};
impl MirBuilder { impl MirBuilder {
/// Handle static method calls: BoxName.method(args) /// Handle static method calls: BoxName.method(args)
@ -30,8 +30,8 @@ impl MirBuilder {
eprintln!("[builder] static-call {}", func_name); eprintln!("[builder] static-call {}", func_name);
} }
// Use legacy global-call emission to avoid unified builtin/extern constraints // Emit unified global call to the static-lowered function (module-local)
self.emit_legacy_call(Some(dst), CallTarget::Global(func_name), arg_values)?; self.emit_unified_call(Some(dst), CallTarget::Global(func_name), arg_values)?;
Ok(dst) Ok(dst)
} }

View File

@ -1,4 +1,4 @@
use super::super::{MirBuilder, MirInstruction, MirType, ValueId, BasicBlockId}; use super::super::{MirBuilder, MirType, ValueId, BasicBlockId};
/// Lightweight propagation at PHI when all inputs agree型/起源)。 /// Lightweight propagation at PHI when all inputs agree型/起源)。
/// 仕様は不変: 一致時のみ dst にコピーする(不一致/未知は何もしない)。 /// 仕様は不変: 一致時のみ dst にコピーする(不一致/未知は何もしない)。

View File

@ -1,4 +1,4 @@
use super::super::{ConstValue, Effect, EffectMask, MirBuilder, MirInstruction, ValueId}; use super::super::{MirBuilder, ValueId};
/// Gate: whether instance→function rewrite is enabled. /// Gate: whether instance→function rewrite is enabled.
fn rewrite_enabled() -> bool { fn rewrite_enabled() -> bool {
@ -48,19 +48,17 @@ pub(crate) fn try_known_rewrite(
if !( (module_has || allow_userbox_rewrite) || (from_new_origin && allow_new_origin) ) { if !( (module_has || allow_userbox_rewrite) || (from_new_origin && allow_new_origin) ) {
return None; return None;
} }
// Materialize function call: pass 'me' first, then args // Materialize function call: pass 'me' first, then args (unified call)
let name_const = match crate::mir::builder::name_const::make_name_const_result(builder, &fname) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let mut call_args = Vec::with_capacity(arity + 1); let mut call_args = Vec::with_capacity(arity + 1);
call_args.push(object_value); call_args.push(object_value);
call_args.append(&mut arg_values); call_args.append(&mut arg_values);
crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args);
let dst = builder.value_gen.next(); let dst = builder.value_gen.next();
if let Err(e) = builder.emit_instruction(MirInstruction::Call { if let Err(e) = builder.emit_unified_call(
dst: Some(dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap), Some(dst),
}) { return Some(Err(e)); } crate::mir::builder::builder_calls::CallTarget::Global(fname.clone()),
call_args,
) { return Some(Err(e)); }
// Annotate and emit choose // Annotate and emit choose
let chosen = fname.clone(); let chosen = fname.clone();
builder.annotate_call_result_from_func_name(dst, &chosen); builder.annotate_call_result_from_func_name(dst, &chosen);
@ -95,16 +93,17 @@ pub(crate) fn try_known_rewrite_to_dst(
let fname = crate::mir::builder::calls::function_lowering::generate_method_function_name(cls, method, arity); 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 }; 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; } if !((module_has || allow_userbox_rewrite) || (from_new_origin && allow_new_origin)) { return None; }
let name_const = match crate::mir::builder::name_const::make_name_const_result(builder, &fname) { // unified global function call (module-local)
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let mut call_args = Vec::with_capacity(arity + 1); let mut call_args = Vec::with_capacity(arity + 1);
call_args.push(object_value); call_args.push(object_value);
call_args.append(&mut arg_values); call_args.append(&mut arg_values);
crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args);
let actual_dst = want_dst.unwrap_or_else(|| builder.value_gen.next()); 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)); } if let Err(e) = builder.emit_unified_call(
Some(actual_dst),
crate::mir::builder::builder_calls::CallTarget::Global(fname.clone()),
call_args,
) { return Some(Err(e)); }
builder.annotate_call_result_from_func_name(actual_dst, &fname); builder.annotate_call_result_from_func_name(actual_dst, &fname);
let meta = serde_json::json!({ let meta = serde_json::json!({
"recv_cls": cls, "recv_cls": cls,
@ -145,19 +144,18 @@ pub(crate) fn try_unique_suffix_rewrite(
} else { } else {
return None; return None;
} }
let name_const = match crate::mir::builder::name_const::make_name_const_result(builder, &fname) { // unified
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let mut call_args = Vec::with_capacity(arg_values.len() + 1); let mut call_args = Vec::with_capacity(arg_values.len() + 1);
call_args.push(object_value); // 'me' call_args.push(object_value); // 'me'
let arity_us = arg_values.len(); let arity_us = arg_values.len();
call_args.append(&mut arg_values); call_args.append(&mut arg_values);
crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args);
let dst = builder.value_gen.next(); let dst = builder.value_gen.next();
if let Err(e) = builder.emit_instruction(MirInstruction::Call { if let Err(e) = builder.emit_unified_call(
dst: Some(dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap), Some(dst),
}) { return Some(Err(e)); } crate::mir::builder::builder_calls::CallTarget::Global(fname.clone()),
call_args,
) { return Some(Err(e)); }
builder.annotate_call_result_from_func_name(dst, &fname); builder.annotate_call_result_from_func_name(dst, &fname);
let meta = serde_json::json!({ let meta = serde_json::json!({
"recv_cls": builder.value_origin_newbox.get(&object_value).cloned().unwrap_or_default(), "recv_cls": builder.value_origin_newbox.get(&object_value).cloned().unwrap_or_default(),
@ -195,7 +193,11 @@ pub(crate) fn try_unique_suffix_rewrite_to_dst(
call_args.append(&mut arg_values); call_args.append(&mut arg_values);
crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args);
let actual_dst = want_dst.unwrap_or_else(|| builder.value_gen.next()); 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)); } if let Err(e) = builder.emit_unified_call(
Some(actual_dst),
crate::mir::builder::builder_calls::CallTarget::Global(fname.clone()),
call_args,
) { return Some(Err(e)); }
builder.annotate_call_result_from_func_name(actual_dst, &fname); builder.annotate_call_result_from_func_name(actual_dst, &fname);
let meta = serde_json::json!({ let meta = serde_json::json!({
"recv_cls": builder.value_origin_newbox.get(&object_value).cloned().unwrap_or_default(), "recv_cls": builder.value_origin_newbox.get(&object_value).cloned().unwrap_or_default(),

View File

@ -1,4 +1,4 @@
use super::super::{ConstValue, Effect, EffectMask, MirBuilder, MirInstruction}; use super::super::MirBuilder;
/// Early special-case: toString/stringify → str互換を処理。 /// Early special-case: toString/stringify → str互換を処理。
/// 戻り値: Some(result_id) なら処理済み。None なら通常経路へ委譲。 /// 戻り値: Some(result_id) なら処理済み。None なら通常経路へ委譲。
@ -31,17 +31,16 @@ pub(crate) fn try_early_str_like(
"certainty": "Known", "certainty": "Known",
}); });
super::super::observe::resolve::emit_choose(builder, meta); super::super::observe::resolve::emit_choose(builder, meta);
let name_const = match crate::mir::builder::name_const::make_name_const_result(builder, &chosen) { // unified
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let mut call_args = Vec::with_capacity(1); let mut call_args = Vec::with_capacity(1);
call_args.push(object_value); call_args.push(object_value);
crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args);
let dst = builder.value_gen.next(); let dst = builder.value_gen.next();
if let Err(e) = builder.emit_instruction(MirInstruction::Call { if let Err(e) = builder.emit_unified_call(
dst: Some(dst), func: name_const, callee: None, args: call_args, effects: EffectMask::READ.add(Effect::ReadHeap), Some(dst),
}) { return Some(Err(e)); } crate::mir::builder::builder_calls::CallTarget::Global(chosen.clone()),
call_args,
) { return Some(Err(e)); }
builder.annotate_call_result_from_func_name(dst, &chosen); builder.annotate_call_result_from_func_name(dst, &chosen);
return Some(Ok(dst)); return Some(Ok(dst));
} }
@ -62,15 +61,16 @@ pub(crate) fn try_early_str_like(
"certainty": "Heuristic", "certainty": "Heuristic",
}); });
super::super::observe::resolve::emit_choose(builder, meta); super::super::observe::resolve::emit_choose(builder, meta);
let name_const = match crate::mir::builder::name_const::make_name_const_result(builder, &fname) { // unified
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let mut call_args = Vec::with_capacity(1); let mut call_args = Vec::with_capacity(1);
call_args.push(object_value); call_args.push(object_value);
crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args);
let dst = builder.value_gen.next(); 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)); } if let Err(e) = builder.emit_unified_call(
Some(dst),
crate::mir::builder::builder_calls::CallTarget::Global(fname.clone()),
call_args,
) { return Some(Err(e)); }
builder.annotate_call_result_from_func_name(dst, &fname); builder.annotate_call_result_from_func_name(dst, &fname);
return Some(Ok(dst)); return Some(Ok(dst));
} else if cands.len() > 1 { } else if cands.len() > 1 {
@ -85,15 +85,16 @@ pub(crate) fn try_early_str_like(
"certainty": "Heuristic", "certainty": "Heuristic",
}); });
super::super::observe::resolve::emit_choose(builder, meta); super::super::observe::resolve::emit_choose(builder, meta);
let name_const = match crate::mir::builder::name_const::make_name_const_result(builder, &fname) { // unified
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let mut call_args = Vec::with_capacity(1); let mut call_args = Vec::with_capacity(1);
call_args.push(object_value); call_args.push(object_value);
crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args);
let dst = builder.value_gen.next(); 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)); } if let Err(e) = builder.emit_unified_call(
Some(dst),
crate::mir::builder::builder_calls::CallTarget::Global(fname.clone()),
call_args,
) { return Some(Err(e)); }
builder.annotate_call_result_from_func_name(dst, &fname); builder.annotate_call_result_from_func_name(dst, &fname);
return Some(Ok(dst)); return Some(Ok(dst));
} }
@ -158,7 +159,11 @@ pub(crate) fn try_early_str_like_to_dst(
call_args.push(object_value); call_args.push(object_value);
crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args);
let actual_dst = want_dst.unwrap_or_else(|| builder.value_gen.next()); 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)); } if let Err(e) = builder.emit_unified_call(
Some(actual_dst),
crate::mir::builder::builder_calls::CallTarget::Global(chosen.clone()),
call_args,
) { return Some(Err(e)); }
builder.annotate_call_result_from_func_name(actual_dst, &chosen); builder.annotate_call_result_from_func_name(actual_dst, &chosen);
return Some(Ok(actual_dst)); return Some(Ok(actual_dst));
} }
@ -185,7 +190,11 @@ pub(crate) fn try_early_str_like_to_dst(
call_args.push(object_value); call_args.push(object_value);
crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args);
let actual_dst = want_dst.unwrap_or_else(|| builder.value_gen.next()); 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)); } if let Err(e) = builder.emit_unified_call(
Some(actual_dst),
crate::mir::builder::builder_calls::CallTarget::Global(fname.clone()),
call_args,
) { return Some(Err(e)); }
builder.annotate_call_result_from_func_name(actual_dst, &fname); builder.annotate_call_result_from_func_name(actual_dst, &fname);
return Some(Ok(actual_dst)); return Some(Ok(actual_dst));
} else if cands.len() > 1 { } else if cands.len() > 1 {
@ -208,7 +217,11 @@ pub(crate) fn try_early_str_like_to_dst(
call_args.push(object_value); call_args.push(object_value);
crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args);
let actual_dst = want_dst.unwrap_or_else(|| builder.value_gen.next()); 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)); } if let Err(e) = builder.emit_unified_call(
Some(actual_dst),
crate::mir::builder::builder_calls::CallTarget::Global(fname.clone()),
call_args,
) { return Some(Err(e)); }
builder.annotate_call_result_from_func_name(actual_dst, &fname); builder.annotate_call_result_from_func_name(actual_dst, &fname);
return Some(Ok(actual_dst)); return Some(Ok(actual_dst));
} }

View File

@ -1,6 +1,5 @@
use crate::mir::builder::MirBuilder; use crate::mir::builder::MirBuilder;
use crate::mir::{ValueId, Callee}; use crate::mir::{ValueId, Callee};
use std::collections::HashMap;
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum LocalKind { pub enum LocalKind {

View File

@ -1,4 +1,4 @@
use super::{ConstValue, Effect, EffectMask, MirInstruction, ValueId}; use super::{Effect, EffectMask, MirInstruction, ValueId};
use crate::ast::{ASTNode, CallExpr}; use crate::ast::{ASTNode, CallExpr};
use crate::mir::TypeOpKind; use crate::mir::TypeOpKind;
use crate::mir::utils::is_current_block_terminated; use crate::mir::utils::is_current_block_terminated;

View File

@ -5,7 +5,7 @@
*/ */
use super::{BasicBlock, BasicBlockId, EffectMask, MirInstruction, MirType, ValueId}; use super::{BasicBlock, BasicBlockId, EffectMask, MirInstruction, MirType, ValueId};
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::fmt; use std::fmt;
/// Function signature for MIR functions /// Function signature for MIR functions
@ -329,6 +329,10 @@ pub struct ModuleMetadata {
/// Optimization level used /// Optimization level used
pub optimization_level: u32, pub optimization_level: u32,
/// Dev idempotence markers for passes (optional; default empty)
/// Key format suggestion: "pass_name:function_name"
pub dev_processed_markers: HashSet<String>,
} }
impl MirModule { impl MirModule {

View File

@ -4,16 +4,13 @@
* SSA-form instructions with effect tracking for optimization * SSA-form instructions with effect tracking for optimization
*/ */
use super::{Effect, EffectMask, ValueId}; use super::{EffectMask, ValueId};
use crate::mir::definitions::Callee; // Import Callee from unified definitions use crate::mir::definitions::Callee; // Import Callee from unified definitions
use crate::mir::types::{ use crate::mir::types::{
BarrierOp, BinaryOp, CompareOp, ConstValue, MirType, TypeOpKind, UnaryOp, WeakRefOp, BarrierOp, BinaryOp, CompareOp, ConstValue, MirType, TypeOpKind, UnaryOp, WeakRefOp,
}; };
use std::fmt; // (unused imports removed)
// Kind-specific metadata (non-functional refactor scaffolding)
use crate::mir::instruction_kinds as inst_meta;
/// MIR instruction types - limited to 20 core instructions /// MIR instruction types - limited to 20 core instructions
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]

View File

@ -2,10 +2,33 @@ use crate::mir::optimizer::MirOptimizer;
use crate::mir::optimizer_stats::OptimizationStats; use crate::mir::optimizer_stats::OptimizationStats;
use crate::mir::{BarrierOp, MirModule, TypeOpKind, ValueId, WeakRefOp}; use crate::mir::{BarrierOp, MirModule, TypeOpKind, ValueId, WeakRefOp};
fn idemp_enabled() -> bool {
std::env::var("NYASH_MIR_DEV_IDEMP").ok().as_deref() == Some("1")
}
fn idemp_key(pass: &str, func_name: &str) -> String {
format!("{}:{}", pass, func_name)
}
fn idemp_already_done(module: &crate::mir::MirModule, key: &str) -> bool {
module.metadata.dev_processed_markers.contains(key)
}
fn idemp_mark(module: &mut crate::mir::MirModule, key: String) {
module.metadata.dev_processed_markers.insert(key);
}
pub fn force_plugin_invoke(_opt: &mut MirOptimizer, module: &mut MirModule) -> OptimizationStats { pub fn force_plugin_invoke(_opt: &mut MirOptimizer, module: &mut MirModule) -> OptimizationStats {
use crate::mir::MirInstruction as I; use crate::mir::MirInstruction as I;
let mut stats = OptimizationStats::new(); let mut stats = OptimizationStats::new();
for (_fname, function) in &mut module.functions { let pass_name = "normalize.force_plugin_invoke";
let func_names: Vec<String> = module.functions.keys().cloned().collect();
for fname in func_names {
if idemp_enabled() {
let key = idemp_key(pass_name, &fname);
if idemp_already_done(module, &key) { continue; }
}
let function = match module.functions.get_mut(&fname) { Some(f) => f, None => continue };
for (_bb, block) in &mut function.blocks { for (_bb, block) in &mut function.blocks {
for inst in &mut block.instructions { for inst in &mut block.instructions {
if let I::BoxCall { if let I::BoxCall {
@ -28,6 +51,7 @@ pub fn force_plugin_invoke(_opt: &mut MirOptimizer, module: &mut MirModule) -> O
} }
} }
} }
if idemp_enabled() { let key = idemp_key(pass_name, &fname); idemp_mark(module, key); }
} }
stats stats
} }
@ -38,7 +62,14 @@ pub fn normalize_python_helper_calls(
) -> OptimizationStats { ) -> OptimizationStats {
use crate::mir::MirInstruction as I; use crate::mir::MirInstruction as I;
let mut stats = OptimizationStats::new(); let mut stats = OptimizationStats::new();
for (_fname, function) in &mut module.functions { let pass_name = "normalize.python_helper_calls";
let func_names: Vec<String> = module.functions.keys().cloned().collect();
for fname in func_names {
if idemp_enabled() {
let key = idemp_key(pass_name, &fname);
if idemp_already_done(module, &key) { continue; }
}
let function = match module.functions.get_mut(&fname) { Some(f) => f, None => continue };
for (_bb, block) in &mut function.blocks { for (_bb, block) in &mut function.blocks {
for inst in &mut block.instructions { for inst in &mut block.instructions {
if let I::PluginInvoke { if let I::PluginInvoke {
@ -62,6 +93,7 @@ pub fn normalize_python_helper_calls(
} }
} }
} }
if idemp_enabled() { let key = idemp_key(pass_name, &fname); idemp_mark(module, key); }
} }
stats stats
} }
@ -80,7 +112,14 @@ pub fn normalize_legacy_instructions(
if core13 { if core13 {
array_to_boxcall = true; array_to_boxcall = true;
} }
for (_fname, function) in &mut module.functions { let pass_name = "normalize.legacy_instructions";
let func_names: Vec<String> = module.functions.keys().cloned().collect();
for fname in func_names {
if idemp_enabled() {
let key = idemp_key(pass_name, &fname);
if idemp_already_done(module, &key) { continue; }
}
let function = match module.functions.get_mut(&fname) { Some(f) => f, None => continue };
for (_bb, block) in &mut function.blocks { for (_bb, block) in &mut function.blocks {
for inst in &mut block.instructions { for inst in &mut block.instructions {
match inst { match inst {
@ -345,6 +384,7 @@ pub fn normalize_legacy_instructions(
} }
} }
} }
if idemp_enabled() { let key = idemp_key(pass_name, &fname); idemp_mark(module, key); }
} }
stats stats
} }
@ -355,7 +395,14 @@ pub fn normalize_ref_field_access(
) -> OptimizationStats { ) -> OptimizationStats {
use crate::mir::MirInstruction as I; use crate::mir::MirInstruction as I;
let mut stats = OptimizationStats::new(); let mut stats = OptimizationStats::new();
for (_fname, function) in &mut module.functions { let pass_name = "normalize.ref_field_access";
let func_names: Vec<String> = module.functions.keys().cloned().collect();
for fname in func_names {
if idemp_enabled() {
let key = idemp_key(pass_name, &fname);
if idemp_already_done(module, &key) { continue; }
}
let function = match module.functions.get_mut(&fname) { Some(f) => f, None => continue };
for (_bb, block) in &mut function.blocks { for (_bb, block) in &mut function.blocks {
let mut out: Vec<I> = Vec::with_capacity(block.instructions.len() + 2); let mut out: Vec<I> = Vec::with_capacity(block.instructions.len() + 2);
let old = std::mem::take(&mut block.instructions); let old = std::mem::take(&mut block.instructions);
@ -462,6 +509,7 @@ pub fn normalize_ref_field_access(
}); });
} }
} }
if idemp_enabled() { let key = idemp_key(pass_name, &fname); idemp_mark(module, key); }
} }
stats stats
} }

View File

@ -4,7 +4,7 @@
//! counts eliminations without rewriting uses (SSA update is TODO). This keeps //! counts eliminations without rewriting uses (SSA update is TODO). This keeps
//! behavior identical while modularizing the pass for future enhancement. //! behavior identical while modularizing the pass for future enhancement.
use crate::mir::{MirFunction, MirInstruction, MirModule, ValueId}; use crate::mir::{MirFunction, MirInstruction, MirModule, MirType, ValueId};
use std::collections::HashMap; use std::collections::HashMap;
/// Run CSE across the module. Returns the number of eliminated expressions. /// Run CSE across the module. Returns the number of eliminated expressions.
@ -19,6 +19,15 @@ pub fn eliminate_common_subexpressions(module: &mut MirModule) -> usize {
fn cse_in_function(function: &mut MirFunction) -> usize { fn cse_in_function(function: &mut MirFunction) -> usize {
let mut expression_map: HashMap<String, ValueId> = HashMap::new(); let mut expression_map: HashMap<String, ValueId> = HashMap::new();
let mut eliminated = 0usize; let mut eliminated = 0usize;
let fast_int = std::env::var("NYASH_LLVM_FAST_INT").ok().as_deref() == Some("1");
// Helper: check if both operands are numeric (Integer/Float) via value type hints
let is_numeric = |vid: ValueId| -> bool {
match function.metadata.value_types.get(&vid) {
Some(MirType::Integer) | Some(MirType::Float) => true,
_ => false,
}
};
for (_bid, block) in &mut function.blocks { for (_bid, block) in &mut function.blocks {
for inst in &mut block.instructions { for inst in &mut block.instructions {
@ -26,8 +35,25 @@ fn cse_in_function(function: &mut MirFunction) -> usize {
let key = instruction_key(inst); let key = instruction_key(inst);
if let Some(&existing) = expression_map.get(&key) { if let Some(&existing) = expression_map.get(&key) {
if let Some(dst) = inst.dst_value() { if let Some(dst) = inst.dst_value() {
// Count as eliminated; rewriting uses is a future improvement. // Prefer existing SSA value in the same block when FAST_INT is enabled.
let _ = (existing, dst); // keep variables referenced if fast_int {
match inst {
MirInstruction::BinOp { op, lhs, rhs, .. } => {
// Only rewrite Add when both operands are numeric (avoid String + String)
let allow = match op {
crate::mir::BinaryOp::Add => is_numeric(*lhs) && is_numeric(*rhs),
_ => true,
};
if allow { *inst = MirInstruction::Copy { dst, src: existing }; }
}
MirInstruction::Compare { .. }
| MirInstruction::UnaryOp { .. }
| MirInstruction::Cast { .. } => {
*inst = MirInstruction::Copy { dst, src: existing };
}
_ => {}
}
}
eliminated += 1; eliminated += 1;
} }
} else if let Some(dst) = inst.dst_value() { } else if let Some(dst) = inst.dst_value() {

View File

@ -63,36 +63,10 @@ pub fn inject_method_ids(module: &mut MirModule) -> usize {
} }
} }
} }
I::PluginInvoke { I::PluginInvoke { .. } => {
dst, // Keep PluginInvoke as-is and let the interpreter handle it via plugin host.
box_val, // This avoids premature lowering that can mask plugin-specific calling
method, // conventions. Method ID resolution for plugins is handled at runtime.
args,
effects,
} => {
if let Some(bt) = origin.get(box_val).cloned() {
// Resolve id as above
let mid_u16 = if let Some(h) = host_guard.as_ref() {
match h.resolve_method(&bt, method) {
Ok(mh) => Some(mh.method_id as u16),
Err(_) => resolve_slot_by_type_name(&bt, method),
}
} else {
resolve_slot_by_type_name(&bt, method)
};
if let Some(mid) = mid_u16 {
// Rewrite to BoxCall with method_id
*inst = I::BoxCall {
dst: dst.take(),
box_val: *box_val,
method: method.clone(),
method_id: Some(mid),
args: args.clone(),
effects: *effects,
};
injected += 1;
}
}
} }
_ => {} _ => {}
} }

View File

@ -21,4 +21,4 @@ pub use control_flow::{
// PHI挿入ヘルパーMirBuilderのextension methodsとして実装 // PHI挿入ヘルパーMirBuilderのextension methodsとして実装
// 使用例: self.insert_phi(vec![(block1, val1), (block2, val2)])? // 使用例: self.insert_phi(vec![(block1, val1), (block2, val2)])?
pub use phi_helpers::*; // Re-exports removed to reduce unused-import warnings. Use builder methods directly.

View File

@ -98,6 +98,13 @@ impl MirVerifier {
} }
} }
// 10. Ret-block purity (optional, dev-only)
if crate::config::env::verify_ret_purity() {
if let Err(mut rpe) = self.verify_ret_block_purity(function) {
local_errors.append(&mut rpe);
}
}
if local_errors.is_empty() { if local_errors.is_empty() {
Ok(()) Ok(())
} else { } else {
@ -255,6 +262,32 @@ impl MirVerifier {
if errors.is_empty() { Ok(()) } else { Err(errors) } if errors.is_empty() { Ok(()) } else { Err(errors) }
} }
/// Verify that any block ending with Return contains no side-effecting instructions before it.
/// Allowed before Return: Const, Copy, Phi, Nop only. Others are considered side-effecting for this policy.
fn verify_ret_block_purity(&self, function: &MirFunction) -> Result<(), Vec<VerificationError>> {
use super::MirInstruction as I;
let mut errors = Vec::new();
for (bid, bb) in &function.blocks {
if let Some(I::Return { .. }) = bb.terminator {
for (idx, inst) in bb.instructions.iter().enumerate() {
let allowed = matches!(
inst,
I::Const { .. } | I::Copy { .. } | I::Phi { .. } | I::Nop
);
if !allowed {
let name = format!("{:?}", inst);
errors.push(VerificationError::RetBlockSideEffect {
block: *bid,
instruction_index: idx,
name,
});
}
}
}
}
if errors.is_empty() { Ok(()) } else { Err(errors) }
}
/// Reject legacy instructions that should be rewritten to Core-15 equivalents /// Reject legacy instructions that should be rewritten to Core-15 equivalents
/// Skips check when NYASH_VERIFY_ALLOW_LEGACY=1 /// Skips check when NYASH_VERIFY_ALLOW_LEGACY=1
fn verify_no_legacy_ops(&self, function: &MirFunction) -> Result<(), Vec<VerificationError>> { fn verify_no_legacy_ops(&self, function: &MirFunction) -> Result<(), Vec<VerificationError>> {

Some files were not shown because too many files have changed in this diff Show More