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:
@ -8,7 +8,7 @@
|
|||||||
| Case | File | Emit | Link | Run | Notes |
|
| Case | File | Emit | Link | Run | Notes |
|
||||||
|------|------|------|------|-----|-------|
|
|------|------|------|------|-----|-------|
|
||||||
| A | `apps/tests/phase87_llvm_exe_min.hako` | ✅ | ✅ | ✅ | **PASS** - Simple return 42, no BoxCall, exit code verified |
|
| A | `apps/tests/phase87_llvm_exe_min.hako` | ✅ | ✅ | ✅ | **PASS** - Simple return 42, no BoxCall, exit code verified |
|
||||||
| B | `apps/tests/loop_min_while.hako` | ❌ | - | - | **TAG-EMIT** - Loop generates invalid LLVM IR (observed: PHI placement/order issue; also mentions empty block) |
|
| B | `apps/tests/loop_min_while.hako` | ✅ | ❌ | - | **TAG-LINK** - EMIT fixed (Phase 131-4), LINK fails (undefined nyash_console_log) |
|
||||||
| B2 | `/tmp/case_b_simple.hako` | ✅ | ✅ | ✅ | **PASS** - Simple print(42) without loop works |
|
| B2 | `/tmp/case_b_simple.hako` | ✅ | ✅ | ✅ | **PASS** - Simple print(42) without loop works |
|
||||||
| C | `apps/tests/llvm_stage3_loop_only.hako` | ❌ | - | - | **TAG-EMIT** - Complex loop (break/continue) fails JoinIR pattern matching |
|
| C | `apps/tests/llvm_stage3_loop_only.hako` | ❌ | - | - | **TAG-EMIT** - Complex loop (break/continue) fails JoinIR pattern matching |
|
||||||
|
|
||||||
@ -72,9 +72,51 @@ This strongly suggests an **emission ordering / insertion-position** problem in
|
|||||||
- PHI insertion rules + debug: `src/llvm_py/phi_wiring/wiring.py` (`NYASH_PHI_ORDERING_DEBUG=1`)
|
- PHI insertion rules + debug: `src/llvm_py/phi_wiring/wiring.py` (`NYASH_PHI_ORDERING_DEBUG=1`)
|
||||||
- “Empty block” safety pass (separate concern): `src/llvm_py/builders/function_lower.py:_enforce_terminators`
|
- “Empty block” safety pass (separate concern): `src/llvm_py/builders/function_lower.py:_enforce_terminators`
|
||||||
|
|
||||||
|
**✅ FIXED (Phase 131-4)**: Multi-pass block lowering architecture
|
||||||
|
|
||||||
|
**Solution implemented**:
|
||||||
|
- **Pass A**: Lower non-terminator instructions (body ops only)
|
||||||
|
- **Pass B**: Finalize PHIs (wire incoming edges) - happens in `function_lower.py`
|
||||||
|
- **Pass C**: Lower deferred terminators (after PHIs are placed)
|
||||||
|
|
||||||
|
**Key changes**:
|
||||||
|
1. `src/llvm_py/builders/block_lower.py`:
|
||||||
|
- Split `lower_blocks()` to defer terminators
|
||||||
|
- Added `lower_terminators()` function for Pass C
|
||||||
|
- Deferred terminators stored in `builder._deferred_terminators`
|
||||||
|
|
||||||
|
2. `src/llvm_py/builders/function_lower.py`:
|
||||||
|
- Updated pass ordering: Pass A → Pass B → Pass C
|
||||||
|
- Added call to `_lower_terminators()` after `_finalize_phis()`
|
||||||
|
|
||||||
|
3. `src/llvm_py/instructions/ret.py`:
|
||||||
|
- Added `_disable_phi_synthesis` flag check
|
||||||
|
- Prevents PHI creation during Pass C (terminators should only use existing values)
|
||||||
|
|
||||||
|
**Result**:
|
||||||
|
- Case B EMIT now succeeds ✅
|
||||||
|
- Generated LLVM IR is valid (PHIs before terminators)
|
||||||
|
- No regression in Cases A and B2
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. TAG-EMIT: JoinIR Pattern Mismatch (Case C)
|
### 2. TAG-LINK: Missing runtime symbols (Case B)
|
||||||
|
|
||||||
|
**File**: `apps/tests/loop_min_while.hako`
|
||||||
|
|
||||||
|
**Link Error**:
|
||||||
|
```
|
||||||
|
/usr/bin/ld: /home/tomoaki/git/hakorune-selfhost/target/aot_objects/loop_min_while.o: in function `condition_fn':
|
||||||
|
<string>:(.text+0x99): undefined reference to `nyash_console_log'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Root Cause**: ExternCall lowering emits calls to runtime functions (e.g., `nyash_console_log`) but these symbols are not provided by NyKernel (`libnyash_kernel.a`).
|
||||||
|
|
||||||
|
**Next Steps**: Map ExternCall names to actual NyKernel symbols or add missing runtime functions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. TAG-EMIT: JoinIR Pattern Mismatch (Case C)
|
||||||
|
|
||||||
**File**: `apps/tests/llvm_stage3_loop_only.hako`
|
**File**: `apps/tests/llvm_stage3_loop_only.hako`
|
||||||
|
|
||||||
@ -127,24 +169,33 @@ Hint: This loop pattern is not supported. All loops must use JoinIR lowering.
|
|||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
### Priority 1: Fix TAG-EMIT (PHI After Terminator Bug) ⚠️ CRITICAL
|
### ✅ Priority 1: COMPLETED - Fix TAG-EMIT (PHI After Terminator Bug)
|
||||||
**Target**: Case B (`loop_min_while.hako`)
|
**Target**: Case B (`loop_min_while.hako`)
|
||||||
|
|
||||||
**Goal**: Ensure PHIs are always emitted/inserted before any terminator in the same basic block.
|
**Status**: ✅ FIXED in Phase 131-4 (see Root Cause #1 above)
|
||||||
|
|
||||||
**Candidate approach** (docs-only; implementation to be decided):
|
**Result**: Case B EMIT now succeeds. LINK still fails (TAG-LINK), but that's a separate issue (Priority 2).
|
||||||
- Split lowering into multi-pass so that PHI placeholders exist before terminators are emitted, or delay terminator emission until after PHI finalization:
|
|
||||||
- (A) Predeclare PHIs at block creation time (placeholders), then emit body ops, then wire incomings, then emit terminators.
|
|
||||||
- (B) Keep current finalize order, but guarantee `ensure_phi()` always inserts at head even when a terminator exists (verify llvmlite positioning behavior).
|
|
||||||
|
|
||||||
**Primary files to look at for the fix**:
|
|
||||||
- `src/llvm_py/builders/function_lower.py` (pass ordering)
|
|
||||||
- `src/llvm_py/builders/block_lower.py` (terminator emission split point)
|
|
||||||
- `src/llvm_py/phi_wiring/wiring.py` (PHI insertion positioning)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Priority 2: Fix TAG-EMIT (JoinIR Pattern Coverage)
|
### Priority 2: Fix TAG-LINK (Missing Runtime Symbols)
|
||||||
|
**Target**: Case B (`loop_min_while.hako`)
|
||||||
|
|
||||||
|
**Approach**:
|
||||||
|
1. Identify all ExternCall lowering paths in Python harness
|
||||||
|
2. Map to actual NyKernel symbols (e.g., `nyash_console_log` → `ny_console_log` or similar)
|
||||||
|
3. Update ExternCall lowering to use correct symbol names
|
||||||
|
4. OR: Add wrapper functions in NyKernel to provide missing symbols
|
||||||
|
|
||||||
|
**Files**:
|
||||||
|
- `src/llvm_py/instructions/externcall.py` - ExternCall lowering
|
||||||
|
- `crates/nyash_kernel/src/lib.rs` - NyKernel runtime symbols
|
||||||
|
|
||||||
|
**Expected**: Case B should LINK ✅ RUN ✅ after fix
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Priority 3: Fix TAG-EMIT (JoinIR Pattern Coverage)
|
||||||
**Target**: Case C (`llvm_stage3_loop_only.hako`)
|
**Target**: Case C (`llvm_stage3_loop_only.hako`)
|
||||||
|
|
||||||
**Approach**:
|
**Approach**:
|
||||||
|
|||||||
@ -1,10 +1,20 @@
|
|||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Tuple
|
||||||
from llvmlite import ir
|
from llvmlite import ir
|
||||||
from trace import debug as trace_debug
|
from trace import debug as trace_debug
|
||||||
from trace import phi_json as trace_phi_json
|
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):
|
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()
|
skipped: set[int] = set()
|
||||||
if loop_plan is not None:
|
if loop_plan is not None:
|
||||||
try:
|
try:
|
||||||
@ -218,19 +228,12 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
|
|||||||
created_ids.append(dst)
|
created_ids.append(dst)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# Lower terminators
|
# Phase 131-4 Pass A: DEFER terminators until after PHI finalization
|
||||||
for inst in term_ops:
|
# Store terminators for Pass C (will be lowered in lower_terminators)
|
||||||
try:
|
if not hasattr(builder, '_deferred_terminators'):
|
||||||
trace_debug(f"[llvm-py] term op: {inst.get('op')} dst={inst.get('dst')} cond={inst.get('cond')}")
|
builder._deferred_terminators = {}
|
||||||
except Exception:
|
if term_ops:
|
||||||
pass
|
builder._deferred_terminators[bid] = (bb, term_ops)
|
||||||
try:
|
|
||||||
if bb.terminator is not None:
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
ib.position_at_end(bb)
|
|
||||||
builder.lower_instruction(ib, inst, func)
|
|
||||||
try:
|
try:
|
||||||
for vid in created_ids:
|
for vid in created_ids:
|
||||||
val = vmap_cur.get(vid)
|
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')
|
delattr(builder, '_current_vmap')
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
||||||
|
|||||||
@ -275,6 +275,7 @@ def lower_function(builder, func_data: Dict[str, Any]):
|
|||||||
except Exception:
|
except Exception:
|
||||||
loop_plan = None
|
loop_plan = None
|
||||||
|
|
||||||
|
# Phase 131-4 Pass A: Lower non-terminator instructions (terminators deferred)
|
||||||
from builders.block_lower import lower_blocks as _lower_blocks
|
from builders.block_lower import lower_blocks as _lower_blocks
|
||||||
_lower_blocks(builder, func, block_by_id, order, loop_plan)
|
_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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Finalize PHIs for this function
|
# Phase 131-4 Pass B: Finalize PHIs (wires incoming edges)
|
||||||
_finalize_phis(builder)
|
_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.
|
# Safety pass: ensure every basic block ends with a terminator.
|
||||||
# This avoids llvmlite IR parse errors like "expected instruction opcode" on empty blocks.
|
# This avoids llvmlite IR parse errors like "expected instruction opcode" on empty blocks.
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -111,7 +111,11 @@ def lower_return(
|
|||||||
ret_val = ir.Constant(return_type, None)
|
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.
|
# 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:
|
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
|
zero_like = False
|
||||||
if isinstance(ret_val, ir.Constant):
|
if isinstance(ret_val, ir.Constant):
|
||||||
if isinstance(return_type, ir.IntType):
|
if isinstance(return_type, ir.IntType):
|
||||||
@ -121,7 +125,7 @@ def lower_return(
|
|||||||
elif isinstance(return_type, ir.PointerType):
|
elif isinstance(return_type, ir.PointerType):
|
||||||
zero_like = (str(ret_val) == str(ir.Constant(return_type, None)))
|
zero_like = (str(ret_val) == str(ir.Constant(return_type, None)))
|
||||||
# Synthesize a PHI for return at the BLOCK HEAD (grouped), not inline.
|
# 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'
|
# Derive current block id from name like 'bb3'
|
||||||
cur_bid = None
|
cur_bid = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user