feat(joinir): Phase 248 - Normalized JoinIR infrastructure

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 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-12 03:15:45 +09:00
parent 59caf5864c
commit ed8e2d3142
32 changed files with 1559 additions and 421 deletions

View File

@ -24,9 +24,11 @@
- Pattern3/4 の公開 API を `can_lower + lower` の最小セットに整理し、内部 helper を箱の中に閉じた。 - Pattern3/4 の公開 API を `can_lower + lower` の最小セットに整理し、内部 helper を箱の中に閉じた。
- `loop_pattern_detection` の classify() が代表ループを P1〜P4 に分類することをユニットテストで固定。 - `loop_pattern_detection` の classify() が代表ループを P1〜P4 に分類することをユニットテストで固定。
- Phase 245-EX / 245C: - 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 に載せるようにした。 - FunctionScopeCapture / CapturedEnv を拡張し、関数パラメータ(例: `s`, `len`)をループ条件/本体で読み取り専用なら CapturedEnv 経由で ConditionEnv に載せるようにした。
- これにより、`p < s.length()` のような header 条件や JsonParser 系ループでのパラメータ参照が、ExprLowerer/ScopeManager から安全に解決される。 - これにより、`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: - Phase 247-EX:
- DigitPos promotion を二重値化し、`digit_pos` から boolean carrier `is_digit_pos`ConditionOnlyと integer carrier `digit_value`LoopStateを生成。 - 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 パターンが安全に利用可能に。 - 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は不変。 - P1/P2 ミニ + JsonParser skip_ws/atoi ミニを Normalized→MIR 直ブリッジで実行できるようにし、normalized_dev ON 時は Structured→Normalized→MIR復元なし経路との比較テストで結果一致を固定。既定経路Structured→MIRは不変。
- Phase 36-NORM-BRIDGE-DIRECTdev-only: - Phase 36-NORM-BRIDGE-DIRECTdev-only:
- Normalized ブリッジを direct 実装Normalized→MIRと Structured 再構成に分離し、shape_guard で P1/P2 ミニ + JsonParser skip_ws/atoi ミニだけ direct 経路に通すよう整理。非対応は `[joinir/normalized-bridge/fallback]` ログ付きで再構成に落とし、テストで direct/従来経路の VM 出力一致を固定。 - Normalized ブリッジを direct 実装Normalized→MIRと Structured 再構成に分離し、shape_guard で P1/P2 ミニ + JsonParser skip_ws/atoi ミニだけ direct 経路に通すよう整理。非対応は `[joinir/normalized-bridge/fallback]` ログ付きで再構成に落とし、テストで direct/従来経路の VM 出力一致を固定。
- Phase 37-NORM-JP-REALdev-only:
- JsonParser `_skip_whitespace` 本体の P2 ループを Program(JSON) フィクスチャで Structured→Normalized→MIR(direct) に通し、Structured 直経路との VM 出力一致を比較するテストを追加。`extract_value``&&`/`||` を BinOp として扱えるようにし、Break パターンの param 推定を柔軟化して real 形状でも panic しないようにした。
- Phase 38-NORM-OBSdev-only:
- Normalized/JoinIR dev 経路のログカテゴリを `[joinir/normalized-bridge/*]` / `[joinir/normalized-dev/shape]` に統一し、`JOINIR_TEST_DEBUG` 下だけ詳細を出すよう静音化。Verifier/FailFast メッセージも shape/役割付きに整え、デバッグ観測性を上げつつ通常実行のノイズを減らした。
- Phase 43-Adev-only:
- JsonParser `_atoi` 本体の Program(JSON) フィクスチャを normalized_dev に追加し、Structured→Normalized→MIR(direct) と Structured→MIR の VM 出力を比較するテストで一致を固定(符号あり/なしの簡易パス対応。canonical 切替は後続フェーズ)。
- Phase 43-Cdev-only:
- JsonParser `_parse_number` 本体の Program(JSON) フィクスチャを normalized_dev に追加し、Structured→Normalized→MIR(direct) と Structured→MIR の VM 出力を比較するテストで一致を固定num_str は現状仕様のまま据え置き、P2-Mid の足慣らし)。
### 1. いまコード側で意識しておきたいフォーカス ### 1. いまコード側で意識しておきたいフォーカス

View File

@ -987,7 +987,7 @@ JoinIR は Rust 側だけでなく、将来的に .hako selfhost コンパイラ
- [x] 退行なし: Phase 190-196 テスト全 PASS ✅ - [x] 退行なし: Phase 190-196 テスト全 PASS ✅
- 詳細: phase197-lightweight-loops-deployment.md - 詳細: 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 | | Function | Pattern | Status | Note |
|----------|---------|--------|------| |----------|---------|--------|------|
@ -998,15 +998,15 @@ JoinIR は Rust 側だけでなく、将来的に .hako selfhost コンパイラ
| `phase195_sum_count` | P3 | ✅ JoinIR OK | Phase 196 検証済みmulti-carrier| | `phase195_sum_count` | P3 | ✅ JoinIR OK | Phase 196 検証済みmulti-carrier|
| `loop_if_phi` | P3 | ✅ JoinIR OK | Phase 196 検証済みsingle-carrier| | `loop_if_phi` | P3 | ✅ JoinIR OK | Phase 196 検証済みsingle-carrier|
| `loop_min_while` | P1 | ✅ JoinIR OK | Phase 165 基本検証済み | | `loop_min_while` | P1 | ✅ JoinIR OK | Phase 165 基本検証済み |
| `_parse_number` | P2 | ⚠️ Deferred | ConditionEnv 制約Phase 200+| | `_parse_number` | P2 | ✅ JoinIR OK | Phase 245B-IMPL: P2-Midnum_str LoopState キャリア)を Structured→Normalized(dev, direct) で固定 |
| `_atoi` | P2 | ⚠️ Deferred | ConditionEnv 制約Phase 200+| | `_atoi` | P2 | ✅ JoinIR OK | Phase 246-EX で NumberAccumulation パターンとして統合P2-Mid、Normalized: mini + real(dev, 符号対応) / canonical 準備中|
| `_parse_string` | P3 | ⚠️ Deferred | 複雑キャリアPhase 195+ 拡張後)| | `_parse_string` | P3 | ⚠️ Deferred | 複雑キャリアPhase 195+ 拡張後)|
| `_unescape_string` | P3 | ⚠️ Deferred | 複雑キャリアPhase 195+ 拡張後)| | `_unescape_string` | P3 | ⚠️ Deferred | 複雑キャリアPhase 195+ 拡張後)|
| `_parse_array` | - | ⚠️ Deferred | 複数 MethodCallPhase 195+| | `_parse_array` | - | ⚠️ Deferred | 複数 MethodCallPhase 195+|
| `_parse_object` | - | ⚠️ Deferred | 複数 MethodCallPhase 195+| | `_parse_object` | - | ⚠️ Deferred | 複数 MethodCallPhase 195+|
**Coverage**: 7/13 ループ JoinIR 対応済み(54% **Coverage**: 9/13 ループ JoinIR 対応済み(約 69%
**Verification**: 4/7 ループ E2E PASS、3/7 structural/routing 確認済み **Verification**: 代表ループP1/P2 Core + Trim/P3については E2E テストで挙動確認済み。詳細なケースごとの状況は各 Phase ドキュメントPhase 197/245/246 など)を参照。
8. **JsonParser 残り複雑ループへの適用Phase 198+, 200+** 8. **JsonParser 残り複雑ループへの適用Phase 198+, 200+**
- Phase 200+: ConditionEnv 拡張 (function-scoped variables) → _parse_number, _atoi - 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 直接生成に切り替えた。 - 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 出力が従来経路と一致することを固定。 - 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 ログ/FailFast の整備
- Normalized/JoinIR dev 経路のログカテゴリを `[joinir/normalized-bridge/*]` / `[joinir/normalized-dev/shape]` に統一し、`JOINIR_TEST_DEBUG` フラグ下のみ詳細を出すよう静音化。Verifier/FailFast メッセージも 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 は比較テスト用/フォールバック用の位置づけにするFailFast ポリシーは維持)。
- `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 3641 で **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-Adev 専用): `_atoi` 本体を Program(JSON) フィクスチャ `jsonparser_atoi_real` で Structured→Normalized→MIR(direct) に通し、Structured 直経路との VM 出力一致を比較テストで固定(符号あり/なしの簡易パスまで対応。canonical 化は後続フェーズで検討)。
- Phase 43-Cdev 専用): `_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 で設計し、後続フェーズで実装に反映する計画を置いておく。

View File

@ -1,5 +1,8 @@
# Phase 181: JsonParser 残りループ設計調査 # 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ループを詳細に分析し、 JsonParser`tools/hako_shared/json_parser.hako`の全11ループを詳細に分析し、
@ -201,6 +204,34 @@ loop(i < len) {
**実行可能性**: Phase 182+ で実装可能Pattern2 Break_atoi の後継 **実行可能性**: 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**: 既に NormalizedMIR(direct) まで実装されPhase 41 canonical route既定経路として扱っているループ群
- **P2-Mid**: JoinIR(Structured) には載っているがNormalized はこれから本格対応する次候補のループ群
- **P2-Heavy**: MethodCall 多数複雑キャリアなどの理由でNormalized 対応は Phase 43 以降に送っている重めのループ群
### 2. JsonParser ループの現在ステータス202512 時点)
| # | ループ | Pattern | P2 クラス | JoinIR 状態 | Normalized 状態 | 備考 |
|----|--------|---------|-----------|-------------|-----------------|------|
| 1 | _skip_whitespace | P2 / P5 Trim | P2-Core | JoinIR OKPhase 173, 197, 245 | NormalizedMIR(direct) / canonicalPhase 37, 41 | mini / real の両方をフィクスチャ化して dev / canonical で比較済み |
| 2 | _trim (leading) | P2 / P5 Trim | P2-Heavy | JoinIR OKTrimLoopHelper 経由 | 未対応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 OKPhase 245-EX | dev NormalizedMIR(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 OKPhase 246-EX | dev NormalizedMIR(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 3541 セクションを参照してね
## Pattern × Box マトリクスJsonParser全体 ## Pattern × Box マトリクスJsonParser全体
``` ```

View File

@ -1,5 +1,8 @@
# Phase 223-1: LoopBodyLocal in Condition - Comprehensive Inventory # Phase 223-1: LoopBodyLocal in Condition - Comprehensive Inventory
Status: HistoricalPhase 26-H 以降の Normalized / DigitPos 導入で一部内容が古くなっています)
Note: LoopBodyLocal が原因で Fail-Fast していたループの在庫を Phase 223 時点で一覧化したメモだよ。DigitPos 系などの一部ループはその後の Phase 224/26-H/34 系で解消済みなので、最新の対応状況は `joinir-architecture-overview.md` と Phase 42 の P2 インベントリを合わせて参照してね。
## Purpose ## 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). 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).

View File

@ -1,5 +1,8 @@
# Phase 224-E: DigitPos Condition Normalizer # Phase 224-E: DigitPos Condition Normalizer
Status: ActiveDigitPos 条件正規化ラインの設計メモ / 実装ガイド)
Scope: digit_pos → is_digit_pos Carrier / ConditionEnv / ExprLowerer の正規化経路の SSOT ドキュメントだよ。Phase 26H / 34 系の JsonParser `_parse_number` / `_atoi` でもこの設計を前提にしている。
## Problem Statement ## Problem Statement
### Background ### Background

View File

@ -1,5 +1,6 @@
Status: Draft Status: Implemented (Phase 245B-IMPL)
Scope: `_parse_number``num_str` を Pattern2/P5 のキャリアとして扱うかどうかを決める設計フェーズ(コード変更なし)。 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` キャリア設計 # Phase 245B: JsonParser `_parse_number` の `num_str` キャリア設計

View File

@ -1,8 +1,9 @@
# Phase 245B: num_str Carrier Design Document # Phase 245B: num_str Carrier Design Document
**Status**: Design Phase **Status**: Implemented (Phase 245B-IMPL)
**Target**: `_parse_number` loop の `num_str` 文字列キャリア対応 **Target**: `_parse_number` loop の `num_str` 文字列キャリア対応
**Scope**: Pattern 2 (loop with break) + 既存インフラ活用 **Scope**: Pattern 2 (loop with break) + 既存インフラ活用
**Notes**: jsonparser_parse_number_real フィクスチャを Structured→Normalized→MIR(direct) で実装し、`num_str = num_str + ch` を LoopState キャリアとして dev テスト固定済み。
--- ---

View File

@ -0,0 +1,148 @@
Status: Planned
Scope: Phase 43NORMCANONP2MID — 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 43NORMCANONP2MID 設計メモJsonParser P2Mid 向け Normalized 拡張)
## 0. ゴールと前提
- ゴール
- JsonParser の本命 P2 ループP2Midを、既存の Normalized インフラの延長で扱えるようにするための設計方針を固める。
- 特に `_parse_number` / `_atoi` 本体 / `_atof_loop` について、
- どのキャリア・bodylocal を EnvLayout に載せるか
- どの JpInst / JpOp パターンが追加で必要か
- StepScheduleBox / DigitPos / NumberAccumulation との責務分担
を整理する。
- 前提
- P2CoreP2 ミニ + JP skip_ws mini/real + JP atoi miniは Phase 41 までで Normalized→MIR(direct) が canonical 済み。
- P2Mid は **JoinIR(Structured) までは載っているが Normalized は未対応** の状態Phase 245/246 系)。
- 本メモは「設計レベル」で止め、実装・テスト追加は後続フェーズ43 実装回)で扱う。
---
## 1. 対象ループと P2Mid クラスの整理
- 対象とする P2MidJsonParser 側)
- `_parse_number`(数値文字列の収集 + digit_pos break
- `_atoi` 本体(範囲チェック + digit_pos + NumberAccumulation
- `_atof_loop`(構造的には `_atoi` と同型の浮動小数点版)
- すべて Pattern2 Break で、既に JoinIR(Structured) には載っている:
- `_parse_number` → Phase 245EX で header / break / `p` 更新を Pattern2 に統合済み(`num_str` は当面対象外)。
- `_atoi` → Phase 246EX で DigitPos dual 値 + NumberAccumulation パターンとして JoinIR 経路に統合済み。
- `_atof_loop` → 設計上 `_atoi` と同型とみなし、P2Mid クラスに含める。
- P2Core との差分
- P2 ミニ / skip_ws / atoi ミニに比べて:
- Carrier の本数(`p` + `result` + 場合によっては `num_str`)が増える。
- bodylocal / 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 / フィールド設計
- P2Mid で 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 LoopStateDigitPos 系)
- P2Core で既に導入済みの `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 のみ)
- P2Mid で追加検証・明文化が必要なパターン
- `_parse_number`
- `substring` / `indexOf` の BoxCall パターンP2Core でも使用済みだが、`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 で導入した StepScheduleBoxPattern2 用は、P2Mid に対しても「どの StepKind をどの順番で評価するか」を決める SSOT として使う。
- `_parse_number` / `_atoi` 本体 / `_atof_loop` では、以下のようなフラグを想定:
- `has_digitpos_body_local`DigitPos 二重値を使うか)
- `has_number_accumulation`(結果キャリアに Mul+Add があるか)
- `has_bodylocal_break`break 条件が bodylocal に依存するか)
- StepScheduleBox は、これらのフラグだけを見て
- 標準 P2: `[HeaderCond, BreakCheck, BodyInit, Updates, Tail]`
- DigitPos / atoi 系: `[HeaderCond, BodyInit, BreakCheck, Updates, Tail]`
など、評価順のバリエーションを返す「薄い箱」のまま保つ。
- Pattern2 lowerer / Normalized 変換側は、この StepSchedule に従って
- header 条件
- bodylocal initDigitPos / Range check 等)
- break 条件
- carrier 更新NumberAccumulation / i++ 等)
を「並べるだけ」にし、条件式の詳細や bodylocal の構造には踏み込まない。
### 3.2 DigitPos / NumberAccumulation との接続
- DigitPos 系
- `digit_pos``is_digit_pos` / `digit_value` の二重値設計は、P2Core と同じく「LoopState キャリアFromHost なし)」として EnvLayout に載せる。
- ExitBinding には出さず、Normalized→MIR 直ブリッジでも Loop 内部だけで完結させる。
- Break 条件 `digit_pos < 0` は Phase 224 の DigitPosConditionNormalizerAST→`!is_digit_pos`を前提にし、Normalized 側は `!is_digit_pos` という bool 条件だけを受け取る。
- NumberAccumulation 系
- Structured 側の LoopUpdateAnalyzer / CarrierUpdateEmitter が `result = result * 10 + digit_value` パターンを `UpdateRhs::NumberAccumulation` として検出・JoinIR 生成済み。
- Normalized→MIR 直ブリッジは、P2Core と同じ Mul + Add シーケンスで MIR を吐く設計を維持し、P2Mid でも追加ロジックを増やさずに流用する。
---
## 4. Bridge / ShapeGuard / canonical 切り替え方針
- ShapeGuard 拡張
- 既存の P2Core 判定P2 ミニ / skip_ws mini/real / atoi miniに加えて、
- `_parse_number` 本体
- `_atoi` 本体
- `_atof_loop`
を P2Mid として検出できる Shape 種別を追加する(例: `ShapeKind::JsonparserParseNumber`, `ShapeKind::JsonparserAtoiCore`)。
- Phase 43 実装フェーズでは、まず P2Mid ループを **dev only** の Normalized 対象にするcanonical 切り替えは Phase 43 後半〜Phase 44 相当で検討)。
- Bridge 側の経路
- `bridge_joinir_to_mir` の入口で:
- P2Core: 既に canonical Normalized→MIR(direct)Phase 41 の状態を維持)。
- P2Mid: `normalized_dev_enabled()` が true のときに限り Structured→Normalized→MIR(direct) を試し、テストで Structured 直経路と比較。
- FailFast 方針
- P2Mid では「Normalized が未対応の領域」がまだ多いため、
- dev / debug ビルドでは invariant 破壊・未対応命令で panicNormalized 実装の穴を早期検出)。
- 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 の役割分担が本メモの通りに整理されている。
- P2Mid`_parse_number` / `_atoi` 本体 / `_atof_loop`)に対して、どこまでを Phase 43 で扱い、どこから先を後続フェーズに回すかの線引きが明文化されている。
- `joinir-architecture-overview.md` の Phase 43 セクション3.20)と、この設計メモの内容が矛盾していない。

View File

@ -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) /// Phase 82: NYASH_PHI_FALLBACK_DISABLED=1 - Disable if_phi fallback (dev mode)
/// ///
/// lifecycle.rs の infer_type_from_phi* callsite を封じて、 /// lifecycle.rs の infer_type_from_phi* callsite を封じて、

View File

@ -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::join_value_space::JoinValueSpace;
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; 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_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::function_scope_capture::CapturedEnv;
use crate::mir::loop_pattern_detection::error_messages; use crate::mir::loop_pattern_detection::error_messages;
use crate::mir::ValueId; use crate::mir::ValueId;
use std::collections::BTreeMap;
fn log_pattern2(verbose: bool, tag: &str, message: impl AsRef<str>) { fn log_pattern2(verbose: bool, tag: &str, message: impl AsRef<str>) {
if verbose { if verbose {
@ -707,19 +709,13 @@ impl MirBuilder {
); );
let original_carrier_count = inputs.carrier_info.carriers.len(); let original_carrier_count = inputs.carrier_info.carriers.len();
inputs.carrier_info.carriers.retain(|carrier| { filter_carriers_for_updates(&mut inputs.carrier_info, &carrier_updates);
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
});
log_pattern2( log_pattern2(
verbose, verbose,
"updates", "updates",
format!( 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, original_carrier_count,
inputs.carrier_info.carriers.len() inputs.carrier_info.carriers.len()
), ),
@ -858,6 +854,19 @@ impl MirBuilder {
} }
} }
/// 更新を持たない FromHost キャリアを落とすヘルパー。
fn filter_carriers_for_updates(
carrier_info: &mut CarrierInfo,
carrier_updates: &BTreeMap<String, UpdateExpr>,
) {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -138,6 +138,8 @@ impl AstToJoinIrLowerer {
"-" => BinOpKind::Sub, "-" => BinOpKind::Sub,
"*" => BinOpKind::Mul, "*" => BinOpKind::Mul,
"/" => BinOpKind::Div, "/" => BinOpKind::Div,
"&&" => BinOpKind::And,
"||" => BinOpKind::Or,
_ => panic!("Unsupported binary op: {}", op_str), _ => panic!("Unsupported binary op: {}", op_str),
}; };

View File

@ -24,10 +24,10 @@ use super::common::{
build_join_module, create_k_exit_function, create_loop_context, parse_program_json, build_join_module, create_k_exit_function, create_loop_context, parse_program_json,
process_local_inits, process_local_inits,
}; };
use super::param_guess::{build_param_order, compute_param_guess};
use super::{AstToJoinIrLowerer, JoinModule, LoweringError}; use super::{AstToJoinIrLowerer, JoinModule, LoweringError};
use crate::mir::join_ir::{JoinFunction, JoinInst}; use crate::mir::join_ir::{JoinFunction, JoinInst};
use crate::mir::ValueId; use crate::mir::ValueId;
use std::collections::BTreeSet;
/// Break パターンを JoinModule に変換 /// Break パターンを JoinModule に変換
/// ///
@ -70,7 +70,10 @@ pub fn lower(
let break_cond_expr = &break_if_stmt["cond"]; 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(&param_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"]; let loop_cond_expr = &loop_node["cond"];
// 5. entry 関数を生成 // 5. entry 関数を生成
@ -245,48 +248,3 @@ fn create_loop_step_function_break(
exit_cont: None, 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<String> =
[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)
}

View File

@ -13,6 +13,7 @@
//! - `create_k_exit_function()`: k_exit 関数生成 //! - `create_k_exit_function()`: k_exit 関数生成
use super::{AstToJoinIrLowerer, JoinModule}; use super::{AstToJoinIrLowerer, JoinModule};
use super::super::stmt_handlers::StatementEffect;
use crate::mir::join_ir::JoinIrPhase; use crate::mir::join_ir::JoinIrPhase;
use crate::mir::join_ir::{JoinFuncId, JoinFunction, JoinInst}; use crate::mir::join_ir::{JoinFuncId, JoinFunction, JoinInst};
use crate::mir::ValueId; 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"); let stmt_type = stmt["type"].as_str().expect("Statement must have type");
match stmt_type { match stmt_type {
"Local" => { "Local" | "Assignment" | "If" => {
let var_name = stmt["name"] let (insts, effect) = lowerer.lower_statement(stmt, ctx);
.as_str()
.expect("Local must have 'name'")
.to_string();
let expr = &stmt["expr"];
// extract_value で式を評価
let (var_id, insts) = lowerer.extract_value(expr, ctx);
init_insts.extend(insts); init_insts.extend(insts);
// 同名再宣言 = var_map を更新(再代入の意味論) if let StatementEffect::VarUpdate { name, value_id } = effect {
ctx.register_param(var_name, var_id); 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), _ => panic!("Unexpected statement type before Loop: {}", stmt_type),
} }

View File

@ -19,6 +19,7 @@ pub mod break_pattern;
pub mod common; pub mod common;
pub mod continue_pattern; pub mod continue_pattern;
pub mod filter; pub mod filter;
pub mod param_guess;
pub mod print_tokens; pub mod print_tokens;
pub mod simple; pub mod simple;

View File

@ -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<String> = 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
}

View File

@ -54,13 +54,21 @@ impl AstToJoinIrLowerer {
pub(crate) fn has_break_in_loop_body(loop_body: &[serde_json::Value]) -> bool { pub(crate) fn has_break_in_loop_body(loop_body: &[serde_json::Value]) -> bool {
loop_body.iter().any(|stmt| { loop_body.iter().any(|stmt| {
if stmt["type"].as_str() == Some("If") { if stmt["type"].as_str() == Some("If") {
if let Some(then_body) = stmt["then"].as_array() { let then_has = stmt["then"]
then_body .as_array()
.iter() .map(|body| {
body.iter()
.any(|s| s["type"].as_str() == Some("Break")) .any(|s| s["type"].as_str() == Some("Break"))
} else { })
false .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 { } else {
false false
} }
@ -71,13 +79,21 @@ impl AstToJoinIrLowerer {
pub(crate) fn has_continue_in_loop_body(loop_body: &[serde_json::Value]) -> bool { pub(crate) fn has_continue_in_loop_body(loop_body: &[serde_json::Value]) -> bool {
loop_body.iter().any(|stmt| { loop_body.iter().any(|stmt| {
if stmt["type"].as_str() == Some("If") { if stmt["type"].as_str() == Some("If") {
if let Some(then_body) = stmt["then"].as_array() { let then_has = stmt["then"]
then_body .as_array()
.iter() .map(|body| {
body.iter()
.any(|s| s["type"].as_str() == Some("Continue")) .any(|s| s["type"].as_str() == Some("Continue"))
} else { })
false .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 { } else {
false false
} }

View File

@ -52,24 +52,25 @@ enum FunctionRoute {
} }
fn resolve_function_route(func_name: &str) -> Result<FunctionRoute, String> { fn resolve_function_route(func_name: &str) -> Result<FunctionRoute, String> {
const IF_RETURN_NAMES: &[&str] = &["test", "local", "_read_value_from_pair"]; const TABLE: &[(&str, FunctionRoute)] = &[
const LOOP_NAMES: &[&str] = &[ ("test", FunctionRoute::IfReturn),
"simple", ("local", FunctionRoute::IfReturn),
"filter", ("_read_value_from_pair", FunctionRoute::IfReturn),
"print_tokens", ("simple", FunctionRoute::LoopFrontend),
"map", ("filter", FunctionRoute::LoopFrontend),
"reduce", ("print_tokens", FunctionRoute::LoopFrontend),
"fold", ("map", FunctionRoute::LoopFrontend),
"jsonparser_skip_ws_mini", ("reduce", FunctionRoute::LoopFrontend),
"jsonparser_atoi_mini", ("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) { if let Some((_, route)) = TABLE.iter().find(|(name, _)| *name == func_name) {
return Ok(FunctionRoute::IfReturn); return Ok(*route);
}
if LOOP_NAMES.contains(&func_name) {
return Ok(FunctionRoute::LoopFrontend);
} }
if func_name == "parse_loop" { if func_name == "parse_loop" {

View File

@ -58,7 +58,6 @@
use crate::ast::ASTNode; use crate::ast::ASTNode;
mod boundary_builder; mod boundary_builder;
mod header_break_lowering; mod header_break_lowering;
mod step_schedule;
#[cfg(test)] #[cfg(test)]
mod tests; 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_body_local_env::LoopBodyLocalEnv;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; 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::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::lowering::update_env::UpdateEnv;
use crate::mir::join_ir::{ use crate::mir::join_ir::{
BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst, BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst,
@ -84,7 +86,6 @@ use crate::mir::ValueId;
use boundary_builder::build_fragment_meta; use boundary_builder::build_fragment_meta;
use header_break_lowering::{lower_break_condition, lower_header_condition}; use header_break_lowering::{lower_break_condition, lower_header_condition};
use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism 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 /// 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={:?}", "[joinir/pattern2] Phase 201: loop_step params - i_param={:?}, carrier_params={:?}",
i_param, carrier_param_ids 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::<Vec<_>>()
);
}
// Phase 169 / Phase 171-fix / Phase 240-EX / Phase 244: Lower condition // Phase 169 / Phase 171-fix / Phase 240-EX / Phase 244: Lower condition
let (cond_value, mut cond_instructions) = lower_header_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); 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. // Decide evaluation order (header/body-init/break/updates/tail) up-front.
let schedule = let schedule_ctx =
Pattern2StepSchedule::for_pattern2(body_local_env.as_ref().map(|env| &**env), carrier_info); Pattern2ScheduleContext::from_env(body_local_env.as_ref().map(|env| &**env), carrier_info);
let schedule_desc: Vec<&str> = schedule let schedule = build_pattern2_schedule(&schedule_ctx);
.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()
);
// Collect fragments per step; append them according to the schedule below. // Collect fragments per step; append them according to the schedule below.
let mut header_block: Vec<JoinInst> = Vec::new(); let mut header_block: Vec<JoinInst> = Vec::new();
@ -661,11 +662,11 @@ pub(crate) fn lower_loop_with_break_minimal(
// Apply scheduled order to assemble the loop_step body. // Apply scheduled order to assemble the loop_step body.
for step in schedule.iter() { for step in schedule.iter() {
match step { match step {
Pattern2Step::HeaderAndNaturalExit => loop_step_func.body.append(&mut header_block), Pattern2StepKind::HeaderCond => loop_step_func.body.append(&mut header_block),
Pattern2Step::BodyLocalInit => loop_step_func.body.append(&mut body_init_block), Pattern2StepKind::BodyInit => loop_step_func.body.append(&mut body_init_block),
Pattern2Step::BreakCondition => loop_step_func.body.append(&mut break_block), Pattern2StepKind::BreakCheck => loop_step_func.body.append(&mut break_block),
Pattern2Step::CarrierUpdates => loop_step_func.body.append(&mut carrier_update_block), Pattern2StepKind::Updates => loop_step_func.body.append(&mut carrier_update_block),
Pattern2Step::TailCall => loop_step_func.body.append(&mut tail_block), Pattern2StepKind::Tail => loop_step_func.body.append(&mut tail_block),
} }
} }

View File

@ -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<Pattern2Step>,
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<Item = Pattern2Step> + '_ {
self.steps.iter().copied()
}
pub(crate) fn reason(&self) -> &'static str {
self.reason
}
}

View File

@ -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_break_minimal; // Phase 188-Impl-2: Pattern 2 minimal lowerer
pub mod loop_with_continue_minimal; pub mod loop_with_continue_minimal;
pub mod method_call_lowerer; // Phase 224-B: MethodCall lowering (metadata-driven) 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 method_return_hint; // Phase 83: P3-D 既知メソッド戻り値型推論箱
pub mod scope_manager; // Phase 231: Unified variable scope management // Phase 195: Pattern 4 minimal lowerer 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 // Phase 242-EX-A: loop_with_if_phi_minimal removed - replaced by loop_with_if_phi_if_sum

View File

@ -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<Pattern2StepKind>,
reason: &'static str,
}
impl Pattern2StepSchedule {
pub(crate) fn iter(&self) -> impl Iterator<Item = Pattern2StepKind> + '_ {
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::<Vec<_>>()
.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<CarrierVar>) -> 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");
}
}

View File

@ -51,8 +51,6 @@ pub use normalized::{
normalize_pattern1_minimal, normalize_pattern2_minimal, normalized_pattern1_to_structured, normalize_pattern1_minimal, normalize_pattern2_minimal, normalized_pattern1_to_structured,
normalized_pattern2_to_structured, NormalizedModule, normalized_pattern2_to_structured, NormalizedModule,
}; };
#[cfg(feature = "normalized_dev")]
pub use normalized::fixtures;
pub use verify::verify_progress_for_skip_ws; pub use verify::verify_progress_for_skip_ws;
// Phase 200-3: Contract verification functions are in merge/mod.rs (private module access) // Phase 200-3: Contract verification functions are in merge/mod.rs (private module access)

View File

@ -93,6 +93,8 @@ pub enum JpOp {
Unary(UnaryOp), Unary(UnaryOp),
Compare(CompareOp), Compare(CompareOp),
BoxCall { box_name: String, method: String }, BoxCall { box_name: String, method: String },
/// 三項演算子cond ? then : else
Select,
} }
/// Normalized JoinIR モジュール(テスト専用)。 /// Normalized JoinIR モジュール(テスト専用)。
@ -115,7 +117,7 @@ impl NormalizedModule {
#[cfg(feature = "normalized_dev")] #[cfg(feature = "normalized_dev")]
fn verify_normalized_pattern1(module: &NormalizedModule) -> Result<(), String> { fn verify_normalized_pattern1(module: &NormalizedModule) -> Result<(), String> {
if module.phase != JoinIrPhase::Normalized { 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 // Env field bounds check
@ -127,7 +129,7 @@ fn verify_normalized_pattern1(module: &NormalizedModule) -> Result<(), String> {
JpInst::EnvLoad { field, .. } | JpInst::EnvStore { field, .. } => { JpInst::EnvLoad { field, .. } | JpInst::EnvStore { field, .. } => {
if *field >= field_count { if *field >= field_count {
return Err(format!( return Err(format!(
"Env field out of range: {} (fields={})", "[joinir/normalized-dev] pattern1: env field out of range: {} (fields={})",
field, field_count field, field_count
)); ));
} }
@ -156,7 +158,7 @@ fn verify_normalized_pattern1(module: &NormalizedModule) -> Result<(), String> {
JpInst::TailCallFn { .. } | JpInst::TailCallKont { .. } | JpInst::If { .. } => {} JpInst::TailCallFn { .. } | JpInst::TailCallKont { .. } | JpInst::If { .. } => {}
_ => { _ => {
return Err(format!( 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 func.name
)) ))
} }
@ -221,6 +223,12 @@ pub fn normalized_pattern1_to_structured(norm: &NormalizedModule) -> JoinModule
op: *op, op: *op,
operand: args.get(0).copied().unwrap_or(ValueId(0)), 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 { JpOp::Compare(op) => func.body.push(JoinInst::Compute(MirLikeInst::Compare {
dst: *dst, dst: *dst,
op: *op, op: *op,
@ -300,8 +308,30 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule {
let mut max = 3; let mut max = 3;
#[cfg(feature = "normalized_dev")] #[cfg(feature = "normalized_dev")]
{ {
if shape_guard::is_jsonparser_atoi_mini(structured) { let shapes = shape_guard::supported_shapes(structured);
max = 8; 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 max
@ -397,6 +427,18 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule {
args: vec![*lhs, *rhs], 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 } => { JoinInst::Jump { cont, args, cond } => {
if let Some(cond_val) = cond { if let Some(cond_val) = cond {
body.push(JpInst::If { 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, .. } => { JoinInst::Call { func, args, k_next, .. } => {
if k_next.is_none() { if k_next.is_none() {
body.push(JpInst::TailCallFn { body.push(JpInst::TailCallFn {
@ -523,6 +578,14 @@ pub fn normalized_pattern2_to_structured(norm: &NormalizedModule) -> JoinModule
op: *op, op: *op,
operand: args.get(0).copied().unwrap_or(ValueId(0)), 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 { JpOp::Compare(op) => func.body.push(JoinInst::Compute(MirLikeInst::Compare {
dst: *dst, dst: *dst,
op: *op, op: *op,
@ -578,7 +641,9 @@ fn verify_normalized_pattern2(
max_env_fields: usize, max_env_fields: usize,
) -> Result<(), String> { ) -> Result<(), String> {
if module.phase != JoinIrPhase::Normalized { 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<u32, usize> = HashMap::new(); let mut layout_sizes: HashMap<u32, usize> = HashMap::new();
@ -586,7 +651,7 @@ fn verify_normalized_pattern2(
let size = layout.fields.len(); let size = layout.fields.len();
if !(1..=max_env_fields).contains(&size) { if !(1..=max_env_fields).contains(&size) {
return Err(format!( return Err(format!(
"Normalized Pattern2 expects 1..={} env fields, got {}", "[joinir/normalized-dev] pattern2: expected 1..={} env fields, got {}",
max_env_fields, size max_env_fields, size
)); ));
} }
@ -615,7 +680,10 @@ fn verify_normalized_pattern2(
| JpInst::If { env, .. } => { | JpInst::If { env, .. } => {
if let Some(expected) = expected_env_len { if let Some(expected) = expected_env_len {
if env.is_empty() { 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; let _ = expected;
} }
@ -631,7 +699,7 @@ fn verify_normalized_pattern2(
| JpInst::If { .. } => {} | JpInst::If { .. } => {}
_ => { _ => {
return Err(format!( 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 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()); 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 { for shape in shapes {
if verbose { if debug {
eprintln!("[joinir/normalized-dev] attempting {:?} normalization", shape); eprintln!(
"[joinir/normalized-dev/roundtrip] attempting {:?} normalization",
shape
);
} }
let attempt = match shape { let attempt = match shape {
@ -827,7 +898,10 @@ pub(crate) fn normalized_dev_roundtrip_structured(
})), })),
NormalizedDevShape::Pattern2Mini NormalizedDevShape::Pattern2Mini
| NormalizedDevShape::JsonparserSkipWsMini | NormalizedDevShape::JsonparserSkipWsMini
| NormalizedDevShape::JsonparserAtoiMini => catch_unwind(AssertUnwindSafe(|| { | NormalizedDevShape::JsonparserSkipWsReal
| NormalizedDevShape::JsonparserAtoiMini
| NormalizedDevShape::JsonparserAtoiReal
| NormalizedDevShape::JsonparserParseNumberReal => catch_unwind(AssertUnwindSafe(|| {
let norm = normalize_pattern2_minimal(module); let norm = normalize_pattern2_minimal(module);
normalized_pattern2_to_structured(&norm) normalized_pattern2_to_structured(&norm)
})), })),
@ -835,9 +909,9 @@ pub(crate) fn normalized_dev_roundtrip_structured(
match attempt { match attempt {
Ok(structured) => { Ok(structured) => {
if verbose { if debug {
eprintln!( eprintln!(
"[joinir/normalized-dev] {:?} normalization succeeded (functions={})", "[joinir/normalized-dev/roundtrip] {:?} normalization succeeded (functions={})",
shape, shape,
structured.functions.len() structured.functions.len()
); );
@ -845,9 +919,9 @@ pub(crate) fn normalized_dev_roundtrip_structured(
return Ok(structured); return Ok(structured);
} }
Err(_) => { Err(_) => {
if verbose { if debug {
eprintln!( eprintln!(
"[joinir/normalized-dev] {:?} normalization failed (unsupported)", "[joinir/normalized-dev/roundtrip] {:?} normalization failed (unsupported)",
shape shape
); );
} }

View File

@ -1,38 +1,111 @@
#![cfg(feature = "normalized_dev")] #![cfg(feature = "normalized_dev")]
use once_cell::sync::Lazy; 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). /// RAII guard for normalized_dev env toggling (NYASH_JOINIR_NORMALIZED_DEV_RUN).
/// 汚染防止のため tests/runner の両方で再利用できるようにここに置く /// ネストを許可し、最初の呼び出し時の状態だけを保存・復元する
pub struct NormalizedDevEnvGuard { pub struct NormalizedDevEnvGuard {
_lock: std::sync::MutexGuard<'static, ()>, active: bool,
prev: Option<String>,
} }
static NORMALIZED_ENV_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(())); #[derive(Default)]
struct EnvState {
stack: Vec<Option<String>>,
}
static NORMALIZED_ENV_STATE: Lazy<Mutex<EnvState>> = Lazy::new(|| Mutex::new(EnvState::default()));
static NORMALIZED_TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
impl NormalizedDevEnvGuard { impl NormalizedDevEnvGuard {
pub fn new(enabled: bool) -> Self { pub fn new(enabled: bool) -> Self {
let lock = NORMALIZED_ENV_LOCK let mut state = NORMALIZED_ENV_STATE
.lock() .lock()
.expect("normalized env mutex poisoned"); .expect("normalized env mutex poisoned");
// Save current value before overriding.
let prev = std::env::var("NYASH_JOINIR_NORMALIZED_DEV_RUN").ok(); let prev = std::env::var("NYASH_JOINIR_NORMALIZED_DEV_RUN").ok();
state.stack.push(prev);
if enabled { if enabled {
std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", "1"); std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", "1");
} else { } else {
std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN"); std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN");
} }
Self { _lock: lock, prev }
Self { active: true }
} }
} }
impl Drop for NormalizedDevEnvGuard { impl Drop for NormalizedDevEnvGuard {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(prev) = &self.prev { 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); std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", prev);
} else { } else {
std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN"); 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, R>(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, R>(f: F) -> R
where
F: FnOnce() -> R,
{
if normalized_dev_enabled() {
f()
} else {
with_dev_env(f)
}
} }

View File

@ -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::lowering::loop_with_break_minimal::lower_loop_with_break_minimal;
use crate::mir::join_ir::JoinModule; use crate::mir::join_ir::JoinModule;
use crate::mir::{BasicBlockId, ValueId}; use crate::mir::{BasicBlockId, ValueId};
use crate::{config::env::joinir_dev_enabled, config::env::joinir_test_debug_enabled};
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
/// Structured Pattern2 (joinir_min_loop 相当) をテスト用に生成するヘルパー。 /// 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) 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 で組み立てるヘルパー。 /// JsonParser _atoi 相当のミニ P2 ループを Structured で組み立てるヘルパー。
/// ///
/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_atoi_mini.program.json /// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_atoi_mini.program.json
@ -127,7 +176,7 @@ pub fn build_jsonparser_atoi_structured_for_normalized_dev() -> JoinModule {
let mut lowerer = AstToJoinIrLowerer::new(); let mut lowerer = AstToJoinIrLowerer::new();
let module = lowerer.lower_program_json(&program_json); 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!( eprintln!(
"[joinir/normalized-dev] jsonparser_atoi_mini structured module: {:#?}", "[joinir/normalized-dev] jsonparser_atoi_mini structured module: {:#?}",
module module
@ -136,3 +185,39 @@ pub fn build_jsonparser_atoi_structured_for_normalized_dev() -> JoinModule {
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,
};
}

View File

@ -1,5 +1,7 @@
#![cfg(feature = "normalized_dev")] #![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}; use crate::mir::join_ir::{JoinFuncId, JoinFunction, JoinInst, JoinModule};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -7,41 +9,100 @@ pub(crate) enum NormalizedDevShape {
Pattern1Mini, Pattern1Mini,
Pattern2Mini, Pattern2Mini,
JsonparserSkipWsMini, JsonparserSkipWsMini,
JsonparserSkipWsReal,
JsonparserAtoiMini, 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 ブリッジで扱う shapedev 限定)。
pub(crate) fn direct_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> { pub(crate) fn direct_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
supported_shapes(module) let shapes = detect_shapes(module);
log_shapes("direct", &shapes);
shapes
} }
/// Structured→Normalized の対象 shapedev 限定)。
pub(crate) fn supported_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> { pub(crate) fn supported_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
let mut shapes = Vec::new(); let shapes = detect_shapes(module);
if is_jsonparser_atoi_mini(module) { log_shapes("roundtrip", &shapes);
shapes.push(NormalizedDevShape::JsonparserAtoiMini); shapes
} }
if is_jsonparser_skip_ws_mini(module) {
shapes.push(NormalizedDevShape::JsonparserSkipWsMini); /// canonical常時 Normalized 経路を通す)対象。
} /// Phase 41: P2 コアセットP2 mini + JP skip_ws mini/real + JP atoi mini
if is_pattern2_mini(module) { pub(crate) fn canonical_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
shapes.push(NormalizedDevShape::Pattern2Mini); let shapes: Vec<_> = detect_shapes(module)
} .into_iter()
if is_pattern1_mini(module) { .filter(|s| {
shapes.push(NormalizedDevShape::Pattern1Mini); matches!(
} s,
NormalizedDevShape::Pattern2Mini
| NormalizedDevShape::JsonparserSkipWsMini
| NormalizedDevShape::JsonparserSkipWsReal
| NormalizedDevShape::JsonparserAtoiMini
)
})
.collect();
log_shapes("canonical", &shapes);
shapes shapes
} }
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) fn is_direct_supported(module: &JoinModule) -> bool { 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 { fn detect_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
let mut shapes: Vec<_> = SHAPE_DETECTORS
.iter()
.filter_map(|(shape, detector)| if detector(module) { Some(*shape) } else { None })
.collect();
// Pattern1 は「最小の後方互換」なので、より具体的な shape が見つかった場合は外しておく。
if shapes.len() > 1 {
shapes.retain(|s| *s != NormalizedDevShape::Pattern1Mini);
}
shapes
}
// --- 判定ロジック(共通) ---
mod detectors {
use super::*;
pub(super) fn is_pattern1_mini(module: &JoinModule) -> bool {
module.is_structured() && find_loop_step(module).is_some() module.is_structured() && find_loop_step(module).is_some()
} }
pub(crate) fn is_pattern2_mini(module: &JoinModule) -> bool { pub(super) fn is_pattern2_mini(module: &JoinModule) -> bool {
if !module.is_structured() || module.functions.len() != 3 { if !module.is_structured() || module.functions.len() != 3 {
return false; return false;
} }
@ -63,17 +124,46 @@ pub(crate) fn is_pattern2_mini(module: &JoinModule) -> bool {
.any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. })); .any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. }));
has_cond_jump && has_tail_call has_cond_jump && has_tail_call
} }
pub(crate) fn is_jsonparser_skip_ws_mini(module: &JoinModule) -> bool { pub(super) fn is_jsonparser_skip_ws_mini(module: &JoinModule) -> bool {
is_pattern2_mini(module) is_pattern2_mini(module)
&& module && module
.functions .functions
.values() .values()
.any(|f| f.name == "jsonparser_skip_ws_mini") .any(|f| f.name == "jsonparser_skip_ws_mini")
} }
pub(crate) fn is_jsonparser_atoi_mini(module: &JoinModule) -> bool { 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 { if !module.is_structured() || module.functions.len() != 3 {
return false; return false;
} }
@ -99,13 +189,81 @@ pub(crate) fn is_jsonparser_atoi_mini(module: &JoinModule) -> bool {
&& module && module
.functions .functions
.values() .values()
.any(|f| f.name.contains("atoi")) .any(|f| f.name == "jsonparser_atoi_mini")
} }
fn find_loop_step(module: &JoinModule) -> Option<&JoinFunction> { 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 module
.functions .functions
.values() .values()
.find(|f| f.name == "loop_step") .find(|f| f.name == "loop_step")
.or_else(|| module.functions.get(&JoinFuncId::new(1))) .or_else(|| module.functions.get(&JoinFuncId::new(1)))
}
}
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);
}
} }

View File

@ -34,7 +34,9 @@ use std::collections::HashMap;
use crate::config::env::normalized_dev_enabled; use crate::config::env::normalized_dev_enabled;
use crate::mir::join_ir::{ConstValue, JoinFuncId, JoinInst, JoinModule, MirLikeInst, VarId}; use crate::mir::join_ir::{ConstValue, JoinFuncId, JoinInst, JoinModule, MirLikeInst, VarId};
#[cfg(feature = "normalized_dev")] #[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 からの再エクスポート // Phase 27.8: ops box からの再エクスポート
pub use crate::mir::join_ir_ops::{JoinIrOpError, JoinValue}; pub use crate::mir::join_ir_ops::{JoinIrOpError, JoinValue};
@ -64,31 +66,32 @@ fn run_joinir_function_normalized_dev(
args: &[JoinValue], args: &[JoinValue],
) -> Result<JoinValue, JoinRuntimeError> { ) -> Result<JoinValue, JoinRuntimeError> {
// Keep dev path opt-in and fail-fast: only Structured P1/P2 minis are supported. // Keep dev path opt-in and fail-fast: only Structured P1/P2 minis are supported.
let verbose = crate::config::env::joinir_dev_enabled(); dev_env::with_dev_env_if_unset(|| {
let debug = dev_env::normalized_dev_logs_enabled();
let args_vec = args.to_vec(); let args_vec = args.to_vec();
let shapes = shape_guard::supported_shapes(module); let shapes = shape_guard::supported_shapes(module);
if shapes.is_empty() { if shapes.is_empty() {
if verbose { if debug {
eprintln!( eprintln!("[joinir/normalized-dev/runner] shape unsupported; staying on Structured path");
"[joinir/runner/normalized-dev] shape unsupported; staying on Structured path"
);
} }
return execute_function(vm, module, entry, args_vec); return execute_function(vm, module, entry, args_vec);
} }
let structured_roundtrip = normalized_dev_roundtrip_structured(module) let structured_roundtrip = normalized_dev_roundtrip_structured(module).map_err(|msg| {
.map_err(|msg| JoinRuntimeError::new(format!("[joinir/runner/normalized-dev] {}", msg)))?; JoinRuntimeError::new(format!("[joinir/normalized-dev/runner] {}", msg))
})?;
if verbose { if debug {
eprintln!( eprintln!(
"[joinir/runner/normalized-dev] normalized roundtrip succeeded (shapes={:?}, functions={})", "[joinir/normalized-dev/runner] normalized roundtrip succeeded (shapes={:?}, functions={})",
shapes, shapes,
structured_roundtrip.functions.len() structured_roundtrip.functions.len()
); );
} }
execute_function(vm, &structured_roundtrip, entry, args_vec) execute_function(vm, &structured_roundtrip, entry, args_vec)
})
} }
fn execute_function( fn execute_function(

View File

@ -62,7 +62,10 @@ fn normalize_for_shape(
} }
NormalizedDevShape::Pattern2Mini NormalizedDevShape::Pattern2Mini
| NormalizedDevShape::JsonparserSkipWsMini | NormalizedDevShape::JsonparserSkipWsMini
| NormalizedDevShape::JsonparserAtoiMini => { | NormalizedDevShape::JsonparserSkipWsReal
| NormalizedDevShape::JsonparserAtoiMini
| NormalizedDevShape::JsonparserAtoiReal
| NormalizedDevShape::JsonparserParseNumberReal => {
catch_unwind(AssertUnwindSafe(|| normalize_pattern2_minimal(module))) catch_unwind(AssertUnwindSafe(|| normalize_pattern2_minimal(module)))
} }
}; };
@ -80,44 +83,79 @@ fn normalize_for_shape(
fn try_normalized_direct_bridge( fn try_normalized_direct_bridge(
module: &JoinModule, module: &JoinModule,
meta: &JoinFuncMetaMap, meta: &JoinFuncMetaMap,
shapes: &[NormalizedDevShape],
allow_structured_fallback: bool,
use_env_guard: bool,
) -> Result<Option<MirModule>, JoinIrVmBridgeError> { ) -> Result<Option<MirModule>, JoinIrVmBridgeError> {
let shapes = shape_guard::direct_shapes(module);
if shapes.is_empty() { 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(); let exec = || {
let debug = crate::mir::join_ir::normalized::dev_env::normalized_dev_logs_enabled();
for shape in shapes { for &shape in shapes {
if verbose { if debug {
eprintln!("[joinir/bridge] attempting normalized→MIR for {:?}", shape); crate::mir::join_ir_vm_bridge::normalized_bridge::log_dev(
"direct",
format!("attempting normalized→MIR for {:?}", shape),
false,
);
} }
match normalize_for_shape(module, shape) { match normalize_for_shape(module, shape) {
Ok(norm) => { Ok(norm) => {
let mir = lower_normalized_to_mir_minimal(&norm, meta)?; let mir =
if verbose { lower_normalized_to_mir_minimal(&norm, meta, allow_structured_fallback)?;
eprintln!( crate::mir::join_ir_vm_bridge::normalized_bridge::log_dev(
"[joinir/bridge] normalized→MIR succeeded (shape={:?}, functions={})", "direct",
format!(
"normalized→MIR succeeded (shape={:?}, functions={})",
shape, shape,
norm.functions.len() norm.functions.len()
),
false,
); );
}
return Ok(Some(mir)); return Ok(Some(mir));
} }
Err(err) => { Err(err) => {
if verbose { if debug {
eprintln!( crate::mir::join_ir_vm_bridge::normalized_bridge::log_dev(
"[joinir/bridge] {:?} normalization failed: {} (continuing)", "direct",
format!(
"{:?} normalization failed: {} (continuing)",
shape, err.message shape, err.message
),
false,
); );
} }
} }
} }
} }
if allow_structured_fallback {
Ok(None)
} else {
Err(JoinIrVmBridgeError::new( Err(JoinIrVmBridgeError::new(
"[joinir/bridge] normalized_dev enabled but no normalization attempt succeeded", "[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 してから既存経路へ。 /// JoinIR → MIR の単一入口。Normalized dev が有効なら roundtrip してから既存経路へ。
@ -125,18 +163,28 @@ pub(crate) fn bridge_joinir_to_mir_with_meta(
module: &JoinModule, module: &JoinModule,
meta: &JoinFuncMetaMap, meta: &JoinFuncMetaMap,
) -> Result<MirModule, JoinIrVmBridgeError> { ) -> Result<MirModule, JoinIrVmBridgeError> {
if crate::config::env::normalized_dev_enabled() {
#[cfg(feature = "normalized_dev")] #[cfg(feature = "normalized_dev")]
{ {
match try_normalized_direct_bridge(module, meta)? { // 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), Some(mir) => return Ok(mir),
None => { None => {
debug_log!( return Err(JoinIrVmBridgeError::new(
"[joinir/bridge] normalized dev enabled but shape unsupported; falling back to Structured path" "[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) lower_joinir_structured_to_mir_with_meta(module, meta)

View File

@ -11,6 +11,22 @@ use crate::mir::MirModule;
mod direct; 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<str>, 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<str>) {
log_dev(category, message, false);
}
/// Direct Normalized → MIR 変換が未対応のときに使うフォールバック。 /// Direct Normalized → MIR 変換が未対応のときに使うフォールバック。
fn lower_normalized_via_structured( fn lower_normalized_via_structured(
norm: &NormalizedModule, norm: &NormalizedModule,
@ -24,9 +40,14 @@ fn lower_normalized_via_structured(
normalized_pattern2_to_structured(norm) normalized_pattern2_to_structured(norm)
}; };
if crate::config::env::joinir_dev_enabled() { log_dev(
eprintln!("[joinir/normalized-bridge/fallback] using structured path (functions={})", structured.functions.len()); "fallback",
} format!(
"using structured path (functions={})",
structured.functions.len()
),
true,
);
lower_joinir_structured_to_mir_with_meta(&structured, meta) 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( pub(crate) fn lower_normalized_to_mir_minimal(
norm: &NormalizedModule, norm: &NormalizedModule,
meta: &JoinFuncMetaMap, meta: &JoinFuncMetaMap,
allow_structured_fallback: bool,
) -> Result<MirModule, JoinIrVmBridgeError> { ) -> Result<MirModule, JoinIrVmBridgeError> {
if norm.phase != JoinIrPhase::Normalized { if norm.phase != JoinIrPhase::Normalized {
return Err(JoinIrVmBridgeError::new( 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() { if dev_debug_enabled() {
eprintln!( log_debug(
"[joinir/normalized-bridge] lowering normalized module (functions={}, env_layouts={})", "debug",
format!(
"lowering normalized module (functions={}, env_layouts={})",
norm.functions.len(), norm.functions.len(),
norm.env_layouts.len() norm.env_layouts.len()
),
); );
for layout in &norm.env_layouts { for layout in &norm.env_layouts {
let fields: Vec<String> = layout let fields: Vec<String> = layout
@ -54,19 +79,21 @@ pub(crate) fn lower_normalized_to_mir_minimal(
.iter() .iter()
.map(|f| format!("{}={:?}", f.name, f.value_id)) .map(|f| format!("{}={:?}", f.name, f.value_id))
.collect(); .collect();
eprintln!( log_debug(
"[joinir/normalized-bridge] env_layout {} fields: {}", "debug",
layout.id, format!("env_layout {} fields: {}", layout.id, fields.join(", ")),
fields.join(", ")
); );
} }
for func in norm.functions.values() { for func in norm.functions.values() {
eprintln!( log_debug(
"[joinir/normalized-bridge] fn {} (id={:?}) env_layout={:?} body_len={}", "debug",
format!(
"fn {} (id={:?}) env_layout={:?} body_len={}",
func.name, func.name,
func.id, func.id,
func.env_layout, func.env_layout,
func.body.len() func.body.len()
),
); );
} }
} }
@ -74,11 +101,20 @@ pub(crate) fn lower_normalized_to_mir_minimal(
// direct 対象は Normalized → MIR をそのまま吐く。未対応 shape は Structured 経由にフォールバック。 // direct 対象は Normalized → MIR をそのまま吐く。未対応 shape は Structured 経由にフォールバック。
match direct::lower_normalized_direct_minimal(norm) { match direct::lower_normalized_direct_minimal(norm) {
Ok(mir) => Ok(mir), Ok(mir) => Ok(mir),
Err(err) => { Err(err) if allow_structured_fallback => {
if crate::config::env::joinir_dev_enabled() { log_dev(
eprintln!("[joinir/normalized-bridge/fallback] direct path failed: {}; falling back to Structured path", err.message); "fallback",
} format!(
"direct path failed: {}; falling back to Structured path",
err.message
),
true,
);
lower_normalized_via_structured(norm, meta) lower_normalized_via_structured(norm, meta)
} }
Err(err) => Err(JoinIrVmBridgeError::new(format!(
"[joinir/normalized-bridge] direct path failed and fallback disabled: {}",
err.message
))),
} }
} }

View File

@ -4,7 +4,6 @@ use super::super::join_func_name;
use super::super::JoinIrVmBridgeError; use super::super::JoinIrVmBridgeError;
use super::super::convert_mir_like_inst; use super::super::convert_mir_like_inst;
use crate::ast::Span; 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::normalized::{JpFuncId, JpFunction, JpInst, JpOp, NormalizedModule};
use crate::mir::join_ir::{JoinFuncId, JoinIrPhase, MirLikeInst}; use crate::mir::join_ir::{JoinFuncId, JoinIrPhase, MirLikeInst};
use crate::mir::{ use crate::mir::{
@ -20,19 +19,20 @@ pub(crate) fn lower_normalized_direct_minimal(
) -> Result<MirModule, JoinIrVmBridgeError> { ) -> Result<MirModule, JoinIrVmBridgeError> {
if norm.phase != JoinIrPhase::Normalized { if norm.phase != JoinIrPhase::Normalized {
return Err(JoinIrVmBridgeError::new( 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 debug_dump = crate::mir::join_ir::normalized::dev_env::normalized_dev_logs_enabled();
let verbose = joinir_dev_enabled(); super::log_dev(
if verbose { "direct",
eprintln!( format!(
"[joinir/normalized-bridge/direct] lowering normalized module (functions={}, env_layouts={})", "using direct normalized bridge (functions={}, env_layouts={})",
norm.functions.len(), norm.functions.len(),
norm.env_layouts.len() norm.env_layouts.len()
),
false,
); );
}
let mut mir_module = MirModule::new("joinir_normalized_direct".to_string()); 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 { if debug_dump {
eprintln!( super::log_debug(
"[joinir/normalized-bridge/direct] produced MIR (debug dump): {:#?}", "direct",
mir_module format!("produced MIR (debug dump): {:#?}", mir_module),
); );
} }
@ -55,7 +55,6 @@ fn lower_normalized_function_direct(
func: &JpFunction, func: &JpFunction,
norm: &NormalizedModule, norm: &NormalizedModule,
) -> Result<MirFunction, JoinIrVmBridgeError> { ) -> Result<MirFunction, JoinIrVmBridgeError> {
let verbose = joinir_dev_enabled();
let env_fields = func let env_fields = func
.env_layout .env_layout
.and_then(|id| norm.env_layouts.iter().find(|layout| layout.id == id)); .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<MirInstruction> = Vec::new(); let mut current_insts: Vec<MirInstruction> = Vec::new();
let mut terminated = false; let mut terminated = false;
if verbose { super::log_debug(
eprintln!( "direct",
"[joinir/normalized-bridge/direct] lowering fn={} params={:?} remapped_params={:?} body_len={}", format!(
func.name, params, remapped_params, func.body.len() "lowering fn={} params={:?} remapped_params={:?} body_len={}",
func.name,
params,
remapped_params,
func.body.len()
),
); );
}
for inst in &func.body { for inst in &func.body {
if terminated { if terminated {
@ -122,6 +125,56 @@ fn lower_normalized_function_direct(
match inst { match inst {
JpInst::Let { dst, op, args } => { 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_dst = remap(*dst, &mut value_map);
let remapped_args = remap_vec(args, &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)?; 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)), lhs: args.get(0).copied().unwrap_or(ValueId(0)),
rhs: args.get(1).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 { JpOp::BoxCall { box_name, method } => Ok(MirLikeInst::BoxCall {
dst: Some(dst), dst: Some(dst),
box_name: box_name.clone(), box_name: box_name.clone(),

View File

@ -2,6 +2,7 @@
//! //!
//! 目的: フィクスチャベースの AST→JoinIR テストを簡潔に書けるようにする //! 目的: フィクスチャベースの AST→JoinIR テストを簡潔に書けるようにする
use crate::config::env::joinir_test_debug_enabled;
use crate::mir::join_ir::frontend::AstToJoinIrLowerer; use crate::mir::join_ir::frontend::AstToJoinIrLowerer;
use crate::mir::join_ir::JoinModule; use crate::mir::join_ir::JoinModule;
use crate::mir::join_ir_ops::JoinValue; use crate::mir::join_ir_ops::JoinValue;
@ -22,7 +23,7 @@ impl JoinIrFrontendTestRunner {
Self { Self {
fixture_path: fixture_path.to_string(), fixture_path: fixture_path.to_string(),
join_module: None, join_module: None,
debug_enabled: std::env::var("JOINIR_TEST_DEBUG").is_ok(), debug_enabled: joinir_test_debug_enabled(),
} }
} }

View File

@ -6,9 +6,14 @@ use nyash_rust::mir::join_ir::{
normalized_pattern2_to_structured, BinOpKind, ConstValue, JoinContId, JoinFuncId, normalized_pattern2_to_structured, BinOpKind, ConstValue, JoinContId, JoinFuncId,
JoinFunction, JoinInst, JoinIrPhase, JoinModule, MirLikeInst, 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::{ use nyash_rust::mir::join_ir::normalized::fixtures::{
build_jsonparser_atoi_structured_for_normalized_dev, 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_jsonparser_skip_ws_structured_for_normalized_dev,
build_pattern2_break_fixture_structured, build_pattern2_minimal_structured, 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_ops::JoinValue;
use nyash_rust::mir::join_ir_vm_bridge::run_joinir_via_vm; use nyash_rust::mir::join_ir_vm_bridge::run_joinir_via_vm;
use nyash_rust::mir::ValueId; 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() { fn assert_normalized_dev_ready() {
assert!( assert!(
nyash_rust::config::env::normalized_dev_enabled(), normalized_dev_enabled(),
"Phase 33: normalized_dev must be enabled for this suite (feature + env)" "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] #[test]
fn normalized_pattern1_minimal_smoke() { fn normalized_pattern1_minimal_smoke() {
let _ctx = normalized_dev_test_ctx();
let structured = build_structured_pattern1(); let structured = build_structured_pattern1();
let normalized = normalize_pattern1_minimal(&structured); let normalized = normalize_pattern1_minimal(&structured);
assert_eq!(normalized.phase, JoinIrPhase::Normalized); assert_eq!(normalized.phase, JoinIrPhase::Normalized);
@ -103,6 +118,7 @@ fn normalized_pattern1_minimal_smoke() {
#[test] #[test]
fn normalized_pattern1_roundtrip_structured_equivalent() { fn normalized_pattern1_roundtrip_structured_equivalent() {
let _ctx = normalized_dev_test_ctx();
let structured = build_structured_pattern1(); let structured = build_structured_pattern1();
let normalized = normalize_pattern1_minimal(&structured); let normalized = normalize_pattern1_minimal(&structured);
let reconstructed = normalized_pattern1_to_structured(&normalized); let reconstructed = normalized_pattern1_to_structured(&normalized);
@ -121,6 +137,7 @@ fn normalized_pattern1_roundtrip_structured_equivalent() {
#[test] #[test]
fn normalized_pattern1_exec_matches_structured() { fn normalized_pattern1_exec_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_structured_pattern1(); let structured = build_structured_pattern1();
let normalized = normalize_pattern1_minimal(&structured); let normalized = normalize_pattern1_minimal(&structured);
let reconstructed = normalized_pattern1_to_structured(&normalized); let reconstructed = normalized_pattern1_to_structured(&normalized);
@ -136,6 +153,7 @@ fn normalized_pattern1_exec_matches_structured() {
#[test] #[test]
fn normalized_pattern1_exec_matches_structured_roundtrip_backup() { fn normalized_pattern1_exec_matches_structured_roundtrip_backup() {
let _ctx = normalized_dev_test_ctx();
let structured = build_structured_pattern1(); let structured = build_structured_pattern1();
let normalized = normalize_pattern1_minimal(&structured); let normalized = normalize_pattern1_minimal(&structured);
let reconstructed = normalized_pattern1_to_structured(&normalized); let reconstructed = normalized_pattern1_to_structured(&normalized);
@ -156,6 +174,7 @@ fn normalized_pattern1_exec_matches_structured_roundtrip_backup() {
#[test] #[test]
fn normalized_pattern2_roundtrip_structure() { fn normalized_pattern2_roundtrip_structure() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern2_minimal_structured(); let structured = build_pattern2_minimal_structured();
let normalized = normalize_pattern2_minimal(&structured); let normalized = normalize_pattern2_minimal(&structured);
assert_eq!(normalized.phase, JoinIrPhase::Normalized); 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] #[test]
fn normalized_pattern2_exec_matches_structured() { fn normalized_pattern2_exec_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern2_minimal_structured(); let structured = build_pattern2_minimal_structured();
let normalized = normalize_pattern2_minimal(&structured); let normalized = normalize_pattern2_minimal(&structured);
let reconstructed = normalized_pattern2_to_structured(&normalized); let reconstructed = normalized_pattern2_to_structured(&normalized);
@ -193,6 +243,7 @@ fn normalized_pattern2_exec_matches_structured() {
#[test] #[test]
#[should_panic(expected = "normalize_pattern2_minimal")] #[should_panic(expected = "normalize_pattern2_minimal")]
fn normalized_pattern2_rejects_non_pattern2_structured() { fn normalized_pattern2_rejects_non_pattern2_structured() {
let _ctx = normalized_dev_test_ctx();
// Pattern1 Structured module should be rejected by Pattern2 normalizer. // Pattern1 Structured module should be rejected by Pattern2 normalizer.
let structured = build_structured_pattern1(); let structured = build_structured_pattern1();
let _ = normalize_pattern2_minimal(&structured); let _ = normalize_pattern2_minimal(&structured);
@ -200,6 +251,7 @@ fn normalized_pattern2_rejects_non_pattern2_structured() {
#[test] #[test]
fn normalized_pattern2_real_loop_roundtrip_structure() { fn normalized_pattern2_real_loop_roundtrip_structure() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern2_break_fixture_structured(); let structured = build_pattern2_break_fixture_structured();
let normalized = normalize_pattern2_minimal(&structured); let normalized = normalize_pattern2_minimal(&structured);
let reconstructed = normalized_pattern2_to_structured(&normalized); let reconstructed = normalized_pattern2_to_structured(&normalized);
@ -221,6 +273,7 @@ fn normalized_pattern2_real_loop_roundtrip_structure() {
#[test] #[test]
fn normalized_pattern2_real_loop_exec_matches_structured() { fn normalized_pattern2_real_loop_exec_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern2_break_fixture_structured(); let structured = build_pattern2_break_fixture_structured();
let normalized = normalize_pattern2_minimal(&structured); let normalized = normalize_pattern2_minimal(&structured);
let reconstructed = normalized_pattern2_to_structured(&normalized); let reconstructed = normalized_pattern2_to_structured(&normalized);
@ -246,6 +299,7 @@ fn normalized_pattern2_real_loop_exec_matches_structured() {
#[test] #[test]
fn normalized_pattern1_runner_dev_switch_matches_structured() { fn normalized_pattern1_runner_dev_switch_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_structured_pattern1(); let structured = build_structured_pattern1();
let entry = structured.entry.expect("structured entry required"); let entry = structured.entry.expect("structured entry required");
let input = [JoinValue::Int(7)]; let input = [JoinValue::Int(7)];
@ -259,6 +313,7 @@ fn normalized_pattern1_runner_dev_switch_matches_structured() {
#[test] #[test]
fn normalized_pattern2_runner_dev_switch_matches_structured() { fn normalized_pattern2_runner_dev_switch_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern2_break_fixture_structured(); let structured = build_pattern2_break_fixture_structured();
let entry = structured.entry.expect("structured entry required"); let entry = structured.entry.expect("structured entry required");
let cases = [0, 1, 3, 5]; let cases = [0, 1, 3, 5];
@ -281,6 +336,7 @@ fn normalized_pattern2_runner_dev_switch_matches_structured() {
#[test] #[test]
fn normalized_pattern2_jsonparser_runner_dev_switch_matches_structured() { 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 structured = build_jsonparser_skip_ws_structured_for_normalized_dev();
let entry = structured.entry.expect("structured entry required"); let entry = structured.entry.expect("structured entry required");
let cases = [0, 1, 2, 5]; let cases = [0, 1, 2, 5];
@ -297,6 +353,7 @@ fn normalized_pattern2_jsonparser_runner_dev_switch_matches_structured() {
#[test] #[test]
fn normalized_pattern2_vm_bridge_direct_matches_structured() { fn normalized_pattern2_vm_bridge_direct_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern2_break_fixture_structured(); let structured = build_pattern2_break_fixture_structured();
let entry = structured.entry.expect("structured entry required"); let entry = structured.entry.expect("structured entry required");
let cases = [0, 1, 3, 5]; let cases = [0, 1, 3, 5];
@ -319,6 +376,7 @@ fn normalized_pattern2_vm_bridge_direct_matches_structured() {
#[test] #[test]
fn normalized_pattern1_vm_bridge_direct_matches_structured() { fn normalized_pattern1_vm_bridge_direct_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_structured_pattern1(); let structured = build_structured_pattern1();
let entry = structured.entry.expect("structured entry required"); let entry = structured.entry.expect("structured entry required");
let cases = [0, 5, 7]; let cases = [0, 5, 7];
@ -335,6 +393,7 @@ fn normalized_pattern1_vm_bridge_direct_matches_structured() {
#[test] #[test]
fn normalized_pattern2_jsonparser_vm_bridge_direct_matches_structured() { 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 structured = build_jsonparser_skip_ws_structured_for_normalized_dev();
let entry = structured.entry.expect("structured entry required"); let entry = structured.entry.expect("structured entry required");
let cases = [0, 1, 2, 5]; 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] #[test]
fn normalized_pattern2_jsonparser_atoi_vm_bridge_direct_matches_structured() { 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 structured = build_jsonparser_atoi_structured_for_normalized_dev();
let entry = structured.entry.expect("structured entry required"); let entry = structured.entry.expect("structured entry required");
let cases = [ 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
);
}
}