feat(llvm): Phase 131-4 P1 完了 - PHI ordering 修正(multi-pass architecture)

Phase 131-4 P1: PHI After Terminator Bug 修正

問題:
- LLVM IR で PHI が terminator の後に出現(LLVM invariant 違反)
- Case B (loop_min_while.hako) が TAG-EMIT で失敗

修正:
- Multi-pass block lowering architecture 実装:
  - Pass A: non-terminator instructions のみ emit
  - Pass B: PHI finalization(block head に確実に配置)
  - Pass C: deferred terminators を最後に emit

変更ファイル:
- src/llvm_py/builders/block_lower.py (~40行):
  - lower_blocks() で terminator を defer
  - lower_terminators() 新設(Pass C)
  - _deferred_terminators dict で管理
- src/llvm_py/builders/function_lower.py (3行):
  - Pass 順序更新: A→B→C
- src/llvm_py/instructions/ret.py (5行):
  - _disable_phi_synthesis flag で Pass C 中の PHI 生成を抑制

テスト結果:
- Case B EMIT:  (修正成功)
- Case B LINK:  (新 TAG-LINK: undefined nyash_console_log)
- Case A/B2:  (退行なし)

箱化モジュール化:
-  Multi-pass で責務分離
-  Flag mechanism で構造的制御
-  ハードコード禁止原則遵守

Next: Phase 131-5 (TAG-LINK 修正)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-14 06:12:31 +09:00
parent 5709026812
commit 73613dcef0
4 changed files with 138 additions and 30 deletions

View File

@ -1,10 +1,20 @@
from typing import Dict, Any, List
from typing import Dict, Any, List, Tuple
from llvmlite import ir
from trace import debug as trace_debug
from trace import phi_json as trace_phi_json
def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, Any]], order: List[int], loop_plan: Dict[str, Any] | None):
"""Lower blocks in multi-pass to ensure PHIs are always before terminators.
Phase 131-4: Multi-pass block lowering architecture
- Pass A: Lower non-terminator instructions only (terminators deferred)
- Pass B: PHI finalization happens in function_lower.py
- Pass C: Lower terminators (happens after PHI finalization)
This ensures LLVM IR invariant: PHI nodes must be at block head before any
other instructions, and terminators must be last.
"""
skipped: set[int] = set()
if loop_plan is not None:
try:
@ -218,19 +228,12 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
created_ids.append(dst)
except Exception:
pass
# Lower terminators
for inst in term_ops:
try:
trace_debug(f"[llvm-py] term op: {inst.get('op')} dst={inst.get('dst')} cond={inst.get('cond')}")
except Exception:
pass
try:
if bb.terminator is not None:
break
except Exception:
pass
ib.position_at_end(bb)
builder.lower_instruction(ib, inst, func)
# Phase 131-4 Pass A: DEFER terminators until after PHI finalization
# Store terminators for Pass C (will be lowered in lower_terminators)
if not hasattr(builder, '_deferred_terminators'):
builder._deferred_terminators = {}
if term_ops:
builder._deferred_terminators[bid] = (bb, term_ops)
try:
for vid in created_ids:
val = vmap_cur.get(vid)
@ -256,3 +259,48 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
delattr(builder, '_current_vmap')
except Exception:
pass
def lower_terminators(builder, func: ir.Function):
"""Phase 131-4 Pass C: Lower deferred terminators after PHI finalization.
This ensures PHI nodes are always at block heads before terminators are added,
maintaining LLVM IR's invariant: PHIs first, then other instructions, then terminators.
"""
if not hasattr(builder, '_deferred_terminators'):
return
deferred = builder._deferred_terminators
trace_debug(f"[llvm-py/pass-c] Lowering {len(deferred)} blocks with deferred terminators")
for bid, (bb, term_ops) in deferred.items():
ib = ir.IRBuilder(bb)
try:
builder.resolver.builder = ib
builder.resolver.module = builder.module
# Phase 131-4: Disable PHI synthesis during terminator lowering
# Terminators should only use values that already exist (from Pass A/B)
builder.resolver._disable_phi_synthesis = True
except Exception:
pass
for inst in term_ops:
try:
trace_debug(f"[llvm-py/pass-c] term op: {inst.get('op')} dst={inst.get('dst')} in bb{bid}")
except Exception:
pass
try:
if bb.terminator is not None:
# Terminator already exists (e.g., from loop lowering), skip
trace_debug(f"[llvm-py/pass-c] bb{bid} already has terminator, skipping")
break
except Exception:
pass
ib.position_at_end(bb)
builder.lower_instruction(ib, inst, func)
# Clean up deferred state
try:
delattr(builder, '_deferred_terminators')
except Exception:
pass

View File

@ -275,6 +275,7 @@ def lower_function(builder, func_data: Dict[str, Any]):
except Exception:
loop_plan = None
# Phase 131-4 Pass A: Lower non-terminator instructions (terminators deferred)
from builders.block_lower import lower_blocks as _lower_blocks
_lower_blocks(builder, func, block_by_id, order, loop_plan)
@ -299,9 +300,13 @@ def lower_function(builder, func_data: Dict[str, Any]):
except Exception:
pass
# Finalize PHIs for this function
# Phase 131-4 Pass B: Finalize PHIs (wires incoming edges)
_finalize_phis(builder)
# Phase 131-4 Pass C: Lower deferred terminators (after PHIs are placed)
from builders.block_lower import lower_terminators as _lower_terminators
_lower_terminators(builder, func)
# Safety pass: ensure every basic block ends with a terminator.
# This avoids llvmlite IR parse errors like "expected instruction opcode" on empty blocks.
try:

View File

@ -111,7 +111,11 @@ def lower_return(
ret_val = ir.Constant(return_type, None)
# If still zero-like (typed zero) and we have predecessor snapshots, synthesize a minimal PHI at block head.
# Phase 131-4: Skip PHI synthesis if disabled (e.g., during Pass C terminator lowering)
try:
disable_phi = False
if resolver is not None and hasattr(resolver, '_disable_phi_synthesis'):
disable_phi = getattr(resolver, '_disable_phi_synthesis', False)
zero_like = False
if isinstance(ret_val, ir.Constant):
if isinstance(return_type, ir.IntType):
@ -121,7 +125,7 @@ def lower_return(
elif isinstance(return_type, ir.PointerType):
zero_like = (str(ret_val) == str(ir.Constant(return_type, None)))
# Synthesize a PHI for return at the BLOCK HEAD (grouped), not inline.
if zero_like and preds is not None and block_end_values is not None and bb_map is not None and isinstance(value_id, int):
if not disable_phi and zero_like and preds is not None and block_end_values is not None and bb_map is not None and isinstance(value_id, int):
# Derive current block id from name like 'bb3'
cur_bid = None
try: