chore: Phase 25.1 完了 - LoopForm v2/Stage1 CLI/環境変数削減 + Phase 26-D からの変更

Phase 25.1 完了成果:
-  LoopForm v2 テスト・ドキュメント・コメント完備
  - 4ケース(A/B/C/D)完全テストカバレッジ
  - 最小再現ケース作成(SSAバグ調査用)
  - SSOT文書作成(loopform_ssot.md)
  - 全ソースに [LoopForm] コメントタグ追加

-  Stage-1 CLI デバッグ環境構築
  - stage1_cli.hako 実装
  - stage1_bridge.rs ブリッジ実装
  - デバッグツール作成(stage1_debug.sh/stage1_minimal.sh)
  - アーキテクチャ改善提案文書

-  環境変数削減計画策定
  - 25変数の完全調査・分類
  - 6段階削減ロードマップ(25→5、80%削減)
  - 即時削除可能変数特定(NYASH_CONFIG/NYASH_DEBUG)

Phase 26-D からの累積変更:
- PHI実装改善(ExitPhiBuilder/HeaderPhiBuilder等)
- MIRビルダーリファクタリング
- 型伝播・最適化パス改善
- その他約300ファイルの累積変更

🎯 技術的成果:
- SSAバグ根本原因特定(条件分岐内loop変数変更)
- Region+next_iパターン適用完了(UsingCollectorBox等)
- LoopFormパターン文書化・テスト化完了
- セルフホスティング基盤強化

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: ChatGPT <noreply@openai.com>
Co-Authored-By: Task Assistant <task@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-21 06:25:17 +09:00
parent baf028a94f
commit f9d100ce01
366 changed files with 14322 additions and 5236 deletions

View File

@ -146,26 +146,26 @@ PR テンプレ(追加項目)
- [ ] 「スモークを通すためだけのハードコード」を入れていません(根治で解決) - [ ] 「スモークを通すためだけのハードコード」を入れていません(根治で解決)
- [ ] 受け入れ基準に記載の検証を実施しましたdev OFF/ログ確認) - [ ] 受け入れ基準に記載の検証を実施しましたdev OFF/ログ確認)
### 5.2 Rust Freeze PolicySelfHost First ### 5.2 Rust Minimal PolicySelfHost First, but not Frozen
目的: 脱Rustで開発効率を最大化する。Rust層は“最小シームバグ修正”のみに留め、分析/ルール/可視化は .hako 側で実装する 目的: 脱Rustを志向しつつも、Stage1 / SelfHost ラインの整備やツールの使いやすさ向上のために、**Rust層で必要な構造的変更やブリッジ強化は積極的に許可する**。分析/ルール/可視化は引き続き .hako 側が主戦場
- 原則 - 原則
- Rustは「SSOTランナー導線resolve→parse→merge」と「VM/Interpreterの安定化(バグ修正、挙動不変)」のみ - Rustは「SSOTランナー導線resolve→parse→merge」と「VM/Interpreterの安定化」を軸にしつつ、Stage1 CLI / selfhost ブリッジ / エラーメッセージ改善など**開発導線の改善**も扱ってよい
- 新規機能・ルール・可視化・チェックは .hako で実装(自己ホスト)。 - 新規の言語ルール・静的解析ロジック・ビジネスロジックは、基本的に .hako で実装(自己ホスト)。
- 変更は既定OFFのトグルで可逆・小差分・戻し手順ありAGENTS.md 5.1に準拠)。 - 変更はできるだけ小さく・可逆に保ちつつ、必要に応じてトグルや dev 用フラグでガードAGENTS.md 5.1 に準拠)。
- 許可(最小のRust変更 - 許可Rustでやってよいことの例
- ランナー導線の保守using textmerge for .hako の維持)。 - ランナー導線の保守・改善StageB/Stage1/Stage0 の配線、using textmerge for .hako の維持・整理)。
- バグ修正既定挙動不変、必要ならトグル既定OFF)。 - Stage1 / StageB / selfhost 向けのブリッジ強化(子プロセス起動、環境変数の集約、エラー表示の改善)。
- Analyzerシームの提供抽出のみ - バグ修正既定挙動を壊さない範囲。必要なら既定OFFトグル
- 例: `--hako-analyze <paths>` → AST/Analysis JSON を stdout/--out に出力 - Analyzerシームの提供AST/Analysis JSON の抽出専用。判定ロジックは持たない)
- 判定ロジックは持たない(抽出専用) - 薄いCLI糖衣`--hako-check` など、.hako 側のアナライザを呼び出すための CLI エントリ
- 薄いCLI糖衣`--hako-check`(内部で `--hako-analyze`→.hakoアナライザをVM経由で実行
- 禁止/抑制 - 禁止/抑制(依然として .hako 側でやる領域)
- ルール実装Lint/命名/依存/特定Box名分岐をRustに持ち込むこと - Lint / 命名規則 / 依存関係チェック / 特定 Box 名への分岐など、**ルール実装の本体**
- 広域リファクタ・既定挙動変更(凍結)。 - LoopSSA / 数値カーネル / 高レベルの最適化ロジックを Rust に新規実装することSelfHost で十分に表現できる範囲)。
- 広域リファクタや既定挙動を大きく変える変更(必要なら Phase/Proposal に切り出してから)。
- .hako 側の責務SelfHost - .hako 側の責務SelfHost
- Lint / 解析 / 可視化 / 関係図DOTを .hako で実装tools/hako_check/*)。 - Lint / 解析 / 可視化 / 関係図DOTを .hako で実装tools/hako_check/*)。
@ -173,11 +173,11 @@ PR テンプレ(追加項目)
- 返り値は件数ベース0=OK、>0=警告/エラー件数)。 - 返り値は件数ベース0=OK、>0=警告/エラー件数)。
- 受け入れ基準Rust変更時 - 受け入れ基準Rust変更時
- quickスモーク/Adapter reps 緑維持(既定挙動不変)。 - quick スモーク / 代表テストが緑維持(既定挙動を意図せず変えていない)。
- 変更はトグルでガード既定OFF・小差分・戻し手順付き - 変更は目的が明確で、小さく・可逆(トグルや限定スコープでガード)であること
- .hako からの利用例/READMEを更新 - .hako からの利用例 / README / proposal を更新し、将来の開発者が迷わないようにしておく
補足:自己ホスト達成済みのため、Rust層は“何かあった時のための最低限の手入れ”に限定。日常の機能拡張は .hako 側で行い、構造/テスト/ドキュメントを伴って進める。 補足:.hako 側の解析・Stage1 ラインが十分に動き始めたので、Rust層は「完全凍結」ではなく、**SelfHost を支えるための最小+必要な整備を行う層**という扱いに緩和する。日常の機能拡張や言語仕様変更はこれまで通り .hako 側で行い、Rust 変更は「導線の改善」「ブリッジ」「可観測性の向上」にフォーカスする。
### 6. Fail-Fast with Structure ### 6. Fail-Fast with Structure
@ -379,6 +379,7 @@ Notes
- Phase17LoopForm SelfHosting & Polish: `docs/development/roadmap/phases/phase-17-loopform-selfhost/` - Phase17LoopForm SelfHosting & Polish: `docs/development/roadmap/phases/phase-17-loopform-selfhost/`
- MacroBoxユーザー拡張: `docs/guides/macro-box.md` - MacroBoxユーザー拡張: `docs/guides/macro-box.md`
- MacroBox in Nyash設計草案: `docs/guides/macro-box-nyash.md` - MacroBox in Nyash設計草案: `docs/guides/macro-box-nyash.md`
- MIR デバッグ総覧dump/hints/__mir__: `docs/guides/testing-guide.md``MIR デバッグの入口まとめ` セクション)
# Repository Guidelines # Repository Guidelines

View File

@ -17,11 +17,37 @@
- .hako 側: - .hako 側:
- StageB コンパイラ本体 / LoopSSA v2 / BreakFinderBox / PhiInjectorBox はまだ部分実装。 - StageB コンパイラ本体 / LoopSSA v2 / BreakFinderBox / PhiInjectorBox はまだ部分実装。
- JSON v0 / selfhost ルートは Rust 側の LoopForm v2 規約に追いつかせる必要がある。 - JSON v0 / selfhost ルートは Rust 側の LoopForm v2 規約に追いつかせる必要がある。
- StageB / FuncScanner ライン:
- Phase 25.3 をクローズし、`stageb_fib_program_defs_canary_vm.sh` が緑(`defs``TestBox.fib/Main.main`、fib.body.body[*] に `Loop`)。
- StageB は block パーサ優先 + defs を Block 包みで構造化。次手: Stage1 UsingResolver ループの Region+next_i 揃え / Stage1 CLI program-json selfhost 準備。
--- ---
## 1. 最近完了した重要タスク ## 1. 最近完了した重要タスク
### 1-0. Phase 25.3 — FuncScanner / StageB defs 安定化(完了)
**目的**
- StageB / FuncScanner ラインで defs が欠落したり Loop が脱落する問題を塞ぎ、selfhost 側の canary を緑に戻す。
**やったこと**
- StageBDriverBox.main:
- main 本文を `{…}` で包んだ block パーサ優先で Program(JSON) に組み立て、defs には `{"type":"Block","body":[…]}` 形式で埋め込むよう整理。
- Program パーサ fallback は `HAKO_STAGEB_PROGRAM_PARSE_FALLBACK=1` の opt-in に封じ込めskip_ws 崩れを回避)。
- StageBFuncScannerBox._scan_methods:
- block パーサ優先に統一し、Program パーサは `HAKO_STAGEB_FUNC_SCAN_PROG_FALLBACK=1` でのみ有効化。
- defs パラメータに必ず `me` を足す従来挙動は維持TestBox/Main いずれも同型で出力)。
- Rust 層の追加変更なしLoopForm v2 / LoopSnapshotMergeBox をそのまま利用)。
**結果**
- `tools/smokes/v2/profiles/quick/core/phase251/stageb_fib_program_defs_canary_vm.sh` が安定して PASS。
- `Program.kind == "Program"`
- `defs``TestBox.fib` / `Main.main` を保持していること。
- `TestBox.fib.body.body[*]``Loop` ノードが含まれること。
を満たした状態で `rc=0` になることを確認。
- FuncScanner / StageB 経由の fib defs ラインは LoopForm v2 + LoopSnapshotMergeBox 上で構造的に安定したとみなし、Phase 25.3 はクローズ。
- 次フェーズの入口が整理できたので、Stage1 UsingResolver ループRegion+next_i 形)と Stage1 CLI program-json/selfhost 導線に着手可能。
### 1-1. Phase 25.1m — Static Method / LoopForm v2 continue + PHI Fix完了 ### 1-1. Phase 25.1m — Static Method / LoopForm v2 continue + PHI Fix完了
**目的** **目的**
@ -138,6 +164,26 @@
--- ---
### 1-4. Phase 25.1A3 — Stage1 CLI bridgestub 実装)
- Rust 側: `src/runner/stage1_bridge.rs``run_refactored` 入口に組み込み、`NYASH_USE_STAGE1_CLI=1` かつ再入ガードなしのときに `lang/src/runner/stage1_cli.hako` を子プロセスで起動する。`STAGE1_EMIT_PROGRAM_JSON` / `STAGE1_EMIT_MIR_JSON` / `STAGE1_BACKEND` / `STAGE1_PROGRAM_JSON` で mode 選択。entry override は `STAGE1_CLI_ENTRY` / `HAKORUNE_STAGE1_ENTRY`
- .hako 側: `Stage1Cli` に最低限の本体を実装。
- `emit_program_json`: Stage1 UsingResolver で prefix を結合し、BuildBox.emit_program_json_v0 で Program(JSON v0) を返す。
- `emit_mir_json`: `MirBuilderBox.emit_from_program_json_v0` をそのまま呼ぶdelegate 未設定なら null
- `run_program_json`: backend==llvm の場合は `env.codegen.emit_object` まで通す。vm/pyvm は当面 MIR(JSON) を stdout に出すのみ(実行は Stage0 橋渡し未配線)。
- CLI: `emit program-json|mir-json` / `run --backend ... <src>` を受理。`NYASH_SCRIPT_ARGS_JSON` を JSON で best-effort 伝播。
- Docs: `docs/development/roadmap/phases/phase-25.1/stage1-usingresolver-loopform.md` に stub 状態を追記run は暫定挙動)。
- Known gaps: vm/pyvm 実行はまだ Stage0 への橋渡し未着手。llvm も emit object 止まりlink/exec は後続)。
### 1-5. Phase 25.1A4 — Using SSOT 薄設計BuildBox include 除去
- SSOT: `lang/src/using/resolve_ssot_box.hako` に README 相当のコメントと I/Fresolve_modules/resolve_prefixを追加。現状は no-op だが「using をここで扱う」窓口を固定。
- Stage1UsingResolver: `resolve_for_program_json` を追加し、SSOT を呼ぶ導線だけ確保(現状は透過返し)。
- BuildBox: 先頭の `include` を削除し、`using lang.compiler.entry.bundle_resolver as BundleResolver` に置換StageB パーサが include を解せない問題の足場づくり)。
- 実行確認: `NYASH_ALLOW_NYASH=1 HAKO_ALLOW_NYASH=1 NYASH_USE_STAGE1_CLI=1 STAGE1_EMIT_PROGRAM_JSON=1 ... cargo run --bin hakorune -- basic_test.hako` が RC=0 で完走(ただし stdout は `RC: 0` のみ、program-json 出力は未配線)。`cargo run ... emit program-json` は Rust CLI 側の表面が未対応のためエラー(想定挙動)。
---
## 2. まだ残っている問題・課題2025-11-18 時点) ## 2. まだ残っている問題・課題2025-11-18 時点)
### 2-1. StageB 本体の型エラー(`String > Integer(13)` ### 2-1. StageB 本体の型エラー(`String > Integer(13)`
@ -231,26 +277,50 @@
## 3. 次にやること(候補タスク) ## 3. 次にやること(候補タスク)
ここから先は「どのフェーズを進めるか」をそのときの優先度で選ぶ感じだよ。 ここから先は「どのフェーズを進めるか」をそのときの優先度で選ぶ感じだよ。
Rust 側は LoopForm v2 / StageB fib / Stage1 UsingResolver 構造テストまで一通り整ったので、当面は Stage1 CLI / selfhost ラインの小さな箱から進める。
1. **StageB / BreakFinder / FuncScanner ラインの SSA 根治Phase 25.1m 続き)** ### A. Stage1 CLI / Stage0 ブリッジPhase 25.1 — いまここ)
- JSON v0 bridge の Loop lowering で、`backedge_to_cond``Jump` だけでなく `Branch(cond→header/exit)` も認識するように修正(`loop_.rs`)。
→ BreakFinderBox._find_loops/2 のような break を含むループでも header PHI が正しく張られるようにした。
- BreakFinderBox は解析用の static box として扱い、`me._find_loops` / `me._jumps_to``BreakFinderBox._find_loops` / `_jumps_to` に正規化。
`me` 未定義による Undefined ValueId を避ける。
- StageB → Stage1 パーサ経路は、`ParserBox.parse_program2` のトップレベルループではなく、`parse_block2``Main.main` の本文をブロックとしてパースし、
Program(JSON v0) を自前で組み立てる構造に変更StageB 固まり問題を回避)。
2. **StageB 再入ガード `env.set/2` の整理** - A1: Stage1 CLI stub を Stage0 Rust ランナーから呼び出すブリッジDONE
- 再入ガードを Box 内 state で持つ方向か、Rust 側に dev 専用 extern を追加するかを決める。 - 実装: `src/runner/stage1_bridge.rs` `src/runner/mod.rs``maybe_run_stage1_cli_stub` を追加。
- どちらにしても「本番経路prod ランナー)には影響しない」ようフラグでガードする。 - `NYASH_USE_STAGE1_CLI=1`(既定 OFFかつ `NYASH_STAGE1_CLI_CHILD!=1` のときにだけ有効。
- entry `.hako``STAGE1_CLI_ENTRY` / `HAKORUNE_STAGE1_ENTRY` で上書き可能(既定: `lang/src/runner/stage1_cli.hako`)。
- 入力ファイル / モード:
- `STAGE1_EMIT_PROGRAM_JSON=1`: `hakorune emit program-json <source.hako>` を子プロセスで実行。
- `STAGE1_EMIT_MIR_JSON=1`: `STAGE1_PROGRAM_JSON` があれば `--from-program-json`、なければ `<source.hako>` から `emit mir-json`
- 上記どちらも無い場合: `hakorune run --backend <backend> <source.hako>`backend は `STAGE1_BACKEND` か CLI の backend 設定)。
- `NYASH_SCRIPT_ARGS_JSON` を拾って `--` 以降の script 引数も Stage1 側に転送。
- 再入防止として子プロセスには `NYASH_STAGE1_CLI_CHILD=1` を付与(子側からは Rust ブリッジを素通り)。
- A2: Stage1 CLI skeleton の責務を doc に固定DONE
- `lang/src/runner/stage1_cli.hako``Stage1Cli.emit_program_json/emit_mir_json/run_program_json/stage1_main` のシグネチャとトグルトポロジーを固定。
- `docs/development/roadmap/phases/phase-25.1/stage1-usingresolver-loopform.md` に Rust 側ブリッジの振る舞いとトグル名(`NYASH_USE_STAGE1_CLI` / `STAGE1_EMIT_*` / `STAGE1_BACKEND` / `NYASH_STAGE1_CLI_CHILD`)を追記。
- A3: 次ステップ(未着手)
- Stage1 CLI skeleton に StageB/BuildBox/MirBuilder 呼び出しを順に実装し、「Program(JSON)/MIR(JSON) を selfhost 経由で emit できる」状態まで持っていく。
- `tools/selfhost/run_stage1_cli.sh` から呼び出す selfhost パスと、Rust CLl からのブリッジパスの両方で JSON I/O 契約が同じになるように揃える。
3. **.hako LoopSSA v2 実装LoopForm v2 への追従)** ### B. Stage1 UsingResolver / LoopForm v2 ラインPhase 25.1e 系フォロー)
- Rust LoopForm v2 の規約Carrier/Pinned / preheader/header/latch/exit`.hako` の LoopSSA に輸入。
- StageB Test2 / selfhost CLI の JSON→MIR 経路で MirVerifier 緑を目標にする。
4. **Selfhost / CLI 周辺のテスト整理** - B1: entry UsingResolver の Region+next_i 化とコメント整備(おおむね DONE
- 代表的な selfhost / StageB / Stage1 CLI ケースに対して、 - `lang/src/compiler/entry/using_resolver_box.hako` の 3 ループentries / JSON スキャン / modules_listは Region+next_i 形に揃え済み。
「Phase 25.1m でどこまで緑になったか」「どこから先が 25.1k 以降の仕事か」を tests / tools 側で見える化する。 - 役割コメント: `resolve_for_source`StageB body_src を受けて prefix を返す)、`_collect_using_entries`JSON スキャンして entries を集める)、`_build_module_map`modules_list を map 化)を明記済み。
- `HAKO_STAGEB_APPLY_USINGS=0` の時は prefix を空 string にしつつ、depth ガードだけは走らせる仕様もコメントで固定。
- B2: UsingResolver 構造テスト(ループ形/PHIの拡充DONE
- `src/tests/mir_stage1_using_resolver_verify.rs` に Region+next_i ループと early-exit JSON スキャンパターンの軽量テストを追加。
- これらは cargo test 経路で常時緑を維持し、v2 quick スモークへの昇格は「実行時間とイズを見ながら後続フェーズで検討」という扱いにするdocs にメモ済み)。
- B3: pipeline_v2 UsingResolver との責務境界DONE
- `lang/src/compiler/pipeline_v2/using_resolver_box.hako` 冒頭に、「entry 側=ファイル I/O + using 収集」「pipeline_v2 側=modules_json 上の alias/path 解決」と役割メモを追加。
- RegexFlow ベースの単一路ループは Region 化不要stateful helperとし、LoopForm v2 の観点からも「観測対象外」とする。
### C. StageB / LoopSSA / Selfhost まわり(中期タスク)
- C1: StageB 再入ガード `env.set/2` の整理(据え置き)
- dev 専用 extern を Rust 側に追加するか、StageB 箱側で state に寄せるかを決める必要があるが、現在はループ/PHI ラインを優先し保留。
- このタスクに着手するときは「prod/CI 経路から完全に切り離した dev ガード」として設計する。
- C2: .hako LoopSSA v2 実装Rust LoopForm v2 への追従)
- Rust LoopForm v2 の Carrier/Pinned/BodyLocalInOut モデルを `.hako` の LoopSSA に輸入し、StageB Test2 / selfhost CLI でも MirVerifier 緑を目指す中〜長期タスク。
- 具体的な対象: `lang/src/compiler/builder/ssa/loopssa.hako` と StageB/BreakFinder 周辺の region 化済みループ。
- C3: Selfhost / CLI 周辺のテスト整理
- 代表的な selfhost / StageB / Stage1 CLI ケースを tests/tools 側でタグ付けquick/integration/selfhost、Phase 25.1 の「どこまでが Rust 側」「どこからが Stage1 側」を見える化する。
--- ---
@ -268,44 +338,51 @@
次にどの箱から攻めるか決めたら、ここに箇条書きで足していこうね。 次にどの箱から攻めるか決めたら、ここに箇条書きで足していこうね。
## 2-? StageB FuncScanner / LoopSnapshotMergeBox / MIR ロガーin progress ## 3. これからやるタスクのラフ一覧25.1 / Stage1 系
- StageBFuncScannerBox を `compiler_stageb.hako` 内に追加し、StageBDriverBox.main からは FuncScannerBox ではなくこちらを呼ぶ構成にしている ここから先は「まず設計と箱分割を書いてから実装」という方針で進めるタスク群だよ
- VM 側の Undefined valueBreakFinderBox._find_loops/2は LoopForm v2 / continue + PHI 修正で解消済み。
- FuncScannerBox.scan_all_boxes/1 については、Phase 25.2 で `LoopSnapshotMergeBox` を導入し、continue/break/exit スナップショットのマージを一元化したことで
13 個の continue を含む複雑ループでも Undefined Value が出ないところまで到達しており、
さらに `lang/src/compiler/tests/funcscanner_fib_min.hako` を使った最小ハーネスでも `NYASH_VM_VERIFY_MIR=1` で Undefined Value が再現しないところまで確認済み。
- LoopSnapshotMergeBox 実装: `src/mir/phi_core/loop_snapshot_merge.rs`
- `merge_continue_for_header` / `merge_exit` / `optimize_same_value` / `sanitize_inputs` など、ヘッダ/exit PHI 用の入力統合ロジックを提供。
- LoopBuilder / LoopFormBuilder は、この箱を経由して continue_merge / exit PHI を構成するようにリファクタ済み。
- 代表テスト:
- ループ系: `mir_stageb_loop_break_continue::*` / `mir_loopform_exit_phi::*` / `mir_stageb_like_args_length::*` はすべて PASS。
- 手書きループ:
- 基本ループ: sum=100+1+2+3+4
- break/continue を含むループ: sum=19, i=7 ✅
- body-local 変数を含むループ: result=6, i=3 ✅exit PHI で body-local を正しく統合)
- StageBFuncScannerBox.scan_all_boxes(src) は依然として fib サンプルに対して defs=[]Program(JSON) に `TestBox.fib` が載っていない)という問題が残っているため、
次の担当者は StageBFuncScannerBox.scan_all_boxes/1 と FuncScannerBox.scan_all_boxes/1 の MIR を比較しつつ、
StageB body 抽出 → StageBFuncScannerBox.scan_all_boxes → Program(JSON v0).defs 注入の導線自体を見直してほしいLoopForm/PHI/スナップショット側は LoopSnapshotMergeBox で安定化済み)。
- Phase 25.3 では、FuncScanner / StageB ラインの観測専用として `.hako` から直接 MIR ログを埋め込める `__mir__` 構文を導入した。
- 構文: `__mir__.log("label", v1, v2, ...)` / `__mir__.mark("label")`
- lowering: `MirInstruction::DebugLog { message: "label", values: [v1, v2, ...] }` に変換されるLoopForm/PHI には影響しない)。
- 実行: `NYASH_MIR_DEBUG_LOG=1` のときだけ `[MIR-LOG] label: %id=value ...` を VM 側で出力し、それ以外は no-op。
- 実装ポイント: `src/mir/builder/calls/build.rs``build_method_call_impl``__mir__` receiver を検出し、`try_build_mir_debug_call``DebugLog` 命令に directly lowering している。
- これにより、FuncScannerBox.scan_all_boxes や StageBFuncScannerBox.scan_all_boxes のループ頭や `box` 検出位置にラベルを打ち、
VM 実行ログと合わせて「どの経路で未初期化の値が残っているか」を .hako 側から追いやすくなった。
- 併せて `MirBuilder::handle_me_method_call``MeCallPolicyBox` で箱化し、me-call のフォールバックを静的メソッド降下に統一した。
- `Box.method/Arity` lowered 関数が存在する場合は従来どおり `me` を先頭引数にした Global call。
- 存在しない場合は `handle_static_method_call(cls, method, arguments)` に委譲し、static helperFuncScannerBox._parse_params / _strip_comments など)呼び出しとして扱う。
- これにより static box 文脈で「実体のない me を receiver にした Method call」が生成される経路が閉じられ、FuncScannerBox._scan_methods/4 系の UndefinedValue は Rust 側で構造的に防止されている。
### A. Rust 層解析LoopForm v2 / JSON v0 / Stage1 観測)
## 3. Static Box フィールド仕様ドキュメント(完了) - A-1: LoopForm v2 / LoopSnapshotMerge の入口確認
- `src/mir/loop_builder.rs` / `src/mir/phi_core/loopform_builder.rs` / `src/mir/phi_core/loop_snapshot_merge.rs` を「Stage1 から見た導線」として読み直し、どのレイヤで Carrier / Pinned / BodyLocalInOut が決まるかを short メモ化する。
- A-2: JSON v0 → MIR ブリッジの導線整理
- `src/runner/json_v0_bridge/lowering/`(特に `loop_.rs`を通して、Program(JSON v0).body / defs.body(Block) が LoopForm v2 までどう運ばれるかを図として docs に落とす。
- A-3: Stage1 UsingResolver の MIR 観測
- 既存テスト `src/tests/mir_stage1_using_resolver_verify.rs` の MIR dump を元に、「どのループが Region+next_i 化候補か」「既に問題なく LoopForm に乗っている場所はどこか」を箇条書きで整理する。
- docs/reference/language/LANGUAGE_REFERENCE_2025.md: 3.4 Static Boxパターンに「static box 内のフィールドはすべて static フィールドとして扱われる」旨を明記した。 ### B. Stage1 UsingResolver 箱化・ループ整理(.hako 側)
- 言語仕様としては、static box の中では `PI: FloatBox` のような宣言で十分であり、追加の `static` キーワードは不要であることをドキュメント上で固定。
- 次タスク候補Claude Code 向け): static box / box のフィールド挙動に関するサンプルとテストVM/JSON v0 両方)の整理、および static box 上の `me` セマンティクス25.1p DebugLog フェーズと連動)の仕様化。
- B-1: Stage1UsingResolverBox の責務分割設計
- `lang/src/compiler/entry/using_resolver_box.hako` を、collect_entries / modules_map / file_read などの小さいロジック箱に概念的に分割し、「どの箱が何を責務とするか」を docs に書く(実際のファイル分割は後段)。
- B-2: すべてのループを Region+next_i 形に揃える計画
- entry の UsingResolver 内に残っている「pos++/continue 多発型」ループを洗い出し、Region+next_i 形にどう書き換えるかを phase-25.1 docs に追記する。
- 目的: LoopForm v2 / PHI から見たときに「1 region / 1 backedge / 明確な次位置決定」という形に統一する。
- 状況メモ: entry 側 3 ループentries/JSON scan/modules_listは Region+next_i 化済み。残りなし。
- B-3: pipeline_v2 UsingResolver との役割分担
- `lang/src/compiler/pipeline_v2/using_resolver_box.hako` と entry/Stage1UsingResolverBox のどちらが「テキスト / JSON / modules_map」のどこまでを担当するかを整理し、責務境界をドキュメントに固定する。
- 状況メモ: pipeline_v2 側は modules_json の alias 解決のみRegexFlow.find_from の単一路ループ。Region 化不要として据え置き、境界だけ明記。
- B-4: 構造テストの追加計画
- Region+next_i パターンの軽量 SSA テスト(すでに 1 本追加済み)を基準に、もう 1〜2 本、UsingResolver ソースに近いパターンJSON スキャンearly-exitを追加する方針を決める。
### C. Stage1 CLI / program-json / mir-json SelfHost 準備
- C-1: Stage1 CLI インターフェースの設計メモ
- `.hako → Program(JSON)` / `Program(JSON) → MIR(JSON)` / `MIR(JSON) → 実行` を Stage1 側からどう呼び出すか(関数名・引数レベル)を docs に先に書く。
- C-2: StageB → Stage1 データフロー図
- `compiler_stageb.hako` → Program(JSON v0) → Stage1 UsingResolver → MirBuilder までのパイプラインを Phase25.1 ドキュメントに 1 枚の図としてまとめる。
- C-3: Rust CLI 側ブリッジの最小ガード案
- Stage0/Rust CLI は「Program(JSON/MIR(JSON) を受け取り VM/LLVM に流すだけ」に縮退させる方針と、既定 OFF トグルselfhost 入口)をどう切るかを設計メモとして追加。
### D. 将来フェーズ向けメモvariable_map 決定化ライン)
- D-1: MirBuilder::variable_map / BoxCompilationContext::variable_map BTreeMap 化案
- どの構造を HashMap→BTreeMap 化するか、どのテスト(`mir_funcscanner_skip_ws_vm_debug_flaky` など)で決定性を確認するかを Phase25.x の設計メモとして書いておく。
- D-2: dev 専用 / flaky テストの扱い方針
- どのテストが dev ignore手動実行用で、いつ/何を満たしたら常時有効に戻すかを、このファイルと関連 README に明確に残す。
このセクションは「すぐ実装する TODO ではなく、25.1〜25.x ラインで踏むべき設計タスク一覧」として使う予定だよ。
(静的 me-call 修正の参考メモはここに残しつつ、別セクションは整理済み)
## 3. Phase 25.1q — LoopForm Front UnificationDONE / follow-up 別タスクへ) ## 3. Phase 25.1q — LoopForm Front UnificationDONE / follow-up 別タスクへ)

View File

@ -43,9 +43,9 @@ Status: design+partial implementationStage1 ビルド導線の初期版まで
- Rust VM/LLVM のコアMIR インタプリタ/コード生成)の提供。 - Rust VM/LLVM のコアMIR インタプリタ/コード生成)の提供。
- Stage1 で AOT されたコア関数(後述)を呼び出すランチャ。 - Stage1 で AOT されたコア関数(後述)を呼び出すランチャ。
**禁止/抑制:** **禁止/抑制(緩和版):**
- パーサ高レイヤStageBMirBuilderAotPrepnumeric core のロジックを Rust 側に新規追加しない。 - パーサ高レイヤStageBMirBuilderAotPrepnumeric core の**意味論そのもの**を Rust 側に新規実装しないSelfHost の責務)
- 新しい Box 実装やランタイム機能を Rust に持ち込まないPhase 25 Rust Freeze を継続)。 - 新しい Box 実装や高レベルランタイム機能を Rust に持ち込むのは原則避ける(ただし Stage1 ブリッジやエラーログ改善など、導線・可観測性に必要な最小限の変更は許可)。
### Stage1 — Hakorune Selfhost Binary ### Stage1 — Hakorune Selfhost Binary
@ -157,6 +157,11 @@ Status: design+partial implementationStage1 ビルド導線の初期版まで
- [ ] Stage0→Stage1→Stage1' のビルドシーケンスを文章で定義(どの組み合わせで自己一致チェックを行うか)。 - [ ] Stage0→Stage1→Stage1' のビルドシーケンスを文章で定義(どの組み合わせで自己一致チェックを行うか)。
- [ ] 「普段使うのは Stage1」「問題発生時に Stage0 から再生成」という運用パターンを docs に記載。 - [ ] 「普段使うのは Stage1」「問題発生時に Stage0 から再生成」という運用パターンを docs に記載。
### E. Stage1 UsingResolver / LoopForm v2 対応(設計)
- [x] 設計ドラフトを追加(`stage1-usingresolver-loopform.md`。Region+next_i 形ループと Carrier/Pinned 対応の指針を明文化。
- [x] Rust 側の観測結果を反映し、具体的なリライト手順とテスト項目を更新するJSON→LoopForm v2 導線StageB→Stage1 データフローのテキスト図を追記)。
## 実装チェックリスト25.1 実行順案) ## 実装チェックリスト25.1 実行順案)
### 1. バイナリ命名と役割の明確化 ### 1. バイナリ命名と役割の明確化

View File

@ -112,6 +112,7 @@ source -> | Stage-B (block) | --> | Stage-1 UsingRes | --> | MirBuilder (.ha
### ループ洗い出しメモentry / pipeline_v2 ### ループ洗い出しメモentry / pipeline_v2
- entry UsingResolverlang/src/compiler/entry/using_resolver_box.hako - entry UsingResolverlang/src/compiler/entry/using_resolver_box.hako
- Region+next_i 化済み: entries イテレーション / JSON スキャン / modules_list 分割 - Region+next_i 化済み: entries イテレーション / JSON スキャン / modules_list 分割
- 追加テスト: modules_list 分割ループstart/next_startをそのまま MIR 化できることを `mir_stage1_using_resolver_module_map_regionized_verifies` で固定。
- 残り: なし(現状の 3 ループは all Region 形) - 残り: なし(現状の 3 ループは all Region 形)
- pipeline_v2 UsingResolverlang/src/compiler/pipeline_v2/using_resolver_box.hako - pipeline_v2 UsingResolverlang/src/compiler/pipeline_v2/using_resolver_box.hako
- 役割: modules_json 上で alias を解決する stateful helperテキスト収集は entry 側)。 - 役割: modules_json 上で alias を解決する stateful helperテキスト収集は entry 側)。

View File

@ -1,5 +1,7 @@
# Phase 25.3 — FuncScanner / StageB defs 安定化 # Phase 25.3 — FuncScanner / StageB defs 安定化
Status: 完了StageB fib defs canary 緑2025-11 時点)
## スコープ / ゴール ## スコープ / ゴール
- 対象レイヤ - 対象レイヤ
@ -22,6 +24,30 @@
- Loop/PHI の意味論は **LoopFormBuilder + LoopSnapshotMergeBox** に完全委譲したまま、 - Loop/PHI の意味論は **LoopFormBuilder + LoopSnapshotMergeBox** に完全委譲したまま、
- FuncScanner / StageB は「テキストスキャンJSON 組み立て」の箱として綺麗に分離された状態にする。 - FuncScanner / StageB は「テキストスキャンJSON 組み立て」の箱として綺麗に分離された状態にする。
## 完了ステータス2024-11-20 時点)
- `tools/smokes/v2/profiles/quick/core/phase251/stageb_fib_program_defs_canary_vm.sh` が緑Phase 25.3 の完了条件)。
- `defs``TestBox.fib` / `Main.main` が入り、`TestBox.fib.body.body[*]``Loop` ノードが含まれる状態を固定。
- StageB 本線の整理:
- `StageBDriverBox.main`: main 本文は `{…}` で包んだ block パーサ優先で JSON 化し、defs 側は `{"type":"Block","body":[…]}` に構造化して注入。
- `StageBFuncScannerBox._scan_methods`: block パーサ優先に揃え、Program パーサは `HAKO_STAGEB_FUNC_SCAN_PROG_FALLBACK=1` の opt-in 時だけ使う安全側トグルに変更skip_ws 崩れの再発防止)。
- Rust 層は無改変LoopForm v2 / LoopSnapshotMergeBox の SSOT をそのまま利用)。
## JSON v0 フロントと AOT ルートの契約Phase 25.3 時点)
- Program(JSON v0) の形:
- ルートは常に `{"version":0,"kind":"Program","body":[...]}`
- defs は `\"defs\":[{ \"name\", \"params\", \"box\", \"body\" }]` を追加する形で注入する。
- StageB / FuncScanner 経由の `defs[*].body` は必ず `{\"type\":\"Block\",\"body\":[Stmt...]}` でラップし、AOT/VM 側からは「通常の Block ノード」として読めるようにする。
- フロント/バックエンドの責務分離:
- StageB / FuncScanner: `.hako` テキスト → Program(JSON v0) まで(ループ/PHI の意味論は持たない)。
- Rust 側 LoopForm v2 / JSON v0 Bridge: Program(JSON v0) → MIR/AOT までLoop/PHI/SSA の SSOT
- StageB トグル整理(抜粋):
- `HAKO_STAGEB_FUNC_SCAN=1`(既定): defs を常に埋める。`0` で StageB からの defs 注入を無効化。
- `HAKO_STAGEB_PROGRAM_PARSE_FALLBACK=1`: main 本文で Program パーサ fallback を有効化(既定は OFF、block パーサ優先)。
- `HAKO_STAGEB_FUNC_SCAN_PROG_FALLBACK=1`: FuncScanner 側で Program パーサ fallback を有効化(既定は OFF
通常の開発・CI ではいずれも OFF にしておき、VM/パーサ調査時だけ optin で使う想定。
## すでに前提としている状態 ## すでに前提としている状態
- LoopForm v2 / PHI / snapshot: - LoopForm v2 / PHI / snapshot:
@ -118,27 +144,28 @@ FuncScanner / StageB のデバッグ時には、`scan_all_boxes` のループ
- ここでの主なバグは「SSA ではなく、StageB が fib ソースから defs を組み立てきれていないこと」なので、 - ここでの主なバグは「SSA ではなく、StageB が fib ソースから defs を組み立てきれていないこと」なので、
ループ構造や LoopForm には手を入れず、StageB 側のテキスト処理と defs 生成パスを中心に見る。 ループ構造や LoopForm には手を入れず、StageB 側のテキスト処理と defs 生成パスを中心に見る。
### 4. fib defs canary & ドキュメント更新 ### 4. fib defs canary & ドキュメント更新(完了)
- 対象: - 対象:
- `tools/smokes/v2/profiles/quick/core/phase251/stageb_fib_program_defs_canary_vm.sh` - `tools/smokes/v2/profiles/quick/core/phase251/stageb_fib_program_defs_canary_vm.sh`
- `docs/development/roadmap/phases/phase-25.1q/README.md` - `docs/development/roadmap/phases/phase-25.1q/README.md`
- `CURRENT_TASK.md` - `CURRENT_TASK.md`
- やること: 今回の結果:
- canary スクリプトで: - `tools/smokes/v2/profiles/quick/core/phase251/stageb_fib_program_defs_canary_vm.sh``rc=0` で安定緑。
- canary 内部で確認している条件:
- `Program.kind == "Program"` - `Program.kind == "Program"`
- `defs``TestBox.fib` が存在すること。 - `defs``TestBox.fib` / `Main.main` が含まれていること。
- `TestBox.fib.body``Loop` ノードが含まれること。 - `TestBox.fib.body` の直下(`body.body[*]``Loop` ノードが最低 1 つ含まれていること。
を満たした状態で `rc=0` になるまで確認する。 - これにより、FuncScanner / StageB 経由の fib defs ラインは LoopForm v2 + LoopSnapshotMergeBox の上で構造的に安定したとみなせる。
ドキュメント側:
- Phase 25.1q / 25.2 の README には、 - Phase 25.1q / 25.2 の README には、
- 「loop/PHI/スナップショットの SSOT は LoopForm v2 + LoopSnapshotMergeBox」 - 「loop/PHI/スナップショットの SSOT は LoopForm v2 + LoopSnapshotMergeBox」
- 「Phase 25.3 で FuncScanner / StageB defs もこの土台の上に載せた」 - 「Phase 25.3 で FuncScanner / StageB defs もこの土台の上に載せた」
というつながりを 1〜2 行で追記する という関係を 1〜2 行で追記済み
- `CURRENT_TASK.md` には: - `CURRENT_TASK.md` では Phase 25.3 を「StageB fib defs canary 緑」まで完了したフェーズとして整理し、
- Phase 25.3 のタスク完了状況FuncScanner fib / StageB fib canary 緑)と、 次フェーズを Stage1 UsingResolver ループ整理 / Stage1 CLI program-json selfhost 導線に設定した。
- その後に繋がる Stage1 UsingResolver / Stage1 CLI selfhost ラインへのブリッジ
を短く整理しておく。
### 5. mir_funcscanner_skip_ws 系テストの扱いflaky → dev 専用) ### 5. mir_funcscanner_skip_ws 系テストの扱いflaky → dev 専用)

View File

@ -69,7 +69,7 @@ Related docs:
- `IntArrayCore`(数値一次元配列コア) - `IntArrayCore`(数値一次元配列コア)
- `MatI64`行列箱・i64版 - `MatI64`行列箱・i64版
などを、「Rust プラグイン実装」ではなく **Hakorune 実装+ごく薄い intrinsic** に置き換えるための設計ロードマップを固める。 などを、「Rust プラグイン実装」ではなく **Hakorune 実装+ごく薄い intrinsic** に置き換えるための設計ロードマップを固める。
- 新しい箱・数値カーネル・標準ランタイム機能は **原則 .hako で実装する** 方針を明文化し、「Rust Freeze PolicySelfHost First)」を Phase 25 で具体化する。 - 新しい箱・数値カーネル・標準ランタイム機能は **原則 .hako で実装する** 方針を明文化し、「Rust Minimal PolicySelfHost First, but not Frozen」として Phase 25 で具体化する。
## レイヤー方針Ring0 / Ring1 ## レイヤー方針Ring0 / Ring1
@ -82,8 +82,8 @@ Related docs:
- **汎用 intrinsic** のみ提供(例: メモリ確保・生ポインタload/store・基本的な memcpy 等) - **汎用 intrinsic** のみ提供(例: メモリ確保・生ポインタload/store・基本的な memcpy 等)
**禁止 / 抑制:** **禁止 / 抑制:**
- 新しい Box 種類IntArrayCore / MatI64 / StringBuilder 等)を Rust 側に増やさない。 - 新しい Box 種類IntArrayCore / MatI64 / StringBuilder 等)の**本体ロジック**を Rust 側に増やさない(型安全な intrinsic のみに留める)
- 新しい最適化ロジック・言語ルール・Box メソッド実装を Rust に追加しないAGENTS.md 5.2 Rust Freeze Policy に準拠)。 - 新しい最適化ロジック・言語ルール・Box メソッド実装を Rust に追加しないAGENTS.md 5.2 Rust Minimal Policy に準拠)。
### Ring1Hakorune / Nyash ― System サブセット) ### Ring1Hakorune / Nyash ― System サブセット)
@ -112,11 +112,11 @@ Related docs:
Phase 25 は「設計とロードマップの確定」が主目的。実装・移行作業自体は後続フェーズ22.x/26.x など)で分割実施する。 Phase 25 は「設計とロードマップの確定」が主目的。実装・移行作業自体は後続フェーズ22.x/26.x など)で分割実施する。
### 1) Rust Freeze の明文化とチェックリスト ### 1) Rust Minimal Policy の明文化とチェックリスト
- 既存の「Rust Freeze PolicySelfHost First」を、ランタイム/箱/数値系に特化して再整理: - 既存の「Rust Freeze PolicySelfHost First」を、「SelfHost を支える最小必要な整備は許可する」Rust Minimal Policy として再整理:
- 新規 Box / ランタイム機能は Rust ではなく .hako で実装する。 - 新規 Box / ランタイム機能は Rust ではなく .hako で実装する。
- Rust 変更は「最小の intrinsic 追加」か「バグ修正」に限定。 - Rust 変更は「最小の intrinsic 追加」「Stage1/StageB ブリッジの改善」「エラーログ・可観測性の向上」か「バグ修正」に限定。
- PR / フェーズ用チェックリスト案を作成: - PR / フェーズ用チェックリスト案を作成:
- [ ] この変更は Ring0 の責務かVM/allocator/LLVM/OS FFI のみ) - [ ] この変更は Ring0 の責務かVM/allocator/LLVM/OS FFI のみ)
- [ ] 新しい Box/アルゴリズムを Rust に追加していないか? - [ ] 新しい Box/アルゴリズムを Rust に追加していないか?

View File

@ -1,6 +1,6 @@
# Stage1 Hakorune CLI DesignProposal # Stage1 Hakorune CLI DesignProposal
Status: design-onlyPhase 25.1 時点では仕様策定と導線の整理まで) Status: design-only + Stage0 stub 実装済みPhase 25.1 時点では仕様策定と導線の整理まで。selfhost EXE 本体は未実装
## ゴール ## ゴール
@ -15,6 +15,8 @@ Status: design-onlyPhase 25.1 時点では仕様策定と導線の整理ま
- 役割: プロセス起動・VM/LLVM コア・ny-llvmc 呼び出しなどの「Ring0」。 - 役割: プロセス起動・VM/LLVM コア・ny-llvmc 呼び出しなどの「Ring0」。
- Ny 側からは `env.mirbuilder.emit` / `env.codegen.emit_object` / `env.codegen.link_object` などの extern 名で見える。 - Ny 側からは `env.mirbuilder.emit` / `env.codegen.emit_object` / `env.codegen.link_object` などの extern 名で見える。
- Stage1 実行時は「CLI として」ではなく、これらのランタイムサービス層C-ABI/externとして利用することを前提とする。 - Stage1 実行時は「CLI として」ではなく、これらのランタイムサービス層C-ABI/externとして利用することを前提とする。
- Phase 25.1 現在は、Rust Stage0 から `.hako` 側 stub CLI`lang/src/runner/stage1_cli.hako`)を子プロセスとして起動する
ブリッジ(`src/runner/stage1_bridge.rs`)のみ実装済みで、自己ホスト EXE`target/selfhost/hakorune`)はまだ設計段階。
- Stage1Hakorune selfhost CLI - Stage1Hakorune selfhost CLI
- 実体: `target/selfhost/hakorune`Phase 25.1 では Ny Executor プロトタイプ)。 - 実体: `target/selfhost/hakorune`Phase 25.1 では Ny Executor プロトタイプ)。
- 役割: `.hako → Program(JSON) → MIR(JSON) → 実行/EXE` というパイプラインの制御。 - 役割: `.hako → Program(JSON) → MIR(JSON) → 実行/EXE` というパイプラインの制御。
@ -64,6 +66,10 @@ Phase 25.1a では、**`emit program-json` / `emit mir-json` / `build exe` の 3
- `--quiet` でログ抑制、`-o/--out` で出力 EXE パスを指定可能。C-API トグル(`NYASH_LLVM_USE_CAPI`, `HAKO_V1_EXTERN_PROVIDER_C_ABI`)が無効な場合は fail-fast。 - `--quiet` でログ抑制、`-o/--out` で出力 EXE パスを指定可能。C-API トグル(`NYASH_LLVM_USE_CAPI`, `HAKO_V1_EXTERN_PROVIDER_C_ABI`)が無効な場合は fail-fast。
- Stage1 バイナリ(`target/selfhost/hakorune`)を直接叩く際は `NYASH_NYRT_SILENT_RESULT=1` を付与し、stdout に JSON だけを流す運用を徹底する(`tools/selfhost/run_stage1_cli.sh` が環境セットとバイナリ検出を担当)。 - Stage1 バイナリ(`target/selfhost/hakorune`)を直接叩く際は `NYASH_NYRT_SILENT_RESULT=1` を付与し、stdout に JSON だけを流す運用を徹底する(`tools/selfhost/run_stage1_cli.sh` が環境セットとバイナリ検出を担当)。
#### デバッグ TipsPhase 25.1a
- `STAGE1_CLI_DEBUG=1` を付けると `.hako``stage1_main` の ENTRY ログが出る。Rust ブリッジが正しく Stage1 stub を呼んでいるか確認する際に使う。
- `NYASH_CLI_VERBOSE=1``2` を付けると Rust 側 bridge (`stage1_bridge.rs`) が子プロセス起動ログを出力する。
## `run` コマンド ## `run` コマンド
```text ```text

View File

@ -98,6 +98,67 @@ python3 tools/phi_trace_check.py --file tmp/phi_trace.jsonl --summary
- `tools/smokes/phi_trace_local.sh`(ビルド→サンプル実行→チェックを一括) - `tools/smokes/phi_trace_local.sh`(ビルド→サンプル実行→チェックを一括)
- `tools/smokes/v2/run.sh --profile quick|integration` で代表スモークを実行 - `tools/smokes/v2/run.sh --profile quick|integration` で代表スモークを実行
## MIR デバッグの入口まとめ
### 1. CLI レベルの MIR ダンプ
- ソースから直接 MIR を確認:
- `./target/release/nyash --dump-mir path/to/program.hako`
- VM 実行経路で MIR を一緒に吐く:
- `NYASH_VM_DUMP_MIR=1 ./target/release/nyash path/to/program.hako`
- JSON で詳細解析したい場合:
- `./target/release/nyash --emit-mir-json mir.json path/to/program.hako`
- 例: `jq '.functions[0].blocks' mir.json` でブロック構造を確認。
### 2. Scope / Loop ヒントNYASH_MIR_HINTS
- 環境変数でヒント出力を制御:
- `NYASH_MIR_HINTS="<target>|<filters>..."`
- 例:
- `NYASH_MIR_HINTS="trace|all"`stderr へ全ヒント)
- `NYASH_MIR_HINTS="jsonl=tmp/hints.jsonl|loop"`(ループ関連のみ JSONL 出力)
- 詳細: `docs/guides/scopebox.md`, `src/mir/hints.rs`
### 3. __mir__ ロガー(.hako から仕込む MIR ログ)
- 目的:
- `.hako` 側のループや SSA まわりを「MIR レベル」で観測するための dev 専用フック。
- 実行意味論には影響しないEffect::DebugLog のみ)。
- 構文(.hako 内):
```hako
__mir__.log("label", v1, v2, ...)
__mir__.mark("label")
```
- 第1引数は String リテラル必須(それ以外は通常の呼び出し扱い)。
- `log` は第2引数以降の式を評価し、その ValueId 群を記録。
- `mark` はラベルだけのマーカー。
- 戻り値は Void 定数扱いのため、式コンテキストに書いても型崩れしない。
- 実行時の有効化:
- `NYASH_MIR_DEBUG_LOG=1 ./target/release/nyash path/to/program.hako`
- VM の MIR interpreter が次のようなログを stderr に出力:
```text
[MIR-LOG] label: %10=123 %11="foo"
```
- 実装位置:
- lowering: `src/mir/builder/calls/build.rs` の `try_build_mir_debug_call`receiver が `__mir__` のときに `MirInstruction::DebugLog` を挿入)。
- 実行: `src/backend/mir_interpreter/handlers/mod.rs``NYASH_MIR_DEBUG_LOG=1` のときだけログを出す)。
- 利用例(ループ観測の定番パターン):
```hako
loop(i < n) {
__mir__.log("loop/head", i, n)
...
}
__mir__.mark("loop/exit")
```
- FuncScanner / StageB では、skip_whitespace や scan_all_boxes のループ頭・出口に挿入して、ValueId と値の流れを追跡する用途で使用している。
## 🔌 **プラグインテスターBID-FFI診断ツール** ## 🔌 **プラグインテスターBID-FFI診断ツール**
```bash ```bash

View File

@ -795,8 +795,8 @@ static box StageBFuncScannerBox {
} }
local body = s.substring(open_idx + 1, close_idx) local body = s.substring(open_idx + 1, close_idx)
// Include Main.main as well so defs covers entry + helpers
local skip_main = 0 local skip_main = 0
if box_name == "Main" { skip_main = 1 }
local include_me = 0 local include_me = 0
if box_name != "Main" { include_me = 1 } if box_name != "Main" { include_me = 1 }
local defs_box = StageBFuncScannerBox._scan_methods(body, box_name, skip_main, include_me) local defs_box = StageBFuncScannerBox._scan_methods(body, box_name, skip_main, include_me)
@ -979,16 +979,29 @@ static box StageBFuncScannerBox {
method_body = FuncScannerBox._strip_comments(method_body) method_body = FuncScannerBox._strip_comments(method_body)
method_body = FuncScannerBox._trim(method_body) method_body = FuncScannerBox._trim(method_body)
// Parse method body to JSON (statement list) using block parser // Parse method body to JSON (statement list)
// Policy: block parser first (braced) to avoid VM dispatch misrouting, then Program parser as backup.
local body_json = null local body_json = null
if method_body.length() > 0 { if method_body.length() > 0 {
// 1st: block parser with explicit braces (LoopForm-friendly, avoids ctx misbinding)
{
local p = new ParserBox() local p = new ParserBox()
p.stage3_enable(1) p.stage3_enable(1)
local block_src = "{" + method_body + "}" local block_src = "{" + method_body + "}"
local block_res = p.parse_block2(block_src, 0) local block_res = p.parse_block2(block_src, 0)
local at = block_res.lastIndexOf("@") local at = block_res.lastIndexOf("@")
if at >= 0 { if at >= 0 { body_json = block_res.substring(0, at) }
body_json = block_res.substring(0, at) }
// 2nd: program parser trims body JSON if block path produced nothing (opt-in)
if body_json == null || body_json == "" || body_json == "[]" {
local allow_prog = env.get("HAKO_STAGEB_FUNC_SCAN_PROG_FALLBACK")
if allow_prog != null && ("" + allow_prog) == "1" {
local p2 = new ParserBox()
p2.stage3_enable(1)
local prog_json = p2.parse_program2(method_body)
local extracted = me._extract_body_from_program(prog_json)
if extracted.length() > 0 { body_json = extracted }
}
} }
} }
@ -1017,6 +1030,49 @@ static box StageBFuncScannerBox {
return 0 return 0
} }
// Helper: extract Program.body array from Program(JSON) string
_extract_body_from_program(prog_json) {
if prog_json == null { return "" }
local s = "" + prog_json
local n = s.length()
local head = "\"body\":"
local hlen = head.length()
local start = -1
{
local i = 0
loop(i + hlen <= n) {
if s.substring(i, i + hlen) == head { start = i + hlen break }
i = i + 1
}
}
if start < 0 { return "" }
// Find '[' after "body":
local open = -1
{
local j = start
loop(j < n) {
local ch = s.substring(j, j + 1)
if ch == "[" { open = j break }
j = j + 1
}
}
if open < 0 { return "" }
local depth = 0
local k = open
loop(k < n) {
local ch2 = s.substring(k, k + 1)
if ch2 == "[" { depth = depth + 1 }
else if ch2 == "]" {
depth = depth - 1
if depth == 0 { return s.substring(open, k + 1) }
}
k = k + 1
}
return ""
}
// Helper: 空白文字スキップFuncScannerBox に委譲) // Helper: 空白文字スキップFuncScannerBox に委譲)
_skip_whitespace(s, idx) { _skip_whitespace(s, idx) {
return FuncScannerBox.skip_whitespace(s, idx) return FuncScannerBox.skip_whitespace(s, idx)
@ -1151,14 +1207,30 @@ static box StageBDriverBox {
// 6) Parse and emit Stage1 JSON v0 (Program) // 6) Parse and emit Stage1 JSON v0 (Program)
// Bridge(JSON v0) が Program v0 を受け取り MIR に lowering するため、ここでは AST(JSON v0) を出力する。 // Bridge(JSON v0) が Program v0 を受け取り MIR に lowering するため、ここでは AST(JSON v0) を出力する。
// 既定で MIR 直出力は行わない(重い経路を避け、一行出力を保証)。 // 既定で MIR 直出力は行わない(重い経路を避け、一行出力を保証)。
// StageB/selfhost: body_src は Main.main 内のブロック本文なので、ParserBox の block パーサ // StageB/selfhost: body_src は Main.main 内のブロック本文(外側の {} は含まない)。
// を直接使って Program(JSON) を構成する(トップレベル parser_loop は使わない) // 安定性向上のため Program パーサを優先し、空の場合のみ block パーサで再度包んで解釈する
local block_res = p.parse_block2(body_src, 0)
local ast_json = "{\"version\":0,\"kind\":\"Program\",\"body\":[]}" local ast_json = "{\"version\":0,\"kind\":\"Program\",\"body\":[]}"
{ {
local body_json = null
// Prefer block parser with explicit braces to avoid VM dispatch drift seen on program parser.
if body_src != null {
local block_src = "{" + body_src + "}"
local block_res = p.parse_block2(block_src, 0)
local at = block_res.lastIndexOf("@") local at = block_res.lastIndexOf("@")
if at >= 0 { if at >= 0 { body_json = block_res.substring(0, at) }
local body_json = block_res.substring(0, at) }
// Optional: enable program parser fallback only when explicitly requested
if (body_json == null || body_json == "" || body_json == "[]") {
local allow_prog = env.get("HAKO_STAGEB_PROGRAM_PARSE_FALLBACK")
if allow_prog != null && ("" + allow_prog) == "1" {
local prog_json = p.parse_program2(body_src)
body_json = StageBFuncScannerBox._extract_body_from_program(prog_json)
}
}
if body_json != null && body_json != "" {
ast_json = "{\"version\":0,\"kind\":\"Program\",\"body\":" + body_json + "}" ast_json = "{\"version\":0,\"kind\":\"Program\",\"body\":" + body_json + "}"
} }
} }
@ -1246,6 +1318,8 @@ static box StageBDriverBox {
local mparams = def.get("params") local mparams = def.get("params")
local mbody = "" + def.get("body_json") local mbody = "" + def.get("body_json")
local mbox = "" + def.get("box") local mbox = "" + def.get("box")
// Wrap body in Block node to align with Program.body shape expectations
local wrapped_body = "{\"type\":\"Block\",\"body\":" + mbody + "}"
// Build params array JSON // Build params array JSON
local params_arr = "[" local params_arr = "["
local pi = 0 local pi = 0
@ -1257,7 +1331,7 @@ static box StageBDriverBox {
} }
params_arr = params_arr + "]" params_arr = params_arr + "]"
if mi > 0 { defs_json = defs_json + "," } if mi > 0 { defs_json = defs_json + "," }
defs_json = defs_json + "{\"name\":\"" + mname + "\",\"params\":" + params_arr + ",\"body\":" + mbody + ",\"box\":\"" + mbox + "\"}" defs_json = defs_json + "{\"name\":\"" + mname + "\",\"params\":" + params_arr + ",\"body\":" + wrapped_body + ",\"box\":\"" + mbox + "\"}"
mi = mi + 1 mi = mi + 1
} }
defs_json = defs_json + "]" defs_json = defs_json + "]"

View File

@ -3,6 +3,9 @@
// - Collect line-based using declarations via UsingCollectorBox. // - Collect line-based using declarations via UsingCollectorBox.
// - Resolve namespace targets against `nyash.toml` `[modules]` entries (fed via env). // - Resolve namespace targets against `nyash.toml` `[modules]` entries (fed via env).
// - Read the referenced .hako files and return a concatenated prefix for StageB. // - Read the referenced .hako files and return a concatenated prefix for StageB.
// Structure tags:
// [Region] JSON/ArrayBox/MapBox ループはすべて Region+next_i 形に統一して SSA を安定化。
// [LoopForm] carrier=pinned を明示し、continue/break を next_i で合流させる。
// //
// Env requirements: // Env requirements:
// - HAKO_STAGEB_MODULES_LIST: `name=path` entries joined by "|||". // - HAKO_STAGEB_MODULES_LIST: `name=path` entries joined by "|||".
@ -13,6 +16,10 @@ using lang.compiler.parser.scan.parser_common_utils_box as ParserCommonUtilsBox
using selfhost.shared.json.utils.json_frag as JsonFragBox using selfhost.shared.json.utils.json_frag as JsonFragBox
static box Stage1UsingResolverBox { static box Stage1UsingResolverBox {
// Entrypoint: collect `using` entries and stitch a prefix.
// - HAKO_STAGEB_APPLY_USINGS=0 → prefix ""defs/body には何も足さない)
// - Depth guard prevents recursive self-calls from cascading.
// [Region] entries 走査は pos/next_pos で一意に前進。pinned: entries/seen, carrier: i。
resolve_for_source(src) { resolve_for_source(src) {
// Shallow recursion guard to prevent accidental self-recursion in Stage1 using resolver. // Shallow recursion guard to prevent accidental self-recursion in Stage1 using resolver.
{ {
@ -24,50 +31,88 @@ static box Stage1UsingResolverBox {
env.set("HAKO_STAGEB_USING_DEPTH", "1") env.set("HAKO_STAGEB_USING_DEPTH", "1")
} }
if src == null { return "" } local prefix = ""
if src != null {
local apply_flag = env.get("HAKO_STAGEB_APPLY_USINGS") local apply_flag = env.get("HAKO_STAGEB_APPLY_USINGS")
if apply_flag != null && ("" + apply_flag) == "0" { return "" } // apply_flag==0 なら prefix は空のまま返すStageB defs/body に変更なし)
if apply_flag == null || ("" + apply_flag) != "0" {
local entries = me._collect_using_entries(src) local entries = me._collect_using_entries(src)
if entries == null || entries.length() == 0 { return "" } if entries != null && entries.length() > 0 {
local modules = me._build_module_map() local modules = me._build_module_map()
local seen = new MapBox() local seen = new MapBox()
local prefix = ""
local i = 0 local i = 0
local n = entries.length() local n = entries.length()
loop(i < n) { loop(i < n) {
// Region+next_i 形で多経路を末尾合流させるSSA/PHI 安定化)
local next_i = i + 1
local entry = entries.get(i) local entry = entries.get(i)
local name = "" + entry.get("name") local name = "" + entry.get("name")
if name == "" { i = i + 1 continue }
local path = entry.get("path") // 早期スキップ条件をまとめて evaluate
local should_emit = 1
if name == "" { should_emit = 0 }
// path 解決(明示 path 優先、無ければ modules マップ)
local path = null
if should_emit == 1 {
path = entry.get("path")
if path == null || ("" + path) == "" { if path == null || ("" + path) == "" {
path = me._lookup_module_path(modules, name) path = me._lookup_module_path(modules, name)
} else { } else {
path = "" + path path = "" + path
} }
if path == null || path == "" { i = i + 1 continue } if path == null || path == "" { should_emit = 0 }
local abs_path = me._abs_path(path)
if abs_path == null || abs_path == "" { i = i + 1 continue }
if seen.get(abs_path) == "1" { i = i + 1 continue }
local code = me._read_file(abs_path)
if code == null { i = i + 1 continue }
seen.set(abs_path, "1")
prefix = prefix + "\n" + code + "\n"
i = i + 1
} }
// Clear depth guard before returning // abs path 化
local abs_path = null
if should_emit == 1 {
abs_path = me._abs_path(path)
if abs_path == null || abs_path == "" { should_emit = 0 }
}
// 重複 guard
if should_emit == 1 {
if seen.get(abs_path) == "1" { should_emit = 0 }
}
// ファイル読み込み
if should_emit == 1 {
local code = me._read_file(abs_path)
if code == null { should_emit = 0 }
if should_emit == 1 {
seen.set(abs_path, "1")
prefix = prefix + "\n" + code + "\n"
}
}
i = next_i
}
}
}
}
// Clear depth guard before returning適用なしでも guard を戻す)
env.set("HAKO_STAGEB_USING_DEPTH", "0") env.set("HAKO_STAGEB_USING_DEPTH", "0")
return prefix return prefix
} }
// Program(JSON v0) に対する using 解決IOなし、SSOT委譲
// - modules_json: nyash.toml [modules] 相当の JSON
// - using_entries_json: UsingCollectorBox.collect の生出力
// - ctx: resolve_ssot_box 向けのヒントmodules/cwd/using_paths など)
resolve_for_program_json(program_json, modules_json, using_entries_json, ctx) {
using hako.using.resolve.ssot as UsingResolveSSOTBox
// MVP: IO せずに SSOT が shape を整えるだけ。現状は透過返し。
local resolved = UsingResolveSSOTBox.resolve_modules(modules_json, using_entries_json, ctx)
// 返すものは modules_json をそのまま(今は純粋パス)。将来 prefix/string を付与する。
return resolved
}
// Collect entries from UsingCollectorBox JSON output. // Collect entries from UsingCollectorBox JSON output.
// Responsibility: JSON を配列にパースするだけpath 解決やファイル読み込みは呼び元)。
// [Region] JSON スキャンは pos/next_pos を明示し、early-exit も next_pos=n で一本化。
_collect_using_entries(src) { _collect_using_entries(src) {
local json = UsingCollectorBox.collect(src) local json = UsingCollectorBox.collect(src)
if json == null || json == "" || json == "[]" { return null } if json == null || json == "" || json == "[]" { return null }
@ -75,8 +120,12 @@ static box Stage1UsingResolverBox {
local pos = 0 local pos = 0
local n = json.length() local n = json.length()
loop(pos < n) { loop(pos < n) {
// Region+next_pos 形break を next_pos=n で表現)
local next_pos = n
local name_idx = JsonFragBox.index_of_from(json, "\"name\":\"", pos) local name_idx = JsonFragBox.index_of_from(json, "\"name\":\"", pos)
if name_idx < 0 { break } if name_idx < 0 {
next_pos = n
} else {
local name = JsonFragBox.read_string_after(json, name_idx + 7) local name = JsonFragBox.read_string_after(json, name_idx + 7)
local obj_end = JsonFragBox.index_of_from(json, "}", name_idx) local obj_end = JsonFragBox.index_of_from(json, "}", name_idx)
if obj_end < 0 { obj_end = n } if obj_end < 0 { obj_end = n }
@ -91,11 +140,16 @@ static box Stage1UsingResolverBox {
entry.set("name", name) entry.set("name", name)
if path != null { entry.set("path", path) } if path != null { entry.set("path", path) }
out.push(entry) out.push(entry)
pos = obj_end + 1 next_pos = obj_end + 1
}
pos = next_pos
} }
return out return out
} }
// Build modules map from env `HAKO_STAGEB_MODULES_LIST` ("name=path" joined by "|||").
// Responsibility: env を MapBox に積むだけ。存在しない/空なら空 Map を返す。
// [Region] delim 走査も start/next_start で一本化し、continue/break を排除した形に固定。
_build_module_map() { _build_module_map() {
local map = new MapBox() local map = new MapBox()
local raw = env.get("HAKO_STAGEB_MODULES_LIST") local raw = env.get("HAKO_STAGEB_MODULES_LIST")
@ -105,13 +159,20 @@ static box Stage1UsingResolverBox {
local delim = "|||" local delim = "|||"
local start = 0 local start = 0
loop(true) { local cont = 1
loop(cont == 1) {
local next_start = str.length()
local next = ParserCommonUtilsBox.index_of(str, start, delim) local next = ParserCommonUtilsBox.index_of(str, start, delim)
local seg = "" local seg = ""
if next >= 0 { seg = str.substring(start, next) } else { seg = str.substring(start, str.length()) } if next >= 0 {
seg = str.substring(start, next)
next_start = next + delim.length()
} else {
seg = str.substring(start, str.length())
cont = 0
}
me._push_module_entry(map, seg) me._push_module_entry(map, seg)
if next < 0 { break } start = next_start
start = next + delim.length()
} }
return map return map
} }
@ -152,7 +213,14 @@ static box Stage1UsingResolverBox {
if path == null { return null } if path == null { return null }
@fb = new FileBox() @fb = new FileBox()
@ok = fb.open(path, "r") @ok = fb.open(path, "r")
if ok != 1 { return null } if ok != 1 {
// Dev note: keep quiet by default; enable verbose by setting HAKO_STAGEB_USING_DEBUG=1.
local dbg = env.get("HAKO_STAGEB_USING_DEBUG")
if dbg != null && ("" + dbg) == "1" {
print("[stageb/using_resolver] failed to open file: " + path)
}
return null
}
@content = fb.read() @content = fb.read()
fb.close() fb.close()
return content return content

View File

@ -51,11 +51,14 @@ box ParserBox {
// === JSON utilities === // === JSON utilities ===
esc_json(s) { esc_json(s) {
// Defensive: accept null/void and normalize to string once
if s == null { return "" }
local str = "" + s
local out = "" local out = ""
local i = 0 local i = 0
local n = s.length() local n = str.length()
loop(i < n) { loop(i < n) {
local ch = s.substring(i, i+1) local ch = str.substring(i, i+1)
if ch == "\\" { out = out + "\\\\" } if ch == "\\" { out = out + "\\\\" }
else { if ch == "\"" { out = out + "\\\"" } else { if ch == "\"" { out = out + "\\\"" }
else { out = out + ch } } else { out = out + ch } }

View File

@ -1,20 +1,116 @@
// Moved from apps/selfhost-compiler/boxes/parser/scan/parser_string_utils_box.hako // Moved from apps/selfhost-compiler/boxes/parser/scan/parser_string_utils_box.hako
// ParserStringUtilsBox — Delegation to StringHelpers (unified string utilities) // ParserStringUtilsBox — Minimal self-contained string helpers
// Responsibility: Backward compatibility wrapper for parser code // Responsibility: Backward compatibility wrapper for parser code
// Notes: All functionality now provided by apps/selfhost/common/string_helpers.hako // Notes: sh_core への依存で VM 差分が出たため、ここに最小実装を内包する。
using sh_core as StringHelpers
static box ParserStringUtilsBox { static box ParserStringUtilsBox {
// Delegate all methods to StringHelpers (centralized implementation) // Numeric to string (minimal)
i2s(v) { return StringHelpers.int_to_str(v) } i2s(v) {
is_digit(ch) { return StringHelpers.is_digit(ch) } local n = ParserStringUtilsBox.to_int(v)
is_space(ch) { return StringHelpers.is_space(ch) } if n == 0 { return "0" }
is_alpha(ch) { return StringHelpers.is_alpha(ch) } if n < 0 { return "-" + ParserStringUtilsBox.i2s(0 - n) }
starts_with(src, i, pat) { return StringHelpers.starts_with(src, i, pat) } local out = ""
starts_with_kw(src, i, kw) { return StringHelpers.starts_with_kw(src, i, kw) } loop(n > 0) {
index_of(src, i, pat) { return StringHelpers.index_of(src, i, pat) } local d = n % 10
trim(s) { return StringHelpers.trim(s) } out = "0123456789".substring(d, d + 1) + out
to_int(s) { return StringHelpers.to_i64(s) } n = n / 10
skip_ws(src, i) { return StringHelpers.skip_ws(src, i) } }
return out
}
is_digit(ch) { return ch >= "0" && ch <= "9" }
is_space(ch) { return ch == " " || ch == "\t" || ch == "\n" || ch == "\r" }
is_alpha(ch) { return (ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || ch == "_" }
starts_with(src, i, pat) {
if src == null || pat == null { return 0 }
local n = src.length()
local m = pat.length()
if i + m > n { return 0 }
local k = 0
loop(k < m) {
if src.substring(i + k, i + k + 1) != pat.substring(k, k + 1) { return 0 }
k = k + 1
}
return 1
}
// Keyword match with boundaryStageB 安定性のためここで実装)
starts_with_kw(src, i, kw) {
if src == null || kw == null { return 0 }
local n = src.length()
local m = kw.length()
if i + m > n { return 0 }
local k = 0
loop(k < m) {
if src.substring(i + k, i + k + 1) != kw.substring(k, k + 1) { return 0 }
k = k + 1
}
local j = i + m
if j >= n { return 1 }
local ch = src.substring(j, j + 1)
if ParserStringUtilsBox.is_alpha(ch) == 1 || ParserStringUtilsBox.is_digit(ch) == 1 { return 0 }
return 1
}
index_of(src, i, pat) {
if src == null || pat == null { return -1 }
local n = src.length()
local m = pat.length()
if m == 0 { return i }
local j = i
loop(j + m <= n) {
if ParserStringUtilsBox.starts_with(src, j, pat) == 1 { return j }
j = j + 1
}
return -1
}
trim(s) {
if s == null { return "" }
local str = "" + s
local n = str.length()
local b = 0
loop(b < n) {
local ch = str.substring(b, b + 1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { b = b + 1 } else { break }
}
local e = n
loop(e > b) {
local ch = str.substring(e - 1, e)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { e = e - 1 } else { break }
}
if e > b { return str.substring(b, e) }
return ""
}
to_int(s) {
if s == null { return 0 }
local str = "" + s
local n = str.length()
if n == 0 { return 0 }
local neg = 0
local i = 0
if str.substring(0, 1) == "-" { neg = 1 i = 1 }
local acc = 0
loop(i < n) {
local ch = str.substring(i, i + 1)
if ch < "0" || ch > "9" { break }
acc = acc * 10 + ("0123456789".indexOf(ch))
i = i + 1
}
if neg == 1 { return 0 - acc }
return acc
}
skip_ws(src, i) {
if src == null { return i }
local n = src.length()
local j = i
loop(j < n) {
local ch = src.substring(j, j + 1)
if ParserStringUtilsBox.is_space(ch) == 1 { j = j + 1 } else { break }
}
return j
}
} }

View File

@ -7,15 +7,16 @@ using sh_core as StringHelpers // Required: using chain resolution not implemen
static box ParserControlBox { static box ParserControlBox {
// Parse: if (cond) { ... } else { ... } // Parse: if (cond) { ... } else { ... }
parse_if(src, i, stmt_start, ctx) { parse_if(src, i, stmt_start, ctx) {
// Shallow recursion guard for StageB / selfhost: protect parse_if from // Depth guard (allow nesting, cap runaway recursion)
// accidental self-recursion via malformed input or control flow bugs. local if_depth_before = 0
{ {
local depth = env.get("HAKO_STAGEB_IF_DEPTH") local depth_raw = env.get("HAKO_STAGEB_IF_DEPTH")
if depth != null && ("" + depth) != "0" { if depth_raw != null { if_depth_before = StringHelpers.to_i64("" + depth_raw) }
if if_depth_before >= 64 {
print("[stageb/recursion] ParserControlBox.parse_if recursion detected") print("[stageb/recursion] ParserControlBox.parse_if recursion detected")
return "{\"type\":\"If\",\"cond\":{\"type\":\"Bool\",\"value\":false},\"then\":[]}" return "{\"type\":\"If\",\"cond\":{\"type\":\"Bool\",\"value\":false},\"then\":[]}"
} }
env.set("HAKO_STAGEB_IF_DEPTH", "1") env.set("HAKO_STAGEB_IF_DEPTH", StringHelpers.int_to_str(if_depth_before + 1))
} }
local j = i + 2 // skip "if" local j = i + 2 // skip "if"
j = ctx.skip_ws(src, j) j = ctx.skip_ws(src, j)
@ -52,7 +53,7 @@ static box ParserControlBox {
} }
ctx.gpos_set(j) ctx.gpos_set(j)
env.set("HAKO_STAGEB_IF_DEPTH", "0") env.set("HAKO_STAGEB_IF_DEPTH", StringHelpers.int_to_str(if_depth_before))
if else_json == null { if else_json == null {
return "{\"type\":\"If\",\"cond\":" + cond + ",\"then\":" + then_json + "}" return "{\"type\":\"If\",\"cond\":" + cond + ",\"then\":" + then_json + "}"
} else { } else {
@ -63,14 +64,15 @@ static box ParserControlBox {
// Parse: loop(cond) { ... } // Parse: loop(cond) { ... }
// Dev tracing: set HAKO_PARSER_TRACE_LOOP=1 to enable debug prints // Dev tracing: set HAKO_PARSER_TRACE_LOOP=1 to enable debug prints
parse_loop(src, i, stmt_start, ctx) { parse_loop(src, i, stmt_start, ctx) {
// Shallow recursion guard for parse_loop local loop_depth_before = 0
{ {
local depth = env.get("HAKO_STAGEB_LOOP_DEPTH") local depth_raw = env.get("HAKO_STAGEB_LOOP_DEPTH")
if depth != null && ("" + depth) != "0" { if depth_raw != null { loop_depth_before = StringHelpers.to_i64("" + depth_raw) }
if loop_depth_before >= 64 {
print("[stageb/recursion] ParserControlBox.parse_loop recursion detected") print("[stageb/recursion] ParserControlBox.parse_loop recursion detected")
return "{\"type\":\"Loop\",\"cond\":{\"type\":\"Bool\",\"value\":false},\"body\":[]}" return "{\"type\":\"Loop\",\"cond\":{\"type\":\"Bool\",\"value\":false},\"body\":[]}"
} }
env.set("HAKO_STAGEB_LOOP_DEPTH", "1") env.set("HAKO_STAGEB_LOOP_DEPTH", StringHelpers.int_to_str(loop_depth_before + 1))
} }
local trace = 0 // dev-only (disabled in Stage-B runtime: no env API here) local trace = 0 // dev-only (disabled in Stage-B runtime: no env API here)
@ -154,7 +156,7 @@ static box ParserControlBox {
if j < src.length() { j = j + 1 } else { j = src.length() } if j < src.length() { j = j + 1 } else { j = src.length() }
} }
ctx.gpos_set(j) ctx.gpos_set(j)
env.set("HAKO_STAGEB_LOOP_DEPTH", "0") env.set("HAKO_STAGEB_LOOP_DEPTH", StringHelpers.int_to_str(loop_depth_before))
return "{\"type\":\"Loop\",\"cond\":" + cond + ",\"body\":" + body_json + "}" return "{\"type\":\"Loop\",\"cond\":" + cond + ",\"body\":" + body_json + "}"
} }
@ -222,15 +224,27 @@ static box ParserControlBox {
// Parse: { stmt1; stmt2; ... } // Parse: { stmt1; stmt2; ... }
parse_block(src, i, ctx) { parse_block(src, i, ctx) {
local depth_before = 0
{ {
local depth = env.get("HAKO_STAGEB_BLOCK_DEPTH") local depth_raw = env.get("HAKO_STAGEB_BLOCK_DEPTH")
if depth != null && ("" + depth) != "0" { if depth_raw != null { depth_before = StringHelpers.to_i64("" + depth_raw) }
if depth_before >= 64 {
print("[stageb/recursion] ParserControlBox.parse_block recursion detected") print("[stageb/recursion] ParserControlBox.parse_block recursion detected")
return "[]@" + ctx.i2s(i) return "[]@" + ctx.i2s(i)
} }
env.set("HAKO_STAGEB_BLOCK_DEPTH", "1") env.set("HAKO_STAGEB_BLOCK_DEPTH", StringHelpers.int_to_str(depth_before + 1))
} }
local j = ctx.skip_ws(src, i) local j = ctx.skip_ws(src, i)
// StageB safety: inline whitespace skip in case ctx.skip_ws fails to advance on VM
{
local n = src.length()
local jj = j
loop(jj < n) {
local ch = src.substring(jj, jj+1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { jj = jj + 1 } else { break }
}
j = jj
}
if src.substring(j, j+1) != "{" { return "[]@" + ctx.i2s(j) } if src.substring(j, j+1) != "{" { return "[]@" + ctx.i2s(j) }
j = j + 1 j = j + 1
@ -283,7 +297,7 @@ static box ParserControlBox {
} }
body = body + "]" body = body + "]"
env.set("HAKO_STAGEB_BLOCK_DEPTH", "0") env.set("HAKO_STAGEB_BLOCK_DEPTH", StringHelpers.int_to_str(depth_before))
return body + "@" + ctx.i2s(j) return body + "@" + ctx.i2s(j)
} }
} }

View File

@ -15,12 +15,23 @@ static box ParserStmtBox {
local depth = env.get("HAKO_STAGEB_STMT_DEPTH") local depth = env.get("HAKO_STAGEB_STMT_DEPTH")
if depth != null && ("" + depth) != "0" { if depth != null && ("" + depth) != "0" {
print("[stageb/recursion] ParserStmtBox.parse recursion detected") print("[stageb/recursion] ParserStmtBox.parse recursion detected")
// Fail-fast: return empty stmt and keep position unchanged // Allow re-entry: reset depth to 1 and continueドロップしない
return "" env.set("HAKO_STAGEB_STMT_DEPTH", "1")
} } else {
env.set("HAKO_STAGEB_STMT_DEPTH", "1") env.set("HAKO_STAGEB_STMT_DEPTH", "1")
} }
}
local j = ctx.skip_ws(src, i) local j = ctx.skip_ws(src, i)
// VM 差分で skip_ws が前進しないパスがあったため、手動で最小限の空白スキップをフォローする。
{
local n = src.length()
local jj = j
loop(jj < n) {
local ch = src.substring(jj, jj+1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { jj = jj + 1 } else { break }
}
j = jj
}
{ {
local tr = env.get("NYASH_PARSER_TRACE") local tr = env.get("NYASH_PARSER_TRACE")
if tr != null && ("" + tr) == "1" { if tr != null && ("" + tr) == "1" {
@ -86,49 +97,6 @@ static box ParserStmtBox {
return out_using return out_using
} }
// assignment: IDENT '=' expr
if j < src.length() && ctx.is_alpha(src.substring(j, j+1)) {
{
local tr = env.get("NYASH_PARSER_TRACE")
if tr != null && ("" + tr) == "1" {
print("[parser/stmt] kind=assign-or-local j=" + ("" + j))
}
}
local idp0 = ctx.read_ident2(src, j)
local at0 = idp0.lastIndexOf("@")
if at0 > 0 {
local name0 = idp0.substring(0, at0)
local k0 = ctx.to_int(idp0.substring(at0+1, idp0.length()))
k0 = ctx.skip_ws(src, k0)
if k0 < src.length() && src.substring(k0, k0+1) == "=" {
local eq_two = "="
if k0 + 1 < src.length() { eq_two = src.substring(k0, k0+2) }
if eq_two != "==" {
k0 = k0 + 1
k0 = ctx.skip_ws(src, k0)
local default_local = "{\"type\":\"Int\",\"value\":0}"
local expr_json0 = default_local
local end_pos0 = k0
if k0 < src.length() {
local ahead = src.substring(k0, k0+1)
if ahead != "}" && ahead != ";" {
expr_json0 = ctx.parse_expr2(src, k0)
end_pos0 = ctx.gpos_get()
}
}
k0 = end_pos0
if k0 <= stmt_start {
if k0 < src.length() { k0 = k0 + 1 } else { k0 = src.length() }
}
ctx.gpos_set(k0)
// Reset recursion guard before returning
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return "{\"type\":\"Local\",\"name\":\"" + name0 + "\",\"expr\":" + expr_json0 + "}"
}
}
}
}
// return statement // return statement
if ctx.starts_with_kw(src, j, "return") == 1 { if ctx.starts_with_kw(src, j, "return") == 1 {
{ {
@ -288,6 +256,49 @@ static box ParserStmtBox {
return out_try return out_try
} }
// assignment: IDENT '=' exprキーワードより後段に配置して、'loop' などを誤認しないようにする)
if j < src.length() && ctx.is_alpha(src.substring(j, j+1)) {
{
local tr = env.get("NYASH_PARSER_TRACE")
if tr != null && ("" + tr) == "1" {
print("[parser/stmt] kind=assign-or-local j=" + ("" + j))
}
}
local idp0 = ctx.read_ident2(src, j)
local at0 = idp0.lastIndexOf("@")
if at0 > 0 {
local name0 = idp0.substring(0, at0)
local k0 = ctx.to_int(idp0.substring(at0+1, idp0.length()))
k0 = ctx.skip_ws(src, k0)
if k0 < src.length() && src.substring(k0, k0+1) == "=" {
local eq_two = "="
if k0 + 1 < src.length() { eq_two = src.substring(k0, k0+2) }
if eq_two != "==" {
k0 = k0 + 1
k0 = ctx.skip_ws(src, k0)
local default_local = "{\"type\":\"Int\",\"value\":0}"
local expr_json0 = default_local
local end_pos0 = k0
if k0 < src.length() {
local ahead = src.substring(k0, k0+1)
if ahead != "}" && ahead != ";" {
expr_json0 = ctx.parse_expr2(src, k0)
end_pos0 = ctx.gpos_get()
}
}
k0 = end_pos0
if k0 <= stmt_start {
if k0 < src.length() { k0 = k0 + 1 } else { k0 = src.length() }
}
ctx.gpos_set(k0)
// Reset recursion guard before returning
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return "{\"type\":\"Local\",\"name\":\"" + name0 + "\",\"expr\":" + expr_json0 + "}"
}
}
}
}
// Fallback: expression or unknown token // Fallback: expression or unknown token
{ {
local tr = env.get("NYASH_PARSER_TRACE") local tr = env.get("NYASH_PARSER_TRACE")
@ -365,6 +376,8 @@ static box ParserStmtBox {
if j < src.length() { j = j + 1 } else { j = src.length() } if j < src.length() { j = j + 1 } else { j = src.length() }
} }
ctx.gpos_set(j) ctx.gpos_set(j)
// Reset recursion guard before returning (using statements participate in stmt depth tracking)
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return "" return ""
} }
} }

View File

@ -1,4 +1,8 @@
// UsingResolverBox — static, stateful resolver helpersインスタンス禁止・VM互換 // UsingResolverBox — static, stateful resolver helpersインスタンス禁止・VM互換
// Boundary memo:
// - entry/using_resolver_box: file I/O + using 収集、modules_list を MapBox へ積む役。
// - pipeline_v2/using_resolver_box: modules_json 上で alias/path をテキスト検索で解決する役。
// RegexFlow の単一路ループのみで continue/backedge 分岐を持たないため、Region+next_i 化は不要と判断。
// State layout (Map): { // State layout (Map): {
// alias_paths: Map, alias_names: Map, alias_keys: Array, // alias_paths: Map, alias_names: Map, alias_keys: Array,
// modules_map: Map, modules_keys: Array // modules_map: Map, modules_keys: Array

View File

@ -107,6 +107,10 @@ static box Stage1Cli {
// CLI dispatcher (stub) // CLI dispatcher (stub)
method stage1_main(args) { method stage1_main(args) {
if env.get("STAGE1_CLI_DEBUG") == "1" {
local argc = 0; if args != null { argc = args.size() }
print("[stage1-cli/debug] stage1_main ENTRY: argc=" + ("" + argc) + " env_emits={prog=" + ("" + env.get("STAGE1_EMIT_PROGRAM_JSON")) + ",mir=" + ("" + env.get("STAGE1_EMIT_MIR_JSON")) + "} backend=" + ("" + env.get("STAGE1_BACKEND")))
}
{ {
local use_cli = env.get("NYASH_USE_STAGE1_CLI") local use_cli = env.get("NYASH_USE_STAGE1_CLI")
if use_cli == null || ("" + use_cli) != "1" { if use_cli == null || ("" + use_cli) != "1" {

View File

@ -9,6 +9,18 @@
// - ctx.using_paths: Array<String>(将来のヒント/本箱では純粋合成のみ) // - ctx.using_paths: Array<String>(将来のヒント/本箱では純粋合成のみ)
// - ctx.cwd: String相対の基準 // - ctx.cwd: String相対の基準
// Quick README (Phase 25.1 using設計)
// - 役割: using 解決の唯一の窓口SSOT。parser/StageB/Stage1 は IO や名前探索をここに委譲する。
// - I/F:
// - resolve(name, ctx) -> path|null : modules/cwd/using_paths から純粋に合成して返すIOなし
// - resolve_modules(modules_json, using_entries_json, ctx) -> modules_json_resolved|null :
// modules_jsonnyash.toml 相当)と using_entries_jsonUsingCollector 出力)を突き合わせ、
// 「解決済み modules_json」同名重複などの調停結果を返す。IOなし。
// - resolve_prefix(using_entries_json, modules_json, ctx) -> prefix_string :
// using_entries_json を modules_json を使ってパス解決し、ファイル読まずに
// 「prefix 文字列空でよいを組み立てるための情報」を返すスコープMVPは空文字返しでOK
// - ポリシー: IO/ファイル読み込みは絶対にしない。より重い処理は entry/pipeline 側の責務。
static box UsingResolveSSOTBox { static box UsingResolveSSOTBox {
/// Resolve a module name to a file path string (or null when not found). /// Resolve a module name to a file path string (or null when not found).
/// name: requested module name (e.g., "hako.mir.builder.internal.lower_return_int") /// name: requested module name (e.g., "hako.mir.builder.internal.lower_return_int")
@ -55,4 +67,16 @@ static box UsingResolveSSOTBox {
if base.endsWith("/") { return base + leaf } if base.endsWith("/") { return base + leaf }
return base + "/" + leaf return base + "/" + leaf
} }
// Merge modules_json + using_entries_json into resolved modules table (pure, no IO).
resolve_modules(modules_json, using_entries_json, ctx) {
// MVP: just echo back modules_json (keep behavior unchanged) to establish interface.
return modules_json
}
// Build prefix string from using_entries_json with modules_json (pure, no IO).
resolve_prefix(using_entries_json, modules_json, ctx) {
// MVP: no file read, so prefix is empty. Interface is reserved for Stage1 entry.
return ""
}
} }

View File

@ -136,6 +136,7 @@ path = "lang/src/shared/common/string_helpers.hako"
"lang.compiler.entry.compiler" = "lang/src/compiler/entry/compiler.hako" "lang.compiler.entry.compiler" = "lang/src/compiler/entry/compiler.hako"
"lang.compiler.entry.compiler_stageb" = "lang/src/compiler/entry/compiler_stageb.hako" "lang.compiler.entry.compiler_stageb" = "lang/src/compiler/entry/compiler_stageb.hako"
"lang.compiler.entry.bundle_resolver" = "lang/src/compiler/entry/bundle_resolver.hako" "lang.compiler.entry.bundle_resolver" = "lang/src/compiler/entry/bundle_resolver.hako"
"lang.compiler.entry.using_resolver_box" = "lang/src/compiler/entry/using_resolver_box.hako"
"lang.compiler.entry.using_resolver" = "lang/src/compiler/entry/using_resolver_box.hako" "lang.compiler.entry.using_resolver" = "lang/src/compiler/entry/using_resolver_box.hako"
"lang.compiler.emit.mir_emitter_box" = "lang/src/compiler/emit/mir_emitter_box.hako" "lang.compiler.emit.mir_emitter_box" = "lang/src/compiler/emit/mir_emitter_box.hako"
"lang.compiler.emit.common.json_emit_box" = "lang/src/compiler/emit/common/json_emit_box.hako" "lang.compiler.emit.common.json_emit_box" = "lang/src/compiler/emit/common/json_emit_box.hako"

View File

@ -2,16 +2,22 @@
// Kept tiny and isolated. Linkage names match include/nyrt.h. // Kept tiny and isolated. Linkage names match include/nyrt.h.
#[no_mangle] #[no_mangle]
pub extern "C" fn nyrt_init() -> i32 { 0 } pub extern "C" fn nyrt_init() -> i32 {
0
}
#[no_mangle] #[no_mangle]
pub extern "C" fn nyrt_teardown() {} pub extern "C" fn nyrt_teardown() {}
#[no_mangle] #[no_mangle]
pub extern "C" fn nyrt_load_mir_json(_json_text: *const ::std::os::raw::c_char) -> u64 { 1 } pub extern "C" fn nyrt_load_mir_json(_json_text: *const ::std::os::raw::c_char) -> u64 {
1
}
#[no_mangle] #[no_mangle]
pub extern "C" fn nyrt_exec_main(_module_handle: u64) -> i32 { 0 } pub extern "C" fn nyrt_exec_main(_module_handle: u64) -> i32 {
0
}
#[no_mangle] #[no_mangle]
pub extern "C" fn nyrt_hostcall( pub extern "C" fn nyrt_hostcall(
@ -20,7 +26,9 @@ pub extern "C" fn nyrt_hostcall(
_payload_json: *const ::std::os::raw::c_char, _payload_json: *const ::std::os::raw::c_char,
_out_buf: *mut ::std::os::raw::c_char, _out_buf: *mut ::std::os::raw::c_char,
_out_buf_len: u32, _out_buf_len: u32,
) -> i32 { 0 } ) -> i32 {
0
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@ -10,8 +10,8 @@ use std::collections::HashMap;
use std::fmt; use std::fmt;
mod span; mod span;
pub use span::Span; pub use span::Span;
mod utils;
mod nodes; mod nodes;
mod utils;
pub use nodes::*; pub use nodes::*;
// Span は src/ast/span.rs へ分離re-export で後方互換維持) // Span は src/ast/span.rs へ分離re-export で後方互換維持)
@ -573,7 +573,6 @@ pub enum ASTNode {
/// meフィールドアクセス: me.field /// meフィールドアクセス: me.field
MeField { field: String, span: Span }, MeField { field: String, span: Span },
/// ローカル変数宣言: local x, y, z /// ローカル変数宣言: local x, y, z
Local { Local {
variables: Vec<String>, variables: Vec<String>,
@ -584,10 +583,7 @@ pub enum ASTNode {
/// ScopeBoxオプション: 診断/マクロ可視性のためのno-opスコープ。 /// ScopeBoxオプション: 診断/マクロ可視性のためのno-opスコープ。
/// 正規化で注入され、MIRビルダがブロックとして処理意味不変 /// 正規化で注入され、MIRビルダがブロックとして処理意味不変
ScopeBox { ScopeBox { body: Vec<ASTNode>, span: Span },
body: Vec<ASTNode>,
span: Span,
},
/// Outbox変数宣言: outbox x, y, z (static関数内専用) /// Outbox変数宣言: outbox x, y, z (static関数内専用)
Outbox { Outbox {

View File

@ -22,7 +22,15 @@ impl TryFrom<ASTNode> for AssignStmt {
type Error = ASTNode; type Error = ASTNode;
fn try_from(node: ASTNode) -> Result<Self, Self::Error> { fn try_from(node: ASTNode) -> Result<Self, Self::Error> {
match node { match node {
ASTNode::Assignment { target, value, span } => Ok(AssignStmt { target, value, span }), ASTNode::Assignment {
target,
value,
span,
} => Ok(AssignStmt {
target,
value,
span,
}),
other => Err(other), other => Err(other),
} }
} }
@ -30,7 +38,11 @@ impl TryFrom<ASTNode> for AssignStmt {
impl From<AssignStmt> for ASTNode { impl From<AssignStmt> for ASTNode {
fn from(s: AssignStmt) -> Self { fn from(s: AssignStmt) -> Self {
ASTNode::Assignment { target: s.target, value: s.value, span: s.span } ASTNode::Assignment {
target: s.target,
value: s.value,
span: s.span,
}
} }
} }
@ -52,7 +64,10 @@ impl TryFrom<ASTNode> for ReturnStmt {
impl From<ReturnStmt> for ASTNode { impl From<ReturnStmt> for ASTNode {
fn from(s: ReturnStmt) -> Self { fn from(s: ReturnStmt) -> Self {
ASTNode::Return { value: s.value, span: s.span } ASTNode::Return {
value: s.value,
span: s.span,
}
} }
} }
@ -68,7 +83,17 @@ impl TryFrom<ASTNode> for IfStmt {
type Error = ASTNode; type Error = ASTNode;
fn try_from(node: ASTNode) -> Result<Self, Self::Error> { fn try_from(node: ASTNode) -> Result<Self, Self::Error> {
match node { match node {
ASTNode::If { condition, then_body, else_body, span } => Ok(IfStmt { condition, then_body, else_body, span }), ASTNode::If {
condition,
then_body,
else_body,
span,
} => Ok(IfStmt {
condition,
then_body,
else_body,
span,
}),
other => Err(other), other => Err(other),
} }
} }
@ -76,7 +101,12 @@ impl TryFrom<ASTNode> for IfStmt {
impl From<IfStmt> for ASTNode { impl From<IfStmt> for ASTNode {
fn from(s: IfStmt) -> Self { fn from(s: IfStmt) -> Self {
ASTNode::If { condition: s.condition, then_body: s.then_body, else_body: s.else_body, span: s.span } ASTNode::If {
condition: s.condition,
then_body: s.then_body,
else_body: s.else_body,
span: s.span,
}
} }
} }
@ -96,8 +126,17 @@ impl TryFrom<ASTNode> for BinaryExpr {
type Error = ASTNode; type Error = ASTNode;
fn try_from(node: ASTNode) -> Result<Self, Self::Error> { fn try_from(node: ASTNode) -> Result<Self, Self::Error> {
match node { match node {
ASTNode::BinaryOp { operator, left, right, span } => ASTNode::BinaryOp {
Ok(BinaryExpr { operator, left, right, span }), operator,
left,
right,
span,
} => Ok(BinaryExpr {
operator,
left,
right,
span,
}),
other => Err(other), other => Err(other),
} }
} }
@ -105,7 +144,12 @@ impl TryFrom<ASTNode> for BinaryExpr {
impl From<BinaryExpr> for ASTNode { impl From<BinaryExpr> for ASTNode {
fn from(e: BinaryExpr) -> Self { fn from(e: BinaryExpr) -> Self {
ASTNode::BinaryOp { operator: e.operator, left: e.left, right: e.right, span: e.span } ASTNode::BinaryOp {
operator: e.operator,
left: e.left,
right: e.right,
span: e.span,
}
} }
} }
@ -120,7 +164,15 @@ impl TryFrom<ASTNode> for CallExpr {
type Error = ASTNode; type Error = ASTNode;
fn try_from(node: ASTNode) -> Result<Self, Self::Error> { fn try_from(node: ASTNode) -> Result<Self, Self::Error> {
match node { match node {
ASTNode::FunctionCall { name, arguments, span } => Ok(CallExpr { name, arguments, span }), ASTNode::FunctionCall {
name,
arguments,
span,
} => Ok(CallExpr {
name,
arguments,
span,
}),
other => Err(other), other => Err(other),
} }
} }
@ -128,7 +180,11 @@ impl TryFrom<ASTNode> for CallExpr {
impl From<CallExpr> for ASTNode { impl From<CallExpr> for ASTNode {
fn from(c: CallExpr) -> Self { fn from(c: CallExpr) -> Self {
ASTNode::FunctionCall { name: c.name, arguments: c.arguments, span: c.span } ASTNode::FunctionCall {
name: c.name,
arguments: c.arguments,
span: c.span,
}
} }
} }
@ -144,8 +200,17 @@ impl TryFrom<ASTNode> for MethodCallExpr {
type Error = ASTNode; type Error = ASTNode;
fn try_from(node: ASTNode) -> Result<Self, Self::Error> { fn try_from(node: ASTNode) -> Result<Self, Self::Error> {
match node { match node {
ASTNode::MethodCall { object, method, arguments, span } => ASTNode::MethodCall {
Ok(MethodCallExpr { object, method, arguments, span }), object,
method,
arguments,
span,
} => Ok(MethodCallExpr {
object,
method,
arguments,
span,
}),
other => Err(other), other => Err(other),
} }
} }
@ -153,7 +218,12 @@ impl TryFrom<ASTNode> for MethodCallExpr {
impl From<MethodCallExpr> for ASTNode { impl From<MethodCallExpr> for ASTNode {
fn from(m: MethodCallExpr) -> Self { fn from(m: MethodCallExpr) -> Self {
ASTNode::MethodCall { object: m.object, method: m.method, arguments: m.arguments, span: m.span } ASTNode::MethodCall {
object: m.object,
method: m.method,
arguments: m.arguments,
span: m.span,
}
} }
} }
@ -168,8 +238,15 @@ impl TryFrom<ASTNode> for FieldAccessExpr {
type Error = ASTNode; type Error = ASTNode;
fn try_from(node: ASTNode) -> Result<Self, Self::Error> { fn try_from(node: ASTNode) -> Result<Self, Self::Error> {
match node { match node {
ASTNode::FieldAccess { object, field, span } => ASTNode::FieldAccess {
Ok(FieldAccessExpr { object, field, span }), object,
field,
span,
} => Ok(FieldAccessExpr {
object,
field,
span,
}),
other => Err(other), other => Err(other),
} }
} }
@ -177,6 +254,10 @@ impl TryFrom<ASTNode> for FieldAccessExpr {
impl From<FieldAccessExpr> for ASTNode { impl From<FieldAccessExpr> for ASTNode {
fn from(f: FieldAccessExpr) -> Self { fn from(f: FieldAccessExpr) -> Self {
ASTNode::FieldAccess { object: f.object, field: f.field, span: f.span } ASTNode::FieldAccess {
object: f.object,
field: f.field,
span: f.span,
}
} }
} }

View File

@ -54,10 +54,20 @@ pub fn eq_vm(a: &VMValue, b: &VMValue) -> bool {
(Float(x), Integer(y)) => *x == (*y as f64), (Float(x), Integer(y)) => *x == (*y as f64),
(BoxRef(ax), BoxRef(by)) => Arc::ptr_eq(ax, by), (BoxRef(ax), BoxRef(by)) => Arc::ptr_eq(ax, by),
// Treat BoxRef(VoidBox/MissingBox) as equal to Void (null) for backward compatibility // Treat BoxRef(VoidBox/MissingBox) as equal to Void (null) for backward compatibility
(BoxRef(bx), Void) => bx.as_any().downcast_ref::<VoidBox>().is_some() (BoxRef(bx), Void) => {
|| bx.as_any().downcast_ref::<crate::boxes::missing_box::MissingBox>().is_some(), bx.as_any().downcast_ref::<VoidBox>().is_some()
(Void, BoxRef(bx)) => bx.as_any().downcast_ref::<VoidBox>().is_some() || bx
|| bx.as_any().downcast_ref::<crate::boxes::missing_box::MissingBox>().is_some(), .as_any()
.downcast_ref::<crate::boxes::missing_box::MissingBox>()
.is_some()
}
(Void, BoxRef(bx)) => {
bx.as_any().downcast_ref::<VoidBox>().is_some()
|| bx
.as_any()
.downcast_ref::<crate::boxes::missing_box::MissingBox>()
.is_some()
}
_ => false, _ => false,
} }
} }

View File

@ -20,9 +20,7 @@ impl MirInterpreter {
if self.call_depth > MAX_CALL_DEPTH { if self.call_depth > MAX_CALL_DEPTH {
eprintln!( eprintln!(
"[vm-call-depth] exceeded {} in fn={} (depth={})", "[vm-call-depth] exceeded {} in fn={} (depth={})",
MAX_CALL_DEPTH, MAX_CALL_DEPTH, func.signature.name, self.call_depth
func.signature.name,
self.call_depth
); );
self.call_depth = self.call_depth.saturating_sub(1); self.call_depth = self.call_depth.saturating_sub(1);
return Err(VMError::InvalidInstruction(format!( return Err(VMError::InvalidInstruction(format!(
@ -31,7 +29,9 @@ impl MirInterpreter {
))); )));
} }
// Phase 1: delegate cross-class reroute / narrow fallbacks to method_router // Phase 1: delegate cross-class reroute / narrow fallbacks to method_router
if let Some(r) = super::method_router::pre_exec_reroute(self, func, arg_vals) { return r; } if let Some(r) = super::method_router::pre_exec_reroute(self, func, arg_vals) {
return r;
}
let saved_regs = mem::take(&mut self.regs); let saved_regs = mem::take(&mut self.regs);
let saved_fn = self.cur_fn.clone(); let saved_fn = self.cur_fn.clone();
self.cur_fn = Some(func.signature.name.clone()); self.cur_fn = Some(func.signature.name.clone());
@ -59,7 +59,11 @@ impl MirInterpreter {
let max_steps: u64 = std::env::var("HAKO_VM_MAX_STEPS") let max_steps: u64 = std::env::var("HAKO_VM_MAX_STEPS")
.ok() .ok()
.and_then(|v| v.parse::<u64>().ok()) .and_then(|v| v.parse::<u64>().ok())
.or_else(|| std::env::var("NYASH_VM_MAX_STEPS").ok().and_then(|v| v.parse::<u64>().ok())) .or_else(|| {
std::env::var("NYASH_VM_MAX_STEPS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
})
.unwrap_or(1_000_000); .unwrap_or(1_000_000);
let mut steps: u64 = 0; let mut steps: u64 = 0;
@ -133,9 +137,7 @@ impl MirInterpreter {
if trace_phi { if trace_phi {
eprintln!( eprintln!(
"[vm-trace-phi] enter bb={:?} last_pred={:?} preds={:?}", "[vm-trace-phi] enter bb={:?} last_pred={:?} preds={:?}",
block.id, block.id, last_pred, block.predecessors
last_pred,
block.predecessors
); );
} }
for inst in block.phi_instructions() { for inst in block.phi_instructions() {
@ -154,7 +156,11 @@ impl MirInterpreter {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
// Dev safety valve: tolerate undefined phi inputs by substituting Void // Dev safety valve: tolerate undefined phi inputs by substituting Void
if std::env::var("NYASH_VM_PHI_TOLERATE_UNDEFINED").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_PHI_TOLERATE_UNDEFINED")
.ok()
.as_deref()
== Some("1")
{
if Self::trace_enabled() { if Self::trace_enabled() {
eprintln!("[vm-trace] phi tolerate undefined input {:?} -> Void (err={:?})", val, e); eprintln!("[vm-trace] phi tolerate undefined input {:?} -> Void (err={:?})", val, e);
} }
@ -179,17 +185,41 @@ impl MirInterpreter {
// Strict PHI (default ON). Can be disabled with HAKO_VM_PHI_STRICT=0 (or NYASH_VM_PHI_STRICT=0). // Strict PHI (default ON). Can be disabled with HAKO_VM_PHI_STRICT=0 (or NYASH_VM_PHI_STRICT=0).
let strict = { let strict = {
let on = |s: &str| { let on = |s: &str| {
matches!(s, "1"|"true"|"on"|"yes"|"y"|"t"|"enabled"|"enable") matches!(
s,
"1" | "true" | "on" | "yes" | "y" | "t" | "enabled" | "enable"
)
}; };
let off = |s: &str| { let off = |s: &str| {
matches!(s, "0"|"false"|"off"|"no"|"n"|"f"|"disabled"|"disable") matches!(
s,
"0" | "false"
| "off"
| "no"
| "n"
| "f"
| "disabled"
| "disable"
)
}; };
if let Ok(v) = std::env::var("HAKO_VM_PHI_STRICT") { if let Ok(v) = std::env::var("HAKO_VM_PHI_STRICT") {
let v = v.to_ascii_lowercase(); let v = v.to_ascii_lowercase();
if off(&v) { false } else if on(&v) { true } else { true } if off(&v) {
false
} else if on(&v) {
true
} else {
true
}
} else if let Ok(v) = std::env::var("NYASH_VM_PHI_STRICT") { } else if let Ok(v) = std::env::var("NYASH_VM_PHI_STRICT") {
let v = v.to_ascii_lowercase(); let v = v.to_ascii_lowercase();
if off(&v) { false } else if on(&v) { true } else { true } if off(&v) {
false
} else if on(&v) {
true
} else {
true
}
} else { } else {
true true
} }
@ -213,7 +243,11 @@ impl MirInterpreter {
let v = match self.reg_load(*val0) { let v = match self.reg_load(*val0) {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
if std::env::var("NYASH_VM_PHI_TOLERATE_UNDEFINED").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_PHI_TOLERATE_UNDEFINED")
.ok()
.as_deref()
== Some("1")
{
if Self::trace_enabled() { if Self::trace_enabled() {
eprintln!("[vm-trace] phi missing pred match {:?}; fallback first input {:?} -> Void (err={:?})", pred, val0, e); eprintln!("[vm-trace] phi missing pred match {:?}; fallback first input {:?} -> Void (err={:?})", pred, val0, e);
} }
@ -243,7 +277,11 @@ impl MirInterpreter {
let v = match self.reg_load(*val) { let v = match self.reg_load(*val) {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
if std::env::var("NYASH_VM_PHI_TOLERATE_UNDEFINED").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_PHI_TOLERATE_UNDEFINED")
.ok()
.as_deref()
== Some("1")
{
if Self::trace_enabled() { if Self::trace_enabled() {
eprintln!("[vm-trace] phi tolerate undefined default input {:?} -> Void (err={:?})", val, e); eprintln!("[vm-trace] phi tolerate undefined default input {:?} -> Void (err={:?})", val, e);
} }
@ -255,10 +293,7 @@ impl MirInterpreter {
}; };
self.regs.insert(dst_id, v); self.regs.insert(dst_id, v);
if Self::trace_enabled() { if Self::trace_enabled() {
eprintln!( eprintln!("[vm-trace] phi dst={:?} take default val={:?}", dst_id, val);
"[vm-trace] phi dst={:?} take default val={:?}",
dst_id, val
);
} }
} }
} }

View File

@ -32,7 +32,8 @@ impl MirInterpreter {
if let Some(op_fn) = self.functions.get("AddOperator.apply/2").cloned() { if let Some(op_fn) = self.functions.get("AddOperator.apply/2").cloned() {
if !in_guard { if !in_guard {
if crate::config::env::operator_box_add_adopt() { if crate::config::env::operator_box_add_adopt() {
let out = self.exec_function_inner(&op_fn, Some(&[a.clone(), b.clone()]))?; let out =
self.exec_function_inner(&op_fn, Some(&[a.clone(), b.clone()]))?;
self.regs.insert(dst, out); self.regs.insert(dst, out);
return Ok(()); return Ok(());
} else { } else {

View File

@ -20,7 +20,9 @@ impl MirInterpreter {
let s_opt: Option<String> = match v0.clone() { let s_opt: Option<String> = match v0.clone() {
VMValue::String(s) => Some(s), VMValue::String(s) => Some(s),
VMValue::BoxRef(b) => { VMValue::BoxRef(b) => {
if let Some(sb) = b.as_any().downcast_ref::<crate::boxes::basic::StringBox>() { if let Some(sb) =
b.as_any().downcast_ref::<crate::boxes::basic::StringBox>()
{
Some(sb.value.clone()) Some(sb.value.clone())
} else { } else {
None None
@ -29,10 +31,13 @@ impl MirInterpreter {
_ => None, _ => None,
}; };
if let Some(s) = s_opt { if let Some(s) = s_opt {
let boxed: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::boxes::basic::StringBox::new(s)); let boxed: Box<dyn crate::box_trait::NyashBox> =
Box::new(crate::boxes::basic::StringBox::new(s));
let created_vm = VMValue::from_nyash_box(boxed); let created_vm = VMValue::from_nyash_box(boxed);
self.regs.insert(dst, created_vm); self.regs.insert(dst, created_vm);
if Self::box_trace_enabled() { self.box_trace_emit_new(box_type, args.len()); } if Self::box_trace_enabled() {
self.box_trace_emit_new(box_type, args.len());
}
return Ok(()); return Ok(());
} }
} }
@ -88,7 +93,7 @@ impl MirInterpreter {
Err(e) => { Err(e) => {
return Err(self.err_with_context( return Err(self.err_with_context(
&format!("PluginInvoke {}.{}", p.box_type, method), &format!("PluginInvoke {}.{}", p.box_type, method),
&format!("{:?}", e) &format!("{:?}", e),
)) ))
} }
} }
@ -115,7 +120,10 @@ impl MirInterpreter {
return Ok(()); return Ok(());
} }
if let VMValue::BoxRef(b) = self.reg_load(box_val)? { if let VMValue::BoxRef(b) = self.reg_load(box_val)? {
if b.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() { if b.as_any()
.downcast_ref::<crate::box_trait::VoidBox>()
.is_some()
{
self.write_string(dst, "null".to_string()); self.write_string(dst, "null".to_string());
return Ok(()); return Ok(());
} }
@ -125,7 +133,8 @@ impl MirInterpreter {
if Self::box_trace_enabled() { if Self::box_trace_enabled() {
let cls = match self.reg_load(box_val).unwrap_or(VMValue::Void) { let cls = match self.reg_load(box_val).unwrap_or(VMValue::Void) {
VMValue::BoxRef(b) => { VMValue::BoxRef(b) => {
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>()
{
inst.class_name.clone() inst.class_name.clone()
} else { } else {
b.type_name().to_string() b.type_name().to_string()
@ -167,7 +176,10 @@ impl MirInterpreter {
} }
} }
VMValue::BoxRef(ref b) => { VMValue::BoxRef(ref b) => {
if b.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() { if b.as_any()
.downcast_ref::<crate::box_trait::VoidBox>()
.is_some()
{
if let Some(val) = super::boxes_void_guards::handle_void_method(method) { if let Some(val) = super::boxes_void_guards::handle_void_method(method) {
self.write_result(dst, val); self.write_result(dst, val);
return Ok(()); return Ok(());
@ -252,9 +264,12 @@ impl MirInterpreter {
// Determine receiver class name when possible // Determine receiver class name when possible
let recv_cls: Option<String> = match self.reg_load(box_val).ok() { let recv_cls: Option<String> = match self.reg_load(box_val).ok() {
Some(VMValue::BoxRef(b)) => { Some(VMValue::BoxRef(b)) => {
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>()
{
Some(inst.class_name.clone()) Some(inst.class_name.clone())
} else { None } } else {
None
}
} }
_ => None, _ => None,
}; };
@ -262,13 +277,19 @@ impl MirInterpreter {
let prefix = format!("{}.", want); let prefix = format!("{}.", want);
cands.retain(|k| k.starts_with(&prefix)); cands.retain(|k| k.starts_with(&prefix));
} }
if cands.len() == 1 { self.functions.get(&cands[0]).cloned() } else { None } if cands.len() == 1 {
self.functions.get(&cands[0]).cloned()
} else {
None
}
} { } {
// Build argv: pass receiver as first arg (me) // Build argv: pass receiver as first arg (me)
let recv_vm = self.reg_load(box_val)?; let recv_vm = self.reg_load(box_val)?;
let mut argv: Vec<VMValue> = Vec::with_capacity(1 + args.len()); let mut argv: Vec<VMValue> = Vec::with_capacity(1 + args.len());
argv.push(recv_vm); argv.push(recv_vm);
for a in args { argv.push(self.reg_load(*a)?); } for a in args {
argv.push(self.reg_load(*a)?);
}
let ret = self.exec_function_inner(&func, Some(&argv))?; let ret = self.exec_function_inner(&func, Some(&argv))?;
self.write_result(dst, ret); self.write_result(dst, ret);
return Ok(()); return Ok(());
@ -312,5 +333,4 @@ impl MirInterpreter {
) -> Result<bool, VMError> { ) -> Result<bool, VMError> {
super::boxes_array::try_handle_array_box(self, dst, box_val, method, args) super::boxes_array::try_handle_array_box(self, dst, box_val, method, args)
} }
} }

View File

@ -31,7 +31,9 @@ pub(super) fn try_handle_array_box(
return Ok(true); return Ok(true);
} }
"pop" => { "pop" => {
if !args.is_empty() { return Err(this.err_invalid("pop expects 0 args")); } if !args.is_empty() {
return Err(this.err_invalid("pop expects 0 args"));
}
let ret = ab.pop(); let ret = ab.pop();
this.write_result(dst, VMValue::from_nyash_box(ret)); this.write_result(dst, VMValue::from_nyash_box(ret));
return Ok(true); return Ok(true);

View File

@ -14,26 +14,39 @@ pub(super) fn try_handle_instance_box(
other => other.to_nyash_box(), other => other.to_nyash_box(),
}; };
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") && method == "toString" { if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") && method == "toString" {
eprintln!("[vm-trace] instance-check recv_box_any.type={} args_len={}", recv_box_any.type_name(), args.len()); eprintln!(
"[vm-trace] instance-check recv_box_any.type={} args_len={}",
recv_box_any.type_name(),
args.len()
);
} }
if let Some(inst) = recv_box_any.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = recv_box_any
.as_any()
.downcast_ref::<crate::instance_v2::InstanceBox>()
{
// Development guard: ensure JsonScanner core fields have sensible defaults // Development guard: ensure JsonScanner core fields have sensible defaults
if inst.class_name == "JsonScanner" { if inst.class_name == "JsonScanner" {
// populate missing fields to avoid Void in comparisons inside is_eof/advance // populate missing fields to avoid Void in comparisons inside is_eof/advance
if inst.get_field_ng("position").is_none() { if inst.get_field_ng("position").is_none() {
let _ = inst.set_field_ng("position".to_string(), crate::value::NyashValue::Integer(0)); let _ =
inst.set_field_ng("position".to_string(), crate::value::NyashValue::Integer(0));
} }
if inst.get_field_ng("length").is_none() { if inst.get_field_ng("length").is_none() {
let _ = inst.set_field_ng("length".to_string(), crate::value::NyashValue::Integer(0)); let _ =
inst.set_field_ng("length".to_string(), crate::value::NyashValue::Integer(0));
} }
if inst.get_field_ng("line").is_none() { if inst.get_field_ng("line").is_none() {
let _ = inst.set_field_ng("line".to_string(), crate::value::NyashValue::Integer(1)); let _ = inst.set_field_ng("line".to_string(), crate::value::NyashValue::Integer(1));
} }
if inst.get_field_ng("column").is_none() { if inst.get_field_ng("column").is_none() {
let _ = inst.set_field_ng("column".to_string(), crate::value::NyashValue::Integer(1)); let _ =
inst.set_field_ng("column".to_string(), crate::value::NyashValue::Integer(1));
} }
if inst.get_field_ng("text").is_none() { if inst.get_field_ng("text").is_none() {
let _ = inst.set_field_ng("text".to_string(), crate::value::NyashValue::String(String::new())); let _ = inst.set_field_ng(
"text".to_string(),
crate::value::NyashValue::String(String::new()),
);
} }
} }
// JsonNodeInstance narrow bridges removed: rely on builder rewrite and instance dispatch // JsonNodeInstance narrow bridges removed: rely on builder rewrite and instance dispatch
@ -47,11 +60,26 @@ pub(super) fn try_handle_instance_box(
); );
} }
// Resolve lowered method function: "Class.method/arity" // Resolve lowered method function: "Class.method/arity"
let primary = format!("{}.{}{}", inst.class_name, method, format!("/{}", args.len())); let primary = format!(
"{}.{}{}",
inst.class_name,
method,
format!("/{}", args.len())
);
// Alternate naming: "ClassInstance.method/arity" // Alternate naming: "ClassInstance.method/arity"
let alt = format!("{}Instance.{}{}", inst.class_name, method, format!("/{}", args.len())); let alt = format!(
"{}Instance.{}{}",
inst.class_name,
method,
format!("/{}", args.len())
);
// Static method variant that takes 'me' explicitly as first arg: "Class.method/(arity+1)" // Static method variant that takes 'me' explicitly as first arg: "Class.method/(arity+1)"
let static_variant = format!("{}.{}{}", inst.class_name, method, format!("/{}", args.len() + 1)); let static_variant = format!(
"{}.{}{}",
inst.class_name,
method,
format!("/{}", args.len() + 1)
);
// Special-case: toString() → stringify/0 if present // Special-case: toString() → stringify/0 if present
// Prefer base class (strip trailing "Instance") stringify when available. // Prefer base class (strip trailing "Instance") stringify when available.
let (stringify_base, stringify_inst) = if method == "toString" && args.is_empty() { let (stringify_base, stringify_inst) = if method == "toString" && args.is_empty() {
@ -64,27 +92,43 @@ pub(super) fn try_handle_instance_box(
Some(format!("{}.stringify/0", base_name)), Some(format!("{}.stringify/0", base_name)),
Some(format!("{}.stringify/0", inst.class_name)), Some(format!("{}.stringify/0", inst.class_name)),
) )
} else { (None, None) }; } else {
(None, None)
};
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!( eprintln!(
"[vm-trace] instance-dispatch class={} method={} arity={} candidates=[{}, {}, {}]", "[vm-trace] instance-dispatch class={} method={} arity={} candidates=[{}, {}, {}]",
inst.class_name, method, args.len(), primary, alt, static_variant inst.class_name,
method,
args.len(),
primary,
alt,
static_variant
); );
} }
// Prefer stringify for toString() if present (semantic alias). Try instance first, then base. // Prefer stringify for toString() if present (semantic alias). Try instance first, then base.
let func_opt = if let Some(ref sname) = stringify_inst { let func_opt = if let Some(ref sname) = stringify_inst {
this.functions.get(sname).cloned() this.functions.get(sname).cloned()
} else { None } } else {
.or_else(|| stringify_base.as_ref().and_then(|n| this.functions.get(n).cloned())) None
}
.or_else(|| {
stringify_base
.as_ref()
.and_then(|n| this.functions.get(n).cloned())
})
.or_else(|| this.functions.get(&primary).cloned()) .or_else(|| this.functions.get(&primary).cloned())
.or_else(|| this.functions.get(&alt).cloned()) .or_else(|| this.functions.get(&alt).cloned())
.or_else(|| this.functions.get(&static_variant).cloned()); .or_else(|| this.functions.get(&static_variant).cloned());
if let Some(func) = func_opt { if let Some(func) = func_opt {
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] instance-dispatch hit -> {}", func.signature.name); eprintln!(
"[vm-trace] instance-dispatch hit -> {}",
func.signature.name
);
} }
// Build argv: me + args (works for both instance and static(me, ...)) // Build argv: me + args (works for both instance and static(me, ...))
let mut argv: Vec<VMValue> = Vec::with_capacity(1 + args.len()); let mut argv: Vec<VMValue> = Vec::with_capacity(1 + args.len());
@ -95,7 +139,9 @@ pub(super) fn try_handle_instance_box(
} }
} }
argv.push(recv_vm.clone()); argv.push(recv_vm.clone());
for a in args { argv.push(this.reg_load(*a)?); } for a in args {
argv.push(this.reg_load(*a)?);
}
let ret = this.exec_function_inner(&func, Some(&argv))?; let ret = this.exec_function_inner(&func, Some(&argv))?;
this.write_result(dst, ret); this.write_result(dst, ret);
return Ok(true); return Ok(true);
@ -120,19 +166,28 @@ pub(super) fn try_handle_instance_box(
if filtered.len() == 1 { if filtered.len() == 1 {
let fname = &filtered[0]; let fname = &filtered[0];
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] instance-dispatch fallback (scoped) -> {}", fname); eprintln!(
"[vm-trace] instance-dispatch fallback (scoped) -> {}",
fname
);
} }
if let Some(func) = this.functions.get(fname).cloned() { if let Some(func) = this.functions.get(fname).cloned() {
let mut argv: Vec<VMValue> = Vec::with_capacity(1 + args.len()); let mut argv: Vec<VMValue> = Vec::with_capacity(1 + args.len());
if method == "birth" && crate::config::env::using_is_dev() { if method == "birth" && crate::config::env::using_is_dev() {
if matches!(recv_vm, VMValue::Void) { if matches!(recv_vm, VMValue::Void) {
return Err(this.err_invalid("Dev assert: birth(me==Void) is forbidden")); return Err(
this.err_invalid("Dev assert: birth(me==Void) is forbidden")
);
} }
} }
argv.push(recv_vm.clone()); argv.push(recv_vm.clone());
for a in args { argv.push(this.reg_load(*a)?); } for a in args {
argv.push(this.reg_load(*a)?);
}
let ret = this.exec_function_inner(&func, Some(&argv))?; let ret = this.exec_function_inner(&func, Some(&argv))?;
if let Some(d) = dst { this.regs.insert(d, ret); } if let Some(d) = dst {
this.regs.insert(d, ret);
}
return Ok(true); return Ok(true);
} }
} else if filtered.len() > 1 { } else if filtered.len() > 1 {
@ -143,7 +198,11 @@ pub(super) fn try_handle_instance_box(
} else { } else {
// No same-class candidate: do not dispatch cross-class // No same-class candidate: do not dispatch cross-class
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] instance-dispatch no same-class candidate for tail .{}{}", method, format!("/{}", args.len())); eprintln!(
"[vm-trace] instance-dispatch no same-class candidate for tail .{}{}",
method,
format!("/{}", args.len())
);
} }
} }
} }

View File

@ -29,7 +29,10 @@ pub(super) fn try_handle_map_box(
let k_vm = this.reg_load(args[0])?; let k_vm = this.reg_load(args[0])?;
// Field access expects a String key; otherwise return a stable tag. // Field access expects a String key; otherwise return a stable tag.
if !matches!(k_vm, VMValue::String(_)) { if !matches!(k_vm, VMValue::String(_)) {
this.write_result(dst, VMValue::String("[map/bad-key] field name must be string".to_string())); this.write_result(
dst,
VMValue::String("[map/bad-key] field name must be string".to_string()),
);
return Ok(true); return Ok(true);
} }
let k = this.load_as_box(args[0])?; let k = this.load_as_box(args[0])?;
@ -41,7 +44,10 @@ pub(super) fn try_handle_map_box(
this.validate_args_exact("MapBox.setField", args, 2)?; this.validate_args_exact("MapBox.setField", args, 2)?;
let k_vm = this.reg_load(args[0])?; let k_vm = this.reg_load(args[0])?;
if !matches!(k_vm, VMValue::String(_)) { if !matches!(k_vm, VMValue::String(_)) {
this.write_result(dst, VMValue::String("[map/bad-key] field name must be string".to_string())); this.write_result(
dst,
VMValue::String("[map/bad-key] field name must be string".to_string()),
);
return Ok(true); return Ok(true);
} }
let k = this.load_as_box(args[0])?; let k = this.load_as_box(args[0])?;
@ -54,7 +60,10 @@ pub(super) fn try_handle_map_box(
this.validate_args_exact("MapBox.set", args, 2)?; this.validate_args_exact("MapBox.set", args, 2)?;
let k_vm = this.reg_load(args[0])?; let k_vm = this.reg_load(args[0])?;
if !matches!(k_vm, VMValue::String(_)) { if !matches!(k_vm, VMValue::String(_)) {
this.write_result(dst, VMValue::String("[map/bad-key] key must be string".to_string())); this.write_result(
dst,
VMValue::String("[map/bad-key] key must be string".to_string()),
);
return Ok(true); return Ok(true);
} }
let k = this.load_as_box(args[0])?; let k = this.load_as_box(args[0])?;
@ -67,7 +76,10 @@ pub(super) fn try_handle_map_box(
this.validate_args_exact("MapBox.get", args, 1)?; this.validate_args_exact("MapBox.get", args, 1)?;
let k_vm = this.reg_load(args[0])?; let k_vm = this.reg_load(args[0])?;
if !matches!(k_vm, VMValue::String(_)) { if !matches!(k_vm, VMValue::String(_)) {
this.write_result(dst, VMValue::String("[map/bad-key] key must be string".to_string())); this.write_result(
dst,
VMValue::String("[map/bad-key] key must be string".to_string()),
);
return Ok(true); return Ok(true);
} }
let k = this.load_as_box(args[0])?; let k = this.load_as_box(args[0])?;
@ -86,7 +98,10 @@ pub(super) fn try_handle_map_box(
this.validate_args_exact("MapBox.delete", args, 1)?; this.validate_args_exact("MapBox.delete", args, 1)?;
let k_vm = this.reg_load(args[0])?; let k_vm = this.reg_load(args[0])?;
if !matches!(k_vm, VMValue::String(_)) { if !matches!(k_vm, VMValue::String(_)) {
this.write_result(dst, VMValue::String("[map/bad-key] key must be string".to_string())); this.write_result(
dst,
VMValue::String("[map/bad-key] key must be string".to_string()),
);
return Ok(true); return Ok(true);
} }
let k = this.load_as_box(args[0])?; let k = this.load_as_box(args[0])?;

View File

@ -9,8 +9,8 @@ pub(super) fn try_handle_object_fields(
) -> Result<bool, VMError> { ) -> Result<bool, VMError> {
// Local helpers to bridge NyashValue <-> VMValue for InstanceBox fields // Local helpers to bridge NyashValue <-> VMValue for InstanceBox fields
fn vm_to_nv(v: &VMValue) -> crate::value::NyashValue { fn vm_to_nv(v: &VMValue) -> crate::value::NyashValue {
use crate::value::NyashValue as NV;
use super::VMValue as VV; use super::VMValue as VV;
use crate::value::NyashValue as NV;
match v { match v {
VV::Integer(i) => NV::Integer(*i), VV::Integer(i) => NV::Integer(*i),
VV::Float(f) => NV::Float(*f), VV::Float(f) => NV::Float(*f),
@ -22,8 +22,8 @@ pub(super) fn try_handle_object_fields(
} }
} }
fn nv_to_vm(v: &crate::value::NyashValue) -> VMValue { fn nv_to_vm(v: &crate::value::NyashValue) -> VMValue {
use crate::value::NyashValue as NV;
use super::VMValue as VV; use super::VMValue as VV;
use crate::value::NyashValue as NV;
match v { match v {
NV::Integer(i) => VV::Integer(*i), NV::Integer(i) => VV::Integer(*i),
NV::Float(f) => VV::Float(*f), NV::Float(f) => VV::Float(*f),
@ -48,7 +48,8 @@ pub(super) fn try_handle_object_fields(
// Create a temporary value to hold the singleton // Create a temporary value to hold the singleton
let temp_id = ValueId(999999999); // Temporary ID for singleton let temp_id = ValueId(999999999); // Temporary ID for singleton
this.regs.insert(temp_id, VMValue::from_nyash_box(Box::new(instance_clone))); this.regs
.insert(temp_id, VMValue::from_nyash_box(Box::new(instance_clone)));
temp_id temp_id
} else { } else {
box_val box_val
@ -59,18 +60,27 @@ pub(super) fn try_handle_object_fields(
// MapBox special-case: bridge to MapBox.get, with string-only key // MapBox special-case: bridge to MapBox.get, with string-only key
if let Ok(VMValue::BoxRef(bref)) = this.reg_load(actual_box_val) { if let Ok(VMValue::BoxRef(bref)) = this.reg_load(actual_box_val) {
if bref.as_any().downcast_ref::<crate::boxes::map_box::MapBox>().is_some() { if bref
.as_any()
.downcast_ref::<crate::boxes::map_box::MapBox>()
.is_some()
{
let key_vm = this.reg_load(args[0])?; let key_vm = this.reg_load(args[0])?;
if let VMValue::String(_) = key_vm { if let VMValue::String(_) = key_vm {
let k = key_vm.to_nyash_box(); let k = key_vm.to_nyash_box();
let map = bref.share_box(); let map = bref.share_box();
if let Some(mb) = map.as_any().downcast_ref::<crate::boxes::map_box::MapBox>() { if let Some(mb) =
map.as_any().downcast_ref::<crate::boxes::map_box::MapBox>()
{
let ret = mb.get(k); let ret = mb.get(k);
this.write_result(dst, VMValue::from_nyash_box(ret)); this.write_result(dst, VMValue::from_nyash_box(ret));
return Ok(true); return Ok(true);
} }
} else { } else {
this.write_string(dst, "[map/bad-key] field name must be string".to_string()); this.write_string(
dst,
"[map/bad-key] field name must be string".to_string(),
);
return Ok(true); return Ok(true);
} }
} }
@ -94,15 +104,22 @@ pub(super) fn try_handle_object_fields(
}; };
// Prefer InstanceBox internal storage (structural correctness) // Prefer InstanceBox internal storage (structural correctness)
if let VMValue::BoxRef(bref) = this.reg_load(actual_box_val)? { if let VMValue::BoxRef(bref) = this.reg_load(actual_box_val)? {
if let Some(inst) = bref.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = bref
.as_any()
.downcast_ref::<crate::instance_v2::InstanceBox>()
{
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] getField instance class={}", inst.class_name); eprintln!("[vm-trace] getField instance class={}", inst.class_name);
} }
// Special-case bridge: JsonParser.length -> tokens.length() // Special-case bridge: JsonParser.length -> tokens.length()
if inst.class_name == "JsonParser" && fname == "length" { if inst.class_name == "JsonParser" && fname == "length" {
if let Some(tokens_shared) = inst.get_field("tokens") { if let Some(tokens_shared) = inst.get_field("tokens") {
let tokens_box: Box<dyn crate::box_trait::NyashBox> = tokens_shared.share_box(); let tokens_box: Box<dyn crate::box_trait::NyashBox> =
if let Some(arr) = tokens_box.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() { tokens_shared.share_box();
if let Some(arr) = tokens_box
.as_any()
.downcast_ref::<crate::boxes::array::ArrayBox>()
{
let len_box = arr.length(); let len_box = arr.length();
this.write_result(dst, VMValue::from_nyash_box(len_box)); this.write_result(dst, VMValue::from_nyash_box(len_box));
return Ok(true); return Ok(true);
@ -112,7 +129,9 @@ pub(super) fn try_handle_object_fields(
// First: prefer fields_ng (NyashValue) when present // First: prefer fields_ng (NyashValue) when present
if let Some(nv) = inst.get_field_ng(&fname) { if let Some(nv) = inst.get_field_ng(&fname) {
// Dev trace: JsonToken field get // Dev trace: JsonToken field get
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") && inst.class_name == "JsonToken" { if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1")
&& inst.class_name == "JsonToken"
{
eprintln!("[vm-trace] JsonToken.getField name={} nv={:?}", fname, nv); eprintln!("[vm-trace] JsonToken.getField name={} nv={:?}", fname, nv);
} }
// Treat complex Box-like values as "missing" for internal storage so that // Treat complex Box-like values as "missing" for internal storage so that
@ -129,13 +148,18 @@ pub(super) fn try_handle_object_fields(
); );
if !is_missing { if !is_missing {
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] getField internal {}.{} -> {:?}", inst.class_name, fname, nv); eprintln!(
"[vm-trace] getField internal {}.{} -> {:?}",
inst.class_name, fname, nv
);
} }
// Special-case: NV::Box should surface as VMValue::BoxRef // Special-case: NV::Box should surface as VMValue::BoxRef
if let crate::value::NyashValue::Box(ref arc_m) = nv { if let crate::value::NyashValue::Box(ref arc_m) = nv {
if let Ok(guard) = arc_m.lock() { if let Ok(guard) = arc_m.lock() {
let cloned: Box<dyn crate::box_trait::NyashBox> = guard.clone_box(); let cloned: Box<dyn crate::box_trait::NyashBox> =
let arc: std::sync::Arc<dyn crate::box_trait::NyashBox> = std::sync::Arc::from(cloned); guard.clone_box();
let arc: std::sync::Arc<dyn crate::box_trait::NyashBox> =
std::sync::Arc::from(cloned);
this.write_result(dst, VMValue::BoxRef(arc)); this.write_result(dst, VMValue::BoxRef(arc));
} else { } else {
this.write_void(dst); this.write_void(dst);
@ -170,8 +194,12 @@ pub(super) fn try_handle_object_fields(
_ => None, _ => None,
}; };
if let Some(v) = def { if let Some(v) = def {
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1")
eprintln!("[vm-trace] getField default JsonScanner.{} -> {:?}", fname, v); {
eprintln!(
"[vm-trace] getField default JsonScanner.{} -> {:?}",
fname, v
);
} }
this.write_result(dst, v); this.write_result(dst, v);
return Ok(true); return Ok(true);
@ -229,10 +257,14 @@ pub(super) fn try_handle_object_fields(
// JsonScanner fields inside JsonScanner.{is_eof,current,advance}, provide // JsonScanner fields inside JsonScanner.{is_eof,current,advance}, provide
// pragmatic defaults to avoid Void comparisons during bring-up. // pragmatic defaults to avoid Void comparisons during bring-up.
if let VMValue::Void = v { if let VMValue::Void = v {
let guard_on = std::env::var("NYASH_VM_SCANNER_DEFAULTS").ok().as_deref() == Some("1"); let guard_on =
std::env::var("NYASH_VM_SCANNER_DEFAULTS").ok().as_deref() == Some("1");
let fn_ctx = this.cur_fn.as_deref().unwrap_or(""); let fn_ctx = this.cur_fn.as_deref().unwrap_or("");
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] getField guard_check ctx={} guard_on={} name={}", fn_ctx, guard_on, fname); eprintln!(
"[vm-trace] getField guard_check ctx={} guard_on={} name={}",
fn_ctx, guard_on, fname
);
} }
if guard_on { if guard_on {
let fn_ctx = this.cur_fn.as_deref().unwrap_or(""); let fn_ctx = this.cur_fn.as_deref().unwrap_or("");
@ -243,7 +275,10 @@ pub(super) fn try_handle_object_fields(
if is_scanner_ctx { if is_scanner_ctx {
// Try class-aware default first // Try class-aware default first
if let Ok(VMValue::BoxRef(bref2)) = this.reg_load(actual_box_val) { if let Ok(VMValue::BoxRef(bref2)) = this.reg_load(actual_box_val) {
if let Some(inst2) = bref2.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst2) = bref2
.as_any()
.downcast_ref::<crate::instance_v2::InstanceBox>()
{
if inst2.class_name == "JsonScanner" { if inst2.class_name == "JsonScanner" {
let fallback = match fname.as_str() { let fallback = match fname.as_str() {
"position" | "length" => Some(VMValue::Integer(0)), "position" | "length" => Some(VMValue::Integer(0)),
@ -252,8 +287,13 @@ pub(super) fn try_handle_object_fields(
_ => None, _ => None,
}; };
if let Some(val) = fallback { if let Some(val) = fallback {
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_TRACE").ok().as_deref()
eprintln!("[vm-trace] getField final_default {} -> {:?}", fname, val); == Some("1")
{
eprintln!(
"[vm-trace] getField final_default {} -> {:?}",
fname, val
);
} }
v = val; v = val;
} }
@ -280,7 +320,11 @@ pub(super) fn try_handle_object_fields(
} }
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
if let VMValue::BoxRef(b) = &v { if let VMValue::BoxRef(b) = &v {
eprintln!("[vm-trace] getField legacy {} -> BoxRef({})", fname, b.type_name()); eprintln!(
"[vm-trace] getField legacy {} -> BoxRef({})",
fname,
b.type_name()
);
} else { } else {
eprintln!("[vm-trace] getField legacy {} -> {:?}", fname, v); eprintln!("[vm-trace] getField legacy {} -> {:?}", fname, v);
} }
@ -299,9 +343,13 @@ pub(super) fn try_handle_object_fields(
// class name unknown here; use receiver type name if possible // class name unknown here; use receiver type name if possible
let cls = match this.reg_load(actual_box_val).unwrap_or(VMValue::Void) { let cls = match this.reg_load(actual_box_val).unwrap_or(VMValue::Void) {
VMValue::BoxRef(b) => { VMValue::BoxRef(b) => {
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) =
b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>()
{
inst.class_name.clone() inst.class_name.clone()
} else { b.type_name().to_string() } } else {
b.type_name().to_string()
}
} }
_ => "<unknown>".to_string(), _ => "<unknown>".to_string(),
}; };
@ -322,7 +370,8 @@ pub(super) fn try_handle_object_fields(
// Create a temporary value to hold the singleton // Create a temporary value to hold the singleton
let temp_id = ValueId(999999998); // Temporary ID for singleton (different from getField) let temp_id = ValueId(999999998); // Temporary ID for singleton (different from getField)
this.regs.insert(temp_id, VMValue::from_nyash_box(Box::new(instance_clone))); this.regs
.insert(temp_id, VMValue::from_nyash_box(Box::new(instance_clone)));
temp_id temp_id
} else { } else {
box_val box_val
@ -333,19 +382,28 @@ pub(super) fn try_handle_object_fields(
// MapBox special-case: bridge to MapBox.set, with string-only key // MapBox special-case: bridge to MapBox.set, with string-only key
if let Ok(VMValue::BoxRef(bref)) = this.reg_load(actual_box_val) { if let Ok(VMValue::BoxRef(bref)) = this.reg_load(actual_box_val) {
if bref.as_any().downcast_ref::<crate::boxes::map_box::MapBox>().is_some() { if bref
.as_any()
.downcast_ref::<crate::boxes::map_box::MapBox>()
.is_some()
{
let key_vm = this.reg_load(args[0])?; let key_vm = this.reg_load(args[0])?;
if let VMValue::String(_) = key_vm { if let VMValue::String(_) = key_vm {
let k = key_vm.to_nyash_box(); let k = key_vm.to_nyash_box();
let v = this.reg_load(args[1])?.to_nyash_box(); let v = this.reg_load(args[1])?.to_nyash_box();
let map = bref.share_box(); let map = bref.share_box();
if let Some(mb) = map.as_any().downcast_ref::<crate::boxes::map_box::MapBox>() { if let Some(mb) =
map.as_any().downcast_ref::<crate::boxes::map_box::MapBox>()
{
let _ = mb.set(k, v); let _ = mb.set(k, v);
this.write_void(dst); this.write_void(dst);
return Ok(true); return Ok(true);
} }
} else { } else {
this.write_string(dst, "[map/bad-key] field name must be string".to_string()); this.write_string(
dst,
"[map/bad-key] field name must be string".to_string(),
);
return Ok(true); return Ok(true);
} }
} }
@ -358,9 +416,15 @@ pub(super) fn try_handle_object_fields(
// Dev trace: JsonToken field set // Dev trace: JsonToken field set
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
if let VMValue::BoxRef(bref) = this.reg_load(actual_box_val)? { if let VMValue::BoxRef(bref) = this.reg_load(actual_box_val)? {
if let Some(inst) = bref.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = bref
.as_any()
.downcast_ref::<crate::instance_v2::InstanceBox>()
{
if inst.class_name == "JsonToken" { if inst.class_name == "JsonToken" {
eprintln!("[vm-trace] JsonToken.setField name={} vmval={:?}", fname, valv); eprintln!(
"[vm-trace] JsonToken.setField name={} vmval={:?}",
fname, valv
);
} }
} }
} }
@ -377,9 +441,13 @@ pub(super) fn try_handle_object_fields(
}; };
let cls = match this.reg_load(actual_box_val).unwrap_or(VMValue::Void) { let cls = match this.reg_load(actual_box_val).unwrap_or(VMValue::Void) {
VMValue::BoxRef(b) => { VMValue::BoxRef(b) => {
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) =
b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>()
{
inst.class_name.clone() inst.class_name.clone()
} else { b.type_name().to_string() } } else {
b.type_name().to_string()
}
} }
_ => "<unknown>".to_string(), _ => "<unknown>".to_string(),
}; };
@ -387,28 +455,52 @@ pub(super) fn try_handle_object_fields(
} }
// Prefer InstanceBox internal storage // Prefer InstanceBox internal storage
if let VMValue::BoxRef(bref) = this.reg_load(actual_box_val)? { if let VMValue::BoxRef(bref) = this.reg_load(actual_box_val)? {
if let Some(inst) = bref.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = bref
.as_any()
.downcast_ref::<crate::instance_v2::InstanceBox>()
{
// Primitives → 内部保存 // Primitives → 内部保存
if matches!(valv, VMValue::Integer(_) | VMValue::Float(_) | VMValue::Bool(_) | VMValue::String(_) | VMValue::Void) { if matches!(
valv,
VMValue::Integer(_)
| VMValue::Float(_)
| VMValue::Bool(_)
| VMValue::String(_)
| VMValue::Void
) {
let _ = inst.set_field_ng(fname.clone(), vm_to_nv(&valv)); let _ = inst.set_field_ng(fname.clone(), vm_to_nv(&valv));
return Ok(true); return Ok(true);
} }
// BoxRef のうち、Integer/Float/Bool/String はプリミティブに剥がして内部保存 // BoxRef のうち、Integer/Float/Bool/String はプリミティブに剥がして内部保存
if let VMValue::BoxRef(bx) = &valv { if let VMValue::BoxRef(bx) = &valv {
if let Some(ib) = bx.as_any().downcast_ref::<crate::box_trait::IntegerBox>() { if let Some(ib) = bx.as_any().downcast_ref::<crate::box_trait::IntegerBox>()
let _ = inst.set_field_ng(fname.clone(), crate::value::NyashValue::Integer(ib.value)); {
let _ = inst.set_field_ng(
fname.clone(),
crate::value::NyashValue::Integer(ib.value),
);
return Ok(true); return Ok(true);
} }
if let Some(fb) = bx.as_any().downcast_ref::<crate::boxes::FloatBox>() { if let Some(fb) = bx.as_any().downcast_ref::<crate::boxes::FloatBox>() {
let _ = inst.set_field_ng(fname.clone(), crate::value::NyashValue::Float(fb.value)); let _ = inst.set_field_ng(
fname.clone(),
crate::value::NyashValue::Float(fb.value),
);
return Ok(true); return Ok(true);
} }
if let Some(bb) = bx.as_any().downcast_ref::<crate::box_trait::BoolBox>() { if let Some(bb) = bx.as_any().downcast_ref::<crate::box_trait::BoolBox>() {
let _ = inst.set_field_ng(fname.clone(), crate::value::NyashValue::Bool(bb.value)); let _ = inst.set_field_ng(
fname.clone(),
crate::value::NyashValue::Bool(bb.value),
);
return Ok(true); return Ok(true);
} }
if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() { if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>()
let _ = inst.set_field_ng(fname.clone(), crate::value::NyashValue::String(sb.value.clone())); {
let _ = inst.set_field_ng(
fname.clone(),
crate::value::NyashValue::String(sb.value.clone()),
);
return Ok(true); return Ok(true);
} }
// For complex Box values (InstanceBox/MapBox/ArrayBox...), store into // For complex Box values (InstanceBox/MapBox/ArrayBox...), store into
@ -419,10 +511,7 @@ pub(super) fn try_handle_object_fields(
} }
} }
let key = this.object_key_for(actual_box_val); let key = this.object_key_for(actual_box_val);
this.obj_fields this.obj_fields.entry(key).or_default().insert(fname, valv);
.entry(key)
.or_default()
.insert(fname, valv);
Ok(true) Ok(true)
} }
_ => Ok(false), _ => Ok(false),

View File

@ -52,7 +52,7 @@ pub(super) fn invoke_plugin_box(
} }
Err(e) => Err(this.err_with_context( Err(e) => Err(this.err_with_context(
&format!("BoxCall {}.{}", p.box_type, method), &format!("BoxCall {}.{}", p.box_type, method),
&format!("{:?}", e) &format!("{:?}", e),
)), )),
} }
} else if recv_box.type_name() == "StringBox" { } else if recv_box.type_name() == "StringBox" {
@ -99,10 +99,7 @@ pub(super) fn invoke_plugin_box(
if let Some(arg_id) = args.get(0) { if let Some(arg_id) = args.get(0) {
let ch = this.reg_load(*arg_id)?.to_string(); let ch = this.reg_load(*arg_id)?.to_string();
let c = ch.chars().next().unwrap_or('\0'); let c = ch.chars().next().unwrap_or('\0');
let is_alpha = let is_alpha = ('A'..='Z').contains(&c) || ('a'..='z').contains(&c) || c == '_';
('A'..='Z').contains(&c) ||
('a'..='z').contains(&c) ||
c == '_';
this.write_result(dst, VMValue::Bool(is_alpha)); this.write_result(dst, VMValue::Bool(is_alpha));
Ok(()) Ok(())
} else { } else {
@ -115,14 +112,21 @@ pub(super) fn invoke_plugin_box(
// Special-case: minimal runtime fallback for common InstanceBox methods when // Special-case: minimal runtime fallback for common InstanceBox methods when
// lowered functions are not available (dev robustness). Keeps behavior stable // lowered functions are not available (dev robustness). Keeps behavior stable
// without changing semantics in the normal path. // without changing semantics in the normal path.
if let Some(inst) = recv_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = recv_box
.as_any()
.downcast_ref::<crate::instance_v2::InstanceBox>()
{
// Generic current() fallback: if object has integer 'position' and string 'text', // Generic current() fallback: if object has integer 'position' and string 'text',
// return one character at that position (or empty at EOF). This covers JsonScanner // return one character at that position (or empty at EOF). This covers JsonScanner
// and compatible scanners without relying on class name. // and compatible scanners without relying on class name.
if method == "current" && args.is_empty() { if method == "current" && args.is_empty() {
if let Some(crate::value::NyashValue::Integer(pos)) = inst.get_field_ng("position") { if let Some(crate::value::NyashValue::Integer(pos)) = inst.get_field_ng("position")
if let Some(crate::value::NyashValue::String(text)) = inst.get_field_ng("text") { {
let s = if pos < 0 || (pos as usize) >= text.len() { String::new() } else { if let Some(crate::value::NyashValue::String(text)) = inst.get_field_ng("text")
{
let s = if pos < 0 || (pos as usize) >= text.len() {
String::new()
} else {
let bytes = text.as_bytes(); let bytes = text.as_bytes();
let i = pos as usize; let i = pos as usize;
let j = (i + 1).min(bytes.len()); let j = (i + 1).min(bytes.len());
@ -137,7 +141,11 @@ pub(super) fn invoke_plugin_box(
// Generic toString fallback for any non-plugin box // Generic toString fallback for any non-plugin box
if method == "toString" { if method == "toString" {
// Map VoidBox.toString → "null" for JSON-friendly semantics // Map VoidBox.toString → "null" for JSON-friendly semantics
let s = if recv_box.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() { let s = if recv_box
.as_any()
.downcast_ref::<crate::box_trait::VoidBox>()
.is_some()
{
"null".to_string() "null".to_string()
} else { } else {
recv_box.to_string_box().value recv_box.to_string_box().value
@ -148,7 +156,10 @@ pub(super) fn invoke_plugin_box(
// Minimal runtime fallback for common InstanceBox.is_eof when lowered function is not present. // Minimal runtime fallback for common InstanceBox.is_eof when lowered function is not present.
// This avoids cross-class leaks and hard errors in union-like flows. // This avoids cross-class leaks and hard errors in union-like flows.
if method == "is_eof" && args.is_empty() { if method == "is_eof" && args.is_empty() {
if let Some(inst) = recv_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = recv_box
.as_any()
.downcast_ref::<crate::instance_v2::InstanceBox>()
{
if inst.class_name == "JsonToken" { if inst.class_name == "JsonToken" {
let is = match inst.get_field_ng("type") { let is = match inst.get_field_ng("type") {
Some(crate::value::NyashValue::String(ref s)) => s == "EOF", Some(crate::value::NyashValue::String(ref s)) => s == "EOF",
@ -158,8 +169,14 @@ pub(super) fn invoke_plugin_box(
return Ok(()); return Ok(());
} }
if inst.class_name == "JsonScanner" { if inst.class_name == "JsonScanner" {
let pos = match inst.get_field_ng("position") { Some(crate::value::NyashValue::Integer(i)) => i, _ => 0 }; let pos = match inst.get_field_ng("position") {
let len = match inst.get_field_ng("length") { Some(crate::value::NyashValue::Integer(i)) => i, _ => 0 }; Some(crate::value::NyashValue::Integer(i)) => i,
_ => 0,
};
let len = match inst.get_field_ng("length") {
Some(crate::value::NyashValue::Integer(i)) => i,
_ => 0,
};
let is = pos >= len; let is = pos >= len;
this.write_result(dst, VMValue::Bool(is)); this.write_result(dst, VMValue::Bool(is));
return Ok(()); return Ok(());
@ -167,7 +184,10 @@ pub(super) fn invoke_plugin_box(
} }
} }
// Dynamic fallback for user-defined InstanceBox: dispatch to lowered function "Class.method/Arity" // Dynamic fallback for user-defined InstanceBox: dispatch to lowered function "Class.method/Arity"
if let Some(inst) = recv_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = recv_box
.as_any()
.downcast_ref::<crate::instance_v2::InstanceBox>()
{
let class_name = inst.class_name.clone(); let class_name = inst.class_name.clone();
let arity = args.len(); // function name arity excludes 'me' let arity = args.len(); // function name arity excludes 'me'
let fname = format!("{}.{}{}", class_name, method, format!("/{}", arity)); let fname = format!("{}.{}{}", class_name, method, format!("/{}", arity));

View File

@ -17,7 +17,11 @@ pub(super) fn try_handle_string_box(
{ {
if let VMValue::String(ref raw) = recv { if let VMValue::String(ref raw) = recv {
let use_cp = std::env::var("NYASH_STR_CP").ok().as_deref() == Some("1"); let use_cp = std::env::var("NYASH_STR_CP").ok().as_deref() == Some("1");
let n = if use_cp { raw.chars().count() as i64 } else { raw.len() as i64 }; let n = if use_cp {
raw.chars().count() as i64
} else {
raw.len() as i64
};
this.write_result(dst, VMValue::Integer(n)); this.write_result(dst, VMValue::Integer(n));
return Ok(true); return Ok(true);
} }
@ -27,7 +31,10 @@ pub(super) fn try_handle_string_box(
let sb_norm_opt: Option<crate::box_trait::StringBox> = match recv.clone() { let sb_norm_opt: Option<crate::box_trait::StringBox> = match recv.clone() {
VMValue::String(s) => Some(crate::box_trait::StringBox::new(s)), VMValue::String(s) => Some(crate::box_trait::StringBox::new(s)),
VMValue::BoxRef(b) => { VMValue::BoxRef(b) => {
if b.as_any().downcast_ref::<crate::box_trait::StringBox>().is_some() { if b.as_any()
.downcast_ref::<crate::box_trait::StringBox>()
.is_some()
{
Some(b.to_string_box()) Some(b.to_string_box())
} else { } else {
None None
@ -35,14 +42,20 @@ pub(super) fn try_handle_string_box(
} }
_ => None, _ => None,
}; };
let Some(sb_norm) = sb_norm_opt else { return Ok(false) }; let Some(sb_norm) = sb_norm_opt else {
return Ok(false);
};
// Only handle known string methods here (receiver is confirmed string) // Only handle known string methods here (receiver is confirmed string)
match method { match method {
"length" | "size" => { "length" | "size" => {
// Bench/profile fast path: return VMValue::Integer directly (avoid boxing overhead) // Bench/profile fast path: return VMValue::Integer directly (avoid boxing overhead)
if std::env::var("NYASH_VM_FAST").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_FAST").ok().as_deref() == Some("1") {
let use_cp = std::env::var("NYASH_STR_CP").ok().as_deref() == Some("1"); let use_cp = std::env::var("NYASH_STR_CP").ok().as_deref() == Some("1");
let n = if use_cp { sb_norm.value.chars().count() as i64 } else { sb_norm.value.len() as i64 }; let n = if use_cp {
sb_norm.value.chars().count() as i64
} else {
sb_norm.value.len() as i64
};
this.write_result(dst, VMValue::Integer(n)); this.write_result(dst, VMValue::Integer(n));
return Ok(true); return Ok(true);
} }
@ -65,7 +78,10 @@ pub(super) fn try_handle_string_box(
} else { } else {
sb_norm.value.clone() sb_norm.value.clone()
}; };
this.write_result(dst, VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(out)))); this.write_result(
dst,
VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(out))),
);
return Ok(true); return Ok(true);
} }
"trim" => { "trim" => {
@ -88,9 +104,9 @@ pub(super) fn try_handle_string_box(
(n, from.max(0) as usize) (n, from.max(0) as usize)
} }
_ => { _ => {
return Err(this.err_invalid( return Err(
"indexOf expects 1 or 2 args (search [, fromIndex])" this.err_invalid("indexOf expects 1 or 2 args (search [, fromIndex])")
)); );
} }
}; };
@ -101,7 +117,8 @@ pub(super) fn try_handle_string_box(
&sb_norm.value[from_index..] &sb_norm.value[from_index..]
}; };
let idx = search_str.find(&needle) let idx = search_str
.find(&needle)
.map(|i| (from_index + i) as i64) .map(|i| (from_index + i) as i64)
.unwrap_or(-1); .unwrap_or(-1);
@ -141,7 +158,10 @@ pub(super) fn try_handle_string_box(
} }
} }
quoted.push('"'); quoted.push('"');
this.write_result(dst, VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(quoted)))); this.write_result(
dst,
VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(quoted))),
);
return Ok(true); return Ok(true);
} }
"substring" => { "substring" => {
@ -160,9 +180,7 @@ pub(super) fn try_handle_string_box(
(s, e) (s, e)
} }
_ => { _ => {
return Err(this.err_invalid( return Err(this.err_invalid("substring expects 1 or 2 args (start [, end])"));
"substring expects 1 or 2 args (start [, end])"
));
} }
}; };
let len = sb_norm.value.chars().count() as i64; let len = sb_norm.value.chars().count() as i64;
@ -170,14 +188,20 @@ pub(super) fn try_handle_string_box(
let end = e_idx.max(start as i64).min(len) as usize; let end = e_idx.max(start as i64).min(len) as usize;
let chars: Vec<char> = sb_norm.value.chars().collect(); let chars: Vec<char> = sb_norm.value.chars().collect();
let sub: String = chars[start..end].iter().collect(); let sub: String = chars[start..end].iter().collect();
this.write_result(dst, VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(sub)))); this.write_result(
dst,
VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(sub))),
);
return Ok(true); return Ok(true);
} }
"concat" => { "concat" => {
this.validate_args_exact("concat", args, 1)?; this.validate_args_exact("concat", args, 1)?;
let rhs = this.reg_load(args[0])?; let rhs = this.reg_load(args[0])?;
let new_s = format!("{}{}", sb_norm.value, rhs.to_string()); let new_s = format!("{}{}", sb_norm.value, rhs.to_string());
this.write_result(dst, VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(new_s)))); this.write_result(
dst,
VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(new_s))),
);
return Ok(true); return Ok(true);
} }
"is_digit_char" => { "is_digit_char" => {

View File

@ -22,9 +22,15 @@ impl MirInterpreter {
match &v { match &v {
VMValue::Void => println!("null"), VMValue::Void => println!("null"),
VMValue::BoxRef(bx) => { VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() { if bx
.as_any()
.downcast_ref::<crate::box_trait::VoidBox>()
.is_some()
{
println!("null"); println!("null");
} else if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() { } else if let Some(sb) =
bx.as_any().downcast_ref::<crate::box_trait::StringBox>()
{
println!("{}", sb.value); println!("{}", sb.value);
} else { } else {
println!("{}", v.to_string()); println!("{}", v.to_string());
@ -56,8 +62,12 @@ impl MirInterpreter {
}; };
panic!("{}", msg); panic!("{}", msg);
} }
"hostbridge.extern_invoke" => Err(self.err_invalid("hostbridge.extern_invoke should be routed via extern_provider_dispatch")), "hostbridge.extern_invoke" => Err(self.err_invalid(
_ => Err(self.err_with_context("extern function", &format!("Unknown: {}", extern_name))), "hostbridge.extern_invoke should be routed via extern_provider_dispatch",
)),
_ => {
Err(self.err_with_context("extern function", &format!("Unknown: {}", extern_name)))
}
} }
} }
} }

View File

@ -12,7 +12,9 @@ impl MirInterpreter {
// Module-local/global function: execute by function table if present (use original name) // Module-local/global function: execute by function table if present (use original name)
if let Some(func) = self.functions.get(func_name).cloned() { if let Some(func) = self.functions.get(func_name).cloned() {
let mut argv: Vec<VMValue> = Vec::with_capacity(args.len()); let mut argv: Vec<VMValue> = Vec::with_capacity(args.len());
for a in args { argv.push(self.reg_load(*a)?); } for a in args {
argv.push(self.reg_load(*a)?);
}
return self.exec_function_inner(&func, Some(&argv)); return self.exec_function_inner(&func, Some(&argv));
} }
@ -60,13 +62,19 @@ impl MirInterpreter {
let s = self.reg_load(*a0)?.to_string(); let s = self.reg_load(*a0)?.to_string();
let opts = crate::host_providers::llvm_codegen::Opts { let opts = crate::host_providers::llvm_codegen::Opts {
out: None, out: None,
nyrt: std::env::var("NYASH_EMIT_EXE_NYRT").ok().map(std::path::PathBuf::from), nyrt: std::env::var("NYASH_EMIT_EXE_NYRT")
opt_level: std::env::var("HAKO_LLVM_OPT_LEVEL").ok().or(Some("0".to_string())), .ok()
.map(std::path::PathBuf::from),
opt_level: std::env::var("HAKO_LLVM_OPT_LEVEL")
.ok()
.or(Some("0".to_string())),
timeout_ms: None, timeout_ms: None,
}; };
match crate::host_providers::llvm_codegen::mir_json_to_object(&s, opts) { match crate::host_providers::llvm_codegen::mir_json_to_object(&s, opts) {
Ok(p) => Ok(VMValue::String(p.to_string_lossy().into_owned())), 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())), Err(e) => {
Err(self.err_with_context("env.codegen.emit_object", &e.to_string()))
}
} }
} else { } else {
Err(self.err_invalid("env.codegen.emit_object expects 1 arg")) Err(self.err_invalid("env.codegen.emit_object expects 1 arg"))
@ -74,32 +82,51 @@ impl MirInterpreter {
} }
"env.codegen.link_object" | "env.codegen.link_object/3" => { "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?] // 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") || 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") { || 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")); 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())); } 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 v = self.reg_load(args[2])?;
let (obj_path, exe_out) = match v { let (obj_path, exe_out) = match v {
VMValue::BoxRef(b) => { VMValue::BoxRef(b) => {
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() { 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 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 elem0 = ab.get(idx0).to_string_box().value;
let mut exe: Option<String> = None; let mut exe: Option<String> = None;
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1)); 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; let e1 = ab.get(idx1).to_string_box().value;
if !e1.is_empty() { exe = Some(e1); } if !e1.is_empty() {
exe = Some(e1);
}
(elem0, exe) (elem0, exe)
} else { (b.to_string_box().value, None) } } else {
(b.to_string_box().value, None)
}
} }
_ => (v.to_string(), None), _ => (v.to_string(), None),
}; };
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok(); let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
let obj = std::path::PathBuf::from(obj_path); 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")); let exe = exe_out
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) { .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())), Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
Err(e) => Err(self.err_with_context("env.codegen.link_object", &e.to_string())) Err(e) => Err(self.err_with_context("env.codegen.link_object", &e.to_string())),
} }
} }
"nyash.builtin.error" => { "nyash.builtin.error" => {

View File

@ -22,18 +22,40 @@ impl MirInterpreter {
if let Some(block) = func.blocks.get(&bb) { if let Some(block) = func.blocks.get(&bb) {
let mut last_recv: Option<ValueId> = None; let mut last_recv: Option<ValueId> = None;
for inst in &block.instructions { for inst in &block.instructions {
if let crate::mir::MirInstruction::NewBox { dst, box_type, .. } = inst { if let crate::mir::MirInstruction::NewBox {
if box_type == box_name { last_recv = Some(*dst); } dst,
box_type,
..
} = inst
{
if box_type == box_name {
last_recv = Some(*dst);
}
} }
} }
if let Some(rid) = last_recv { if let Some(rid) = last_recv {
if let Ok(v) = self.reg_load(rid) { v } else { return Err(e); } if let Ok(v) = self.reg_load(rid) {
v
} else {
return Err(e);
}
} else { } else {
// Dev fallback (guarded): use args[0] as surrogate receiver if explicitly allowed // 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") let tolerate = std::env::var("NYASH_VM_RECV_ARG_FALLBACK")
|| std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1"); .ok()
if tolerate { if let Some(a0) = args.get(0) { self.reg_load(*a0)? } else { return Err(e); } } .as_deref()
else { return Err(e); } == 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 { } else {
return Err(e); return Err(e);
@ -43,10 +65,18 @@ impl MirInterpreter {
} }
} else { } else {
// Dev fallback (guarded): use args[0] as surrogate receiver if explicitly allowed // 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") 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"); || 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); } } if tolerate {
else { return Err(e); } if let Some(a0) = args.get(0) {
self.reg_load(*a0)?
} else {
return Err(e);
}
} else {
return Err(e);
}
} }
} }
}; };
@ -57,7 +87,9 @@ impl MirInterpreter {
// ArrayBox bridge // ArrayBox bridge
if let Some(arr) = bx.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() { if let Some(arr) = bx.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
match method { match method {
"birth" => { return Ok(VMValue::Void); } "birth" => {
return Ok(VMValue::Void);
}
"push" => { "push" => {
if let Some(a0) = args.get(0) { if let Some(a0) = args.get(0) {
let v = self.load_as_box(*a0)?; let v = self.load_as_box(*a0)?;
@ -162,14 +194,20 @@ impl MirInterpreter {
"substring" => { "substring" => {
let start = if let Some(a0) = args.get(0) { let start = if let Some(a0) = args.get(0) {
self.reg_load(*a0)?.as_integer().unwrap_or(0) self.reg_load(*a0)?.as_integer().unwrap_or(0)
} else { 0 }; } else {
0
};
let end = if let Some(a1) = args.get(1) { let end = if let Some(a1) = args.get(1) {
self.reg_load(*a1)?.as_integer().unwrap_or(s.len() as i64) self.reg_load(*a1)?.as_integer().unwrap_or(s.len() as i64)
} else { s.len() as i64 }; } else {
s.len() as i64
};
let len = s.len() as i64; let len = s.len() as i64;
let i0 = start.max(0).min(len) as usize; let i0 = start.max(0).min(len) as usize;
let i1 = end.max(0).min(len) as usize; let i1 = end.max(0).min(len) as usize;
if i0 > i1 { return Ok(VMValue::String(String::new())); } if i0 > i1 {
return Ok(VMValue::String(String::new()));
}
// Note: operating on bytes; Nyash strings are UTF8, but tests are ASCII only here // Note: operating on bytes; Nyash strings are UTF8, but tests are ASCII only here
let bytes = s.as_bytes(); let bytes = s.as_bytes();
let sub = String::from_utf8(bytes[i0..i1].to_vec()).unwrap_or_default(); let sub = String::from_utf8(bytes[i0..i1].to_vec()).unwrap_or_default();
@ -209,8 +247,7 @@ impl MirInterpreter {
"is_space" => { "is_space" => {
if let Some(arg_id) = args.get(0) { if let Some(arg_id) = args.get(0) {
let ch = self.reg_load(*arg_id)?.to_string(); let ch = self.reg_load(*arg_id)?.to_string();
let is_ws = let is_ws = ch == " " || ch == "\t" || ch == "\n" || ch == "\r";
ch == " " || ch == "\t" || ch == "\n" || ch == "\r";
Ok(VMValue::Bool(is_ws)) Ok(VMValue::Bool(is_ws))
} else { } else {
Err(self.err_invalid("is_space requires 1 argument")) Err(self.err_invalid("is_space requires 1 argument"))
@ -221,10 +258,9 @@ impl MirInterpreter {
if let Some(arg_id) = args.get(0) { if let Some(arg_id) = args.get(0) {
let ch = self.reg_load(*arg_id)?.to_string(); let ch = self.reg_load(*arg_id)?.to_string();
let c = ch.chars().next().unwrap_or('\0'); let c = ch.chars().next().unwrap_or('\0');
let is_alpha = let is_alpha = ('A'..='Z').contains(&c)
('A'..='Z').contains(&c) || || ('a'..='z').contains(&c)
('a'..='z').contains(&c) || || c == '_';
c == '_';
Ok(VMValue::Bool(is_alpha)) Ok(VMValue::Bool(is_alpha))
} else { } else {
Err(self.err_invalid("is_alpha requires 1 argument")) Err(self.err_invalid("is_alpha requires 1 argument"))
@ -234,8 +270,8 @@ impl MirInterpreter {
} }
} else if let Some(p) = box_ref } else if let Some(p) = box_ref
.as_any() .as_any()
.downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() .downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>(
{ ) {
let host = crate::runtime::plugin_loader_unified::get_global_plugin_host(); let host = crate::runtime::plugin_loader_unified::get_global_plugin_host();
let host = host.read().unwrap(); let host = host.read().unwrap();
let argv = self.load_args_as_boxes(args)?; let argv = self.load_args_as_boxes(args)?;
@ -249,14 +285,17 @@ impl MirInterpreter {
Ok(None) => Ok(VMValue::Void), Ok(None) => Ok(VMValue::Void),
Err(e) => Err(self.err_with_context( Err(e) => Err(self.err_with_context(
&format!("Plugin method {}.{}", p.box_type, method), &format!("Plugin method {}.{}", p.box_type, method),
&format!("{:?}", e) &format!("{:?}", e),
)), )),
} }
} else { } else {
Err(self.err_method_not_found(&box_ref.type_name(), method)) Err(self.err_method_not_found(&box_ref.type_name(), method))
} }
} }
_ => Err(self.err_with_context("method call", &format!("{} not supported on {:?}", method, receiver))), _ => Err(self.err_with_context(
"method call",
&format!("{} not supported on {:?}", method, receiver),
)),
} }
} }
} }

View File

@ -4,9 +4,9 @@
use super::*; use super::*;
mod externs;
mod global; mod global;
mod method; mod method;
mod externs;
// legacy by-name resolver has been removed (Phase 2 complete) // legacy by-name resolver has been removed (Phase 2 complete)
impl MirInterpreter { impl MirInterpreter {
@ -19,18 +19,43 @@ impl MirInterpreter {
) -> Result<(), VMError> { ) -> Result<(), VMError> {
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") { if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
match callee { match callee {
Some(Callee::Global(n)) => eprintln!("[hb:path] call Callee::Global {} argc={}", n, args.len()), Some(Callee::Global(n)) => {
Some(Callee::Method{ box_name, method, ..}) => eprintln!("[hb:path] call Callee::Method {}.{} argc={}", box_name, method, args.len()), eprintln!("[hb:path] call Callee::Global {} argc={}", n, 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::Method {
Some(Callee::Value(_)) => eprintln!("[hb:path] call Callee::Value argc={}", args.len()), box_name, method, ..
Some(Callee::Extern(n)) => eprintln!("[hb:path] call Callee::Extern {} argc={}", n, args.len()), }) => eprintln!(
None => eprintln!("[hb:path] call Legacy func_id={:?} argc={}", func, args.len()), "[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 // SSOT fast-path: route hostbridge.extern_invoke to extern dispatcher regardless of resolution form
if let Some(Callee::Global(func_name)) = callee { if let Some(Callee::Global(func_name)) = callee {
if func_name == "hostbridge.extern_invoke" || func_name.starts_with("hostbridge.extern_invoke/") { if func_name == "hostbridge.extern_invoke"
|| func_name.starts_with("hostbridge.extern_invoke/")
{
let v = self.execute_extern_function("hostbridge.extern_invoke", args)?; let v = self.execute_extern_function("hostbridge.extern_invoke", args)?;
self.write_result(dst, v); self.write_result(dst, v);
return Ok(()); return Ok(());
@ -44,7 +69,9 @@ impl MirInterpreter {
if let VMValue::String(ref s) = name_val { if let VMValue::String(ref s) = name_val {
if let Some(f) = self.functions.get(s).cloned() { if let Some(f) = self.functions.get(s).cloned() {
let mut argv: Vec<VMValue> = Vec::with_capacity(args.len()); let mut argv: Vec<VMValue> = Vec::with_capacity(args.len());
for a in args { argv.push(self.reg_load(*a)?); } for a in args {
argv.push(self.reg_load(*a)?);
}
self.exec_function_inner(&f, Some(&argv))? self.exec_function_inner(&f, Some(&argv))?
} else { } else {
return Err(self.err_with_context("call", &format!( return Err(self.err_with_context("call", &format!(
@ -53,7 +80,10 @@ impl MirInterpreter {
))); )));
} }
} else { } else {
return Err(self.err_with_context("call", "by-name calls unsupported without Callee attachment")); return Err(self.err_with_context(
"call",
"by-name calls unsupported without Callee attachment",
));
} }
}; };
self.write_result(dst, call_result); self.write_result(dst, call_result);
@ -67,10 +97,15 @@ impl MirInterpreter {
) -> Result<VMValue, VMError> { ) -> Result<VMValue, VMError> {
match callee { match callee {
Callee::Global(func_name) => self.execute_global_function(func_name, args), Callee::Global(func_name) => self.execute_global_function(func_name, args),
Callee::Method { box_name, method, receiver, .. } => { Callee::Method {
self.execute_method_callee(box_name, method, receiver, args) box_name,
method,
receiver,
..
} => self.execute_method_callee(box_name, method, receiver, args),
Callee::Constructor { box_type } => {
Err(self.err_unsupported(&format!("Constructor calls for {}", box_type)))
} }
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::Closure { .. } => Err(self.err_unsupported("Closure creation in VM")),
Callee::Value(func_val_id) => { Callee::Value(func_val_id) => {
let _ = self.reg_load(*func_val_id)?; let _ = self.reg_load(*func_val_id)?;
@ -79,5 +114,4 @@ impl MirInterpreter {
Callee::Extern(extern_name) => self.execute_extern_function(extern_name, args), Callee::Extern(extern_name) => self.execute_extern_function(extern_name, args),
} }
} }
} }

View File

@ -10,8 +10,12 @@ impl MirInterpreter {
let key = format!("{}.{}", target, method); let key = format!("{}.{}", target, method);
for pat in flt.split(',') { for pat in flt.split(',') {
let p = pat.trim(); let p = pat.trim();
if p.is_empty() { continue; } if p.is_empty() {
if p == method || p == key { return true; } continue;
}
if p == method || p == key {
return true;
}
} }
return false; return false;
} }
@ -23,7 +27,9 @@ impl MirInterpreter {
if let JsonValue::Object(ref mut m) = v { if let JsonValue::Object(ref mut m) = v {
if !m.contains_key("version") { if !m.contains_key("version") {
m.insert("version".to_string(), JsonValue::from(0)); m.insert("version".to_string(), JsonValue::from(0));
if let Ok(out) = serde_json::to_string(&v) { return out; } if let Ok(out) = serde_json::to_string(&v) {
return out;
}
} }
} }
s.to_string() s.to_string()
@ -64,9 +70,15 @@ impl MirInterpreter {
VMValue::Void => println!("null"), VMValue::Void => println!("null"),
VMValue::String(s) => println!("{}", s), VMValue::String(s) => println!("{}", s),
VMValue::BoxRef(bx) => { VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() { if bx
.as_any()
.downcast_ref::<crate::box_trait::VoidBox>()
.is_some()
{
println!("null"); println!("null");
} else if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() { } else if let Some(sb) =
bx.as_any().downcast_ref::<crate::box_trait::StringBox>()
{
println!("{}", sb.value); println!("{}", sb.value);
} else { } else {
println!("{}", v.to_string()); println!("{}", v.to_string());
@ -90,9 +102,15 @@ impl MirInterpreter {
VMValue::Void => eprintln!("[warn] null"), VMValue::Void => eprintln!("[warn] null"),
VMValue::String(s) => eprintln!("[warn] {}", s), VMValue::String(s) => eprintln!("[warn] {}", s),
VMValue::BoxRef(bx) => { VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() { if bx
.as_any()
.downcast_ref::<crate::box_trait::VoidBox>()
.is_some()
{
eprintln!("[warn] null"); eprintln!("[warn] null");
} else if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() { } else if let Some(sb) =
bx.as_any().downcast_ref::<crate::box_trait::StringBox>()
{
eprintln!("[warn] {}", sb.value); eprintln!("[warn] {}", sb.value);
} else { } else {
eprintln!("[warn] {}", v.to_string()); eprintln!("[warn] {}", v.to_string());
@ -116,9 +134,15 @@ impl MirInterpreter {
VMValue::Void => eprintln!("[error] null"), VMValue::Void => eprintln!("[error] null"),
VMValue::String(s) => eprintln!("[error] {}", s), VMValue::String(s) => eprintln!("[error] {}", s),
VMValue::BoxRef(bx) => { VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() { if bx
.as_any()
.downcast_ref::<crate::box_trait::VoidBox>()
.is_some()
{
eprintln!("[error] null"); eprintln!("[error] null");
} else if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() { } else if let Some(sb) =
bx.as_any().downcast_ref::<crate::box_trait::StringBox>()
{
eprintln!("[error] {}", sb.value); eprintln!("[error] {}", sb.value);
} else { } else {
eprintln!("[error] {}", v.to_string()); eprintln!("[error] {}", v.to_string());
@ -140,15 +164,29 @@ impl MirInterpreter {
if std::env::var("HAKO_V1_EXTERN_PROVIDER").ok().as_deref() == Some("1") { if std::env::var("HAKO_V1_EXTERN_PROVIDER").ok().as_deref() == Some("1") {
return Some(Ok(VMValue::String(String::new()))); return Some(Ok(VMValue::String(String::new())));
} }
if args.is_empty() { return Some(Err(ErrorBuilder::arg_count_mismatch("env.mirbuilder.emit", 1, args.len()))); } if args.is_empty() {
let program_json = match self.reg_load(args[0]) { Ok(v) => v.to_string(), Err(e) => return Some(Err(e)) }; return Some(Err(ErrorBuilder::arg_count_mismatch(
"env.mirbuilder.emit",
1,
args.len(),
)));
}
let program_json = match self.reg_load(args[0]) {
Ok(v) => v.to_string(),
Err(e) => return Some(Err(e)),
};
// Phase 21.8: Read imports from environment variable if present // Phase 21.8: Read imports from environment variable if present
let imports = if let Ok(imports_json) = std::env::var("HAKO_MIRBUILDER_IMPORTS") { let imports = if let Ok(imports_json) = std::env::var("HAKO_MIRBUILDER_IMPORTS") {
match serde_json::from_str::<std::collections::HashMap<String, String>>(&imports_json) { match serde_json::from_str::<std::collections::HashMap<String, String>>(
&imports_json,
) {
Ok(map) => map, Ok(map) => map,
Err(e) => { Err(e) => {
eprintln!("[mirbuilder/imports] Failed to parse HAKO_MIRBUILDER_IMPORTS: {}", e); eprintln!(
"[mirbuilder/imports] Failed to parse HAKO_MIRBUILDER_IMPORTS: {}",
e
);
std::collections::HashMap::new() std::collections::HashMap::new()
} }
} }
@ -156,9 +194,16 @@ impl MirInterpreter {
std::collections::HashMap::new() std::collections::HashMap::new()
}; };
let res = match crate::host_providers::mir_builder::program_json_to_mir_json_with_imports(&program_json, imports) { let res =
match crate::host_providers::mir_builder::program_json_to_mir_json_with_imports(
&program_json,
imports,
) {
Ok(s) => Ok(VMValue::String(Self::patch_mir_json_version(&s))), Ok(s) => Ok(VMValue::String(Self::patch_mir_json_version(&s))),
Err(e) => Err(ErrorBuilder::with_context("env.mirbuilder.emit", &e.to_string())), Err(e) => Err(ErrorBuilder::with_context(
"env.mirbuilder.emit",
&e.to_string(),
)),
}; };
Some(res) Some(res)
} }
@ -167,55 +212,114 @@ impl MirInterpreter {
if std::env::var("HAKO_V1_EXTERN_PROVIDER").ok().as_deref() == Some("1") { if std::env::var("HAKO_V1_EXTERN_PROVIDER").ok().as_deref() == Some("1") {
return Some(Ok(VMValue::String(String::new()))); return Some(Ok(VMValue::String(String::new())));
} }
if args.is_empty() { return Some(Err(ErrorBuilder::arg_count_mismatch("env.codegen.emit_object", 1, args.len()))); } if args.is_empty() {
let mir_json_raw = match self.reg_load(args[0]) { Ok(v) => v.to_string(), Err(e) => return Some(Err(e)) }; return Some(Err(ErrorBuilder::arg_count_mismatch(
"env.codegen.emit_object",
1,
args.len(),
)));
}
let mir_json_raw = match self.reg_load(args[0]) {
Ok(v) => v.to_string(),
Err(e) => return Some(Err(e)),
};
// Normalize to v1 shape if missing/legacy (prevents harness NoneType errors) // Normalize to v1 shape if missing/legacy (prevents harness NoneType errors)
let mir_json = Self::patch_mir_json_version(&mir_json_raw); let mir_json = Self::patch_mir_json_version(&mir_json_raw);
let opts = crate::host_providers::llvm_codegen::Opts { let opts = crate::host_providers::llvm_codegen::Opts {
out: None, out: None,
nyrt: std::env::var("NYASH_EMIT_EXE_NYRT").ok().map(std::path::PathBuf::from), nyrt: std::env::var("NYASH_EMIT_EXE_NYRT")
opt_level: std::env::var("HAKO_LLVM_OPT_LEVEL").ok().or(Some("0".to_string())), .ok()
.map(std::path::PathBuf::from),
opt_level: std::env::var("HAKO_LLVM_OPT_LEVEL")
.ok()
.or(Some("0".to_string())),
timeout_ms: None, timeout_ms: None,
}; };
let res = match crate::host_providers::llvm_codegen::mir_json_to_object(&mir_json, opts) { let res = match crate::host_providers::llvm_codegen::mir_json_to_object(
&mir_json, opts,
) {
Ok(p) => Ok(VMValue::String(p.to_string_lossy().into_owned())), Ok(p) => Ok(VMValue::String(p.to_string_lossy().into_owned())),
Err(e) => Err(ErrorBuilder::with_context("env.codegen.emit_object", &e.to_string())), Err(e) => Err(ErrorBuilder::with_context(
"env.codegen.emit_object",
&e.to_string(),
)),
}; };
Some(res) Some(res)
} }
"env.codegen.link_object" => { "env.codegen.link_object" => {
// Only supported on C-API route; expect 1 or 2 args: obj_path [, exe_out] // Only supported on C-API route; expect 1 or 2 args: obj_path [, exe_out]
let obj_path = match args.get(0) { let obj_path = match args.get(0) {
Some(v) => match self.reg_load(*v) { Ok(v) => v.to_string(), Err(e) => return Some(Err(e)) }, Some(v) => match self.reg_load(*v) {
None => return Some(Err(self.err_invalid("env.codegen.link_object expects 1+ args"))), Ok(v) => v.to_string(),
Err(e) => return Some(Err(e)),
},
None => {
return Some(Err(
self.err_invalid("env.codegen.link_object expects 1+ args")
))
}
}; };
let exe_out = match args.get(1) { let exe_out = match args.get(1) {
Some(v) => Some(match self.reg_load(*v) { Ok(v) => v.to_string(), Err(e) => return Some(Err(e)) }), Some(v) => Some(match self.reg_load(*v) {
Ok(v) => v.to_string(),
Err(e) => return Some(Err(e)),
}),
None => None, None => None,
}; };
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok(); let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
// Require C-API toggles // Require C-API toggles
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") || 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") { || std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI")
return Some(Err(ErrorBuilder::invalid_instruction("env.codegen.link_object: C-API route disabled"))); .ok()
.as_deref()
!= Some("1")
{
return Some(Err(ErrorBuilder::invalid_instruction(
"env.codegen.link_object: C-API route disabled",
)));
} }
let obj = std::path::PathBuf::from(obj_path); 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")); let exe = exe_out
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) { .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(()) => Some(Ok(VMValue::String(exe.to_string_lossy().into_owned()))), Ok(()) => Some(Ok(VMValue::String(exe.to_string_lossy().into_owned()))),
Err(e) => Some(Err(ErrorBuilder::with_context("env.codegen.link_object", &e.to_string()))), Err(e) => Some(Err(ErrorBuilder::with_context(
"env.codegen.link_object",
&e.to_string(),
))),
} }
} }
// Environment // Environment
"env.get" => { "env.get" => {
if args.is_empty() { return Some(Err(ErrorBuilder::arg_count_mismatch("env.get", 1, args.len()))); } if args.is_empty() {
let key = match self.reg_load(args[0]) { Ok(v) => v.to_string(), Err(e) => return Some(Err(e)) }; return Some(Err(ErrorBuilder::arg_count_mismatch(
"env.get",
1,
args.len(),
)));
}
let key = match self.reg_load(args[0]) {
Ok(v) => v.to_string(),
Err(e) => return Some(Err(e)),
};
let val = std::env::var(&key).ok(); let val = std::env::var(&key).ok();
Some(Ok(match val { Some(s) => VMValue::String(s), None => VMValue::Void })) Some(Ok(match val {
Some(s) => VMValue::String(s),
None => VMValue::Void,
}))
} }
"env.set" => { "env.set" => {
if args.len() < 2 { if args.len() < 2 {
return Some(Err(ErrorBuilder::arg_count_mismatch("env.set", 2, args.len()))); return Some(Err(ErrorBuilder::arg_count_mismatch(
"env.set",
2,
args.len(),
)));
} }
let key = match self.reg_load(args[0]) { let key = match self.reg_load(args[0]) {
Ok(v) => v.to_string(), Ok(v) => v.to_string(),
@ -247,24 +351,24 @@ impl MirInterpreter {
} }
} }
} else { } else {
return Some(Err(self.err_invalid( return Some(Err(
"env.box_introspect.kind expects 1 arg", self.err_invalid("env.box_introspect.kind expects 1 arg")
))); ));
} }
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
let result = plugin_loader_v2::handle_box_introspect("kind", &collected); let result = plugin_loader_v2::handle_box_introspect("kind", &collected);
#[cfg(any(not(feature = "plugins"), target_arch = "wasm32"))] #[cfg(any(not(feature = "plugins"), target_arch = "wasm32"))]
let result: crate::bid::BidResult<Option<Box<dyn crate::box_trait::NyashBox>>> = let result: crate::bid::BidResult<
Err(crate::bid::BidError::PluginError); Option<Box<dyn crate::box_trait::NyashBox>>,
> = Err(crate::bid::BidError::PluginError);
match result { match result {
Ok(Some(b)) => Some(Ok(VMValue::BoxRef(Arc::from(b)))), Ok(Some(b)) => Some(Ok(VMValue::BoxRef(Arc::from(b)))),
Ok(None) => Some(Ok(VMValue::Void)), Ok(None) => Some(Ok(VMValue::Void)),
Err(e) => Some(Err(self.err_with_context( Err(e) => Some(Err(
"env.box_introspect.kind", self.err_with_context("env.box_introspect.kind", &format!("{:?}", e))
&format!("{:?}", e), )),
))),
} }
} }
"hostbridge.extern_invoke" => { "hostbridge.extern_invoke" => {
@ -272,17 +376,30 @@ impl MirInterpreter {
eprintln!("[hb:entry:provider] hostbridge.extern_invoke"); eprintln!("[hb:entry:provider] hostbridge.extern_invoke");
} }
if args.len() < 2 { if args.len() < 2 {
return Some(Err(self.err_invalid("extern_invoke expects at least 2 args"))); return Some(Err(
self.err_invalid("extern_invoke expects at least 2 args")
));
} }
let name = match self.reg_load(args[0]) { Ok(v) => v.to_string(), Err(e) => return Some(Err(e)) }; let name = match self.reg_load(args[0]) {
let method = match self.reg_load(args[1]) { Ok(v) => v.to_string(), Err(e) => return Some(Err(e)) }; Ok(v) => v.to_string(),
Err(e) => return Some(Err(e)),
};
let method = match self.reg_load(args[1]) {
Ok(v) => v.to_string(),
Err(e) => return Some(Err(e)),
};
// Extract first payload arg (optional) // Extract first payload arg (optional)
let mut first_arg_str: Option<String> = None; let mut first_arg_str: Option<String> = None;
if let Some(a2) = args.get(2) { if let Some(a2) = args.get(2) {
let v = match self.reg_load(*a2) { Ok(v) => v, Err(e) => return Some(Err(e)) }; let v = match self.reg_load(*a2) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
match v { match v {
VMValue::BoxRef(b) => { VMValue::BoxRef(b) => {
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() { if let Some(ab) =
b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>()
{
let idx: Box<dyn crate::box_trait::NyashBox> = let idx: Box<dyn crate::box_trait::NyashBox> =
Box::new(crate::box_trait::IntegerBox::new(0)); Box::new(crate::box_trait::IntegerBox::new(0));
let elem = ab.get(idx); let elem = ab.get(idx);
@ -299,16 +416,27 @@ impl MirInterpreter {
eprintln!("[hb:dispatch:provider] {} {}", name, method); eprintln!("[hb:dispatch:provider] {} {}", name, method);
} }
let out = match (name.as_str(), method.as_str()) { let out = match (name.as_str(), method.as_str()) {
("env.codegen", "link_object") if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") => { ("env.codegen", "link_object")
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") =>
{
// Trace payload shape before actual handling // Trace payload shape before actual handling
if let Some(a2) = args.get(2) { if let Some(a2) = args.get(2) {
let v = match self.reg_load(*a2) { Ok(v) => v, Err(_) => VMValue::Void }; let v = match self.reg_load(*a2) {
Ok(v) => v,
Err(_) => VMValue::Void,
};
match &v { match &v {
VMValue::BoxRef(b) => { VMValue::BoxRef(b) => {
if b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>().is_some() { if b.as_any()
.downcast_ref::<crate::boxes::array::ArrayBox>()
.is_some()
{
eprintln!("[hb:provider:args] link_object third=ArrayBox"); eprintln!("[hb:provider:args] link_object third=ArrayBox");
} else { } else {
eprintln!("[hb:provider:args] link_object third=BoxRef({})", b.type_name()); eprintln!(
"[hb:provider:args] link_object third=BoxRef({})",
b.type_name()
);
} }
} }
other => { other => {
@ -327,13 +455,19 @@ impl MirInterpreter {
}; };
match v { match v {
VMValue::BoxRef(b) => { VMValue::BoxRef(b) => {
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() { if let Some(ab) =
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0)); 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 elem0 = ab.get(idx0).to_string_box().value;
let mut exe: Option<String> = None; let mut exe: Option<String> = None;
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1)); 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; let e1 = ab.get(idx1).to_string_box().value;
if !e1.is_empty() { exe = Some(e1); } if !e1.is_empty() {
exe = Some(e1);
}
(elem0, exe) (elem0, exe)
} else { } else {
(b.to_string_box().value, None) (b.to_string_box().value, None)
@ -342,25 +476,47 @@ impl MirInterpreter {
_ => (v.to_string(), None), _ => (v.to_string(), None),
} }
} else { } else {
return Some(Err(self.err_invalid("extern_invoke env.codegen.link_object expects args array"))); return Some(Err(self.err_invalid(
"extern_invoke env.codegen.link_object expects args array",
)));
}; };
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") || 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") { || std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI")
return Some(Err(ErrorBuilder::invalid_instruction("env.codegen.link_object: C-API route disabled"))); .ok()
.as_deref()
!= Some("1")
{
return Some(Err(ErrorBuilder::invalid_instruction(
"env.codegen.link_object: C-API route disabled",
)));
} }
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok(); let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
let obj = std::path::PathBuf::from(objs); let obj = std::path::PathBuf::from(objs);
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe")); let exe = exe_out
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) { .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())), Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
Err(e) => Err(ErrorBuilder::with_context("env.codegen.link_object", &e.to_string())) Err(e) => Err(ErrorBuilder::with_context(
"env.codegen.link_object",
&e.to_string(),
)),
} }
} }
("env.mirbuilder", "emit") => { ("env.mirbuilder", "emit") => {
if let Some(s) = first_arg_str { if let Some(s) = first_arg_str {
// Phase 21.8: Read imports from environment variable if present // Phase 21.8: Read imports from environment variable if present
let imports = if let Ok(imports_json) = std::env::var("HAKO_MIRBUILDER_IMPORTS") { let imports = if let Ok(imports_json) =
match serde_json::from_str::<std::collections::HashMap<String, String>>(&imports_json) { std::env::var("HAKO_MIRBUILDER_IMPORTS")
{
match serde_json::from_str::<
std::collections::HashMap<String, String>,
>(&imports_json)
{
Ok(map) => map, Ok(map) => map,
Err(e) => { Err(e) => {
eprintln!("[mirbuilder/imports] Failed to parse HAKO_MIRBUILDER_IMPORTS: {}", e); eprintln!("[mirbuilder/imports] Failed to parse HAKO_MIRBUILDER_IMPORTS: {}", e);
@ -383,16 +539,21 @@ impl MirInterpreter {
if let Some(s) = first_arg_str { if let Some(s) = first_arg_str {
let opts = crate::host_providers::llvm_codegen::Opts { let opts = crate::host_providers::llvm_codegen::Opts {
out: None, out: None,
nyrt: std::env::var("NYASH_EMIT_EXE_NYRT").ok().map(std::path::PathBuf::from), nyrt: std::env::var("NYASH_EMIT_EXE_NYRT")
.ok()
.map(std::path::PathBuf::from),
opt_level: std::env::var("HAKO_LLVM_OPT_LEVEL").ok(), opt_level: std::env::var("HAKO_LLVM_OPT_LEVEL").ok(),
timeout_ms: None, timeout_ms: None,
}; };
match crate::host_providers::llvm_codegen::mir_json_to_object(&s, opts) { match crate::host_providers::llvm_codegen::mir_json_to_object(&s, opts)
{
Ok(p) => Ok(VMValue::String(p.to_string_lossy().into_owned())), 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())), Err(e) => Err(self
.err_with_context("env.codegen.emit_object", &e.to_string())),
} }
} else { } else {
Err(self.err_invalid("extern_invoke env.codegen.emit_object expects 1 arg")) Err(self
.err_invalid("extern_invoke env.codegen.emit_object expects 1 arg"))
} }
} }
("env.codegen", "link_object") => { ("env.codegen", "link_object") => {
@ -408,12 +569,18 @@ impl MirInterpreter {
}; };
match v { match v {
VMValue::BoxRef(b) => { VMValue::BoxRef(b) => {
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() { if let Some(ab) =
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0)); 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));
obj_s = Some(ab.get(idx0).to_string_box().value); obj_s = Some(ab.get(idx0).to_string_box().value);
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1)); let idx1: Box<dyn crate::box_trait::NyashBox> =
Box::new(crate::box_trait::IntegerBox::new(1));
let s1 = ab.get(idx1).to_string_box().value; let s1 = ab.get(idx1).to_string_box().value;
if !s1.is_empty() { exe_s = Some(s1); } if !s1.is_empty() {
exe_s = Some(s1);
}
} else { } else {
obj_s = Some(b.to_string_box().value); obj_s = Some(b.to_string_box().value);
} }
@ -426,18 +593,37 @@ impl MirInterpreter {
} }
let objs = match obj_s { let objs = match obj_s {
Some(s) => s, Some(s) => s,
None => return Some(Err(self.err_invalid("extern_invoke env.codegen.link_object expects args"))), None => {
return Some(Err(self.err_invalid(
"extern_invoke env.codegen.link_object expects args",
)))
}
}; };
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") || 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") { || std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI")
return Some(Err(ErrorBuilder::invalid_instruction("env.codegen.link_object: C-API route disabled"))); .ok()
.as_deref()
!= Some("1")
{
return Some(Err(ErrorBuilder::invalid_instruction(
"env.codegen.link_object: C-API route disabled",
)));
} }
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok(); let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
let obj = std::path::PathBuf::from(objs); let obj = std::path::PathBuf::from(objs);
let exe = exe_s.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe")); let exe = exe_s
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) { .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())), Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
Err(e) => Err(ErrorBuilder::with_context("env.codegen.link_object", &e.to_string())) Err(e) => Err(ErrorBuilder::with_context(
"env.codegen.link_object",
&e.to_string(),
)),
} }
} }
("env.box_introspect", "kind") => { ("env.box_introspect", "kind") => {
@ -487,9 +673,7 @@ impl MirInterpreter {
} }
} }
other => { other => {
if std::env::var("NYASH_BOX_INTROSPECT_TRACE") if std::env::var("NYASH_BOX_INTROSPECT_TRACE").ok().as_deref()
.ok()
.as_deref()
== Some("1") == Some("1")
{ {
eprintln!( eprintln!(
@ -509,16 +693,15 @@ impl MirInterpreter {
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
let result = plugin_loader_v2::handle_box_introspect("kind", &collected); let result = plugin_loader_v2::handle_box_introspect("kind", &collected);
#[cfg(any(not(feature = "plugins"), target_arch = "wasm32"))] #[cfg(any(not(feature = "plugins"), target_arch = "wasm32"))]
let result: crate::bid::BidResult<Option<Box<dyn NyashBox>>> = let result: crate::bid::BidResult<
Err(crate::bid::BidError::PluginError); Option<Box<dyn NyashBox>>,
> = Err(crate::bid::BidError::PluginError);
match result { match result {
Ok(Some(b)) => Ok(VMValue::BoxRef(Arc::from(b))), Ok(Some(b)) => Ok(VMValue::BoxRef(Arc::from(b))),
Ok(None) => Ok(VMValue::Void), Ok(None) => Ok(VMValue::Void),
Err(e) => Err(self.err_with_context( Err(e) => Err(self
"env.box_introspect.kind", .err_with_context("env.box_introspect.kind", &format!("{:?}", e))),
&format!("{:?}", e),
)),
} }
} }
_ => { _ => {
@ -529,7 +712,7 @@ impl MirInterpreter {
"hostbridge.extern_invoke unsupported for {}.{} [provider] (check extern_provider_dispatch / env.* handlers)", "hostbridge.extern_invoke unsupported for {}.{} [provider] (check extern_provider_dispatch / env.* handlers)",
name, method name, method
))) )))
}, }
}; };
Some(out) Some(out)
} }

View File

@ -10,7 +10,9 @@ impl MirInterpreter {
if let JsonValue::Object(ref mut m) = v { if let JsonValue::Object(ref mut m) = v {
if !m.contains_key("version") { if !m.contains_key("version") {
m.insert("version".to_string(), JsonValue::from(0)); m.insert("version".to_string(), JsonValue::from(0));
if let Ok(out) = serde_json::to_string(&v) { return out; } if let Ok(out) = serde_json::to_string(&v) {
return out;
}
} }
} }
s.to_string() s.to_string()
@ -52,23 +54,47 @@ impl MirInterpreter {
if let Some(a0) = args.get(0) { if let Some(a0) = args.get(0) {
let v = self.reg_load(*a0)?; let v = self.reg_load(*a0)?;
// Dev-only: mirror print-trace for extern console.log // Dev-only: mirror print-trace for extern console.log
if Self::print_trace_enabled() { self.print_trace_emit(&v); } if Self::print_trace_enabled() {
self.print_trace_emit(&v);
}
// Treat VM Void and BoxRef(VoidBox) as JSON null for dev ergonomics // Treat VM Void and BoxRef(VoidBox) as JSON null for dev ergonomics
match &v { match &v {
VMValue::Void => { println!("null"); self.write_void(dst); return Ok(()); } VMValue::Void => {
println!("null");
self.write_void(dst);
return Ok(());
}
VMValue::BoxRef(bx) => { VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() { if bx
println!("null"); self.write_void(dst); return Ok(()); .as_any()
.downcast_ref::<crate::box_trait::VoidBox>()
.is_some()
{
println!("null");
self.write_void(dst);
return Ok(());
} }
if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() { if let Some(sb) =
println!("{}", sb.value); self.write_void(dst); return Ok(()); bx.as_any().downcast_ref::<crate::box_trait::StringBox>()
{
println!("{}", sb.value);
self.write_void(dst);
return Ok(());
} }
} }
VMValue::String(s) => { println!("{}", s); self.write_void(dst); return Ok(()); } VMValue::String(s) => {
println!("{}", s);
self.write_void(dst);
return Ok(());
}
_ => {} _ => {}
} }
// Operator Box (Stringify) dev flag gated // Operator Box (Stringify) dev flag gated
if std::env::var("NYASH_OPERATOR_BOX_STRINGIFY").ok().as_deref() == Some("1") { if std::env::var("NYASH_OPERATOR_BOX_STRINGIFY")
.ok()
.as_deref()
== Some("1")
{
if let Some(op) = self.functions.get("StringifyOperator.apply/1").cloned() { if let Some(op) = self.functions.get("StringifyOperator.apply/1").cloned() {
let out = self.exec_function_inner(&op, Some(&[v.clone()]))?; let out = self.exec_function_inner(&op, Some(&[v.clone()]))?;
println!("{}", out.to_string()); println!("{}", out.to_string());
@ -151,20 +177,28 @@ impl MirInterpreter {
Ok(()) Ok(())
} }
("env.mirbuilder", "emit") => { ("env.mirbuilder", "emit") => {
let ret = self.extern_provider_dispatch("env.mirbuilder.emit", args).unwrap_or(Ok(VMValue::Void))?; let ret = self
.extern_provider_dispatch("env.mirbuilder.emit", args)
.unwrap_or(Ok(VMValue::Void))?;
self.write_result(dst, ret); self.write_result(dst, ret);
Ok(()) Ok(())
} }
("env.codegen", "emit_object") => { ("env.codegen", "emit_object") => {
let ret = self.extern_provider_dispatch("env.codegen.emit_object", args).unwrap_or(Ok(VMValue::Void))?; let ret = self
.extern_provider_dispatch("env.codegen.emit_object", args)
.unwrap_or(Ok(VMValue::Void))?;
self.write_result(dst, ret); self.write_result(dst, ret);
Ok(()) Ok(())
} }
("env.codegen", "link_object") => { ("env.codegen", "link_object") => {
// Args in third param (ArrayBox): [obj_path, exe_out?] // Args in third param (ArrayBox): [obj_path, exe_out?]
// Note: This branch is used for ExternCall form; provider toggles must be ON. // Note: This branch is used for ExternCall form; provider toggles must be ON.
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") || 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") { || 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")); return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
} }
// Extract array payload // Extract array payload
@ -172,13 +206,19 @@ impl MirInterpreter {
let v = self.reg_load(*a2)?; let v = self.reg_load(*a2)?;
match v { match v {
VMValue::BoxRef(b) => { VMValue::BoxRef(b) => {
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() { if let Some(ab) =
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0)); 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 elem0 = ab.get(idx0).to_string_box().value;
let mut exe: Option<String> = None; let mut exe: Option<String> = None;
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1)); 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; let e1 = ab.get(idx1).to_string_box().value;
if !e1.is_empty() { exe = Some(e1); } if !e1.is_empty() {
exe = Some(e1);
}
(elem0, exe) (elem0, exe)
} else { } else {
(b.to_string_box().value, None) (b.to_string_box().value, None)
@ -187,13 +227,18 @@ impl MirInterpreter {
_ => (v.to_string(), None), _ => (v.to_string(), None),
} }
} else { } else {
return Err(self.err_invalid("extern_invoke env.codegen.link_object expects args array")); return Err(self
.err_invalid("extern_invoke env.codegen.link_object expects args array"));
}; };
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok(); let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
let obj = std::path::PathBuf::from(obj_path); 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")); let exe = exe_out
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref())
.map_err(|e| self.err_with_context("env.codegen.link_object", &e.to_string()))?; .map_err(|e| {
self.err_with_context("env.codegen.link_object", &e.to_string())
})?;
self.write_result(dst, VMValue::String(exe.to_string_lossy().into_owned())); self.write_result(dst, VMValue::String(exe.to_string_lossy().into_owned()));
Ok(()) Ok(())
} }
@ -208,17 +253,18 @@ impl MirInterpreter {
("hostbridge", "extern_invoke") => { ("hostbridge", "extern_invoke") => {
if let Some(res) = self.extern_provider_dispatch("hostbridge.extern_invoke", args) { if let Some(res) = self.extern_provider_dispatch("hostbridge.extern_invoke", args) {
match res { match res {
Ok(v) => { self.write_result(dst, v); } Ok(v) => {
Err(e) => { return Err(e); } self.write_result(dst, v);
}
Err(e) => {
return Err(e);
}
} }
return Ok(()); return Ok(());
} }
return Err(self.err_invalid("hostbridge.extern_invoke unsupported [externals]")); return Err(self.err_invalid("hostbridge.extern_invoke unsupported [externals]"));
} }
_ => Err(self.err_invalid(format!( _ => Err(self.err_invalid(format!("ExternCall {}.{} not supported", iface, method))),
"ExternCall {}.{} not supported",
iface, method
))),
} }
} }
} }

View File

@ -13,16 +13,28 @@ impl MirInterpreter {
let v = self.reg_load(value)?; let v = self.reg_load(value)?;
// Align with calls.rs behavior: Void/BoxRef(VoidBox) prints as null; raw String/StringBox unquoted // Align with calls.rs behavior: Void/BoxRef(VoidBox) prints as null; raw String/StringBox unquoted
match &v { match &v {
VMValue::Void => { println!("null"); return Ok(()); } VMValue::Void => {
println!("null");
return Ok(());
}
VMValue::BoxRef(bx) => { VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() { if bx
println!("null"); return Ok(()); .as_any()
.downcast_ref::<crate::box_trait::VoidBox>()
.is_some()
{
println!("null");
return Ok(());
} }
if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() { if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
println!("{}", sb.value); return Ok(()); println!("{}", sb.value);
return Ok(());
} }
} }
VMValue::String(s) => { println!("{}", s); return Ok(()); } VMValue::String(s) => {
println!("{}", s);
return Ok(());
}
_ => {} _ => {}
} }
println!("{}", v.to_string()); println!("{}", v.to_string());

View File

@ -3,9 +3,7 @@ use super::*;
// VM dispatch trace macro (used across handlers) // VM dispatch trace macro (used across handlers)
macro_rules! trace_dispatch { macro_rules! trace_dispatch {
($method:expr, $handler:expr) => { ($method:expr, $handler:expr) => {
if $method == "length" if $method == "length" && std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
&& std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1")
{
eprintln!("[vm-trace] length dispatch handler={}", $handler); eprintln!("[vm-trace] length dispatch handler={}", $handler);
} }
}; };
@ -14,15 +12,15 @@ macro_rules! trace_dispatch {
mod arithmetic; mod arithmetic;
mod boxes; mod boxes;
mod boxes_array; mod boxes_array;
mod boxes_string; mod boxes_instance;
mod boxes_map; mod boxes_map;
mod boxes_object_fields; mod boxes_object_fields;
mod boxes_instance;
mod boxes_plugin; mod boxes_plugin;
mod boxes_string;
mod boxes_void_guards; mod boxes_void_guards;
mod calls; mod calls;
mod externals;
mod extern_provider; mod extern_provider;
mod externals;
mod memory; mod memory;
mod misc; mod misc;
@ -90,10 +88,7 @@ impl MirInterpreter {
} }
MirInstruction::DebugLog { message, values } => { MirInstruction::DebugLog { message, values } => {
// Dev-only: MIR-level debug logging (no new values defined). // Dev-only: MIR-level debug logging (no new values defined).
if std::env::var("NYASH_MIR_DEBUG_LOG") if std::env::var("NYASH_MIR_DEBUG_LOG").ok().as_deref() == Some("1") {
.ok()
.as_deref() == Some("1")
{
eprint!("[MIR-LOG] {}:", message); eprint!("[MIR-LOG] {}:", message);
for vid in values { for vid in values {
let v = self.reg_load(*vid).unwrap_or(VMValue::Void); let v = self.reg_load(*vid).unwrap_or(VMValue::Void);

View File

@ -8,10 +8,26 @@ impl MirInterpreter {
match v { match v {
VMValue::Void => "void", VMValue::Void => "void",
VMValue::BoxRef(b) => { VMValue::BoxRef(b) => {
if b.as_any().downcast_ref::<crate::boxes::null_box::NullBox>().is_some() { "null" } if b.as_any()
else if b.as_any().downcast_ref::<crate::boxes::missing_box::MissingBox>().is_some() { "missing" } .downcast_ref::<crate::boxes::null_box::NullBox>()
else if b.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() { "void" } .is_some()
else { "" } {
"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 {
""
}
} }
_ => "", _ => "",
} }
@ -23,11 +39,7 @@ impl MirInterpreter {
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1")
|| std::env::var("NYASH_VM_TRACE_EXEC").ok().as_deref() == Some("1") || std::env::var("NYASH_VM_TRACE_EXEC").ok().as_deref() == Some("1")
{ {
let keys: Vec<String> = self let keys: Vec<String> = self.regs.keys().map(|k| format!("{:?}", k)).collect();
.regs
.keys()
.map(|k| format!("{:?}", k))
.collect();
eprintln!( eprintln!(
"[vm-trace] reg_load undefined id={:?} fn={} last_block={:?} last_inst={:?} regs={}", "[vm-trace] reg_load undefined id={:?} fn={} last_block={:?} last_inst={:?} regs={}",
id, id,
@ -39,8 +51,16 @@ impl MirInterpreter {
} }
// Dev-time safety valve: tolerate undefined registers as Void when enabled // Dev-time safety valve: tolerate undefined registers as Void when enabled
let tolerate = std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1") let tolerate = std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1")
|| std::env::var("HAKO_PHI_VERIFY").ok().map(|v| v.to_ascii_lowercase()).map(|v| v=="1"||v=="true"||v=="on").unwrap_or(false) || std::env::var("HAKO_PHI_VERIFY")
|| std::env::var("NYASH_PHI_VERIFY").ok().map(|v| v.to_ascii_lowercase()).map(|v| v=="1"||v=="true"||v=="on").unwrap_or(false); .ok()
.map(|v| v.to_ascii_lowercase())
.map(|v| v == "1" || v == "true" || v == "on")
.unwrap_or(false)
|| std::env::var("NYASH_PHI_VERIFY")
.ok()
.map(|v| v.to_ascii_lowercase())
.map(|v| v == "1" || v == "true" || v == "on")
.unwrap_or(false);
if tolerate { if tolerate {
return Ok(VMValue::Void); return Ok(VMValue::Void);
} }
@ -87,21 +107,43 @@ impl MirInterpreter {
v v
}; };
(norm(a), norm(b)) (norm(a), norm(b))
} else { (a, b) }; } else {
(a, b)
};
// Dev: nullish trace for binop // Dev: nullish trace for binop
if crate::config::env::null_missing_box_enabled() && Self::box_trace_enabled() { if crate::config::env::null_missing_box_enabled() && Self::box_trace_enabled() {
let (ak, bk) = (crate::backend::abi_util::tag_of_vm(&a), crate::backend::abi_util::tag_of_vm(&b)); let (ak, bk) = (
crate::backend::abi_util::tag_of_vm(&a),
crate::backend::abi_util::tag_of_vm(&b),
);
let (an, bn) = (Self::tag_nullish(&a), Self::tag_nullish(&b)); let (an, bn) = (Self::tag_nullish(&a), Self::tag_nullish(&b));
let op_s = match op { BinaryOp::Add=>"Add", BinaryOp::Sub=>"Sub", BinaryOp::Mul=>"Mul", BinaryOp::Div=>"Div", BinaryOp::Mod=>"Mod", BinaryOp::BitAnd=>"BitAnd", BinaryOp::BitOr=>"BitOr", BinaryOp::BitXor=>"BitXor", BinaryOp::And=>"And", BinaryOp::Or=>"Or", BinaryOp::Shl=>"Shl", BinaryOp::Shr=>"Shr" }; let op_s = match op {
BinaryOp::Add => "Add",
BinaryOp::Sub => "Sub",
BinaryOp::Mul => "Mul",
BinaryOp::Div => "Div",
BinaryOp::Mod => "Mod",
BinaryOp::BitAnd => "BitAnd",
BinaryOp::BitOr => "BitOr",
BinaryOp::BitXor => "BitXor",
BinaryOp::And => "And",
BinaryOp::Or => "Or",
BinaryOp::Shl => "Shl",
BinaryOp::Shr => "Shr",
};
eprintln!("{{\"ev\":\"binop\",\"op\":\"{}\",\"a_k\":\"{}\",\"b_k\":\"{}\",\"a_n\":\"{}\",\"b_n\":\"{}\"}}", op_s, ak, bk, an, bn); eprintln!("{{\"ev\":\"binop\",\"op\":\"{}\",\"a_k\":\"{}\",\"b_k\":\"{}\",\"a_n\":\"{}\",\"b_n\":\"{}\"}}", op_s, ak, bk, an, bn);
} }
Ok(match (op, a, b) { Ok(match (op, a, b) {
// Dev-only safety valves for Add (guarded by tolerance or --dev): // Dev-only safety valves for Add (guarded by tolerance or --dev):
// - Treat Void as 0 for numeric + // - Treat Void as 0 for numeric +
// - Treat Void as empty string for string + // - Treat Void as empty string for string +
(Add, VMValue::Void, Integer(y)) | (Add, Integer(y), VMValue::Void) if tolerate => Integer(y), (Add, VMValue::Void, Integer(y)) | (Add, Integer(y), VMValue::Void) if tolerate => {
Integer(y)
}
(Add, VMValue::Void, Float(y)) | (Add, Float(y), VMValue::Void) if tolerate => Float(y), (Add, VMValue::Void, Float(y)) | (Add, Float(y), VMValue::Void) if tolerate => Float(y),
(Add, String(s), VMValue::Void) | (Add, VMValue::Void, String(s)) if tolerate => String(s), (Add, String(s), VMValue::Void) | (Add, VMValue::Void, String(s)) if tolerate => {
String(s)
}
// Dev-only safety valve for Sub (guarded): treat Void as 0 // Dev-only safety valve for Sub (guarded): treat Void as 0
(Sub, Integer(x), VMValue::Void) if tolerate => Integer(x), (Sub, Integer(x), VMValue::Void) if tolerate => Integer(x),
(Sub, VMValue::Void, Integer(y)) if tolerate => Integer(0 - y), (Sub, VMValue::Void, Integer(y)) if tolerate => Integer(0 - y),
@ -142,7 +184,7 @@ impl MirInterpreter {
return Err(VMError::TypeError(format!( return Err(VMError::TypeError(format!(
"unsupported binop {:?} on {:?} and {:?}", "unsupported binop {:?} on {:?} and {:?}",
opk, va, vb opk, va, vb
))) )));
} }
}) })
} }
@ -162,7 +204,9 @@ impl MirInterpreter {
v v
}; };
(norm(a), norm(b)) (norm(a), norm(b))
} else { (a, b) }; } else {
(a, b)
};
// Dev-only safety valve: tolerate Void in comparisons when enabled or in --dev // Dev-only safety valve: tolerate Void in comparisons when enabled or in --dev
// → treat Void as 0 for numeric, empty for string // → treat Void as 0 for numeric, empty for string
let (a2, b2) = if tolerate { let (a2, b2) = if tolerate {
@ -195,9 +239,19 @@ impl MirInterpreter {
}; };
// Dev: nullish trace for compare // Dev: nullish trace for compare
if crate::config::env::null_missing_box_enabled() && Self::box_trace_enabled() { if crate::config::env::null_missing_box_enabled() && Self::box_trace_enabled() {
let (ak, bk) = (crate::backend::abi_util::tag_of_vm(&a2), crate::backend::abi_util::tag_of_vm(&b2)); let (ak, bk) = (
crate::backend::abi_util::tag_of_vm(&a2),
crate::backend::abi_util::tag_of_vm(&b2),
);
let (an, bn) = (Self::tag_nullish(&a2), Self::tag_nullish(&b2)); let (an, bn) = (Self::tag_nullish(&a2), Self::tag_nullish(&b2));
let op_s = match op { CompareOp::Eq=>"Eq", CompareOp::Ne=>"Ne", CompareOp::Lt=>"Lt", CompareOp::Le=>"Le", CompareOp::Gt=>"Gt", CompareOp::Ge=>"Ge" }; let op_s = match op {
CompareOp::Eq => "Eq",
CompareOp::Ne => "Ne",
CompareOp::Lt => "Lt",
CompareOp::Le => "Le",
CompareOp::Gt => "Gt",
CompareOp::Ge => "Ge",
};
eprintln!("{{\"ev\":\"cmp\",\"op\":\"{}\",\"a_k\":\"{}\",\"b_k\":\"{}\",\"a_n\":\"{}\",\"b_n\":\"{}\"}}", op_s, ak, bk, an, bn); eprintln!("{{\"ev\":\"cmp\",\"op\":\"{}\",\"a_k\":\"{}\",\"b_k\":\"{}\",\"a_n\":\"{}\",\"b_n\":\"{}\"}}", op_s, ak, bk, an, bn);
} }
let result = match (op, &a3, &b3) { let result = match (op, &a3, &b3) {
@ -230,7 +284,6 @@ impl MirInterpreter {
}; };
Ok(result) Ok(result)
} }
} }
// ---- Box trace (dev-only observer) ---- // ---- Box trace (dev-only observer) ----
@ -243,11 +296,15 @@ impl MirInterpreter {
fn box_trace_filter_match(class_name: &str) -> bool { fn box_trace_filter_match(class_name: &str) -> bool {
if let Ok(filt) = std::env::var("NYASH_BOX_TRACE_FILTER") { if let Ok(filt) = std::env::var("NYASH_BOX_TRACE_FILTER") {
let want = filt.trim(); let want = filt.trim();
if want.is_empty() { return true; } if want.is_empty() {
return true;
}
// comma/space separated tokens; match if any token is contained in class // comma/space separated tokens; match if any token is contained in class
for tok in want.split(|c: char| c == ',' || c.is_whitespace()) { for tok in want.split(|c: char| c == ',' || c.is_whitespace()) {
let t = tok.trim(); let t = tok.trim();
if !t.is_empty() && class_name.contains(t) { return true; } if !t.is_empty() && class_name.contains(t) {
return true;
}
} }
false false
} else { } else {
@ -272,34 +329,49 @@ impl MirInterpreter {
} }
pub(super) fn box_trace_emit_new(&self, class_name: &str, argc: usize) { pub(super) fn box_trace_emit_new(&self, class_name: &str, argc: usize) {
if !Self::box_trace_enabled() || !Self::box_trace_filter_match(class_name) { return; } if !Self::box_trace_enabled() || !Self::box_trace_filter_match(class_name) {
return;
}
eprintln!( eprintln!(
"{{\"ev\":\"new\",\"class\":\"{}\",\"argc\":{}}}", "{{\"ev\":\"new\",\"class\":\"{}\",\"argc\":{}}}",
Self::json_escape(class_name), argc Self::json_escape(class_name),
argc
); );
} }
pub(super) fn box_trace_emit_call(&self, class_name: &str, method: &str, argc: usize) { pub(super) fn box_trace_emit_call(&self, class_name: &str, method: &str, argc: usize) {
if !Self::box_trace_enabled() || !Self::box_trace_filter_match(class_name) { return; } if !Self::box_trace_enabled() || !Self::box_trace_filter_match(class_name) {
return;
}
eprintln!( eprintln!(
"{{\"ev\":\"call\",\"class\":\"{}\",\"method\":\"{}\",\"argc\":{}}}", "{{\"ev\":\"call\",\"class\":\"{}\",\"method\":\"{}\",\"argc\":{}}}",
Self::json_escape(class_name), Self::json_escape(method), argc Self::json_escape(class_name),
Self::json_escape(method),
argc
); );
} }
pub(super) fn box_trace_emit_get(&self, class_name: &str, field: &str, val_kind: &str) { pub(super) fn box_trace_emit_get(&self, class_name: &str, field: &str, val_kind: &str) {
if !Self::box_trace_enabled() || !Self::box_trace_filter_match(class_name) { return; } if !Self::box_trace_enabled() || !Self::box_trace_filter_match(class_name) {
return;
}
eprintln!( eprintln!(
"{{\"ev\":\"get\",\"class\":\"{}\",\"field\":\"{}\",\"val\":\"{}\"}}", "{{\"ev\":\"get\",\"class\":\"{}\",\"field\":\"{}\",\"val\":\"{}\"}}",
Self::json_escape(class_name), Self::json_escape(field), Self::json_escape(val_kind) Self::json_escape(class_name),
Self::json_escape(field),
Self::json_escape(val_kind)
); );
} }
pub(super) fn box_trace_emit_set(&self, class_name: &str, field: &str, val_kind: &str) { pub(super) fn box_trace_emit_set(&self, class_name: &str, field: &str, val_kind: &str) {
if !Self::box_trace_enabled() || !Self::box_trace_filter_match(class_name) { return; } if !Self::box_trace_enabled() || !Self::box_trace_filter_match(class_name) {
return;
}
eprintln!( eprintln!(
"{{\"ev\":\"set\",\"class\":\"{}\",\"field\":\"{}\",\"val\":\"{}\"}}", "{{\"ev\":\"set\",\"class\":\"{}\",\"field\":\"{}\",\"val\":\"{}\"}}",
Self::json_escape(class_name), Self::json_escape(field), Self::json_escape(val_kind) Self::json_escape(class_name),
Self::json_escape(field),
Self::json_escape(val_kind)
); );
} }
} }
@ -312,7 +384,9 @@ impl MirInterpreter {
} }
pub(super) fn print_trace_emit(&self, val: &VMValue) { pub(super) fn print_trace_emit(&self, val: &VMValue) {
if !Self::print_trace_enabled() { return; } if !Self::print_trace_enabled() {
return;
}
let (kind, class, nullish) = match val { let (kind, class, nullish) = match val {
VMValue::Integer(_) => ("Integer", "".to_string(), None), VMValue::Integer(_) => ("Integer", "".to_string(), None),
VMValue::Float(_) => ("Float", "".to_string(), None), VMValue::Float(_) => ("Float", "".to_string(), None),
@ -324,17 +398,43 @@ impl MirInterpreter {
// Prefer InstanceBox.class_name when available // Prefer InstanceBox.class_name when available
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
let tag = if crate::config::env::null_missing_box_enabled() { let tag = if crate::config::env::null_missing_box_enabled() {
if b.as_any().downcast_ref::<crate::boxes::null_box::NullBox>().is_some() { Some("null") } if b.as_any()
else if b.as_any().downcast_ref::<crate::boxes::missing_box::MissingBox>().is_some() { Some("missing") } .downcast_ref::<crate::boxes::null_box::NullBox>()
else { None } .is_some()
} else { None }; {
Some("null")
} else if b
.as_any()
.downcast_ref::<crate::boxes::missing_box::MissingBox>()
.is_some()
{
Some("missing")
} else {
None
}
} else {
None
};
("BoxRef", inst.class_name.clone(), tag) ("BoxRef", inst.class_name.clone(), tag)
} else { } else {
let tag = if crate::config::env::null_missing_box_enabled() { let tag = if crate::config::env::null_missing_box_enabled() {
if b.as_any().downcast_ref::<crate::boxes::null_box::NullBox>().is_some() { Some("null") } if b.as_any()
else if b.as_any().downcast_ref::<crate::boxes::missing_box::MissingBox>().is_some() { Some("missing") } .downcast_ref::<crate::boxes::null_box::NullBox>()
else { None } .is_some()
} else { None }; {
Some("null")
} else if b
.as_any()
.downcast_ref::<crate::boxes::missing_box::MissingBox>()
.is_some()
{
Some("missing")
} else {
None
}
} else {
None
};
("BoxRef", b.type_name().to_string(), tag) ("BoxRef", b.type_name().to_string(), tag)
} }
} }

View File

@ -12,8 +12,8 @@
*/ */
use super::{MirFunction, MirInterpreter}; use super::{MirFunction, MirInterpreter};
use serde_json::json;
use crate::backend::vm::{VMError, VMValue}; use crate::backend::vm::{VMError, VMValue};
use serde_json::json;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct ParsedSig<'a> { struct ParsedSig<'a> {
@ -25,16 +25,25 @@ struct ParsedSig<'a> {
fn parse_method_signature(name: &str) -> Option<ParsedSig<'_>> { fn parse_method_signature(name: &str) -> Option<ParsedSig<'_>> {
let dot = name.find('.')?; let dot = name.find('.')?;
let slash = name.rfind('/')?; let slash = name.rfind('/')?;
if dot >= slash { return None; } if dot >= slash {
return None;
}
let class = &name[..dot]; let class = &name[..dot];
let method = &name[dot + 1..slash]; let method = &name[dot + 1..slash];
let arity_str = &name[slash + 1..]; let arity_str = &name[slash + 1..];
Some(ParsedSig { class, method, arity_str }) Some(ParsedSig {
class,
method,
arity_str,
})
} }
fn extract_instance_box_class(arg0: &VMValue) -> Option<String> { fn extract_instance_box_class(arg0: &VMValue) -> Option<String> {
if let VMValue::BoxRef(bx) = arg0 { if let VMValue::BoxRef(bx) = arg0 {
if let Some(inst) = bx.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = bx
.as_any()
.downcast_ref::<crate::instance_v2::InstanceBox>()
{
return Some(inst.class_name.clone()); return Some(inst.class_name.clone());
} }
} }
@ -47,7 +56,12 @@ fn reroute_to_correct_method(
parsed: &ParsedSig<'_>, parsed: &ParsedSig<'_>,
arg_vals: Option<&[VMValue]>, arg_vals: Option<&[VMValue]>,
) -> Option<Result<VMValue, VMError>> { ) -> Option<Result<VMValue, VMError>> {
let target = format!("{}.{}{}", recv_cls, parsed.method, format!("/{}", parsed.arity_str)); let target = format!(
"{}.{}{}",
recv_cls,
parsed.method,
format!("/{}", parsed.arity_str)
);
if let Some(f) = interp.functions.get(&target).cloned() { if let Some(f) = interp.functions.get(&target).cloned() {
// Debug: emit class-reroute event (dev-only) // Debug: emit class-reroute event (dev-only)
crate::debug::hub::emit( crate::debug::hub::emit(
@ -149,7 +163,10 @@ fn try_special_method(
if parsed.method == "is_eof" && parsed.arity_str == "0" { if parsed.method == "is_eof" && parsed.arity_str == "0" {
if let Some(args) = arg_vals { if let Some(args) = arg_vals {
if let VMValue::BoxRef(bx) = &args[0] { if let VMValue::BoxRef(bx) = &args[0] {
if let Some(inst) = bx.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() { if let Some(inst) = bx
.as_any()
.downcast_ref::<crate::instance_v2::InstanceBox>()
{
if recv_cls == "JsonToken" { if recv_cls == "JsonToken" {
let is = match inst.get_field_ng("type") { let is = match inst.get_field_ng("type") {
Some(crate::value::NyashValue::String(ref s)) => s == "EOF", Some(crate::value::NyashValue::String(ref s)) => s == "EOF",
@ -158,8 +175,14 @@ fn try_special_method(
return Some(Ok(VMValue::Bool(is))); return Some(Ok(VMValue::Bool(is)));
} }
if recv_cls == "JsonScanner" { if recv_cls == "JsonScanner" {
let pos = match inst.get_field_ng("position") { Some(crate::value::NyashValue::Integer(i)) => i, _ => 0 }; let pos = match inst.get_field_ng("position") {
let len = match inst.get_field_ng("length") { Some(crate::value::NyashValue::Integer(i)) => i, _ => 0 }; Some(crate::value::NyashValue::Integer(i)) => i,
_ => 0,
};
let len = match inst.get_field_ng("length") {
Some(crate::value::NyashValue::Integer(i)) => i,
_ => 0,
};
return Some(Ok(VMValue::Bool(pos >= len))); return Some(Ok(VMValue::Bool(pos >= len)));
} }
} }
@ -183,16 +206,35 @@ pub(super) fn pre_exec_reroute(
func: &MirFunction, func: &MirFunction,
arg_vals: Option<&[VMValue]>, arg_vals: Option<&[VMValue]>,
) -> Option<Result<VMValue, VMError>> { ) -> Option<Result<VMValue, VMError>> {
let args = match arg_vals { Some(a) => a, None => return None }; let args = match arg_vals {
if args.is_empty() { return None; } Some(a) => a,
let parsed = match parse_method_signature(func.signature.name.as_str()) { Some(p) => p, None => return None }; None => return None,
let recv_cls = match extract_instance_box_class(&args[0]) { Some(c) => c, None => return None }; };
if args.is_empty() {
return None;
}
let parsed = match parse_method_signature(func.signature.name.as_str()) {
Some(p) => p,
None => return None,
};
let recv_cls = match extract_instance_box_class(&args[0]) {
Some(c) => c,
None => return None,
};
// Always consider special re-routes (e.g., toString→stringify) even when class matches // Always consider special re-routes (e.g., toString→stringify) even when class matches
if let Some(r) = try_special_reroute(interp, &recv_cls, &parsed, arg_vals) { return Some(r); } if let Some(r) = try_special_reroute(interp, &recv_cls, &parsed, arg_vals) {
if recv_cls == parsed.class { return None; } return Some(r);
}
if recv_cls == parsed.class {
return None;
}
// Class mismatch: reroute to same method on the receiver's class // Class mismatch: reroute to same method on the receiver's class
if let Some(r) = reroute_to_correct_method(interp, &recv_cls, &parsed, arg_vals) { return Some(r); } if let Some(r) = reroute_to_correct_method(interp, &recv_cls, &parsed, arg_vals) {
return Some(r);
}
// Narrow special fallback (e.g., is_eof) // Narrow special fallback (e.g., is_eof)
if let Some(r) = try_special_method(&recv_cls, &parsed, arg_vals) { return Some(r); } if let Some(r) = try_special_method(&recv_cls, &parsed, arg_vals) {
return Some(r);
}
None None
} }

View File

@ -71,20 +71,32 @@ impl MirInterpreter {
} }
/// Register static box declarations (called from vm.rs during setup) /// Register static box declarations (called from vm.rs during setup)
pub fn register_static_box_decl(&mut self, name: String, decl: crate::core::model::BoxDeclaration) { pub fn register_static_box_decl(
&mut self,
name: String,
decl: crate::core::model::BoxDeclaration,
) {
self.static_box_decls.insert(name, decl); self.static_box_decls.insert(name, decl);
} }
/// Ensure static box singleton instance exists, create if not /// Ensure static box singleton instance exists, create if not
/// Returns mutable reference to the singleton instance /// Returns mutable reference to the singleton instance
fn ensure_static_box_instance(&mut self, box_name: &str) -> Result<&mut crate::instance_v2::InstanceBox, VMError> { fn ensure_static_box_instance(
&mut self,
box_name: &str,
) -> Result<&mut crate::instance_v2::InstanceBox, VMError> {
// Check if instance already exists // Check if instance already exists
if !self.static_boxes.contains_key(box_name) { if !self.static_boxes.contains_key(box_name) {
// Get declaration // Get declaration
let decl = self.static_box_decls.get(box_name) let decl = self
.ok_or_else(|| VMError::InvalidInstruction( .static_box_decls
format!("static box declaration not found: {}", box_name) .get(box_name)
))? .ok_or_else(|| {
VMError::InvalidInstruction(format!(
"static box declaration not found: {}",
box_name
))
})?
.clone(); .clone();
// Create instance from declaration // Create instance from declaration
@ -97,15 +109,20 @@ impl MirInterpreter {
self.static_boxes.insert(box_name.to_string(), instance); self.static_boxes.insert(box_name.to_string(), instance);
if std::env::var("NYASH_VM_STATIC_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_VM_STATIC_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-static] created singleton instance for static box: {}", box_name); eprintln!(
"[vm-static] created singleton instance for static box: {}",
box_name
);
} }
} }
// Return mutable reference // Return mutable reference
self.static_boxes.get_mut(box_name) self.static_boxes.get_mut(box_name).ok_or_else(|| {
.ok_or_else(|| VMError::InvalidInstruction( VMError::InvalidInstruction(format!(
format!("static box instance not found after creation: {}", box_name) "static box instance not found after creation: {}",
box_name
)) ))
})
} }
/// Check if a function name represents a static box method /// Check if a function name represents a static box method
@ -169,7 +186,12 @@ impl MirInterpreter {
// Build helpful error message // Build helpful error message
let mut names: Vec<&String> = module.functions.keys().collect(); let mut names: Vec<&String> = module.functions.keys().collect();
names.sort(); names.sort();
let avail = names.into_iter().take(12).cloned().collect::<Vec<_>>().join(", "); let avail = names
.into_iter()
.take(12)
.cloned()
.collect::<Vec<_>>()
.join(", ");
let tried = candidates.join(", "); let tried = candidates.join(", ");
let msg = format!( let msg = format!(
"entry function not found. searched: [{}]. available: [{}]. hint: define 'static box Main {{ method main(args){{ ... }} }}' or set NYASH_ENTRY=Name", "entry function not found. searched: [{}]. available: [{}]. hint: define 'static box Main {{ method main(args){{ ... }} }}' or set NYASH_ENTRY=Name",
@ -200,9 +222,13 @@ impl MirInterpreter {
argv_list = out; argv_list = out;
} }
} else if let Ok(s) = std::env::var("NYASH_SCRIPT_ARGS_JSON") { } else if let Ok(s) = std::env::var("NYASH_SCRIPT_ARGS_JSON") {
if let Ok(v) = serde_json::from_str::<Vec<String>>(&s) { argv_list = v; } if let Ok(v) = serde_json::from_str::<Vec<String>>(&s) {
argv_list = v;
}
} else if let Ok(s) = std::env::var("NYASH_ARGV") { } else if let Ok(s) = std::env::var("NYASH_ARGV") {
if let Ok(v) = serde_json::from_str::<Vec<String>>(&s) { argv_list = v; } if let Ok(v) = serde_json::from_str::<Vec<String>>(&s) {
argv_list = v;
}
} }
// Construct ArrayBox of StringBox // Construct ArrayBox of StringBox
let array = crate::boxes::array::ArrayBox::new(); let array = crate::boxes::array::ArrayBox::new();

View File

@ -3,8 +3,8 @@
//! レジスタ値の読み込み+型変換チェーンを統一します。 //! レジスタ値の読み込み+型変換チェーンを統一します。
use super::super::*; use super::super::*;
use crate::mir::ValueId;
use crate::box_trait::NyashBox; use crate::box_trait::NyashBox;
use crate::mir::ValueId;
impl MirInterpreter { impl MirInterpreter {
/// レジスタ値をBox<dyn NyashBox>として読み込む /// レジスタ値をBox<dyn NyashBox>として読み込む
@ -53,7 +53,13 @@ impl MirInterpreter {
VMValue::Bool(_) => "Bool", VMValue::Bool(_) => "Bool",
VMValue::String(_) => "String", VMValue::String(_) => "String",
VMValue::Void => "Void", VMValue::Void => "Void",
VMValue::BoxRef(b) => return Err(self.err_type_mismatch("load_as_int", "Integer", &b.type_name())), VMValue::BoxRef(b) => {
return Err(self.err_type_mismatch(
"load_as_int",
"Integer",
&b.type_name(),
))
}
VMValue::Future(_) => "Future", VMValue::Future(_) => "Future",
}; };
Err(self.err_type_mismatch("load_as_int", "Integer", type_name)) Err(self.err_type_mismatch("load_as_int", "Integer", type_name))
@ -83,7 +89,9 @@ impl MirInterpreter {
VMValue::Bool(_) => "Bool", VMValue::Bool(_) => "Bool",
VMValue::String(_) => "String", VMValue::String(_) => "String",
VMValue::Void => "Void", VMValue::Void => "Void",
VMValue::BoxRef(b) => return Err(self.err_type_mismatch("load_as_bool", "Bool", &b.type_name())), VMValue::BoxRef(b) => {
return Err(self.err_type_mismatch("load_as_bool", "Bool", &b.type_name()))
}
VMValue::Future(_) => "Future", VMValue::Future(_) => "Future",
}; };
Err(self.err_type_mismatch("load_as_bool", "Bool", type_name)) Err(self.err_type_mismatch("load_as_bool", "Bool", type_name))

View File

@ -42,7 +42,10 @@ impl ErrorBuilder {
#[inline] #[inline]
#[allow(dead_code)] #[allow(dead_code)]
pub fn type_mismatch(method: &str, expected: &str, actual: &str) -> VMError { pub fn type_mismatch(method: &str, expected: &str, actual: &str) -> VMError {
VMError::InvalidInstruction(format!("{} expects {} type, got {}", method, expected, actual)) VMError::InvalidInstruction(format!(
"{} expects {} type, got {}",
method, expected, actual
))
} }
/// Index out of bounds error /// Index out of bounds error
@ -60,7 +63,10 @@ impl ErrorBuilder {
#[inline] #[inline]
#[allow(dead_code)] #[allow(dead_code)]
pub fn out_of_bounds(method: &str, index: usize, len: usize) -> VMError { pub fn out_of_bounds(method: &str, index: usize, len: usize) -> VMError {
VMError::InvalidInstruction(format!("{} index out of bounds: {} >= {}", method, index, len)) VMError::InvalidInstruction(format!(
"{} index out of bounds: {} >= {}",
method, index, len
))
} }
/// Unsupported operation error /// Unsupported operation error

View File

@ -1,11 +1,11 @@
//! MIR Interpreter共通ユーティリティ //! MIR Interpreter共通ユーティリティ
pub mod destination_helpers;
pub mod arg_validation; pub mod arg_validation;
pub mod receiver_helpers;
pub mod error_helpers;
pub mod conversion_helpers; pub mod conversion_helpers;
pub mod destination_helpers;
pub mod error_helpers;
pub mod naming; pub mod naming;
pub mod receiver_helpers;
// 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
// Selective re-export (only naming is widely used via utils::normalize_arity_suffix) // Selective re-export (only naming is widely used via utils::normalize_arity_suffix)

View File

@ -12,4 +12,3 @@ pub fn normalize_arity_suffix(name: &str) -> &str {
None => name, None => name,
} }
} }

View File

@ -23,9 +23,7 @@ impl MirInterpreter {
let receiver_value = self.reg_load(receiver)?; let receiver_value = self.reg_load(receiver)?;
match receiver_value { match receiver_value {
VMValue::BoxRef(b) => Ok(b), VMValue::BoxRef(b) => Ok(b),
_ => Err(VMError::InvalidInstruction( _ => Err(VMError::InvalidInstruction("receiver must be Box".into())),
"receiver must be Box".into(),
)),
} }
} }
} }

View File

@ -7,5 +7,5 @@
// Re-export all arithmetic operations from the dedicated arithmetic module // Re-export all arithmetic operations from the dedicated arithmetic module
pub use crate::boxes::arithmetic::{ pub use crate::boxes::arithmetic::{
AddBox, SubtractBox, MultiplyBox, DivideBox, ModuloBox, CompareBox, AddBox, CompareBox, DivideBox, ModuloBox, MultiplyBox, SubtractBox,
}; };

View File

@ -5,8 +5,8 @@
* 🎯 Phase 2.4: Delete this file to remove builtin ArrayBox support * 🎯 Phase 2.4: Delete this file to remove builtin ArrayBox support
*/ */
use crate::box_trait::NyashBox;
use crate::box_factory::RuntimeError; use crate::box_factory::RuntimeError;
use crate::box_trait::NyashBox;
/// Create builtin ArrayBox instance /// Create builtin ArrayBox instance
/// ///

View File

@ -5,8 +5,8 @@
* 🎯 Phase 2.3: Delete this file to remove builtin BoolBox support * 🎯 Phase 2.3: Delete this file to remove builtin BoolBox support
*/ */
use crate::box_trait::{NyashBox, BoolBox};
use crate::box_factory::RuntimeError; use crate::box_factory::RuntimeError;
use crate::box_trait::{BoolBox, NyashBox};
/// Create builtin BoolBox instance /// Create builtin BoolBox instance
/// ///

View File

@ -5,8 +5,8 @@
* 🎯 Phase 2.6: Delete this file to remove builtin ConsoleBox support (LAST) * 🎯 Phase 2.6: Delete this file to remove builtin ConsoleBox support (LAST)
*/ */
use crate::box_trait::NyashBox;
use crate::box_factory::RuntimeError; use crate::box_factory::RuntimeError;
use crate::box_trait::NyashBox;
/// Create builtin ConsoleBox instance /// Create builtin ConsoleBox instance
/// ///

View File

@ -7,8 +7,8 @@
* 🎯 Core-ro mode: This is used directly * 🎯 Core-ro mode: This is used directly
*/ */
use crate::box_trait::NyashBox;
use crate::box_factory::RuntimeError; use crate::box_factory::RuntimeError;
use crate::box_trait::NyashBox;
use crate::boxes::file::core_ro::CoreRoFileIo; use crate::boxes::file::core_ro::CoreRoFileIo;
use crate::boxes::file::FileBox; use crate::boxes::file::FileBox;
use std::sync::Arc; use std::sync::Arc;
@ -28,9 +28,7 @@ pub fn create(_args: &[Box<dyn NyashBox>]) -> Result<Box<dyn NyashBox>, RuntimeE
}); });
} }
eprintln!( eprintln!("[FileBox] Using builtin core-ro fallback implementation");
"[FileBox] Using builtin core-ro fallback implementation"
);
// Create FileBox with core-ro provider directly // Create FileBox with core-ro provider directly
// Don't rely on global provider_lock which may not be initialized // Don't rely on global provider_lock which may not be initialized

View File

@ -5,8 +5,8 @@
* 🎯 Phase 2.2: Delete this file to remove builtin IntegerBox support * 🎯 Phase 2.2: Delete this file to remove builtin IntegerBox support
*/ */
use crate::box_trait::{NyashBox, IntegerBox};
use crate::box_factory::RuntimeError; use crate::box_factory::RuntimeError;
use crate::box_trait::{IntegerBox, NyashBox};
/// Create builtin IntegerBox instance /// Create builtin IntegerBox instance
/// ///

View File

@ -5,8 +5,8 @@
* 🎯 Phase 2.5: Delete this file to remove builtin MapBox support * 🎯 Phase 2.5: Delete this file to remove builtin MapBox support
*/ */
use crate::box_trait::NyashBox;
use crate::box_factory::RuntimeError; use crate::box_factory::RuntimeError;
use crate::box_trait::NyashBox;
/// Create builtin MapBox instance /// Create builtin MapBox instance
/// ///

View File

@ -15,12 +15,12 @@
*/ */
// Phase 2.1-2.6: Delete these modules one by one // Phase 2.1-2.6: Delete these modules one by one
pub mod string_box; // DELETE: Phase 2.1 (plugin ready)
pub mod integer_box; // DELETE: Phase 2.2 (plugin ready)
pub mod bool_box; // DELETE: Phase 2.3 (plugin needed)
pub mod array_box; // DELETE: Phase 2.4 (plugin check) pub mod array_box; // DELETE: Phase 2.4 (plugin check)
pub mod bool_box; // DELETE: Phase 2.3 (plugin needed)
pub mod console_box;
pub mod integer_box; // DELETE: Phase 2.2 (plugin ready)
pub mod map_box; // DELETE: Phase 2.5 (plugin check) pub mod map_box; // DELETE: Phase 2.5 (plugin check)
pub mod console_box; // DELETE: Phase 2.6 (LAST - critical for logging) pub mod string_box; // DELETE: Phase 2.1 (plugin ready) // DELETE: Phase 2.6 (LAST - critical for logging)
// Fallback support (Phase 15.5: Fallback Guarantee) // Fallback support (Phase 15.5: Fallback Guarantee)
pub mod file_box; // FALLBACK: Core-ro FileBox for auto/core-ro modes pub mod file_box; // FALLBACK: Core-ro FileBox for auto/core-ro modes

View File

@ -5,8 +5,8 @@
* 📋 Discussion needed: Is null a language primitive or plugin concern? * 📋 Discussion needed: Is null a language primitive or plugin concern?
*/ */
use crate::box_trait::NyashBox;
use crate::box_factory::RuntimeError; use crate::box_factory::RuntimeError;
use crate::box_trait::NyashBox;
/// Create builtin NullBox instance /// Create builtin NullBox instance
/// ///

View File

@ -5,8 +5,8 @@
* 🎯 Phase 2.1: Delete this file to remove builtin StringBox support * 🎯 Phase 2.1: Delete this file to remove builtin StringBox support
*/ */
use crate::box_trait::{NyashBox, StringBox};
use crate::box_factory::RuntimeError; use crate::box_factory::RuntimeError;
use crate::box_trait::{NyashBox, StringBox};
/// Create builtin StringBox instance /// Create builtin StringBox instance
/// ///

View File

@ -138,7 +138,10 @@ impl UnifiedBoxRegistry {
Some("strict_plugin_first") | _ => FactoryPolicy::StrictPluginFirst, // Phase 15.5: Plugin First DEFAULT! Some("strict_plugin_first") | _ => FactoryPolicy::StrictPluginFirst, // Phase 15.5: Plugin First DEFAULT!
}; };
eprintln!("[UnifiedBoxRegistry] 🎯 Factory Policy: {:?} (Phase 15.5: Everything is Plugin!)", policy); eprintln!(
"[UnifiedBoxRegistry] 🎯 Factory Policy: {:?} (Phase 15.5: Everything is Plugin!)",
policy
);
Self::with_policy(policy) Self::with_policy(policy)
} }
@ -271,7 +274,9 @@ impl UnifiedBoxRegistry {
// Prefer plugin-builtins when enabled and provider is available in v2 registry // Prefer plugin-builtins when enabled and provider is available in v2 registry
// BUT: Skip if plugins are explicitly disabled // BUT: Skip if plugins are explicitly disabled
let plugins_disabled = std::env::var("NYASH_DISABLE_PLUGINS").ok().as_deref() == Some("1"); let plugins_disabled = std::env::var("NYASH_DISABLE_PLUGINS").ok().as_deref() == Some("1");
if !plugins_disabled && std::env::var("NYASH_USE_PLUGIN_BUILTINS").ok().as_deref() == Some("1") { if !plugins_disabled
&& std::env::var("NYASH_USE_PLUGIN_BUILTINS").ok().as_deref() == Some("1")
{
use crate::runtime::{get_global_registry, BoxProvider}; use crate::runtime::{get_global_registry, BoxProvider};
// Allowlist types for override: env NYASH_PLUGIN_OVERRIDE_TYPES="ArrayBox,MapBox" (default: none) // Allowlist types for override: env NYASH_PLUGIN_OVERRIDE_TYPES="ArrayBox,MapBox" (default: none)
let allow: Vec<String> = if let Ok(list) = std::env::var("NYASH_PLUGIN_OVERRIDE_TYPES") let allow: Vec<String> = if let Ok(list) = std::env::var("NYASH_PLUGIN_OVERRIDE_TYPES")
@ -338,7 +343,8 @@ impl UnifiedBoxRegistry {
Err(e) => { Err(e) => {
// FileBox special case: handle fallback based on mode // FileBox special case: handle fallback based on mode
if name == "FileBox" { if name == "FileBox" {
if let Some(fallback_result) = self.try_filebox_fallback(name, args, &e) { if let Some(fallback_result) = self.try_filebox_fallback(name, args, &e)
{
return fallback_result; return fallback_result;
} }
} }
@ -386,14 +392,23 @@ impl UnifiedBoxRegistry {
match mode { match mode {
provider_registry::FileBoxMode::PluginOnly => { provider_registry::FileBoxMode::PluginOnly => {
// Fail-Fast: return the original error immediately // Fail-Fast: return the original error immediately
eprintln!("[FileBox] Plugin creation failed in plugin-only mode: {}", original_error); eprintln!(
"[FileBox] Plugin creation failed in plugin-only mode: {}",
original_error
);
Some(Err(RuntimeError::InvalidOperation { Some(Err(RuntimeError::InvalidOperation {
message: format!("FileBox plugin creation failed (plugin-only mode): {}", original_error), message: format!(
"FileBox plugin creation failed (plugin-only mode): {}",
original_error
),
})) }))
} }
provider_registry::FileBoxMode::Auto => { provider_registry::FileBoxMode::Auto => {
// Auto mode: try fallback to builtin/core-ro // Auto mode: try fallback to builtin/core-ro
eprintln!("[FileBox] Plugin creation failed, falling back to builtin/core-ro: {}", original_error); eprintln!(
"[FileBox] Plugin creation failed, falling back to builtin/core-ro: {}",
original_error
);
// Try builtin factory if available // Try builtin factory if available
for factory in &self.factories { for factory in &self.factories {

View File

@ -6,8 +6,8 @@
*/ */
use super::BoxFactory; use super::BoxFactory;
use crate::box_trait::NyashBox;
use super::RuntimeError; use super::RuntimeError;
use crate::box_trait::NyashBox;
use crate::runtime::get_global_registry; use crate::runtime::get_global_registry;
/// Factory for plugin-based Box types /// Factory for plugin-based Box types
@ -29,11 +29,20 @@ impl BoxFactory for PluginBoxFactory {
) -> Result<Box<dyn NyashBox>, RuntimeError> { ) -> Result<Box<dyn NyashBox>, RuntimeError> {
// Check if plugins are disabled // Check if plugins are disabled
let plugins_disabled = std::env::var("NYASH_DISABLE_PLUGINS").ok().as_deref() == Some("1"); let plugins_disabled = std::env::var("NYASH_DISABLE_PLUGINS").ok().as_deref() == Some("1");
eprintln!("[PluginBoxFactory] NYASH_DISABLE_PLUGINS={} for {}", eprintln!(
std::env::var("NYASH_DISABLE_PLUGINS").ok().as_deref().unwrap_or("not set"), name); "[PluginBoxFactory] NYASH_DISABLE_PLUGINS={} for {}",
std::env::var("NYASH_DISABLE_PLUGINS")
.ok()
.as_deref()
.unwrap_or("not set"),
name
);
if plugins_disabled { if plugins_disabled {
return Err(RuntimeError::InvalidOperation { return Err(RuntimeError::InvalidOperation {
message: format!("Plugins disabled (NYASH_DISABLE_PLUGINS=1), cannot create {}", name), message: format!(
"Plugins disabled (NYASH_DISABLE_PLUGINS=1), cannot create {}",
name
),
}); });
} }

View File

@ -6,9 +6,9 @@
*/ */
use super::BoxFactory; use super::BoxFactory;
use super::{RuntimeError, SharedState};
use crate::box_trait::NyashBox; use crate::box_trait::NyashBox;
use crate::instance_v2::InstanceBox; use crate::instance_v2::InstanceBox;
use super::{RuntimeError, SharedState};
/// Factory for user-defined Box types /// Factory for user-defined Box types
pub struct UserDefinedBoxFactory { pub struct UserDefinedBoxFactory {

View File

@ -16,16 +16,14 @@
use crate::box_trait::{BoolBox, IntegerBox, NyashBox, StringBox}; use crate::box_trait::{BoolBox, IntegerBox, NyashBox, StringBox};
use crate::boxes::FloatBox; use crate::boxes::FloatBox;
use crate::operator_traits::{ use crate::operator_traits::{DynamicAdd, DynamicDiv, DynamicMul, DynamicSub, OperatorError};
DynamicAdd, DynamicDiv, DynamicMul, DynamicSub, OperatorError,
};
// Phase 1-2: Import macros, helpers, and static implementations from separate modules // Phase 1-2: Import macros, helpers, and static implementations from separate modules
mod macros;
mod helpers; mod helpers;
mod macros;
mod static_ops; mod static_ops;
pub use helpers::{concat_result, can_repeat}; pub use helpers::{can_repeat, concat_result};
pub use macros::impl_static_numeric_ops; pub use macros::impl_static_numeric_ops;
// Phase 2: Static implementations are now in static_ops.rs // Phase 2: Static implementations are now in static_ops.rs
@ -392,16 +390,27 @@ impl OperatorResolver {
#[inline] #[inline]
fn try_dyn_left_add(left: &dyn NyashBox, right: &dyn NyashBox) -> Option<Box<dyn NyashBox>> { fn try_dyn_left_add(left: &dyn NyashBox, right: &dyn NyashBox) -> Option<Box<dyn NyashBox>> {
if let Some(int_box) = left.as_any().downcast_ref::<crate::box_trait::IntegerBox>() { if let Some(int_box) = left.as_any().downcast_ref::<crate::box_trait::IntegerBox>() {
if let Some(result) = int_box.try_add(right) { return Some(result); } if let Some(result) = int_box.try_add(right) {
return Some(result);
}
} }
if let Some(str_box) = left.as_any().downcast_ref::<crate::box_trait::StringBox>() { if let Some(str_box) = left.as_any().downcast_ref::<crate::box_trait::StringBox>() {
if let Some(result) = str_box.try_add(right) { return Some(result); } if let Some(result) = str_box.try_add(right) {
return Some(result);
}
}
if let Some(float_box) = left
.as_any()
.downcast_ref::<crate::boxes::math_box::FloatBox>()
{
if let Some(result) = float_box.try_add(right) {
return Some(result);
} }
if let Some(float_box) = left.as_any().downcast_ref::<crate::boxes::math_box::FloatBox>() {
if let Some(result) = float_box.try_add(right) { return Some(result); }
} }
if let Some(bool_box) = left.as_any().downcast_ref::<crate::box_trait::BoolBox>() { if let Some(bool_box) = left.as_any().downcast_ref::<crate::box_trait::BoolBox>() {
if let Some(result) = bool_box.try_add(right) { return Some(result); } if let Some(result) = bool_box.try_add(right) {
return Some(result);
}
} }
None None
} }
@ -409,13 +418,22 @@ impl OperatorResolver {
#[inline] #[inline]
fn try_dyn_left_sub(left: &dyn NyashBox, right: &dyn NyashBox) -> Option<Box<dyn NyashBox>> { fn try_dyn_left_sub(left: &dyn NyashBox, right: &dyn NyashBox) -> Option<Box<dyn NyashBox>> {
if let Some(int_box) = left.as_any().downcast_ref::<crate::box_trait::IntegerBox>() { if let Some(int_box) = left.as_any().downcast_ref::<crate::box_trait::IntegerBox>() {
if let Some(result) = int_box.try_sub(right) { return Some(result); } if let Some(result) = int_box.try_sub(right) {
return Some(result);
}
}
if let Some(float_box) = left
.as_any()
.downcast_ref::<crate::boxes::math_box::FloatBox>()
{
if let Some(result) = float_box.try_sub(right) {
return Some(result);
} }
if let Some(float_box) = left.as_any().downcast_ref::<crate::boxes::math_box::FloatBox>() {
if let Some(result) = float_box.try_sub(right) { return Some(result); }
} }
if let Some(bool_box) = left.as_any().downcast_ref::<crate::box_trait::BoolBox>() { if let Some(bool_box) = left.as_any().downcast_ref::<crate::box_trait::BoolBox>() {
if let Some(result) = bool_box.try_sub(right) { return Some(result); } if let Some(result) = bool_box.try_sub(right) {
return Some(result);
}
} }
None None
} }
@ -423,16 +441,27 @@ impl OperatorResolver {
#[inline] #[inline]
fn try_dyn_left_mul(left: &dyn NyashBox, right: &dyn NyashBox) -> Option<Box<dyn NyashBox>> { fn try_dyn_left_mul(left: &dyn NyashBox, right: &dyn NyashBox) -> Option<Box<dyn NyashBox>> {
if let Some(int_box) = left.as_any().downcast_ref::<crate::box_trait::IntegerBox>() { if let Some(int_box) = left.as_any().downcast_ref::<crate::box_trait::IntegerBox>() {
if let Some(result) = int_box.try_mul(right) { return Some(result); } if let Some(result) = int_box.try_mul(right) {
return Some(result);
}
} }
if let Some(str_box) = left.as_any().downcast_ref::<crate::box_trait::StringBox>() { if let Some(str_box) = left.as_any().downcast_ref::<crate::box_trait::StringBox>() {
if let Some(result) = str_box.try_mul(right) { return Some(result); } if let Some(result) = str_box.try_mul(right) {
return Some(result);
}
}
if let Some(float_box) = left
.as_any()
.downcast_ref::<crate::boxes::math_box::FloatBox>()
{
if let Some(result) = float_box.try_mul(right) {
return Some(result);
} }
if let Some(float_box) = left.as_any().downcast_ref::<crate::boxes::math_box::FloatBox>() {
if let Some(result) = float_box.try_mul(right) { return Some(result); }
} }
if let Some(bool_box) = left.as_any().downcast_ref::<crate::box_trait::BoolBox>() { if let Some(bool_box) = left.as_any().downcast_ref::<crate::box_trait::BoolBox>() {
if let Some(result) = bool_box.try_mul(right) { return Some(result); } if let Some(result) = bool_box.try_mul(right) {
return Some(result);
}
} }
None None
} }
@ -442,7 +471,10 @@ impl OperatorResolver {
if let Some(int_box) = left.as_any().downcast_ref::<crate::box_trait::IntegerBox>() { if let Some(int_box) = left.as_any().downcast_ref::<crate::box_trait::IntegerBox>() {
return int_box.try_div(right); return int_box.try_div(right);
} }
if let Some(float_box) = left.as_any().downcast_ref::<crate::boxes::math_box::FloatBox>() { if let Some(float_box) = left
.as_any()
.downcast_ref::<crate::boxes::math_box::FloatBox>()
{
return float_box.try_div(right); return float_box.try_div(right);
} }
if let Some(bool_box) = left.as_any().downcast_ref::<crate::box_trait::BoolBox>() { if let Some(bool_box) = left.as_any().downcast_ref::<crate::box_trait::BoolBox>() {
@ -458,7 +490,9 @@ impl OperatorResolver {
// Try to cast to concrete types first and use their DynamicAdd implementation // Try to cast to concrete types first and use their DynamicAdd implementation
// This approach uses the concrete types rather than trait objects // This approach uses the concrete types rather than trait objects
if let Some(result) = Self::try_dyn_left_add(left, right) { return Ok(result); } if let Some(result) = Self::try_dyn_left_add(left, right) {
return Ok(result);
}
Err(OperatorError::UnsupportedOperation { Err(OperatorError::UnsupportedOperation {
operator: "+".to_string(), operator: "+".to_string(),
@ -472,7 +506,9 @@ impl OperatorResolver {
left: &dyn NyashBox, left: &dyn NyashBox,
right: &dyn NyashBox, right: &dyn NyashBox,
) -> Result<Box<dyn NyashBox>, OperatorError> { ) -> Result<Box<dyn NyashBox>, OperatorError> {
if let Some(result) = Self::try_dyn_left_sub(left, right) { return Ok(result); } if let Some(result) = Self::try_dyn_left_sub(left, right) {
return Ok(result);
}
Err(OperatorError::UnsupportedOperation { Err(OperatorError::UnsupportedOperation {
operator: "-".to_string(), operator: "-".to_string(),
@ -486,7 +522,9 @@ impl OperatorResolver {
left: &dyn NyashBox, left: &dyn NyashBox,
right: &dyn NyashBox, right: &dyn NyashBox,
) -> Result<Box<dyn NyashBox>, OperatorError> { ) -> Result<Box<dyn NyashBox>, OperatorError> {
if let Some(result) = Self::try_dyn_left_mul(left, right) { return Ok(result); } if let Some(result) = Self::try_dyn_left_mul(left, right) {
return Ok(result);
}
Err(OperatorError::UnsupportedOperation { Err(OperatorError::UnsupportedOperation {
operator: "*".to_string(), operator: "*".to_string(),
@ -500,7 +538,9 @@ impl OperatorResolver {
left: &dyn NyashBox, left: &dyn NyashBox,
right: &dyn NyashBox, right: &dyn NyashBox,
) -> Result<Box<dyn NyashBox>, OperatorError> { ) -> Result<Box<dyn NyashBox>, OperatorError> {
if let Some(result) = Self::try_dyn_left_div(left, right) { return Ok(result); } if let Some(result) = Self::try_dyn_left_div(left, right) {
return Ok(result);
}
Err(OperatorError::UnsupportedOperation { Err(OperatorError::UnsupportedOperation {
operator: "/".to_string(), operator: "/".to_string(),

View File

@ -6,8 +6,8 @@
use crate::box_trait::{BoolBox, IntegerBox, StringBox}; use crate::box_trait::{BoolBox, IntegerBox, StringBox};
use crate::boxes::FloatBox; use crate::boxes::FloatBox;
use crate::operator_traits::{NyashAdd, NyashMul};
use crate::impl_static_numeric_ops; use crate::impl_static_numeric_ops;
use crate::operator_traits::{NyashAdd, NyashMul};
// ===== Macro-generated static implementations ===== // ===== Macro-generated static implementations =====

View File

@ -160,15 +160,7 @@ pub trait NyashBox: BoxCore + Debug {
// ===== Basic Box Types (Re-exported from basic module) ===== // ===== Basic Box Types (Re-exported from basic module) =====
// Re-export all basic box types from the dedicated basic module // Re-export all basic box types from the dedicated basic module
pub use crate::boxes::basic::{ pub use crate::boxes::basic::{BoolBox, ErrorBox, FileBox, IntegerBox, StringBox, VoidBox};
BoolBox, ErrorBox, FileBox, IntegerBox, StringBox, VoidBox,
};
// Old Box implementations have been moved to separate files // Old Box implementations have been moved to separate files
// ArrayBox is now defined in boxes::array module // ArrayBox is now defined in boxes::array module

View File

@ -13,19 +13,19 @@
// Individual arithmetic operation implementations // Individual arithmetic operation implementations
mod add_box; mod add_box;
mod subtract_box; mod compare_box;
mod multiply_box;
mod divide_box; mod divide_box;
mod modulo_box; mod modulo_box;
mod compare_box; mod multiply_box;
mod subtract_box;
// Re-export all arithmetic box types // Re-export all arithmetic box types
pub use add_box::AddBox; pub use add_box::AddBox;
pub use subtract_box::SubtractBox; pub use compare_box::CompareBox;
pub use multiply_box::MultiplyBox;
pub use divide_box::DivideBox; pub use divide_box::DivideBox;
pub use modulo_box::ModuloBox; pub use modulo_box::ModuloBox;
pub use compare_box::CompareBox; pub use multiply_box::MultiplyBox;
pub use subtract_box::SubtractBox;
// Re-export for convenience - common pattern in arithmetic operations // Re-export for convenience - common pattern in arithmetic operations
pub use crate::box_trait::{BoolBox, IntegerBox, NyashBox, StringBox}; pub use crate::box_trait::{BoolBox, IntegerBox, NyashBox, StringBox};

View File

@ -2,7 +2,7 @@
//! //!
//! Implements the core BoolBox type for true/false values. //! Implements the core BoolBox type for true/false values.
use crate::box_trait::{NyashBox, BoxCore, BoxBase, StringBox}; use crate::box_trait::{BoxBase, BoxCore, NyashBox, StringBox};
use std::any::Any; use std::any::Any;
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};

View File

@ -2,7 +2,7 @@
//! //!
//! Implements the ErrorBox type for representing error information. //! Implements the ErrorBox type for representing error information.
use crate::box_trait::{NyashBox, BoxCore, BoxBase, StringBox, BoolBox}; use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox};
use std::any::Any; use std::any::Any;
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};

View File

@ -2,7 +2,7 @@
//! //!
//! Implements the core IntegerBox type for 64-bit signed integers. //! Implements the core IntegerBox type for 64-bit signed integers.
use crate::box_trait::{NyashBox, BoxCore, BoxBase, StringBox, BoolBox}; use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox};
use std::any::Any; use std::any::Any;
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};

View File

@ -4,17 +4,17 @@
//! fundamental data types in Nyash: String, Integer, Boolean, Void, File, and Error. //! fundamental data types in Nyash: String, Integer, Boolean, Void, File, and Error.
// Individual basic box implementations // Individual basic box implementations
mod string_box;
mod integer_box;
mod bool_box; mod bool_box;
mod void_box;
mod file_box;
mod error_box; mod error_box;
mod file_box;
mod integer_box;
mod string_box;
mod void_box;
// Re-export all basic box types // Re-export all basic box types
pub use string_box::StringBox;
pub use integer_box::IntegerBox;
pub use bool_box::BoolBox; pub use bool_box::BoolBox;
pub use void_box::VoidBox;
pub use file_box::FileBox;
pub use error_box::ErrorBox; pub use error_box::ErrorBox;
pub use file_box::FileBox;
pub use integer_box::IntegerBox;
pub use string_box::StringBox;
pub use void_box::VoidBox;

View File

@ -2,7 +2,7 @@
//! //!
//! Implements the core StringBox type with all string manipulation methods. //! Implements the core StringBox type with all string manipulation methods.
use crate::box_trait::{NyashBox, BoxCore, BoxBase}; use crate::box_trait::{BoxBase, BoxCore, NyashBox};
use crate::boxes::ArrayBox; use crate::boxes::ArrayBox;
use std::any::Any; use std::any::Any;
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};

View File

@ -2,7 +2,7 @@
//! //!
//! Implements the core VoidBox type representing empty or null results. //! Implements the core VoidBox type representing empty or null results.
use crate::box_trait::{NyashBox, BoxCore, BoxBase, StringBox, BoolBox}; use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox};
use std::any::Any; use std::any::Any;
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};

View File

@ -99,9 +99,9 @@
* - call stackは直近100件まで自動保持 * - call stackは直近100件まで自動保持
*/ */
use crate::box_factory::RuntimeError;
use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox, VoidBox}; use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox, VoidBox};
use crate::instance_v2::InstanceBox; use crate::instance_v2::InstanceBox;
use crate::box_factory::RuntimeError;
use chrono::Local; use chrono::Local;
use std::any::Any; use std::any::Any;
use std::collections::HashMap; use std::collections::HashMap;

View File

@ -33,8 +33,8 @@
* - `run()`はブロッキング動作(アプリ終了まで制御を返さない) * - `run()`はブロッキング動作(アプリ終了まで制御を返さない)
*/ */
use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox};
use crate::box_factory::RuntimeError; use crate::box_factory::RuntimeError;
use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox};
use eframe::{self, egui, epaint::Vec2}; use eframe::{self, egui, epaint::Vec2};
use std::any::Any; use std::any::Any;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};

View File

@ -1,8 +1,8 @@
//! Thin FileBox shim that delegates to a selected provider. //! Thin FileBox shim that delegates to a selected provider.
//! Not wired into the registry yet (safe placeholder). //! Not wired into the registry yet (safe placeholder).
use super::provider::{FileCaps, FileIo, FileResult};
use std::sync::Arc; use std::sync::Arc;
use super::provider::{FileIo, FileCaps, FileResult};
#[allow(dead_code)] #[allow(dead_code)]
pub struct FileBoxShim { pub struct FileBoxShim {
@ -16,9 +16,16 @@ impl FileBoxShim {
let caps = provider.caps(); let caps = provider.caps();
Self { provider, caps } Self { provider, caps }
} }
pub fn open(&self, path: &str) -> FileResult<()> { self.provider.open(path) } pub fn open(&self, path: &str) -> FileResult<()> {
pub fn read(&self) -> FileResult<String> { self.provider.read() } self.provider.open(path)
pub fn close(&self) -> FileResult<()> { self.provider.close() } }
pub fn caps(&self) -> FileCaps { self.caps } pub fn read(&self) -> FileResult<String> {
self.provider.read()
}
pub fn close(&self) -> FileResult<()> {
self.provider.close()
}
pub fn caps(&self) -> FileCaps {
self.caps
}
} }

View File

@ -3,10 +3,12 @@
//! Provides ProviderFactory implementation for the builtin FileBox (core-ro). //! Provides ProviderFactory implementation for the builtin FileBox (core-ro).
//! This is auto-registered when feature "builtin-filebox" is enabled. //! This is auto-registered when feature "builtin-filebox" is enabled.
use std::sync::Arc;
use crate::boxes::file::provider::FileIo;
use crate::boxes::file::core_ro::CoreRoFileIo; use crate::boxes::file::core_ro::CoreRoFileIo;
use crate::runner::modes::common_util::provider_registry::{ProviderFactory, register_provider_factory}; use crate::boxes::file::provider::FileIo;
use crate::runner::modes::common_util::provider_registry::{
register_provider_factory, ProviderFactory,
};
use std::sync::Arc;
/// Builtin FileBox factory (static registration) /// Builtin FileBox factory (static registration)
pub struct BuiltinFileBoxFactory; pub struct BuiltinFileBoxFactory;

View File

@ -3,10 +3,10 @@
// 参考: 既存Boxの設計思想 // 参考: 既存Boxの設計思想
// SSOT provider design (ring0/1) — modules are currently placeholders // SSOT provider design (ring0/1) — modules are currently placeholders
pub mod provider; // trait FileIo / FileCaps / FileError
pub mod core_ro; // Core readonly provider
pub mod box_shim; // Thin delegating shim pub mod box_shim; // Thin delegating shim
pub mod builtin_factory; // Builtin FileBox ProviderFactory pub mod builtin_factory;
pub mod core_ro; // Core readonly provider
pub mod provider; // trait FileIo / FileCaps / FileError // Builtin FileBox ProviderFactory
use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox}; use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox};
use crate::runtime::provider_lock; use crate::runtime::provider_lock;
@ -64,7 +64,8 @@ impl FileBox {
.ok_or("FileBox provider not initialized")? .ok_or("FileBox provider not initialized")?
.clone(); .clone();
provider.open(path) provider
.open(path)
.map_err(|e| format!("Failed to open: {}", e))?; .map_err(|e| format!("Failed to open: {}", e))?;
Ok(FileBox { Ok(FileBox {
@ -76,8 +77,7 @@ impl FileBox {
pub fn read_to_string(&self) -> Result<String, String> { pub fn read_to_string(&self) -> Result<String, String> {
if let Some(ref provider) = self.provider { if let Some(ref provider) = self.provider {
provider.read() provider.read().map_err(|e| format!("Read failed: {}", e))
.map_err(|e| format!("Read failed: {}", e))
} else { } else {
Err("No provider available".to_string()) Err("No provider available".to_string())
} }
@ -85,7 +85,8 @@ impl FileBox {
pub fn write_all(&self, _buf: &[u8]) -> Result<(), String> { pub fn write_all(&self, _buf: &[u8]) -> Result<(), String> {
// Fail-Fast by capability: consult provider caps // Fail-Fast by capability: consult provider caps
let caps = self.provider let caps = self
.provider
.as_ref() .as_ref()
.map(|p| p.caps()) .map(|p| p.caps())
.or_else(|| provider_lock::get_filebox_caps()) .or_else(|| provider_lock::get_filebox_caps())
@ -107,15 +108,20 @@ impl FileBox {
/// ファイルに内容を書き込む /// ファイルに内容を書き込む
pub fn write(&self, _content: Box<dyn NyashBox>) -> Box<dyn NyashBox> { pub fn write(&self, _content: Box<dyn NyashBox>) -> Box<dyn NyashBox> {
let caps = self.provider let caps = self
.provider
.as_ref() .as_ref()
.map(|p| p.caps()) .map(|p| p.caps())
.or_else(|| provider_lock::get_filebox_caps()) .or_else(|| provider_lock::get_filebox_caps())
.unwrap_or_else(|| provider::FileCaps::read_only()); .unwrap_or_else(|| provider::FileCaps::read_only());
if !caps.write { if !caps.write {
return Box::new(StringBox::new("Error: write unsupported by provider (read-only)")); return Box::new(StringBox::new(
"Error: write unsupported by provider (read-only)",
));
} }
Box::new(StringBox::new("Error: write supported but not implemented in this build")) Box::new(StringBox::new(
"Error: write supported but not implemented in this build",
))
} }
/// ファイルが存在するかチェック /// ファイルが存在するかチェック
@ -126,28 +132,38 @@ impl FileBox {
/// ファイルを削除 /// ファイルを削除
pub fn delete(&self) -> Box<dyn NyashBox> { pub fn delete(&self) -> Box<dyn NyashBox> {
let caps = self.provider let caps = self
.provider
.as_ref() .as_ref()
.map(|p| p.caps()) .map(|p| p.caps())
.or_else(|| provider_lock::get_filebox_caps()) .or_else(|| provider_lock::get_filebox_caps())
.unwrap_or_else(|| provider::FileCaps::read_only()); .unwrap_or_else(|| provider::FileCaps::read_only());
if !caps.write { if !caps.write {
return Box::new(StringBox::new("Error: delete unsupported by provider (read-only)")); return Box::new(StringBox::new(
"Error: delete unsupported by provider (read-only)",
));
} }
Box::new(StringBox::new("Error: delete supported but not implemented in this build")) Box::new(StringBox::new(
"Error: delete supported but not implemented in this build",
))
} }
/// ファイルをコピー /// ファイルをコピー
pub fn copy(&self, _dest: &str) -> Box<dyn NyashBox> { pub fn copy(&self, _dest: &str) -> Box<dyn NyashBox> {
let caps = self.provider let caps = self
.provider
.as_ref() .as_ref()
.map(|p| p.caps()) .map(|p| p.caps())
.or_else(|| provider_lock::get_filebox_caps()) .or_else(|| provider_lock::get_filebox_caps())
.unwrap_or_else(|| provider::FileCaps::read_only()); .unwrap_or_else(|| provider::FileCaps::read_only());
if !caps.write { if !caps.write {
return Box::new(StringBox::new("Error: copy unsupported by provider (read-only)")); return Box::new(StringBox::new(
"Error: copy unsupported by provider (read-only)",
));
} }
Box::new(StringBox::new("Error: copy supported but not implemented in this build")) Box::new(StringBox::new(
"Error: copy supported but not implemented in this build",
))
} }
} }

View File

@ -12,7 +12,12 @@ pub struct FileCaps {
} }
impl FileCaps { impl FileCaps {
pub const fn read_only() -> Self { Self { read: true, write: false } } pub const fn read_only() -> Self {
Self {
read: true,
write: false,
}
}
} }
/// Unified error type (thin placeholder for now) /// Unified error type (thin placeholder for now)
@ -40,9 +45,10 @@ pub trait FileIo: Send + Sync {
pub fn normalize_newlines(s: &str) -> String { pub fn normalize_newlines(s: &str) -> String {
let mut out = String::with_capacity(s.len()); let mut out = String::with_capacity(s.len());
for b in s.as_bytes() { for b in s.as_bytes() {
if *b == b'\r' { continue; } if *b == b'\r' {
continue;
}
out.push(*b as char); out.push(*b as char);
} }
out out
} }

View File

@ -279,7 +279,11 @@ impl HTTPResponseBox {
} }
/// ステータスコード・メッセージ設定 /// ステータスコード・メッセージ設定
pub fn set_status(&self, code: Box<dyn NyashBox>, message: Box<dyn NyashBox>) -> Box<dyn NyashBox> { pub fn set_status(
&self,
code: Box<dyn NyashBox>,
message: Box<dyn NyashBox>,
) -> Box<dyn NyashBox> {
let code_val = code.to_string_box().value.parse::<i32>().unwrap_or(200); let code_val = code.to_string_box().value.parse::<i32>().unwrap_or(200);
let message_val = message.to_string_box().value; let message_val = message.to_string_box().value;
*self.status_code.lock().unwrap() = code_val; *self.status_code.lock().unwrap() = code_val;
@ -288,7 +292,11 @@ impl HTTPResponseBox {
} }
/// ヘッダー設定 /// ヘッダー設定
pub fn set_header(&self, name: Box<dyn NyashBox>, value: Box<dyn NyashBox>) -> Box<dyn NyashBox> { pub fn set_header(
&self,
name: Box<dyn NyashBox>,
value: Box<dyn NyashBox>,
) -> Box<dyn NyashBox> {
let name_str = name.to_string_box().value; let name_str = name.to_string_box().value;
let value_str = value.to_string_box().value; let value_str = value.to_string_box().value;
self.headers.lock().unwrap().insert(name_str, value_str); self.headers.lock().unwrap().insert(name_str, value_str);
@ -326,7 +334,10 @@ impl HTTPResponseBox {
let version = self.http_version.lock().unwrap().clone(); let version = self.http_version.lock().unwrap().clone();
let status_code = *self.status_code.lock().unwrap(); let status_code = *self.status_code.lock().unwrap();
let status_message = self.status_message.lock().unwrap().clone(); let status_message = self.status_message.lock().unwrap().clone();
response.push_str(&format!("{} {} {}\r\n", version, status_code, status_message)); response.push_str(&format!(
"{} {} {}\r\n",
version, status_code, status_message
));
// Headers // Headers
for (name, value) in self.headers.lock().unwrap().iter() { for (name, value) in self.headers.lock().unwrap().iter() {
@ -335,7 +346,8 @@ impl HTTPResponseBox {
// Content-Length if not already set // Content-Length if not already set
let body_len = { self.body.lock().unwrap().len() }; let body_len = { self.body.lock().unwrap().len() };
let need_len = { !self.headers.lock().unwrap().contains_key("content-length") && body_len > 0 }; let need_len =
{ !self.headers.lock().unwrap().contains_key("content-length") && body_len > 0 };
if need_len { if need_len {
response.push_str(&format!("Content-Length: {}\r\n", body_len)); response.push_str(&format!("Content-Length: {}\r\n", body_len));
} }
@ -367,10 +379,11 @@ impl HTTPResponseBox {
let response = HTTPResponseBox::new(); let response = HTTPResponseBox::new();
*response.status_code.lock().unwrap() = 200; *response.status_code.lock().unwrap() = 200;
*response.status_message.lock().unwrap() = "OK".to_string(); *response.status_message.lock().unwrap() = "OK".to_string();
response.headers.lock().unwrap().insert( response
"Content-Type".to_string(), .headers
"application/json".to_string(), .lock()
); .unwrap()
.insert("Content-Type".to_string(), "application/json".to_string());
*response.body.lock().unwrap() = content.to_string_box().value; *response.body.lock().unwrap() = content.to_string_box().value;
response response
} }
@ -384,7 +397,8 @@ impl HTTPResponseBox {
"Content-Type".to_string(), "Content-Type".to_string(),
"text/html; charset=utf-8".to_string(), "text/html; charset=utf-8".to_string(),
); );
*response.body.lock().unwrap() = "<html><body><h1>404 - Not Found</h1></body></html>".to_string(); *response.body.lock().unwrap() =
"<html><body><h1>404 - Not Found</h1></body></html>".to_string();
response response
} }
} }

View File

@ -161,7 +161,10 @@ impl MapBox {
} }
value.clone_box() value.clone_box()
} }
None => Box::new(StringBox::new(&format!("[map/missing] Key not found: {}", key_str))), None => Box::new(StringBox::new(&format!(
"[map/missing] Key not found: {}",
key_str
))),
} }
} }
@ -202,7 +205,10 @@ impl MapBox {
.unwrap() .unwrap()
.values() .values()
.map(|v| { .map(|v| {
if v.as_any().downcast_ref::<crate::instance_v2::InstanceBox>().is_some() { if v.as_any()
.downcast_ref::<crate::instance_v2::InstanceBox>()
.is_some()
{
v.share_box() v.share_box()
} else { } else {
v.clone_box() v.clone_box()

View File

@ -16,28 +16,48 @@ pub struct MissingBox {
impl MissingBox { impl MissingBox {
pub fn new() -> Self { pub fn new() -> Self {
Self { base: BoxBase::new() } Self {
base: BoxBase::new(),
}
} }
pub fn is_missing(&self) -> bool { true } pub fn is_missing(&self) -> bool {
true
}
} }
impl BoxCore for MissingBox { impl BoxCore for MissingBox {
fn box_id(&self) -> u64 { self.base.id } fn box_id(&self) -> u64 {
fn parent_type_id(&self) -> Option<std::any::TypeId> { self.base.parent_type_id } self.base.id
}
fn parent_type_id(&self) -> Option<std::any::TypeId> {
self.base.parent_type_id
}
fn fmt_box(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt_box(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// 開発時の可視性向上のための文字列表現。prod では基本的に表面化させない想定。 // 開発時の可視性向上のための文字列表現。prod では基本的に表面化させない想定。
write!(f, "(missing)") write!(f, "(missing)")
} }
fn as_any(&self) -> &dyn Any { self } fn as_any(&self) -> &dyn Any {
fn as_any_mut(&mut self) -> &mut dyn Any { self } self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
} }
impl NyashBox for MissingBox { impl NyashBox for MissingBox {
fn type_name(&self) -> &'static str { "MissingBox" } fn type_name(&self) -> &'static str {
fn to_string_box(&self) -> StringBox { StringBox::new("(missing)") } "MissingBox"
fn clone_box(&self) -> Box<dyn NyashBox> { Box::new(self.clone()) } }
fn share_box(&self) -> Box<dyn NyashBox> { self.clone_box() } fn to_string_box(&self) -> StringBox {
StringBox::new("(missing)")
}
fn clone_box(&self) -> Box<dyn NyashBox> {
Box::new(self.clone())
}
fn share_box(&self) -> Box<dyn NyashBox> {
self.clone_box()
}
fn equals(&self, other: &dyn NyashBox) -> BoolBox { fn equals(&self, other: &dyn NyashBox) -> BoolBox {
// 欠損どうしは論理同値とみなすが、通常の等価比較は境界で禁止される想定。 // 欠損どうしは論理同値とみなすが、通常の等価比較は境界で禁止される想定。
BoolBox::new(other.as_any().downcast_ref::<MissingBox>().is_some()) BoolBox::new(other.as_any().downcast_ref::<MissingBox>().is_some())
@ -45,6 +65,7 @@ impl NyashBox for MissingBox {
} }
impl Display for MissingBox { impl Display for MissingBox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.fmt_box(f) } fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.fmt_box(f)
}
} }

View File

@ -145,8 +145,8 @@ pub use egui_box::EguiBox;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub use web::{WebCanvasBox, WebConsoleBox, WebDisplayBox}; pub use web::{WebCanvasBox, WebConsoleBox, WebDisplayBox};
pub mod null_box;
pub mod missing_box; pub mod missing_box;
pub mod null_box;
// High-priority Box types // High-priority Box types
pub mod array; pub mod array;
@ -168,8 +168,8 @@ pub mod intent_box;
pub mod p2p_box; pub mod p2p_box;
// null関数も再エクスポート // null関数も再エクスポート
pub use null_box::{null, NullBox};
pub use missing_box::MissingBox; pub use missing_box::MissingBox;
pub use null_box::{null, NullBox};
// High-priority Box types re-export // High-priority Box types re-export
pub use array::ArrayBox; pub use array::ArrayBox;

View File

@ -1,7 +1,7 @@
use super::utils::parse_debug_fuel;
use super::CliConfig;
use clap::{Arg, ArgMatches, Command}; use clap::{Arg, ArgMatches, Command};
use serde_json; use serde_json;
use super::CliConfig;
use super::utils::parse_debug_fuel;
pub fn parse() -> CliConfig { pub fn parse() -> CliConfig {
let argv: Vec<String> = std::env::args().collect(); let argv: Vec<String> = std::env::args().collect();
@ -13,10 +13,7 @@ pub fn parse() -> CliConfig {
} }
// Provide HEX-escaped JSON as an alternate robust path for multiline/special bytes // Provide HEX-escaped JSON as an alternate robust path for multiline/special bytes
// Each arg is encoded as lowercase hex of its UTF-8 bytes // Each arg is encoded as lowercase hex of its UTF-8 bytes
let hex_args: Vec<String> = script_args let hex_args: Vec<String> = script_args.iter().map(|s| hex_encode_utf8(s)).collect();
.iter()
.map(|s| hex_encode_utf8(s))
.collect();
if let Ok(hex_json) = serde_json::to_string(&hex_args) { if let Ok(hex_json) = serde_json::to_string(&hex_args) {
std::env::set_var("NYASH_SCRIPT_ARGS_HEX_JSON", hex_json); std::env::set_var("NYASH_SCRIPT_ARGS_HEX_JSON", hex_json);
} }
@ -118,8 +115,12 @@ fn hex_encode_utf8(s: &str) -> String {
} }
pub fn from_matches(matches: &ArgMatches) -> CliConfig { pub fn from_matches(matches: &ArgMatches) -> CliConfig {
if matches.get_flag("stage3") { std::env::set_var("NYASH_NY_COMPILER_STAGE3", "1"); } if matches.get_flag("stage3") {
if let Some(a) = matches.get_one::<String>("ny-compiler-args") { std::env::set_var("NYASH_NY_COMPILER_CHILD_ARGS", a); } std::env::set_var("NYASH_NY_COMPILER_STAGE3", "1");
}
if let Some(a) = matches.get_one::<String>("ny-compiler-args") {
std::env::set_var("NYASH_NY_COMPILER_CHILD_ARGS", a);
}
let cfg = CliConfig { let cfg = CliConfig {
file: matches.get_one::<String>("file").cloned(), file: matches.get_one::<String>("file").cloned(),
debug_fuel: parse_debug_fuel(matches.get_one::<String>("debug-fuel").unwrap()), debug_fuel: parse_debug_fuel(matches.get_one::<String>("debug-fuel").unwrap()),
@ -134,7 +135,11 @@ pub fn from_matches(matches: &ArgMatches) -> CliConfig {
compile_native: matches.get_flag("compile-native") || matches.get_flag("aot"), compile_native: matches.get_flag("compile-native") || matches.get_flag("aot"),
output_file: matches.get_one::<String>("output").cloned(), output_file: matches.get_one::<String>("output").cloned(),
benchmark: matches.get_flag("benchmark"), benchmark: matches.get_flag("benchmark"),
iterations: matches.get_one::<String>("iterations").unwrap().parse().unwrap_or(10), iterations: matches
.get_one::<String>("iterations")
.unwrap()
.parse()
.unwrap_or(10),
vm_stats: matches.get_flag("vm-stats"), vm_stats: matches.get_flag("vm-stats"),
vm_stats_json: matches.get_flag("vm-stats-json"), vm_stats_json: matches.get_flag("vm-stats-json"),
jit_exec: matches.get_flag("jit-exec"), jit_exec: matches.get_flag("jit-exec"),
@ -145,7 +150,9 @@ pub fn from_matches(matches: &ArgMatches) -> CliConfig {
jit_events_compile: matches.get_flag("jit-events-compile"), jit_events_compile: matches.get_flag("jit-events-compile"),
jit_events_runtime: matches.get_flag("jit-events-runtime"), jit_events_runtime: matches.get_flag("jit-events-runtime"),
jit_events_path: matches.get_one::<String>("jit-events-path").cloned(), jit_events_path: matches.get_one::<String>("jit-events-path").cloned(),
jit_threshold: matches.get_one::<String>("jit-threshold").and_then(|s| s.parse::<u32>().ok()), jit_threshold: matches
.get_one::<String>("jit-threshold")
.and_then(|s| s.parse::<u32>().ok()),
jit_phi_min: matches.get_flag("jit-phi-min"), jit_phi_min: matches.get_flag("jit-phi-min"),
jit_hostcall: matches.get_flag("jit-hostcall"), jit_hostcall: matches.get_flag("jit-hostcall"),
jit_handle_debug: matches.get_flag("jit-handle-debug"), jit_handle_debug: matches.get_flag("jit-handle-debug"),
@ -158,7 +165,10 @@ pub fn from_matches(matches: &ArgMatches) -> CliConfig {
run_task: matches.get_one::<String>("run-task").cloned(), run_task: matches.get_one::<String>("run-task").cloned(),
load_ny_plugins: matches.get_flag("load-ny-plugins"), load_ny_plugins: matches.get_flag("load-ny-plugins"),
gc_mode: matches.get_one::<String>("gc").cloned(), gc_mode: matches.get_one::<String>("gc").cloned(),
parser_ny: matches.get_one::<String>("parser").map(|s| s == "ny").unwrap_or(false), parser_ny: matches
.get_one::<String>("parser")
.map(|s| s == "ny")
.unwrap_or(false),
ny_parser_pipe: matches.get_flag("ny-parser-pipe"), ny_parser_pipe: matches.get_flag("ny-parser-pipe"),
json_file: matches.get_one::<String>("json-file").cloned(), json_file: matches.get_one::<String>("json-file").cloned(),
mir_json_file: matches.get_one::<String>("mir-json-file").cloned(), mir_json_file: matches.get_one::<String>("mir-json-file").cloned(),
@ -168,7 +178,10 @@ pub fn from_matches(matches: &ArgMatches) -> CliConfig {
build_aot: matches.get_one::<String>("build-aot").cloned(), build_aot: matches.get_one::<String>("build-aot").cloned(),
build_profile: matches.get_one::<String>("build-profile").cloned(), build_profile: matches.get_one::<String>("build-profile").cloned(),
build_target: matches.get_one::<String>("build-target").cloned(), build_target: matches.get_one::<String>("build-target").cloned(),
cli_usings: matches.get_many::<String>("using").map(|v| v.cloned().collect()).unwrap_or_else(|| Vec::new()), cli_usings: matches
.get_many::<String>("using")
.map(|v| v.cloned().collect())
.unwrap_or_else(|| Vec::new()),
emit_mir_json: matches.get_one::<String>("emit-mir-json").cloned(), emit_mir_json: matches.get_one::<String>("emit-mir-json").cloned(),
program_json_to_mir: matches.get_one::<String>("program-json-to-mir").cloned(), program_json_to_mir: matches.get_one::<String>("program-json-to-mir").cloned(),
emit_exe: matches.get_one::<String>("emit-exe").cloned(), emit_exe: matches.get_one::<String>("emit-exe").cloned(),
@ -179,42 +192,94 @@ pub fn from_matches(matches: &ArgMatches) -> CliConfig {
macro_ctx_json: matches.get_one::<String>("macro-ctx-json").cloned(), macro_ctx_json: matches.get_one::<String>("macro-ctx-json").cloned(),
}; };
if cfg.cli_verbose { std::env::set_var("NYASH_CLI_VERBOSE", "1"); } if cfg.cli_verbose {
if cfg.vm_stats { std::env::set_var("NYASH_VM_STATS", "1"); } std::env::set_var("NYASH_CLI_VERBOSE", "1");
if cfg.vm_stats_json { std::env::set_var("NYASH_VM_STATS_JSON", "1"); } }
if cfg.jit_exec { std::env::set_var("NYASH_JIT_EXEC", "1"); } if cfg.vm_stats {
if cfg.jit_stats { std::env::set_var("NYASH_JIT_STATS", "1"); } std::env::set_var("NYASH_VM_STATS", "1");
if cfg.jit_stats_json { std::env::set_var("NYASH_JIT_STATS_JSON", "1"); } }
if cfg.jit_dump { std::env::set_var("NYASH_JIT_DUMP", "1"); } if cfg.vm_stats_json {
if cfg.jit_events { std::env::set_var("NYASH_JIT_EVENTS", "1"); } std::env::set_var("NYASH_VM_STATS_JSON", "1");
if cfg.jit_events_compile { std::env::set_var("NYASH_JIT_EVENTS_COMPILE", "1"); } }
if cfg.jit_events_runtime { std::env::set_var("NYASH_JIT_EVENTS_RUNTIME", "1"); } if cfg.jit_exec {
if let Some(p) = &cfg.jit_events_path { std::env::set_var("NYASH_JIT_EVENTS_PATH", p); } std::env::set_var("NYASH_JIT_EXEC", "1");
if let Some(t) = cfg.jit_threshold { std::env::set_var("NYASH_JIT_THRESHOLD", t.to_string()); } }
if cfg.jit_phi_min { std::env::set_var("NYASH_JIT_PHI_MIN", "1"); } if cfg.jit_stats {
if cfg.jit_hostcall { std::env::set_var("NYASH_JIT_HOSTCALL", "1"); } std::env::set_var("NYASH_JIT_STATS", "1");
if cfg.jit_handle_debug { std::env::set_var("NYASH_JIT_HANDLE_DEBUG", "1"); } }
if cfg.jit_native_f64 { std::env::set_var("NYASH_JIT_NATIVE_F64", "1"); } if cfg.jit_stats_json {
if cfg.jit_native_bool { std::env::set_var("NYASH_JIT_NATIVE_BOOL", "1"); } std::env::set_var("NYASH_JIT_STATS_JSON", "1");
if cfg.jit_only { std::env::set_var("NYASH_JIT_ONLY", "1"); } }
if cfg.jit_direct { std::env::set_var("NYASH_JIT_DIRECT", "1"); } if cfg.jit_dump {
if let Some(gc) = &cfg.gc_mode { std::env::set_var("NYASH_GC_MODE", gc); } std::env::set_var("NYASH_JIT_DUMP", "1");
}
if cfg.jit_events {
std::env::set_var("NYASH_JIT_EVENTS", "1");
}
if cfg.jit_events_compile {
std::env::set_var("NYASH_JIT_EVENTS_COMPILE", "1");
}
if cfg.jit_events_runtime {
std::env::set_var("NYASH_JIT_EVENTS_RUNTIME", "1");
}
if let Some(p) = &cfg.jit_events_path {
std::env::set_var("NYASH_JIT_EVENTS_PATH", p);
}
if let Some(t) = cfg.jit_threshold {
std::env::set_var("NYASH_JIT_THRESHOLD", t.to_string());
}
if cfg.jit_phi_min {
std::env::set_var("NYASH_JIT_PHI_MIN", "1");
}
if cfg.jit_hostcall {
std::env::set_var("NYASH_JIT_HOSTCALL", "1");
}
if cfg.jit_handle_debug {
std::env::set_var("NYASH_JIT_HANDLE_DEBUG", "1");
}
if cfg.jit_native_f64 {
std::env::set_var("NYASH_JIT_NATIVE_F64", "1");
}
if cfg.jit_native_bool {
std::env::set_var("NYASH_JIT_NATIVE_BOOL", "1");
}
if cfg.jit_only {
std::env::set_var("NYASH_JIT_ONLY", "1");
}
if cfg.jit_direct {
std::env::set_var("NYASH_JIT_DIRECT", "1");
}
if let Some(gc) = &cfg.gc_mode {
std::env::set_var("NYASH_GC_MODE", gc);
}
if matches.get_flag("run-tests") { if matches.get_flag("run-tests") {
std::env::set_var("NYASH_RUN_TESTS", "1"); std::env::set_var("NYASH_RUN_TESTS", "1");
if let Some(filter) = matches.get_one::<String>("test-filter") { std::env::set_var("NYASH_TEST_FILTER", filter); } if let Some(filter) = matches.get_one::<String>("test-filter") {
std::env::set_var("NYASH_TEST_FILTER", filter);
}
if let Some(entry) = matches.get_one::<String>("test-entry") { if let Some(entry) = matches.get_one::<String>("test-entry") {
let v = entry.as_str(); let v = entry.as_str();
if v == "wrap" || v == "override" { std::env::set_var("NYASH_TEST_ENTRY", v); } if v == "wrap" || v == "override" {
std::env::set_var("NYASH_TEST_ENTRY", v);
}
} }
if let Some(ret) = matches.get_one::<String>("test-return") { if let Some(ret) = matches.get_one::<String>("test-return") {
let v = ret.as_str(); let v = ret.as_str();
if v == "tests" || v == "original" { std::env::set_var("NYASH_TEST_RETURN", v); } if v == "tests" || v == "original" {
std::env::set_var("NYASH_TEST_RETURN", v);
} }
} }
if matches.get_flag("macro-preexpand") { std::env::set_var("NYASH_MACRO_SELFHOST_PRE_EXPAND", "1"); } }
if matches.get_flag("macro-preexpand-auto") { std::env::set_var("NYASH_MACRO_SELFHOST_PRE_EXPAND", "auto"); } if matches.get_flag("macro-preexpand") {
if matches.get_flag("macro-top-level-allow") { std::env::set_var("NYASH_MACRO_TOPLEVEL_ALLOW", "1"); } std::env::set_var("NYASH_MACRO_SELFHOST_PRE_EXPAND", "1");
}
if matches.get_flag("macro-preexpand-auto") {
std::env::set_var("NYASH_MACRO_SELFHOST_PRE_EXPAND", "auto");
}
if matches.get_flag("macro-top-level-allow") {
std::env::set_var("NYASH_MACRO_TOPLEVEL_ALLOW", "1");
}
if let Some(p) = matches.get_one::<String>("macro-profile") { if let Some(p) = matches.get_one::<String>("macro-profile") {
match p.as_str() { match p.as_str() {
"dev" | "ci-fast" | "strict" => { "dev" | "ci-fast" | "strict" => {

View File

@ -6,7 +6,6 @@ mod args;
mod groups; mod groups;
mod utils; mod utils;
/// Command-line configuration structure /// Command-line configuration structure
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CliConfig { pub struct CliConfig {
@ -69,14 +68,22 @@ pub struct CliConfig {
pub macro_ctx_json: Option<String>, pub macro_ctx_json: Option<String>,
} }
pub use groups::{BackendConfig, BuildConfig, CliGroups, DebugConfig, EmitConfig, InputConfig, JitConfig, ParserPipeConfig}; pub use groups::{
BackendConfig, BuildConfig, CliGroups, DebugConfig, EmitConfig, InputConfig, JitConfig,
ParserPipeConfig,
};
impl CliConfig { impl CliConfig {
pub fn parse() -> Self { args::parse() } pub fn parse() -> Self {
args::parse()
}
pub fn as_groups(&self) -> CliGroups { pub fn as_groups(&self) -> CliGroups {
CliGroups { CliGroups {
input: InputConfig { file: self.file.clone(), cli_usings: self.cli_usings.clone() }, input: InputConfig {
file: self.file.clone(),
cli_usings: self.cli_usings.clone(),
},
debug: DebugConfig { debug: DebugConfig {
debug_fuel: self.debug_fuel, debug_fuel: self.debug_fuel,
dump_ast: self.dump_ast, dump_ast: self.dump_ast,

View File

@ -6,4 +6,3 @@ pub fn parse_debug_fuel(value: &str) -> Option<usize> {
value.parse::<usize>().ok() value.parse::<usize>().ok()
} }
} }

View File

@ -121,7 +121,9 @@ pub fn await_max_ms() -> u64 {
/// Enable MIR PHI non-generation for Bridge compatibility mode only. /// Enable MIR PHI non-generation for Bridge compatibility mode only.
/// フェーズM.2: MirBuilder/LoopBuilderでPHI統一済み、Bridge層の互換性制御のみ /// フェーズM.2: MirBuilder/LoopBuilderでPHI統一済み、Bridge層の互換性制御のみ
/// Default: PHI-ON (Phase 15 direction), override with NYASH_MIR_NO_PHI=1 /// Default: PHI-ON (Phase 15 direction), override with NYASH_MIR_NO_PHI=1
pub fn mir_no_phi() -> bool { env_bool("NYASH_MIR_NO_PHI") } pub fn mir_no_phi() -> bool {
env_bool("NYASH_MIR_NO_PHI")
}
/// Allow verifier to skip SSA/dominance/merge checks for PHI-less MIR. /// Allow verifier to skip SSA/dominance/merge checks for PHI-less MIR.
pub fn verify_allow_no_phi() -> bool { pub fn verify_allow_no_phi() -> bool {
@ -131,11 +133,15 @@ pub fn verify_allow_no_phi() -> bool {
/// Enable strict edge-copy policy verification in PHI-off mode. /// Enable strict edge-copy policy verification in PHI-off mode.
/// When enabled, merge blocks must receive merged values via predecessor copies only, /// When enabled, merge blocks must receive merged values via predecessor copies only,
/// 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 /// 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. /// 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") } 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 {
@ -172,7 +178,9 @@ pub fn env_bool_default(key: &str, default: bool) -> bool {
/// Global fail-fast policy for runtime fallbacks. /// Global fail-fast policy for runtime fallbacks.
/// Default: ON (true) to prohibit silent/different-route fallbacks in Rust layer. /// Default: ON (true) to prohibit silent/different-route fallbacks in Rust layer.
/// 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). // VM legacy by-name call fallback was removed (Phase 2 complete).
@ -204,7 +212,9 @@ pub fn plugin_only() -> bool {
/// Core-13 "pure" mode: after normalization, only the 13 canonical ops are allowed. /// Core-13 "pure" mode: after normalization, only the 13 canonical ops are allowed.
/// If enabled, the optimizer will try lightweight rewrites for Load/Store/NewBox/Unary, /// If enabled, the optimizer will try lightweight rewrites for Load/Store/NewBox/Unary,
/// and the final verifier will reject any remaining non-Core-13 ops. /// and the final verifier will reject any remaining non-Core-13 ops.
pub fn mir_core13_pure() -> bool { env_bool("NYASH_MIR_CORE13_PURE") } pub fn mir_core13_pure() -> bool {
env_bool("NYASH_MIR_CORE13_PURE")
}
/// Enable heuristic pre-pin of comparison operands in if/loop headers. /// Enable heuristic pre-pin of comparison operands in if/loop headers.
/// Default: OFF (0). Set NYASH_MIR_PREPIN=1 to enable. /// Default: OFF (0). Set NYASH_MIR_PREPIN=1 to enable.
@ -235,14 +245,26 @@ pub fn opt_diag_fail() -> bool {
// ---- Legacy compatibility (dev-only) ---- // ---- Legacy compatibility (dev-only) ----
/// Enable legacy InstanceBox fields (SharedNyashBox map) for compatibility. /// Enable legacy InstanceBox fields (SharedNyashBox map) for compatibility.
/// Default: OFF. Set NYASH_LEGACY_FIELDS_ENABLE=1 to materialize and use legacy fields. /// 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") } 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 {
pub fn gc_barrier_trace() -> bool { env_bool("NYASH_GC_BARRIER_TRACE") } env_bool("NYASH_GC_TRACE")
pub fn runtime_checkpoint_trace() -> bool { env_bool("NYASH_RUNTIME_CHECKPOINT_TRACE") } }
pub fn vm_pic_stats() -> bool { env_bool("NYASH_VM_PIC_STATS") } pub fn gc_barrier_trace() -> bool {
pub fn vm_vt_trace() -> bool { env_bool("NYASH_VM_VT_TRACE") } env_bool("NYASH_GC_BARRIER_TRACE")
}
pub fn runtime_checkpoint_trace() -> bool {
env_bool("NYASH_RUNTIME_CHECKPOINT_TRACE")
}
pub fn vm_pic_stats() -> bool {
env_bool("NYASH_VM_PIC_STATS")
}
pub fn vm_vt_trace() -> bool {
env_bool("NYASH_VM_VT_TRACE")
}
pub fn vm_pic_trace() -> bool { pub fn vm_pic_trace() -> bool {
std::env::var("NYASH_VM_PIC_TRACE").ok().as_deref() == Some("1") std::env::var("NYASH_VM_PIC_TRACE").ok().as_deref() == Some("1")
} }
@ -349,7 +371,11 @@ pub fn extern_trace() -> bool {
// ---- Operator Boxes adopt defaults ---- // ---- Operator Boxes adopt defaults ----
/// CompareOperator.apply adopt: default ON (prod/devともに採用) /// CompareOperator.apply adopt: default ON (prod/devともに採用)
pub fn operator_box_compare_adopt() -> bool { pub fn operator_box_compare_adopt() -> bool {
match std::env::var("NYASH_OPERATOR_BOX_COMPARE_ADOPT").ok().as_deref().map(|v| v.to_ascii_lowercase()) { match std::env::var("NYASH_OPERATOR_BOX_COMPARE_ADOPT")
.ok()
.as_deref()
.map(|v| v.to_ascii_lowercase())
{
Some(ref s) if s == "0" || s == "false" || s == "off" => false, Some(ref s) if s == "0" || s == "false" || s == "off" => false,
Some(ref s) if s == "1" || s == "true" || s == "on" => true, Some(ref s) if s == "1" || s == "true" || s == "on" => true,
_ => true, // default ON _ => true, // default ON
@ -357,7 +383,11 @@ pub fn operator_box_compare_adopt() -> bool {
} }
/// AddOperator.apply adopt: default OFF順次昇格のため /// AddOperator.apply adopt: default OFF順次昇格のため
pub fn operator_box_add_adopt() -> bool { pub fn operator_box_add_adopt() -> bool {
match std::env::var("NYASH_OPERATOR_BOX_ADD_ADOPT").ok().as_deref().map(|v| v.to_ascii_lowercase()) { match std::env::var("NYASH_OPERATOR_BOX_ADD_ADOPT")
.ok()
.as_deref()
.map(|v| v.to_ascii_lowercase())
{
Some(ref s) if s == "0" || s == "false" || s == "off" => false, Some(ref s) if s == "0" || s == "false" || s == "off" => false,
_ => true, // default ON (promoted after validation) _ => true, // default ON (promoted after validation)
} }
@ -415,9 +445,15 @@ pub fn enable_using() -> bool {
pub fn using_profile() -> String { pub fn using_profile() -> String {
std::env::var("NYASH_USING_PROFILE").unwrap_or_else(|_| "dev".to_string()) std::env::var("NYASH_USING_PROFILE").unwrap_or_else(|_| "dev".to_string())
} }
pub fn using_is_prod() -> bool { using_profile().eq_ignore_ascii_case("prod") } pub fn using_is_prod() -> bool {
pub fn using_is_ci() -> bool { using_profile().eq_ignore_ascii_case("ci") } using_profile().eq_ignore_ascii_case("prod")
pub fn using_is_dev() -> bool { using_profile().eq_ignore_ascii_case("dev") } }
pub fn using_is_ci() -> bool {
using_profile().eq_ignore_ascii_case("ci")
}
pub fn using_is_dev() -> bool {
using_profile().eq_ignore_ascii_case("dev")
}
/// Allow `using "path"` statements in source (dev-only by default). /// Allow `using "path"` statements in source (dev-only by default).
pub fn allow_using_file() -> bool { pub fn allow_using_file() -> bool {
// SSOT 徹底: 全プロファイルで既定禁止nyash.toml を唯一の真実に) // SSOT 徹底: 全プロファイルで既定禁止nyash.toml を唯一の真実に)
@ -432,7 +468,11 @@ pub fn allow_using_file() -> bool {
/// 1) Explicit env `NYASH_USING_AST` = 1/true/on → enabled, = 0/false/off → disabled /// 1) Explicit env `NYASH_USING_AST` = 1/true/on → enabled, = 0/false/off → disabled
/// 2) Default by profile: dev/ci → ON, prod → OFF /// 2) Default by profile: dev/ci → ON, prod → OFF
pub fn using_ast_enabled() -> bool { pub fn using_ast_enabled() -> bool {
match std::env::var("NYASH_USING_AST").ok().as_deref().map(|v| v.to_ascii_lowercase()) { match std::env::var("NYASH_USING_AST")
.ok()
.as_deref()
.map(|v| v.to_ascii_lowercase())
{
Some(ref s) if s == "1" || s == "true" || s == "on" => true, Some(ref s) if s == "1" || s == "true" || s == "on" => true,
Some(ref s) if s == "0" || s == "false" || s == "off" => false, Some(ref s) if s == "0" || s == "false" || s == "off" => false,
_ => !using_is_prod(), // dev/ci → true, prod → false _ => !using_is_prod(), // dev/ci → true, prod → false
@ -443,7 +483,11 @@ pub fn using_ast_enabled() -> bool {
/// - dev/ci: default true (allow, with WARN) /// - dev/ci: default true (allow, with WARN)
/// Override with NYASH_VM_USER_INSTANCE_BOXCALL={0|1} /// Override with NYASH_VM_USER_INSTANCE_BOXCALL={0|1}
pub fn vm_allow_user_instance_boxcall() -> bool { pub fn vm_allow_user_instance_boxcall() -> bool {
match std::env::var("NYASH_VM_USER_INSTANCE_BOXCALL").ok().as_deref().map(|v| v.to_ascii_lowercase()) { match std::env::var("NYASH_VM_USER_INSTANCE_BOXCALL")
.ok()
.as_deref()
.map(|v| v.to_ascii_lowercase())
{
Some(ref s) if s == "0" || s == "false" || s == "off" => false, Some(ref s) if s == "0" || s == "false" || s == "off" => false,
Some(ref s) if s == "1" || s == "true" || s == "on" => true, Some(ref s) if s == "1" || s == "true" || s == "on" => true,
_ => !using_is_prod(), _ => !using_is_prod(),
@ -630,7 +674,10 @@ fn warn_alias_once(alias: &str, primary: &str) {
let set = WARNED_ALIASES.get_or_init(|| Mutex::new(HashSet::new())); let set = WARNED_ALIASES.get_or_init(|| Mutex::new(HashSet::new()));
if let Ok(mut s) = set.lock() { if let Ok(mut s) = set.lock() {
if !s.contains(alias) { if !s.contains(alias) {
eprintln!("[deprecate/env] '{}' is deprecated; use '{}'", alias, primary); eprintln!(
"[deprecate/env] '{}' is deprecated; use '{}'",
alias, primary
);
s.insert(alias.to_string()); s.insert(alias.to_string());
} }
} }
@ -652,7 +699,9 @@ pub fn llvm_opt_level() -> String {
/// GateC(Core) route request (primary: NYASH_GATE_C_CORE; alias: HAKO_GATE_C_CORE) /// GateC(Core) route request (primary: NYASH_GATE_C_CORE; alias: HAKO_GATE_C_CORE)
pub fn gate_c_core() -> bool { pub fn gate_c_core() -> bool {
if env_bool("NYASH_GATE_C_CORE") { return true; } if env_bool("NYASH_GATE_C_CORE") {
return true;
}
if env_bool("HAKO_GATE_C_CORE") { if env_bool("HAKO_GATE_C_CORE") {
warn_alias_once("HAKO_GATE_C_CORE", "NYASH_GATE_C_CORE"); warn_alias_once("HAKO_GATE_C_CORE", "NYASH_GATE_C_CORE");
return true; return true;

View File

@ -14,11 +14,18 @@ pub enum ProviderPolicy {
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FileBoxMode { Auto, CoreRo, PluginOnly } pub enum FileBoxMode {
Auto,
CoreRo,
PluginOnly,
}
/// Read global provider policy (affects Auto mode only) /// Read global provider policy (affects Auto mode only)
pub fn provider_policy_from_env() -> ProviderPolicy { pub fn provider_policy_from_env() -> ProviderPolicy {
match std::env::var("HAKO_PROVIDER_POLICY").unwrap_or_else(|_| "strict-plugin-first".to_string()).as_str() { match std::env::var("HAKO_PROVIDER_POLICY")
.unwrap_or_else(|_| "strict-plugin-first".to_string())
.as_str()
{
"safe-core-first" => ProviderPolicy::SafeCoreFirst, "safe-core-first" => ProviderPolicy::SafeCoreFirst,
"static-preferred" => ProviderPolicy::StaticPreferred, "static-preferred" => ProviderPolicy::StaticPreferred,
_ => ProviderPolicy::StrictPluginFirst, _ => ProviderPolicy::StrictPluginFirst,
@ -27,13 +34,18 @@ pub fn provider_policy_from_env() -> ProviderPolicy {
/// Read FileBox mode from environment variables /// Read FileBox mode from environment variables
pub fn filebox_mode_from_env() -> FileBoxMode { pub fn filebox_mode_from_env() -> FileBoxMode {
match std::env::var("NYASH_FILEBOX_MODE").unwrap_or_else(|_| "auto".to_string()).as_str() { match std::env::var("NYASH_FILEBOX_MODE")
.unwrap_or_else(|_| "auto".to_string())
.as_str()
{
"core-ro" => FileBoxMode::CoreRo, "core-ro" => FileBoxMode::CoreRo,
"plugin-only" => FileBoxMode::PluginOnly, "plugin-only" => FileBoxMode::PluginOnly,
_ => { _ => {
if std::env::var("NYASH_DISABLE_PLUGINS").as_deref() == Ok("1") { if std::env::var("NYASH_DISABLE_PLUGINS").as_deref() == Ok("1") {
FileBoxMode::CoreRo FileBoxMode::CoreRo
} else { FileBoxMode::Auto } } else {
FileBoxMode::Auto
}
} }
} }
} }
@ -44,4 +56,3 @@ pub fn filebox_mode_from_env() -> FileBoxMode {
pub fn allow_filebox_fallback_override(quiet_pipe: bool) -> bool { pub fn allow_filebox_fallback_override(quiet_pipe: bool) -> bool {
quiet_pipe || crate::config::env::env_bool("NYASH_FILEBOX_ALLOW_FALLBACK") quiet_pipe || crate::config::env::env_bool("NYASH_FILEBOX_ALLOW_FALLBACK")
} }

View File

@ -9,7 +9,13 @@ static EMIT_COUNTER: AtomicU64 = AtomicU64::new(0);
/// - NYASH_DEBUG_ENABLE=1 master gate /// - NYASH_DEBUG_ENABLE=1 master gate
/// - NYASH_DEBUG_KINDS=resolve,ssa allowed cats (comma-separated) /// - NYASH_DEBUG_KINDS=resolve,ssa allowed cats (comma-separated)
/// - NYASH_DEBUG_SINK=path file to append JSONL events /// - NYASH_DEBUG_SINK=path file to append JSONL events
pub fn emit(cat: &str, kind: &str, fn_name: Option<&str>, region_id: Option<&str>, meta: serde_json::Value) { pub fn emit(
cat: &str,
kind: &str,
fn_name: Option<&str>,
region_id: Option<&str>,
meta: serde_json::Value,
) {
if std::env::var("NYASH_DEBUG_ENABLE").ok().as_deref() != Some("1") { if std::env::var("NYASH_DEBUG_ENABLE").ok().as_deref() != Some("1") {
return; return;
} }
@ -25,7 +31,9 @@ pub fn emit(cat: &str, kind: &str, fn_name: Option<&str>, region_id: Option<&str
.unwrap_or(1); .unwrap_or(1);
if sample_every > 1 { if sample_every > 1 {
let n = EMIT_COUNTER.fetch_add(1, Ordering::Relaxed) + 1; let n = EMIT_COUNTER.fetch_add(1, Ordering::Relaxed) + 1;
if n % sample_every != 0 { return; } if n % sample_every != 0 {
return;
}
} }
let sink = match std::env::var("NYASH_DEBUG_SINK") { let sink = match std::env::var("NYASH_DEBUG_SINK") {
Ok(s) if !s.is_empty() => s, Ok(s) if !s.is_empty() => s,
@ -41,7 +49,11 @@ pub fn emit(cat: &str, kind: &str, fn_name: Option<&str>, region_id: Option<&str
"kind": kind, "kind": kind,
"meta": meta, "meta": meta,
}); });
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&sink) { if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&sink)
{
let _ = writeln!(f, "{}", obj.to_string()); let _ = writeln!(f, "{}", obj.to_string());
} }
} }

View File

@ -1,2 +1,2 @@
pub mod log;
pub mod hub; pub mod hub;
pub mod log;

View File

@ -36,37 +36,15 @@ pub static OPERATORS_DIV_RULES: &[(&str, &str, &str, &str)] = &[
]; ];
pub fn lookup_keyword(word: &str) -> Option<&'static str> { pub fn lookup_keyword(word: &str) -> Option<&'static str> {
for (k, t) in KEYWORDS { for (k, t) in KEYWORDS {
if *k == word { return Some(*t); } if *k == word {
return Some(*t);
}
} }
None None
} }
pub static SYNTAX_ALLOWED_STATEMENTS: &[&str] = &[ pub static SYNTAX_ALLOWED_STATEMENTS: &[&str] = &[
"box", "box", "global", "function", "static", "if", "loop", "break", "return", "print", "nowait",
"global", "include", "local", "outbox", "try", "throw", "using", "from",
"function",
"static",
"if",
"loop",
"break",
"return",
"print",
"nowait",
"include",
"local",
"outbox",
"try",
"throw",
"using",
"from",
];
pub static SYNTAX_ALLOWED_BINOPS: &[&str] = &[
"add",
"sub",
"mul",
"div",
"and",
"or",
"eq",
"ne",
]; ];
pub static SYNTAX_ALLOWED_BINOPS: &[&str] = &["add", "sub", "mul", "div", "and", "or", "eq", "ne"];

View File

@ -1,8 +1,8 @@
use std::ffi::{CStr, CString};
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::ffi::{CString, CStr};
pub struct Opts { pub struct Opts {
pub out: Option<PathBuf>, pub out: Option<PathBuf>,
@ -13,9 +13,13 @@ pub struct Opts {
fn resolve_ny_llvmc() -> PathBuf { fn resolve_ny_llvmc() -> PathBuf {
if let Ok(s) = std::env::var("NYASH_NY_LLVM_COMPILER") { if let Ok(s) = std::env::var("NYASH_NY_LLVM_COMPILER") {
if !s.is_empty() { return PathBuf::from(s); } if !s.is_empty() {
return PathBuf::from(s);
}
}
if let Ok(p) = which::which("ny-llvmc") {
return p;
} }
if let Ok(p) = which::which("ny-llvmc") { return p; }
PathBuf::from("target/release/ny-llvmc") PathBuf::from("target/release/ny-llvmc")
} }
@ -25,7 +29,10 @@ pub fn mir_json_to_object(mir_json: &str, opts: Opts) -> Result<PathBuf, String>
// Optional provider selection (C-API) — guarded by env flags // Optional provider selection (C-API) — guarded by env flags
// NYASH_LLVM_USE_CAPI=1 and HAKO_V1_EXTERN_PROVIDER_C_ABI=1 // NYASH_LLVM_USE_CAPI=1 and HAKO_V1_EXTERN_PROVIDER_C_ABI=1
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() == Some("1") 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") && std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI")
.ok()
.as_deref()
== Some("1")
{ {
// Basic shape check first // Basic shape check first
if !mir_json.contains("\"functions\"") || !mir_json.contains("\"blocks\"") { if !mir_json.contains("\"functions\"") || !mir_json.contains("\"blocks\"") {
@ -37,11 +44,19 @@ pub fn mir_json_to_object(mir_json: &str, opts: Opts) -> Result<PathBuf, String>
let tmp_dir = std::env::temp_dir(); let tmp_dir = std::env::temp_dir();
let in_path = tmp_dir.join("hako_llvm_in.json"); let in_path = tmp_dir.join("hako_llvm_in.json");
{ {
let mut f = fs::File::create(&in_path).map_err(|e| format!("[llvmemit/tmp/write-failed] {}", e))?; let mut f = fs::File::create(&in_path)
f.write_all(mir_json.as_bytes()).map_err(|e| format!("[llvmemit/tmp/write-failed] {}", e))?; .map_err(|e| format!("[llvmemit/tmp/write-failed] {}", e))?;
f.write_all(mir_json.as_bytes())
.map_err(|e| format!("[llvmemit/tmp/write-failed] {}", e))?;
}
let out_path = if let Some(p) = opts.out.clone() {
p
} else {
tmp_dir.join("hako_llvm_out.o")
};
if let Some(parent) = out_path.parent() {
let _ = fs::create_dir_all(parent);
} }
let out_path = if let Some(p) = opts.out.clone() { p } else { tmp_dir.join("hako_llvm_out.o") };
if let Some(parent) = out_path.parent() { let _ = fs::create_dir_all(parent); }
match compile_via_capi(&in_path, &out_path) { match compile_via_capi(&in_path, &out_path) {
Ok(()) => return Ok(out_path), Ok(()) => return Ok(out_path),
Err(e) => { Err(e) => {
@ -74,26 +89,41 @@ pub fn mir_json_to_object(mir_json: &str, opts: Opts) -> Result<PathBuf, String>
let tmp_dir = std::env::temp_dir(); let tmp_dir = std::env::temp_dir();
let in_path = tmp_dir.join("hako_llvm_in.json"); let in_path = tmp_dir.join("hako_llvm_in.json");
{ {
let mut f = fs::File::create(&in_path).map_err(|e| format!("[llvmemit/tmp/write-failed] {}", e))?; let mut f =
f.write_all(mir_json.as_bytes()).map_err(|e| format!("[llvmemit/tmp/write-failed] {}", e))?; fs::File::create(&in_path).map_err(|e| format!("[llvmemit/tmp/write-failed] {}", e))?;
f.write_all(mir_json.as_bytes())
.map_err(|e| format!("[llvmemit/tmp/write-failed] {}", e))?;
} }
// Output path // Output path
let out_path = if let Some(p) = opts.out.clone() { p } else { tmp_dir.join("hako_llvm_out.o") }; let out_path = if let Some(p) = opts.out.clone() {
if let Some(parent) = out_path.parent() { let _ = fs::create_dir_all(parent); } p
} else {
tmp_dir.join("hako_llvm_out.o")
};
if let Some(parent) = out_path.parent() {
let _ = fs::create_dir_all(parent);
}
// Build command: ny-llvmc --in <json> --emit obj --out <out> // Build command: ny-llvmc --in <json> --emit obj --out <out>
let mut cmd = Command::new(&ny_llvmc); let mut cmd = Command::new(&ny_llvmc);
cmd.arg("--in").arg(&in_path) cmd.arg("--in")
.arg("--emit").arg("obj") .arg(&in_path)
.arg("--out").arg(&out_path); .arg("--emit")
if let Some(nyrt) = opts.nyrt.as_ref() { cmd.arg("--nyrt").arg(nyrt); } .arg("obj")
.arg("--out")
.arg(&out_path);
if let Some(nyrt) = opts.nyrt.as_ref() {
cmd.arg("--nyrt").arg(nyrt);
}
if let Some(level) = opts.opt_level.as_ref() { if let Some(level) = opts.opt_level.as_ref() {
cmd.env("HAKO_LLVM_OPT_LEVEL", level); cmd.env("HAKO_LLVM_OPT_LEVEL", level);
cmd.env("NYASH_LLVM_OPT_LEVEL", level); cmd.env("NYASH_LLVM_OPT_LEVEL", level);
} }
let status = cmd.status().map_err(|e| format!("[llvmemit/spawn/error] {}", e))?; let status = cmd
.status()
.map_err(|e| format!("[llvmemit/spawn/error] {}", e))?;
if !status.success() { if !status.success() {
let code = status.code().unwrap_or(1); let code = status.code().unwrap_or(1);
let tag = format!("[llvmemit/ny-llvmc/failed status={}]", code); let tag = format!("[llvmemit/ny-llvmc/failed status={}]", code);
@ -121,19 +151,28 @@ fn compile_via_capi(json_in: &Path, obj_out: &Path) -> Result<(), String> {
unsafe { unsafe {
// Resolve library path // Resolve library path
let mut candidates: Vec<PathBuf> = Vec::new(); let mut candidates: Vec<PathBuf> = Vec::new();
if let Ok(p) = std::env::var("HAKO_AOT_FFI_LIB") { if !p.is_empty() { candidates.push(PathBuf::from(p)); } } if let Ok(p) = std::env::var("HAKO_AOT_FFI_LIB") {
if !p.is_empty() {
candidates.push(PathBuf::from(p));
}
}
candidates.push(PathBuf::from("target/release/libhako_llvmc_ffi.so")); candidates.push(PathBuf::from("target/release/libhako_llvmc_ffi.so"));
candidates.push(PathBuf::from("lib/libhako_llvmc_ffi.so")); candidates.push(PathBuf::from("lib/libhako_llvmc_ffi.so"));
let lib_path = candidates.into_iter().find(|p| p.exists()) let lib_path = candidates
.into_iter()
.find(|p| p.exists())
.ok_or_else(|| "FFI library not found (set HAKO_AOT_FFI_LIB)".to_string())?; .ok_or_else(|| "FFI library not found (set HAKO_AOT_FFI_LIB)".to_string())?;
let lib = Library::new(lib_path).map_err(|e| format!("dlopen failed: {}", e))?; let lib = Library::new(lib_path).map_err(|e| format!("dlopen failed: {}", e))?;
// Symbol: int hako_llvmc_compile_json(const char*, const char*, char**) // Symbol: int hako_llvmc_compile_json(const char*, const char*, char**)
type CompileFn = unsafe extern "C" fn(*const c_char, *const c_char, *mut *mut c_char) -> c_int; type CompileFn =
unsafe extern "C" fn(*const c_char, *const c_char, *mut *mut c_char) -> c_int;
let func: libloading::Symbol<CompileFn> = lib let func: libloading::Symbol<CompileFn> = lib
.get(b"hako_llvmc_compile_json\0") .get(b"hako_llvmc_compile_json\0")
.map_err(|e| format!("dlsym failed: {}", e))?; .map_err(|e| format!("dlsym failed: {}", e))?;
let cin = CString::new(json_in.to_string_lossy().as_bytes()).map_err(|_| "invalid json path".to_string())?; let cin = CString::new(json_in.to_string_lossy().as_bytes())
let cout = CString::new(obj_out.to_string_lossy().as_bytes()).map_err(|_| "invalid out path".to_string())?; .map_err(|_| "invalid json path".to_string())?;
let cout = CString::new(obj_out.to_string_lossy().as_bytes())
.map_err(|_| "invalid out path".to_string())?;
let mut err_ptr: *mut c_char = std::ptr::null_mut(); let mut err_ptr: *mut c_char = std::ptr::null_mut();
// Avoid recursive FFI-in-FFI: force inner AOT to use CLI path // Avoid recursive FFI-in-FFI: force inner AOT to use CLI path
let prev = std::env::var("HAKO_AOT_USE_FFI").ok(); let prev = std::env::var("HAKO_AOT_USE_FFI").ok();
@ -156,17 +195,31 @@ fn compile_via_capi(json_in: &Path, obj_out: &Path) -> Result<(), String> {
); );
} }
let rc = func(cin.as_ptr(), cout.as_ptr(), &mut err_ptr as *mut *mut c_char); let rc = func(
if let Some(v) = prev { std::env::set_var("HAKO_AOT_USE_FFI", v); } else { std::env::remove_var("HAKO_AOT_USE_FFI"); } cin.as_ptr(),
cout.as_ptr(),
&mut err_ptr as *mut *mut c_char,
);
if let Some(v) = prev {
std::env::set_var("HAKO_AOT_USE_FFI", v);
} else {
std::env::remove_var("HAKO_AOT_USE_FFI");
}
if rc != 0 { if rc != 0 {
let msg = if !err_ptr.is_null() { CStr::from_ptr(err_ptr).to_string_lossy().to_string() } else { "compile failed".to_string() }; let msg = if !err_ptr.is_null() {
CStr::from_ptr(err_ptr).to_string_lossy().to_string()
} else {
"compile failed".to_string()
};
// Free error string (allocated by C side) // Free error string (allocated by C side)
if !err_ptr.is_null() { if !err_ptr.is_null() {
free(err_ptr as *mut c_void); free(err_ptr as *mut c_void);
} }
return Err(msg); return Err(msg);
} }
if !obj_out.exists() { return Err("object not produced".into()); } if !obj_out.exists() {
return Err("object not produced".into());
}
Ok(()) Ok(())
} }
} }
@ -177,13 +230,19 @@ fn compile_via_capi(_json_in: &Path, _obj_out: &Path) -> Result<(), String> {
} }
/// Link an object to an executable via C-API FFI bundle. /// Link an object to an executable via C-API FFI bundle.
pub fn link_object_capi(obj_in: &Path, exe_out: &Path, extra_ldflags: Option<&str>) -> Result<(), String> { pub fn link_object_capi(
obj_in: &Path,
exe_out: &Path,
extra_ldflags: Option<&str>,
) -> Result<(), String> {
// Compute effective ldflags // Compute effective ldflags
let mut eff: Option<String> = extra_ldflags.map(|s| s.to_string()); let mut eff: Option<String> = extra_ldflags.map(|s| s.to_string());
let empty = eff.as_deref().map(|s| s.trim().is_empty()).unwrap_or(true); let empty = eff.as_deref().map(|s| s.trim().is_empty()).unwrap_or(true);
if empty { if empty {
if let Ok(s) = std::env::var("HAKO_AOT_LDFLAGS") { if let Ok(s) = std::env::var("HAKO_AOT_LDFLAGS") {
if !s.trim().is_empty() { eff = Some(s); } if !s.trim().is_empty() {
eff = Some(s);
}
} }
} }
if eff.is_none() { if eff.is_none() {
@ -224,35 +283,68 @@ fn link_via_capi(obj_in: &Path, exe_out: &Path, extra_ldflags: Option<&str>) ->
unsafe { unsafe {
let mut candidates: Vec<PathBuf> = Vec::new(); let mut candidates: Vec<PathBuf> = Vec::new();
if let Ok(p) = std::env::var("HAKO_AOT_FFI_LIB") { if !p.is_empty() { candidates.push(PathBuf::from(p)); } } if let Ok(p) = std::env::var("HAKO_AOT_FFI_LIB") {
if !p.is_empty() {
candidates.push(PathBuf::from(p));
}
}
candidates.push(PathBuf::from("target/release/libhako_llvmc_ffi.so")); candidates.push(PathBuf::from("target/release/libhako_llvmc_ffi.so"));
candidates.push(PathBuf::from("lib/libhako_llvmc_ffi.so")); candidates.push(PathBuf::from("lib/libhako_llvmc_ffi.so"));
let lib_path = candidates.into_iter().find(|p| p.exists()) let lib_path = candidates
.into_iter()
.find(|p| p.exists())
.ok_or_else(|| "FFI library not found (set HAKO_AOT_FFI_LIB)".to_string())?; .ok_or_else(|| "FFI library not found (set HAKO_AOT_FFI_LIB)".to_string())?;
let lib = Library::new(lib_path).map_err(|e| format!("dlopen failed: {}", e))?; let lib = Library::new(lib_path).map_err(|e| format!("dlopen failed: {}", e))?;
// int hako_llvmc_link_obj(const char*, const char*, const char*, char**) // int hako_llvmc_link_obj(const char*, const char*, const char*, char**)
type LinkFn = unsafe extern "C" fn(*const c_char, *const c_char, *const c_char, *mut *mut c_char) -> c_int; type LinkFn = unsafe extern "C" fn(
*const c_char,
*const c_char,
*const c_char,
*mut *mut c_char,
) -> c_int;
let func: libloading::Symbol<LinkFn> = lib let func: libloading::Symbol<LinkFn> = lib
.get(b"hako_llvmc_link_obj\0") .get(b"hako_llvmc_link_obj\0")
.map_err(|e| format!("dlsym failed: {}", e))?; .map_err(|e| format!("dlsym failed: {}", e))?;
let cobj = CString::new(obj_in.to_string_lossy().as_bytes()).map_err(|_| "invalid obj path".to_string())?; let cobj = CString::new(obj_in.to_string_lossy().as_bytes())
let cexe = CString::new(exe_out.to_string_lossy().as_bytes()).map_err(|_| "invalid exe path".to_string())?; .map_err(|_| "invalid obj path".to_string())?;
let cexe = CString::new(exe_out.to_string_lossy().as_bytes())
.map_err(|_| "invalid exe path".to_string())?;
let ldflags_owned; let ldflags_owned;
let cflags_ptr = if let Some(s) = extra_ldflags { ldflags_owned = CString::new(s).map_err(|_| "invalid ldflags".to_string())?; ldflags_owned.as_ptr() } else { std::ptr::null() }; let cflags_ptr = if let Some(s) = extra_ldflags {
ldflags_owned = CString::new(s).map_err(|_| "invalid ldflags".to_string())?;
ldflags_owned.as_ptr()
} else {
std::ptr::null()
};
let mut err_ptr: *mut c_char = std::ptr::null_mut(); let mut err_ptr: *mut c_char = std::ptr::null_mut();
// Avoid recursive FFI-in-FFI // Avoid recursive FFI-in-FFI
let prev = std::env::var("HAKO_AOT_USE_FFI").ok(); let prev = std::env::var("HAKO_AOT_USE_FFI").ok();
std::env::set_var("HAKO_AOT_USE_FFI", "0"); std::env::set_var("HAKO_AOT_USE_FFI", "0");
let rc = func(cobj.as_ptr(), cexe.as_ptr(), cflags_ptr, &mut err_ptr as *mut *mut c_char); let rc = func(
if let Some(v) = prev { std::env::set_var("HAKO_AOT_USE_FFI", v); } else { std::env::remove_var("HAKO_AOT_USE_FFI"); } cobj.as_ptr(),
cexe.as_ptr(),
cflags_ptr,
&mut err_ptr as *mut *mut c_char,
);
if let Some(v) = prev {
std::env::set_var("HAKO_AOT_USE_FFI", v);
} else {
std::env::remove_var("HAKO_AOT_USE_FFI");
}
if rc != 0 { if rc != 0 {
let msg = if !err_ptr.is_null() { CStr::from_ptr(err_ptr).to_string_lossy().to_string() } else { "link failed".to_string() }; let msg = if !err_ptr.is_null() {
CStr::from_ptr(err_ptr).to_string_lossy().to_string()
} else {
"link failed".to_string()
};
if !err_ptr.is_null() { if !err_ptr.is_null() {
free(err_ptr as *mut c_void); free(err_ptr as *mut c_void);
} }
return Err(msg); return Err(msg);
} }
if !exe_out.exists() { return Err("exe not produced".into()); } if !exe_out.exists() {
return Err("exe not produced".into());
}
Ok(()) Ok(())
} }
} }
@ -263,21 +355,31 @@ fn link_via_capi(_obj_in: &Path, _exe_out: &Path, _extra: Option<&str>) -> Resul
} }
fn resolve_python3() -> Option<PathBuf> { fn resolve_python3() -> Option<PathBuf> {
if let Ok(p) = which::which("python3") { return Some(p); } if let Ok(p) = which::which("python3") {
if let Ok(p) = which::which("python") { return Some(p); } return Some(p);
}
if let Ok(p) = which::which("python") {
return Some(p);
}
None None
} }
fn resolve_llvmlite_harness() -> Option<PathBuf> { fn resolve_llvmlite_harness() -> Option<PathBuf> {
if let Ok(root) = std::env::var("NYASH_ROOT") { if let Ok(root) = std::env::var("NYASH_ROOT") {
let p = PathBuf::from(root).join("tools/llvmlite_harness.py"); let p = PathBuf::from(root).join("tools/llvmlite_harness.py");
if p.exists() { return Some(p); } if p.exists() {
return Some(p);
}
} }
let p = PathBuf::from("tools/llvmlite_harness.py"); let p = PathBuf::from("tools/llvmlite_harness.py");
if p.exists() { return Some(p); } if p.exists() {
return Some(p);
}
// Also try repo-relative (target may run elsewhere) // Also try repo-relative (target may run elsewhere)
let p2 = PathBuf::from("../tools/llvmlite_harness.py"); let p2 = PathBuf::from("../tools/llvmlite_harness.py");
if p2.exists() { return Some(p2); } if p2.exists() {
return Some(p2);
}
None None
} }
@ -303,17 +405,27 @@ fn mir_json_to_object_llvmlite(mir_json: &str, opts: &Opts) -> Result<PathBuf, S
let tmp_dir = std::env::temp_dir(); let tmp_dir = std::env::temp_dir();
let in_path = tmp_dir.join("hako_llvm_in.json"); let in_path = tmp_dir.join("hako_llvm_in.json");
{ {
let mut f = fs::File::create(&in_path).map_err(|e| format!("[llvmemit/tmp/write-failed] {}", e))?; let mut f =
f.write_all(mir_json.as_bytes()).map_err(|e| format!("[llvmemit/tmp/write-failed] {}", e))?; fs::File::create(&in_path).map_err(|e| format!("[llvmemit/tmp/write-failed] {}", e))?;
f.write_all(mir_json.as_bytes())
.map_err(|e| format!("[llvmemit/tmp/write-failed] {}", e))?;
}
let out_path = if let Some(p) = opts.out.clone() {
p
} else {
tmp_dir.join("hako_llvm_out.o")
};
if let Some(parent) = out_path.parent() {
let _ = fs::create_dir_all(parent);
} }
let out_path = if let Some(p) = opts.out.clone() { p } else { tmp_dir.join("hako_llvm_out.o") };
if let Some(parent) = out_path.parent() { let _ = fs::create_dir_all(parent); }
// Run: python3 tools/llvmlite_harness.py --in <json> --out <out> // Run: python3 tools/llvmlite_harness.py --in <json> --out <out>
let status = Command::new(&py) let status = Command::new(&py)
.arg(&harness) .arg(&harness)
.arg("--in").arg(&in_path) .arg("--in")
.arg("--out").arg(&out_path) .arg(&in_path)
.arg("--out")
.arg(&out_path)
.status() .status()
.map_err(|e| format!("[llvmemit/llvmlite/spawn/error] {}", e))?; .map_err(|e| format!("[llvmemit/llvmlite/spawn/error] {}", e))?;
if !status.success() { if !status.success() {

View File

@ -11,7 +11,10 @@ pub fn program_json_to_mir_json(program_json: &str) -> Result<String, String> {
} }
/// Convert Program(JSON v0) to MIR(JSON v0) with using imports support. /// Convert Program(JSON v0) to MIR(JSON v0) with using imports support.
pub fn program_json_to_mir_json_with_imports(program_json: &str, imports: HashMap<String, String>) -> Result<String, String> { pub fn program_json_to_mir_json_with_imports(
program_json: &str,
imports: HashMap<String, String>,
) -> Result<String, String> {
// Basic header check // Basic header check
if !program_json.contains("\"version\"") || !program_json.contains("\"kind\"") { if !program_json.contains("\"version\"") || !program_json.contains("\"kind\"") {
let tag = "[mirbuilder/input/invalid] missing version/kind keys"; let tag = "[mirbuilder/input/invalid] missing version/kind keys";
@ -20,7 +23,8 @@ pub fn program_json_to_mir_json_with_imports(program_json: &str, imports: HashMa
} }
// Parse Program(JSON v0) into a MIR Module with imports // Parse Program(JSON v0) into a MIR Module with imports
let module = match runner::json_v0_bridge::parse_json_v0_to_module_with_imports(program_json, imports) { let module =
match runner::json_v0_bridge::parse_json_v0_to_module_with_imports(program_json, imports) {
Ok(m) => m, Ok(m) => m,
Err(e) => { Err(e) => {
let tag = format!("[mirbuilder/parse/error] {}", e); let tag = format!("[mirbuilder/parse/error] {}", e);
@ -57,8 +61,12 @@ pub fn program_json_to_mir_json_with_imports(program_json: &str, imports: HashMa
if let Some(funcs) = m.get("functions").cloned() { if let Some(funcs) = m.get("functions").cloned() {
let v1 = serde_json::json!({"schema_version":"1.0","functions": funcs}); let v1 = serde_json::json!({"schema_version":"1.0","functions": funcs});
serde_json::to_string(&v1).unwrap_or(s0) serde_json::to_string(&v1).unwrap_or(s0)
} else { s0 } } else {
} else { s0 } s0
}
} else {
s0
}
} }
_ => s0, _ => s0,
}; };
@ -128,7 +136,10 @@ mod tests {
let mir_json = result.unwrap(); let mir_json = result.unwrap();
// MIR JSON should contain functions // MIR JSON should contain functions
assert!(mir_json.contains("functions"), "MIR JSON should contain functions"); assert!(
mir_json.contains("functions"),
"MIR JSON should contain functions"
);
eprintln!("[test] MIR JSON generated successfully with MatI64 imports"); eprintln!("[test] MIR JSON generated successfully with MatI64 imports");
} }
} }

View File

@ -1,3 +1,2 @@
pub mod mir_builder;
pub mod llvm_codegen; pub mod llvm_codegen;
pub mod mir_builder;

View File

@ -108,7 +108,11 @@ impl InstanceBox {
base: BoxBase::new(), base: BoxBase::new(),
finalized: Arc::new(Mutex::new(false)), finalized: Arc::new(Mutex::new(false)),
// レガシー互換フィールド既定OFF // レガシー互換フィールド既定OFF
fields: if legacy_enabled { Some(Arc::new(Mutex::new(legacy_field_map))) } else { None }, 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)),
@ -240,10 +244,7 @@ impl InstanceBox {
} }
/// レガシー互換weak field取得 /// レガシー互換weak field取得
pub fn get_weak_field( pub fn get_weak_field(&self, field_name: &str) -> Option<NyashValue> {
&self,
field_name: &str,
) -> Option<NyashValue> {
self.get_field_ng(field_name) self.get_field_ng(field_name)
} }

View File

@ -76,7 +76,9 @@ pub mod host_providers;
pub mod providers; pub mod providers;
// CABI PoC shim (20.36/20.37) // CABI PoC shim (20.36/20.37)
pub mod abi { pub mod nyrt_shim; } pub mod abi {
pub mod nyrt_shim;
}
// Expose the macro engine module under a raw identifier; the source lives under `src/macro/`. // Expose the macro engine module under a raw identifier; the source lives under `src/macro/`.
#[path = "macro/mod.rs"] #[path = "macro/mod.rs"]
@ -91,12 +93,8 @@ pub mod tests;
// Re-export main types for easy access // Re-export main types for easy access
pub use ast::{ASTNode, BinaryOperator, LiteralValue}; pub use ast::{ASTNode, BinaryOperator, LiteralValue};
pub use box_arithmetic::{AddBox, CompareBox, DivideBox, ModuloBox, MultiplyBox, SubtractBox}; pub use box_arithmetic::{AddBox, CompareBox, DivideBox, ModuloBox, MultiplyBox, SubtractBox};
pub use box_trait::{BoolBox, IntegerBox, NyashBox, StringBox, VoidBox};
pub use environment::{Environment, PythonCompatEnvironment};
pub use box_factory::RuntimeError; pub use box_factory::RuntimeError;
pub use parser::{NyashParser, ParseError}; pub use box_trait::{BoolBox, IntegerBox, NyashBox, StringBox, VoidBox};
pub use tokenizer::{NyashTokenizer, Token, TokenType};
pub use type_box::{MethodSignature, TypeBox, TypeRegistry}; // 🌟 TypeBox exports
pub use boxes::console_box::ConsoleBox; pub use boxes::console_box::ConsoleBox;
pub use boxes::debug_box::DebugBox; pub use boxes::debug_box::DebugBox;
pub use boxes::map_box::MapBox; pub use boxes::map_box::MapBox;
@ -106,8 +104,12 @@ pub use boxes::random_box::RandomBox;
pub use boxes::sound_box::SoundBox; pub use boxes::sound_box::SoundBox;
pub use boxes::time_box::{DateTimeBox, TimeBox, TimerBox}; pub use boxes::time_box::{DateTimeBox, TimeBox, TimerBox};
pub use channel_box::{ChannelBox, MessageBox}; pub use channel_box::{ChannelBox, MessageBox};
pub use environment::{Environment, PythonCompatEnvironment};
pub use instance_v2::InstanceBox; // 🎯 新実装テストnyash_rustパス使用 pub use instance_v2::InstanceBox; // 🎯 新実装テストnyash_rustパス使用
pub use method_box::{BoxType, EphemeralInstance, FunctionDefinition, MethodBox}; pub use method_box::{BoxType, EphemeralInstance, FunctionDefinition, MethodBox};
pub use parser::{NyashParser, ParseError};
pub use tokenizer::{NyashTokenizer, Token, TokenType};
pub use type_box::{MethodSignature, TypeBox, TypeRegistry}; // 🌟 TypeBox exports
pub use value::NyashValue; pub use value::NyashValue;

View File

@ -1,5 +1,5 @@
use nyash_rust::ast::{ASTNode, BinaryOperator, LiteralValue, Span, UnaryOperator};
use serde_json::{json, Value}; use serde_json::{json, Value};
use nyash_rust::ast::{ASTNode, LiteralValue, BinaryOperator, UnaryOperator, Span};
pub fn ast_to_json(ast: &ASTNode) -> Value { pub fn ast_to_json(ast: &ASTNode) -> Value {
match ast.clone() { match ast.clone() {
@ -7,7 +7,9 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
"kind": "Program", "kind": "Program",
"statements": statements.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>() "statements": statements.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>()
}), }),
ASTNode::Loop { condition, body, .. } => json!({ ASTNode::Loop {
condition, body, ..
} => json!({
"kind": "Loop", "kind": "Loop",
"condition": ast_to_json(&condition), "condition": ast_to_json(&condition),
"body": body.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>() "body": body.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>()
@ -27,18 +29,32 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
"target": ast_to_json(&target), "target": ast_to_json(&target),
"value": ast_to_json(&value), "value": ast_to_json(&value),
}), }),
ASTNode::Local { variables, initial_values, .. } => json!({ ASTNode::Local {
variables,
initial_values,
..
} => json!({
"kind": "Local", "kind": "Local",
"variables": variables, "variables": variables,
"inits": initial_values.into_iter().map(|opt| opt.map(|v| ast_to_json(&v))).collect::<Vec<_>>() "inits": initial_values.into_iter().map(|opt| opt.map(|v| ast_to_json(&v))).collect::<Vec<_>>()
}), }),
ASTNode::If { condition, then_body, else_body, .. } => json!({ ASTNode::If {
condition,
then_body,
else_body,
..
} => json!({
"kind": "If", "kind": "If",
"condition": ast_to_json(&condition), "condition": ast_to_json(&condition),
"then": then_body.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>(), "then": then_body.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>(),
"else": else_body.map(|v| v.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>()), "else": else_body.map(|v| v.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>()),
}), }),
ASTNode::TryCatch { try_body, catch_clauses, finally_body, .. } => json!({ ASTNode::TryCatch {
try_body,
catch_clauses,
finally_body,
..
} => json!({
"kind": "TryCatch", "kind": "TryCatch",
"try": try_body.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>(), "try": try_body.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>(),
"catch": catch_clauses.into_iter().map(|cc| json!({ "catch": catch_clauses.into_iter().map(|cc| json!({
@ -48,7 +64,14 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
})).collect::<Vec<_>>(), })).collect::<Vec<_>>(),
"cleanup": finally_body.map(|v| v.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>()) "cleanup": finally_body.map(|v| v.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>())
}), }),
ASTNode::FunctionDeclaration { name, params, body, is_static, is_override, .. } => json!({ ASTNode::FunctionDeclaration {
name,
params,
body,
is_static,
is_override,
..
} => json!({
"kind": "FunctionDeclaration", "kind": "FunctionDeclaration",
"name": name, "name": name,
"params": params, "params": params,
@ -58,24 +81,38 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
}), }),
ASTNode::Variable { name, .. } => json!({"kind":"Variable","name":name}), ASTNode::Variable { name, .. } => json!({"kind":"Variable","name":name}),
ASTNode::Literal { value, .. } => json!({"kind":"Literal","value": lit_to_json(&value)}), ASTNode::Literal { value, .. } => json!({"kind":"Literal","value": lit_to_json(&value)}),
ASTNode::BinaryOp { operator, left, right, .. } => json!({ ASTNode::BinaryOp {
operator,
left,
right,
..
} => json!({
"kind":"BinaryOp", "kind":"BinaryOp",
"op": bin_to_str(&operator), "op": bin_to_str(&operator),
"left": ast_to_json(&left), "left": ast_to_json(&left),
"right": ast_to_json(&right), "right": ast_to_json(&right),
}), }),
ASTNode::UnaryOp { operator, operand, .. } => json!({ ASTNode::UnaryOp {
operator, operand, ..
} => json!({
"kind":"UnaryOp", "kind":"UnaryOp",
"op": un_to_str(&operator), "op": un_to_str(&operator),
"operand": ast_to_json(&operand), "operand": ast_to_json(&operand),
}), }),
ASTNode::MethodCall { object, method, arguments, .. } => json!({ ASTNode::MethodCall {
object,
method,
arguments,
..
} => json!({
"kind":"MethodCall", "kind":"MethodCall",
"object": ast_to_json(&object), "object": ast_to_json(&object),
"method": method, "method": method,
"arguments": arguments.into_iter().map(|a| ast_to_json(&a)).collect::<Vec<_>>() "arguments": arguments.into_iter().map(|a| ast_to_json(&a)).collect::<Vec<_>>()
}), }),
ASTNode::FunctionCall { name, arguments, .. } => json!({ ASTNode::FunctionCall {
name, arguments, ..
} => json!({
"kind":"FunctionCall", "kind":"FunctionCall",
"name": name, "name": name,
"arguments": arguments.into_iter().map(|a| ast_to_json(&a)).collect::<Vec<_>>() "arguments": arguments.into_iter().map(|a| ast_to_json(&a)).collect::<Vec<_>>()
@ -88,7 +125,12 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
"kind":"Map", "kind":"Map",
"entries": entries.into_iter().map(|(k,v)| json!({"k":k,"v":ast_to_json(&v)})).collect::<Vec<_>>() "entries": entries.into_iter().map(|(k,v)| json!({"k":k,"v":ast_to_json(&v)})).collect::<Vec<_>>()
}), }),
ASTNode::MatchExpr { scrutinee, arms, else_expr, .. } => json!({ ASTNode::MatchExpr {
scrutinee,
arms,
else_expr,
..
} => json!({
"kind":"MatchExpr", "kind":"MatchExpr",
"scrutinee": ast_to_json(&scrutinee), "scrutinee": ast_to_json(&scrutinee),
"arms": arms.into_iter().map(|(lit, body)| json!({ "arms": arms.into_iter().map(|(lit, body)| json!({
@ -108,45 +150,163 @@ pub fn json_to_ast(v: &Value) -> Option<ASTNode> {
let k = v.get("kind")?.as_str()?; let k = v.get("kind")?.as_str()?;
Some(match k { Some(match k {
"Program" => { "Program" => {
let stmts = v.get("statements")?.as_array()?.iter().filter_map(json_to_ast).collect::<Vec<_>>(); let stmts = v
ASTNode::Program { statements: stmts, span: Span::unknown() } .get("statements")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect::<Vec<_>>();
ASTNode::Program {
statements: stmts,
span: Span::unknown(),
}
} }
"Loop" => ASTNode::Loop { "Loop" => ASTNode::Loop {
condition: Box::new(json_to_ast(v.get("condition")?)?), condition: Box::new(json_to_ast(v.get("condition")?)?),
body: v.get("body")?.as_array()?.iter().filter_map(json_to_ast).collect::<Vec<_>>(), body: v
.get("body")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect::<Vec<_>>(),
span: Span::unknown(), span: Span::unknown(),
}, },
"Print" => ASTNode::Print { expression: Box::new(json_to_ast(v.get("expression")?)?), span: Span::unknown() }, "Print" => ASTNode::Print {
"Return" => ASTNode::Return { value: v.get("value").and_then(json_to_ast).map(Box::new), span: Span::unknown() }, expression: Box::new(json_to_ast(v.get("expression")?)?),
"Break" => ASTNode::Break { span: Span::unknown() }, span: Span::unknown(),
"Continue" => ASTNode::Continue { span: Span::unknown() }, },
"Assignment" => ASTNode::Assignment { target: Box::new(json_to_ast(v.get("target")?)?), value: Box::new(json_to_ast(v.get("value")?)?), span: Span::unknown() }, "Return" => ASTNode::Return {
"Local" => { value: v.get("value").and_then(json_to_ast).map(Box::new),
let vars = v.get("variables")?.as_array()?.iter().filter_map(|s| s.as_str().map(|x| x.to_string())).collect(); span: Span::unknown(),
let inits = v.get("inits")?.as_array()?.iter().map(|initv| { },
if initv.is_null() { None } else { json_to_ast(initv).map(Box::new) } "Break" => ASTNode::Break {
}).collect(); span: Span::unknown(),
ASTNode::Local { variables: vars, initial_values: inits, span: Span::unknown() } },
"Continue" => ASTNode::Continue {
span: Span::unknown(),
},
"Assignment" => ASTNode::Assignment {
target: Box::new(json_to_ast(v.get("target")?)?),
value: Box::new(json_to_ast(v.get("value")?)?),
span: Span::unknown(),
},
"Local" => {
let vars = v
.get("variables")?
.as_array()?
.iter()
.filter_map(|s| s.as_str().map(|x| x.to_string()))
.collect();
let inits = v
.get("inits")?
.as_array()?
.iter()
.map(|initv| {
if initv.is_null() {
None
} else {
json_to_ast(initv).map(Box::new)
}
})
.collect();
ASTNode::Local {
variables: vars,
initial_values: inits,
span: Span::unknown(),
}
}
"If" => ASTNode::If {
condition: Box::new(json_to_ast(v.get("condition")?)?),
then_body: v
.get("then")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect::<Vec<_>>(),
else_body: v.get("else").and_then(|a| {
a.as_array()
.map(|arr| arr.iter().filter_map(json_to_ast).collect::<Vec<_>>())
}),
span: Span::unknown(),
}, },
"If" => ASTNode::If { condition: Box::new(json_to_ast(v.get("condition")?)?), then_body: v.get("then")?.as_array()?.iter().filter_map(json_to_ast).collect::<Vec<_>>(), else_body: v.get("else").and_then(|a| a.as_array().map(|arr| arr.iter().filter_map(json_to_ast).collect::<Vec<_>>())), span: Span::unknown() },
"FunctionDeclaration" => ASTNode::FunctionDeclaration { "FunctionDeclaration" => ASTNode::FunctionDeclaration {
name: v.get("name")?.as_str()?.to_string(), name: v.get("name")?.as_str()?.to_string(),
params: v.get("params")?.as_array()?.iter().filter_map(|s| s.as_str().map(|x| x.to_string())).collect(), params: v
body: v.get("body")?.as_array()?.iter().filter_map(json_to_ast).collect(), .get("params")?
.as_array()?
.iter()
.filter_map(|s| s.as_str().map(|x| x.to_string()))
.collect(),
body: v
.get("body")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect(),
is_static: v.get("static").and_then(|b| b.as_bool()).unwrap_or(false), is_static: v.get("static").and_then(|b| b.as_bool()).unwrap_or(false),
is_override: v.get("override").and_then(|b| b.as_bool()).unwrap_or(false), is_override: v.get("override").and_then(|b| b.as_bool()).unwrap_or(false),
span: Span::unknown(), span: Span::unknown(),
}, },
"Variable" => ASTNode::Variable { name: v.get("name")?.as_str()?.to_string(), span: Span::unknown() }, "Variable" => ASTNode::Variable {
"Literal" => ASTNode::Literal { value: json_to_lit(v.get("value")?)?, span: Span::unknown() }, name: v.get("name")?.as_str()?.to_string(),
"BinaryOp" => ASTNode::BinaryOp { operator: str_to_bin(v.get("op")?.as_str()?)?, left: Box::new(json_to_ast(v.get("left")?)?), right: Box::new(json_to_ast(v.get("right")?)?), span: Span::unknown() }, span: Span::unknown(),
"UnaryOp" => ASTNode::UnaryOp { operator: str_to_un(v.get("op")?.as_str()?)?, operand: Box::new(json_to_ast(v.get("operand")?)?), span: Span::unknown() }, },
"MethodCall" => ASTNode::MethodCall { object: Box::new(json_to_ast(v.get("object")?)?), method: v.get("method")?.as_str()?.to_string(), arguments: v.get("arguments")?.as_array()?.iter().filter_map(json_to_ast).collect(), span: Span::unknown() }, "Literal" => ASTNode::Literal {
"FunctionCall" => ASTNode::FunctionCall { name: v.get("name")?.as_str()?.to_string(), arguments: v.get("arguments")?.as_array()?.iter().filter_map(json_to_ast).collect(), span: Span::unknown() }, value: json_to_lit(v.get("value")?)?,
"Array" => ASTNode::ArrayLiteral { elements: v.get("elements")?.as_array()?.iter().filter_map(json_to_ast).collect(), span: Span::unknown() }, span: Span::unknown(),
"Map" => ASTNode::MapLiteral { entries: v.get("entries")?.as_array()?.iter().filter_map(|e| { },
"BinaryOp" => ASTNode::BinaryOp {
operator: str_to_bin(v.get("op")?.as_str()?)?,
left: Box::new(json_to_ast(v.get("left")?)?),
right: Box::new(json_to_ast(v.get("right")?)?),
span: Span::unknown(),
},
"UnaryOp" => ASTNode::UnaryOp {
operator: str_to_un(v.get("op")?.as_str()?)?,
operand: Box::new(json_to_ast(v.get("operand")?)?),
span: Span::unknown(),
},
"MethodCall" => ASTNode::MethodCall {
object: Box::new(json_to_ast(v.get("object")?)?),
method: v.get("method")?.as_str()?.to_string(),
arguments: v
.get("arguments")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect(),
span: Span::unknown(),
},
"FunctionCall" => ASTNode::FunctionCall {
name: v.get("name")?.as_str()?.to_string(),
arguments: v
.get("arguments")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect(),
span: Span::unknown(),
},
"Array" => ASTNode::ArrayLiteral {
elements: v
.get("elements")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect(),
span: Span::unknown(),
},
"Map" => ASTNode::MapLiteral {
entries: v
.get("entries")?
.as_array()?
.iter()
.filter_map(|e| {
Some((e.get("k")?.as_str()?.to_string(), json_to_ast(e.get("v")?)?)) Some((e.get("k")?.as_str()?.to_string(), json_to_ast(e.get("v")?)?))
}).collect(), span: Span::unknown() }, })
.collect(),
span: Span::unknown(),
},
"MatchExpr" => { "MatchExpr" => {
let scr = json_to_ast(v.get("scrutinee")?)?; let scr = json_to_ast(v.get("scrutinee")?)?;
let arms_json = v.get("arms")?.as_array()?.iter(); let arms_json = v.get("arms")?.as_array()?.iter();
@ -166,18 +326,47 @@ pub fn json_to_ast(v: &Value) -> Option<ASTNode> {
} }
} }
"TryCatch" => { "TryCatch" => {
let try_b = v.get("try")?.as_array()?.iter().filter_map(json_to_ast).collect::<Vec<_>>(); let try_b = v
.get("try")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect::<Vec<_>>();
let mut catches = Vec::new(); let mut catches = Vec::new();
if let Some(arr) = v.get("catch").and_then(|x| x.as_array()) { if let Some(arr) = v.get("catch").and_then(|x| x.as_array()) {
for c in arr.iter() { for c in arr.iter() {
let exc_t = match c.get("type") { Some(t) if !t.is_null() => t.as_str().map(|s| s.to_string()), _ => None }; let exc_t = match c.get("type") {
let var = match c.get("var") { Some(vv) if !vv.is_null() => vv.as_str().map(|s| s.to_string()), _ => None }; Some(t) if !t.is_null() => t.as_str().map(|s| s.to_string()),
let body = c.get("body")?.as_array()?.iter().filter_map(json_to_ast).collect::<Vec<_>>(); _ => None,
catches.push(nyash_rust::ast::CatchClause { exception_type: exc_t, variable_name: var, body, span: Span::unknown() }); };
let var = match c.get("var") {
Some(vv) if !vv.is_null() => vv.as_str().map(|s| s.to_string()),
_ => None,
};
let body = c
.get("body")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect::<Vec<_>>();
catches.push(nyash_rust::ast::CatchClause {
exception_type: exc_t,
variable_name: var,
body,
span: Span::unknown(),
});
} }
} }
let cleanup = v.get("cleanup").and_then(|cl| cl.as_array().map(|arr| arr.iter().filter_map(json_to_ast).collect::<Vec<_>>())); let cleanup = v.get("cleanup").and_then(|cl| {
ASTNode::TryCatch { try_body: try_b, catch_clauses: catches, finally_body: cleanup, span: Span::unknown() } cl.as_array()
.map(|arr| arr.iter().filter_map(json_to_ast).collect::<Vec<_>>())
});
ASTNode::TryCatch {
try_body: try_b,
catch_clauses: catches,
finally_body: cleanup,
span: Span::unknown(),
}
} }
_ => return None, _ => return None,
}) })

View File

@ -32,14 +32,18 @@ impl MacroCtx {
} }
} }
pub fn gensym(&self, prefix: &str) -> String { gensym(prefix) } pub fn gensym(&self, prefix: &str) -> String {
gensym(prefix)
}
pub fn report(&self, level: &str, message: &str) { pub fn report(&self, level: &str, message: &str) {
eprintln!("[macro][{}] {}", level, message); eprintln!("[macro][{}] {}", level, message);
} }
pub fn get_env(&self, key: &str) -> Option<String> { pub fn get_env(&self, key: &str) -> Option<String> {
if !self.caps.env { return None; } if !self.caps.env {
return None;
}
std::env::var(key).ok() std::env::var(key).ok()
} }
} }

View File

@ -1,5 +1,5 @@
use nyash_rust::ast::Span; use nyash_rust::ast::Span;
use nyash_rust::{ASTNode, ast::LiteralValue, ast::BinaryOperator}; use nyash_rust::{ast::BinaryOperator, ast::LiteralValue, ASTNode};
use std::time::Instant; use std::time::Instant;
/// HIR Patch description (MVP placeholder) /// HIR Patch description (MVP placeholder)
@ -16,10 +16,20 @@ pub struct MacroEngine {
impl MacroEngine { impl MacroEngine {
pub fn new() -> Self { pub fn new() -> Self {
let max_passes = std::env::var("NYASH_MACRO_MAX_PASSES").ok().and_then(|v| v.parse().ok()).unwrap_or(32); let max_passes = std::env::var("NYASH_MACRO_MAX_PASSES")
let cycle_window = std::env::var("NYASH_MACRO_CYCLE_WINDOW").ok().and_then(|v| v.parse().ok()).unwrap_or(8); .ok()
.and_then(|v| v.parse().ok())
.unwrap_or(32);
let cycle_window = std::env::var("NYASH_MACRO_CYCLE_WINDOW")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(8);
let trace = std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1"); let trace = std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1");
Self { max_passes, cycle_window, trace } Self {
max_passes,
cycle_window,
trace,
}
} }
/// Expand all macros with depth/cycle guards and return patched AST. /// Expand all macros with depth/cycle guards and return patched AST.
@ -29,25 +39,48 @@ impl MacroEngine {
let mut history: std::collections::VecDeque<ASTNode> = std::collections::VecDeque::new(); let mut history: std::collections::VecDeque<ASTNode> = std::collections::VecDeque::new();
for pass in 0..self.max_passes { for pass in 0..self.max_passes {
let t0 = Instant::now(); let t0 = Instant::now();
let before_len = crate::r#macro::ast_json::ast_to_json(&cur).to_string().len(); let before_len = crate::r#macro::ast_json::ast_to_json(&cur)
.to_string()
.len();
let next0 = self.expand_node(&cur); let next0 = self.expand_node(&cur);
// Apply user MacroBoxes once per pass (if enabled) // Apply user MacroBoxes once per pass (if enabled)
let next = crate::r#macro::macro_box::expand_all_once(&next0); let next = crate::r#macro::macro_box::expand_all_once(&next0);
let after_len = crate::r#macro::ast_json::ast_to_json(&next).to_string().len(); let after_len = crate::r#macro::ast_json::ast_to_json(&next)
.to_string()
.len();
let dt = t0.elapsed(); let dt = t0.elapsed();
if self.trace { eprintln!("[macro][engine] pass={} changed={} bytes:{}=>{} dt={:?}", pass, (next != cur), before_len, after_len, dt); } if self.trace {
eprintln!(
"[macro][engine] pass={} changed={} bytes:{}=>{} dt={:?}",
pass,
(next != cur),
before_len,
after_len,
dt
);
}
jsonl_trace(pass, before_len, after_len, next != cur, dt); jsonl_trace(pass, before_len, after_len, next != cur, dt);
if next == cur { return (cur, patches); } if next == cur {
return (cur, patches);
}
// cycle detection in small window // cycle detection in small window
if history.iter().any(|h| *h == next) { if history.iter().any(|h| *h == next) {
eprintln!("[macro][engine] cycle detected at pass {} — stopping expansion", pass); eprintln!(
"[macro][engine] cycle detected at pass {} — stopping expansion",
pass
);
return (cur, patches); return (cur, patches);
} }
history.push_back(cur); history.push_back(cur);
if history.len() > self.cycle_window { let _ = history.pop_front(); } if history.len() > self.cycle_window {
let _ = history.pop_front();
}
cur = next; cur = next;
} }
eprintln!("[macro][engine] max passes ({}) exceeded — stopping expansion", self.max_passes); eprintln!(
"[macro][engine] max passes ({}) exceeded — stopping expansion",
self.max_passes
);
(cur, patches) (cur, patches)
} }
@ -57,23 +90,55 @@ impl MacroEngine {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][visit] Program: statements={}", statements.len()); eprintln!("[macro][visit] Program: statements={}", statements.len());
} }
let new_stmts = statements.into_iter().map(|n| { let new_stmts = statements
.into_iter()
.map(|n| {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][visit] child kind...",); eprintln!("[macro][visit] child kind...",);
} }
self.expand_node(&n) self.expand_node(&n)
}).collect(); })
ASTNode::Program { statements: new_stmts, span } .collect();
ASTNode::Program {
statements: new_stmts,
span,
} }
ASTNode::BoxDeclaration { name, fields, public_fields, private_fields, mut methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, is_static, static_init, span } => { }
ASTNode::BoxDeclaration {
name,
fields,
public_fields,
private_fields,
mut methods,
constructors,
init_fields,
weak_fields,
is_interface,
extends,
implements,
type_parameters,
is_static,
static_init,
span,
} => {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][visit] BoxDeclaration: {} (fields={})", name, fields.len()); eprintln!(
"[macro][visit] BoxDeclaration: {} (fields={})",
name,
fields.len()
);
} }
// Derive set: default Equals+ToString when macro is enabled // Derive set: default Equals+ToString when macro is enabled
let derive_all = std::env::var("NYASH_MACRO_DERIVE_ALL").ok().as_deref() == Some("1"); let derive_all =
let derive_set = std::env::var("NYASH_MACRO_DERIVE").ok().unwrap_or_else(|| "Equals,ToString".to_string()); std::env::var("NYASH_MACRO_DERIVE_ALL").ok().as_deref() == Some("1");
let derive_set = std::env::var("NYASH_MACRO_DERIVE")
.ok()
.unwrap_or_else(|| "Equals,ToString".to_string());
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][derive] box={} derive_all={} set={}", name, derive_all, derive_set); eprintln!(
"[macro][derive] box={} derive_all={} set={}",
name, derive_all, derive_set
);
} }
let want_equals = derive_all || derive_set.contains("Equals"); let want_equals = derive_all || derive_set.contains("Equals");
let want_tostring = derive_all || derive_set.contains("ToString"); let want_tostring = derive_all || derive_set.contains("ToString");
@ -81,19 +146,43 @@ impl MacroEngine {
let field_view: &Vec<String> = &public_fields; let field_view: &Vec<String> = &public_fields;
if want_equals && !methods.contains_key("equals") { if want_equals && !methods.contains_key("equals") {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][derive] equals for {} (public fields: {})", name, field_view.len()); eprintln!(
"[macro][derive] equals for {} (public fields: {})",
name,
field_view.len()
);
} }
let m = build_equals_method(&name, field_view); let m = build_equals_method(&name, field_view);
methods.insert("equals".to_string(), m); methods.insert("equals".to_string(), m);
} }
if want_tostring && !methods.contains_key("toString") { if want_tostring && !methods.contains_key("toString") {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][derive] toString for {} (public fields: {})", name, field_view.len()); eprintln!(
"[macro][derive] toString for {} (public fields: {})",
name,
field_view.len()
);
} }
let m = build_tostring_method(&name, field_view); let m = build_tostring_method(&name, field_view);
methods.insert("toString".to_string(), m); methods.insert("toString".to_string(), m);
} }
ASTNode::BoxDeclaration { name, fields, public_fields, private_fields, methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, is_static, static_init, span } ASTNode::BoxDeclaration {
name,
fields,
public_fields,
private_fields,
methods,
constructors,
init_fields,
weak_fields,
is_interface,
extends,
implements,
type_parameters,
is_static,
static_init,
span,
}
} }
other => other, other => other,
} }
@ -102,7 +191,9 @@ impl MacroEngine {
fn jsonl_trace(pass: usize, before: usize, after: usize, changed: bool, dt: std::time::Duration) { fn jsonl_trace(pass: usize, before: usize, after: usize, changed: bool, dt: std::time::Duration) {
if let Ok(path) = std::env::var("NYASH_MACRO_TRACE_JSONL") { if let Ok(path) = std::env::var("NYASH_MACRO_TRACE_JSONL") {
if path.is_empty() { return; } if path.is_empty() {
return;
}
let rec = serde_json::json!({ let rec = serde_json::json!({
"event": "macro_pass", "event": "macro_pass",
"pass": pass, "pass": pass,
@ -110,8 +201,13 @@ fn jsonl_trace(pass: usize, before: usize, after: usize, changed: bool, dt: std:
"before_bytes": before, "before_bytes": before,
"after_bytes": after, "after_bytes": after,
"dt_us": dt.as_micros() as u64, "dt_us": dt.as_micros() as u64,
}).to_string(); })
let _ = std::fs::OpenOptions::new().create(true).append(true).open(path).and_then(|mut f| { .to_string();
let _ = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.and_then(|mut f| {
use std::io::Write; use std::io::Write;
writeln!(f, "{}", rec) writeln!(f, "{}", rec)
}); });
@ -120,7 +216,9 @@ fn jsonl_trace(pass: usize, before: usize, after: usize, changed: bool, dt: std:
fn me_field(name: &str) -> ASTNode { fn me_field(name: &str) -> ASTNode {
ASTNode::FieldAccess { ASTNode::FieldAccess {
object: Box::new(ASTNode::Me { span: Span::unknown() }), object: Box::new(ASTNode::Me {
span: Span::unknown(),
}),
field: name.to_string(), field: name.to_string(),
span: Span::unknown(), span: Span::unknown(),
} }
@ -128,30 +226,56 @@ fn me_field(name: &str) -> ASTNode {
fn var_field(var: &str, field: &str) -> ASTNode { fn var_field(var: &str, field: &str) -> ASTNode {
ASTNode::FieldAccess { ASTNode::FieldAccess {
object: Box::new(ASTNode::Variable { name: var.to_string(), span: Span::unknown() }), object: Box::new(ASTNode::Variable {
name: var.to_string(),
span: Span::unknown(),
}),
field: field.to_string(), field: field.to_string(),
span: Span::unknown(), span: Span::unknown(),
} }
} }
fn bin_add(lhs: ASTNode, rhs: ASTNode) -> ASTNode { fn bin_add(lhs: ASTNode, rhs: ASTNode) -> ASTNode {
ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(lhs), right: Box::new(rhs), span: Span::unknown() } ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(lhs),
right: Box::new(rhs),
span: Span::unknown(),
}
} }
fn bin_and(lhs: ASTNode, rhs: ASTNode) -> ASTNode { fn bin_and(lhs: ASTNode, rhs: ASTNode) -> ASTNode {
ASTNode::BinaryOp { operator: BinaryOperator::And, left: Box::new(lhs), right: Box::new(rhs), span: Span::unknown() } ASTNode::BinaryOp {
operator: BinaryOperator::And,
left: Box::new(lhs),
right: Box::new(rhs),
span: Span::unknown(),
}
} }
fn bin_eq(lhs: ASTNode, rhs: ASTNode) -> ASTNode { fn bin_eq(lhs: ASTNode, rhs: ASTNode) -> ASTNode {
ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(lhs), right: Box::new(rhs), span: Span::unknown() } ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(lhs),
right: Box::new(rhs),
span: Span::unknown(),
}
} }
fn lit_str(s: &str) -> ASTNode { ASTNode::Literal { value: LiteralValue::String(s.to_string()), span: Span::unknown() } } fn lit_str(s: &str) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::String(s.to_string()),
span: Span::unknown(),
}
}
fn build_equals_method(_box_name: &str, fields: &Vec<String>) -> ASTNode { fn build_equals_method(_box_name: &str, fields: &Vec<String>) -> ASTNode {
// equals(other) { return me.f1 == other.f1 && ...; } // equals(other) { return me.f1 == other.f1 && ...; }
let cond = if fields.is_empty() { let cond = if fields.is_empty() {
ASTNode::Literal { value: LiteralValue::Bool(true), span: Span::unknown() } ASTNode::Literal {
value: LiteralValue::Bool(true),
span: Span::unknown(),
}
} else { } else {
let mut it = fields.iter(); let mut it = fields.iter();
let first = it.next().unwrap(); let first = it.next().unwrap();
@ -166,7 +290,10 @@ fn build_equals_method(_box_name: &str, fields: &Vec<String>) -> ASTNode {
ASTNode::FunctionDeclaration { ASTNode::FunctionDeclaration {
name: "equals".to_string(), name: "equals".to_string(),
params: vec![param_name.clone()], params: vec![param_name.clone()],
body: vec![ASTNode::Return { value: Some(Box::new(cond)), span: Span::unknown() }], body: vec![ASTNode::Return {
value: Some(Box::new(cond)),
span: Span::unknown(),
}],
is_static: false, is_static: false,
is_override: false, is_override: false,
span: Span::unknown(), span: Span::unknown(),
@ -178,7 +305,9 @@ fn build_tostring_method(box_name: &str, fields: &Vec<String>) -> ASTNode {
let mut expr = lit_str(&format!("{}(", box_name)); let mut expr = lit_str(&format!("{}(", box_name));
let mut first = true; let mut first = true;
for f in fields { for f in fields {
if !first { expr = bin_add(expr, lit_str(",")); } if !first {
expr = bin_add(expr, lit_str(","));
}
first = false; first = false;
expr = bin_add(expr, me_field(f)); expr = bin_add(expr, me_field(f));
} }
@ -186,7 +315,10 @@ fn build_tostring_method(box_name: &str, fields: &Vec<String>) -> ASTNode {
ASTNode::FunctionDeclaration { ASTNode::FunctionDeclaration {
name: "toString".to_string(), name: "toString".to_string(),
params: vec![], params: vec![],
body: vec![ASTNode::Return { value: Some(Box::new(expr)), span: Span::unknown() }], body: vec![ASTNode::Return {
value: Some(Box::new(expr)),
span: Span::unknown(),
}],
is_static: false, is_static: false,
is_override: false, is_override: false,
span: Span::unknown(), span: Span::unknown(),

View File

@ -1,5 +1,5 @@
use std::sync::{Mutex, OnceLock};
use nyash_rust::ASTNode; use nyash_rust::ASTNode;
use std::sync::{Mutex, OnceLock};
/// MacroBox API — user-extensible macro expansion units (experimental) /// MacroBox API — user-extensible macro expansion units (experimental)
/// ///
@ -33,13 +33,17 @@ pub fn register(m: &'static dyn MacroBox) {
/// Legacy env `NYASH_MACRO_BOX=1` still forces ON, but by default we /// Legacy env `NYASH_MACRO_BOX=1` still forces ON, but by default we
/// synchronize with the macro system gate so user macros run when macros are enabled. /// synchronize with the macro system gate so user macros run when macros are enabled.
pub fn enabled() -> bool { pub fn enabled() -> bool {
if std::env::var("NYASH_MACRO_BOX").ok().as_deref() == Some("1") { return true; } if std::env::var("NYASH_MACRO_BOX").ok().as_deref() == Some("1") {
return true;
}
super::enabled() super::enabled()
} }
/// Expand AST by applying all registered MacroBoxes in order once. /// Expand AST by applying all registered MacroBoxes in order once.
pub fn expand_all_once(ast: &ASTNode) -> ASTNode { pub fn expand_all_once(ast: &ASTNode) -> ASTNode {
if !enabled() { return ast.clone(); } if !enabled() {
return ast.clone();
}
let reg = registry(); let reg = registry();
let guard = reg.lock().expect("macro registry poisoned"); let guard = reg.lock().expect("macro registry poisoned");
let mut cur = ast.clone(); let mut cur = ast.clone();
@ -55,39 +59,127 @@ pub fn expand_all_once(ast: &ASTNode) -> ASTNode {
pub struct UppercasePrintMacro; pub struct UppercasePrintMacro;
impl MacroBox for UppercasePrintMacro { impl MacroBox for UppercasePrintMacro {
fn name(&self) -> &'static str { "UppercasePrintMacro" } fn name(&self) -> &'static str {
"UppercasePrintMacro"
}
fn expand(&self, ast: &ASTNode) -> ASTNode { fn expand(&self, ast: &ASTNode) -> ASTNode {
use nyash_rust::ast::{ASTNode as A, LiteralValue, Span}; use nyash_rust::ast::{ASTNode as A, LiteralValue, Span};
fn go(n: &A) -> A { fn go(n: &A) -> A {
match n.clone() { match n.clone() {
A::Program { statements, span } => A::Program { statements: statements.into_iter().map(|c| go(&c)).collect(), span }, A::Program { statements, span } => A::Program {
statements: statements.into_iter().map(|c| go(&c)).collect(),
span,
},
A::Print { expression, span } => { A::Print { expression, span } => {
match &*expression { match &*expression {
A::Literal { value: LiteralValue::String(s), .. } => { A::Literal {
value: LiteralValue::String(s),
..
} => {
// Demo: if string starts with "UPPER:", uppercase the rest. // Demo: if string starts with "UPPER:", uppercase the rest.
if let Some(rest) = s.strip_prefix("UPPER:") { if let Some(rest) = s.strip_prefix("UPPER:") {
let up = rest.to_uppercase(); let up = rest.to_uppercase();
A::Print { expression: Box::new(A::Literal { value: LiteralValue::String(up), span: Span::unknown() }), span } A::Print {
} else { A::Print { expression: Box::new(go(&*expression)), span } } expression: Box::new(A::Literal {
value: LiteralValue::String(up),
span: Span::unknown(),
}),
span,
} }
other => A::Print { expression: Box::new(go(other)), span } } else {
A::Print {
expression: Box::new(go(&*expression)),
span,
} }
} }
A::Assignment { target, value, span } => A::Assignment { target: Box::new(go(&*target)), value: Box::new(go(&*value)), span }, }
A::If { condition, then_body, else_body, span } => A::If { other => A::Print {
expression: Box::new(go(other)),
span,
},
}
}
A::Assignment {
target,
value,
span,
} => A::Assignment {
target: Box::new(go(&*target)),
value: Box::new(go(&*value)),
span,
},
A::If {
condition,
then_body,
else_body,
span,
} => A::If {
condition: Box::new(go(&*condition)), condition: Box::new(go(&*condition)),
then_body: then_body.into_iter().map(|c| go(&c)).collect(), then_body: then_body.into_iter().map(|c| go(&c)).collect(),
else_body: else_body.map(|v| v.into_iter().map(|c| go(&c)).collect()), else_body: else_body.map(|v| v.into_iter().map(|c| go(&c)).collect()),
span, span,
}, },
A::Return { value, span } => A::Return { value: value.as_ref().map(|v| Box::new(go(v))), span }, A::Return { value, span } => A::Return {
A::FieldAccess { object, field, span } => A::FieldAccess { object: Box::new(go(&*object)), field, span }, value: value.as_ref().map(|v| Box::new(go(v))),
A::MethodCall { object, method, arguments, span } => A::MethodCall { object: Box::new(go(&*object)), method, arguments: arguments.into_iter().map(|c| go(&c)).collect(), span }, span,
A::FunctionCall { name, arguments, span } => A::FunctionCall { name, arguments: arguments.into_iter().map(|c| go(&c)).collect(), span }, },
A::BinaryOp { operator, left, right, span } => A::BinaryOp { operator, left: Box::new(go(&*left)), right: Box::new(go(&*right)), span }, A::FieldAccess {
A::UnaryOp { operator, operand, span } => A::UnaryOp { operator, operand: Box::new(go(&*operand)), span }, object,
A::ArrayLiteral { elements, span } => A::ArrayLiteral { elements: elements.into_iter().map(|c| go(&c)).collect(), span }, field,
A::MapLiteral { entries, span } => A::MapLiteral { entries: entries.into_iter().map(|(k,v)| (k, go(&v))).collect(), span }, span,
} => A::FieldAccess {
object: Box::new(go(&*object)),
field,
span,
},
A::MethodCall {
object,
method,
arguments,
span,
} => A::MethodCall {
object: Box::new(go(&*object)),
method,
arguments: arguments.into_iter().map(|c| go(&c)).collect(),
span,
},
A::FunctionCall {
name,
arguments,
span,
} => A::FunctionCall {
name,
arguments: arguments.into_iter().map(|c| go(&c)).collect(),
span,
},
A::BinaryOp {
operator,
left,
right,
span,
} => A::BinaryOp {
operator,
left: Box::new(go(&*left)),
right: Box::new(go(&*right)),
span,
},
A::UnaryOp {
operator,
operand,
span,
} => A::UnaryOp {
operator,
operand: Box::new(go(&*operand)),
span,
},
A::ArrayLiteral { elements, span } => A::ArrayLiteral {
elements: elements.into_iter().map(|c| go(&c)).collect(),
span,
},
A::MapLiteral { entries, span } => A::MapLiteral {
entries: entries.into_iter().map(|(k, v)| (k, go(&v))).collect(),
span,
},
other => other, other => other,
} }
} }
@ -101,13 +193,17 @@ static INIT_FLAG: OnceLock<()> = OnceLock::new();
pub fn init_builtin() { pub fn init_builtin() {
INIT_FLAG.get_or_init(|| { INIT_FLAG.get_or_init(|| {
// Explicit example toggle // Explicit example toggle
if std::env::var("NYASH_MACRO_BOX_EXAMPLE").ok().as_deref() == Some("1") { register(&UppercasePrintMacro); } if std::env::var("NYASH_MACRO_BOX_EXAMPLE").ok().as_deref() == Some("1") {
register(&UppercasePrintMacro);
}
// Comma-separated names: NYASH_MACRO_BOX_ENABLE="UppercasePrintMacro,Other" // Comma-separated names: NYASH_MACRO_BOX_ENABLE="UppercasePrintMacro,Other"
if let Ok(list) = std::env::var("NYASH_MACRO_BOX_ENABLE") { if let Ok(list) = std::env::var("NYASH_MACRO_BOX_ENABLE") {
for name in list.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) { for name in list.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
match name { match name {
"UppercasePrintMacro" => register(&UppercasePrintMacro), "UppercasePrintMacro" => register(&UppercasePrintMacro),
_ => { eprintln!("[macro][box] unknown MacroBox '{}', ignoring", name); } _ => {
eprintln!("[macro][box] unknown MacroBox '{}', ignoring", name);
}
} }
} }
} }

View File

@ -28,7 +28,9 @@ pub fn init_from_env() {
if std::env::var("NYASH_MACRO_BOX_CHILD_RUNNER").ok().is_some() { if std::env::var("NYASH_MACRO_BOX_CHILD_RUNNER").ok().is_some() {
eprintln!("[macro][compat] NYASH_MACRO_BOX_CHILD_RUNNER is deprecated; runner mode is managed automatically"); eprintln!("[macro][compat] NYASH_MACRO_BOX_CHILD_RUNNER is deprecated; runner mode is managed automatically");
} }
let Some(paths) = paths else { return; }; let Some(paths) = paths else {
return;
};
for p in paths.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) { for p in paths.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
if let Err(e) = try_load_one(p) { if let Err(e) = try_load_one(p) {
// Quiet by default; print only when tracing is enabled to reduce noise in normal runs // Quiet by default; print only when tracing is enabled to reduce noise in normal runs
@ -47,35 +49,72 @@ fn try_load_one(path: &str) -> Result<(), String> {
let prev_sugar = std::env::var("NYASH_SYNTAX_SUGAR_LEVEL").ok(); let prev_sugar = std::env::var("NYASH_SYNTAX_SUGAR_LEVEL").ok();
std::env::set_var("NYASH_SYNTAX_SUGAR_LEVEL", "basic"); std::env::set_var("NYASH_SYNTAX_SUGAR_LEVEL", "basic");
let ast_res = nyash_rust::parser::NyashParser::parse_from_string(&src); let ast_res = nyash_rust::parser::NyashParser::parse_from_string(&src);
if let Some(v) = prev_sugar { std::env::set_var("NYASH_SYNTAX_SUGAR_LEVEL", v); } else { std::env::remove_var("NYASH_SYNTAX_SUGAR_LEVEL"); } if let Some(v) = prev_sugar {
std::env::set_var("NYASH_SYNTAX_SUGAR_LEVEL", v);
} else {
std::env::remove_var("NYASH_SYNTAX_SUGAR_LEVEL");
}
let ast = ast_res.map_err(|e| format!("parse error: {:?}", e))?; let ast = ast_res.map_err(|e| format!("parse error: {:?}", e))?;
// Find a BoxDeclaration with static function expand(...) // Find a BoxDeclaration with static function expand(...)
if let ASTNode::Program { statements, .. } = ast { if let ASTNode::Program { statements, .. } = ast {
// Capabilities: conservative scan before registration // Capabilities: conservative scan before registration
if let Err(msg) = caps_allow_macro_source(&ASTNode::Program { statements: statements.clone(), span: nyash_rust::ast::Span::unknown() }) { if let Err(msg) = caps_allow_macro_source(&ASTNode::Program {
statements: statements.clone(),
span: nyash_rust::ast::Span::unknown(),
}) {
eprintln!("[macro][box_ny][caps] {} (in '{}')", msg, path); eprintln!("[macro][box_ny][caps] {} (in '{}')", msg, path);
if strict_enabled() { return Err(msg); } if strict_enabled() {
return Err(msg);
}
return Ok(()); return Ok(());
} }
for st in &statements { for st in &statements {
if let ASTNode::BoxDeclaration { name: box_name, methods, .. } = st { if let ASTNode::BoxDeclaration {
if let Some(ASTNode::FunctionDeclaration { name: mname, body: exp_body, params, .. }) = methods.get("expand") { name: box_name,
methods,
..
} = st
{
if let Some(ASTNode::FunctionDeclaration {
name: mname,
body: exp_body,
params,
..
}) = methods.get("expand")
{
if mname == "expand" { if mname == "expand" {
let reg_name = derive_box_name(&box_name, methods.get("name")); let reg_name = derive_box_name(&box_name, methods.get("name"));
// Prefer Nyash runner route by default (self-hosting). Child-proxy only when explicitly enabled. // Prefer Nyash runner route by default (self-hosting). Child-proxy only when explicitly enabled.
let use_child = std::env::var("NYASH_MACRO_BOX_CHILD").ok().map(|v| v != "0" && v != "false" && v != "off").unwrap_or(true); let use_child = std::env::var("NYASH_MACRO_BOX_CHILD")
.ok()
.map(|v| v != "0" && v != "false" && v != "off")
.unwrap_or(true);
if use_child { if use_child {
let nm = reg_name; let nm = reg_name;
let file_static: &'static str = Box::leak(path.to_string().into_boxed_str()); let file_static: &'static str =
crate::r#macro::macro_box::register(Box::leak(Box::new(NyChildMacroBox { nm, file: file_static }))); Box::leak(path.to_string().into_boxed_str());
eprintln!("[macro][box_ny] registered child-proxy MacroBox '{}' for {}", nm, path); crate::r#macro::macro_box::register(Box::leak(Box::new(
NyChildMacroBox {
nm,
file: file_static,
},
)));
eprintln!(
"[macro][box_ny] registered child-proxy MacroBox '{}' for {}",
nm, path
);
} else { } else {
// Heuristic mapping by name first, otherwise inspect body pattern. // Heuristic mapping by name first, otherwise inspect body pattern.
let mut mapped = false; let mut mapped = false;
match reg_name { match reg_name {
"UppercasePrintMacro" => { "UppercasePrintMacro" => {
crate::r#macro::macro_box::register(&crate::r#macro::macro_box::UppercasePrintMacro); crate::r#macro::macro_box::register(
eprintln!("[macro][box_ny] registered built-in '{}' from {}", reg_name, path); &crate::r#macro::macro_box::UppercasePrintMacro,
);
eprintln!(
"[macro][box_ny] registered built-in '{}' from {}",
reg_name, path
);
mapped = true; mapped = true;
} }
_ => {} _ => {}
@ -83,14 +122,20 @@ fn try_load_one(path: &str) -> Result<(), String> {
if !mapped { if !mapped {
if expand_is_identity(exp_body, params) { if expand_is_identity(exp_body, params) {
let nm = reg_name; let nm = reg_name;
crate::r#macro::macro_box::register(Box::leak(Box::new(NyIdentityMacroBox { nm }))); crate::r#macro::macro_box::register(Box::leak(Box::new(
NyIdentityMacroBox { nm },
)));
eprintln!("[macro][box_ny] registered Ny MacroBox '{}' (identity by body) from {}", nm, path); eprintln!("[macro][box_ny] registered Ny MacroBox '{}' (identity by body) from {}", nm, path);
} else if expand_indicates_uppercase(exp_body, params) { } else if expand_indicates_uppercase(exp_body, params) {
crate::r#macro::macro_box::register(&crate::r#macro::macro_box::UppercasePrintMacro); crate::r#macro::macro_box::register(
&crate::r#macro::macro_box::UppercasePrintMacro,
);
eprintln!("[macro][box_ny] registered built-in 'UppercasePrintMacro' by body pattern from {}", path); eprintln!("[macro][box_ny] registered built-in 'UppercasePrintMacro' by body pattern from {}", path);
} else { } else {
let nm = reg_name; let nm = reg_name;
crate::r#macro::macro_box::register(Box::leak(Box::new(NyIdentityMacroBox { nm }))); crate::r#macro::macro_box::register(Box::leak(Box::new(
NyIdentityMacroBox { nm },
)));
eprintln!("[macro][box_ny] registered Ny MacroBox '{}' (identity: unknown body) from {}", nm, path); eprintln!("[macro][box_ny] registered Ny MacroBox '{}' (identity: unknown body) from {}", nm, path);
} }
} }
@ -102,19 +147,38 @@ fn try_load_one(path: &str) -> Result<(), String> {
} }
// Fallback: accept top-level `static function MacroBoxSpec.expand(json)` without a BoxDeclaration // Fallback: accept top-level `static function MacroBoxSpec.expand(json)` without a BoxDeclaration
// Default OFF for safety; can be enabled via CLI/env // Default OFF for safety; can be enabled via CLI/env
let allow_top = std::env::var("NYASH_MACRO_TOPLEVEL_ALLOW").ok().map(|v| v != "0" && v != "false" && v != "off").unwrap_or(false); let allow_top = std::env::var("NYASH_MACRO_TOPLEVEL_ALLOW")
.ok()
.map(|v| v != "0" && v != "false" && v != "off")
.unwrap_or(false);
for st in &statements { for st in &statements {
if let ASTNode::FunctionDeclaration { is_static: true, name, .. } = st { if let ASTNode::FunctionDeclaration {
is_static: true,
name,
..
} = st
{
if let Some((box_name, method)) = name.split_once('.') { if let Some((box_name, method)) = name.split_once('.') {
if method == "expand" { if method == "expand" {
let nm: &'static str = Box::leak(box_name.to_string().into_boxed_str()); let nm: &'static str = Box::leak(box_name.to_string().into_boxed_str());
let file_static: &'static str = Box::leak(path.to_string().into_boxed_str()); let file_static: &'static str =
let use_child = std::env::var("NYASH_MACRO_BOX_CHILD").ok().map(|v| v != "0" && v != "false" && v != "off").unwrap_or(true); Box::leak(path.to_string().into_boxed_str());
let use_child = std::env::var("NYASH_MACRO_BOX_CHILD")
.ok()
.map(|v| v != "0" && v != "false" && v != "off")
.unwrap_or(true);
if use_child && allow_top { if use_child && allow_top {
crate::r#macro::macro_box::register(Box::leak(Box::new(NyChildMacroBox { nm, file: file_static }))); crate::r#macro::macro_box::register(Box::leak(Box::new(
NyChildMacroBox {
nm,
file: file_static,
},
)));
eprintln!("[macro][box_ny] registered child-proxy MacroBox '{}' (top-level static) for {}", nm, path); eprintln!("[macro][box_ny] registered child-proxy MacroBox '{}' (top-level static) for {}", nm, path);
} else { } else {
crate::r#macro::macro_box::register(Box::leak(Box::new(NyIdentityMacroBox { nm }))); crate::r#macro::macro_box::register(Box::leak(Box::new(
NyIdentityMacroBox { nm },
)));
eprintln!("[macro][box_ny] registered identity MacroBox '{}' (top-level static) for {}", nm, path); eprintln!("[macro][box_ny] registered identity MacroBox '{}' (top-level static) for {}", nm, path);
} }
return Ok(()); return Ok(());
@ -131,7 +195,11 @@ fn derive_box_name(default: &str, name_fn: Option<&ASTNode>) -> &'static str {
if let Some(ASTNode::FunctionDeclaration { body, .. }) = name_fn { if let Some(ASTNode::FunctionDeclaration { body, .. }) = name_fn {
if body.len() == 1 { if body.len() == 1 {
if let ASTNode::Return { value: Some(v), .. } = &body[0] { if let ASTNode::Return { value: Some(v), .. } = &body[0] {
if let ASTNode::Literal { value: nyash_rust::ast::LiteralValue::String(s), .. } = &**v { if let ASTNode::Literal {
value: nyash_rust::ast::LiteralValue::String(s),
..
} = &**v
{
let owned = s.clone(); let owned = s.clone();
return Box::leak(owned.into_boxed_str()); return Box::leak(owned.into_boxed_str());
} }
@ -141,21 +209,33 @@ fn derive_box_name(default: &str, name_fn: Option<&ASTNode>) -> &'static str {
Box::leak(default.to_string().into_boxed_str()) Box::leak(default.to_string().into_boxed_str())
} }
pub(crate) struct NyIdentityMacroBox { nm: &'static str } pub(crate) struct NyIdentityMacroBox {
nm: &'static str,
}
impl super::macro_box::MacroBox for NyIdentityMacroBox { impl super::macro_box::MacroBox for NyIdentityMacroBox {
fn name(&self) -> &'static str { self.nm } fn name(&self) -> &'static str {
self.nm
}
fn expand(&self, ast: &ASTNode) -> ASTNode { fn expand(&self, ast: &ASTNode) -> ASTNode {
if std::env::var("NYASH_MACRO_BOX_NY_IDENTITY_ROUNDTRIP").ok().as_deref() == Some("1") { if std::env::var("NYASH_MACRO_BOX_NY_IDENTITY_ROUNDTRIP")
.ok()
.as_deref()
== Some("1")
{
let j = crate::r#macro::ast_json::ast_to_json(ast); let j = crate::r#macro::ast_json::ast_to_json(ast);
if let Some(a2) = crate::r#macro::ast_json::json_to_ast(&j) { return a2; } if let Some(a2) = crate::r#macro::ast_json::json_to_ast(&j) {
return a2;
}
} }
ast.clone() ast.clone()
} }
} }
fn expand_is_identity(body: &Vec<ASTNode>, params: &Vec<String>) -> bool { fn expand_is_identity(body: &Vec<ASTNode>, params: &Vec<String>) -> bool {
if body.len() != 1 { return false; } if body.len() != 1 {
return false;
}
if let ASTNode::Return { value: Some(v), .. } = &body[0] { if let ASTNode::Return { value: Some(v), .. } = &body[0] {
if let ASTNode::Variable { name, .. } = &**v { if let ASTNode::Variable { name, .. } = &**v {
return params.get(0).map(|p| p == name).unwrap_or(false); return params.get(0).map(|p| p == name).unwrap_or(false);
@ -165,11 +245,15 @@ fn expand_is_identity(body: &Vec<ASTNode>, params: &Vec<String>) -> bool {
} }
fn expand_indicates_uppercase(body: &Vec<ASTNode>, params: &Vec<String>) -> bool { fn expand_indicates_uppercase(body: &Vec<ASTNode>, params: &Vec<String>) -> bool {
if body.len() != 1 { return false; } if body.len() != 1 {
return false;
}
let p0 = params.get(0).cloned().unwrap_or_else(|| "ast".to_string()); let p0 = params.get(0).cloned().unwrap_or_else(|| "ast".to_string());
match &body[0] { match &body[0] {
ASTNode::Return { value: Some(v), .. } => match &**v { ASTNode::Return { value: Some(v), .. } => match &**v {
ASTNode::FunctionCall { name, arguments, .. } => { ASTNode::FunctionCall {
name, arguments, ..
} => {
if (name == "uppercase_print" || name == "upper_print") && arguments.len() == 1 { if (name == "uppercase_print" || name == "upper_print") && arguments.len() == 1 {
if let ASTNode::Variable { name: an, .. } = &arguments[0] { if let ASTNode::Variable { name: an, .. } = &arguments[0] {
return an == &p0; return an == &p0;
@ -184,32 +268,80 @@ fn expand_indicates_uppercase(body: &Vec<ASTNode>, params: &Vec<String>) -> bool
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacroBehavior { Identity, Uppercase, ArrayPrependZero, MapInsertTag, LoopNormalize, IfMatchNormalize, ForForeachNormalize, EnvTagString } pub enum MacroBehavior {
Identity,
Uppercase,
ArrayPrependZero,
MapInsertTag,
LoopNormalize,
IfMatchNormalize,
ForForeachNormalize,
EnvTagString,
}
pub fn analyze_macro_file(path: &str) -> MacroBehavior { pub fn analyze_macro_file(path: &str) -> MacroBehavior {
let src = match std::fs::read_to_string(path) { Ok(s) => s, Err(_) => return MacroBehavior::Identity }; let src = match std::fs::read_to_string(path) {
let ast = match nyash_rust::parser::NyashParser::parse_from_string(&src) { Ok(a) => a, Err(_) => return MacroBehavior::Identity }; Ok(s) => s,
Err(_) => return MacroBehavior::Identity,
};
let ast = match nyash_rust::parser::NyashParser::parse_from_string(&src) {
Ok(a) => a,
Err(_) => return MacroBehavior::Identity,
};
// Quick heuristics based on literals present in file // Quick heuristics based on literals present in file
fn ast_has_literal_string(a: &ASTNode, needle: &str) -> bool { fn ast_has_literal_string(a: &ASTNode, needle: &str) -> bool {
use nyash_rust::ast::ASTNode as A; use nyash_rust::ast::ASTNode as A;
match a { match a {
A::Literal { value: nyash_rust::ast::LiteralValue::String(s), .. } => s.contains(needle), A::Literal {
A::Program { statements, .. } => statements.iter().any(|n| ast_has_literal_string(n, needle)), value: nyash_rust::ast::LiteralValue::String(s),
..
} => s.contains(needle),
A::Program { statements, .. } => {
statements.iter().any(|n| ast_has_literal_string(n, needle))
}
A::Print { expression, .. } => ast_has_literal_string(expression, needle), A::Print { expression, .. } => ast_has_literal_string(expression, needle),
A::Return { value, .. } => value.as_ref().map(|v| ast_has_literal_string(v, needle)).unwrap_or(false), A::Return { value, .. } => value
A::Assignment { target, value, .. } => ast_has_literal_string(target, needle) || ast_has_literal_string(value, needle), .as_ref()
A::If { condition, then_body, else_body, .. } => { .map(|v| ast_has_literal_string(v, needle))
.unwrap_or(false),
A::Assignment { target, value, .. } => {
ast_has_literal_string(target, needle) || ast_has_literal_string(value, needle)
}
A::If {
condition,
then_body,
else_body,
..
} => {
ast_has_literal_string(condition, needle) ast_has_literal_string(condition, needle)
|| then_body.iter().any(|n| ast_has_literal_string(n, needle)) || then_body.iter().any(|n| ast_has_literal_string(n, needle))
|| else_body.as_ref().map(|v| v.iter().any(|n| ast_has_literal_string(n, needle))).unwrap_or(false) || else_body
.as_ref()
.map(|v| v.iter().any(|n| ast_has_literal_string(n, needle)))
.unwrap_or(false)
}
A::FunctionDeclaration { body, .. } => {
body.iter().any(|n| ast_has_literal_string(n, needle))
}
A::BinaryOp { left, right, .. } => {
ast_has_literal_string(left, needle) || ast_has_literal_string(right, needle)
} }
A::FunctionDeclaration { body, .. } => body.iter().any(|n| ast_has_literal_string(n, needle)),
A::BinaryOp { left, right, .. } => ast_has_literal_string(left, needle) || ast_has_literal_string(right, needle),
A::UnaryOp { operand, .. } => ast_has_literal_string(operand, needle), A::UnaryOp { operand, .. } => ast_has_literal_string(operand, needle),
A::MethodCall { object, arguments, .. } => ast_has_literal_string(object, needle) || arguments.iter().any(|n| ast_has_literal_string(n, needle)), A::MethodCall {
A::FunctionCall { arguments, .. } => arguments.iter().any(|n| ast_has_literal_string(n, needle)), object, arguments, ..
A::ArrayLiteral { elements, .. } => elements.iter().any(|n| ast_has_literal_string(n, needle)), } => {
A::MapLiteral { entries, .. } => entries.iter().any(|(_, v)| ast_has_literal_string(v, needle)), ast_has_literal_string(object, needle)
|| arguments.iter().any(|n| ast_has_literal_string(n, needle))
}
A::FunctionCall { arguments, .. } => {
arguments.iter().any(|n| ast_has_literal_string(n, needle))
}
A::ArrayLiteral { elements, .. } => {
elements.iter().any(|n| ast_has_literal_string(n, needle))
}
A::MapLiteral { entries, .. } => entries
.iter()
.any(|(_, v)| ast_has_literal_string(v, needle)),
_ => false, _ => false,
} }
} }
@ -218,29 +350,59 @@ pub fn analyze_macro_file(path: &str) -> MacroBehavior {
match a { match a {
A::Program { statements, .. } => statements.iter().any(|n| ast_has_method(n, method)), A::Program { statements, .. } => statements.iter().any(|n| ast_has_method(n, method)),
A::Print { expression, .. } => ast_has_method(expression, method), A::Print { expression, .. } => ast_has_method(expression, method),
A::Return { value, .. } => value.as_ref().map(|v| ast_has_method(v, method)).unwrap_or(false), A::Return { value, .. } => value
A::Assignment { target, value, .. } => ast_has_method(target, method) || ast_has_method(value, method), .as_ref()
A::If { condition, then_body, else_body, .. } => ast_has_method(condition, method) .map(|v| ast_has_method(v, method))
.unwrap_or(false),
A::Assignment { target, value, .. } => {
ast_has_method(target, method) || ast_has_method(value, method)
}
A::If {
condition,
then_body,
else_body,
..
} => {
ast_has_method(condition, method)
|| then_body.iter().any(|n| ast_has_method(n, method)) || then_body.iter().any(|n| ast_has_method(n, method))
|| else_body.as_ref().map(|v| v.iter().any(|n| ast_has_method(n, method))).unwrap_or(false), || else_body
.as_ref()
.map(|v| v.iter().any(|n| ast_has_method(n, method)))
.unwrap_or(false)
}
A::FunctionDeclaration { body, .. } => body.iter().any(|n| ast_has_method(n, method)), A::FunctionDeclaration { body, .. } => body.iter().any(|n| ast_has_method(n, method)),
A::BinaryOp { left, right, .. } => ast_has_method(left, method) || ast_has_method(right, method), A::BinaryOp { left, right, .. } => {
ast_has_method(left, method) || ast_has_method(right, method)
}
A::UnaryOp { operand, .. } => ast_has_method(operand, method), A::UnaryOp { operand, .. } => ast_has_method(operand, method),
A::MethodCall { object, method: m, arguments, .. } => m == method A::MethodCall {
object,
method: m,
arguments,
..
} => {
m == method
|| ast_has_method(object, method) || ast_has_method(object, method)
|| arguments.iter().any(|n| ast_has_method(n, method)), || arguments.iter().any(|n| ast_has_method(n, method))
A::FunctionCall { arguments, .. } => arguments.iter().any(|n| ast_has_method(n, method)), }
A::FunctionCall { arguments, .. } => {
arguments.iter().any(|n| ast_has_method(n, method))
}
A::ArrayLiteral { elements, .. } => elements.iter().any(|n| ast_has_method(n, method)), A::ArrayLiteral { elements, .. } => elements.iter().any(|n| ast_has_method(n, method)),
A::MapLiteral { entries, .. } => entries.iter().any(|(_, v)| ast_has_method(v, method)), A::MapLiteral { entries, .. } => entries.iter().any(|(_, v)| ast_has_method(v, method)),
_ => false, _ => false,
} }
} }
// Detect array prepend-zero macro by pattern strings present in macro source // Detect array prepend-zero macro by pattern strings present in macro source
if ast_has_literal_string(&ast, "\"kind\":\"Array\",\"elements\":[") || ast_has_literal_string(&ast, "\"elements\":[") { if ast_has_literal_string(&ast, "\"kind\":\"Array\",\"elements\":[")
|| ast_has_literal_string(&ast, "\"elements\":[")
{
return MacroBehavior::ArrayPrependZero; return MacroBehavior::ArrayPrependZero;
} }
// Detect map insert-tag macro by pattern strings // Detect map insert-tag macro by pattern strings
if ast_has_literal_string(&ast, "\"kind\":\"Map\",\"entries\":[") || ast_has_literal_string(&ast, "\"entries\":[") { if ast_has_literal_string(&ast, "\"kind\":\"Map\",\"entries\":[")
|| ast_has_literal_string(&ast, "\"entries\":[")
{
return MacroBehavior::MapInsertTag; return MacroBehavior::MapInsertTag;
} }
// Detect upper-string macro by pattern or toUpperCase usage // Detect upper-string macro by pattern or toUpperCase usage
@ -253,23 +415,47 @@ pub fn analyze_macro_file(path: &str) -> MacroBehavior {
} }
if let ASTNode::Program { statements, .. } = ast { if let ASTNode::Program { statements, .. } = ast {
for st in statements { for st in statements {
if let ASTNode::BoxDeclaration { name: _, methods, .. } = st { if let ASTNode::BoxDeclaration {
name: _, methods, ..
} = st
{
// Detect LoopNormalize/IfMatchNormalize by name() returning a specific string // Detect LoopNormalize/IfMatchNormalize by name() returning a specific string
if let Some(ASTNode::FunctionDeclaration { name: mname, body, .. }) = methods.get("name") { if let Some(ASTNode::FunctionDeclaration {
name: mname, body, ..
}) = methods.get("name")
{
if mname == "name" { if mname == "name" {
if body.len() == 1 { if body.len() == 1 {
if let ASTNode::Return { value: Some(v), .. } = &body[0] { if let ASTNode::Return { value: Some(v), .. } = &body[0] {
if let ASTNode::Literal { value: nyash_rust::ast::LiteralValue::String(s), .. } = &**v { if let ASTNode::Literal {
if s == "LoopNormalize" { return MacroBehavior::LoopNormalize; } value: nyash_rust::ast::LiteralValue::String(s),
if s == "IfMatchNormalize" { return MacroBehavior::IfMatchNormalize; } ..
if s == "ForForeach" { return MacroBehavior::ForForeachNormalize; } } = &**v
if s == "EnvTagString" { return MacroBehavior::EnvTagString; } {
if s == "LoopNormalize" {
return MacroBehavior::LoopNormalize;
}
if s == "IfMatchNormalize" {
return MacroBehavior::IfMatchNormalize;
}
if s == "ForForeach" {
return MacroBehavior::ForForeachNormalize;
}
if s == "EnvTagString" {
return MacroBehavior::EnvTagString;
} }
} }
} }
} }
} }
if let Some(ASTNode::FunctionDeclaration { name: mname, body, params, .. }) = methods.get("expand") { }
if let Some(ASTNode::FunctionDeclaration {
name: mname,
body,
params,
..
}) = methods.get("expand")
{
if mname == "expand" { if mname == "expand" {
if expand_indicates_uppercase(body, params) { if expand_indicates_uppercase(body, params) {
return MacroBehavior::Uppercase; return MacroBehavior::Uppercase;
@ -282,7 +468,10 @@ pub fn analyze_macro_file(path: &str) -> MacroBehavior {
MacroBehavior::Identity MacroBehavior::Identity
} }
struct NyChildMacroBox { nm: &'static str, file: &'static str } struct NyChildMacroBox {
nm: &'static str,
file: &'static str,
}
fn cap_enabled(name: &str) -> bool { fn cap_enabled(name: &str) -> bool {
match std::env::var(name).ok() { match std::env::var(name).ok() {
@ -301,67 +490,148 @@ fn caps_allow_macro_source(ast: &ASTNode) -> Result<(), String> {
fn scan(n: &A, seen: &mut Vec<String>) { fn scan(n: &A, seen: &mut Vec<String>) {
match n { match n {
A::New { class, .. } => seen.push(class.clone()), A::New { class, .. } => seen.push(class.clone()),
A::Program { statements, .. } => for s in statements { scan(s, seen); }, A::Program { statements, .. } => {
A::FunctionDeclaration { body, .. } => for s in body { scan(s, seen); }, for s in statements {
A::Assignment { target, value, .. } => { scan(target, seen); scan(value, seen); }, scan(s, seen);
A::Return { value, .. } => if let Some(v) = value { scan(v, seen); }, }
A::If { condition, then_body, else_body, .. } => { }
scan(condition, seen); A::FunctionDeclaration { body, .. } => {
for s in then_body { scan(s, seen); } for s in body {
if let Some(b) = else_body { for s in b { scan(s, seen); } } scan(s, seen);
}
}
A::Assignment { target, value, .. } => {
scan(target, seen);
scan(value, seen);
}
A::Return { value, .. } => {
if let Some(v) = value {
scan(v, seen);
}
}
A::If {
condition,
then_body,
else_body,
..
} => {
scan(condition, seen);
for s in then_body {
scan(s, seen);
}
if let Some(b) = else_body {
for s in b {
scan(s, seen);
}
}
}
A::BinaryOp { left, right, .. } => {
scan(left, seen);
scan(right, seen);
} }
A::BinaryOp { left, right, .. } => { scan(left, seen); scan(right, seen); }
A::UnaryOp { operand, .. } => scan(operand, seen), A::UnaryOp { operand, .. } => scan(operand, seen),
A::MethodCall { object, arguments, .. } => { scan(object, seen); for a in arguments { scan(a, seen); } } A::MethodCall {
A::FunctionCall { arguments, .. } => for a in arguments { scan(a, seen); }, object, arguments, ..
A::ArrayLiteral { elements, .. } => for e in elements { scan(e, seen); }, } => {
A::MapLiteral { entries, .. } => for (_, v) in entries { scan(v, seen); }, scan(object, seen);
for a in arguments {
scan(a, seen);
}
}
A::FunctionCall { arguments, .. } => {
for a in arguments {
scan(a, seen);
}
}
A::ArrayLiteral { elements, .. } => {
for e in elements {
scan(e, seen);
}
}
A::MapLiteral { entries, .. } => {
for (_, v) in entries {
scan(v, seen);
}
}
_ => {} _ => {}
} }
} }
let mut boxes = Vec::new(); let mut boxes = Vec::new();
scan(ast, &mut boxes); scan(ast, &mut boxes);
if !allow_io && boxes.iter().any(|c| c == "FileBox" || c == "PathBox" || c == "DirBox") { if !allow_io
&& boxes
.iter()
.any(|c| c == "FileBox" || c == "PathBox" || c == "DirBox")
{
return Err("macro capability violation: IO (File/Path/Dir) denied".into()); return Err("macro capability violation: IO (File/Path/Dir) denied".into());
} }
if !allow_net && boxes.iter().any(|c| c.contains("HTTP") || c.contains("Http") || c == "SocketBox") { if !allow_net
&& boxes
.iter()
.any(|c| c.contains("HTTP") || c.contains("Http") || c == "SocketBox")
{
return Err("macro capability violation: NET (HTTP/Socket) denied".into()); return Err("macro capability violation: NET (HTTP/Socket) denied".into());
} }
Ok(()) Ok(())
} }
impl super::macro_box::MacroBox for NyChildMacroBox { impl super::macro_box::MacroBox for NyChildMacroBox {
fn name(&self) -> &'static str { self.nm } fn name(&self) -> &'static str {
self.nm
}
fn expand(&self, ast: &ASTNode) -> ASTNode { fn expand(&self, ast: &ASTNode) -> ASTNode {
// Parent-side proxy: prefer runner script (PyVM) when enabled; otherwise fallback to internal child mode. // Parent-side proxy: prefer runner script (PyVM) when enabled; otherwise fallback to internal child mode.
let exe = match std::env::current_exe() { let exe = match std::env::current_exe() {
Ok(p) => p, Ok(p) => p,
Err(e) => { eprintln!("[macro-proxy] current_exe failed: {}", e); return ast.clone(); } Err(e) => {
eprintln!("[macro-proxy] current_exe failed: {}", e);
return ast.clone();
}
}; };
// Prefer Nyash runner route by default for self-hosting; legacy env can force internal child with 0. // Prefer Nyash runner route by default for self-hosting; legacy env can force internal child with 0.
let use_runner = std::env::var("NYASH_MACRO_BOX_CHILD_RUNNER").ok().map(|v| v != "0" && v != "false" && v != "off").unwrap_or(false); let use_runner = std::env::var("NYASH_MACRO_BOX_CHILD_RUNNER")
.ok()
.map(|v| v != "0" && v != "false" && v != "off")
.unwrap_or(false);
if std::env::var("NYASH_MACRO_BOX_CHILD_RUNNER").ok().is_some() { if std::env::var("NYASH_MACRO_BOX_CHILD_RUNNER").ok().is_some() {
eprintln!("[macro][compat] NYASH_MACRO_BOX_CHILD_RUNNER is deprecated; prefer defaults"); eprintln!(
"[macro][compat] NYASH_MACRO_BOX_CHILD_RUNNER is deprecated; prefer defaults"
);
} }
let mut cmd = std::process::Command::new(exe.clone()); let mut cmd = std::process::Command::new(exe.clone());
// Build MacroCtx JSON once (caps only, MVP) // Build MacroCtx JSON once (caps only, MVP)
let mctx = crate::r#macro::ctx::MacroCtx::from_env(); let mctx = crate::r#macro::ctx::MacroCtx::from_env();
let ctx_json = format!("{{\"caps\":{{\"io\":{},\"net\":{},\"env\":{}}}}}", mctx.caps.io, mctx.caps.net, mctx.caps.env); let ctx_json = format!(
"{{\"caps\":{{\"io\":{},\"net\":{},\"env\":{}}}}}",
mctx.caps.io, mctx.caps.net, mctx.caps.env
);
if use_runner { if use_runner {
// Synthesize a tiny runner that inlines the macro file and calls MacroBoxSpec.expand // Synthesize a tiny runner that inlines the macro file and calls MacroBoxSpec.expand
use std::io::Write as _; use std::io::Write as _;
let tmp_dir = std::path::Path::new("tmp"); let tmp_dir = std::path::Path::new("tmp");
let _ = std::fs::create_dir_all(tmp_dir); let _ = std::fs::create_dir_all(tmp_dir);
let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis(); let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let tmp_path = tmp_dir.join(format!("macro_expand_runner_{}.hako", ts)); let tmp_path = tmp_dir.join(format!("macro_expand_runner_{}.hako", ts));
let mut f = match std::fs::File::create(&tmp_path) { Ok(x) => x, Err(e) => { eprintln!("[macro-proxy] create tmp runner failed: {}", e); return ast.clone(); } }; let mut f = match std::fs::File::create(&tmp_path) {
Ok(x) => x,
Err(e) => {
eprintln!("[macro-proxy] create tmp runner failed: {}", e);
return ast.clone();
}
};
let macro_src = std::fs::read_to_string(self.file) let macro_src = std::fs::read_to_string(self.file)
.unwrap_or_else(|_| String::from("// failed to read macro file\n")); .unwrap_or_else(|_| String::from("// failed to read macro file\n"));
let script = format!( let script = format!(
"{}\n\nfunction main(args) {{\n if args.length() == 0 {{\n print(\"{{}}\")\n return 0\n }}\n local j, r, ctx\n j = args.get(0)\n if args.length() > 1 {{ ctx = args.get(1) }} else {{ ctx = \"{{}}\" }}\n r = MacroBoxSpec.expand(j, ctx)\n print(r)\n return 0\n}}\n", "{}\n\nfunction main(args) {{\n if args.length() == 0 {{\n print(\"{{}}\")\n return 0\n }}\n local j, r, ctx\n j = args.get(0)\n if args.length() > 1 {{ ctx = args.get(1) }} else {{ ctx = \"{{}}\" }}\n r = MacroBoxSpec.expand(j, ctx)\n print(r)\n return 0\n}}\n",
macro_src macro_src
); );
if let Err(e) = f.write_all(script.as_bytes()) { eprintln!("[macro-proxy] write tmp runner failed: {}", e); return ast.clone(); } if let Err(e) = f.write_all(script.as_bytes()) {
eprintln!("[macro-proxy] write tmp runner failed: {}", e);
return ast.clone();
}
// Run Nyash runner script under PyVM: nyash --backend vm <tmp_runner> -- <json> // Run Nyash runner script under PyVM: nyash --backend vm <tmp_runner> -- <json>
cmd.arg("--backend").arg("vm").arg(tmp_path); cmd.arg("--backend").arg("vm").arg(tmp_path);
// Append script args after '--' // Append script args after '--'
@ -372,7 +642,8 @@ impl super::macro_box::MacroBox for NyChildMacroBox {
cmd.stdin(std::process::Stdio::null()); cmd.stdin(std::process::Stdio::null());
} else { } else {
// Internal child mode: --macro-expand-child <macro file> with stdin JSON // Internal child mode: --macro-expand-child <macro file> with stdin JSON
cmd.arg("--macro-expand-child").arg(self.file) cmd.arg("--macro-expand-child")
.arg(self.file)
.stdin(std::process::Stdio::piped()); .stdin(std::process::Stdio::piped());
// Provide MacroCtx via env for internal child // Provide MacroCtx via env for internal child
cmd.env("NYASH_MACRO_CTX_JSON", ctx_json.clone()); cmd.env("NYASH_MACRO_CTX_JSON", ctx_json.clone());
@ -393,13 +664,21 @@ impl super::macro_box::MacroBox for NyChildMacroBox {
cmd.env_remove("NYASH_MACRO_BOX_CHILD"); cmd.env_remove("NYASH_MACRO_BOX_CHILD");
cmd.env_remove("NYASH_MACRO_BOX_CHILD_RUNNER"); cmd.env_remove("NYASH_MACRO_BOX_CHILD_RUNNER");
// Timeout // Timeout
let timeout_ms: u64 = std::env::var("NYASH_NY_COMPILER_TIMEOUT_MS").ok().and_then(|s| s.parse().ok()).unwrap_or(2000); let timeout_ms: u64 = std::env::var("NYASH_NY_COMPILER_TIMEOUT_MS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(2000);
// Spawn // Spawn
let mut child = match cmd.spawn() { Ok(c) => c, Err(e) => { let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
eprintln!("[macro-proxy] spawn failed: {}", e); eprintln!("[macro-proxy] spawn failed: {}", e);
if strict_enabled() { std::process::exit(2); } if strict_enabled() {
std::process::exit(2);
}
return ast.clone(); return ast.clone();
} }; }
};
// Write stdin only in internal child mode // Write stdin only in internal child mode
if !use_runner { if !use_runner {
if let Some(mut sin) = child.stdin.take() { if let Some(mut sin) = child.stdin.take() {
@ -415,34 +694,60 @@ impl super::macro_box::MacroBox for NyChildMacroBox {
loop { loop {
match child.try_wait() { match child.try_wait() {
Ok(Some(_status)) => { Ok(Some(_status)) => {
if let Some(mut so) = child.stdout.take() { use std::io::Read; let _ = so.read_to_string(&mut out); } if let Some(mut so) = child.stdout.take() {
use std::io::Read;
let _ = so.read_to_string(&mut out);
}
break; break;
} }
Ok(None) => { Ok(None) => {
if start.elapsed() >= Duration::from_millis(timeout_ms) { if start.elapsed() >= Duration::from_millis(timeout_ms) {
let _ = child.kill(); let _ = child.wait(); let _ = child.kill();
let _ = child.wait();
eprintln!("[macro-proxy] timeout {} ms", timeout_ms); eprintln!("[macro-proxy] timeout {} ms", timeout_ms);
if strict_enabled() { std::process::exit(124); } if strict_enabled() {
std::process::exit(124);
}
return ast.clone(); return ast.clone();
} }
std::thread::sleep(Duration::from_millis(5)); std::thread::sleep(Duration::from_millis(5));
} }
Err(e) => { eprintln!("[macro-proxy] wait error: {}", e); if strict_enabled() { std::process::exit(2); } return ast.clone(); } Err(e) => {
eprintln!("[macro-proxy] wait error: {}", e);
if strict_enabled() {
std::process::exit(2);
}
return ast.clone();
}
} }
} }
// capture stderr for diagnostics and continue // capture stderr for diagnostics and continue
// Capture stderr for diagnostics // Capture stderr for diagnostics
let mut err = String::new(); let mut err = String::new();
if let Some(mut se) = child.stderr.take() { use std::io::Read; let _ = se.read_to_string(&mut err); } if let Some(mut se) = child.stderr.take() {
use std::io::Read;
let _ = se.read_to_string(&mut err);
}
// Parse output JSON // Parse output JSON
match serde_json::from_str::<serde_json::Value>(&out) { match serde_json::from_str::<serde_json::Value>(&out) {
Ok(v) => match crate::r#macro::ast_json::json_to_ast(&v) { Ok(v) => match crate::r#macro::ast_json::json_to_ast(&v) {
Some(a) => a, Some(a) => a,
None => { eprintln!("[macro-proxy] child JSON did not map to AST. stderr=\n{}", err); if strict_enabled() { std::process::exit(2); } ast.clone() } None => {
eprintln!(
"[macro-proxy] child JSON did not map to AST. stderr=\n{}",
err
);
if strict_enabled() {
std::process::exit(2);
}
ast.clone()
}
}, },
Err(e) => { Err(e) => {
eprintln!("[macro-proxy] invalid JSON from child: {}\n-- child stderr --\n{}\n-- end stderr --", e, err); eprintln!("[macro-proxy] invalid JSON from child: {}\n-- child stderr --\n{}\n-- end stderr --", e, err);
if strict_enabled() { std::process::exit(2); } if strict_enabled() {
std::process::exit(2);
}
ast.clone() ast.clone()
} }
} }

View File

@ -3,22 +3,28 @@
//! Goal: Provide minimal, typed interfaces for AST pattern matching and //! Goal: Provide minimal, typed interfaces for AST pattern matching and
//! HIR patch based expansion. Backends (MIR/JIT/LLVM) remain unchanged. //! HIR patch based expansion. Backends (MIR/JIT/LLVM) remain unchanged.
pub mod pattern; pub mod ast_json;
pub mod ctx;
pub mod engine; pub mod engine;
pub mod macro_box; pub mod macro_box;
pub mod macro_box_ny; pub mod macro_box_ny;
pub mod ast_json; pub mod pattern;
pub mod ctx;
use nyash_rust::ASTNode; use nyash_rust::ASTNode;
/// Enable/disable macro system via env gate. /// Enable/disable macro system via env gate.
pub fn enabled() -> bool { pub fn enabled() -> bool {
// Default ON. Disable with NYASH_MACRO_DISABLE=1 or NYASH_MACRO_ENABLE=0/false/off. // Default ON. Disable with NYASH_MACRO_DISABLE=1 or NYASH_MACRO_ENABLE=0/false/off.
if let Ok(v) = std::env::var("NYASH_MACRO_DISABLE") { if v == "1" { return false; } } if let Ok(v) = std::env::var("NYASH_MACRO_DISABLE") {
if v == "1" {
return false;
}
}
if let Ok(v) = std::env::var("NYASH_MACRO_ENABLE") { if let Ok(v) = std::env::var("NYASH_MACRO_ENABLE") {
let v = v.to_ascii_lowercase(); let v = v.to_ascii_lowercase();
if v == "0" || v == "false" || v == "off" { return false; } if v == "0" || v == "false" || v == "off" {
return false;
}
return true; return true;
} }
true true
@ -26,15 +32,21 @@ pub fn enabled() -> bool {
/// A hook to dump AST for `--expand` (pre/post). Expansion is no-op for now. /// A hook to dump AST for `--expand` (pre/post). Expansion is no-op for now.
pub fn maybe_expand_and_dump(ast: &ASTNode, _dump_only: bool) -> ASTNode { pub fn maybe_expand_and_dump(ast: &ASTNode, _dump_only: bool) -> ASTNode {
if !enabled() { return ast.clone(); } if !enabled() {
return ast.clone();
}
// Initialize user macro boxes (if any, behind env gates) // Initialize user macro boxes (if any, behind env gates)
self::macro_box::init_builtin(); self::macro_box::init_builtin();
self::macro_box_ny::init_from_env(); self::macro_box_ny::init_from_env();
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { eprintln!("[macro] input AST: {:?}", ast); } if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro] input AST: {:?}", ast);
}
let mut eng = self::engine::MacroEngine::new(); let mut eng = self::engine::MacroEngine::new();
let (out, _patches) = eng.expand(ast); let (out, _patches) = eng.expand(ast);
let out2 = maybe_inject_test_harness(&out); let out2 = maybe_inject_test_harness(&out);
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { eprintln!("[macro] output AST: {:?}", out2); } if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro] output AST: {:?}", out2);
}
out2 out2
} }
@ -44,7 +56,11 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
} }
// Test call plan // Test call plan
#[derive(Clone)] #[derive(Clone)]
struct TestPlan { label: String, setup: Option<nyash_rust::ASTNode>, call: nyash_rust::ASTNode } struct TestPlan {
label: String,
setup: Option<nyash_rust::ASTNode>,
call: nyash_rust::ASTNode,
}
// Collect tests (top-level and Box) // Collect tests (top-level and Box)
let mut tests: Vec<TestPlan> = Vec::new(); let mut tests: Vec<TestPlan> = Vec::new();
@ -56,9 +72,16 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
// - Detailed per-test: { "Box.method": { "args": [...], "instance": {"ctor":"new|birth","args":[...] } } } // - Detailed per-test: { "Box.method": { "args": [...], "instance": {"ctor":"new|birth","args":[...] } } }
// - Typed values inside args supported via objects (see json_to_ast below) // - Typed values inside args supported via objects (see json_to_ast below)
#[derive(Clone, Default)] #[derive(Clone, Default)]
struct InstanceSpec { ctor: String, args: Vec<nyash_rust::ASTNode>, type_args: Vec<String> } struct InstanceSpec {
ctor: String,
args: Vec<nyash_rust::ASTNode>,
type_args: Vec<String>,
}
#[derive(Clone, Default)] #[derive(Clone, Default)]
struct TestArgSpec { args: Vec<nyash_rust::ASTNode>, instance: Option<InstanceSpec> } struct TestArgSpec {
args: Vec<nyash_rust::ASTNode>,
instance: Option<InstanceSpec>,
}
fn json_err(msg: &str) { fn json_err(msg: &str) {
eprintln!("[macro][test][args] {}", msg); eprintln!("[macro][test][args] {}", msg);
@ -67,76 +90,163 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
fn json_to_ast(v: &serde_json::Value) -> Result<nyash_rust::ASTNode, String> { fn json_to_ast(v: &serde_json::Value) -> Result<nyash_rust::ASTNode, String> {
use nyash_rust::ast::{ASTNode as A, LiteralValue, Span}; use nyash_rust::ast::{ASTNode as A, LiteralValue, Span};
match v { match v {
serde_json::Value::String(st) => Ok(A::Literal { value: LiteralValue::String(st.clone()), span: Span::unknown() }), serde_json::Value::String(st) => Ok(A::Literal {
serde_json::Value::Bool(b) => Ok(A::Literal { value: LiteralValue::Bool(*b), span: Span::unknown() }), value: LiteralValue::String(st.clone()),
span: Span::unknown(),
}),
serde_json::Value::Bool(b) => Ok(A::Literal {
value: LiteralValue::Bool(*b),
span: Span::unknown(),
}),
serde_json::Value::Number(n) => { serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() { if let Some(i) = n.as_i64() {
Ok(A::Literal { value: LiteralValue::Integer(i), span: Span::unknown() }) Ok(A::Literal {
value: LiteralValue::Integer(i),
span: Span::unknown(),
})
} else if let Some(f) = n.as_f64() { } else if let Some(f) = n.as_f64() {
Ok(A::Literal { value: LiteralValue::Float(f), span: Span::unknown() }) Ok(A::Literal {
value: LiteralValue::Float(f),
span: Span::unknown(),
})
} else { } else {
Err("unsupported number literal".into()) Err("unsupported number literal".into())
} }
} }
serde_json::Value::Null => Ok(A::Literal { value: LiteralValue::Null, span: Span::unknown() }), serde_json::Value::Null => Ok(A::Literal {
value: LiteralValue::Null,
span: Span::unknown(),
}),
serde_json::Value::Array(elems) => { serde_json::Value::Array(elems) => {
// Treat nested arrays as ArrayLiteral by default // Treat nested arrays as ArrayLiteral by default
let mut out = Vec::with_capacity(elems.len()); let mut out = Vec::with_capacity(elems.len());
for x in elems { out.push(json_to_ast(x)?); } for x in elems {
Ok(A::ArrayLiteral { elements: out, span: Span::unknown() }) out.push(json_to_ast(x)?);
}
Ok(A::ArrayLiteral {
elements: out,
span: Span::unknown(),
})
} }
serde_json::Value::Object(obj) => { serde_json::Value::Object(obj) => {
// Typed shorthands accepted: {i:1}|{int:1}, {f:1.2}|{float:1.2}, {s:"x"}|{string:"x"}, {b:true}|{bool:true} // Typed shorthands accepted: {i:1}|{int:1}, {f:1.2}|{float:1.2}, {s:"x"}|{string:"x"}, {b:true}|{bool:true}
if let Some(v) = obj.get("i").or_else(|| obj.get("int")) { return json_to_ast(v); } if let Some(v) = obj.get("i").or_else(|| obj.get("int")) {
if let Some(v) = obj.get("f").or_else(|| obj.get("float")) { return json_to_ast(v); } return json_to_ast(v);
if let Some(v) = obj.get("s").or_else(|| obj.get("string")) { return json_to_ast(v); } }
if let Some(v) = obj.get("b").or_else(|| obj.get("bool")) { return json_to_ast(v); } if let Some(v) = obj.get("f").or_else(|| obj.get("float")) {
return json_to_ast(v);
}
if let Some(v) = obj.get("s").or_else(|| obj.get("string")) {
return json_to_ast(v);
}
if let Some(v) = obj.get("b").or_else(|| obj.get("bool")) {
return json_to_ast(v);
}
if let Some(map) = obj.get("map") { if let Some(map) = obj.get("map") {
if let Some(mo) = map.as_object() { if let Some(mo) = map.as_object() {
let mut ents: Vec<(String, nyash_rust::ASTNode)> = Vec::with_capacity(mo.len()); let mut ents: Vec<(String, nyash_rust::ASTNode)> =
for (k, vv) in mo { ents.push((k.clone(), json_to_ast(vv)?)); } Vec::with_capacity(mo.len());
return Ok(A::MapLiteral { entries: ents, span: Span::unknown() }); for (k, vv) in mo {
} else { return Err("map must be an object".into()); } ents.push((k.clone(), json_to_ast(vv)?));
}
return Ok(A::MapLiteral {
entries: ents,
span: Span::unknown(),
});
} else {
return Err("map must be an object".into());
}
} }
if let Some(arr) = obj.get("array") { if let Some(arr) = obj.get("array") {
if let Some(va) = arr.as_array() { if let Some(va) = arr.as_array() {
let mut out = Vec::with_capacity(va.len()); let mut out = Vec::with_capacity(va.len());
for x in va { out.push(json_to_ast(x)?); } for x in va {
return Ok(A::ArrayLiteral { elements: out, span: Span::unknown() }); out.push(json_to_ast(x)?);
} else { return Err("array must be an array".into()); } }
return Ok(A::ArrayLiteral {
elements: out,
span: Span::unknown(),
});
} else {
return Err("array must be an array".into());
}
} }
if let Some(name) = obj.get("var").and_then(|v| v.as_str()) { if let Some(name) = obj.get("var").and_then(|v| v.as_str()) {
return Ok(A::Variable { name: name.to_string(), span: Span::unknown() }); return Ok(A::Variable {
name: name.to_string(),
span: Span::unknown(),
});
} }
if let Some(name) = obj.get("call").and_then(|v| v.as_str()) { if let Some(name) = obj.get("call").and_then(|v| v.as_str()) {
let mut args: Vec<A> = Vec::new(); let mut args: Vec<A> = Vec::new();
if let Some(va) = obj.get("args").and_then(|v| v.as_array()) { if let Some(va) = obj.get("args").and_then(|v| v.as_array()) {
for x in va { args.push(json_to_ast(x)?); } for x in va {
args.push(json_to_ast(x)?);
} }
return Ok(A::FunctionCall { name: name.to_string(), arguments: args, span: Span::unknown() }); }
return Ok(A::FunctionCall {
name: name.to_string(),
arguments: args,
span: Span::unknown(),
});
} }
if let Some(method) = obj.get("method").and_then(|v| v.as_str()) { if let Some(method) = obj.get("method").and_then(|v| v.as_str()) {
let objv = obj.get("object").ok_or_else(|| "method requires 'object'".to_string())?; let objv = obj
.get("object")
.ok_or_else(|| "method requires 'object'".to_string())?;
let object = json_to_ast(objv)?; let object = json_to_ast(objv)?;
let mut args: Vec<A> = Vec::new(); let mut args: Vec<A> = Vec::new();
if let Some(va) = obj.get("args").and_then(|v| v.as_array()) { if let Some(va) = obj.get("args").and_then(|v| v.as_array()) {
for x in va { args.push(json_to_ast(x)?); } for x in va {
args.push(json_to_ast(x)?);
} }
return Ok(A::MethodCall { object: Box::new(object), method: method.to_string(), arguments: args, span: Span::unknown() }); }
return Ok(A::MethodCall {
object: Box::new(object),
method: method.to_string(),
arguments: args,
span: Span::unknown(),
});
} }
if let Some(bx) = obj.get("box").and_then(|v| v.as_str()) { if let Some(bx) = obj.get("box").and_then(|v| v.as_str()) {
let mut args: Vec<A> = Vec::new(); let mut args: Vec<A> = Vec::new();
if let Some(va) = obj.get("args").and_then(|v| v.as_array()) { if let Some(va) = obj.get("args").and_then(|v| v.as_array()) {
for x in va { args.push(json_to_ast(x)?); } for x in va {
args.push(json_to_ast(x)?);
} }
let type_args: Vec<String> = obj.get("type_args").and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()).unwrap_or_default(); }
let type_args: Vec<String> = obj
.get("type_args")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|x| x.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let ctor = obj.get("ctor").and_then(|v| v.as_str()).unwrap_or("new"); let ctor = obj.get("ctor").and_then(|v| v.as_str()).unwrap_or("new");
if ctor == "new" { if ctor == "new" {
return Ok(A::New { class: bx.to_string(), arguments: args, type_arguments: type_args, span: Span::unknown() }); return Ok(A::New {
class: bx.to_string(),
arguments: args,
type_arguments: type_args,
span: Span::unknown(),
});
} else if ctor == "birth" { } else if ctor == "birth" {
return Ok(A::MethodCall { object: Box::new(A::Variable { name: bx.to_string(), span: Span::unknown() }), method: "birth".into(), arguments: args, span: Span::unknown() }); return Ok(A::MethodCall {
object: Box::new(A::Variable {
name: bx.to_string(),
span: Span::unknown(),
}),
method: "birth".into(),
arguments: args,
span: Span::unknown(),
});
} else { } else {
return Err(format!("unknown ctor '{}', expected 'new' or 'birth'", ctor)); return Err(format!(
"unknown ctor '{}', expected 'new' or 'birth'",
ctor
));
} }
} }
Err("unknown object mapping for AST".into()) Err("unknown object mapping for AST".into())
@ -148,38 +258,90 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
match v { match v {
serde_json::Value::Array(arr) => { serde_json::Value::Array(arr) => {
let mut out: Vec<nyash_rust::ASTNode> = Vec::new(); let mut out: Vec<nyash_rust::ASTNode> = Vec::new();
for a in arr { match json_to_ast(a) { Ok(n) => out.push(n), Err(e) => { json_err(&format!("args element error: {}", e)); return None; } } } for a in arr {
Some(TestArgSpec { args: out, instance: None }) match json_to_ast(a) {
Ok(n) => out.push(n),
Err(e) => {
json_err(&format!("args element error: {}", e));
return None;
}
}
}
Some(TestArgSpec {
args: out,
instance: None,
})
} }
serde_json::Value::Object(obj) => { serde_json::Value::Object(obj) => {
let mut spec = TestArgSpec::default(); let mut spec = TestArgSpec::default();
if let Some(a) = obj.get("args").and_then(|v| v.as_array()) { if let Some(a) = obj.get("args").and_then(|v| v.as_array()) {
let mut out: Vec<nyash_rust::ASTNode> = Vec::new(); let mut out: Vec<nyash_rust::ASTNode> = Vec::new();
for x in a { match json_to_ast(x) { Ok(n) => out.push(n), Err(e) => { json_err(&format!("args element error: {}", e)); return None; } } } for x in a {
match json_to_ast(x) {
Ok(n) => out.push(n),
Err(e) => {
json_err(&format!("args element error: {}", e));
return None;
}
}
}
spec.args = out; spec.args = out;
} }
if let Some(inst) = obj.get("instance").and_then(|v| v.as_object()) { if let Some(inst) = obj.get("instance").and_then(|v| v.as_object()) {
let ctor = inst.get("ctor").and_then(|v| v.as_str()).unwrap_or("new").to_string(); let ctor = inst
let type_args: Vec<String> = inst.get("type_args").and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()).unwrap_or_default(); .get("ctor")
.and_then(|v| v.as_str())
.unwrap_or("new")
.to_string();
let type_args: Vec<String> = inst
.get("type_args")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|x| x.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let mut args: Vec<nyash_rust::ASTNode> = Vec::new(); let mut args: Vec<nyash_rust::ASTNode> = Vec::new();
if let Some(va) = inst.get("args").and_then(|v| v.as_array()) { if let Some(va) = inst.get("args").and_then(|v| v.as_array()) {
for x in va { match json_to_ast(x) { Ok(n) => args.push(n), Err(e) => { json_err(&format!("instance.args element error: {}", e)); return None; } } } for x in va {
match json_to_ast(x) {
Ok(n) => args.push(n),
Err(e) => {
json_err(&format!("instance.args element error: {}", e));
return None;
} }
spec.instance = Some(InstanceSpec { ctor, args, type_args }); }
}
}
spec.instance = Some(InstanceSpec {
ctor,
args,
type_args,
});
} }
Some(spec) Some(spec)
} }
_ => { json_err("test value must be array or object"); None } _ => {
json_err("test value must be array or object");
None
}
} }
} }
let args_map: Option<std::collections::HashMap<String, TestArgSpec>> = (|| { let args_map: Option<std::collections::HashMap<String, TestArgSpec>> = (|| {
if let Ok(s) = std::env::var("NYASH_TEST_ARGS_JSON") { if let Ok(s) = std::env::var("NYASH_TEST_ARGS_JSON") {
if s.trim().is_empty() { return None; } if s.trim().is_empty() {
return None;
}
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) { if let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) {
let mut map = std::collections::HashMap::new(); let mut map = std::collections::HashMap::new();
if let Some(obj) = v.as_object() { if let Some(obj) = v.as_object() {
for (k, vv) in obj { if let Some(spec) = parse_test_arg_spec(vv) { map.insert(k.clone(), spec); } } for (k, vv) in obj {
if let Some(spec) = parse_test_arg_spec(vv) {
map.insert(k.clone(), spec);
}
}
return Some(map); return Some(map);
} }
} }
@ -190,21 +352,48 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
for st in statements { for st in statements {
match st { match st {
nyash_rust::ASTNode::FunctionDeclaration { name, params, .. } => { nyash_rust::ASTNode::FunctionDeclaration { name, params, .. } => {
if name == "main" { has_main_fn = true; _main_params_len = params.len(); } if name == "main" {
has_main_fn = true;
_main_params_len = params.len();
}
if name.starts_with("test_") { if name.starts_with("test_") {
let label = name.clone(); let label = name.clone();
// select args: JSON map > defaults > skip // select args: JSON map > defaults > skip
let mut maybe_args: Option<Vec<nyash_rust::ASTNode>> = None; let mut maybe_args: Option<Vec<nyash_rust::ASTNode>> = None;
if let Some(m) = &args_map { if let Some(v) = m.get(&label) { maybe_args = Some(v.args.clone()); } } if let Some(m) = &args_map {
let args = if let Some(a) = maybe_args { a } if let Some(v) = m.get(&label) {
else if !params.is_empty() && std::env::var("NYASH_TEST_ARGS_DEFAULTS").ok().as_deref() == Some("1") { maybe_args = Some(v.args.clone());
let mut a: Vec<nyash_rust::ASTNode> = Vec::new(); for _ in params { a.push(nyash_rust::ASTNode::Literal{ value: nyash_rust::ast::LiteralValue::Integer(0), span: nyash_rust::ast::Span::unknown() }); } a }
} else if params.is_empty() { Vec::new() } }
else { let args = if let Some(a) = maybe_args {
a
} else if !params.is_empty()
&& std::env::var("NYASH_TEST_ARGS_DEFAULTS").ok().as_deref()
== Some("1")
{
let mut a: Vec<nyash_rust::ASTNode> = Vec::new();
for _ in params {
a.push(nyash_rust::ASTNode::Literal {
value: nyash_rust::ast::LiteralValue::Integer(0),
span: nyash_rust::ast::Span::unknown(),
});
}
a
} else if params.is_empty() {
Vec::new()
} else {
eprintln!("[macro][test][args] missing args for {} (need {}), skipping (set NYASH_TEST_ARGS_DEFAULTS=1 for zero defaults)", label, params.len()); eprintln!("[macro][test][args] missing args for {} (need {}), skipping (set NYASH_TEST_ARGS_DEFAULTS=1 for zero defaults)", label, params.len());
continue continue;
}; };
tests.push(TestPlan { label, setup: None, call: nyash_rust::ASTNode::FunctionCall { name: name.clone(), arguments: args, span: nyash_rust::ast::Span::unknown() } }); tests.push(TestPlan {
label,
setup: None,
call: nyash_rust::ASTNode::FunctionCall {
name: name.clone(),
arguments: args,
span: nyash_rust::ast::Span::unknown(),
},
});
} }
} }
_ => {} _ => {}
@ -214,45 +403,101 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
// Collect Box tests: static and instance (no-arg only for instance) // Collect Box tests: static and instance (no-arg only for instance)
if let nyash_rust::ASTNode::Program { statements, .. } = ast { if let nyash_rust::ASTNode::Program { statements, .. } = ast {
for st in statements { for st in statements {
if let nyash_rust::ASTNode::BoxDeclaration { name: box_name, methods, .. } = st { if let nyash_rust::ASTNode::BoxDeclaration {
name: box_name,
methods,
..
} = st
{
for (mname, mnode) in methods { for (mname, mnode) in methods {
if !mname.starts_with("test_") { continue; } if !mname.starts_with("test_") {
if let nyash_rust::ASTNode::FunctionDeclaration { is_static, params, .. } = mnode { continue;
}
if let nyash_rust::ASTNode::FunctionDeclaration {
is_static, params, ..
} = mnode
{
if *is_static { if *is_static {
// Static: BoxName.test_*() // Static: BoxName.test_*()
let mut args: Vec<nyash_rust::ASTNode> = Vec::new(); let mut args: Vec<nyash_rust::ASTNode> = Vec::new();
if let Some(m) = &args_map { if let Some(v) = m.get(&format!("{}.{}", box_name, mname)) { args = v.args.clone(); } } if let Some(m) = &args_map {
if let Some(v) = m.get(&format!("{}.{}", box_name, mname)) {
args = v.args.clone();
}
}
if args.is_empty() && !params.is_empty() { if args.is_empty() && !params.is_empty() {
if std::env::var("NYASH_TEST_ARGS_DEFAULTS").ok().as_deref() == Some("1") { for _ in params { args.push(nyash_rust::ASTNode::Literal { value: nyash_rust::ast::LiteralValue::Integer(0), span: nyash_rust::ast::Span::unknown() }); } } if std::env::var("NYASH_TEST_ARGS_DEFAULTS").ok().as_deref()
else { == Some("1")
{
for _ in params {
args.push(nyash_rust::ASTNode::Literal {
value: nyash_rust::ast::LiteralValue::Integer(0),
span: nyash_rust::ast::Span::unknown(),
});
}
} else {
eprintln!("[macro][test][args] missing args for {}.{} (need {}), skipping", box_name, mname, params.len()); eprintln!("[macro][test][args] missing args for {}.{} (need {}), skipping", box_name, mname, params.len());
continue; continue;
} }
} }
let call = nyash_rust::ASTNode::MethodCall { let call = nyash_rust::ASTNode::MethodCall {
object: Box::new(nyash_rust::ASTNode::Variable { name: box_name.clone(), span: nyash_rust::ast::Span::unknown() }), object: Box::new(nyash_rust::ASTNode::Variable {
name: box_name.clone(),
span: nyash_rust::ast::Span::unknown(),
}),
method: mname.clone(), method: mname.clone(),
arguments: args, arguments: args,
span: nyash_rust::ast::Span::unknown(), span: nyash_rust::ast::Span::unknown(),
}; };
tests.push(TestPlan { label: format!("{}.{}", box_name, mname), setup: None, call }); tests.push(TestPlan {
label: format!("{}.{}", box_name, mname),
setup: None,
call,
});
} else { } else {
// Instance: try new BoxName() then .test_*() // Instance: try new BoxName() then .test_*()
let inst_var = format!("__t_{}", box_name.to_lowercase()); let inst_var = format!("__t_{}", box_name.to_lowercase());
// Instance override via JSON // Instance override via JSON
let mut inst_ctor: Option<InstanceSpec> = None; let mut inst_ctor: Option<InstanceSpec> = None;
if let Some(m) = &args_map { if let Some(v) = m.get(&format!("{}.{}", box_name, mname)) { inst_ctor = v.instance.clone(); } } if let Some(m) = &args_map {
if let Some(v) = m.get(&format!("{}.{}", box_name, mname)) {
inst_ctor = v.instance.clone();
}
}
let inst_init: nyash_rust::ASTNode = if let Some(spec) = inst_ctor { let inst_init: nyash_rust::ASTNode = if let Some(spec) = inst_ctor {
match spec.ctor.as_str() { match spec.ctor.as_str() {
"new" => nyash_rust::ASTNode::New { class: box_name.clone(), arguments: spec.args, type_arguments: spec.type_args, span: nyash_rust::ast::Span::unknown() }, "new" => nyash_rust::ASTNode::New {
"birth" => nyash_rust::ASTNode::MethodCall { object: Box::new(nyash_rust::ASTNode::Variable { name: box_name.clone(), span: nyash_rust::ast::Span::unknown() }), method: "birth".into(), arguments: spec.args, span: nyash_rust::ast::Span::unknown() }, class: box_name.clone(),
arguments: spec.args,
type_arguments: spec.type_args,
span: nyash_rust::ast::Span::unknown(),
},
"birth" => nyash_rust::ASTNode::MethodCall {
object: Box::new(nyash_rust::ASTNode::Variable {
name: box_name.clone(),
span: nyash_rust::ast::Span::unknown(),
}),
method: "birth".into(),
arguments: spec.args,
span: nyash_rust::ast::Span::unknown(),
},
other => { other => {
eprintln!("[macro][test][args] unknown ctor '{}' for {}.{}, using new()", other, box_name, mname); eprintln!("[macro][test][args] unknown ctor '{}' for {}.{}, using new()", other, box_name, mname);
nyash_rust::ASTNode::New { class: box_name.clone(), arguments: vec![], type_arguments: vec![], span: nyash_rust::ast::Span::unknown() } nyash_rust::ASTNode::New {
class: box_name.clone(),
arguments: vec![],
type_arguments: vec![],
span: nyash_rust::ast::Span::unknown(),
}
} }
} }
} else { } else {
nyash_rust::ASTNode::New { class: box_name.clone(), arguments: vec![], type_arguments: vec![], span: nyash_rust::ast::Span::unknown() } nyash_rust::ASTNode::New {
class: box_name.clone(),
arguments: vec![],
type_arguments: vec![],
span: nyash_rust::ast::Span::unknown(),
}
}; };
let setup = nyash_rust::ASTNode::Local { let setup = nyash_rust::ASTNode::Local {
variables: vec![inst_var.clone()], variables: vec![inst_var.clone()],
@ -260,21 +505,40 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
span: nyash_rust::ast::Span::unknown(), span: nyash_rust::ast::Span::unknown(),
}; };
let mut args: Vec<nyash_rust::ASTNode> = Vec::new(); let mut args: Vec<nyash_rust::ASTNode> = Vec::new();
if let Some(m) = &args_map { if let Some(v) = m.get(&format!("{}.{}", box_name, mname)) { args = v.args.clone(); } } if let Some(m) = &args_map {
if let Some(v) = m.get(&format!("{}.{}", box_name, mname)) {
args = v.args.clone();
}
}
if args.is_empty() && !params.is_empty() { if args.is_empty() && !params.is_empty() {
if std::env::var("NYASH_TEST_ARGS_DEFAULTS").ok().as_deref() == Some("1") { for _ in params { args.push(nyash_rust::ASTNode::Literal { value: nyash_rust::ast::LiteralValue::Integer(0), span: nyash_rust::ast::Span::unknown() }); } } if std::env::var("NYASH_TEST_ARGS_DEFAULTS").ok().as_deref()
else { == Some("1")
{
for _ in params {
args.push(nyash_rust::ASTNode::Literal {
value: nyash_rust::ast::LiteralValue::Integer(0),
span: nyash_rust::ast::Span::unknown(),
});
}
} else {
eprintln!("[macro][test][args] missing args for {}.{} (need {}), skipping", box_name, mname, params.len()); eprintln!("[macro][test][args] missing args for {}.{} (need {}), skipping", box_name, mname, params.len());
continue; continue;
} }
} }
let call = nyash_rust::ASTNode::MethodCall { let call = nyash_rust::ASTNode::MethodCall {
object: Box::new(nyash_rust::ASTNode::Variable { name: inst_var.clone(), span: nyash_rust::ast::Span::unknown() }), object: Box::new(nyash_rust::ASTNode::Variable {
name: inst_var.clone(),
span: nyash_rust::ast::Span::unknown(),
}),
method: mname.clone(), method: mname.clone(),
arguments: args, arguments: args,
span: nyash_rust::ast::Span::unknown(), span: nyash_rust::ast::Span::unknown(),
}; };
tests.push(TestPlan { label: format!("{}.{}", box_name, mname), setup: Some(setup), call }); tests.push(TestPlan {
label: format!("{}.{}", box_name, mname),
setup: Some(setup),
call,
});
} }
} }
} }
@ -288,7 +552,9 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
} }
} }
if tests.is_empty() { if tests.is_empty() {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { eprintln!("[macro][test] no tests found (functions starting with 'test_')"); } if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][test] no tests found (functions starting with 'test_')");
}
return ast.clone(); return ast.clone();
} }
// Decide entry policy when main exists // Decide entry policy when main exists
@ -297,34 +563,128 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
let ret_policy = std::env::var("NYASH_TEST_RETURN").ok(); // Some("tests"|"original") default tests let ret_policy = std::env::var("NYASH_TEST_RETURN").ok(); // Some("tests"|"original") default tests
// Build harness: top-level function main(args) { ... } // Build harness: top-level function main(args) { ... }
use nyash_rust::ast::{ASTNode as A, Span, LiteralValue, BinaryOperator}; use nyash_rust::ast::{ASTNode as A, BinaryOperator, LiteralValue, Span};
let mut body: Vec<A> = Vec::new(); let mut body: Vec<A> = Vec::new();
// locals: pass=0, fail=0 // locals: pass=0, fail=0
body.push(A::Local { variables: vec!["pass".into(), "fail".into()], initial_values: vec![Some(Box::new(A::Literal{ value: LiteralValue::Integer(0), span: Span::unknown()})), Some(Box::new(A::Literal{ value: LiteralValue::Integer(0), span: Span::unknown()}))], span: Span::unknown() }); body.push(A::Local {
variables: vec!["pass".into(), "fail".into()],
initial_values: vec![
Some(Box::new(A::Literal {
value: LiteralValue::Integer(0),
span: Span::unknown(),
})),
Some(Box::new(A::Literal {
value: LiteralValue::Integer(0),
span: Span::unknown(),
})),
],
span: Span::unknown(),
});
for tp in &tests { for tp in &tests {
// optional setup // optional setup
if let Some(set) = tp.setup.clone() { body.push(set); } if let Some(set) = tp.setup.clone() {
body.push(set);
}
// local r = CALL // local r = CALL
body.push(A::Local { variables: vec!["r".into()], initial_values: vec![Some(Box::new(tp.call.clone()))], span: Span::unknown() }); body.push(A::Local {
variables: vec!["r".into()],
initial_values: vec![Some(Box::new(tp.call.clone()))],
span: Span::unknown(),
});
// if r { print("PASS t"); pass = pass + 1 } else { print("FAIL t"); fail = fail + 1 } // if r { print("PASS t"); pass = pass + 1 } else { print("FAIL t"); fail = fail + 1 }
let pass_msg = A::Literal { value: LiteralValue::String(format!("PASS {}", tp.label)), span: Span::unknown() }; let pass_msg = A::Literal {
let fail_msg = A::Literal { value: LiteralValue::String(format!("FAIL {}", tp.label)), span: Span::unknown() }; value: LiteralValue::String(format!("PASS {}", tp.label)),
span: Span::unknown(),
};
let fail_msg = A::Literal {
value: LiteralValue::String(format!("FAIL {}", tp.label)),
span: Span::unknown(),
};
let then_body = vec![ let then_body = vec![
A::Print { expression: Box::new(pass_msg), span: Span::unknown() }, A::Print {
A::Assignment { target: Box::new(A::Variable{ name: "pass".into(), span: Span::unknown() }), value: Box::new(A::BinaryOp{ operator: BinaryOperator::Add, left: Box::new(A::Variable{ name: "pass".into(), span: Span::unknown() }), right: Box::new(A::Literal{ value: LiteralValue::Integer(1), span: Span::unknown() }), span: Span::unknown() }), span: Span::unknown() }, expression: Box::new(pass_msg),
span: Span::unknown(),
},
A::Assignment {
target: Box::new(A::Variable {
name: "pass".into(),
span: Span::unknown(),
}),
value: Box::new(A::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(A::Variable {
name: "pass".into(),
span: Span::unknown(),
}),
right: Box::new(A::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
]; ];
let else_body = vec![ let else_body = vec![
A::Print { expression: Box::new(fail_msg), span: Span::unknown() }, A::Print {
A::Assignment { target: Box::new(A::Variable{ name: "fail".into(), span: Span::unknown() }), value: Box::new(A::BinaryOp{ operator: BinaryOperator::Add, left: Box::new(A::Variable{ name: "fail".into(), span: Span::unknown() }), right: Box::new(A::Literal{ value: LiteralValue::Integer(1), span: Span::unknown() }), span: Span::unknown() }), span: Span::unknown() }, expression: Box::new(fail_msg),
span: Span::unknown(),
},
A::Assignment {
target: Box::new(A::Variable {
name: "fail".into(),
span: Span::unknown(),
}),
value: Box::new(A::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(A::Variable {
name: "fail".into(),
span: Span::unknown(),
}),
right: Box::new(A::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
]; ];
body.push(A::If { condition: Box::new(A::Variable { name: "r".into(), span: Span::unknown() }), then_body, else_body: Some(else_body), span: Span::unknown() }); body.push(A::If {
condition: Box::new(A::Variable {
name: "r".into(),
span: Span::unknown(),
}),
then_body,
else_body: Some(else_body),
span: Span::unknown(),
});
} }
// print summary and return fail // print summary and return fail
body.push(A::Print { expression: Box::new(A::Literal{ value: LiteralValue::String(format!("Summary: {} tests", tests.len())), span: Span::unknown() }), span: Span::unknown() }); body.push(A::Print {
body.push(A::Return { value: Some(Box::new(A::Variable{ name: "fail".into(), span: Span::unknown() })), span: Span::unknown() }); expression: Box::new(A::Literal {
value: LiteralValue::String(format!("Summary: {} tests", tests.len())),
span: Span::unknown(),
}),
span: Span::unknown(),
});
body.push(A::Return {
value: Some(Box::new(A::Variable {
name: "fail".into(),
span: Span::unknown(),
})),
span: Span::unknown(),
});
// Build harness main body as above // Build harness main body as above
let make_harness_main = |body: Vec<A>| -> A { let make_harness_main = |body: Vec<A>| -> A {
A::FunctionDeclaration { name: "main".into(), params: vec!["args".into()], body, is_static: false, is_override: false, span: Span::unknown() } A::FunctionDeclaration {
name: "main".into(),
params: vec!["args".into()],
body,
is_static: false,
is_override: false,
span: Span::unknown(),
}
}; };
// Transform AST according to policy // Transform AST according to policy
@ -334,27 +694,64 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
let mut orig_call_fn: Option<A> = None; let mut orig_call_fn: Option<A> = None;
for st in statements { for st in statements {
match st { match st {
A::FunctionDeclaration { name, params, body: orig_body, is_static, is_override, span: fspan } if name == "main" => { A::FunctionDeclaration {
name,
params,
body: orig_body,
is_static,
is_override,
span: fspan,
} if name == "main" => {
if has_main_fn && (force || entry_mode.is_some()) { if has_main_fn && (force || entry_mode.is_some()) {
// rename original main // rename original main
let new_name = "__ny_orig_main".to_string(); let new_name = "__ny_orig_main".to_string();
out_stmts.push(A::FunctionDeclaration { name: new_name.clone(), params: params.clone(), body: orig_body.clone(), is_static, is_override, span: fspan }); out_stmts.push(A::FunctionDeclaration {
name: new_name.clone(),
params: params.clone(),
body: orig_body.clone(),
is_static,
is_override,
span: fspan,
});
_renamed_main = true; _renamed_main = true;
if entry_mode.as_deref() == Some("wrap") { if entry_mode.as_deref() == Some("wrap") {
let args_exprs = if params.len() >= 1 { vec![A::Variable { name: "args".into(), span: nyash_rust::ast::Span::unknown() }] } else { vec![] }; let args_exprs = if params.len() >= 1 {
orig_call_fn = Some(A::FunctionCall { name: new_name, arguments: args_exprs, span: nyash_rust::ast::Span::unknown() }); vec![A::Variable {
name: "args".into(),
span: nyash_rust::ast::Span::unknown(),
}]
} else {
vec![]
};
orig_call_fn = Some(A::FunctionCall {
name: new_name,
arguments: args_exprs,
span: nyash_rust::ast::Span::unknown(),
});
} }
} else { } else {
// keep as-is (no injection) // keep as-is (no injection)
out_stmts.push(A::FunctionDeclaration { name, params, body: orig_body, is_static, is_override, span: fspan }); out_stmts.push(A::FunctionDeclaration {
name,
params,
body: orig_body,
is_static,
is_override,
span: fspan,
});
} }
} }
other => out_stmts.push(other), other => out_stmts.push(other),
} }
} }
if has_main_fn && !(force || entry_mode.is_some()) { if has_main_fn && !(force || entry_mode.is_some()) {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { eprintln!("[macro][test] existing main detected; skip harness (set --test-entry or NYASH_TEST_FORCE=1)"); } if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
return nyash_rust::ASTNode::Program { statements: out_stmts, span }; eprintln!("[macro][test] existing main detected; skip harness (set --test-entry or NYASH_TEST_FORCE=1)");
}
return nyash_rust::ASTNode::Program {
statements: out_stmts,
span,
};
} }
// Compose harness main now // Compose harness main now
let mut body2 = body; let mut body2 = body;
@ -362,9 +759,19 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
if let Some(call) = orig_call_fn.take() { if let Some(call) = orig_call_fn.take() {
if ret_policy.as_deref() == Some("original") { if ret_policy.as_deref() == Some("original") {
// local __ny_orig_ret = __ny_orig_main(args) // local __ny_orig_ret = __ny_orig_main(args)
body2.push(A::Local { variables: vec!["__ny_orig_ret".into()], initial_values: vec![Some(Box::new(call))], span: nyash_rust::ast::Span::unknown() }); body2.push(A::Local {
variables: vec!["__ny_orig_ret".into()],
initial_values: vec![Some(Box::new(call))],
span: nyash_rust::ast::Span::unknown(),
});
// return __ny_orig_ret // return __ny_orig_ret
body2.push(A::Return { value: Some(Box::new(A::Variable { name: "__ny_orig_ret".into(), span: nyash_rust::ast::Span::unknown() })), span: nyash_rust::ast::Span::unknown() }); body2.push(A::Return {
value: Some(Box::new(A::Variable {
name: "__ny_orig_ret".into(),
span: nyash_rust::ast::Span::unknown(),
})),
span: nyash_rust::ast::Span::unknown(),
});
} else { } else {
// default: tests policy; still call original but ignore result // default: tests policy; still call original but ignore result
body2.push(call); body2.push(call);
@ -373,7 +780,10 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
} }
let harness_fn = make_harness_main(body2); let harness_fn = make_harness_main(body2);
out_stmts.push(harness_fn); out_stmts.push(harness_fn);
return nyash_rust::ASTNode::Program { statements: out_stmts, span }; return nyash_rust::ASTNode::Program {
statements: out_stmts,
span,
};
} }
ast.clone() ast.clone()
} }

View File

@ -1,5 +1,5 @@
use std::collections::HashMap;
use nyash_rust::ASTNode; use nyash_rust::ASTNode;
use std::collections::HashMap;
/// Minimal pattern trait — MVP /// Minimal pattern trait — MVP
pub trait MacroPattern { pub trait MacroPattern {
@ -10,35 +10,71 @@ pub trait MacroPattern {
pub struct AstBuilder; pub struct AstBuilder;
impl AstBuilder { impl AstBuilder {
pub fn new() -> Self { Self } pub fn new() -> Self {
Self
}
pub fn quote(&self, code: &str) -> ASTNode { pub fn quote(&self, code: &str) -> ASTNode {
// MVP: parse string into AST using existing parser // MVP: parse string into AST using existing parser
match nyash_rust::parser::NyashParser::parse_from_string(code) { match nyash_rust::parser::NyashParser::parse_from_string(code) {
Ok(ast) => ast, Ok(ast) => ast,
Err(_) => ASTNode::Program { statements: vec![], span: nyash_rust::ast::Span::unknown() }, Err(_) => ASTNode::Program {
statements: vec![],
span: nyash_rust::ast::Span::unknown(),
},
} }
} }
pub fn unquote(&self, template: &ASTNode, _bindings: &HashMap<String, ASTNode>) -> ASTNode { pub fn unquote(&self, template: &ASTNode, _bindings: &HashMap<String, ASTNode>) -> ASTNode {
// Replace Variables named like "$name" with corresponding bound AST // Replace Variables named like "$name" with corresponding bound AST
fn is_placeholder(name: &str) -> Option<&str> { fn is_placeholder(name: &str) -> Option<&str> {
if name.starts_with('$') && name.len() > 1 { Some(&name[1..]) } else { None } if name.starts_with('$') && name.len() > 1 {
Some(&name[1..])
} else {
None
}
} }
fn is_variadic(name: &str) -> Option<&str> { fn is_variadic(name: &str) -> Option<&str> {
if name.starts_with("$...") && name.len() > 4 { Some(&name[4..]) } else { None } if name.starts_with("$...") && name.len() > 4 {
Some(&name[4..])
} else {
None
}
} }
fn subst(node: &ASTNode, binds: &HashMap<String, ASTNode>) -> ASTNode { fn subst(node: &ASTNode, binds: &HashMap<String, ASTNode>) -> ASTNode {
match node.clone() { match node.clone() {
ASTNode::Variable { name, .. } => { ASTNode::Variable { name, .. } => {
if let Some(k) = is_placeholder(&name) { if let Some(k) = is_placeholder(&name) {
if let Some(v) = binds.get(k) { return v.clone(); } if let Some(v) = binds.get(k) {
return v.clone();
}
} }
node.clone() node.clone()
} }
ASTNode::BinaryOp { operator, left, right, span } => ASTNode::BinaryOp { ASTNode::BinaryOp {
operator, left: Box::new(subst(&left, binds)), right: Box::new(subst(&right, binds)), span operator,
left,
right,
span,
} => ASTNode::BinaryOp {
operator,
left: Box::new(subst(&left, binds)),
right: Box::new(subst(&right, binds)),
span,
}, },
ASTNode::UnaryOp { operator, operand, span } => ASTNode::UnaryOp { operator, operand: Box::new(subst(&operand, binds)), span }, ASTNode::UnaryOp {
ASTNode::MethodCall { object, method, arguments, span } => { operator,
operand,
span,
} => ASTNode::UnaryOp {
operator,
operand: Box::new(subst(&operand, binds)),
span,
},
ASTNode::MethodCall {
object,
method,
arguments,
span,
} => {
let mut out_args: Vec<ASTNode> = Vec::new(); let mut out_args: Vec<ASTNode> = Vec::new();
let mut i = 0usize; let mut i = 0usize;
while i < arguments.len() { while i < arguments.len() {
@ -54,9 +90,18 @@ impl AstBuilder {
out_args.push(subst(&arguments[i], binds)); out_args.push(subst(&arguments[i], binds));
i += 1; i += 1;
} }
ASTNode::MethodCall { object: Box::new(subst(&object, binds)), method, arguments: out_args, span } ASTNode::MethodCall {
object: Box::new(subst(&object, binds)),
method,
arguments: out_args,
span,
} }
ASTNode::FunctionCall { name, arguments, span } => { }
ASTNode::FunctionCall {
name,
arguments,
span,
} => {
let mut out_args: Vec<ASTNode> = Vec::new(); let mut out_args: Vec<ASTNode> = Vec::new();
let mut i = 0usize; let mut i = 0usize;
while i < arguments.len() { while i < arguments.len() {
@ -72,7 +117,11 @@ impl AstBuilder {
out_args.push(subst(&arguments[i], binds)); out_args.push(subst(&arguments[i], binds));
i += 1; i += 1;
} }
ASTNode::FunctionCall { name, arguments: out_args, span } ASTNode::FunctionCall {
name,
arguments: out_args,
span,
}
} }
ASTNode::ArrayLiteral { elements, span } => { ASTNode::ArrayLiteral { elements, span } => {
// Splice variadic placeholder inside arrays // Splice variadic placeholder inside arrays
@ -91,21 +140,55 @@ impl AstBuilder {
out_elems.push(subst(&elements[i], binds)); out_elems.push(subst(&elements[i], binds));
i += 1; i += 1;
} }
ASTNode::ArrayLiteral { elements: out_elems, span } ASTNode::ArrayLiteral {
elements: out_elems,
span,
} }
ASTNode::MapLiteral { entries, span } => {
ASTNode::MapLiteral { entries: entries.into_iter().map(|(k, v)| (k, subst(&v, binds))).collect(), span }
} }
ASTNode::FieldAccess { object, field, span } => ASTNode::FieldAccess { object: Box::new(subst(&object, binds)), field, span }, ASTNode::MapLiteral { entries, span } => ASTNode::MapLiteral {
ASTNode::Assignment { target, value, span } => ASTNode::Assignment { target: Box::new(subst(&target, binds)), value: Box::new(subst(&value, binds)), span }, entries: entries
ASTNode::Return { value, span } => ASTNode::Return { value: value.as_ref().map(|v| Box::new(subst(v, binds))), span }, .into_iter()
ASTNode::If { condition, then_body, else_body, span } => ASTNode::If { .map(|(k, v)| (k, subst(&v, binds)))
.collect(),
span,
},
ASTNode::FieldAccess {
object,
field,
span,
} => ASTNode::FieldAccess {
object: Box::new(subst(&object, binds)),
field,
span,
},
ASTNode::Assignment {
target,
value,
span,
} => ASTNode::Assignment {
target: Box::new(subst(&target, binds)),
value: Box::new(subst(&value, binds)),
span,
},
ASTNode::Return { value, span } => ASTNode::Return {
value: value.as_ref().map(|v| Box::new(subst(v, binds))),
span,
},
ASTNode::If {
condition,
then_body,
else_body,
span,
} => ASTNode::If {
condition: Box::new(subst(&condition, binds)), condition: Box::new(subst(&condition, binds)),
then_body: then_body.into_iter().map(|n| subst(&n, binds)).collect(), then_body: then_body.into_iter().map(|n| subst(&n, binds)).collect(),
else_body: else_body.map(|v| v.into_iter().map(|n| subst(&n, binds)).collect()), else_body: else_body.map(|v| v.into_iter().map(|n| subst(&n, binds)).collect()),
span, span,
}, },
ASTNode::Program { statements, span } => ASTNode::Program { statements: statements.into_iter().map(|n| subst(&n, binds)).collect(), span }, ASTNode::Program { statements, span } => ASTNode::Program {
statements: statements.into_iter().map(|n| subst(&n, binds)).collect(),
span,
},
other => other, other => other,
} }
} }
@ -114,115 +197,326 @@ impl AstBuilder {
} }
/// Simple template-based pattern that uses Variables named "$name" as bind points. /// Simple template-based pattern that uses Variables named "$name" as bind points.
pub struct TemplatePattern { pub template: ASTNode } pub struct TemplatePattern {
pub template: ASTNode,
}
impl TemplatePattern { pub fn new(template: ASTNode) -> Self { Self { template } } } impl TemplatePattern {
pub fn new(template: ASTNode) -> Self {
Self { template }
}
}
impl MacroPattern for TemplatePattern { impl MacroPattern for TemplatePattern {
fn match_ast(&self, node: &ASTNode) -> Option<HashMap<String, ASTNode>> { fn match_ast(&self, node: &ASTNode) -> Option<HashMap<String, ASTNode>> {
fn is_placeholder(name: &str) -> Option<&str> { fn is_placeholder(name: &str) -> Option<&str> {
if name.starts_with('$') && name.len() > 1 { Some(&name[1..]) } else { None } if name.starts_with('$') && name.len() > 1 {
Some(&name[1..])
} else {
None
}
} }
fn is_variadic(name: &str) -> Option<&str> { fn is_variadic(name: &str) -> Option<&str> {
if name.starts_with("$...") && name.len() > 4 { Some(&name[4..]) } else { None } if name.starts_with("$...") && name.len() > 4 {
Some(&name[4..])
} else {
None
}
} }
fn go(tpl: &ASTNode, tgt: &ASTNode, out: &mut HashMap<String, ASTNode>) -> bool { fn go(tpl: &ASTNode, tgt: &ASTNode, out: &mut HashMap<String, ASTNode>) -> bool {
match (tpl, tgt) { match (tpl, tgt) {
(ASTNode::Variable { name, .. }, v) => { (ASTNode::Variable { name, .. }, v) => {
if let Some(k) = is_placeholder(name) { out.insert(k.to_string(), v.clone()); true } else { tpl == tgt } if let Some(k) = is_placeholder(name) {
out.insert(k.to_string(), v.clone());
true
} else {
tpl == tgt
} }
(ASTNode::Literal { .. }, _) | (ASTNode::Me { .. }, _) | (ASTNode::This { .. }, _) => tpl == tgt,
(ASTNode::BinaryOp { operator: op1, left: l1, right: r1, .. }, ASTNode::BinaryOp { operator: op2, left: l2, right: r2, .. }) => {
op1 == op2 && go(l1, l2, out) && go(r1, r2, out)
} }
(ASTNode::UnaryOp { operator: o1, operand: a1, .. }, ASTNode::UnaryOp { operator: o2, operand: a2, .. }) => { (ASTNode::Literal { .. }, _)
o1 == o2 && go(a1, a2, out) | (ASTNode::Me { .. }, _)
| (ASTNode::This { .. }, _) => tpl == tgt,
(
ASTNode::BinaryOp {
operator: op1,
left: l1,
right: r1,
..
},
ASTNode::BinaryOp {
operator: op2,
left: l2,
right: r2,
..
},
) => op1 == op2 && go(l1, l2, out) && go(r1, r2, out),
(
ASTNode::UnaryOp {
operator: o1,
operand: a1,
..
},
ASTNode::UnaryOp {
operator: o2,
operand: a2,
..
},
) => o1 == o2 && go(a1, a2, out),
(
ASTNode::MethodCall {
object: o1,
method: m1,
arguments: a1,
..
},
ASTNode::MethodCall {
object: o2,
method: m2,
arguments: a2,
..
},
) => {
if m1 != m2 {
return false;
}
if !go(o1, o2, out) {
return false;
} }
(ASTNode::MethodCall { object: o1, method: m1, arguments: a1, .. }, ASTNode::MethodCall { object: o2, method: m2, arguments: a2, .. }) => {
if m1 != m2 { return false; }
if !go(o1, o2, out) { return false; }
// Support variadic anywhere in a1 // Support variadic anywhere in a1
let mut varpos: Option<(usize, String)> = None; let mut varpos: Option<(usize, String)> = None;
for (i, arg) in a1.iter().enumerate() { for (i, arg) in a1.iter().enumerate() {
if let ASTNode::Variable { name, .. } = arg { if let Some(vn) = is_variadic(name) { varpos = Some((i, vn.to_string())); break; } } if let ASTNode::Variable { name, .. } = arg {
if let Some(vn) = is_variadic(name) {
varpos = Some((i, vn.to_string()));
break;
}
}
} }
if let Some((k, vn)) = varpos { if let Some((k, vn)) = varpos {
let suffix_len = a1.len() - k - 1; let suffix_len = a1.len() - k - 1;
if a2.len() < k + suffix_len { return false; } if a2.len() < k + suffix_len {
for (x, y) in a1[..k].iter().zip(a2.iter()) { if !go(x, y, out) { return false; } } return false;
}
for (x, y) in a1[..k].iter().zip(a2.iter()) {
if !go(x, y, out) {
return false;
}
}
for (i, x) in a1[a1.len() - suffix_len..].iter().enumerate() { for (i, x) in a1[a1.len() - suffix_len..].iter().enumerate() {
let y = &a2[a2.len() - suffix_len + i]; let y = &a2[a2.len() - suffix_len + i];
if !go(x, y, out) { return false; } if !go(x, y, out) {
return false;
}
} }
let tail: Vec<ASTNode> = a2[k..a2.len() - suffix_len].to_vec(); let tail: Vec<ASTNode> = a2[k..a2.len() - suffix_len].to_vec();
out.insert(vn, ASTNode::Program { statements: tail, span: nyash_rust::ast::Span::unknown() }); out.insert(
vn,
ASTNode::Program {
statements: tail,
span: nyash_rust::ast::Span::unknown(),
},
);
return true; return true;
} }
if a1.len() != a2.len() { return false; } if a1.len() != a2.len() {
for (x, y) in a1.iter().zip(a2.iter()) { if !go(x, y, out) { return false; } } return false;
}
for (x, y) in a1.iter().zip(a2.iter()) {
if !go(x, y, out) {
return false;
}
}
true true
} }
(ASTNode::FunctionCall { name: n1, arguments: a1, .. }, ASTNode::FunctionCall { name: n2, arguments: a2, .. }) => { (
if n1 != n2 { return false; } ASTNode::FunctionCall {
name: n1,
arguments: a1,
..
},
ASTNode::FunctionCall {
name: n2,
arguments: a2,
..
},
) => {
if n1 != n2 {
return false;
}
let mut varpos: Option<(usize, String)> = None; let mut varpos: Option<(usize, String)> = None;
for (i, arg) in a1.iter().enumerate() { for (i, arg) in a1.iter().enumerate() {
if let ASTNode::Variable { name, .. } = arg { if let Some(vn) = is_variadic(name) { varpos = Some((i, vn.to_string())); break; } } if let ASTNode::Variable { name, .. } = arg {
if let Some(vn) = is_variadic(name) {
varpos = Some((i, vn.to_string()));
break;
}
}
} }
if let Some((k, vn)) = varpos { if let Some((k, vn)) = varpos {
let suffix_len = a1.len() - k - 1; let suffix_len = a1.len() - k - 1;
if a2.len() < k + suffix_len { return false; } if a2.len() < k + suffix_len {
for (x, y) in a1[..k].iter().zip(a2.iter()) { if !go(x, y, out) { return false; } } return false;
}
for (x, y) in a1[..k].iter().zip(a2.iter()) {
if !go(x, y, out) {
return false;
}
}
for (i, x) in a1[a1.len() - suffix_len..].iter().enumerate() { for (i, x) in a1[a1.len() - suffix_len..].iter().enumerate() {
let y = &a2[a2.len() - suffix_len + i]; let y = &a2[a2.len() - suffix_len + i];
if !go(x, y, out) { return false; } if !go(x, y, out) {
return false;
}
} }
let tail: Vec<ASTNode> = a2[k..a2.len() - suffix_len].to_vec(); let tail: Vec<ASTNode> = a2[k..a2.len() - suffix_len].to_vec();
out.insert(vn, ASTNode::Program { statements: tail, span: nyash_rust::ast::Span::unknown() }); out.insert(
vn,
ASTNode::Program {
statements: tail,
span: nyash_rust::ast::Span::unknown(),
},
);
return true; return true;
} }
if a1.len() != a2.len() { return false; } if a1.len() != a2.len() {
for (x, y) in a1.iter().zip(a2.iter()) { if !go(x, y, out) { return false; } } return false;
}
for (x, y) in a1.iter().zip(a2.iter()) {
if !go(x, y, out) {
return false;
}
}
true true
} }
(ASTNode::ArrayLiteral { elements: e1, .. }, ASTNode::ArrayLiteral { elements: e2, .. }) => { (
ASTNode::ArrayLiteral { elements: e1, .. },
ASTNode::ArrayLiteral { elements: e2, .. },
) => {
let mut varpos: Option<(usize, String)> = None; let mut varpos: Option<(usize, String)> = None;
for (i, el) in e1.iter().enumerate() { for (i, el) in e1.iter().enumerate() {
if let ASTNode::Variable { name, .. } = el { if let Some(vn) = is_variadic(name) { varpos = Some((i, vn.to_string())); break; } } if let ASTNode::Variable { name, .. } = el {
if let Some(vn) = is_variadic(name) {
varpos = Some((i, vn.to_string()));
break;
}
}
} }
if let Some((k, vn)) = varpos { if let Some((k, vn)) = varpos {
let suffix_len = e1.len() - k - 1; let suffix_len = e1.len() - k - 1;
if e2.len() < k + suffix_len { return false; } if e2.len() < k + suffix_len {
for (x, y) in e1[..k].iter().zip(e2.iter()) { if !go(x, y, out) { return false; } } return false;
}
for (x, y) in e1[..k].iter().zip(e2.iter()) {
if !go(x, y, out) {
return false;
}
}
for (i, x) in e1[e1.len() - suffix_len..].iter().enumerate() { for (i, x) in e1[e1.len() - suffix_len..].iter().enumerate() {
let y = &e2[e2.len() - suffix_len + i]; let y = &e2[e2.len() - suffix_len + i];
if !go(x, y, out) { return false; } if !go(x, y, out) {
return false;
}
} }
let tail: Vec<ASTNode> = e2[k..e2.len() - suffix_len].to_vec(); let tail: Vec<ASTNode> = e2[k..e2.len() - suffix_len].to_vec();
out.insert(vn, ASTNode::Program { statements: tail, span: nyash_rust::ast::Span::unknown() }); out.insert(
vn,
ASTNode::Program {
statements: tail,
span: nyash_rust::ast::Span::unknown(),
},
);
return true; return true;
} }
if e1.len() != e2.len() { return false; } if e1.len() != e2.len() {
for (x, y) in e1.iter().zip(e2.iter()) { if !go(x, y, out) { return false; } } return false;
}
for (x, y) in e1.iter().zip(e2.iter()) {
if !go(x, y, out) {
return false;
}
}
true true
} }
(ASTNode::MapLiteral { entries: m1, .. }, ASTNode::MapLiteral { entries: m2, .. }) => { (
if m1.len() != m2.len() { return false; } ASTNode::MapLiteral { entries: m1, .. },
ASTNode::MapLiteral { entries: m2, .. },
) => {
if m1.len() != m2.len() {
return false;
}
for ((k1, v1), (k2, v2)) in m1.iter().zip(m2.iter()) { for ((k1, v1), (k2, v2)) in m1.iter().zip(m2.iter()) {
if k1 != k2 { return false; } if k1 != k2 {
if !go(v1, v2, out) { return false; } return false;
}
if !go(v1, v2, out) {
return false;
}
} }
true true
} }
(ASTNode::FieldAccess { object: o1, field: f1, .. }, ASTNode::FieldAccess { object: o2, field: f2, .. }) => f1 == f2 && go(o1, o2, out), (
(ASTNode::Assignment { target: t1, value: v1, .. }, ASTNode::Assignment { target: t2, value: v2, .. }) => go(t1, t2, out) && go(v1, v2, out), ASTNode::FieldAccess {
(ASTNode::Return { value: v1, .. }, ASTNode::Return { value: v2, .. }) => match (v1, v2) { (Some(a), Some(b)) => go(a, b, out), (None, None) => true, _ => false }, object: o1,
(ASTNode::If { condition: c1, then_body: t1, else_body: e1, .. }, ASTNode::If { condition: c2, then_body: t2, else_body: e2, .. }) => { field: f1,
if !go(c1, c2, out) || t1.len() != t2.len() { return false; } ..
for (x, y) in t1.iter().zip(t2.iter()) { if !go(x, y, out) { return false; } } },
ASTNode::FieldAccess {
object: o2,
field: f2,
..
},
) => f1 == f2 && go(o1, o2, out),
(
ASTNode::Assignment {
target: t1,
value: v1,
..
},
ASTNode::Assignment {
target: t2,
value: v2,
..
},
) => go(t1, t2, out) && go(v1, v2, out),
(ASTNode::Return { value: v1, .. }, ASTNode::Return { value: v2, .. }) => {
match (v1, v2) {
(Some(a), Some(b)) => go(a, b, out),
(None, None) => true,
_ => false,
}
}
(
ASTNode::If {
condition: c1,
then_body: t1,
else_body: e1,
..
},
ASTNode::If {
condition: c2,
then_body: t2,
else_body: e2,
..
},
) => {
if !go(c1, c2, out) || t1.len() != t2.len() {
return false;
}
for (x, y) in t1.iter().zip(t2.iter()) {
if !go(x, y, out) {
return false;
}
}
match (e1, e2) { match (e1, e2) {
(Some(a), Some(b)) => { (Some(a), Some(b)) => {
if a.len() != b.len() { return false; } if a.len() != b.len() {
for (x, y) in a.iter().zip(b.iter()) { if !go(x, y, out) { return false; } } return false;
}
for (x, y) in a.iter().zip(b.iter()) {
if !go(x, y, out) {
return false;
}
}
true true
} }
(None, None) => true, (None, None) => true,
@ -233,19 +527,31 @@ impl MacroPattern for TemplatePattern {
} }
} }
let mut out = HashMap::new(); let mut out = HashMap::new();
if go(&self.template, node, &mut out) { Some(out) } else { None } if go(&self.template, node, &mut out) {
Some(out)
} else {
None
}
} }
} }
/// ORパターン: いずれかのテンプレートにマッチすれば成功 /// ORパターン: いずれかのテンプレートにマッチすれば成功
pub struct OrPattern { pub alts: Vec<TemplatePattern> } pub struct OrPattern {
pub alts: Vec<TemplatePattern>,
}
impl OrPattern { pub fn new(alts: Vec<TemplatePattern>) -> Self { Self { alts } } } impl OrPattern {
pub fn new(alts: Vec<TemplatePattern>) -> Self {
Self { alts }
}
}
impl MacroPattern for OrPattern { impl MacroPattern for OrPattern {
fn match_ast(&self, node: &ASTNode) -> Option<HashMap<String, ASTNode>> { fn match_ast(&self, node: &ASTNode) -> Option<HashMap<String, ASTNode>> {
for tp in &self.alts { for tp in &self.alts {
if let Some(b) = tp.match_ast(node) { return Some(b); } if let Some(b) = tp.match_ast(node) {
return Some(b);
}
} }
None None
} }

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