diff --git a/docs/development/current/main/phase131-3-llvm-lowering-inventory.md b/docs/development/current/main/phase131-3-llvm-lowering-inventory.md index 65535457..1389560d 100644 --- a/docs/development/current/main/phase131-3-llvm-lowering-inventory.md +++ b/docs/development/current/main/phase131-3-llvm-lowering-inventory.md @@ -8,7 +8,7 @@ | Case | File | Emit | Link | Run | Notes | |------|------|------|------|-----|-------| | 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 | | 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`) - “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': +:(.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` @@ -127,24 +169,33 @@ Hint: This loop pattern is not supported. All loops must use JoinIR lowering. ## 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`) -**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): -- 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) +**Result**: Case B EMIT now succeeds. LINK still fails (TAG-LINK), but that's a separate issue (Priority 2). --- -### 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`) **Approach**: diff --git a/src/llvm_py/builders/block_lower.py b/src/llvm_py/builders/block_lower.py index 95e105d8..8a88afc7 100644 --- a/src/llvm_py/builders/block_lower.py +++ b/src/llvm_py/builders/block_lower.py @@ -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 diff --git a/src/llvm_py/builders/function_lower.py b/src/llvm_py/builders/function_lower.py index 4639445b..92b9be1f 100644 --- a/src/llvm_py/builders/function_lower.py +++ b/src/llvm_py/builders/function_lower.py @@ -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: diff --git a/src/llvm_py/instructions/ret.py b/src/llvm_py/instructions/ret.py index 27ca80e8..dd6815a0 100644 --- a/src/llvm_py/instructions/ret.py +++ b/src/llvm_py/instructions/ret.py @@ -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: