From ed8e2d31426f1ad78c12e5bd6751c77a3d603578 Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Fri, 12 Dec 2025 03:15:45 +0900 Subject: [PATCH] feat(joinir): Phase 248 - Normalized JoinIR infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major refactoring of JoinIR normalization pipeline: Key changes: - Structured→Normalized→MIR(direct) pipeline established - ShapeGuard enhanced with Pattern2 loop validation - dev_env.rs: New development fixtures and env control - fixtures.rs: jsonparser_parse_number_real fixture - normalized_bridge/direct.rs: Direct MIR generation from Normalized - pattern2_step_schedule.rs: Extracted step scheduling logic Files changed: - normalized.rs: Enhanced NormalizedJoinModule with DevEnv support - shape_guard.rs: Pattern2-specific validation (+300 lines) - normalized_bridge.rs: Unified bridge with direct path - loop_with_break_minimal.rs: Integrated step scheduling - Deleted: step_schedule.rs (moved to pattern2_step_schedule.rs) New files: - param_guess.rs: Loop parameter inference - pattern2_step_schedule.rs: Step scheduling for Pattern2 - phase43-norm-canon-p2-mid.md: Design doc Tests: 937/937 PASS (+6 from baseline 931) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CURRENT_TASK.md | 12 +- .../main/joinir-architecture-overview.md | 68 +++- .../main/phase181-jsonparser-loop-roadmap.md | 31 ++ ...se223-loopbodylocal-condition-inventory.md | 3 + .../phase224-digitpos-condition-normalizer.md | 3 + ...e245b-jsonparser-num-str-carrier-design.md | 5 +- .../main/phase245b-num_str-carrier-design.md | 3 +- .../current/main/phase43-norm-canon-p2-mid.md | 148 +++++++++ src/config/env/joinir_dev.rs | 5 + .../joinir/patterns/pattern2_with_break.rs | 33 +- src/mir/join_ir/frontend/ast_lowerer/expr.rs | 2 + .../loop_patterns/break_pattern.rs | 52 +-- .../ast_lowerer/loop_patterns/common.rs | 19 +- .../frontend/ast_lowerer/loop_patterns/mod.rs | 1 + .../ast_lowerer/loop_patterns/param_guess.rs | 71 ++++ .../frontend/ast_lowerer/loop_patterns_old.rs | 44 ++- src/mir/join_ir/frontend/ast_lowerer/mod.rs | 33 +- .../lowering/loop_with_break_minimal.rs | 49 +-- .../loop_with_break_minimal/step_schedule.rs | 76 ----- src/mir/join_ir/lowering/mod.rs | 1 + .../lowering/pattern2_step_schedule.rs | 224 +++++++++++++ src/mir/join_ir/mod.rs | 2 - src/mir/join_ir/normalized.rs | 108 +++++- src/mir/join_ir/normalized/dev_env.rs | 95 +++++- src/mir/join_ir/normalized/fixtures.rs | 89 ++++- src/mir/join_ir/normalized/shape_guard.rs | 314 +++++++++++++----- src/mir/join_ir_runner.rs | 45 +-- src/mir/join_ir_vm_bridge/bridge.rs | 120 +++++-- .../join_ir_vm_bridge/normalized_bridge.rs | 80 +++-- .../normalized_bridge/direct.rs | 102 ++++-- src/tests/helpers/joinir_frontend.rs | 3 +- tests/normalized_joinir_min.rs | 139 +++++++- 32 files changed, 1559 insertions(+), 421 deletions(-) create mode 100644 docs/development/current/main/phase43-norm-canon-p2-mid.md create mode 100644 src/mir/join_ir/frontend/ast_lowerer/loop_patterns/param_guess.rs delete mode 100644 src/mir/join_ir/lowering/loop_with_break_minimal/step_schedule.rs create mode 100644 src/mir/join_ir/lowering/pattern2_step_schedule.rs diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 5601c74a..95ae8cea 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -24,9 +24,11 @@ - Pattern3/4 の公開 API を `can_lower + lower` の最小セットに整理し、内部 helper を箱の中に閉じた。 - `loop_pattern_detection` の classify() が代表ループを P1〜P4 に分類することをユニットテストで固定。 - Phase 245-EX / 245C: - - `_parse_number` のループについて、ループ変数 `p` の header/break 条件と `p = p + 1` 更新を Pattern2 + ExprLowerer 経路に載せて本番化(num_str は未導入)。 + - `_parse_number` のループについて、ループ変数 `p` の header/break 条件と `p = p + 1` 更新を Pattern2 + ExprLowerer 経路に載せて本番化。 - FunctionScopeCapture / CapturedEnv を拡張し、関数パラメータ(例: `s`, `len`)をループ条件/本体で読み取り専用なら CapturedEnv 経由で ConditionEnv に載せるようにした。 - これにより、`p < s.length()` のような header 条件や JsonParser 系ループでのパラメータ参照が、ExprLowerer/ScopeManager から安全に解決される。 +- Phase 245B-IMPL: + - `_parse_number` 本体で `num_str = num_str + ch` を LoopState キャリアとして扱い、Program(JSON) フィクスチャ `jsonparser_parse_number_real` を Structured→Normalized→MIR(direct) で dev テスト固定(出力は num_str 文字列)。 - Phase 247-EX: - DigitPos promotion を二重値化し、`digit_pos` から boolean carrier `is_digit_pos`(ConditionOnly)と integer carrier `digit_value`(LoopState)を生成。 - UpdateEnv で `digit_pos` 解決時に `digit_value` を優先し、NumberAccumulation(`result = result * 10 + digit_pos`)と break 条件の両方で DigitPos パターンが安全に利用可能に。 @@ -51,6 +53,14 @@ - P1/P2 ミニ + JsonParser skip_ws/atoi ミニを Normalized→MIR 直ブリッジで実行できるようにし、normalized_dev ON 時は Structured→Normalized→MIR(復元なし)経路との比較テストで結果一致を固定。既定経路(Structured→MIR)は不変。 - Phase 36-NORM-BRIDGE-DIRECT(dev-only): - Normalized ブリッジを direct 実装(Normalized→MIR)と Structured 再構成に分離し、shape_guard で P1/P2 ミニ + JsonParser skip_ws/atoi ミニだけ direct 経路に通すよう整理。非対応は `[joinir/normalized-bridge/fallback]` ログ付きで再構成に落とし、テストで direct/従来経路の VM 出力一致を固定。 +- Phase 37-NORM-JP-REAL(dev-only): + - JsonParser `_skip_whitespace` 本体の P2 ループを Program(JSON) フィクスチャで Structured→Normalized→MIR(direct) に通し、Structured 直経路との VM 出力一致を比較するテストを追加。`extract_value` が `&&`/`||` を BinOp として扱えるようにし、Break パターンの param 推定を柔軟化して real 形状でも panic しないようにした。 +- Phase 38-NORM-OBS(dev-only): + - Normalized/JoinIR dev 経路のログカテゴリを `[joinir/normalized-bridge/*]` / `[joinir/normalized-dev/shape]` に統一し、`JOINIR_TEST_DEBUG` 下だけ詳細を出すよう静音化。Verifier/Fail‑Fast メッセージも shape/役割付きに整え、デバッグ観測性を上げつつ通常実行のノイズを減らした。 +- Phase 43-A(dev-only): + - JsonParser `_atoi` 本体の Program(JSON) フィクスチャを normalized_dev に追加し、Structured→Normalized→MIR(direct) と Structured→MIR の VM 出力を比較するテストで一致を固定(符号あり/なしの簡易パス対応。canonical 切替は後続フェーズ)。 +- Phase 43-C(dev-only): + - JsonParser `_parse_number` 本体の Program(JSON) フィクスチャを normalized_dev に追加し、Structured→Normalized→MIR(direct) と Structured→MIR の VM 出力を比較するテストで一致を固定(num_str は現状仕様のまま据え置き、P2-Mid の足慣らし)。 ### 1. いまコード側で意識しておきたいフォーカス diff --git a/docs/development/current/main/joinir-architecture-overview.md b/docs/development/current/main/joinir-architecture-overview.md index 9d3e58c0..bc17f380 100644 --- a/docs/development/current/main/joinir-architecture-overview.md +++ b/docs/development/current/main/joinir-architecture-overview.md @@ -987,7 +987,7 @@ JoinIR は Rust 側だけでなく、将来的に .hako selfhost コンパイラ - [x] 退行なし: Phase 190-196 テスト全 PASS ✅ - 詳細: phase197-lightweight-loops-deployment.md -7. **JsonParser/selfhost 実戦 JoinIR 適用状況** (2025-12-09 更新) +7. **JsonParser/selfhost 実戦 JoinIR 適用状況** (2025-12-09 更新 → Phase 42 で棚卸し済み) | Function | Pattern | Status | Note | |----------|---------|--------|------| @@ -998,15 +998,15 @@ JoinIR は Rust 側だけでなく、将来的に .hako selfhost コンパイラ | `phase195_sum_count` | P3 | ✅ JoinIR OK | Phase 196 検証済み(multi-carrier)| | `loop_if_phi` | P3 | ✅ JoinIR OK | Phase 196 検証済み(single-carrier)| | `loop_min_while` | P1 | ✅ JoinIR OK | Phase 165 基本検証済み | -| `_parse_number` | P2 | ⚠️ Deferred | ConditionEnv 制約(Phase 200+)| -| `_atoi` | P2 | ⚠️ Deferred | ConditionEnv 制約(Phase 200+)| +| `_parse_number` | P2 | ✅ JoinIR OK | Phase 245B-IMPL: P2-Mid(num_str LoopState キャリア)を Structured→Normalized(dev, direct) で固定 | +| `_atoi` | P2 | ✅ JoinIR OK | Phase 246-EX で NumberAccumulation パターンとして統合(P2-Mid、Normalized: mini + real(dev, 符号対応) / canonical 準備中)| | `_parse_string` | P3 | ⚠️ Deferred | 複雑キャリア(Phase 195+ 拡張後)| | `_unescape_string` | P3 | ⚠️ Deferred | 複雑キャリア(Phase 195+ 拡張後)| | `_parse_array` | - | ⚠️ Deferred | 複数 MethodCall(Phase 195+)| | `_parse_object` | - | ⚠️ Deferred | 複数 MethodCall(Phase 195+)| -**Coverage**: 7/13 ループ JoinIR 対応済み(54%) -**Verification**: 4/7 ループ E2E PASS、3/7 structural/routing 確認済み +**Coverage**: 9/13 ループ JoinIR 対応済み(約 69%) +**Verification**: 代表ループ(P1/P2 Core + Trim/P3)については E2E テストで挙動確認済み。詳細なケースごとの状況は各 Phase ドキュメント(Phase 197/245/246 など)を参照。 8. **JsonParser 残り複雑ループへの適用(Phase 198+, 200+)** - Phase 200+: ConditionEnv 拡張 (function-scoped variables) → _parse_number, _atoi @@ -1247,3 +1247,61 @@ Normalized JoinIR を 1 段挟むと、開発の手触りがどう変わるか - Normalized ブリッジを direct 実装と Structured 再構成の二段に分離し、shape_guard で direct 対象(P1/P2 ミニ + JsonParser skip_ws/atoi ミニ)だけを Normalized→MIR 直接生成に切り替えた。 - direct 経路は `normalized_bridge::direct` に閉じ込め、非対応形状は `[joinir/normalized-bridge/fallback]` ログ付きで Structured 再構成経路に落とす構造に整理。dev テストでは direct 経路の VM 出力が従来経路と一致することを固定。 + +### 3.15 Phase 37-NORM-JP-REAL – JsonParser `_skip_whitespace` 本体を dev Normalized で比較 + +- JsonParser 本体の `_skip_whitespace` ループを Program(JSON) フィクスチャ化し、`shape_guard` で real 版を検知して Structured→Normalized→MIR(direct) の dev 経路に通すように拡張。`extract_value` は `&&`/`||` を BinOp として受け付けるようにした。 +- Break パターンのパラメータ推定を柔軟化(loop_var/acc/n が無いケースでも loop_var を優先し、acc が無ければ同一キャリアとして扱う)し、skip_ws real の構造で panic しないようにした。 +- tests/normalized_joinir_min.rs に `_skip_whitespace` real フィクスチャの VM 比較テストを追加し、env ON 時は Structured→Normalized→MIR(direct) と Structured 直経路の stdout が一致することを固定(env OFF は既存経路のまま)。 +- normalized_dev 用フィクスチャは `docs/private/roadmap2/phases/normalized_dev/fixtures/` に配置し、Program(JSON) から `AstToJoinIrLowerer` で読み込む運用に統一した。 + +### 3.16 Phase 38-NORM-OBS – Normalized dev ログ/Fail‑Fast の整備 + +- Normalized/JoinIR dev 経路のログカテゴリを `[joinir/normalized-bridge/*]` / `[joinir/normalized-dev/shape]` に統一し、`JOINIR_TEST_DEBUG` フラグ下のみ詳細を出すよう静音化。Verifier/Fail‑Fast メッセージも shape/役割付きに整理してデバッグ観測性を強化。 + +### 3.17 Phase 40-NORM-CANON-TESTS – テスト側で Normalized を“当たり前”に通す + +- `normalized_dev_enabled()` と env ガードを整理し、P1/P2 ミニ + JsonParser skip_ws/atoi ミニ/real の代表テストは「Normalized dev 経路が必ず通る」前提にする(壊れたら normalized_* スイートが赤になる)。 +- 既存の Structured 直経路は比較用に維持しつつ、tests/normalized_joinir_min.rs 経路では Structured→Normalized→MIR(direct) が第一観測点になるように整備(本番 CLI は Structured→MIR のまま)。 + +### 3.18 Phase 41-NORM-CANON-P2-CORE – Pattern2 コアケースの canonical Normalized 化 + +- Pattern2 のコアセット(P2 ミニ + JsonParser skip_ws/atoi ミニ/real)について、JoinIR→MIR Bridge の既定を Normalized→MIR に寄せ、Structured→MIR は比較テスト用/フォールバック用の位置づけにする(Fail‑Fast ポリシーは維持)。 +- `shape_guard` で「Normalized 対応と宣言した P2 コアループ」は常に Normalized 経路を通すようにし、Normalized 側の invariant 破損は dev では panic、本番では明示エラーで早期検出する設計に寄せる。 + +### 3.19 Phase 42-NORM-P2-INVENTORY – P2 コア/ミドル/ヘビーの棚卸し + +- JsonParser / selfhost で **現役の P2 ループ** を洗い出し、次の 3 クラスに整理した: + - **P2-Core**(すでに Normalized canonical なもの) + - test fixture 系: `loop_min_while` P2 ミニ, Phase 34 break fixture (`i/acc/n`) + - JsonParser 系: `_skip_whitespace` mini/real, `_atoi` mini + - これらは Phase 36–41 で **Structured→Normalized→MIR(direct)** が canonical になっており、`bridge_joinir_to_mir` でも優先的に Normalized 経路が選ばれる。 + - **P2-Mid**(次に Normalized を当てる候補) + - JsonParser: `_parse_number`, `_atoi` 本体, `_atof_loop` + - いずれも Pattern2 Break で JoinIR(Structured) には載っており(Phase 245/246 系)、Normalized への写像は今後の拡張対象として扱う。 + - **P2-Heavy**(複数 MethodCall / 複雑キャリアを持つもの) + - JsonParser: `_parse_string`, `_parse_array`, `_parse_object`, `_unescape_string` + - P2/P3/P4 が混在し、複雑なキャリアや MethodCall 多数のため、Phase 43 以降の後続フェーズで設計する。 +- P2-Core については Phase 41 で canonical Normalized 化が完了しており、Structured→MIR は比較テスト用 / フォールバック用の経路として扱う。 +- P2-Mid のうち、Phase 43 ではまず `_parse_number` を第 1 候補、`_atoi` 本体を第 2 候補として扱い、Normalized→MIR(direct) に必要な追加インフラ(EnvLayout 拡張 / JpInst パターン拡張)を段階的に入れていく前提を整理した。 + +### 3.20 Phase 43-NORM-CANON-P2-MID – JsonParser 本命 P2(_parse_number/_atoi)への適用 + +- JsonParser `_parse_number` / `_atoi` 本体の Pattern2 ループを、既存インフラ(DigitPos dual 値, LoopLocalZero, StepScheduleBox, ExprLowerer/MethodCall, Normalized ブリッジ)上で Structured→Normalized→MIR(direct) に載せる。 +- dev で Structured 直経路との VM 実行結果一致を固定した上で、段階的に「この関数だけ Normalized canonical」とみなすプロファイル/フラグを導入し、最終的に JsonParser P2 の canonical route を Normalized 側に寄せるための足場にする。 +- Phase 43-A(dev 専用): `_atoi` 本体を Program(JSON) フィクスチャ `jsonparser_atoi_real` で Structured→Normalized→MIR(direct) に通し、Structured 直経路との VM 出力一致を比較テストで固定(符号あり/なしの簡易パスまで対応。canonical 化は後続フェーズで検討)。 +- Phase 43-C(dev 専用): `_parse_number` 本体を Program(JSON) フィクスチャ `jsonparser_parse_number_real` で Structured→Normalized→MIR(direct) に通し、`num_str = num_str + ch` の LoopState キャリアを含めた状態で Structured 直経路との VM 出力一致を比較テストで固定。 + +### 3.21 Phase 44-SHAPE-CAP – shape_guard の能力ベース化(計画) + +- 現状の shape_guard は `JsonparserSkipWsMini/Real`, `JsonparserAtoiMini/Real` など「関数名ベースの個別 shape」が増えつつあるため、将来的には: + - 「P2 / LoopParam1 / Carrier≤N / MethodCall パターン = このセット」のような **能力ベースの ShapeCapability テーブル** に寄せる。 + - JsonParser/selfhost の各ループは「どの capability を満たしているか」を参照するだけにし、関数名ベタ書き依存を減らす。 +- この Phase では docs 上で API/テーブル設計を固め、コード側では shape_guard の内部表現を Capability 中心に書き換える前段として扱う。 + +### 3.22 Phase 45-NORM-MODE – JoinIR モードの一本化(計画) + +- 現状は `normalized_dev_enabled()`, `NYASH_JOINIR_NORMALIZED_DEV_RUN`, `JOINIR_TEST_DEBUG` など複数の env/feature でモードを切り替えているため、将来的には: + - `JoinIrMode = { StructuredOnly, NormalizedDev, NormalizedCanonical }` のような enum を導入し、 + - env/feature はこのモードの初期値を決めるだけに寄せる(コード側の `if`/分岐を減らす)。 +- この Phase では JoinIR パイプラインの「モード遷移図」と `JoinIrMode` API を docs で設計し、後続フェーズで実装に反映する計画を置いておく。 diff --git a/docs/development/current/main/phase181-jsonparser-loop-roadmap.md b/docs/development/current/main/phase181-jsonparser-loop-roadmap.md index 0d6c8ccb..6fc94f48 100644 --- a/docs/development/current/main/phase181-jsonparser-loop-roadmap.md +++ b/docs/development/current/main/phase181-jsonparser-loop-roadmap.md @@ -1,5 +1,8 @@ # Phase 181: JsonParser 残りループ設計調査 +Status: Historical + Updated in Phase 42 +Note: Phase 181 時点の設計調査に基づくドキュメントだよ。最新の P2 分類と JoinIR/Normalized 状態は、このファイル内の「Phase 42 時点の P2 インベントリ」と `joinir-architecture-overview.md` を SSOT として見てね。 + ## 概要 JsonParser(`tools/hako_shared/json_parser.hako`)の全11ループを詳細に分析し、 @@ -201,6 +204,34 @@ loop(i < len) { **実行可能性**: ✅ Phase 182+ で実装可能(Pattern2 Break、_atoi の後継) +## Phase 42 時点の P2 インベントリ(JsonParser) + +Phase 42 では、上の 11 ループについて「P2 としてどこまで JoinIR / Normalized で扱えているか」を棚卸しして、P2-Core / P2-Mid / P2-Heavy の 3 クラスに整理したよ。 + +### 1. 分類ポリシー + +- **P2-Core**: 既に Normalized→MIR(direct) まで実装され、Phase 41 で canonical route(既定経路)として扱っているループ群。 +- **P2-Mid**: JoinIR(Structured) には載っているが、Normalized はこれから本格対応する「次候補」のループ群。 +- **P2-Heavy**: MethodCall 多数・複雑キャリアなどの理由で、Normalized 対応は Phase 43 以降に送っている重めのループ群。 + +### 2. JsonParser ループの現在ステータス(2025‑12 時点) + +| # | ループ | Pattern | P2 クラス | JoinIR 状態 | Normalized 状態 | 備考 | +|----|--------|---------|-----------|-------------|-----------------|------| +| 1 | _skip_whitespace | P2 / P5 Trim | P2-Core | ✅ JoinIR OK(Phase 173, 197, 245 系) | ✅ Normalized→MIR(direct) / canonical(Phase 37, 41) | mini / real の両方をフィクスチャ化して dev / canonical で比較済み | +| 2 | _trim (leading) | P2 / P5 Trim | P2-Heavy | ✅ JoinIR OK(TrimLoopHelper 経由) | 未対応(P5 専用経路のまま) | Trim/P5 専用 lowerer で処理。Normalized 対応は将来検討 | +| 3 | _trim (trailing) | P2 / P5 Trim | P2-Heavy | ✅ JoinIR OK | 未対応 | leading と同様に Trim/P5 ラインで運用 | +| 4 | _parse_number | P2 Break | P2-Mid | ✅ JoinIR OK(Phase 245-EX) | ✅ dev Normalized→MIR(direct)(Phase 43-C、フィクスチャ `jsonparser_parse_number_real`。num_str は現状仕様のまま据え置き) | header/break/p 更新は JoinIR 経路に載せ済み。数値正規化は Phase 43 以降で拡張予定 | +| 5 | _parse_string | P2/P4 | P2-Heavy | 部分的に JoinIR 対応(Pattern3/4 拡張後に対象) | 未対応 | return/continue・複数キャリアを含むため heavy クラス扱い | +| 6 | _atoi | P2 Break | P2-Mid | ✅ JoinIR OK(Phase 246-EX) | ✅ dev Normalized→MIR(direct)(mini + 本体符号あり/なし、Phase 43-A) | P2-Core には `_atoi` mini fixture が入っている。本体は Phase 43 以降で canonical 化予定 | +| 7 | _match_literal | P1 Simple | (P1) | ✅ JoinIR OK | Normalized 対応は P1 ラインで別途管理 | P1 simple なので P2 クラス分類の対象外。Phase 197 で JoinIR E2E 検証済み | +| 8 | _parse_array | P4 Continue | P2-Heavy | ⚠️ Deferred(複数 MethodCall) | 未対応 | continue + MethodCall 多数のため heavy クラス。ConditionEnv/MethodCall 拡張後に扱う | +| 9 | _parse_object | P4 Continue | P2-Heavy | ⚠️ Deferred | 未対応 | _parse_array と同種の heavy ループ | +| 10 | _unescape_string | P4 Continue | P2-Heavy | ⚠️ Deferred | 未対応 | 複数キャリア + flatten を含む。Pattern3/4 拡張後の対象 | +| 11 | _atof_loop | P2 Break | P2-Mid | JoinIR 対応候補(_atoi と同型) | 未対応 | `_atoi` 後継として P2-Mid 候補に分類。Phase 43 以降で `_atoi` 本体と一緒に扱う想定 | + +最新の canonical / dev Normalized 経路や Shape 判定ロジックの詳細は `joinir-architecture-overview.md`(Phase 35–41 セクション)を参照してね。 + ## Pattern × Box マトリクス(JsonParser全体) ``` diff --git a/docs/development/current/main/phase223-loopbodylocal-condition-inventory.md b/docs/development/current/main/phase223-loopbodylocal-condition-inventory.md index aaf3afb5..3f88487e 100644 --- a/docs/development/current/main/phase223-loopbodylocal-condition-inventory.md +++ b/docs/development/current/main/phase223-loopbodylocal-condition-inventory.md @@ -1,5 +1,8 @@ # Phase 223-1: LoopBodyLocal in Condition - Comprehensive Inventory +Status: Historical(Phase 26-H 以降の Normalized / DigitPos 導入で一部内容が古くなっています) +Note: LoopBodyLocal が原因で Fail-Fast していたループの在庫を Phase 223 時点で一覧化したメモだよ。DigitPos 系などの一部ループはその後の Phase 224/26-H/34 系で解消済みなので、最新の対応状況は `joinir-architecture-overview.md` と Phase 42 の P2 インベントリを合わせて参照してね。 + ## Purpose This document inventories all loops that are currently **blocked** by the LoopConditionScopeBox Fail-Fast mechanism because they have `LoopBodyLocal` variables appearing in loop conditions (header, break, or continue). diff --git a/docs/development/current/main/phase224-digitpos-condition-normalizer.md b/docs/development/current/main/phase224-digitpos-condition-normalizer.md index d5046764..312f62e1 100644 --- a/docs/development/current/main/phase224-digitpos-condition-normalizer.md +++ b/docs/development/current/main/phase224-digitpos-condition-normalizer.md @@ -1,5 +1,8 @@ # Phase 224-E: DigitPos Condition Normalizer +Status: Active(DigitPos 条件正規化ラインの設計メモ / 実装ガイド) +Scope: digit_pos → is_digit_pos Carrier / ConditionEnv / ExprLowerer の正規化経路の SSOT ドキュメントだよ。Phase 26‑H / 34 系の JsonParser `_parse_number` / `_atoi` でもこの設計を前提にしている。 + ## Problem Statement ### Background diff --git a/docs/development/current/main/phase245b-jsonparser-num-str-carrier-design.md b/docs/development/current/main/phase245b-jsonparser-num-str-carrier-design.md index aa65c187..bb270232 100644 --- a/docs/development/current/main/phase245b-jsonparser-num-str-carrier-design.md +++ b/docs/development/current/main/phase245b-jsonparser-num-str-carrier-design.md @@ -1,5 +1,6 @@ -Status: Draft -Scope: `_parse_number` で `num_str` を Pattern2/P5 のキャリアとして扱うかどうかを決める設計フェーズ(コード変更なし)。 +Status: Implemented (Phase 245B-IMPL) +Scope: `_parse_number` で `num_str` を Pattern2/P5 のキャリアとして扱うかどうかを決める設計フェーズ(実装は Structured→Normalized dev フィクスチャで完了)。 +Notes: jsonparser_parse_number_real フィクスチャで `num_str = num_str + ch` を LoopState キャリアとして実装し、dev Normalized 比較テストで固定済み。 # Phase 245B: JsonParser `_parse_number` の `num_str` キャリア設計 diff --git a/docs/development/current/main/phase245b-num_str-carrier-design.md b/docs/development/current/main/phase245b-num_str-carrier-design.md index ed8daaf0..8d6af433 100644 --- a/docs/development/current/main/phase245b-num_str-carrier-design.md +++ b/docs/development/current/main/phase245b-num_str-carrier-design.md @@ -1,8 +1,9 @@ # Phase 245B: num_str Carrier Design Document -**Status**: Design Phase +**Status**: Implemented (Phase 245B-IMPL) **Target**: `_parse_number` loop の `num_str` 文字列キャリア対応 **Scope**: Pattern 2 (loop with break) + 既存インフラ活用 +**Notes**: jsonparser_parse_number_real フィクスチャを Structured→Normalized→MIR(direct) で実装し、`num_str = num_str + ch` を LoopState キャリアとして dev テスト固定済み。 --- diff --git a/docs/development/current/main/phase43-norm-canon-p2-mid.md b/docs/development/current/main/phase43-norm-canon-p2-mid.md new file mode 100644 index 00000000..ada2e47a --- /dev/null +++ b/docs/development/current/main/phase43-norm-canon-p2-mid.md @@ -0,0 +1,148 @@ +Status: Planned +Scope: Phase 43‑NORM‑CANON‑P2‑MID — JsonParser 本命 P2(`_parse_number` / `_atoi` 本体 / `_atof_loop`)を Normalized 基準に寄せるための事前設計メモ。 +Update (Phase 43-A): `_atoi` 本体を Program(JSON) フィクスチャ `jsonparser_atoi_real` で dev Normalized→MIR(direct) 経路に載せ、Structured 直経路との VM 出力一致を確認済み(符号あり/なしの簡易パスまで対応。canonical 化は後続で検討)。 +Update (Phase 43-C): `_parse_number` 本体を Program(JSON) フィクスチャ `jsonparser_parse_number_real` で dev Normalized→MIR(direct) 経路に載せ、Structured 直経路との VM 出力一致を dev テストで固定(num_str は現状仕様のまま据え置き)。 + +# Phase 43‑NORM‑CANON‑P2‑MID 設計メモ(JsonParser P2‑Mid 向け Normalized 拡張) + +## 0. ゴールと前提 + +- ゴール + - JsonParser の本命 P2 ループ(P2‑Mid)を、既存の Normalized インフラの延長で扱えるようにするための設計方針を固める。 + - 特に `_parse_number` / `_atoi` 本体 / `_atof_loop` について、 + - どのキャリア・body‑local を EnvLayout に載せるか + - どの JpInst / JpOp パターンが追加で必要か + - StepScheduleBox / DigitPos / NumberAccumulation との責務分担 + を整理する。 +- 前提 + - P2‑Core(P2 ミニ + JP skip_ws mini/real + JP atoi mini)は Phase 41 までで Normalized→MIR(direct) が canonical 済み。 + - P2‑Mid は **JoinIR(Structured) までは載っているが Normalized は未対応** の状態(Phase 245/246 系)。 + - 本メモは「設計レベル」で止め、実装・テスト追加は後続フェーズ(43 実装回)で扱う。 + +--- + +## 1. 対象ループと P2‑Mid クラスの整理 + +- 対象とする P2‑Mid(JsonParser 側) + - `_parse_number`(数値文字列の収集 + digit_pos break) + - `_atoi` 本体(範囲チェック + digit_pos + NumberAccumulation) + - `_atof_loop`(構造的には `_atoi` と同型の浮動小数点版) +- すべて Pattern2 Break で、既に JoinIR(Structured) には載っている: + - `_parse_number` → Phase 245‑EX で header / break / `p` 更新を Pattern2 に統合済み(`num_str` は当面対象外)。 + - `_atoi` → Phase 246‑EX で DigitPos dual 値 + NumberAccumulation パターンとして JoinIR 経路に統合済み。 + - `_atof_loop` → 設計上 `_atoi` と同型とみなし、P2‑Mid クラスに含める。 +- P2‑Core との差分 + - P2 ミニ / skip_ws / atoi ミニに比べて: + - Carrier の本数(`p` + `result` + 場合によっては `num_str`)が増える。 + - body‑local / Derived Carrier(`digit_pos`, `is_digit_pos`, `digit_value` 等)の依存関係が複雑。 + - 一部で文字列連結(`num_str = num_str + ch`)や Range チェック(`"0" <= ch <= "9"`)が入る。 + +--- + +## 2. Normalized IR に必要な拡張(EnvLayout / JpInst / JpOp) + +### 2.1 EnvLayout / フィールド設計 + +- P2‑Mid で EnvLayout に載せる候補 + - LoopState キャリア + - `_parse_number`: `p`(必須)、`num_str`(Phase 43 では **オプション**、まず `p` 単独で正規化する案も許容)。 + - `_atoi` 本体 / `_atof_loop`: `i`, `result`(NumberAccumulation キャリア)。 + - Condition 専用 / FromHost + - `len` / `s` / `digits` などの不変値は ParamRole::Condition として EnvLayout 側に持たない(現状どおり)。 + - Derived LoopState(DigitPos 系) + - P2‑Core で既に導入済みの `digit_value` / `is_digit_pos` と同じ方針: + - EnvLayout に「FromHost ではない LoopState キャリア」として載せる。 + - host_slot を持たず、ExitBinding には出さない(ループ内部完結キャリア)。 + +### 2.2 JpInst / JpOp 側の必要パターン + +- 既存の Normalized が既に扱っているもの + - `Let { dst, op: Const / BinOp / Unary / Compare / BoxCall, args }` + - `If { cond, then_target, else_target, env }` + - `TailCallFn` / `TailCallKont`(loop_step / k_exit のみ) +- P2‑Mid で追加検証・明文化が必要なパターン + - `_parse_number` + - `substring` / `indexOf` の BoxCall パターン(P2‑Core でも使用済みだが、`num_str` 周辺の利用を含めてドキュメントで SSOT 化する)。 + - 文字列連結 `num_str = num_str + ch` を + - 当面は「扱わない」(`num_str` を Carrier から外す)案 + - もしくは `BinOp(Add)` として Normalized→MIR 直ブリッジに追加する案 + のどちらにするかを Phase 43 実装メモで最終決定する。 + - `_atoi` 本体 / `_atof_loop` + - Range チェック `ch < "0" || ch > "9"`: + - ExprLowerer / ConditionEnv 側で既に対応済みであれば、Normalized には Compare + BinOp(or) の形で入る。 + - Normalized では追加の JpOp は不要(Compare / BinOp を利用)。 + - NumberAccumulation パターン: + - Structured 側では `UpdateRhs::NumberAccumulation` として扱っているので、 + - Normalized → MIR 直ブリッジ側で「Mul + Add + digit_value」の形を既に対応済み。 + - Phase 43 では `_atoi` 本体 / `_atof_loop` でも同じシーケンスになることを前提とし、JpInst 種別追加は行わない。 + +--- + +## 3. StepScheduleBox / DigitPos / NumberAccumulation の役割分担 + +### 3.1 StepScheduleBox(評価順)の適用範囲 + +- Phase 39 で導入した StepScheduleBox(Pattern2 用)は、P2‑Mid に対しても「どの StepKind をどの順番で評価するか」を決める SSOT として使う。 +- `_parse_number` / `_atoi` 本体 / `_atof_loop` では、以下のようなフラグを想定: + - `has_digitpos_body_local`(DigitPos 二重値を使うか) + - `has_number_accumulation`(結果キャリアに Mul+Add があるか) + - `has_bodylocal_break`(break 条件が body‑local に依存するか) +- StepScheduleBox は、これらのフラグだけを見て + - 標準 P2: `[HeaderCond, BreakCheck, BodyInit, Updates, Tail]` + - DigitPos / atoi 系: `[HeaderCond, BodyInit, BreakCheck, Updates, Tail]` + など、評価順のバリエーションを返す「薄い箱」のまま保つ。 +- Pattern2 lowerer / Normalized 変換側は、この StepSchedule に従って + - header 条件 + - body‑local init(DigitPos / Range check 等) + - break 条件 + - carrier 更新(NumberAccumulation / i++ 等) + を「並べるだけ」にし、条件式の詳細や body‑local の構造には踏み込まない。 + +### 3.2 DigitPos / NumberAccumulation との接続 + +- DigitPos 系 + - `digit_pos` → `is_digit_pos` / `digit_value` の二重値設計は、P2‑Core と同じく「LoopState キャリア(FromHost なし)」として EnvLayout に載せる。 + - ExitBinding には出さず、Normalized→MIR 直ブリッジでも Loop 内部だけで完結させる。 + - Break 条件 `digit_pos < 0` は Phase 224 の DigitPosConditionNormalizer(AST→`!is_digit_pos`)を前提にし、Normalized 側は `!is_digit_pos` という bool 条件だけを受け取る。 +- NumberAccumulation 系 + - Structured 側の LoopUpdateAnalyzer / CarrierUpdateEmitter が `result = result * 10 + digit_value` パターンを `UpdateRhs::NumberAccumulation` として検出・JoinIR 生成済み。 + - Normalized→MIR 直ブリッジは、P2‑Core と同じ Mul + Add シーケンスで MIR を吐く設計を維持し、P2‑Mid でも追加ロジックを増やさずに流用する。 + +--- + +## 4. Bridge / ShapeGuard / canonical 切り替え方針 + +- ShapeGuard 拡張 + - 既存の P2‑Core 判定(P2 ミニ / skip_ws mini/real / atoi mini)に加えて、 + - `_parse_number` 本体 + - `_atoi` 本体 + - `_atof_loop` + を P2‑Mid として検出できる Shape 種別を追加する(例: `ShapeKind::JsonparserParseNumber`, `ShapeKind::JsonparserAtoiCore`)。 + - Phase 43 実装フェーズでは、まず P2‑Mid ループを **dev only** の Normalized 対象にする(canonical 切り替えは Phase 43 後半〜Phase 44 相当で検討)。 +- Bridge 側の経路 + - `bridge_joinir_to_mir` の入口で: + - P2‑Core: 既に canonical Normalized→MIR(direct)(Phase 41 の状態を維持)。 + - P2‑Mid: `normalized_dev_enabled()` が true のときに限り Structured→Normalized→MIR(direct) を試し、テストで Structured 直経路と比較。 + - Fail‑Fast 方針 + - P2‑Mid では「Normalized が未対応の領域」がまだ多いため、 + - dev / debug ビルドでは invariant 破壊・未対応命令で panic(Normalized 実装の穴を早期検出)。 + - release / canonical OFF 時は Structured→MIR 直経路に落とす(サイレントフォールバックではなく「Normalized をそもそも使わない」構成にする)。 + +--- + +## 5. テストと完了条件(Phase 43 実装フェーズ向けメモ) + +- テスト戦略(概要) + - `_parse_number`: + - 代表ケース("42", "7z" など)で + - Structured→MIR→VM + - Structured→Normalized→MIR(direct)→VM + の stdout / RC を比較する dev テストを追加。 + - `num_str` をまだ Normalized キャリアに載せない場合でも、「p / break 条件 / DigitPos 周り」が齟齬なく動くことを確認する。 + - `_atoi` 本体 / `_atof_loop`: + - 既存の `_atoi` mini dev fixture と同じ観点で、NumberAccumulation / DigitPos / Range check が Normalized 経路で再現できるかをチェック。 + - Mini / 本体 / `_atof_loop` が同じ normalized helper / bridge ロジックを共有できることを確認する。 +- 完了条件(Phase 43 実装のためのチェックリスト) + - EnvLayout / JpInst / JpOp / StepScheduleBox / DigitPos / NumberAccumulation の役割分担が本メモの通りに整理されている。 + - P2‑Mid(`_parse_number` / `_atoi` 本体 / `_atof_loop`)に対して、どこまでを Phase 43 で扱い、どこから先を後続フェーズに回すかの線引きが明文化されている。 + - `joinir-architecture-overview.md` の Phase 43 セクション(3.20)と、この設計メモの内容が矛盾していない。 diff --git a/src/config/env/joinir_dev.rs b/src/config/env/joinir_dev.rs index 1ace6892..c4ec6074 100644 --- a/src/config/env/joinir_dev.rs +++ b/src/config/env/joinir_dev.rs @@ -117,6 +117,11 @@ pub fn normalized_dev_enabled() -> bool { } } +/// JOINIR_TEST_DEBUG=1 (or NYASH_JOINIR_TEST_DEBUG=1) - Verbose logging for normalized dev tests +pub fn joinir_test_debug_enabled() -> bool { + env_bool("JOINIR_TEST_DEBUG") || env_bool("NYASH_JOINIR_TEST_DEBUG") +} + /// Phase 82: NYASH_PHI_FALLBACK_DISABLED=1 - Disable if_phi fallback (dev mode) /// /// lifecycle.rs の infer_type_from_phi* callsite を封じて、 diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs index e497d95c..f72a1ab3 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs @@ -8,9 +8,11 @@ use crate::mir::join_ir::lowering::condition_env::{ConditionBinding, ConditionEn use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; +use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr; use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv; use crate::mir::loop_pattern_detection::error_messages; use crate::mir::ValueId; +use std::collections::BTreeMap; fn log_pattern2(verbose: bool, tag: &str, message: impl AsRef) { if verbose { @@ -701,25 +703,19 @@ impl MirBuilder { verbose, "updates", format!( - "Phase 176-3: Analyzed {} carrier updates", - carrier_updates.len() - ), - ); + "Phase 176-3: Analyzed {} carrier updates", + carrier_updates.len() + ), + ); let original_carrier_count = inputs.carrier_info.carriers.len(); - inputs.carrier_info.carriers.retain(|carrier| { - use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole}; - carrier_updates.contains_key(&carrier.name) - || carrier.role == CarrierRole::ConditionOnly - || carrier.init == CarrierInit::FromHost - || carrier.init == CarrierInit::LoopLocalZero - }); + filter_carriers_for_updates(&mut inputs.carrier_info, &carrier_updates); log_pattern2( verbose, "updates", format!( - "Phase 176-4: Filtered carriers: {} → {} (kept only carriers with updates)", + "Phase 176-4: Filtered carriers: {} → {} (kept only carriers with updates/condition-only/loop-local-zero)", original_carrier_count, inputs.carrier_info.carriers.len() ), @@ -858,6 +854,19 @@ impl MirBuilder { } } +/// 更新を持たない FromHost キャリアを落とすヘルパー。 +fn filter_carriers_for_updates( + carrier_info: &mut CarrierInfo, + carrier_updates: &BTreeMap, +) { + use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole}; + carrier_info.carriers.retain(|carrier| { + carrier_updates.contains_key(&carrier.name) + || carrier.role == CarrierRole::ConditionOnly + || carrier.init == CarrierInit::LoopLocalZero + }); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/mir/join_ir/frontend/ast_lowerer/expr.rs b/src/mir/join_ir/frontend/ast_lowerer/expr.rs index 724f0d62..52fbf57e 100644 --- a/src/mir/join_ir/frontend/ast_lowerer/expr.rs +++ b/src/mir/join_ir/frontend/ast_lowerer/expr.rs @@ -138,6 +138,8 @@ impl AstToJoinIrLowerer { "-" => BinOpKind::Sub, "*" => BinOpKind::Mul, "/" => BinOpKind::Div, + "&&" => BinOpKind::And, + "||" => BinOpKind::Or, _ => panic!("Unsupported binary op: {}", op_str), }; diff --git a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs index 70af341a..a36793ea 100644 --- a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs +++ b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs @@ -24,10 +24,10 @@ use super::common::{ build_join_module, create_k_exit_function, create_loop_context, parse_program_json, process_local_inits, }; +use super::param_guess::{build_param_order, compute_param_guess}; use super::{AstToJoinIrLowerer, JoinModule, LoweringError}; use crate::mir::join_ir::{JoinFunction, JoinInst}; use crate::mir::ValueId; -use std::collections::BTreeSet; /// Break パターンを JoinModule に変換 /// @@ -70,7 +70,10 @@ pub fn lower( let break_cond_expr = &break_if_stmt["cond"]; - let (param_order, loop_var_name, acc_name) = compute_param_order(&entry_ctx); + let param_guess = compute_param_guess(&entry_ctx); + let param_order = build_param_order(¶m_guess, &entry_ctx); + let loop_var_name = param_guess.loop_var.0.clone(); + let acc_name = param_guess.acc.0.clone(); let loop_cond_expr = &loop_node["cond"]; // 5. entry 関数を生成 @@ -245,48 +248,3 @@ fn create_loop_step_function_break( exit_cont: None, }) } - -fn compute_param_order( - entry_ctx: &super::super::context::ExtractCtx, -) -> (Vec<(String, ValueId)>, String, String) { - let loop_var_name = "i".to_string(); - let loop_var = entry_ctx - .get_var(&loop_var_name) - .expect("i must be initialized"); - - let (acc_name, acc_var) = if let Some(v) = entry_ctx.get_var("acc") { - ("acc".to_string(), v) - } else if let Some(v) = entry_ctx.get_var("result") { - ("result".to_string(), v) - } else { - panic!("acc or result must be initialized"); - }; - - let (len_name, len_var) = if let Some(v) = entry_ctx.get_var("n") { - ("n".to_string(), v) - } else if let Some(v) = entry_ctx.get_var("len") { - ("len".to_string(), v) - } else { - panic!("n or len must be provided as parameter"); - }; - - let mut param_order = vec![ - (loop_var_name.clone(), loop_var), - (acc_name.clone(), acc_var), - (len_name.clone(), len_var), - ]; - - let mut seen: BTreeSet = - [loop_var_name.clone(), acc_name.clone(), len_name.clone()] - .into_iter() - .collect(); - - for (name, var_id) in &entry_ctx.var_map { - if !seen.contains(name) { - param_order.push((name.clone(), *var_id)); - seen.insert(name.clone()); - } - } - - (param_order, loop_var_name, acc_name) -} diff --git a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/common.rs b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/common.rs index cd168821..760bfc6e 100644 --- a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/common.rs +++ b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/common.rs @@ -13,6 +13,7 @@ //! - `create_k_exit_function()`: k_exit 関数生成 use super::{AstToJoinIrLowerer, JoinModule}; +use super::super::stmt_handlers::StatementEffect; use crate::mir::join_ir::JoinIrPhase; use crate::mir::join_ir::{JoinFuncId, JoinFunction, JoinInst}; use crate::mir::ValueId; @@ -119,19 +120,15 @@ pub fn process_local_inits( let stmt_type = stmt["type"].as_str().expect("Statement must have type"); match stmt_type { - "Local" => { - let var_name = stmt["name"] - .as_str() - .expect("Local must have 'name'") - .to_string(); - let expr = &stmt["expr"]; - - // extract_value で式を評価 - let (var_id, insts) = lowerer.extract_value(expr, ctx); + "Local" | "Assignment" | "If" => { + let (insts, effect) = lowerer.lower_statement(stmt, ctx); init_insts.extend(insts); - // 同名再宣言 = var_map を更新(再代入の意味論) - ctx.register_param(var_name, var_id); + if let StatementEffect::VarUpdate { name, value_id } = effect { + ctx.register_param(name, value_id); + } else if matches!(effect, StatementEffect::SideEffect) { + panic!("Unexpected side-effecting statement before Loop: {}", stmt_type); + } } _ => panic!("Unexpected statement type before Loop: {}", stmt_type), } diff --git a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/mod.rs b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/mod.rs index 73ecddcd..0ba80901 100644 --- a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/mod.rs +++ b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/mod.rs @@ -19,6 +19,7 @@ pub mod break_pattern; pub mod common; pub mod continue_pattern; pub mod filter; +pub mod param_guess; pub mod print_tokens; pub mod simple; diff --git a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/param_guess.rs b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/param_guess.rs new file mode 100644 index 00000000..21ff999d --- /dev/null +++ b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/param_guess.rs @@ -0,0 +1,71 @@ +use std::collections::BTreeSet; + +use crate::mir::ValueId; + +/// 推定したループ引数の並び。 +#[derive(Debug, Clone)] +pub(crate) struct ParamGuess { + pub loop_var: (String, ValueId), + pub acc: (String, ValueId), + pub len: Option<(String, ValueId)>, +} + +/// Break パターン向けのパラメータ推定テーブル。 +/// - ループ変数優先順: p → i → 先頭の var +/// - アキュムレータ優先順: num_str → acc → result → ループ変数 +pub(crate) fn compute_param_guess(ctx: &super::super::context::ExtractCtx) -> ParamGuess { + let loop_var = ctx + .get_var("p") + .map(|v| ("p".to_string(), v)) + .or_else(|| ctx.get_var("i").map(|v| ("i".to_string(), v))) + .or_else(|| { + ctx.var_map + .iter() + .find(|(name, _)| name.as_str() != "me") + .map(|(name, v)| (name.clone(), *v)) + }) + .unwrap_or_else(|| panic!("[joinir/frontend] break_pattern: loop variable missing")); + + let acc = if let Some(v) = ctx.get_var("num_str") { + ("num_str".to_string(), v) + } else if let Some(v) = ctx.get_var("acc") { + ("acc".to_string(), v) + } else if let Some(v) = ctx.get_var("result") { + ("result".to_string(), v) + } else { + loop_var.clone() + }; + + let len = ctx + .get_var("n") + .map(|v| ("n".to_string(), v)) + .or_else(|| ctx.get_var("len").map(|v| ("len".to_string(), v))); + + ParamGuess { loop_var, acc, len } +} + +/// 推定結果と ExtractCtx からパラメータの並びを構成する。 +pub(crate) fn build_param_order( + guess: &ParamGuess, + entry_ctx: &super::super::context::ExtractCtx, +) -> Vec<(String, ValueId)> { + let mut order = Vec::new(); + order.push(guess.loop_var.clone()); + if guess.acc.0 != guess.loop_var.0 { + order.push(guess.acc.clone()); + } + if let Some(len) = &guess.len { + if len.0 != guess.loop_var.0 && len.0 != guess.acc.0 { + order.push(len.clone()); + } + } + + let mut seen: BTreeSet = order.iter().map(|(n, _)| n.clone()).collect(); + for (name, var_id) in &entry_ctx.var_map { + if !seen.contains(name) { + order.push((name.clone(), *var_id)); + seen.insert(name.clone()); + } + } + order +} diff --git a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns_old.rs b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns_old.rs index 057710b4..092c6835 100644 --- a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns_old.rs +++ b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns_old.rs @@ -54,13 +54,21 @@ impl AstToJoinIrLowerer { pub(crate) fn has_break_in_loop_body(loop_body: &[serde_json::Value]) -> bool { loop_body.iter().any(|stmt| { if stmt["type"].as_str() == Some("If") { - if let Some(then_body) = stmt["then"].as_array() { - then_body - .iter() - .any(|s| s["type"].as_str() == Some("Break")) - } else { - false - } + let then_has = stmt["then"] + .as_array() + .map(|body| { + body.iter() + .any(|s| s["type"].as_str() == Some("Break")) + }) + .unwrap_or(false); + let else_has = stmt["else"] + .as_array() + .map(|body| { + body.iter() + .any(|s| s["type"].as_str() == Some("Break")) + }) + .unwrap_or(false); + then_has || else_has } else { false } @@ -71,13 +79,21 @@ impl AstToJoinIrLowerer { pub(crate) fn has_continue_in_loop_body(loop_body: &[serde_json::Value]) -> bool { loop_body.iter().any(|stmt| { if stmt["type"].as_str() == Some("If") { - if let Some(then_body) = stmt["then"].as_array() { - then_body - .iter() - .any(|s| s["type"].as_str() == Some("Continue")) - } else { - false - } + let then_has = stmt["then"] + .as_array() + .map(|body| { + body.iter() + .any(|s| s["type"].as_str() == Some("Continue")) + }) + .unwrap_or(false); + let else_has = stmt["else"] + .as_array() + .map(|body| { + body.iter() + .any(|s| s["type"].as_str() == Some("Continue")) + }) + .unwrap_or(false); + then_has || else_has } else { false } diff --git a/src/mir/join_ir/frontend/ast_lowerer/mod.rs b/src/mir/join_ir/frontend/ast_lowerer/mod.rs index 8a008060..b415aebd 100644 --- a/src/mir/join_ir/frontend/ast_lowerer/mod.rs +++ b/src/mir/join_ir/frontend/ast_lowerer/mod.rs @@ -52,24 +52,25 @@ enum FunctionRoute { } fn resolve_function_route(func_name: &str) -> Result { - const IF_RETURN_NAMES: &[&str] = &["test", "local", "_read_value_from_pair"]; - const LOOP_NAMES: &[&str] = &[ - "simple", - "filter", - "print_tokens", - "map", - "reduce", - "fold", - "jsonparser_skip_ws_mini", - "jsonparser_atoi_mini", + const TABLE: &[(&str, FunctionRoute)] = &[ + ("test", FunctionRoute::IfReturn), + ("local", FunctionRoute::IfReturn), + ("_read_value_from_pair", FunctionRoute::IfReturn), + ("simple", FunctionRoute::LoopFrontend), + ("filter", FunctionRoute::LoopFrontend), + ("print_tokens", FunctionRoute::LoopFrontend), + ("map", FunctionRoute::LoopFrontend), + ("reduce", FunctionRoute::LoopFrontend), + ("fold", FunctionRoute::LoopFrontend), + ("jsonparser_skip_ws_mini", FunctionRoute::LoopFrontend), + ("jsonparser_skip_ws_real", FunctionRoute::LoopFrontend), + ("jsonparser_atoi_mini", FunctionRoute::LoopFrontend), + ("jsonparser_atoi_real", FunctionRoute::LoopFrontend), + ("jsonparser_parse_number_real", FunctionRoute::LoopFrontend), ]; - if IF_RETURN_NAMES.contains(&func_name) { - return Ok(FunctionRoute::IfReturn); - } - - if LOOP_NAMES.contains(&func_name) { - return Ok(FunctionRoute::LoopFrontend); + if let Some((_, route)) = TABLE.iter().find(|(name, _)| *name == func_name) { + return Ok(*route); } if func_name == "parse_loop" { diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal.rs b/src/mir/join_ir/lowering/loop_with_break_minimal.rs index ee7ca3fd..3b47bb4f 100644 --- a/src/mir/join_ir/lowering/loop_with_break_minimal.rs +++ b/src/mir/join_ir/lowering/loop_with_break_minimal.rs @@ -58,7 +58,6 @@ use crate::ast::ASTNode; mod boundary_builder; mod header_break_lowering; -mod step_schedule; #[cfg(test)] mod tests; @@ -71,6 +70,9 @@ use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr; +use crate::mir::join_ir::lowering::pattern2_step_schedule::{ + build_pattern2_schedule, Pattern2ScheduleContext, Pattern2StepKind, +}; use crate::mir::join_ir::lowering::update_env::UpdateEnv; use crate::mir::join_ir::{ BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst, @@ -84,7 +86,6 @@ use crate::mir::ValueId; use boundary_builder::build_fragment_meta; use header_break_lowering::{lower_break_condition, lower_header_condition}; use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism -use step_schedule::{Pattern2Step, Pattern2StepSchedule}; /// Lower Pattern 2 (Loop with Conditional Break) to JoinIR /// @@ -260,6 +261,20 @@ pub(crate) fn lower_loop_with_break_minimal( "[joinir/pattern2] Phase 201: loop_step params - i_param={:?}, carrier_params={:?}", i_param, carrier_param_ids ); + if crate::config::env::joinir_dev_enabled() + || crate::config::env::joinir_test_debug_enabled() + { + eprintln!( + "[joinir/pattern2/debug] loop_var='{}' env.get(loop_var)={:?}, carriers={:?}", + loop_var_name, + env.get(loop_var_name), + carrier_info + .carriers + .iter() + .map(|c| (c.name.clone(), c.join_id)) + .collect::>() + ); + } // Phase 169 / Phase 171-fix / Phase 240-EX / Phase 244: Lower condition let (cond_value, mut cond_instructions) = lower_header_condition( @@ -327,23 +342,9 @@ pub(crate) fn lower_loop_with_break_minimal( let mut loop_step_func = JoinFunction::new(loop_step_id, "loop_step".to_string(), loop_params); // Decide evaluation order (header/body-init/break/updates/tail) up-front. - let schedule = - Pattern2StepSchedule::for_pattern2(body_local_env.as_ref().map(|env| &**env), carrier_info); - let schedule_desc: Vec<&str> = schedule - .iter() - .map(|step| match step { - Pattern2Step::HeaderAndNaturalExit => "header+exit", - Pattern2Step::BodyLocalInit => "body-init", - Pattern2Step::BreakCondition => "break", - Pattern2Step::CarrierUpdates => "updates", - Pattern2Step::TailCall => "tail", - }) - .collect(); - eprintln!( - "[pattern2/schedule] Selected Pattern2 step schedule: {:?} ({})", - schedule_desc, - schedule.reason() - ); + let schedule_ctx = + Pattern2ScheduleContext::from_env(body_local_env.as_ref().map(|env| &**env), carrier_info); + let schedule = build_pattern2_schedule(&schedule_ctx); // Collect fragments per step; append them according to the schedule below. let mut header_block: Vec = Vec::new(); @@ -661,11 +662,11 @@ pub(crate) fn lower_loop_with_break_minimal( // Apply scheduled order to assemble the loop_step body. for step in schedule.iter() { match step { - Pattern2Step::HeaderAndNaturalExit => loop_step_func.body.append(&mut header_block), - Pattern2Step::BodyLocalInit => loop_step_func.body.append(&mut body_init_block), - Pattern2Step::BreakCondition => loop_step_func.body.append(&mut break_block), - Pattern2Step::CarrierUpdates => loop_step_func.body.append(&mut carrier_update_block), - Pattern2Step::TailCall => loop_step_func.body.append(&mut tail_block), + Pattern2StepKind::HeaderCond => loop_step_func.body.append(&mut header_block), + Pattern2StepKind::BodyInit => loop_step_func.body.append(&mut body_init_block), + Pattern2StepKind::BreakCheck => loop_step_func.body.append(&mut break_block), + Pattern2StepKind::Updates => loop_step_func.body.append(&mut carrier_update_block), + Pattern2StepKind::Tail => loop_step_func.body.append(&mut tail_block), } } diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal/step_schedule.rs b/src/mir/join_ir/lowering/loop_with_break_minimal/step_schedule.rs deleted file mode 100644 index 84245b12..00000000 --- a/src/mir/join_ir/lowering/loop_with_break_minimal/step_schedule.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Pattern 2 step scheduler. -//! -//! Decides the evaluation order for Pattern 2 lowering without hardcoding it -//! in the lowerer. This keeps the lowerer focused on emitting fragments while -//! the scheduler decides how to interleave them (e.g., body-local init before -//! break checks when the break depends on body-local values). - -use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierInit}; -use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; - -/// Steps that can be reordered by the scheduler. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum Pattern2Step { - HeaderAndNaturalExit, - BodyLocalInit, - BreakCondition, - CarrierUpdates, - TailCall, -} - -/// Data-driven schedule for Pattern 2 lowering. -#[derive(Debug, Clone)] -pub(crate) struct Pattern2StepSchedule { - steps: Vec, - reason: &'static str, -} - -impl Pattern2StepSchedule { - /// Choose a schedule based on whether the break condition relies on fresh - /// body-local values (DigitPos-style). - pub(crate) fn for_pattern2( - body_local_env: Option<&LoopBodyLocalEnv>, - carrier_info: &CarrierInfo, - ) -> Self { - let has_body_locals = body_local_env.map(|env| !env.is_empty()).unwrap_or(false); - let has_loop_local_carrier = carrier_info - .carriers - .iter() - .any(|c| matches!(c.init, CarrierInit::LoopLocalZero)); - - // If there are body-local dependencies, evaluate them before the break - // condition so the break uses fresh values. - if has_body_locals || has_loop_local_carrier { - Self { - steps: vec![ - Pattern2Step::HeaderAndNaturalExit, - Pattern2Step::BodyLocalInit, - Pattern2Step::BreakCondition, - Pattern2Step::CarrierUpdates, - Pattern2Step::TailCall, - ], - reason: "body-local break dependency", - } - } else { - // Default order: header → break → updates → tail. - Self { - steps: vec![ - Pattern2Step::HeaderAndNaturalExit, - Pattern2Step::BreakCondition, - Pattern2Step::BodyLocalInit, - Pattern2Step::CarrierUpdates, - Pattern2Step::TailCall, - ], - reason: "default", - } - } - } - - pub(crate) fn iter(&self) -> impl Iterator + '_ { - self.steps.iter().copied() - } - - pub(crate) fn reason(&self) -> &'static str { - self.reason - } -} diff --git a/src/mir/join_ir/lowering/mod.rs b/src/mir/join_ir/lowering/mod.rs index dd3f6428..9d6c2681 100644 --- a/src/mir/join_ir/lowering/mod.rs +++ b/src/mir/join_ir/lowering/mod.rs @@ -61,6 +61,7 @@ pub(crate) mod loop_view_builder; // Phase 33-23: Loop lowering dispatch pub mod loop_with_break_minimal; // Phase 188-Impl-2: Pattern 2 minimal lowerer pub mod loop_with_continue_minimal; pub mod method_call_lowerer; // Phase 224-B: MethodCall lowering (metadata-driven) +pub(crate) mod pattern2_step_schedule; // Phase 39: Pattern2 evaluation order scheduler pub mod method_return_hint; // Phase 83: P3-D 既知メソッド戻り値型推論箱 pub mod scope_manager; // Phase 231: Unified variable scope management // Phase 195: Pattern 4 minimal lowerer // Phase 242-EX-A: loop_with_if_phi_minimal removed - replaced by loop_with_if_phi_if_sum diff --git a/src/mir/join_ir/lowering/pattern2_step_schedule.rs b/src/mir/join_ir/lowering/pattern2_step_schedule.rs new file mode 100644 index 00000000..742d04dc --- /dev/null +++ b/src/mir/join_ir/lowering/pattern2_step_schedule.rs @@ -0,0 +1,224 @@ +//! Pattern 2 step scheduler (StepScheduleBox). +//! +//! Decides the evaluation order for Pattern 2 lowering without hardcoding it +//! inside the lowerer. This keeps the lowerer focused on emitting fragments, +//! while this box decides how to interleave them (e.g., body-local init before +//! break checks when the break depends on body-local values). + +use crate::config::env; +use crate::config::env::joinir_dev::joinir_test_debug_enabled; +use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierInit}; +use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; + +/// Steps that can be reordered by the scheduler. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Pattern2StepKind { + HeaderCond, + BodyInit, + BreakCheck, + Updates, + Tail, +} + +impl Pattern2StepKind { + fn as_str(&self) -> &'static str { + match self { + Pattern2StepKind::HeaderCond => "header-cond", + Pattern2StepKind::BodyInit => "body-init", + Pattern2StepKind::BreakCheck => "break", + Pattern2StepKind::Updates => "updates", + Pattern2StepKind::Tail => "tail", + } + } +} + +/// Data-driven schedule for Pattern 2 lowering. +#[derive(Debug, Clone)] +pub(crate) struct Pattern2StepSchedule { + steps: Vec, + reason: &'static str, +} + +impl Pattern2StepSchedule { + pub(crate) fn iter(&self) -> impl Iterator + '_ { + self.steps.iter().copied() + } + + pub(crate) fn reason(&self) -> &'static str { + self.reason + } + + pub(crate) fn steps(&self) -> &[Pattern2StepKind] { + &self.steps + } +} + +/// Minimal context for deciding the step order. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Pattern2ScheduleContext { + pub(crate) has_body_local_init: bool, + pub(crate) has_loop_local_carrier: bool, +} + +impl Pattern2ScheduleContext { + pub(crate) fn from_env( + body_local_env: Option<&LoopBodyLocalEnv>, + carrier_info: &CarrierInfo, + ) -> Self { + let has_body_local_init = body_local_env.map(|env| !env.is_empty()).unwrap_or(false); + let has_loop_local_carrier = carrier_info + .carriers + .iter() + .any(|c| matches!(c.init, CarrierInit::LoopLocalZero)); + + Self { + has_body_local_init, + has_loop_local_carrier, + } + } + + fn requires_body_init_before_break(&self) -> bool { + self.has_body_local_init || self.has_loop_local_carrier + } +} + +/// Build a schedule for Pattern 2 lowering. +/// +/// - Default P2: header → break → body-init → updates → tail +/// - Body-local break dependency (DigitPos/_atoi style): +/// header → body-init → break → updates → tail +pub(crate) fn build_pattern2_schedule(ctx: &Pattern2ScheduleContext) -> Pattern2StepSchedule { + let schedule = if ctx.requires_body_init_before_break() { + Pattern2StepSchedule { + steps: vec![ + Pattern2StepKind::HeaderCond, + Pattern2StepKind::BodyInit, + Pattern2StepKind::BreakCheck, + Pattern2StepKind::Updates, + Pattern2StepKind::Tail, + ], + reason: "body-local break dependency", + } + } else { + Pattern2StepSchedule { + steps: vec![ + Pattern2StepKind::HeaderCond, + Pattern2StepKind::BreakCheck, + Pattern2StepKind::BodyInit, + Pattern2StepKind::Updates, + Pattern2StepKind::Tail, + ], + reason: "default", + } + }; + + log_schedule(ctx, &schedule); + schedule +} + +fn log_schedule(ctx: &Pattern2ScheduleContext, schedule: &Pattern2StepSchedule) { + if !(env::joinir_dev_enabled() || joinir_test_debug_enabled()) { + return; + } + + let steps_desc = schedule + .steps() + .iter() + .map(Pattern2StepKind::as_str) + .collect::>() + .join(" -> "); + + eprintln!( + "[joinir/p2-sched] steps={steps_desc} reason={} ctx={{body_local_init={}, loop_local_carrier={}}}", + schedule.reason(), + ctx.has_body_local_init, + ctx.has_loop_local_carrier + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mir::join_ir::lowering::carrier_info::{CarrierRole, CarrierVar}; + use crate::mir::ValueId; + + fn carrier(loop_local: bool) -> CarrierVar { + let init = if loop_local { + CarrierInit::LoopLocalZero + } else { + CarrierInit::FromHost + }; + CarrierVar::with_role_and_init( + "c".to_string(), + ValueId(1), + CarrierRole::LoopState, + init, + ) + } + + fn carrier_info(carriers: Vec) -> CarrierInfo { + CarrierInfo { + loop_var_name: "i".to_string(), + loop_var_id: ValueId(0), + carriers, + trim_helper: None, + promoted_loopbodylocals: vec![], + } + } + + #[test] + fn default_schedule_break_before_body_init() { + let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![])); + let schedule = build_pattern2_schedule(&ctx); + assert_eq!( + schedule.steps(), + &[ + Pattern2StepKind::HeaderCond, + Pattern2StepKind::BreakCheck, + Pattern2StepKind::BodyInit, + Pattern2StepKind::Updates, + Pattern2StepKind::Tail + ] + ); + assert_eq!(schedule.reason(), "default"); + } + + #[test] + fn body_local_moves_init_before_break() { + let mut body_env = LoopBodyLocalEnv::new(); + body_env.insert("tmp".to_string(), ValueId(5)); + + let ctx = + Pattern2ScheduleContext::from_env(Some(&body_env), &carrier_info(vec![carrier(false)])); + let schedule = build_pattern2_schedule(&ctx); + assert_eq!( + schedule.steps(), + &[ + Pattern2StepKind::HeaderCond, + Pattern2StepKind::BodyInit, + Pattern2StepKind::BreakCheck, + Pattern2StepKind::Updates, + Pattern2StepKind::Tail + ] + ); + assert_eq!(schedule.reason(), "body-local break dependency"); + } + + #[test] + fn loop_local_carrier_triggers_body_first() { + let ctx = + Pattern2ScheduleContext::from_env(None, &carrier_info(vec![carrier(true)])); + let schedule = build_pattern2_schedule(&ctx); + assert_eq!( + schedule.steps(), + &[ + Pattern2StepKind::HeaderCond, + Pattern2StepKind::BodyInit, + Pattern2StepKind::BreakCheck, + Pattern2StepKind::Updates, + Pattern2StepKind::Tail + ] + ); + assert_eq!(schedule.reason(), "body-local break dependency"); + } +} diff --git a/src/mir/join_ir/mod.rs b/src/mir/join_ir/mod.rs index 3e3f02cf..2ddb3d56 100644 --- a/src/mir/join_ir/mod.rs +++ b/src/mir/join_ir/mod.rs @@ -51,8 +51,6 @@ pub use normalized::{ normalize_pattern1_minimal, normalize_pattern2_minimal, normalized_pattern1_to_structured, normalized_pattern2_to_structured, NormalizedModule, }; -#[cfg(feature = "normalized_dev")] -pub use normalized::fixtures; pub use verify::verify_progress_for_skip_ws; // Phase 200-3: Contract verification functions are in merge/mod.rs (private module access) diff --git a/src/mir/join_ir/normalized.rs b/src/mir/join_ir/normalized.rs index 6326af15..b73342de 100644 --- a/src/mir/join_ir/normalized.rs +++ b/src/mir/join_ir/normalized.rs @@ -93,6 +93,8 @@ pub enum JpOp { Unary(UnaryOp), Compare(CompareOp), BoxCall { box_name: String, method: String }, + /// 三項演算子(cond ? then : else) + Select, } /// Normalized JoinIR モジュール(テスト専用)。 @@ -115,7 +117,7 @@ impl NormalizedModule { #[cfg(feature = "normalized_dev")] fn verify_normalized_pattern1(module: &NormalizedModule) -> Result<(), String> { if module.phase != JoinIrPhase::Normalized { - return Err("Normalized verifier: phase must be Normalized".to_string()); + return Err("[joinir/normalized-dev] pattern1: phase must be Normalized".to_string()); } // Env field bounds check @@ -127,7 +129,7 @@ fn verify_normalized_pattern1(module: &NormalizedModule) -> Result<(), String> { JpInst::EnvLoad { field, .. } | JpInst::EnvStore { field, .. } => { if *field >= field_count { return Err(format!( - "Env field out of range: {} (fields={})", + "[joinir/normalized-dev] pattern1: env field out of range: {} (fields={})", field, field_count )); } @@ -156,7 +158,7 @@ fn verify_normalized_pattern1(module: &NormalizedModule) -> Result<(), String> { JpInst::TailCallFn { .. } | JpInst::TailCallKont { .. } | JpInst::If { .. } => {} _ => { return Err(format!( - "Function '{}' does not end with tail call/if", + "[joinir/normalized-dev] pattern1: function '{}' does not end with tail call/if", func.name )) } @@ -221,6 +223,12 @@ pub fn normalized_pattern1_to_structured(norm: &NormalizedModule) -> JoinModule op: *op, operand: args.get(0).copied().unwrap_or(ValueId(0)), })), + JpOp::Select => func.body.push(JoinInst::Compute(MirLikeInst::Select { + dst: *dst, + cond: args.get(0).copied().unwrap_or(ValueId(0)), + then_val: args.get(1).copied().unwrap_or(ValueId(0)), + else_val: args.get(2).copied().unwrap_or(ValueId(0)), + })), JpOp::Compare(op) => func.body.push(JoinInst::Compute(MirLikeInst::Compare { dst: *dst, op: *op, @@ -300,8 +308,30 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule { let mut max = 3; #[cfg(feature = "normalized_dev")] { - if shape_guard::is_jsonparser_atoi_mini(structured) { - max = 8; + let shapes = shape_guard::supported_shapes(structured); + if shapes + .iter() + .any(|s| matches!(s, NormalizedDevShape::JsonparserAtoiMini)) + { + max = max.max(8); + } + if shapes + .iter() + .any(|s| matches!(s, NormalizedDevShape::JsonparserAtoiReal)) + { + max = max.max(10); + } + if shapes + .iter() + .any(|s| matches!(s, NormalizedDevShape::JsonparserParseNumberReal)) + { + max = max.max(12); + } + if shapes + .iter() + .any(|s| matches!(s, NormalizedDevShape::JsonparserSkipWsReal)) + { + max = max.max(6); } } max @@ -397,6 +427,18 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule { args: vec![*lhs, *rhs], }) } + JoinInst::Compute(MirLikeInst::Select { + dst, + cond, + then_val, + else_val, + }) => { + body.push(JpInst::Let { + dst: *dst, + op: JpOp::Select, + args: vec![*cond, *then_val, *else_val], + }) + } JoinInst::Jump { cont, args, cond } => { if let Some(cond_val) = cond { body.push(JpInst::If { @@ -412,6 +454,19 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule { }); } } + JoinInst::Select { + dst, + cond, + then_val, + else_val, + .. + } => { + body.push(JpInst::Let { + dst: *dst, + op: JpOp::Select, + args: vec![*cond, *then_val, *else_val], + }); + } JoinInst::Call { func, args, k_next, .. } => { if k_next.is_none() { body.push(JpInst::TailCallFn { @@ -523,6 +578,14 @@ pub fn normalized_pattern2_to_structured(norm: &NormalizedModule) -> JoinModule op: *op, operand: args.get(0).copied().unwrap_or(ValueId(0)), })), + JpOp::Select => func + .body + .push(JoinInst::Compute(MirLikeInst::Select { + dst: *dst, + cond: args.get(0).copied().unwrap_or(ValueId(0)), + then_val: args.get(1).copied().unwrap_or(ValueId(0)), + else_val: args.get(2).copied().unwrap_or(ValueId(0)), + })), JpOp::Compare(op) => func.body.push(JoinInst::Compute(MirLikeInst::Compare { dst: *dst, op: *op, @@ -578,7 +641,9 @@ fn verify_normalized_pattern2( max_env_fields: usize, ) -> Result<(), String> { if module.phase != JoinIrPhase::Normalized { - return Err("Normalized verifier (Pattern2): phase must be Normalized".to_string()); + return Err( + "[joinir/normalized-dev] pattern2: phase must be Normalized".to_string(), + ); } let mut layout_sizes: HashMap = HashMap::new(); @@ -586,7 +651,7 @@ fn verify_normalized_pattern2( let size = layout.fields.len(); if !(1..=max_env_fields).contains(&size) { return Err(format!( - "Normalized Pattern2 expects 1..={} env fields, got {}", + "[joinir/normalized-dev] pattern2: expected 1..={} env fields, got {}", max_env_fields, size )); } @@ -615,7 +680,10 @@ fn verify_normalized_pattern2( | JpInst::If { env, .. } => { if let Some(expected) = expected_env_len { if env.is_empty() { - return Err("Normalized Pattern2 env must not be empty".to_string()); + return Err( + "[joinir/normalized-dev] pattern2: env must not be empty" + .to_string(), + ); } let _ = expected; } @@ -631,7 +699,7 @@ fn verify_normalized_pattern2( | JpInst::If { .. } => {} _ => { return Err(format!( - "Function '{}' does not end with tail call/if", + "[joinir/normalized-dev] pattern2: function '{}' does not end with tail call/if", func.name )); } @@ -813,11 +881,14 @@ pub(crate) fn normalized_dev_roundtrip_structured( return Err("[joinir/normalized-dev] module shape is not supported by normalized_dev".into()); } - let verbose = crate::config::env::joinir_dev_enabled(); + let debug = dev_env::normalized_dev_logs_enabled() && crate::config::env::joinir_dev_enabled(); for shape in shapes { - if verbose { - eprintln!("[joinir/normalized-dev] attempting {:?} normalization", shape); + if debug { + eprintln!( + "[joinir/normalized-dev/roundtrip] attempting {:?} normalization", + shape + ); } let attempt = match shape { @@ -827,7 +898,10 @@ pub(crate) fn normalized_dev_roundtrip_structured( })), NormalizedDevShape::Pattern2Mini | NormalizedDevShape::JsonparserSkipWsMini - | NormalizedDevShape::JsonparserAtoiMini => catch_unwind(AssertUnwindSafe(|| { + | NormalizedDevShape::JsonparserSkipWsReal + | NormalizedDevShape::JsonparserAtoiMini + | NormalizedDevShape::JsonparserAtoiReal + | NormalizedDevShape::JsonparserParseNumberReal => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_pattern2_minimal(module); normalized_pattern2_to_structured(&norm) })), @@ -835,9 +909,9 @@ pub(crate) fn normalized_dev_roundtrip_structured( match attempt { Ok(structured) => { - if verbose { + if debug { eprintln!( - "[joinir/normalized-dev] {:?} normalization succeeded (functions={})", + "[joinir/normalized-dev/roundtrip] {:?} normalization succeeded (functions={})", shape, structured.functions.len() ); @@ -845,9 +919,9 @@ pub(crate) fn normalized_dev_roundtrip_structured( return Ok(structured); } Err(_) => { - if verbose { + if debug { eprintln!( - "[joinir/normalized-dev] {:?} normalization failed (unsupported)", + "[joinir/normalized-dev/roundtrip] {:?} normalization failed (unsupported)", shape ); } diff --git a/src/mir/join_ir/normalized/dev_env.rs b/src/mir/join_ir/normalized/dev_env.rs index b6b63e42..a8b05b12 100644 --- a/src/mir/join_ir/normalized/dev_env.rs +++ b/src/mir/join_ir/normalized/dev_env.rs @@ -1,38 +1,111 @@ #![cfg(feature = "normalized_dev")] use once_cell::sync::Lazy; -use std::sync::Mutex; +use std::sync::{Mutex, MutexGuard}; /// RAII guard for normalized_dev env toggling (NYASH_JOINIR_NORMALIZED_DEV_RUN). -/// 汚染防止のため tests/runner の両方で再利用できるようにここに置く。 +/// ネストを許可し、最初の呼び出し時の状態だけを保存・復元する。 pub struct NormalizedDevEnvGuard { - _lock: std::sync::MutexGuard<'static, ()>, - prev: Option, + active: bool, } -static NORMALIZED_ENV_LOCK: Lazy> = Lazy::new(|| Mutex::new(())); +#[derive(Default)] +struct EnvState { + stack: Vec>, +} + +static NORMALIZED_ENV_STATE: Lazy> = Lazy::new(|| Mutex::new(EnvState::default())); +static NORMALIZED_TEST_LOCK: Lazy> = Lazy::new(|| Mutex::new(())); impl NormalizedDevEnvGuard { pub fn new(enabled: bool) -> Self { - let lock = NORMALIZED_ENV_LOCK + let mut state = NORMALIZED_ENV_STATE .lock() .expect("normalized env mutex poisoned"); + + // Save current value before overriding. let prev = std::env::var("NYASH_JOINIR_NORMALIZED_DEV_RUN").ok(); + state.stack.push(prev); + if enabled { std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", "1"); } else { std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN"); } - Self { _lock: lock, prev } + + Self { active: true } } } impl Drop for NormalizedDevEnvGuard { fn drop(&mut self) { - if let Some(prev) = &self.prev { - std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", prev); - } else { - std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN"); + if !self.active { + return; + } + let mut state = NORMALIZED_ENV_STATE + .lock() + .expect("normalized env mutex poisoned"); + if let Some(prev) = state.stack.pop() { + if let Some(prev) = prev { + std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", prev); + } else { + std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN"); + } } } } + +/// normalized_dev feature + env の ON/OFF をまとめた判定。 +pub fn normalized_dev_enabled() -> bool { + crate::config::env::normalized_dev_enabled() +} + +/// normalized_dev かつ test/debug ログが有効なときだけ true。 +pub fn normalized_dev_logs_enabled() -> bool { + crate::config::env::normalized_dev_enabled() && crate::config::env::joinir_test_debug_enabled() +} + +/// テスト用コンテキスト:env を ON にしつつロックで並列汚染を防ぐ。 +pub struct NormalizedTestContext<'a> { + _lock: MutexGuard<'a, ()>, + _env_guard: NormalizedDevEnvGuard, +} + +impl<'a> NormalizedTestContext<'a> { + fn new(lock: MutexGuard<'a, ()>) -> Self { + let env_guard = NormalizedDevEnvGuard::new(true); + NormalizedTestContext { + _lock: lock, + _env_guard: env_guard, + } + } +} + +/// テストで使う共通ガード。 +pub fn test_ctx() -> NormalizedTestContext<'static> { + let lock = NORMALIZED_TEST_LOCK + .lock() + .unwrap_or_else(|e| e.into_inner()); + NormalizedTestContext::new(lock) +} + +/// 簡易ラッパー:クロージャを normalized_dev ON で実行。 +pub fn with_dev_env(f: F) -> R +where + F: FnOnce() -> R, +{ + let _ctx = test_ctx(); + f() +} + +/// env が既に ON のときはそのまま、OFF のときだけ with_dev_env を噛ませる。 +pub fn with_dev_env_if_unset(f: F) -> R +where + F: FnOnce() -> R, +{ + if normalized_dev_enabled() { + f() + } else { + with_dev_env(f) + } +} diff --git a/src/mir/join_ir/normalized/fixtures.rs b/src/mir/join_ir/normalized/fixtures.rs index f111ae54..e2d5c125 100644 --- a/src/mir/join_ir/normalized/fixtures.rs +++ b/src/mir/join_ir/normalized/fixtures.rs @@ -9,6 +9,7 @@ use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr; use crate::mir::join_ir::lowering::loop_with_break_minimal::lower_loop_with_break_minimal; use crate::mir::join_ir::JoinModule; use crate::mir::{BasicBlockId, ValueId}; +use crate::{config::env::joinir_dev_enabled, config::env::joinir_test_debug_enabled}; use std::collections::{BTreeMap, BTreeSet}; /// Structured Pattern2 (joinir_min_loop 相当) をテスト用に生成するヘルパー。 @@ -113,6 +114,54 @@ pub fn build_jsonparser_skip_ws_structured_for_normalized_dev() -> JoinModule { lowerer.lower_program_json(&program_json) } +/// JsonParser _skip_whitespace 本体相当の P2 ループを Structured で組み立てるヘルパー。 +/// +/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_skip_ws_real.program.json +pub fn build_jsonparser_skip_ws_real_structured_for_normalized_dev() -> JoinModule { + const FIXTURE: &str = include_str!( + "../../../../docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_skip_ws_real.program.json" + ); + + let program_json: serde_json::Value = + serde_json::from_str(FIXTURE).expect("jsonparser skip_ws real fixture should be valid JSON"); + + let mut lowerer = AstToJoinIrLowerer::new(); + let module = lowerer.lower_program_json(&program_json); + + if joinir_dev_enabled() && joinir_test_debug_enabled() { + eprintln!( + "[joinir/normalized-dev] jsonparser_skip_ws_real structured module: {:#?}", + module + ); + } + + module +} + +/// JsonParser _parse_number 本体相当の P2 ループを Structured で組み立てるヘルパー。 +/// +/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_parse_number_real.program.json +pub fn build_jsonparser_parse_number_real_structured_for_normalized_dev() -> JoinModule { + const FIXTURE: &str = include_str!( + "../../../../docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_parse_number_real.program.json" + ); + + let program_json: serde_json::Value = + serde_json::from_str(FIXTURE).expect("jsonparser parse_number real fixture should be valid JSON"); + + let mut lowerer = AstToJoinIrLowerer::new(); + let module = lowerer.lower_program_json(&program_json); + + if joinir_dev_enabled() && joinir_test_debug_enabled() { + eprintln!( + "[joinir/normalized-dev] jsonparser_parse_number_real structured module: {:#?}", + module + ); + } + + module +} + /// JsonParser _atoi 相当のミニ P2 ループを Structured で組み立てるヘルパー。 /// /// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_atoi_mini.program.json @@ -127,12 +176,48 @@ pub fn build_jsonparser_atoi_structured_for_normalized_dev() -> JoinModule { let mut lowerer = AstToJoinIrLowerer::new(); let module = lowerer.lower_program_json(&program_json); - if std::env::var("JOINIR_TEST_DEBUG").is_ok() { + if joinir_dev_enabled() && joinir_test_debug_enabled() { eprintln!( - "[joinir/normalized-dev] jsonparser_atoi_mini structured module: {:#?}", + "[joinir/normalized-dev] jsonparser_atoi_mini structured module: {:#?}", module ); } module } + +/// JsonParser _atoi 本体相当の P2 ループを Structured で組み立てるヘルパー。 +/// +/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_atoi_real.program.json +pub fn build_jsonparser_atoi_real_structured_for_normalized_dev() -> JoinModule { + const FIXTURE: &str = include_str!( + "../../../../docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_atoi_real.program.json" + ); + + let program_json: serde_json::Value = + serde_json::from_str(FIXTURE).expect("jsonparser atoi real fixture should be valid JSON"); + + let mut lowerer = AstToJoinIrLowerer::new(); + let module = lowerer.lower_program_json(&program_json); + + if joinir_dev_enabled() && joinir_test_debug_enabled() { + eprintln!( + "[joinir/normalized-dev] jsonparser_atoi_real structured module: {:#?}", + module + ); + } + + module +} + +/// まとめて import したいとき用のプレリュード。 +pub mod prelude { + pub use super::{ + build_jsonparser_atoi_real_structured_for_normalized_dev, + build_jsonparser_atoi_structured_for_normalized_dev, + build_jsonparser_parse_number_real_structured_for_normalized_dev, + build_jsonparser_skip_ws_real_structured_for_normalized_dev, + build_jsonparser_skip_ws_structured_for_normalized_dev, + build_pattern2_break_fixture_structured, build_pattern2_minimal_structured, + }; +} diff --git a/src/mir/join_ir/normalized/shape_guard.rs b/src/mir/join_ir/normalized/shape_guard.rs index eba7f143..6a983c1d 100644 --- a/src/mir/join_ir/normalized/shape_guard.rs +++ b/src/mir/join_ir/normalized/shape_guard.rs @@ -1,5 +1,7 @@ #![cfg(feature = "normalized_dev")] +use crate::config::env::joinir_dev_enabled; +use crate::mir::join_ir::normalized::dev_env; use crate::mir::join_ir::{JoinFuncId, JoinFunction, JoinInst, JoinModule}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -7,105 +9,261 @@ pub(crate) enum NormalizedDevShape { Pattern1Mini, Pattern2Mini, JsonparserSkipWsMini, + JsonparserSkipWsReal, JsonparserAtoiMini, + JsonparserAtoiReal, + JsonparserParseNumberReal, } -/// 直接 Normalized→MIR ブリッジで扱う shape を返す(dev 限定)。 +type Detector = fn(&JoinModule) -> bool; + +const SHAPE_DETECTORS: &[(NormalizedDevShape, Detector)] = &[ + (NormalizedDevShape::Pattern1Mini, detectors::is_pattern1_mini), + (NormalizedDevShape::Pattern2Mini, detectors::is_pattern2_mini), + ( + NormalizedDevShape::JsonparserSkipWsMini, + detectors::is_jsonparser_skip_ws_mini, + ), + ( + NormalizedDevShape::JsonparserSkipWsReal, + detectors::is_jsonparser_skip_ws_real, + ), + ( + NormalizedDevShape::JsonparserAtoiMini, + detectors::is_jsonparser_atoi_mini, + ), + ( + NormalizedDevShape::JsonparserAtoiReal, + detectors::is_jsonparser_atoi_real, + ), + ( + NormalizedDevShape::JsonparserParseNumberReal, + detectors::is_jsonparser_parse_number_real, + ), +]; + +/// direct ブリッジで扱う shape(dev 限定)。 pub(crate) fn direct_shapes(module: &JoinModule) -> Vec { - supported_shapes(module) + let shapes = detect_shapes(module); + log_shapes("direct", &shapes); + shapes } +/// Structured→Normalized の対象 shape(dev 限定)。 pub(crate) fn supported_shapes(module: &JoinModule) -> Vec { - let mut shapes = Vec::new(); - if is_jsonparser_atoi_mini(module) { - shapes.push(NormalizedDevShape::JsonparserAtoiMini); - } - if is_jsonparser_skip_ws_mini(module) { - shapes.push(NormalizedDevShape::JsonparserSkipWsMini); - } - if is_pattern2_mini(module) { - shapes.push(NormalizedDevShape::Pattern2Mini); - } - if is_pattern1_mini(module) { - shapes.push(NormalizedDevShape::Pattern1Mini); - } + let shapes = detect_shapes(module); + log_shapes("roundtrip", &shapes); + shapes +} + +/// canonical(常時 Normalized 経路を通す)対象。 +/// Phase 41: P2 コアセット(P2 mini + JP skip_ws mini/real + JP atoi mini)。 +pub(crate) fn canonical_shapes(module: &JoinModule) -> Vec { + let shapes: Vec<_> = detect_shapes(module) + .into_iter() + .filter(|s| { + matches!( + s, + NormalizedDevShape::Pattern2Mini + | NormalizedDevShape::JsonparserSkipWsMini + | NormalizedDevShape::JsonparserSkipWsReal + | NormalizedDevShape::JsonparserAtoiMini + ) + }) + .collect(); + log_shapes("canonical", &shapes); shapes } #[allow(dead_code)] pub(crate) fn is_direct_supported(module: &JoinModule) -> bool { - !direct_shapes(module).is_empty() + !detect_shapes(module).is_empty() } -pub(crate) fn is_pattern1_mini(module: &JoinModule) -> bool { - module.is_structured() && find_loop_step(module).is_some() -} +fn detect_shapes(module: &JoinModule) -> Vec { + let mut shapes: Vec<_> = SHAPE_DETECTORS + .iter() + .filter_map(|(shape, detector)| if detector(module) { Some(*shape) } else { None }) + .collect(); -pub(crate) fn is_pattern2_mini(module: &JoinModule) -> bool { - if !module.is_structured() || module.functions.len() != 3 { - return false; - } - let loop_func = match find_loop_step(module) { - Some(f) => f, - None => return false, - }; - if !(1..=3).contains(&loop_func.params.len()) { - return false; + // Pattern1 は「最小の後方互換」なので、より具体的な shape が見つかった場合は外しておく。 + if shapes.len() > 1 { + shapes.retain(|s| *s != NormalizedDevShape::Pattern1Mini); } - let has_cond_jump = loop_func - .body - .iter() - .any(|inst| matches!(inst, JoinInst::Jump { cond: Some(_), .. })); - let has_tail_call = loop_func - .body - .iter() - .any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. })); - - has_cond_jump && has_tail_call + shapes } -pub(crate) fn is_jsonparser_skip_ws_mini(module: &JoinModule) -> bool { - is_pattern2_mini(module) - && module +// --- 判定ロジック(共通) --- +mod detectors { + use super::*; + + pub(super) fn is_pattern1_mini(module: &JoinModule) -> bool { + module.is_structured() && find_loop_step(module).is_some() + } + + pub(super) fn is_pattern2_mini(module: &JoinModule) -> bool { + if !module.is_structured() || module.functions.len() != 3 { + return false; + } + let loop_func = match find_loop_step(module) { + Some(f) => f, + None => return false, + }; + if !(1..=3).contains(&loop_func.params.len()) { + return false; + } + + let has_cond_jump = loop_func + .body + .iter() + .any(|inst| matches!(inst, JoinInst::Jump { cond: Some(_), .. })); + let has_tail_call = loop_func + .body + .iter() + .any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. })); + + has_cond_jump && has_tail_call + } + + pub(super) fn is_jsonparser_skip_ws_mini(module: &JoinModule) -> bool { + is_pattern2_mini(module) + && module + .functions + .values() + .any(|f| f.name == "jsonparser_skip_ws_mini") + } + + pub(crate) fn is_jsonparser_skip_ws_real(module: &JoinModule) -> bool { + if !module.is_structured() || module.functions.len() != 3 { + return false; + } + let loop_func = match find_loop_step(module) { + Some(f) => f, + None => return false, + }; + if !(2..=6).contains(&loop_func.params.len()) { + return false; + } + + let has_cond_jump = loop_func + .body + .iter() + .any(|inst| matches!(inst, JoinInst::Jump { cond: Some(_), .. })); + let has_tail_call = loop_func + .body + .iter() + .any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. })); + + has_cond_jump + && has_tail_call + && module + .functions + .values() + .any(|f| f.name == "jsonparser_skip_ws_real") + } + + pub(crate) fn is_jsonparser_atoi_mini(module: &JoinModule) -> bool { + if !module.is_structured() || module.functions.len() != 3 { + return false; + } + let loop_func = match find_loop_step(module) { + Some(f) => f, + None => return false, + }; + if !(3..=8).contains(&loop_func.params.len()) { + return false; + } + + let has_cond_jump = loop_func + .body + .iter() + .any(|inst| matches!(inst, JoinInst::Jump { cond: Some(_), .. })); + let has_tail_call = loop_func + .body + .iter() + .any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. })); + + has_cond_jump + && has_tail_call + && module + .functions + .values() + .any(|f| f.name == "jsonparser_atoi_mini") + } + + pub(crate) fn is_jsonparser_atoi_real(module: &JoinModule) -> bool { + if !module.is_structured() || module.functions.len() != 3 { + return false; + } + let loop_func = match find_loop_step(module) { + Some(f) => f, + None => return false, + }; + if !(3..=10).contains(&loop_func.params.len()) { + return false; + } + + let has_cond_jump = loop_func + .body + .iter() + .any(|inst| matches!(inst, JoinInst::Jump { cond: Some(_), .. })); + let has_tail_call = loop_func + .body + .iter() + .any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. })); + + has_cond_jump + && has_tail_call + && module + .functions + .values() + .any(|f| f.name == "jsonparser_atoi_real") + } + + pub(crate) fn is_jsonparser_parse_number_real(module: &JoinModule) -> bool { + if !module.is_structured() || module.functions.len() != 3 { + return false; + } + let loop_func = match find_loop_step(module) { + Some(f) => f, + None => return false, + }; + if !(3..=12).contains(&loop_func.params.len()) { + return false; + } + + let has_cond_jump = loop_func + .body + .iter() + .any(|inst| matches!(inst, JoinInst::Jump { cond: Some(_), .. })); + let has_tail_call = loop_func + .body + .iter() + .any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. })); + + has_cond_jump + && has_tail_call + && module + .functions + .values() + .any(|f| f.name == "jsonparser_parse_number_real") + } + + pub(super) fn find_loop_step(module: &JoinModule) -> Option<&JoinFunction> { + module .functions .values() - .any(|f| f.name == "jsonparser_skip_ws_mini") -} - -pub(crate) fn is_jsonparser_atoi_mini(module: &JoinModule) -> bool { - if !module.is_structured() || module.functions.len() != 3 { - return false; + .find(|f| f.name == "loop_step") + .or_else(|| module.functions.get(&JoinFuncId::new(1))) } - let loop_func = match find_loop_step(module) { - Some(f) => f, - None => return false, - }; - if !(3..=8).contains(&loop_func.params.len()) { - return false; +} + +fn log_shapes(tag: &str, shapes: &[NormalizedDevShape]) { + if shapes.is_empty() { + return; + } + if dev_env::normalized_dev_logs_enabled() && joinir_dev_enabled() { + eprintln!("[joinir/normalized-dev/shape] {}: {:?}", tag, shapes); } - - let has_cond_jump = loop_func - .body - .iter() - .any(|inst| matches!(inst, JoinInst::Jump { cond: Some(_), .. })); - let has_tail_call = loop_func - .body - .iter() - .any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. })); - - has_cond_jump - && has_tail_call - && module - .functions - .values() - .any(|f| f.name.contains("atoi")) -} - -fn find_loop_step(module: &JoinModule) -> Option<&JoinFunction> { - module - .functions - .values() - .find(|f| f.name == "loop_step") - .or_else(|| module.functions.get(&JoinFuncId::new(1))) } diff --git a/src/mir/join_ir_runner.rs b/src/mir/join_ir_runner.rs index 7e1a24fd..0bc71350 100644 --- a/src/mir/join_ir_runner.rs +++ b/src/mir/join_ir_runner.rs @@ -34,7 +34,9 @@ use std::collections::HashMap; use crate::config::env::normalized_dev_enabled; use crate::mir::join_ir::{ConstValue, JoinFuncId, JoinInst, JoinModule, MirLikeInst, VarId}; #[cfg(feature = "normalized_dev")] -use crate::mir::join_ir::normalized::{normalized_dev_roundtrip_structured, shape_guard}; +use crate::mir::join_ir::normalized::{ + dev_env, normalized_dev_roundtrip_structured, shape_guard, +}; // Phase 27.8: ops box からの再エクスポート pub use crate::mir::join_ir_ops::{JoinIrOpError, JoinValue}; @@ -64,31 +66,32 @@ fn run_joinir_function_normalized_dev( args: &[JoinValue], ) -> Result { // Keep dev path opt-in and fail-fast: only Structured P1/P2 minis are supported. - let verbose = crate::config::env::joinir_dev_enabled(); - let args_vec = args.to_vec(); + dev_env::with_dev_env_if_unset(|| { + let debug = dev_env::normalized_dev_logs_enabled(); + let args_vec = args.to_vec(); - let shapes = shape_guard::supported_shapes(module); - if shapes.is_empty() { - if verbose { + let shapes = shape_guard::supported_shapes(module); + if shapes.is_empty() { + if debug { + eprintln!("[joinir/normalized-dev/runner] shape unsupported; staying on Structured path"); + } + return execute_function(vm, module, entry, args_vec); + } + + let structured_roundtrip = normalized_dev_roundtrip_structured(module).map_err(|msg| { + JoinRuntimeError::new(format!("[joinir/normalized-dev/runner] {}", msg)) + })?; + + if debug { eprintln!( - "[joinir/runner/normalized-dev] shape unsupported; staying on Structured path" + "[joinir/normalized-dev/runner] normalized roundtrip succeeded (shapes={:?}, functions={})", + shapes, + structured_roundtrip.functions.len() ); } - return execute_function(vm, module, entry, args_vec); - } - let structured_roundtrip = normalized_dev_roundtrip_structured(module) - .map_err(|msg| JoinRuntimeError::new(format!("[joinir/runner/normalized-dev] {}", msg)))?; - - if verbose { - eprintln!( - "[joinir/runner/normalized-dev] normalized roundtrip succeeded (shapes={:?}, functions={})", - shapes, - structured_roundtrip.functions.len() - ); - } - - execute_function(vm, &structured_roundtrip, entry, args_vec) + execute_function(vm, &structured_roundtrip, entry, args_vec) + }) } fn execute_function( diff --git a/src/mir/join_ir_vm_bridge/bridge.rs b/src/mir/join_ir_vm_bridge/bridge.rs index b02fbd2e..151754ce 100644 --- a/src/mir/join_ir_vm_bridge/bridge.rs +++ b/src/mir/join_ir_vm_bridge/bridge.rs @@ -62,7 +62,10 @@ fn normalize_for_shape( } NormalizedDevShape::Pattern2Mini | NormalizedDevShape::JsonparserSkipWsMini - | NormalizedDevShape::JsonparserAtoiMini => { + | NormalizedDevShape::JsonparserSkipWsReal + | NormalizedDevShape::JsonparserAtoiMini + | NormalizedDevShape::JsonparserAtoiReal + | NormalizedDevShape::JsonparserParseNumberReal => { catch_unwind(AssertUnwindSafe(|| normalize_pattern2_minimal(module))) } }; @@ -80,44 +83,79 @@ fn normalize_for_shape( fn try_normalized_direct_bridge( module: &JoinModule, meta: &JoinFuncMetaMap, + shapes: &[NormalizedDevShape], + allow_structured_fallback: bool, + use_env_guard: bool, ) -> Result, JoinIrVmBridgeError> { - let shapes = shape_guard::direct_shapes(module); if shapes.is_empty() { - return Ok(None); + crate::mir::join_ir_vm_bridge::normalized_bridge::log_dev( + "fallback", + "normalized dev enabled but shape unsupported; using Structured path", + true, + ); + return if allow_structured_fallback { + Ok(None) + } else { + Err(JoinIrVmBridgeError::new( + "[joinir/bridge] canonical normalized route requested but shape unsupported", + )) + }; } - let verbose = crate::config::env::joinir_dev_enabled(); - - for shape in shapes { - if verbose { - eprintln!("[joinir/bridge] attempting normalized→MIR for {:?}", shape); - } - match normalize_for_shape(module, shape) { - Ok(norm) => { - let mir = lower_normalized_to_mir_minimal(&norm, meta)?; - if verbose { - eprintln!( - "[joinir/bridge] normalized→MIR succeeded (shape={:?}, functions={})", - shape, - norm.functions.len() - ); - } - return Ok(Some(mir)); + let exec = || { + let debug = crate::mir::join_ir::normalized::dev_env::normalized_dev_logs_enabled(); + for &shape in shapes { + if debug { + crate::mir::join_ir_vm_bridge::normalized_bridge::log_dev( + "direct", + format!("attempting normalized→MIR for {:?}", shape), + false, + ); } - Err(err) => { - if verbose { - eprintln!( - "[joinir/bridge] {:?} normalization failed: {} (continuing)", - shape, err.message + match normalize_for_shape(module, shape) { + Ok(norm) => { + let mir = + lower_normalized_to_mir_minimal(&norm, meta, allow_structured_fallback)?; + crate::mir::join_ir_vm_bridge::normalized_bridge::log_dev( + "direct", + format!( + "normalized→MIR succeeded (shape={:?}, functions={})", + shape, + norm.functions.len() + ), + false, ); + return Ok(Some(mir)); + } + Err(err) => { + if debug { + crate::mir::join_ir_vm_bridge::normalized_bridge::log_dev( + "direct", + format!( + "{:?} normalization failed: {} (continuing)", + shape, err.message + ), + false, + ); + } } } } - } - Err(JoinIrVmBridgeError::new( - "[joinir/bridge] normalized_dev enabled but no normalization attempt succeeded", - )) + if allow_structured_fallback { + Ok(None) + } else { + Err(JoinIrVmBridgeError::new( + "[joinir/bridge] canonical normalized route failed for all shapes", + )) + } + }; + + if use_env_guard { + crate::mir::join_ir::normalized::dev_env::with_dev_env_if_unset(exec) + } else { + exec() + } } /// JoinIR → MIR の単一入口。Normalized dev が有効なら roundtrip してから既存経路へ。 @@ -125,18 +163,28 @@ pub(crate) fn bridge_joinir_to_mir_with_meta( module: &JoinModule, meta: &JoinFuncMetaMap, ) -> Result { - if crate::config::env::normalized_dev_enabled() { - #[cfg(feature = "normalized_dev")] - { - match try_normalized_direct_bridge(module, meta)? { + #[cfg(feature = "normalized_dev")] + { + // Phase 41: canonical shapes は env が OFF でも常に Normalized → MIR を通す。 + let canonical_shapes = shape_guard::canonical_shapes(module); + if !canonical_shapes.is_empty() { + match try_normalized_direct_bridge(module, meta, &canonical_shapes, false, false)? { Some(mir) => return Ok(mir), None => { - debug_log!( - "[joinir/bridge] normalized dev enabled but shape unsupported; falling back to Structured path" - ); + return Err(JoinIrVmBridgeError::new( + "[joinir/bridge] canonical normalized route returned None unexpectedly", + )) } } } + + if crate::config::env::normalized_dev_enabled() { + let shapes = shape_guard::direct_shapes(module); + match try_normalized_direct_bridge(module, meta, &shapes, true, true)? { + Some(mir) => return Ok(mir), + None => {} + } + } } lower_joinir_structured_to_mir_with_meta(module, meta) diff --git a/src/mir/join_ir_vm_bridge/normalized_bridge.rs b/src/mir/join_ir_vm_bridge/normalized_bridge.rs index a4940f0b..9a86ad44 100644 --- a/src/mir/join_ir_vm_bridge/normalized_bridge.rs +++ b/src/mir/join_ir_vm_bridge/normalized_bridge.rs @@ -11,6 +11,22 @@ use crate::mir::MirModule; mod direct; +fn dev_debug_enabled() -> bool { + crate::mir::join_ir::normalized::dev_env::normalized_dev_logs_enabled() +} + +/// Dev logging helper with unified category prefix. +pub(super) fn log_dev(category: &str, message: impl AsRef, important: bool) { + let debug = dev_debug_enabled(); + if debug || important { + eprintln!("[joinir/normalized-dev/bridge/{}] {}", category, message.as_ref()); + } +} + +pub(super) fn log_debug(category: &str, message: impl AsRef) { + log_dev(category, message, false); +} + /// Direct Normalized → MIR 変換が未対応のときに使うフォールバック。 fn lower_normalized_via_structured( norm: &NormalizedModule, @@ -24,9 +40,14 @@ fn lower_normalized_via_structured( normalized_pattern2_to_structured(norm) }; - if crate::config::env::joinir_dev_enabled() { - eprintln!("[joinir/normalized-bridge/fallback] using structured path (functions={})", structured.functions.len()); - } + log_dev( + "fallback", + format!( + "using structured path (functions={})", + structured.functions.len() + ), + true, + ); lower_joinir_structured_to_mir_with_meta(&structured, meta) } @@ -35,6 +56,7 @@ fn lower_normalized_via_structured( pub(crate) fn lower_normalized_to_mir_minimal( norm: &NormalizedModule, meta: &JoinFuncMetaMap, + allow_structured_fallback: bool, ) -> Result { if norm.phase != JoinIrPhase::Normalized { return Err(JoinIrVmBridgeError::new( @@ -42,11 +64,14 @@ pub(crate) fn lower_normalized_to_mir_minimal( )); } - if std::env::var("JOINIR_TEST_DEBUG").is_ok() { - eprintln!( - "[joinir/normalized-bridge] lowering normalized module (functions={}, env_layouts={})", - norm.functions.len(), - norm.env_layouts.len() + if dev_debug_enabled() { + log_debug( + "debug", + format!( + "lowering normalized module (functions={}, env_layouts={})", + norm.functions.len(), + norm.env_layouts.len() + ), ); for layout in &norm.env_layouts { let fields: Vec = layout @@ -54,19 +79,21 @@ pub(crate) fn lower_normalized_to_mir_minimal( .iter() .map(|f| format!("{}={:?}", f.name, f.value_id)) .collect(); - eprintln!( - "[joinir/normalized-bridge] env_layout {} fields: {}", - layout.id, - fields.join(", ") + log_debug( + "debug", + format!("env_layout {} fields: {}", layout.id, fields.join(", ")), ); } for func in norm.functions.values() { - eprintln!( - "[joinir/normalized-bridge] fn {} (id={:?}) env_layout={:?} body_len={}", - func.name, - func.id, - func.env_layout, - func.body.len() + log_debug( + "debug", + format!( + "fn {} (id={:?}) env_layout={:?} body_len={}", + func.name, + func.id, + func.env_layout, + func.body.len() + ), ); } } @@ -74,11 +101,20 @@ pub(crate) fn lower_normalized_to_mir_minimal( // direct 対象は Normalized → MIR をそのまま吐く。未対応 shape は Structured 経由にフォールバック。 match direct::lower_normalized_direct_minimal(norm) { Ok(mir) => Ok(mir), - Err(err) => { - if crate::config::env::joinir_dev_enabled() { - eprintln!("[joinir/normalized-bridge/fallback] direct path failed: {}; falling back to Structured path", err.message); - } + Err(err) if allow_structured_fallback => { + log_dev( + "fallback", + format!( + "direct path failed: {}; falling back to Structured path", + err.message + ), + true, + ); lower_normalized_via_structured(norm, meta) } + Err(err) => Err(JoinIrVmBridgeError::new(format!( + "[joinir/normalized-bridge] direct path failed and fallback disabled: {}", + err.message + ))), } } diff --git a/src/mir/join_ir_vm_bridge/normalized_bridge/direct.rs b/src/mir/join_ir_vm_bridge/normalized_bridge/direct.rs index d5ec7669..30d28f1b 100644 --- a/src/mir/join_ir_vm_bridge/normalized_bridge/direct.rs +++ b/src/mir/join_ir_vm_bridge/normalized_bridge/direct.rs @@ -4,7 +4,6 @@ use super::super::join_func_name; use super::super::JoinIrVmBridgeError; use super::super::convert_mir_like_inst; use crate::ast::Span; -use crate::config::env::joinir_dev_enabled; use crate::mir::join_ir::normalized::{JpFuncId, JpFunction, JpInst, JpOp, NormalizedModule}; use crate::mir::join_ir::{JoinFuncId, JoinIrPhase, MirLikeInst}; use crate::mir::{ @@ -20,19 +19,20 @@ pub(crate) fn lower_normalized_direct_minimal( ) -> Result { if norm.phase != JoinIrPhase::Normalized { return Err(JoinIrVmBridgeError::new( - "[joinir/normalized-bridge] expected Normalized JoinIR module", + "[joinir/normalized-bridge/direct] expected Normalized JoinIR module", )); } - let debug_dump = std::env::var("JOINIR_TEST_DEBUG").is_ok(); - let verbose = joinir_dev_enabled(); - if verbose { - eprintln!( - "[joinir/normalized-bridge/direct] lowering normalized module (functions={}, env_layouts={})", + let debug_dump = crate::mir::join_ir::normalized::dev_env::normalized_dev_logs_enabled(); + super::log_dev( + "direct", + format!( + "using direct normalized bridge (functions={}, env_layouts={})", norm.functions.len(), norm.env_layouts.len() - ); - } + ), + false, + ); let mut mir_module = MirModule::new("joinir_normalized_direct".to_string()); @@ -42,9 +42,9 @@ pub(crate) fn lower_normalized_direct_minimal( } if debug_dump { - eprintln!( - "[joinir/normalized-bridge/direct] produced MIR (debug dump): {:#?}", - mir_module + super::log_debug( + "direct", + format!("produced MIR (debug dump): {:#?}", mir_module), ); } @@ -55,7 +55,6 @@ fn lower_normalized_function_direct( func: &JpFunction, norm: &NormalizedModule, ) -> Result { - let verbose = joinir_dev_enabled(); let env_fields = func .env_layout .and_then(|id| norm.env_layouts.iter().find(|layout| layout.id == id)); @@ -108,12 +107,16 @@ fn lower_normalized_function_direct( let mut current_insts: Vec = Vec::new(); let mut terminated = false; - if verbose { - eprintln!( - "[joinir/normalized-bridge/direct] lowering fn={} params={:?} remapped_params={:?} body_len={}", - func.name, params, remapped_params, func.body.len() - ); - } + super::log_debug( + "direct", + format!( + "lowering fn={} params={:?} remapped_params={:?} body_len={}", + func.name, + params, + remapped_params, + func.body.len() + ), + ); for inst in &func.body { if terminated { @@ -122,6 +125,56 @@ fn lower_normalized_function_direct( match inst { JpInst::Let { dst, op, args } => { + if matches!(op, JpOp::Select) { + let cond = remap(*args.get(0).unwrap_or(&ValueId(0)), &mut value_map); + let then_val = remap(*args.get(1).unwrap_or(&ValueId(0)), &mut value_map); + let else_val = remap(*args.get(2).unwrap_or(&ValueId(0)), &mut value_map); + let remapped_dst = remap(*dst, &mut value_map); + + let then_bb = BasicBlockId(next_block_id); + next_block_id += 1; + let else_bb = BasicBlockId(next_block_id); + next_block_id += 1; + let merge_bb = BasicBlockId(next_block_id); + next_block_id += 1; + + finalize_block( + &mut mir_func, + current_block_id, + mem::take(&mut current_insts), + MirInstruction::Branch { + condition: cond, + then_bb, + else_bb, + }, + None, + ); + + finalize_block( + &mut mir_func, + then_bb, + Vec::new(), + MirInstruction::Jump { target: merge_bb }, + None, + ); + finalize_block( + &mut mir_func, + else_bb, + Vec::new(), + MirInstruction::Jump { target: merge_bb }, + None, + ); + + current_block_id = merge_bb; + current_insts = vec![MirInstruction::Phi { + dst: remapped_dst, + inputs: vec![(then_bb, then_val), (else_bb, else_val)], + type_hint: None, + }]; + mir_func.next_value_id = value_map.len() as u32; + continue; + } + let remapped_dst = remap(*dst, &mut value_map); let remapped_args = remap_vec(args, &mut value_map); let mir_like = jp_op_to_mir_like(remapped_dst, op, &remapped_args)?; @@ -276,6 +329,17 @@ fn jp_op_to_mir_like( lhs: args.get(0).copied().unwrap_or(ValueId(0)), rhs: args.get(1).copied().unwrap_or(ValueId(0)), }), + JpOp::Select => { + let cond = args.get(0).copied().unwrap_or(ValueId(0)); + let then_val = args.get(1).copied().unwrap_or(ValueId(0)); + let else_val = args.get(2).copied().unwrap_or(ValueId(0)); + Ok(MirLikeInst::Select { + dst, + cond, + then_val, + else_val, + }) + } JpOp::BoxCall { box_name, method } => Ok(MirLikeInst::BoxCall { dst: Some(dst), box_name: box_name.clone(), diff --git a/src/tests/helpers/joinir_frontend.rs b/src/tests/helpers/joinir_frontend.rs index 60325999..1c75f52a 100644 --- a/src/tests/helpers/joinir_frontend.rs +++ b/src/tests/helpers/joinir_frontend.rs @@ -2,6 +2,7 @@ //! //! 目的: フィクスチャベースの AST→JoinIR テストを簡潔に書けるようにする +use crate::config::env::joinir_test_debug_enabled; use crate::mir::join_ir::frontend::AstToJoinIrLowerer; use crate::mir::join_ir::JoinModule; use crate::mir::join_ir_ops::JoinValue; @@ -22,7 +23,7 @@ impl JoinIrFrontendTestRunner { Self { fixture_path: fixture_path.to_string(), join_module: None, - debug_enabled: std::env::var("JOINIR_TEST_DEBUG").is_ok(), + debug_enabled: joinir_test_debug_enabled(), } } diff --git a/tests/normalized_joinir_min.rs b/tests/normalized_joinir_min.rs index f519172c..d97e6bb2 100644 --- a/tests/normalized_joinir_min.rs +++ b/tests/normalized_joinir_min.rs @@ -6,9 +6,14 @@ use nyash_rust::mir::join_ir::{ normalized_pattern2_to_structured, BinOpKind, ConstValue, JoinContId, JoinFuncId, JoinFunction, JoinInst, JoinIrPhase, JoinModule, MirLikeInst, }; -use nyash_rust::mir::join_ir::normalized::dev_env::NormalizedDevEnvGuard; +use nyash_rust::mir::join_ir::normalized::dev_env::{ + normalized_dev_enabled, test_ctx, NormalizedDevEnvGuard, NormalizedTestContext, +}; use nyash_rust::mir::join_ir::normalized::fixtures::{ build_jsonparser_atoi_structured_for_normalized_dev, + build_jsonparser_atoi_real_structured_for_normalized_dev, + build_jsonparser_parse_number_real_structured_for_normalized_dev, + build_jsonparser_skip_ws_real_structured_for_normalized_dev, build_jsonparser_skip_ws_structured_for_normalized_dev, build_pattern2_break_fixture_structured, build_pattern2_minimal_structured, }; @@ -16,10 +21,19 @@ use nyash_rust::mir::join_ir_runner::run_joinir_function; use nyash_rust::mir::join_ir_ops::JoinValue; use nyash_rust::mir::join_ir_vm_bridge::run_joinir_via_vm; use nyash_rust::mir::ValueId; +fn normalized_dev_test_ctx() -> NormalizedTestContext<'static> { + let ctx = test_ctx(); + assert!( + normalized_dev_enabled(), + "Phase 40: normalized_dev must be enabled for normalized_* tests (feature + NYASH_JOINIR_NORMALIZED_DEV_RUN=1)" + ); + ctx +} + fn assert_normalized_dev_ready() { assert!( - nyash_rust::config::env::normalized_dev_enabled(), - "Phase 33: normalized_dev must be enabled for this suite (feature + env)" + normalized_dev_enabled(), + "Phase 40: normalized_dev must be enabled for normalized_* tests (feature + NYASH_JOINIR_NORMALIZED_DEV_RUN=1)" ); } @@ -87,6 +101,7 @@ fn build_structured_pattern1() -> JoinModule { #[test] fn normalized_pattern1_minimal_smoke() { + let _ctx = normalized_dev_test_ctx(); let structured = build_structured_pattern1(); let normalized = normalize_pattern1_minimal(&structured); assert_eq!(normalized.phase, JoinIrPhase::Normalized); @@ -103,6 +118,7 @@ fn normalized_pattern1_minimal_smoke() { #[test] fn normalized_pattern1_roundtrip_structured_equivalent() { + let _ctx = normalized_dev_test_ctx(); let structured = build_structured_pattern1(); let normalized = normalize_pattern1_minimal(&structured); let reconstructed = normalized_pattern1_to_structured(&normalized); @@ -121,6 +137,7 @@ fn normalized_pattern1_roundtrip_structured_equivalent() { #[test] fn normalized_pattern1_exec_matches_structured() { + let _ctx = normalized_dev_test_ctx(); let structured = build_structured_pattern1(); let normalized = normalize_pattern1_minimal(&structured); let reconstructed = normalized_pattern1_to_structured(&normalized); @@ -136,6 +153,7 @@ fn normalized_pattern1_exec_matches_structured() { #[test] fn normalized_pattern1_exec_matches_structured_roundtrip_backup() { + let _ctx = normalized_dev_test_ctx(); let structured = build_structured_pattern1(); let normalized = normalize_pattern1_minimal(&structured); let reconstructed = normalized_pattern1_to_structured(&normalized); @@ -156,6 +174,7 @@ fn normalized_pattern1_exec_matches_structured_roundtrip_backup() { #[test] fn normalized_pattern2_roundtrip_structure() { + let _ctx = normalized_dev_test_ctx(); let structured = build_pattern2_minimal_structured(); let normalized = normalize_pattern2_minimal(&structured); assert_eq!(normalized.phase, JoinIrPhase::Normalized); @@ -175,8 +194,39 @@ fn normalized_pattern2_roundtrip_structure() { } } +#[test] +fn normalized_pattern2_jsonparser_parse_number_real_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_jsonparser_parse_number_real_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let cases = [ + ("42", 0, "42"), + ("123abc", 0, "123"), + ("9", 0, "9"), + ("abc", 0, ""), + ("xx7yy", 2, "7"), + ("007", 0, "007"), + ]; + + for (s, pos, expected) in cases { + let input = [JoinValue::Str(s.to_string()), JoinValue::Int(pos)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s); + assert_eq!( + dev, + JoinValue::Str(expected.to_string()), + "unexpected result for input '{}' (pos={}) (expected num_str)", + s, + pos + ); + } +} + #[test] fn normalized_pattern2_exec_matches_structured() { + let _ctx = normalized_dev_test_ctx(); let structured = build_pattern2_minimal_structured(); let normalized = normalize_pattern2_minimal(&structured); let reconstructed = normalized_pattern2_to_structured(&normalized); @@ -193,6 +243,7 @@ fn normalized_pattern2_exec_matches_structured() { #[test] #[should_panic(expected = "normalize_pattern2_minimal")] fn normalized_pattern2_rejects_non_pattern2_structured() { + let _ctx = normalized_dev_test_ctx(); // Pattern1 Structured module should be rejected by Pattern2 normalizer. let structured = build_structured_pattern1(); let _ = normalize_pattern2_minimal(&structured); @@ -200,6 +251,7 @@ fn normalized_pattern2_rejects_non_pattern2_structured() { #[test] fn normalized_pattern2_real_loop_roundtrip_structure() { + let _ctx = normalized_dev_test_ctx(); let structured = build_pattern2_break_fixture_structured(); let normalized = normalize_pattern2_minimal(&structured); let reconstructed = normalized_pattern2_to_structured(&normalized); @@ -221,6 +273,7 @@ fn normalized_pattern2_real_loop_roundtrip_structure() { #[test] fn normalized_pattern2_real_loop_exec_matches_structured() { + let _ctx = normalized_dev_test_ctx(); let structured = build_pattern2_break_fixture_structured(); let normalized = normalize_pattern2_minimal(&structured); let reconstructed = normalized_pattern2_to_structured(&normalized); @@ -246,6 +299,7 @@ fn normalized_pattern2_real_loop_exec_matches_structured() { #[test] fn normalized_pattern1_runner_dev_switch_matches_structured() { + let _ctx = normalized_dev_test_ctx(); let structured = build_structured_pattern1(); let entry = structured.entry.expect("structured entry required"); let input = [JoinValue::Int(7)]; @@ -259,6 +313,7 @@ fn normalized_pattern1_runner_dev_switch_matches_structured() { #[test] fn normalized_pattern2_runner_dev_switch_matches_structured() { + let _ctx = normalized_dev_test_ctx(); let structured = build_pattern2_break_fixture_structured(); let entry = structured.entry.expect("structured entry required"); let cases = [0, 1, 3, 5]; @@ -281,6 +336,7 @@ fn normalized_pattern2_runner_dev_switch_matches_structured() { #[test] fn normalized_pattern2_jsonparser_runner_dev_switch_matches_structured() { + let _ctx = normalized_dev_test_ctx(); let structured = build_jsonparser_skip_ws_structured_for_normalized_dev(); let entry = structured.entry.expect("structured entry required"); let cases = [0, 1, 2, 5]; @@ -297,6 +353,7 @@ fn normalized_pattern2_jsonparser_runner_dev_switch_matches_structured() { #[test] fn normalized_pattern2_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); let structured = build_pattern2_break_fixture_structured(); let entry = structured.entry.expect("structured entry required"); let cases = [0, 1, 3, 5]; @@ -319,6 +376,7 @@ fn normalized_pattern2_vm_bridge_direct_matches_structured() { #[test] fn normalized_pattern1_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); let structured = build_structured_pattern1(); let entry = structured.entry.expect("structured entry required"); let cases = [0, 5, 7]; @@ -335,6 +393,7 @@ fn normalized_pattern1_vm_bridge_direct_matches_structured() { #[test] fn normalized_pattern2_jsonparser_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); let structured = build_jsonparser_skip_ws_structured_for_normalized_dev(); let entry = structured.entry.expect("structured entry required"); let cases = [0, 1, 2, 5]; @@ -349,8 +408,37 @@ fn normalized_pattern2_jsonparser_vm_bridge_direct_matches_structured() { } } +#[test] +fn normalized_pattern2_jsonparser_skip_ws_real_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_jsonparser_skip_ws_real_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let cases = [ + (" abc", 0, 3), + ("abc", 0, 0), + (" \t\nx", 0, 3), + (" \t\nx", 2, 3), + ]; + + for (s, pos, expected) in cases { + let input = [JoinValue::Str(s.to_string()), JoinValue::Int(pos)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s); + assert_eq!( + dev, + JoinValue::Int(expected), + "unexpected result for input '{}' (pos={})", + s, + pos + ); + } +} + #[test] fn normalized_pattern2_jsonparser_atoi_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); let structured = build_jsonparser_atoi_structured_for_normalized_dev(); let entry = structured.entry.expect("structured entry required"); let cases = [ @@ -374,3 +462,48 @@ fn normalized_pattern2_jsonparser_atoi_vm_bridge_direct_matches_structured() { ); } } + +#[test] +fn normalized_pattern2_jsonparser_atoi_real_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_jsonparser_atoi_real_structured_for_normalized_dev(); + if nyash_rust::config::env::joinir_test_debug_enabled() { + eprintln!( + "[joinir/normalized-dev/test] structured jsonparser_atoi_real: {:#?}", + structured + ); + let normalized = normalize_pattern2_minimal(&structured); + eprintln!( + "[joinir/normalized-dev/test] normalized jsonparser_atoi_real: {:#?}", + normalized + ); + } + let entry = structured.entry.expect("structured entry required"); + let cases = [ + ("42", 42), + ("123abc", 123), + ("007", 7), + ("", 0), + ("abc", 0), + ("-42", -42), + ("+7", 7), + ("-0", 0), + ("-12x", -12), + ("+", 0), + ("-", 0), + ]; + + for (s, expected) in cases { + let input = [JoinValue::Str(s.to_string())]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s); + assert_eq!( + dev, + JoinValue::Int(expected), + "unexpected result for input '{}'", + s + ); + } +}