feat(llvm): Phase 131-13/14 - MIR JSON順序修正 & 2パスsnapshot解決

## Phase 131-13: MIR JSON 命令順序修正
- copy 遅延ロジック削除(~80行)
- MIR の def→use 順序をそのまま出力(SSOT)
- PHI 先頭集約のみ維持

## Phase 131-14: jump-only block 2パス snapshot 解決
- Pass A: jump-only block はメタ記録のみ
- Pass B: resolve_jump_only_snapshots() で CFG ベース解決
- path compression で連鎖を効率的に解決
- サイクル検出で Fail-Fast

## 結果
-  STRICT モードでエラーなし
-  bb7 が bb5 の snapshot を正しく継承
-  ループが正しく動作(1, 2 出力確認)
- ⚠️ print/concat で segfault(別問題、次Phase)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-15 00:39:43 +09:00
parent eb70dfc5bb
commit 7f57a1bb05
7 changed files with 559 additions and 113 deletions

View File

@ -1,9 +1,51 @@
from typing import Dict, Any, List, Tuple, NamedTuple
import os
import sys
from llvmlite import ir
from trace import debug as trace_debug
from trace import phi_json as trace_phi_json
def is_jump_only_block(block_info: Dict) -> bool:
"""Phase 131-14-B: Detect pure jump-only blocks (trampoline blocks).
A pure jump-only block has:
- NO PHI instructions (PHI blocks do meaningful work - value merging)
- NO other instructions except a single terminator (jump/branch/ret)
- Acts as a pure trampoline/routing block
Blocks with PHI instructions are NOT jump-only because they perform value
merging and must compute their own snapshots.
"""
instructions = block_info.get("instructions", [])
# Check if block has any PHI instructions
has_phi = any(i.get("op") == "phi" for i in instructions)
if has_phi:
# PHI blocks are NOT jump-only - they do value merging
return False
# Check if block has only terminator instructions
non_term = [
i for i in instructions
if i.get("op") not in ("ret", "jump", "branch")
]
return len(non_term) == 0
def get_predecessors(bid: int, preds: Dict[int, List[int]]) -> List[int]:
"""Phase 131-14 P0-3: Get predecessors for a block.
Args:
bid: Block ID
preds: Predecessor map (bid -> [predecessor_bids])
Returns:
List of predecessor block IDs
"""
return preds.get(bid, [])
class DeferredTerminator(NamedTuple):
"""Phase 131-12-P1: Deferred terminator with vmap snapshot.
@ -15,12 +57,138 @@ class DeferredTerminator(NamedTuple):
vmap_snapshot: Dict[int, ir.Value]
def resolve_jump_only_snapshots(builder, block_by_id: Dict[int, Dict[str, Any]]):
"""Phase 131-14-B P0-2: Resolve jump-only block snapshots (Pass B).
This function runs AFTER all blocks have been lowered (Pass A) but BEFORE
PHI finalization. It resolves snapshots for jump-only blocks by following
the CFG to find the nearest non-jump-only predecessor.
Uses path compression to efficiently handle chains of jump-only blocks.
SSOT: Snapshots are based on CFG structure, not processing order.
"""
import sys
strict_mode = os.environ.get('NYASH_LLVM_STRICT') == '1'
trace_vmap = os.environ.get('NYASH_LLVM_TRACE_VMAP') == '1'
jump_only = getattr(builder, '_jump_only_blocks', {})
if not jump_only:
if trace_vmap:
print("[vmap/resolve/passB] No jump-only blocks to resolve", file=sys.stderr)
return
if trace_vmap:
print(f"[vmap/resolve/passB] Resolving {len(jump_only)} jump-only blocks: {sorted(jump_only.keys())}", file=sys.stderr)
resolved = {} # bid -> snapshot dict
def resolve(bid: int, visited: set | None = None) -> Dict[int, Any]:
"""Recursively resolve snapshot for a block, with cycle detection."""
if visited is None:
visited = set()
# Cycle detection
if bid in visited:
if strict_mode:
raise RuntimeError(
f"[LLVM_PY/STRICT] Phase 131-14-B: Cycle detected in jump-only chain: "
f"{visited} -> {bid}"
)
if trace_vmap:
print(f"[vmap/resolve/passB] WARNING: Cycle at bb{bid}, returning empty", file=sys.stderr)
return {}
visited.add(bid)
# Already resolved (path compression cache)
if bid in resolved:
if trace_vmap:
print(f"[vmap/resolve/passB] bb{bid} already resolved (cached)", file=sys.stderr)
return resolved[bid]
# Normal block - already has snapshot from Pass A
if bid in builder.block_end_values:
snapshot = builder.block_end_values[bid]
if trace_vmap:
print(
f"[vmap/resolve/passB] bb{bid} is normal block with snapshot "
f"({len(snapshot)} values)",
file=sys.stderr
)
return snapshot
# Jump-only block - resolve from predecessor
if bid in jump_only:
pred_bid = jump_only[bid]
if trace_vmap:
print(f"[vmap/resolve/passB] bb{bid} is jump-only, resolving from pred bb{pred_bid}", file=sys.stderr)
# Recursively resolve predecessor
pred_snapshot = resolve(pred_bid, visited)
if not pred_snapshot:
if strict_mode:
raise RuntimeError(
f"[LLVM_PY/STRICT] Phase 131-14-B: jump-only block bb{bid} "
f"cannot resolve snapshot from predecessor bb{pred_bid} "
f"(predecessor has no snapshot)"
)
if trace_vmap:
print(
f"[vmap/resolve/passB] WARNING: bb{bid} pred bb{pred_bid} has no snapshot, "
f"using empty dict",
file=sys.stderr
)
pred_snapshot = {}
# Cache the result (path compression)
resolved[bid] = dict(pred_snapshot)
if trace_vmap:
print(
f"[vmap/resolve/passB] bb{bid} resolved from bb{pred_bid}: "
f"{len(resolved[bid])} values",
file=sys.stderr
)
return resolved[bid]
# Unknown block (should not happen if Pass A worked correctly)
if strict_mode:
raise RuntimeError(
f"[LLVM_PY/STRICT] Phase 131-14-B: block bb{bid} is neither normal "
f"nor jump-only (invalid state)"
)
if trace_vmap:
print(f"[vmap/resolve/passB] WARNING: bb{bid} unknown state, returning empty", file=sys.stderr)
return {}
# Resolve all jump-only blocks
for bid in sorted(jump_only.keys()):
snapshot = resolve(bid)
builder.block_end_values[bid] = snapshot
if trace_vmap:
print(
f"[vmap/resolve/passB] ✅ bb{bid} final snapshot: "
f"{len(snapshot)} values, keys={sorted(snapshot.keys())[:10]}",
file=sys.stderr
)
if trace_vmap:
print(f"[vmap/resolve/passB] Pass B complete: resolved {len(jump_only)} jump-only blocks", file=sys.stderr)
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
Phase 131-14-B: Two-pass snapshot resolution
- Pass A: Lower non-terminator instructions only (terminators deferred)
- jump-only blocks: record metadata only, NO snapshot resolution
- Pass B: PHI finalization happens in function_lower.py
- resolve_jump_only_snapshots() called BEFORE PHI finalization
- Pass C: Lower terminators (happens after PHI finalization)
This ensures LLVM IR invariant: PHI nodes must be at block head before any
@ -278,16 +446,94 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
except Exception:
pass
# End-of-block snapshot
snap = dict(vmap_cur)
try:
keys = sorted(list(snap.keys()))
except Exception:
keys = list(snap.keys())
trace_phi_json({"phi": "snapshot", "block": int(bid), "keys": [int(k) for k in keys[:20]]})
for vid in created_ids:
if vid in vmap_cur:
builder.def_blocks.setdefault(vid, set()).add(block_data.get("id", 0))
builder.block_end_values[bid] = snap
# Phase 131-14-B P0-1: Jump-only blocks - record metadata only (Pass A)
strict_mode = os.environ.get('NYASH_LLVM_STRICT') == '1'
trace_vmap = os.environ.get('NYASH_LLVM_TRACE_VMAP') == '1'
# Initialize jump_only_blocks dict if not exists
if not hasattr(builder, '_jump_only_blocks'):
builder._jump_only_blocks = {}
is_jump_only = is_jump_only_block(block_data)
if trace_vmap:
print(
f"[vmap/snapshot] bb{bid} is_jump_only={is_jump_only} "
f"instructions={[i.get('op') for i in block_data.get('instructions', [])]}",
file=sys.stderr
)
if is_jump_only:
# Phase 131-14-B: Jump-only blocks - record metadata, defer snapshot resolution to Pass B
preds_list = get_predecessors(bid, builder.preds)
if len(preds_list) == 0:
# No predecessors - error in STRICT mode
if strict_mode:
raise RuntimeError(
f"[LLVM_PY/STRICT] Phase 131-14-B: jump-only block bb{bid} "
f"has no predecessors (orphan trampoline)"
)
# Non-STRICT: use current vmap_cur (defensive fallback)
snap = dict(vmap_cur)
if trace_vmap:
print(
f"[vmap/snapshot] bb{bid} jump-only with 0 preds: "
f"using vmap_cur keys={sorted(snap.keys())}",
file=sys.stderr
)
elif len(preds_list) == 1:
# Single predecessor - record metadata for Pass B resolution
pred_bid = preds_list[0]
builder._jump_only_blocks[bid] = pred_bid
# DO NOT create snapshot here - will be resolved in Pass B
# Set snap to None to indicate "skip storing in block_end_values"
snap = None
if trace_vmap:
print(
f"[vmap/snapshot/passA] bb{bid} jump-only: recorded pred=bb{pred_bid}, "
f"snapshot deferred to Pass B",
file=sys.stderr
)
else:
# Multiple predecessors - error in STRICT mode (merge rules not yet defined)
if strict_mode:
raise RuntimeError(
f"[LLVM_PY/STRICT] Phase 131-14-B: jump-only block bb{bid} "
f"has multiple predecessors: {preds_list} "
f"(merge propagation not implemented)"
)
# Non-STRICT: use current vmap_cur (defensive fallback)
snap = dict(vmap_cur)
if trace_vmap:
print(
f"[vmap/snapshot] bb{bid} jump-only with multiple preds {preds_list}: "
f"using vmap_cur keys={sorted(snap.keys())}",
file=sys.stderr
)
else:
# Normal block: use its own vmap_cur
snap = dict(vmap_cur)
# Phase 131-14-B: Only store snapshot if not deferred (snap is not None)
if snap is not None:
try:
keys = sorted(list(snap.keys()))
except Exception:
keys = list(snap.keys())
trace_phi_json({"phi": "snapshot", "block": int(bid), "keys": [int(k) for k in keys[:20]]})
for vid in created_ids:
if vid in vmap_cur:
builder.def_blocks.setdefault(vid, set()).add(block_data.get("id", 0))
builder.block_end_values[bid] = snap
else:
# Jump-only block with deferred snapshot - don't store yet
if trace_vmap:
print(
f"[vmap/snapshot/passA] bb{bid} snapshot deferred (not stored in block_end_values)",
file=sys.stderr
)
try:
delattr(builder, '_current_vmap')
except Exception:

View File

@ -279,6 +279,10 @@ def lower_function(builder, func_data: Dict[str, Any]):
from builders.block_lower import lower_blocks as _lower_blocks
_lower_blocks(builder, func, block_by_id, order, loop_plan)
# Phase 131-14-B Pass B: Resolve jump-only block snapshots (BEFORE PHI finalization)
from builders.block_lower import resolve_jump_only_snapshots as _resolve_jump_only_snapshots
_resolve_jump_only_snapshots(builder, block_by_id)
# Optional: capture lowering ctx for downstream helpers
try:
builder.ctx = dict(
@ -300,7 +304,7 @@ def lower_function(builder, func_data: Dict[str, Any]):
except Exception:
pass
# Phase 131-4 Pass B: Finalize PHIs (wires incoming edges)
# Phase 131-4 Pass B (now Pass B2): Finalize PHIs (wires incoming edges)
_finalize_phis(builder)
# Phase 131-4 Pass C: Lower deferred terminators (after PHIs are placed)

View File

@ -202,9 +202,8 @@ def wire_incomings(builder, block_id: int, dst_vid: int, incoming: List[Tuple[in
if original_vs != vs:
trace({"phi": "wire_replaced_src", "original": original_vs, "replaced": vs})
try:
val = builder.resolver._value_at_end_i64(
vs, pred_match, builder.preds, builder.block_end_values, builder.vmap, builder.bb_map
)
# P0-4: Use resolve_incoming for PHI incoming values
val = builder.resolver.resolve_incoming(pred_match, vs)
trace({"phi": "wire_resolved", "vs": vs, "pred": pred_match, "val_type": type(val).__name__})
except Exception as e:
trace({"phi": "wire_resolve_fail", "vs": vs, "pred": pred_match, "error": str(e)})

View File

@ -3,7 +3,7 @@ Resolver API (Python version)
Based on src/backend/llvm/compiler/codegen/instructions/resolver.rs
"""
from typing import Dict, Optional, Any, Tuple
from typing import Dict, Optional, Any, Tuple, Set
import os
from trace import phi as trace_phi
from trace import values as trace_values
@ -63,6 +63,8 @@ class Resolver:
self.block_phi_incomings = {}
# P0-1: SSOT for end-of-block values (snapshots)
self.block_end_values = {}
# P0-3: Circular reference detection (hang prevention)
self._visited: Set[Tuple[int, int]] = set()
def mark_string(self, value_id: int) -> None:
try:
@ -75,7 +77,81 @@ class Resolver:
return int(value_id) in self.string_ids
except Exception:
return False
def _check_cycle(self, block_id: int, value_id: int):
"""P0-3: Circular reference detection (hang prevention)"""
key = (block_id, value_id)
if key in self._visited:
raise RuntimeError(
f"[LLVM_PY] Circular reference detected: bb{block_id} v{value_id}"
)
self._visited.add(key)
def resolve_cur(self, block_id: int, value_id: int, vmap_cur: Dict[int, ir.Value]) -> ir.Value:
"""P0-1: Same-block instruction lowering (vmap_cur as primary source)
Used for lowering instructions within the same basic block where the value
is defined and used. Checks vmap_cur first, then applies fail-fast checks.
Args:
block_id: Current basic block ID
value_id: Value ID to resolve
vmap_cur: Current block's value map (def->use tracking)
Returns:
LLVM IR value (i64)
"""
# 1. Check vmap_cur first
val = vmap_cur.get(value_id)
if val is not None:
return val
# 2. Fail-Fast: def_blocks has bb but vmap_cur doesn't → lowerer bug
if value_id in self.def_blocks and block_id in self.def_blocks[value_id]:
if os.environ.get('NYASH_LLVM_STRICT') == '1':
raise RuntimeError(
f"[LLVM_PY/STRICT] resolve_cur: v{value_id} defined in bb{block_id} "
f"but not in vmap_cur. Lowerer order bug?"
)
# 3. vmap_cur miss → undefined error
if os.environ.get('NYASH_LLVM_STRICT') == '1':
raise RuntimeError(
f"[LLVM_PY/STRICT] resolve_cur: v{value_id} not found in bb{block_id} vmap_cur. "
f"Available: {sorted(vmap_cur.keys())}"
)
# Non-STRICT: fallback to 0
return ir.Constant(ir.IntType(64), 0)
def resolve_incoming(self, pred_block_id: int, value_id: int) -> ir.Value:
"""P0-2: PHI incoming resolution (snapshot-only reference)
Used for resolving PHI incoming values from predecessor blocks.
Only looks at block_end_values snapshot, never vmap_cur.
Args:
pred_block_id: Predecessor block ID
value_id: Value ID to resolve from predecessor
Returns:
LLVM IR value (i64)
"""
snapshot = self.block_end_values.get(pred_block_id, {})
val = snapshot.get(value_id)
if val is not None:
return val
# Fail-Fast: snapshot miss → structural bug
if os.environ.get('NYASH_LLVM_STRICT') == '1':
raise RuntimeError(
f"[LLVM_PY/STRICT] resolve_incoming: v{value_id} not in bb{pred_block_id} snapshot. "
f"Available: {sorted(snapshot.keys())}"
)
# Non-STRICT: fallback to 0
return ir.Constant(ir.IntType(64), 0)
def resolve_i64(
self,
value_id: int,