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 を箱の中に閉じた。
- `loop_pattern_detection` の classify() が代表ループを P1〜P4 に分類することをユニットテストで固定。
- Phase 245-EX / 245C:
- `_parse_number` のループについて、ループ変数 `p` の header/break 条件と `p = p + 1` 更新を Pattern2 + ExprLowerer 経路に載せて本番化num_str は未導入)
- `_parse_number` のループについて、ループ変数 `p` の header/break 条件と `p = p + 1` 更新を Pattern2 + ExprLowerer 経路に載せて本番化。
- FunctionScopeCapture / CapturedEnv を拡張し、関数パラメータ(例: `s`, `len`)をループ条件/本体で読み取り専用なら CapturedEnv 経由で ConditionEnv に載せるようにした。
- これにより、`p < s.length()` のような header 条件や JsonParser 系ループでのパラメータ参照が、ExprLowerer/ScopeManager から安全に解決される。
- Phase 245B-IMPL:
- `_parse_number` 本体で `num_str = num_str + ch` を LoopState キャリアとして扱い、Program(JSON) フィクスチャ `jsonparser_parse_number_real` を Structured→Normalized→MIR(direct) で dev テスト固定(出力は num_str 文字列)。
- Phase 247-EX:
- DigitPos promotion を二重値化し、`digit_pos` から boolean carrier `is_digit_pos`ConditionOnlyと integer carrier `digit_value`LoopStateを生成。
- UpdateEnv で `digit_pos` 解決時に `digit_value` を優先し、NumberAccumulation`result = result * 10 + digit_pos`)と break 条件の両方で DigitPos パターンが安全に利用可能に。
@ -51,6 +53,14 @@
- P1/P2 ミニ + JsonParser skip_ws/atoi ミニを Normalized→MIR 直ブリッジで実行できるようにし、normalized_dev ON 時は Structured→Normalized→MIR復元なし経路との比較テストで結果一致を固定。既定経路Structured→MIRは不変。
- Phase 36-NORM-BRIDGE-DIRECTdev-only:
- Normalized ブリッジを direct 実装Normalized→MIRと Structured 再構成に分離し、shape_guard で P1/P2 ミニ + JsonParser skip_ws/atoi ミニだけ direct 経路に通すよう整理。非対応は `[joinir/normalized-bridge/fallback]` ログ付きで再構成に落とし、テストで direct/従来経路の VM 出力一致を固定。
- Phase 37-NORM-JP-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. いまコード側で意識しておきたいフォーカス

View File

@ -987,7 +987,7 @@ JoinIR は Rust 側だけでなく、将来的に .hako selfhost コンパイラ
- [x] 退行なし: Phase 190-196 テスト全 PASS ✅
- 詳細: phase197-lightweight-loops-deployment.md
7. **JsonParser/selfhost 実戦 JoinIR 適用状況** (2025-12-09 更新)
7. **JsonParser/selfhost 実戦 JoinIR 適用状況** (2025-12-09 更新 → Phase 42 で棚卸し済み)
| Function | Pattern | Status | Note |
|----------|---------|--------|------|
@ -998,15 +998,15 @@ JoinIR は Rust 側だけでなく、将来的に .hako selfhost コンパイラ
| `phase195_sum_count` | P3 | ✅ JoinIR OK | Phase 196 検証済みmulti-carrier|
| `loop_if_phi` | P3 | ✅ JoinIR OK | Phase 196 検証済みsingle-carrier|
| `loop_min_while` | P1 | ✅ JoinIR OK | Phase 165 基本検証済み |
| `_parse_number` | P2 | ⚠️ Deferred | ConditionEnv 制約Phase 200+|
| `_atoi` | P2 | ⚠️ Deferred | ConditionEnv 制約Phase 200+|
| `_parse_number` | P2 | ✅ JoinIR OK | Phase 245B-IMPL: P2-Midnum_str LoopState キャリア)を Structured→Normalized(dev, direct) で固定 |
| `_atoi` | P2 | ✅ JoinIR OK | Phase 246-EX で NumberAccumulation パターンとして統合P2-Mid、Normalized: mini + real(dev, 符号対応) / canonical 準備中|
| `_parse_string` | P3 | ⚠️ Deferred | 複雑キャリアPhase 195+ 拡張後)|
| `_unescape_string` | P3 | ⚠️ Deferred | 複雑キャリアPhase 195+ 拡張後)|
| `_parse_array` | - | ⚠️ Deferred | 複数 MethodCallPhase 195+|
| `_parse_object` | - | ⚠️ Deferred | 複数 MethodCallPhase 195+|
**Coverage**: 7/13 ループ JoinIR 対応済み(54%
**Verification**: 4/7 ループ E2E PASS、3/7 structural/routing 確認済み
**Coverage**: 9/13 ループ JoinIR 対応済み(約 69%
**Verification**: 代表ループP1/P2 Core + Trim/P3については E2E テストで挙動確認済み。詳細なケースごとの状況は各 Phase ドキュメントPhase 197/245/246 など)を参照。
8. **JsonParser 残り複雑ループへの適用Phase 198+, 200+**
- Phase 200+: ConditionEnv 拡張 (function-scoped variables) → _parse_number, _atoi
@ -1247,3 +1247,61 @@ Normalized JoinIR を 1 段挟むと、開発の手触りがどう変わるか
- Normalized ブリッジを direct 実装と Structured 再構成の二段に分離し、shape_guard で direct 対象P1/P2 ミニ + JsonParser skip_ws/atoi ミニ)だけを Normalized→MIR 直接生成に切り替えた。
- direct 経路は `normalized_bridge::direct` に閉じ込め、非対応形状は `[joinir/normalized-bridge/fallback]` ログ付きで Structured 再構成経路に落とす構造に整理。dev テストでは direct 経路の VM 出力が従来経路と一致することを固定。
### 3.15 Phase 37-NORM-JP-REAL JsonParser `_skip_whitespace` 本体を dev Normalized で比較
- JsonParser 本体の `_skip_whitespace` ループを Program(JSON) フィクスチャ化し、`shape_guard` で real 版を検知して Structured→Normalized→MIR(direct) の dev 経路に通すように拡張。`extract_value` は `&&`/`||` を BinOp として受け付けるようにした。
- Break パターンのパラメータ推定を柔軟化loop_var/acc/n が無いケースでも loop_var を優先し、acc が無ければ同一キャリアとして扱うし、skip_ws real の構造で panic しないようにした。
- tests/normalized_joinir_min.rs に `_skip_whitespace` real フィクスチャの VM 比較テストを追加し、env ON 時は Structured→Normalized→MIR(direct) と Structured 直経路の stdout が一致することを固定env OFF は既存経路のまま)。
- normalized_dev 用フィクスチャは `docs/private/roadmap2/phases/normalized_dev/fixtures/` に配置し、Program(JSON) から `AstToJoinIrLowerer` で読み込む運用に統一した。
### 3.16 Phase 38-NORM-OBS Normalized dev ログ/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 残りループ設計調査
Status: Historical + Updated in Phase 42
Note: Phase 181 時点の設計調査に基づくドキュメントだよ。最新の P2 分類と JoinIR/Normalized 状態は、このファイル内の「Phase 42 時点の P2 インベントリ」と `joinir-architecture-overview.md` を SSOT として見てね。
## 概要
JsonParser`tools/hako_shared/json_parser.hako`の全11ループを詳細に分析し、
@ -201,6 +204,34 @@ loop(i < len) {
**実行可能性**: Phase 182+ で実装可能Pattern2 Break_atoi の後継
## Phase 42 時点の P2 インベントリJsonParser
Phase 42 では上の 11 ループについてP2 としてどこまで JoinIR / Normalized で扱えているかを棚卸ししてP2-Core / P2-Mid / P2-Heavy 3 クラスに整理したよ
### 1. 分類ポリシー
- **P2-Core**: 既に 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全体
```

View File

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

View File

@ -1,5 +1,6 @@
Status: Draft
Scope: `_parse_number``num_str` を Pattern2/P5 のキャリアとして扱うかどうかを決める設計フェーズ(コード変更なし)。
Status: Implemented (Phase 245B-IMPL)
Scope: `_parse_number``num_str` を Pattern2/P5 のキャリアとして扱うかどうかを決める設計フェーズ(実装は Structured→Normalized dev フィクスチャで完了)。
Notes: jsonparser_parse_number_real フィクスチャで `num_str = num_str + ch` を LoopState キャリアとして実装し、dev Normalized 比較テストで固定済み。
# Phase 245B: JsonParser `_parse_number` の `num_str` キャリア設計

View File

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

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)
///
/// 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::loop_body_local_env::LoopBodyLocalEnv;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr;
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
use crate::mir::loop_pattern_detection::error_messages;
use crate::mir::ValueId;
use std::collections::BTreeMap;
fn log_pattern2(verbose: bool, tag: &str, message: impl AsRef<str>) {
if verbose {
@ -707,19 +709,13 @@ impl MirBuilder {
);
let original_carrier_count = inputs.carrier_info.carriers.len();
inputs.carrier_info.carriers.retain(|carrier| {
use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole};
carrier_updates.contains_key(&carrier.name)
|| carrier.role == CarrierRole::ConditionOnly
|| carrier.init == CarrierInit::FromHost
|| carrier.init == CarrierInit::LoopLocalZero
});
filter_carriers_for_updates(&mut inputs.carrier_info, &carrier_updates);
log_pattern2(
verbose,
"updates",
format!(
"Phase 176-4: Filtered carriers: {}{} (kept only carriers with updates)",
"Phase 176-4: Filtered carriers: {}{} (kept only carriers with updates/condition-only/loop-local-zero)",
original_carrier_count,
inputs.carrier_info.carriers.len()
),
@ -858,6 +854,19 @@ impl MirBuilder {
}
}
/// 更新を持たない FromHost キャリアを落とすヘルパー。
fn filter_carriers_for_updates(
carrier_info: &mut CarrierInfo,
carrier_updates: &BTreeMap<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)]
mod tests {
use super::*;

View File

@ -138,6 +138,8 @@ impl AstToJoinIrLowerer {
"-" => BinOpKind::Sub,
"*" => BinOpKind::Mul,
"/" => BinOpKind::Div,
"&&" => BinOpKind::And,
"||" => BinOpKind::Or,
_ => 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,
process_local_inits,
};
use super::param_guess::{build_param_order, compute_param_guess};
use super::{AstToJoinIrLowerer, JoinModule, LoweringError};
use crate::mir::join_ir::{JoinFunction, JoinInst};
use crate::mir::ValueId;
use std::collections::BTreeSet;
/// Break パターンを JoinModule に変換
///
@ -70,7 +70,10 @@ pub fn lower(
let break_cond_expr = &break_if_stmt["cond"];
let (param_order, loop_var_name, acc_name) = compute_param_order(&entry_ctx);
let param_guess = compute_param_guess(&entry_ctx);
let param_order = build_param_order(&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"];
// 5. entry 関数を生成
@ -245,48 +248,3 @@ fn create_loop_step_function_break(
exit_cont: None,
})
}
fn compute_param_order(
entry_ctx: &super::super::context::ExtractCtx,
) -> (Vec<(String, ValueId)>, String, String) {
let loop_var_name = "i".to_string();
let loop_var = entry_ctx
.get_var(&loop_var_name)
.expect("i must be initialized");
let (acc_name, acc_var) = if let Some(v) = entry_ctx.get_var("acc") {
("acc".to_string(), v)
} else if let Some(v) = entry_ctx.get_var("result") {
("result".to_string(), v)
} else {
panic!("acc or result must be initialized");
};
let (len_name, len_var) = if let Some(v) = entry_ctx.get_var("n") {
("n".to_string(), v)
} else if let Some(v) = entry_ctx.get_var("len") {
("len".to_string(), v)
} else {
panic!("n or len must be provided as parameter");
};
let mut param_order = vec![
(loop_var_name.clone(), loop_var),
(acc_name.clone(), acc_var),
(len_name.clone(), len_var),
];
let mut seen: BTreeSet<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 関数生成
use super::{AstToJoinIrLowerer, JoinModule};
use super::super::stmt_handlers::StatementEffect;
use crate::mir::join_ir::JoinIrPhase;
use crate::mir::join_ir::{JoinFuncId, JoinFunction, JoinInst};
use crate::mir::ValueId;
@ -119,19 +120,15 @@ pub fn process_local_inits(
let stmt_type = stmt["type"].as_str().expect("Statement must have type");
match stmt_type {
"Local" => {
let var_name = stmt["name"]
.as_str()
.expect("Local must have 'name'")
.to_string();
let expr = &stmt["expr"];
// extract_value で式を評価
let (var_id, insts) = lowerer.extract_value(expr, ctx);
"Local" | "Assignment" | "If" => {
let (insts, effect) = lowerer.lower_statement(stmt, ctx);
init_insts.extend(insts);
// 同名再宣言 = var_map を更新(再代入の意味論)
ctx.register_param(var_name, var_id);
if let StatementEffect::VarUpdate { name, value_id } = effect {
ctx.register_param(name, value_id);
} else if matches!(effect, StatementEffect::SideEffect) {
panic!("Unexpected side-effecting statement before Loop: {}", stmt_type);
}
}
_ => panic!("Unexpected statement type before Loop: {}", stmt_type),
}

View File

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

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

View File

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

View File

@ -58,7 +58,6 @@
use crate::ast::ASTNode;
mod boundary_builder;
mod header_break_lowering;
mod step_schedule;
#[cfg(test)]
mod tests;
@ -71,6 +70,9 @@ use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr;
use crate::mir::join_ir::lowering::pattern2_step_schedule::{
build_pattern2_schedule, Pattern2ScheduleContext, Pattern2StepKind,
};
use crate::mir::join_ir::lowering::update_env::UpdateEnv;
use crate::mir::join_ir::{
BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst,
@ -84,7 +86,6 @@ use crate::mir::ValueId;
use boundary_builder::build_fragment_meta;
use header_break_lowering::{lower_break_condition, lower_header_condition};
use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism
use step_schedule::{Pattern2Step, Pattern2StepSchedule};
/// Lower Pattern 2 (Loop with Conditional Break) to JoinIR
///
@ -260,6 +261,20 @@ pub(crate) fn lower_loop_with_break_minimal(
"[joinir/pattern2] Phase 201: loop_step params - i_param={:?}, carrier_params={:?}",
i_param, carrier_param_ids
);
if crate::config::env::joinir_dev_enabled()
|| crate::config::env::joinir_test_debug_enabled()
{
eprintln!(
"[joinir/pattern2/debug] loop_var='{}' env.get(loop_var)={:?}, carriers={:?}",
loop_var_name,
env.get(loop_var_name),
carrier_info
.carriers
.iter()
.map(|c| (c.name.clone(), c.join_id))
.collect::<Vec<_>>()
);
}
// Phase 169 / Phase 171-fix / Phase 240-EX / Phase 244: Lower condition
let (cond_value, mut cond_instructions) = lower_header_condition(
@ -327,23 +342,9 @@ pub(crate) fn lower_loop_with_break_minimal(
let mut loop_step_func = JoinFunction::new(loop_step_id, "loop_step".to_string(), loop_params);
// Decide evaluation order (header/body-init/break/updates/tail) up-front.
let schedule =
Pattern2StepSchedule::for_pattern2(body_local_env.as_ref().map(|env| &**env), carrier_info);
let schedule_desc: Vec<&str> = schedule
.iter()
.map(|step| match step {
Pattern2Step::HeaderAndNaturalExit => "header+exit",
Pattern2Step::BodyLocalInit => "body-init",
Pattern2Step::BreakCondition => "break",
Pattern2Step::CarrierUpdates => "updates",
Pattern2Step::TailCall => "tail",
})
.collect();
eprintln!(
"[pattern2/schedule] Selected Pattern2 step schedule: {:?} ({})",
schedule_desc,
schedule.reason()
);
let schedule_ctx =
Pattern2ScheduleContext::from_env(body_local_env.as_ref().map(|env| &**env), carrier_info);
let schedule = build_pattern2_schedule(&schedule_ctx);
// Collect fragments per step; append them according to the schedule below.
let mut header_block: Vec<JoinInst> = Vec::new();
@ -661,11 +662,11 @@ pub(crate) fn lower_loop_with_break_minimal(
// Apply scheduled order to assemble the loop_step body.
for step in schedule.iter() {
match step {
Pattern2Step::HeaderAndNaturalExit => loop_step_func.body.append(&mut header_block),
Pattern2Step::BodyLocalInit => loop_step_func.body.append(&mut body_init_block),
Pattern2Step::BreakCondition => loop_step_func.body.append(&mut break_block),
Pattern2Step::CarrierUpdates => loop_step_func.body.append(&mut carrier_update_block),
Pattern2Step::TailCall => loop_step_func.body.append(&mut tail_block),
Pattern2StepKind::HeaderCond => loop_step_func.body.append(&mut header_block),
Pattern2StepKind::BodyInit => loop_step_func.body.append(&mut body_init_block),
Pattern2StepKind::BreakCheck => loop_step_func.body.append(&mut break_block),
Pattern2StepKind::Updates => loop_step_func.body.append(&mut carrier_update_block),
Pattern2StepKind::Tail => loop_step_func.body.append(&mut tail_block),
}
}

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

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,
normalized_pattern2_to_structured, NormalizedModule,
};
#[cfg(feature = "normalized_dev")]
pub use normalized::fixtures;
pub use verify::verify_progress_for_skip_ws;
// Phase 200-3: Contract verification functions are in merge/mod.rs (private module access)

View File

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

View File

@ -1,38 +1,111 @@
#![cfg(feature = "normalized_dev")]
use once_cell::sync::Lazy;
use std::sync::Mutex;
use std::sync::{Mutex, MutexGuard};
/// RAII guard for normalized_dev env toggling (NYASH_JOINIR_NORMALIZED_DEV_RUN).
/// 汚染防止のため tests/runner の両方で再利用できるようにここに置く
/// ネストを許可し、最初の呼び出し時の状態だけを保存・復元する
pub struct NormalizedDevEnvGuard {
_lock: std::sync::MutexGuard<'static, ()>,
prev: Option<String>,
active: bool,
}
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 {
pub fn new(enabled: bool) -> Self {
let lock = NORMALIZED_ENV_LOCK
let mut state = NORMALIZED_ENV_STATE
.lock()
.expect("normalized env mutex poisoned");
// Save current value before overriding.
let prev = std::env::var("NYASH_JOINIR_NORMALIZED_DEV_RUN").ok();
state.stack.push(prev);
if enabled {
std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", "1");
} else {
std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN");
}
Self { _lock: lock, prev }
Self { active: true }
}
}
impl Drop for NormalizedDevEnvGuard {
fn drop(&mut self) {
if let Some(prev) = &self.prev {
if !self.active {
return;
}
let mut state = NORMALIZED_ENV_STATE
.lock()
.expect("normalized env mutex poisoned");
if let Some(prev) = state.stack.pop() {
if let Some(prev) = prev {
std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", prev);
} else {
std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN");
}
}
}
}
/// normalized_dev feature + env の ON/OFF をまとめた判定。
pub fn normalized_dev_enabled() -> bool {
crate::config::env::normalized_dev_enabled()
}
/// normalized_dev かつ test/debug ログが有効なときだけ true。
pub fn normalized_dev_logs_enabled() -> bool {
crate::config::env::normalized_dev_enabled() && crate::config::env::joinir_test_debug_enabled()
}
/// テスト用コンテキストenv を ON にしつつロックで並列汚染を防ぐ。
pub struct NormalizedTestContext<'a> {
_lock: MutexGuard<'a, ()>,
_env_guard: NormalizedDevEnvGuard,
}
impl<'a> NormalizedTestContext<'a> {
fn new(lock: MutexGuard<'a, ()>) -> Self {
let env_guard = NormalizedDevEnvGuard::new(true);
NormalizedTestContext {
_lock: lock,
_env_guard: env_guard,
}
}
}
/// テストで使う共通ガード。
pub fn test_ctx() -> NormalizedTestContext<'static> {
let lock = NORMALIZED_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
NormalizedTestContext::new(lock)
}
/// 簡易ラッパー:クロージャを normalized_dev ON で実行。
pub fn with_dev_env<F, 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::JoinModule;
use crate::mir::{BasicBlockId, ValueId};
use crate::{config::env::joinir_dev_enabled, config::env::joinir_test_debug_enabled};
use std::collections::{BTreeMap, BTreeSet};
/// Structured Pattern2 (joinir_min_loop 相当) をテスト用に生成するヘルパー。
@ -113,6 +114,54 @@ pub fn build_jsonparser_skip_ws_structured_for_normalized_dev() -> JoinModule {
lowerer.lower_program_json(&program_json)
}
/// JsonParser _skip_whitespace 本体相当の P2 ループを Structured で組み立てるヘルパー。
///
/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_skip_ws_real.program.json
pub fn build_jsonparser_skip_ws_real_structured_for_normalized_dev() -> JoinModule {
const FIXTURE: &str = include_str!(
"../../../../docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_skip_ws_real.program.json"
);
let program_json: serde_json::Value =
serde_json::from_str(FIXTURE).expect("jsonparser skip_ws real fixture should be valid JSON");
let mut lowerer = AstToJoinIrLowerer::new();
let module = lowerer.lower_program_json(&program_json);
if joinir_dev_enabled() && joinir_test_debug_enabled() {
eprintln!(
"[joinir/normalized-dev] jsonparser_skip_ws_real structured module: {:#?}",
module
);
}
module
}
/// JsonParser _parse_number 本体相当の P2 ループを Structured で組み立てるヘルパー。
///
/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_parse_number_real.program.json
pub fn build_jsonparser_parse_number_real_structured_for_normalized_dev() -> JoinModule {
const FIXTURE: &str = include_str!(
"../../../../docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_parse_number_real.program.json"
);
let program_json: serde_json::Value =
serde_json::from_str(FIXTURE).expect("jsonparser parse_number real fixture should be valid JSON");
let mut lowerer = AstToJoinIrLowerer::new();
let module = lowerer.lower_program_json(&program_json);
if joinir_dev_enabled() && joinir_test_debug_enabled() {
eprintln!(
"[joinir/normalized-dev] jsonparser_parse_number_real structured module: {:#?}",
module
);
}
module
}
/// JsonParser _atoi 相当のミニ P2 ループを Structured で組み立てるヘルパー。
///
/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_atoi_mini.program.json
@ -127,7 +176,7 @@ pub fn build_jsonparser_atoi_structured_for_normalized_dev() -> JoinModule {
let mut lowerer = AstToJoinIrLowerer::new();
let module = lowerer.lower_program_json(&program_json);
if std::env::var("JOINIR_TEST_DEBUG").is_ok() {
if joinir_dev_enabled() && joinir_test_debug_enabled() {
eprintln!(
"[joinir/normalized-dev] jsonparser_atoi_mini structured module: {:#?}",
module
@ -136,3 +185,39 @@ pub fn build_jsonparser_atoi_structured_for_normalized_dev() -> JoinModule {
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")]
use crate::config::env::joinir_dev_enabled;
use crate::mir::join_ir::normalized::dev_env;
use crate::mir::join_ir::{JoinFuncId, JoinFunction, JoinInst, JoinModule};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -7,41 +9,100 @@ pub(crate) enum NormalizedDevShape {
Pattern1Mini,
Pattern2Mini,
JsonparserSkipWsMini,
JsonparserSkipWsReal,
JsonparserAtoiMini,
JsonparserAtoiReal,
JsonparserParseNumberReal,
}
/// 直接 Normalized→MIR ブリッジで扱う shape を返すdev 限定)。
type Detector = fn(&JoinModule) -> bool;
const SHAPE_DETECTORS: &[(NormalizedDevShape, Detector)] = &[
(NormalizedDevShape::Pattern1Mini, detectors::is_pattern1_mini),
(NormalizedDevShape::Pattern2Mini, detectors::is_pattern2_mini),
(
NormalizedDevShape::JsonparserSkipWsMini,
detectors::is_jsonparser_skip_ws_mini,
),
(
NormalizedDevShape::JsonparserSkipWsReal,
detectors::is_jsonparser_skip_ws_real,
),
(
NormalizedDevShape::JsonparserAtoiMini,
detectors::is_jsonparser_atoi_mini,
),
(
NormalizedDevShape::JsonparserAtoiReal,
detectors::is_jsonparser_atoi_real,
),
(
NormalizedDevShape::JsonparserParseNumberReal,
detectors::is_jsonparser_parse_number_real,
),
];
/// direct ブリッジで扱う shapedev 限定)。
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> {
let mut shapes = Vec::new();
if is_jsonparser_atoi_mini(module) {
shapes.push(NormalizedDevShape::JsonparserAtoiMini);
}
if is_jsonparser_skip_ws_mini(module) {
shapes.push(NormalizedDevShape::JsonparserSkipWsMini);
}
if is_pattern2_mini(module) {
shapes.push(NormalizedDevShape::Pattern2Mini);
}
if is_pattern1_mini(module) {
shapes.push(NormalizedDevShape::Pattern1Mini);
}
let shapes = detect_shapes(module);
log_shapes("roundtrip", &shapes);
shapes
}
/// canonical常時 Normalized 経路を通す)対象。
/// Phase 41: P2 コアセットP2 mini + JP skip_ws mini/real + JP atoi mini
pub(crate) fn canonical_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
let shapes: Vec<_> = detect_shapes(module)
.into_iter()
.filter(|s| {
matches!(
s,
NormalizedDevShape::Pattern2Mini
| NormalizedDevShape::JsonparserSkipWsMini
| NormalizedDevShape::JsonparserSkipWsReal
| NormalizedDevShape::JsonparserAtoiMini
)
})
.collect();
log_shapes("canonical", &shapes);
shapes
}
#[allow(dead_code)]
pub(crate) fn is_direct_supported(module: &JoinModule) -> bool {
!direct_shapes(module).is_empty()
!detect_shapes(module).is_empty()
}
pub(crate) fn is_pattern1_mini(module: &JoinModule) -> bool {
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()
}
}
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 {
return false;
}
@ -63,17 +124,46 @@ pub(crate) fn is_pattern2_mini(module: &JoinModule) -> bool {
.any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. }));
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)
&& module
.functions
.values()
.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 {
return false;
}
@ -99,13 +189,81 @@ pub(crate) fn is_jsonparser_atoi_mini(module: &JoinModule) -> bool {
&& module
.functions
.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
.functions
.values()
.find(|f| f.name == "loop_step")
.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::mir::join_ir::{ConstValue, JoinFuncId, JoinInst, JoinModule, MirLikeInst, VarId};
#[cfg(feature = "normalized_dev")]
use crate::mir::join_ir::normalized::{normalized_dev_roundtrip_structured, shape_guard};
use crate::mir::join_ir::normalized::{
dev_env, normalized_dev_roundtrip_structured, shape_guard,
};
// Phase 27.8: ops box からの再エクスポート
pub use crate::mir::join_ir_ops::{JoinIrOpError, JoinValue};
@ -64,31 +66,32 @@ fn run_joinir_function_normalized_dev(
args: &[JoinValue],
) -> Result<JoinValue, JoinRuntimeError> {
// 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 shapes = shape_guard::supported_shapes(module);
if shapes.is_empty() {
if verbose {
eprintln!(
"[joinir/runner/normalized-dev] shape unsupported; staying on Structured path"
);
if debug {
eprintln!("[joinir/normalized-dev/runner] shape unsupported; staying on Structured path");
}
return execute_function(vm, module, entry, args_vec);
}
let structured_roundtrip = normalized_dev_roundtrip_structured(module)
.map_err(|msg| JoinRuntimeError::new(format!("[joinir/runner/normalized-dev] {}", msg)))?;
let structured_roundtrip = normalized_dev_roundtrip_structured(module).map_err(|msg| {
JoinRuntimeError::new(format!("[joinir/normalized-dev/runner] {}", msg))
})?;
if verbose {
if debug {
eprintln!(
"[joinir/runner/normalized-dev] normalized roundtrip succeeded (shapes={:?}, functions={})",
"[joinir/normalized-dev/runner] normalized roundtrip succeeded (shapes={:?}, functions={})",
shapes,
structured_roundtrip.functions.len()
);
}
execute_function(vm, &structured_roundtrip, entry, args_vec)
})
}
fn execute_function(

View File

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

View File

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

View File

@ -4,7 +4,6 @@ use super::super::join_func_name;
use super::super::JoinIrVmBridgeError;
use super::super::convert_mir_like_inst;
use crate::ast::Span;
use crate::config::env::joinir_dev_enabled;
use crate::mir::join_ir::normalized::{JpFuncId, JpFunction, JpInst, JpOp, NormalizedModule};
use crate::mir::join_ir::{JoinFuncId, JoinIrPhase, MirLikeInst};
use crate::mir::{
@ -20,19 +19,20 @@ pub(crate) fn lower_normalized_direct_minimal(
) -> Result<MirModule, JoinIrVmBridgeError> {
if norm.phase != JoinIrPhase::Normalized {
return Err(JoinIrVmBridgeError::new(
"[joinir/normalized-bridge] expected Normalized JoinIR module",
"[joinir/normalized-bridge/direct] expected Normalized JoinIR module",
));
}
let debug_dump = std::env::var("JOINIR_TEST_DEBUG").is_ok();
let verbose = joinir_dev_enabled();
if verbose {
eprintln!(
"[joinir/normalized-bridge/direct] lowering normalized module (functions={}, env_layouts={})",
let debug_dump = crate::mir::join_ir::normalized::dev_env::normalized_dev_logs_enabled();
super::log_dev(
"direct",
format!(
"using direct normalized bridge (functions={}, env_layouts={})",
norm.functions.len(),
norm.env_layouts.len()
),
false,
);
}
let mut mir_module = MirModule::new("joinir_normalized_direct".to_string());
@ -42,9 +42,9 @@ pub(crate) fn lower_normalized_direct_minimal(
}
if debug_dump {
eprintln!(
"[joinir/normalized-bridge/direct] produced MIR (debug dump): {:#?}",
mir_module
super::log_debug(
"direct",
format!("produced MIR (debug dump): {:#?}", mir_module),
);
}
@ -55,7 +55,6 @@ fn lower_normalized_function_direct(
func: &JpFunction,
norm: &NormalizedModule,
) -> Result<MirFunction, JoinIrVmBridgeError> {
let verbose = joinir_dev_enabled();
let env_fields = func
.env_layout
.and_then(|id| norm.env_layouts.iter().find(|layout| layout.id == id));
@ -108,12 +107,16 @@ fn lower_normalized_function_direct(
let mut current_insts: Vec<MirInstruction> = Vec::new();
let mut terminated = false;
if verbose {
eprintln!(
"[joinir/normalized-bridge/direct] lowering fn={} params={:?} remapped_params={:?} body_len={}",
func.name, params, remapped_params, func.body.len()
super::log_debug(
"direct",
format!(
"lowering fn={} params={:?} remapped_params={:?} body_len={}",
func.name,
params,
remapped_params,
func.body.len()
),
);
}
for inst in &func.body {
if terminated {
@ -122,6 +125,56 @@ fn lower_normalized_function_direct(
match inst {
JpInst::Let { dst, op, args } => {
if matches!(op, JpOp::Select) {
let cond = remap(*args.get(0).unwrap_or(&ValueId(0)), &mut value_map);
let then_val = remap(*args.get(1).unwrap_or(&ValueId(0)), &mut value_map);
let else_val = remap(*args.get(2).unwrap_or(&ValueId(0)), &mut value_map);
let remapped_dst = remap(*dst, &mut value_map);
let then_bb = BasicBlockId(next_block_id);
next_block_id += 1;
let else_bb = BasicBlockId(next_block_id);
next_block_id += 1;
let merge_bb = BasicBlockId(next_block_id);
next_block_id += 1;
finalize_block(
&mut mir_func,
current_block_id,
mem::take(&mut current_insts),
MirInstruction::Branch {
condition: cond,
then_bb,
else_bb,
},
None,
);
finalize_block(
&mut mir_func,
then_bb,
Vec::new(),
MirInstruction::Jump { target: merge_bb },
None,
);
finalize_block(
&mut mir_func,
else_bb,
Vec::new(),
MirInstruction::Jump { target: merge_bb },
None,
);
current_block_id = merge_bb;
current_insts = vec![MirInstruction::Phi {
dst: remapped_dst,
inputs: vec![(then_bb, then_val), (else_bb, else_val)],
type_hint: None,
}];
mir_func.next_value_id = value_map.len() as u32;
continue;
}
let remapped_dst = remap(*dst, &mut value_map);
let remapped_args = remap_vec(args, &mut value_map);
let mir_like = jp_op_to_mir_like(remapped_dst, op, &remapped_args)?;
@ -276,6 +329,17 @@ fn jp_op_to_mir_like(
lhs: args.get(0).copied().unwrap_or(ValueId(0)),
rhs: args.get(1).copied().unwrap_or(ValueId(0)),
}),
JpOp::Select => {
let cond = args.get(0).copied().unwrap_or(ValueId(0));
let then_val = args.get(1).copied().unwrap_or(ValueId(0));
let else_val = args.get(2).copied().unwrap_or(ValueId(0));
Ok(MirLikeInst::Select {
dst,
cond,
then_val,
else_val,
})
}
JpOp::BoxCall { box_name, method } => Ok(MirLikeInst::BoxCall {
dst: Some(dst),
box_name: box_name.clone(),

View File

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

View File

@ -6,9 +6,14 @@ use nyash_rust::mir::join_ir::{
normalized_pattern2_to_structured, BinOpKind, ConstValue, JoinContId, JoinFuncId,
JoinFunction, JoinInst, JoinIrPhase, JoinModule, MirLikeInst,
};
use nyash_rust::mir::join_ir::normalized::dev_env::NormalizedDevEnvGuard;
use nyash_rust::mir::join_ir::normalized::dev_env::{
normalized_dev_enabled, test_ctx, NormalizedDevEnvGuard, NormalizedTestContext,
};
use nyash_rust::mir::join_ir::normalized::fixtures::{
build_jsonparser_atoi_structured_for_normalized_dev,
build_jsonparser_atoi_real_structured_for_normalized_dev,
build_jsonparser_parse_number_real_structured_for_normalized_dev,
build_jsonparser_skip_ws_real_structured_for_normalized_dev,
build_jsonparser_skip_ws_structured_for_normalized_dev,
build_pattern2_break_fixture_structured, build_pattern2_minimal_structured,
};
@ -16,10 +21,19 @@ use nyash_rust::mir::join_ir_runner::run_joinir_function;
use nyash_rust::mir::join_ir_ops::JoinValue;
use nyash_rust::mir::join_ir_vm_bridge::run_joinir_via_vm;
use nyash_rust::mir::ValueId;
fn normalized_dev_test_ctx() -> NormalizedTestContext<'static> {
let ctx = test_ctx();
assert!(
normalized_dev_enabled(),
"Phase 40: normalized_dev must be enabled for normalized_* tests (feature + NYASH_JOINIR_NORMALIZED_DEV_RUN=1)"
);
ctx
}
fn assert_normalized_dev_ready() {
assert!(
nyash_rust::config::env::normalized_dev_enabled(),
"Phase 33: normalized_dev must be enabled for this suite (feature + env)"
normalized_dev_enabled(),
"Phase 40: normalized_dev must be enabled for normalized_* tests (feature + NYASH_JOINIR_NORMALIZED_DEV_RUN=1)"
);
}
@ -87,6 +101,7 @@ fn build_structured_pattern1() -> JoinModule {
#[test]
fn normalized_pattern1_minimal_smoke() {
let _ctx = normalized_dev_test_ctx();
let structured = build_structured_pattern1();
let normalized = normalize_pattern1_minimal(&structured);
assert_eq!(normalized.phase, JoinIrPhase::Normalized);
@ -103,6 +118,7 @@ fn normalized_pattern1_minimal_smoke() {
#[test]
fn normalized_pattern1_roundtrip_structured_equivalent() {
let _ctx = normalized_dev_test_ctx();
let structured = build_structured_pattern1();
let normalized = normalize_pattern1_minimal(&structured);
let reconstructed = normalized_pattern1_to_structured(&normalized);
@ -121,6 +137,7 @@ fn normalized_pattern1_roundtrip_structured_equivalent() {
#[test]
fn normalized_pattern1_exec_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_structured_pattern1();
let normalized = normalize_pattern1_minimal(&structured);
let reconstructed = normalized_pattern1_to_structured(&normalized);
@ -136,6 +153,7 @@ fn normalized_pattern1_exec_matches_structured() {
#[test]
fn normalized_pattern1_exec_matches_structured_roundtrip_backup() {
let _ctx = normalized_dev_test_ctx();
let structured = build_structured_pattern1();
let normalized = normalize_pattern1_minimal(&structured);
let reconstructed = normalized_pattern1_to_structured(&normalized);
@ -156,6 +174,7 @@ fn normalized_pattern1_exec_matches_structured_roundtrip_backup() {
#[test]
fn normalized_pattern2_roundtrip_structure() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern2_minimal_structured();
let normalized = normalize_pattern2_minimal(&structured);
assert_eq!(normalized.phase, JoinIrPhase::Normalized);
@ -175,8 +194,39 @@ fn normalized_pattern2_roundtrip_structure() {
}
}
#[test]
fn normalized_pattern2_jsonparser_parse_number_real_vm_bridge_direct_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_jsonparser_parse_number_real_structured_for_normalized_dev();
let entry = structured.entry.expect("structured entry required");
let cases = [
("42", 0, "42"),
("123abc", 0, "123"),
("9", 0, "9"),
("abc", 0, ""),
("xx7yy", 2, "7"),
("007", 0, "007"),
];
for (s, pos, expected) in cases {
let input = [JoinValue::Str(s.to_string()), JoinValue::Int(pos)];
let base = run_joinir_vm_bridge(&structured, entry, &input, false);
let dev = run_joinir_vm_bridge(&structured, entry, &input, true);
assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s);
assert_eq!(
dev,
JoinValue::Str(expected.to_string()),
"unexpected result for input '{}' (pos={}) (expected num_str)",
s,
pos
);
}
}
#[test]
fn normalized_pattern2_exec_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern2_minimal_structured();
let normalized = normalize_pattern2_minimal(&structured);
let reconstructed = normalized_pattern2_to_structured(&normalized);
@ -193,6 +243,7 @@ fn normalized_pattern2_exec_matches_structured() {
#[test]
#[should_panic(expected = "normalize_pattern2_minimal")]
fn normalized_pattern2_rejects_non_pattern2_structured() {
let _ctx = normalized_dev_test_ctx();
// Pattern1 Structured module should be rejected by Pattern2 normalizer.
let structured = build_structured_pattern1();
let _ = normalize_pattern2_minimal(&structured);
@ -200,6 +251,7 @@ fn normalized_pattern2_rejects_non_pattern2_structured() {
#[test]
fn normalized_pattern2_real_loop_roundtrip_structure() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern2_break_fixture_structured();
let normalized = normalize_pattern2_minimal(&structured);
let reconstructed = normalized_pattern2_to_structured(&normalized);
@ -221,6 +273,7 @@ fn normalized_pattern2_real_loop_roundtrip_structure() {
#[test]
fn normalized_pattern2_real_loop_exec_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern2_break_fixture_structured();
let normalized = normalize_pattern2_minimal(&structured);
let reconstructed = normalized_pattern2_to_structured(&normalized);
@ -246,6 +299,7 @@ fn normalized_pattern2_real_loop_exec_matches_structured() {
#[test]
fn normalized_pattern1_runner_dev_switch_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_structured_pattern1();
let entry = structured.entry.expect("structured entry required");
let input = [JoinValue::Int(7)];
@ -259,6 +313,7 @@ fn normalized_pattern1_runner_dev_switch_matches_structured() {
#[test]
fn normalized_pattern2_runner_dev_switch_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern2_break_fixture_structured();
let entry = structured.entry.expect("structured entry required");
let cases = [0, 1, 3, 5];
@ -281,6 +336,7 @@ fn normalized_pattern2_runner_dev_switch_matches_structured() {
#[test]
fn normalized_pattern2_jsonparser_runner_dev_switch_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_jsonparser_skip_ws_structured_for_normalized_dev();
let entry = structured.entry.expect("structured entry required");
let cases = [0, 1, 2, 5];
@ -297,6 +353,7 @@ fn normalized_pattern2_jsonparser_runner_dev_switch_matches_structured() {
#[test]
fn normalized_pattern2_vm_bridge_direct_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern2_break_fixture_structured();
let entry = structured.entry.expect("structured entry required");
let cases = [0, 1, 3, 5];
@ -319,6 +376,7 @@ fn normalized_pattern2_vm_bridge_direct_matches_structured() {
#[test]
fn normalized_pattern1_vm_bridge_direct_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_structured_pattern1();
let entry = structured.entry.expect("structured entry required");
let cases = [0, 5, 7];
@ -335,6 +393,7 @@ fn normalized_pattern1_vm_bridge_direct_matches_structured() {
#[test]
fn normalized_pattern2_jsonparser_vm_bridge_direct_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_jsonparser_skip_ws_structured_for_normalized_dev();
let entry = structured.entry.expect("structured entry required");
let cases = [0, 1, 2, 5];
@ -349,8 +408,37 @@ fn normalized_pattern2_jsonparser_vm_bridge_direct_matches_structured() {
}
}
#[test]
fn normalized_pattern2_jsonparser_skip_ws_real_vm_bridge_direct_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_jsonparser_skip_ws_real_structured_for_normalized_dev();
let entry = structured.entry.expect("structured entry required");
let cases = [
(" abc", 0, 3),
("abc", 0, 0),
(" \t\nx", 0, 3),
(" \t\nx", 2, 3),
];
for (s, pos, expected) in cases {
let input = [JoinValue::Str(s.to_string()), JoinValue::Int(pos)];
let base = run_joinir_vm_bridge(&structured, entry, &input, false);
let dev = run_joinir_vm_bridge(&structured, entry, &input, true);
assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s);
assert_eq!(
dev,
JoinValue::Int(expected),
"unexpected result for input '{}' (pos={})",
s,
pos
);
}
}
#[test]
fn normalized_pattern2_jsonparser_atoi_vm_bridge_direct_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_jsonparser_atoi_structured_for_normalized_dev();
let entry = structured.entry.expect("structured entry required");
let cases = [
@ -374,3 +462,48 @@ fn normalized_pattern2_jsonparser_atoi_vm_bridge_direct_matches_structured() {
);
}
}
#[test]
fn normalized_pattern2_jsonparser_atoi_real_vm_bridge_direct_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_jsonparser_atoi_real_structured_for_normalized_dev();
if nyash_rust::config::env::joinir_test_debug_enabled() {
eprintln!(
"[joinir/normalized-dev/test] structured jsonparser_atoi_real: {:#?}",
structured
);
let normalized = normalize_pattern2_minimal(&structured);
eprintln!(
"[joinir/normalized-dev/test] normalized jsonparser_atoi_real: {:#?}",
normalized
);
}
let entry = structured.entry.expect("structured entry required");
let cases = [
("42", 42),
("123abc", 123),
("007", 7),
("", 0),
("abc", 0),
("-42", -42),
("+7", 7),
("-0", 0),
("-12x", -12),
("+", 0),
("-", 0),
];
for (s, expected) in cases {
let input = [JoinValue::Str(s.to_string())];
let base = run_joinir_vm_bridge(&structured, entry, &input, false);
let dev = run_joinir_vm_bridge(&structured, entry, &input, true);
assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s);
assert_eq!(
dev,
JoinValue::Int(expected),
"unexpected result for input '{}'",
s
);
}
}