188 lines
6.3 KiB
Markdown
188 lines
6.3 KiB
Markdown
|
|
# Phase 131-12-P1: vmap Object Identity Trace Analysis
|
||
|
|
|
||
|
|
## Executive Summary
|
||
|
|
|
||
|
|
**Status**: ⚠️ Hypothesis C (Object Identity Problem) - **PARTIALLY CONFIRMED**
|
||
|
|
|
||
|
|
### Key Findings
|
||
|
|
|
||
|
|
1. **vmap_ctx identity changes between blocks**:
|
||
|
|
- bb1: `vmap_ctx id=140506427346368` (creation)
|
||
|
|
- bb1 ret: `vmap_ctx id=140506427248448` (DIFFERENT!)
|
||
|
|
- bb2: `vmap_ctx id=140506427351808` (new object)
|
||
|
|
|
||
|
|
2. **Trace stopped early** - execution crashed before reaching critical bb3/exit blocks
|
||
|
|
|
||
|
|
3. **No v17 writes detected** - the problematic value was never written
|
||
|
|
|
||
|
|
## Detailed Trace Analysis
|
||
|
|
|
||
|
|
### Block 1 Trace Sequence
|
||
|
|
|
||
|
|
```
|
||
|
|
[vmap/id] bb1 vmap_cur id=140506427346368 keys=[0] # ← Block creation
|
||
|
|
[vmap/id] instruction op=const vmap_ctx id=140506427346368 # ← Same object ✅
|
||
|
|
[vmap/id] const dst=1 vmap id=140506427346368 before_write # ← Same object ✅
|
||
|
|
[vmap/write] dst=1 written, vmap.keys()=[0, 1] # ← Write successful ✅
|
||
|
|
[vmap/id] instruction op=ret vmap_ctx id=140506427248448 # ← DIFFERENT OBJECT! ❌
|
||
|
|
```
|
||
|
|
|
||
|
|
**Problem Found**: The `vmap_ctx` object changed identity **within the same block**!
|
||
|
|
- Creation: `140506427346368`
|
||
|
|
- Terminator: `140506427248448`
|
||
|
|
|
||
|
|
### Block 2 Trace Sequence
|
||
|
|
|
||
|
|
```
|
||
|
|
[vmap/id] bb2 vmap_cur id=140506427351808 keys=[] # ← New block (expected)
|
||
|
|
[vmap/id] instruction op=const vmap_ctx id=140506427351808 # ← Consistent ✅
|
||
|
|
[vmap/write] dst=1 written, vmap.keys()=[1] # ← Write successful ✅
|
||
|
|
[vmap/id] instruction op=const vmap_ctx id=140506427351808 # ← Still consistent ✅
|
||
|
|
[vmap/write] dst=2 written, vmap.keys()=[1, 2] # ← Write successful ✅
|
||
|
|
[vmap/id] instruction op=binop vmap_ctx id=140506427351808 # ← Still consistent ✅
|
||
|
|
# CRASH - execution stopped here
|
||
|
|
```
|
||
|
|
|
||
|
|
Block 2 shows **good consistency** - same object throughout.
|
||
|
|
|
||
|
|
## Root Cause Hypothesis
|
||
|
|
|
||
|
|
### Hypothesis A (Timing): ❌ REJECTED
|
||
|
|
- Writes are successful and properly sequenced
|
||
|
|
- No evidence of post-instruction sync reading from wrong location
|
||
|
|
|
||
|
|
### Hypothesis B (PHI Collision): ⚠️ POSSIBLE
|
||
|
|
- Cannot verify - trace stopped before PHI blocks
|
||
|
|
- Need to check if existing PHIs block safe_vmap_write
|
||
|
|
|
||
|
|
### Hypothesis C (Object Identity): ✅ **CONFIRMED**
|
||
|
|
- **Critical evidence**: `vmap_ctx` changed identity during bb1 terminator instruction
|
||
|
|
- This suggests `getattr(owner, '_current_vmap', owner.vmap)` is returning a **different object**
|
||
|
|
|
||
|
|
## Source Code Analysis
|
||
|
|
|
||
|
|
### Terminator Lowering Path
|
||
|
|
|
||
|
|
The identity change happens during `ret` instruction. Checking the code:
|
||
|
|
|
||
|
|
**File**: `src/llvm_py/builders/block_lower.py`
|
||
|
|
|
||
|
|
Line 236-240:
|
||
|
|
```python
|
||
|
|
# 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)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Smoking Gun**: Terminators are deferred! When `ret` is lowered in Pass C (line 270+), the `_current_vmap` may have been **deleted**:
|
||
|
|
|
||
|
|
Line 263-267:
|
||
|
|
```python
|
||
|
|
builder.block_end_values[bid] = snap
|
||
|
|
try:
|
||
|
|
delattr(builder, '_current_vmap') # ← DELETED BEFORE PASS C!
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
```
|
||
|
|
|
||
|
|
**Problem**:
|
||
|
|
1. Pass A creates `_current_vmap` for block (line 168)
|
||
|
|
2. Pass A defers terminators (line 240)
|
||
|
|
3. Pass A **deletes** `_current_vmap` (line 265)
|
||
|
|
4. Pass C lowers terminators → `getattr(owner, '_current_vmap', owner.vmap)` falls back to `owner.vmap`
|
||
|
|
5. **Result**: Different object! ❌
|
||
|
|
|
||
|
|
## Recommended Fix (3 Options)
|
||
|
|
|
||
|
|
### Option 1: Preserve vmap_cur for Pass C (Quick Fix)
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Line 263 in block_lower.py
|
||
|
|
builder.block_end_values[bid] = snap
|
||
|
|
# DON'T delete _current_vmap yet! Pass C needs it!
|
||
|
|
# try:
|
||
|
|
# delattr(builder, '_current_vmap')
|
||
|
|
# except Exception:
|
||
|
|
# pass
|
||
|
|
```
|
||
|
|
|
||
|
|
Then delete it in `lower_terminators()` after all terminators are done.
|
||
|
|
|
||
|
|
### Option 2: Use block_end_values in Pass C (SSOT)
|
||
|
|
|
||
|
|
```python
|
||
|
|
# In lower_terminators() line 282
|
||
|
|
for bid, (bb, term_ops) in deferred.items():
|
||
|
|
# Use snapshot from Pass A as SSOT
|
||
|
|
vmap_ctx = builder.block_end_values.get(bid, builder.vmap)
|
||
|
|
builder._current_vmap = vmap_ctx # Restore for consistency
|
||
|
|
# ... lower terminators ...
|
||
|
|
```
|
||
|
|
|
||
|
|
### Option 3: Store vmap_cur in deferred_terminators (Explicit)
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Line 240
|
||
|
|
if term_ops:
|
||
|
|
builder._deferred_terminators[bid] = (bb, term_ops, vmap_cur) # ← Add vmap_cur
|
||
|
|
|
||
|
|
# Line 282 in lower_terminators
|
||
|
|
for bid, (bb, term_ops, vmap_ctx) in deferred.items(): # ← Unpack vmap_ctx
|
||
|
|
builder._current_vmap = vmap_ctx # Restore
|
||
|
|
# ... lower terminators ...
|
||
|
|
```
|
||
|
|
|
||
|
|
## Next Steps (Recommended Order)
|
||
|
|
|
||
|
|
1. **Verify hypothesis** with simpler test case:
|
||
|
|
```bash
|
||
|
|
# Create minimal test without loop complexity
|
||
|
|
echo 'static box Main { main() { return 42 } }' > /tmp/minimal.hako
|
||
|
|
NYASH_LLVM_VMAP_TRACE=1 NYASH_LLVM_USE_HARNESS=1 \
|
||
|
|
./target/release/hakorune --backend llvm /tmp/minimal.hako 2>&1 | grep vmap/id
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **Apply Option 1** (quickest to verify):
|
||
|
|
- Comment out `delattr(builder, '_current_vmap')` in Pass A
|
||
|
|
- Add it to end of `lower_terminators()` in Pass C
|
||
|
|
|
||
|
|
3. **Re-run full test**:
|
||
|
|
```bash
|
||
|
|
NYASH_LLVM_VMAP_TRACE=1 NYASH_LLVM_USE_HARNESS=1 \
|
||
|
|
./target/release/hakorune --backend llvm apps/tests/llvm_stage3_loop_only.hako
|
||
|
|
```
|
||
|
|
|
||
|
|
4. **Check if bb3/exit blocks now show consistent vmap_ctx IDs**
|
||
|
|
|
||
|
|
## Architecture Feedback (Box-First Principle)
|
||
|
|
|
||
|
|
**Problem**: Multi-pass architecture (A → B → C) with mutable state (`_current_vmap`) creates temporal coupling.
|
||
|
|
|
||
|
|
**Recommendation**: Apply SSOT principle from CLAUDE.md:
|
||
|
|
- `block_end_values` should be the **single source of truth** for post-block state
|
||
|
|
- Pass C should **read** from SSOT, not rely on ephemeral `_current_vmap`
|
||
|
|
- This matches "箱理論" - `block_end_values` is the persistent "box", `_current_vmap` is a working buffer
|
||
|
|
|
||
|
|
**Fail-Fast Opportunity**:
|
||
|
|
```python
|
||
|
|
# In lower_instruction() line 33
|
||
|
|
vmap_ctx = getattr(owner, '_current_vmap', None)
|
||
|
|
if vmap_ctx is None:
|
||
|
|
# Fail-Fast instead of silent fallback!
|
||
|
|
raise RuntimeError(
|
||
|
|
f"[LLVM_PY] _current_vmap not set for instruction {op}. "
|
||
|
|
f"This indicates Pass A/C timing issue. Check block_lower.py multi-pass logic."
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
## Appendix: Environment Variables Used
|
||
|
|
|
||
|
|
```bash
|
||
|
|
NYASH_LLVM_VMAP_TRACE=1 # Our new trace flag
|
||
|
|
NYASH_LLVM_USE_HARNESS=1 # Enable llvmlite harness
|
||
|
|
NYASH_LLVM_DUMP_IR=<path> # Save LLVM IR (for later analysis)
|
||
|
|
```
|