Files
hakorune/docs/development/current/main/design/normalized-expr-lowering.md
tomoaki 5e662eaaf6 fix(normalization): Phase 143 execution fix - Param region SSOT
Problem: normalized_helpers allocated env params as ValueId(1,2...)
in PHI Reserved region (0-99) instead of Param region (100-999)
per JoinValueSpace contract.

Root cause: All 4 normalized shadow modules started from
next_value_id=1, violating the Param region contract.

Solution:
- Add NormalizedHelperBox::alloc_env_params_param_region()
  that allocates params starting from PARAM_MIN (100)
- Update 4 normalized shadow files to use new API:
  - loop_true_if_break_continue.rs
  - loop_true_break_once.rs
  - if_as_last_join_k.rs
  - post_if_post_k.rs
- Fix instruction_rewriter.rs type mismatch
  (func.signature.params → func.params)

Verification:
- Unit tests: 69/69 PASS
- VM smoke: exit code 7 
- LLVM EXE smoke: exit code 7  (timeout resolved!)

ValueId Space Contract (Phase 201):
| Region       | Range    | Purpose                      |
|--------------|----------|------------------------------|
| PHI Reserved | 0-99     | Loop header PHI dst          |
| Param        | 100-999  | env params (flag, counter)   |
| Local        | 1000+    | Const, BinOp, condition      |

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-19 08:23:20 +09:00

9.7 KiB
Raw Blame History

Normalized Expression Lowering (ExprLowererBox)

Status: SSOT
Scope: Normalized shadow での expression loweringpure のみ)を、パターン総当たりではなく AST walker で一般化する。
Related:

  • docs/development/current/main/30-Backlog.md
  • docs/development/current/main/phases/phase-138/README.md
  • docs/development/current/main/phases/phase-140/README.md
  • docs/development/current/main/phases/phase-141/README.md
  • src/mir/control_tree/normalized_shadow/common/return_value_lowerer_box.rs
  • src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs
  • src/mir/control_tree/normalized_shadow/common/expr_lowering_contract.rs
  • src/mir/builder/control_flow/normalization/README.md

目的(なぜ必要か)

Phase 131138 で loop(true) 系の Normalized shadow を段階的に拡張したが、return/式の表現力を「形ごとのパターン追加」で増やし続けると、パターン爆発で持続不可能になる。

ここでの収束方針は次の通り:

  • 制御フローの骨格loop/if/post_k/continuationは正規化段階投入で固める
  • return value を含むは一般化AST walkerで受ける

非目標

  • Call/MethodCall の一般化effects + typing を含む)を Phase 140 でやらない
    → Phase 141+ に分離して段階投入する。
  • PHI 生成を Normalized 側に戻すことPHI-free 維持。必要なら後段に押し出す)

SSOT: NormalizedExprLowererBox

役割

NormalizedExprLowererBox は、pure expression を JoinIRNormalized dialectへ lowering する SSOT になる。

  • 入力: ASTAstNodeHandle / ASTNode+ envBTreeMap<String, ValueId>+ 出力先 Vec<JoinInst> + next_value_id
  • 出力: Result<Option<ValueId>, String>
    • Ok(Some(vid)): lowering 成功(vid が式の値)
    • Ok(None): out-of-scope既存経路へフォールバック、既定挙動不変
    • Err(...): 内部不整合のみstrict では fail-fast

Pure Expression の定義Phase 140 の範囲)

副作用がなく、Control-flow を内包しない式のみ。

  • Variable
  • LiteralInteger/Bool を主対象。String/Float は必要になったら追加)
  • UnaryOpnot, -
  • BinaryOp+ - * /
  • Compare== < <= > >= 等の比較系)

Call/MethodCall は対象外Phase 141+)。

ReturnValueLowererBox の縮退方針

Phase 138 時点では ReturnValueLowererBox が return の形var/int/addを直接扱っている。

Phase 140 以降は、ReturnValueLowererBox を「return 構文None/Someを処理する薄い箱」に縮退し、実体は NormalizedExprLowererBox に委譲する方針とする。

実装 SSOT:

  • src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs
  • src/mir/control_tree/normalized_shadow/common/return_value_lowerer_box.rs

収束ロードマップ

  • Phase 139: if-only post_if_post_k.rs の return を ReturnValueLowererBox へ統一(出口 SSOT 完成)
  • Phase 140: NormalizedExprLowererBox 初版pure のみ導入、return は ExprLowerer 経由へ寄せる
  • Phase 141 P0: impure 拡張点contractを SSOT 化Call/MethodCall はまだ out-of-scope
  • Phase 141 P1: “既知 intrinsic だけ” を許可して段階投入(それ以外は out-of-scope のまま)
  • Phase 141 P2+: Call/MethodCall を effects + typing で分離しつつ段階投入

パターン爆発の分解(実務)

パターン爆発には “別種” が混ざるため、対策も分ける。

  • return 値 / 右辺)の爆発: return 7, return x+2, return x+y, return (x+2)+3
    • 対策: ExprLowererAST walkerの対応ードを増やす 1 箇所に収束させる。
  • 制御loop/if/break/continueの爆発: loop(true){break}loop(true){if(...) break}
    • 対策: ControlTree/StepTree の語彙If/Loop/Exitを増やし、同じ lowering に通す。
    • “新パターン” ではなく “語彙追加” にするのが収束の鍵。

正規化単位NormalizationPlan の粒度)

現状は NormalizationPlanBox が block suffix を単位として正規化loop + post assigns + return など)を扱う。

将来の収束案として、正規化単位を statementloop 1 個) に寄せ、post/return は通常の lowering に任せる選択肢がある。

  • 利点: suffix パターン増殖の圧力が下がる
  • 注意: 既存の Phase 132138 の成果post_k/DirectValue/exit reconnectと整合する形で段階移行すること
  • 既定方針: Phase 140pure exprで ExprLowerer を確立した後に再検討するPhase 141+ の設計判断)

受け入れ基準(設計)

  • "形ごとの if 分岐追加" ではなく、AST walker の対応ノード追加が主たる拡張点になっている
  • out-of-scope は Ok(None) でフォールバックし、既定挙動不変を維持する
  • JoinIR merge は by-name 推測をせず、boundary/contract を SSOT として従う

Available Inputs SSOT (Phase 141 P1.5)

問題Problem

Suffix normalization couldn't access prefix-built local variables (e.g., s = "abc").

Bug scenario:

s = "abc"          // Prefix: builder.variable_map["s"] = ValueId(42)
flag = 1
if flag == 1 {     // Suffix normalization starts
    s = s
}
return s.length()  // ERROR: 's' not in available_inputs!

Root cause: AvailableInputsCollectorBox::collect(builder, None) only collected function params and CapturedEnv, missing prefix-built local variables.

解決策Solution

3-source merge with priority order:

  1. Function params (highest priority - from scope_ctx.function_param_names)
  2. Prefix variables (medium priority - from builder.variable_map)
  3. CapturedEnv (lowest priority - from closure captures)

Contract

AvailableInputsCollectorBox::collect(
    builder: &MirBuilder,
    captured_env: Option<&CapturedEnv>,
    prefix_variables: Option<&BTreeMap<String, ValueId>>, // NEW: Phase 141 P1.5
) -> BTreeMap<String, ValueId>

Usage

// At call site (e.g., stmts.rs)
let prefix_variables = Some(&self.variable_ctx.variable_map);

// Execute normalization
NormalizationExecuteBox::execute(
    builder,
    &plan,
    remaining,
    func_name,
    debug,
    prefix_variables,  // Pass through
)?;

// Inside execute_loop_only/execute_loop_with_post
let available_inputs = AvailableInputsCollectorBox::collect(
    builder,
    None,
    prefix_variables,  // Merge prefix variables
);

Edge Cases

  • Variable shadowing: Function params override prefix variables
  • Empty prefix: prefix_variables = None (e.g., suffix with no prefix execution)
  • Mutation: variable_map is cloned/borrowed, not moved

SSOT Location

  • Implementation: src/mir/control_tree/normalized_shadow/available_inputs_collector.rs
  • Call sites:
    • src/mir/builder/control_flow/normalization/execute_box.rs (execute_loop_only, execute_loop_with_post)
    • src/mir/builder/control_flow/joinir/patterns/policies/normalized_shadow_suffix_router_box.rs
    • src/mir/builder/stmts.rs (build_block suffix router call)

Known Intrinsic SSOT (Phase 141 P1.5)

目的

“既知 intrinsic だけ” の段階投入を、by-name の直書き増殖ではなく SSOT registry に収束させる。

SSOT

  • Registry: src/mir/control_tree/normalized_shadow/common/known_intrinsics.rs
    • KnownIntrinsicRegistryBox::{lookup,get_spec}
  • Marker enum: src/mir/control_tree/normalized_shadow/common/expr_lowering_contract.rs
    • KnownIntrinsic は “識別子” のみmetadata は registry に集約)

Diagnostics

  • OutOfScopeReason::IntrinsicNotWhitelisted を追加し、MethodCall の out-of-scope 理由を精密化する。

ValueId Space Contract (Phase 143 fix)

問題

Normalized shadow modules allocate env params using alloc_value_id() starting from 1, but JoinValueSpace contract requires Param region (100-999).

Wrong (before fix):

let mut next_value_id: u32 = 1;
let params = NormalizedHelperBox::alloc_env_params(&fields, &mut next_value_id);
// → [ValueId(1), ValueId(2), ...] — PHI Reserved region!

Correct (after fix):

let (params, mut next_local) = NormalizedHelperBox::alloc_env_params_param_region(&fields);
// → [ValueId(100), ValueId(101), ...] — Param region ✅

Contract (Phase 201 SSOT)

 0          100        1000                     u32::MAX
 ├──────────┼──────────┼──────────────────────────┤
 │  PHI     │  Param   │       Local             │
 │  Reserved│  Region  │       Region            │
 └──────────┴──────────┴──────────────────────────┘
Region Range Purpose
PHI Reserved 0-99 Loop header PHI dst
Param 100-999 env params (flag, counter, etc.)
Local 1000+ Const, BinOp, condition results

SSOT

  • Constants: src/mir/join_ir/lowering/join_value_space.rs
    • PARAM_MIN = 100
    • LOCAL_MIN = 1000
  • API: NormalizedHelperBox::alloc_env_params_param_region()
    • Location: src/mir/control_tree/normalized_shadow/common/normalized_helpers.rs
    • Returns: (Vec<ValueId>, u32) — (params in 100+ range, next_local starting at 1000)

Affected Files

  • loop_true_if_break_continue.rs
  • loop_true_break_once.rs
  • if_as_last_join_k.rs
  • post_if_post_k.rs

All normalized shadow modules must use alloc_env_params_param_region() instead of alloc_env_params() to ensure env params are in the correct region.