## 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>
198 lines
6.6 KiB
Markdown
198 lines
6.6 KiB
Markdown
# 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
|
|
|
|
```python
|
|
# 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)
|
|
```python
|
|
vmap_cur = {...} # Block-local SSA values
|
|
builder._current_vmap = vmap_cur
|
|
# All instructions in this block use the SAME object
|
|
```
|
|
|
|
### Actual (Broken State)
|
|
```python
|
|
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)
|
|
```python
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
|
|
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
|
|
```python
|
|
# 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.
|