## Phase 131-11-H: ループキャリアPHI型修正 - PHI生成時に初期値(entry block)の型のみ使用 - backedge の値を型推論に使わない(循環依存回避) - NYASH_CARRIER_PHI_DEBUG=1 でトレース ## Phase 131-12-P0: def_blocks 登録 & STRICT エラー化 - safe_vmap_write() で PHI 上書き保護 - resolver miss を STRICT でエラー化(フォールバック 0 禁止) - def_blocks 自動登録 ## Phase 131-12-P1: vmap_cur スナップショット実装 - DeferredTerminator 構造体(block, term_ops, vmap_snapshot) - Pass A で vmap_cur をスナップショット - Pass C でスナップショット復元(try-finally) - STRICT モード assert ## 結果 - ✅ MIR PHI型: Integer(正しい) - ✅ VM: Result: 3 - ✅ vmap snapshot 機構: 動作確認 - ⚠️ LLVM: Result: 0(別のバグ、次Phase で調査) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6.6 KiB
6.6 KiB
Phase 131-12-P1: vmap Object Identity Trace - Summary
Status: ✅ Root Cause Identified
Date: 2025-12-14
Investigation: vmap_cur object identity issue causing wrong values in LLVM backend
Result: Hypothesis C confirmed - Object identity problem in Pass A→C temporal coupling
Critical Discovery
The Smoking Gun
# Pass A (block_lower.py line 168)
builder._current_vmap = vmap_cur # ← Create per-block vmap
# Pass A (block_lower.py line 240)
builder._deferred_terminators[bid] = (bb, term_ops) # ← Defer terminators
# Pass A (block_lower.py line 265)
delattr(builder, '_current_vmap') # ← DELETE vmap_cur ❌
# Pass C (lower_terminators, line 282)
# When lowering deferred terminators:
vmap_ctx = getattr(owner, '_current_vmap', owner.vmap) # ← Falls back to global vmap! ❌
Problem: Pass A deletes _current_vmap before Pass C runs, causing terminators to use the wrong vmap object.
Trace Evidence
bb1 block creation: vmap_ctx id=140506427346368 ← Creation
bb1 const instruction: vmap_ctx id=140506427346368 ← Same (good)
bb1 ret terminator: vmap_ctx id=140506427248448 ← DIFFERENT (bad!)
^^^^^^^^^^^^^^
This is owner.vmap, not vmap_cur!
Impact: Values written to vmap_cur in Pass A are invisible to terminators in Pass C.
The Bug Flow
- Pass A: Create
vmap_curfor block - Pass A: Lower body instructions → writes go to
vmap_cur - Pass A: Store terminators for later
- Pass A: Delete
_current_vmap← THE BUG - Pass C: Lower terminators → fallback to
owner.vmap(different object!) - Result: Terminators read from wrong vmap, missing all Pass A writes
Proof: Per-Block vs Global vmap
Expected (Per-Block Context)
vmap_cur = {...} # Block-local SSA values
builder._current_vmap = vmap_cur
# All instructions in this block use the SAME object
Actual (Broken State)
vmap_cur = {...} # Block-local SSA values
builder._current_vmap = vmap_cur # Pass A body instructions use this
# Pass A ends
delattr(builder, '_current_vmap') # DELETED!
# Pass C starts
vmap_ctx = owner.vmap # Falls back to GLOBAL vmap (different object!)
# Terminators see different data than body instructions! ❌
Fix Options (Recommended: Option 3)
Option 1: Don't Delete Until Pass C Completes
- Quick fix but creates temporal coupling
- Harder to reason about state lifetime
Option 2: Read from block_end_values SSOT
- Good: Uses snapshot as source of truth
- Issue: Requires restoring to builder state
Option 3: Store vmap_cur in Deferred Data (RECOMMENDED)
# Pass A (line 240)
builder._deferred_terminators[bid] = (bb, term_ops, vmap_cur) # ← Add vmap_cur
# Pass C (line 282)
for bid, (bb, term_ops, vmap_ctx) in deferred.items():
builder._current_vmap = vmap_ctx # ← Restore exact context
# Lower terminators with correct vmap
Why Option 3?
- Explicit ownership: vmap_cur is passed through deferred tuple
- No temporal coupling: Pass C gets exact context from Pass A
- SSOT principle: One source of vmap per block
- Fail-Fast: Type error if tuple structure changes
Architecture Impact
Current Problem
- Temporal Coupling: Pass C depends on Pass A's ephemeral state
- Silent Fallback: Wrong vmap used without error
- Hidden Sharing: Global vmap shared across blocks
Fixed Architecture (Box-First)
Pass A: Create vmap_cur (per-block "box")
↓
Store in deferred tuple (explicit ownership transfer)
↓
Pass C: Restore vmap_cur from tuple (unpack "box")
↓
Use exact same object (SSOT)
Aligns with CLAUDE.md principles:
- ✅ Box-First: vmap_cur is a "box" passed between passes
- ✅ SSOT: One vmap per block, explicit transfer
- ✅ Fail-Fast: Type error if deferred tuple changes
Test Commands
Verify Fix
# Before fix: Shows different IDs for terminator
NYASH_LLVM_VMAP_TRACE=1 NYASH_LLVM_USE_HARNESS=1 \
./target/release/hakorune --backend llvm apps/tests/llvm_stage3_loop_only.hako 2>&1 | \
grep "\[vmap/id\]"
# After fix: Should show SAME ID throughout block
Full Verification
# Check full execution
NYASH_LLVM_VMAP_TRACE=1 NYASH_LLVM_USE_HARNESS=1 \
./target/release/hakorune --backend llvm apps/tests/llvm_stage3_loop_only.hako
# Expected: Result: 3 (matching VM)
Files Modified
Trace Implementation (Phase 131-12-P1)
/home/tomoaki/git/hakorune-selfhost/src/llvm_py/builders/block_lower.py/home/tomoaki/git/hakorune-selfhost/src/llvm_py/builders/instruction_lower.py/home/tomoaki/git/hakorune-selfhost/src/llvm_py/instructions/const.py/home/tomoaki/git/hakorune-selfhost/src/llvm_py/instructions/copy.py/home/tomoaki/git/hakorune-selfhost/src/llvm_py/instructions/binop.py/home/tomoaki/git/hakorune-selfhost/src/llvm_py/utils/values.py
Fix Target (Next Phase)
/home/tomoaki/git/hakorune-selfhost/src/llvm_py/builders/block_lower.py(Option 3)
Related Documents
- Investigation:
/docs/development/current/main/investigations/phase131-12-case-c-llvm-wrong-result.md - Detailed Analysis:
/docs/development/current/main/investigations/phase131-12-p1-vmap-identity-analysis.md - LLVM Inventory:
/docs/development/current/main/phase131-3-llvm-lowering-inventory.md - Environment Variables:
/docs/reference/environment-variables.md
Next Steps
- Implement Option 3 fix (store vmap_cur in deferred tuple)
- Add Fail-Fast check in instruction_lower.py (detect missing _current_vmap)
- Verify with trace (consistent IDs across Pass A→C)
- Run full test suite (ensure VM/LLVM parity)
- Document pattern (for future multi-pass architectures)
Lessons Learned
Box-First Principle Application
- Mutable builder state (
_current_vmap) should be explicitly passed through phases - Don't rely on
getattrfallbacks - they hide bugs - Per-block context is a "box" - treat it as first-class data
Fail-Fast Opportunity
# BEFORE (silent fallback)
vmap_ctx = getattr(owner, '_current_vmap', owner.vmap) # Wrong vmap silently used
# AFTER (fail-fast)
vmap_ctx = getattr(owner, '_current_vmap', None)
if vmap_ctx is None:
raise RuntimeError("Pass A/C timing bug: _current_vmap not set")
SSOT Enforcement
block_end_valuesis snapshot SSOT_current_vmapis working buffer- Pass C should restore working buffer from SSOT or deferred data
Investigation Complete: Root cause identified with high confidence. Ready for fix implementation.