Files
hakorune/docs/development/current/main/investigations/phase131-12-p1-trace-summary.md
nyash-codex 7dfd6ff1d9 feat(llvm): Phase 131-11-H/12 - ループキャリアPHI型修正 & vmap snapshot SSOT
## 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>
2025-12-14 21:28:41 +09:00

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

  1. Pass A: Create vmap_cur for block
  2. Pass A: Lower body instructions → writes go to vmap_cur
  3. Pass A: Store terminators for later
  4. Pass A: Delete _current_vmap ← THE BUG
  5. Pass C: Lower terminators → fallback to owner.vmap (different object!)
  6. 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! ❌

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
# 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)
  • 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

  1. Implement Option 3 fix (store vmap_cur in deferred tuple)
  2. Add Fail-Fast check in instruction_lower.py (detect missing _current_vmap)
  3. Verify with trace (consistent IDs across Pass A→C)
  4. Run full test suite (ensure VM/LLVM parity)
  5. 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 getattr fallbacks - 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_values is snapshot SSOT
  • _current_vmap is 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.