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>
This commit is contained in:
nyash-codex
2025-12-14 21:28:41 +09:00
parent 413504d6de
commit 7dfd6ff1d9
21 changed files with 1391 additions and 133 deletions

View File

@ -1,9 +1,20 @@
from typing import Dict, Any, List, Tuple
from typing import Dict, Any, List, Tuple, NamedTuple
from llvmlite import ir
from trace import debug as trace_debug
from trace import phi_json as trace_phi_json
class DeferredTerminator(NamedTuple):
"""Phase 131-12-P1: Deferred terminator with vmap snapshot.
This structure captures the terminator operations along with the vmap state
at the end of Pass A, ensuring Pass C uses the correct SSA context.
"""
bb: ir.Block
term_ops: List[Dict[str, Any]]
vmap_snapshot: Dict[int, ir.Value]
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.
@ -34,6 +45,8 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
try:
builder.resolver.builder = ib
builder.resolver.module = builder.module
# P0-1: Set current block_id for def_blocks tracking
builder.resolver.current_block_id = bid
except Exception:
pass
builder.loop_count += 1
@ -116,6 +129,8 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
try:
builder.resolver.builder = ib
builder.resolver.module = builder.module
# P0-1: Set current block_id for def_blocks tracking
builder.resolver.current_block_id = bid
except Exception:
pass
block_data = block_by_id.get(bid, {})
@ -162,6 +177,10 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
except Exception:
vmap_cur = dict(builder.vmap)
builder._current_vmap = vmap_cur
# Phase 131-12-P1: Object identity trace for vmap_cur investigation
import os, sys
if os.environ.get('NYASH_LLVM_VMAP_TRACE') == '1':
print(f"[vmap/id] bb{bid} vmap_cur id={id(vmap_cur)} keys={sorted(vmap_cur.keys())[:10]}", file=sys.stderr)
created_ids: List[int] = []
defined_here_all: set = set()
for _inst in body_ops:
@ -229,11 +248,17 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
except Exception:
pass
# Phase 131-4 Pass A: DEFER terminators until after PHI finalization
# Store terminators for Pass C (will be lowered in lower_terminators)
# Phase 131-12-P1 P0-2: Store terminators WITH vmap_cur snapshot for Pass C
if not hasattr(builder, '_deferred_terminators'):
builder._deferred_terminators = {}
if term_ops:
builder._deferred_terminators[bid] = (bb, term_ops)
# CRITICAL: dict(vmap_cur) creates a snapshot copy to prevent mutation issues
vmap_snapshot = dict(vmap_cur)
builder._deferred_terminators[bid] = DeferredTerminator(bb, term_ops, vmap_snapshot)
# Phase 131-12-P1: Trace snapshot creation
import os, sys
if os.environ.get('NYASH_LLVM_VMAP_TRACE') == '1':
print(f"[vmap/id] Pass A bb{bid} snapshot id={id(vmap_snapshot)} keys={sorted(vmap_snapshot.keys())[:10]}", file=sys.stderr)
# Phase 131-7: Sync ALL created values to global vmap (not just PHIs)
# This ensures Pass C (deferred terminators) can access values from Pass A
try:
@ -265,9 +290,11 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
def lower_terminators(builder, func: ir.Function):
"""Phase 131-4 Pass C: Lower deferred terminators after PHI finalization.
Phase 131-12-P1 P0-3: Restore vmap_cur snapshot for each block's terminator lowering.
This ensures PHI nodes are always at block heads before terminators are added,
maintaining LLVM IR's invariant: PHIs first, then other instructions, then terminators.
The vmap snapshot ensures terminators see the SSA context from Pass A, not later mutations.
"""
if not hasattr(builder, '_deferred_terminators'):
return
@ -275,31 +302,65 @@ def lower_terminators(builder, func: ir.Function):
deferred = builder._deferred_terminators
trace_debug(f"[llvm-py/pass-c] Lowering {len(deferred)} blocks with deferred terminators")
for bid, (bb, term_ops) in deferred.items():
ib = ir.IRBuilder(bb)
try:
builder.resolver.builder = ib
builder.resolver.module = builder.module
# Phase 131-4: Disable PHI synthesis during terminator lowering
# Terminators should only use values that already exist (from Pass A/B)
builder.resolver._disable_phi_synthesis = True
except Exception:
pass
import os, sys
strict_mode = os.environ.get('NYASH_LLVM_STRICT') == '1'
for inst in term_ops:
for bid, deferred_term in deferred.items():
# Phase 131-12-P1: Unpack DeferredTerminator with vmap snapshot
bb = deferred_term.bb
term_ops = deferred_term.term_ops
vmap_snapshot = deferred_term.vmap_snapshot
# Phase 131-12-P1 P0-4: STRICT mode assertion
if strict_mode:
assert vmap_snapshot is not None, f"STRICT: vmap_snapshot must exist for bb{bid}"
trace_debug(f"[llvm-py/pass-c/strict] bb{bid} vmap_snapshot id={id(vmap_snapshot)}")
# Phase 131-12-P1 P0-3: Save and restore _current_vmap
old_current_vmap = getattr(builder, '_current_vmap', None)
builder._current_vmap = vmap_snapshot
# Trace snapshot restoration
if os.environ.get('NYASH_LLVM_VMAP_TRACE') == '1':
print(f"[vmap/id] Pass C bb{bid} restored snapshot id={id(vmap_snapshot)} keys={sorted(vmap_snapshot.keys())[:10]}", file=sys.stderr)
# Phase 131-12-P1 P0-4: STRICT mode verification
if strict_mode:
assert hasattr(builder, '_current_vmap'), f"STRICT: _current_vmap must be set for bb{bid} terminator lowering"
assert id(builder._current_vmap) == id(vmap_snapshot), f"STRICT: _current_vmap must match snapshot for bb{bid}"
try:
ib = ir.IRBuilder(bb)
try:
trace_debug(f"[llvm-py/pass-c] term op: {inst.get('op')} dst={inst.get('dst')} in bb{bid}")
builder.resolver.builder = ib
builder.resolver.module = builder.module
# Phase 131-4: Disable PHI synthesis during terminator lowering
# Terminators should only use values that already exist (from Pass A/B)
builder.resolver._disable_phi_synthesis = True
except Exception:
pass
try:
if bb.terminator is not None:
# Terminator already exists (e.g., from loop lowering), skip
trace_debug(f"[llvm-py/pass-c] bb{bid} already has terminator, skipping")
break
except Exception:
pass
ib.position_at_end(bb)
builder.lower_instruction(ib, inst, func)
for inst in term_ops:
try:
trace_debug(f"[llvm-py/pass-c] term op: {inst.get('op')} dst={inst.get('dst')} in bb{bid}")
except Exception:
pass
try:
if bb.terminator is not None:
# Terminator already exists (e.g., from loop lowering), skip
trace_debug(f"[llvm-py/pass-c] bb{bid} already has terminator, skipping")
break
except Exception:
pass
ib.position_at_end(bb)
builder.lower_instruction(ib, inst, func)
finally:
# Phase 131-12-P1 P0-3: Restore previous _current_vmap state (prevent side effects)
if old_current_vmap is None:
if hasattr(builder, '_current_vmap'):
delattr(builder, '_current_vmap')
else:
builder._current_vmap = old_current_vmap
# Clean up deferred state
try:

View File

@ -31,6 +31,10 @@ def lower_instruction(owner, builder: ir.IRBuilder, inst: Dict[str, Any], func:
op = inst.get("op")
# Pick current vmap context (per-block context during lowering)
vmap_ctx = getattr(owner, '_current_vmap', owner.vmap)
# Phase 131-12-P1: Object identity trace for vmap_ctx investigation
import os, sys
if os.environ.get('NYASH_LLVM_VMAP_TRACE') == '1':
print(f"[vmap/id] instruction op={op} vmap_ctx id={id(vmap_ctx)}", file=sys.stderr)
if op == "const":
dst = inst.get("dst")

View File

@ -5,7 +5,7 @@ Handles +, -, *, /, %, &, |, ^, <<, >>
import llvmlite.ir as ir
from typing import Dict, Optional, Any
from utils.values import resolve_i64_strict
from utils.values import resolve_i64_strict, safe_vmap_write
import os
from .compare import lower_compare
@ -28,7 +28,7 @@ def _canonicalize_i64(builder: ir.IRBuilder, value, vid, vmap: Dict[int, ir.Valu
elif width > 64:
value = builder.trunc(value, target, name=f"{hint}_trunc_{vid}")
if isinstance(vid, int):
vmap[vid] = value
safe_vmap_write(vmap, vid, value, f"canonicalize_{hint}")
return value
def lower_binop(
@ -233,7 +233,7 @@ def lower_binop(
if callee is None:
callee = ir.Function(builder.module, hh_fnty, name='nyash.string.concat_hh')
res = builder.call(callee, [hl, hr], name=f"concat_hh_{dst}")
vmap[dst] = res
safe_vmap_write(vmap, dst, res, "binop_concat_hh", resolver=resolver)
else:
# Mixed string + non-string (e.g., "len=" + 5). Use pointer concat helpers then box.
i32 = ir.IntType(32); i8p = ir.IntType(8).as_pointer(); i64 = ir.IntType(64)
@ -290,7 +290,7 @@ def lower_binop(
boxer = f; break
if boxer is None:
boxer = ir.Function(builder.module, ir.FunctionType(i64, [i8p]), name='nyash.box.from_i8_string')
vmap[dst] = builder.call(boxer, [p], name=f"concat_box_{dst}")
safe_vmap_write(vmap, dst, builder.call(boxer, [p], name=f"concat_box_{dst}"), "binop_concat_si")
else:
li = as_i64(lhs_val)
rp = to_i8p_from_vid(rhs, rhs_raw, rhs_val, 'r')
@ -307,7 +307,7 @@ def lower_binop(
boxer = f; break
if boxer is None:
boxer = ir.Function(builder.module, ir.FunctionType(i64, [i8p]), name='nyash.box.from_i8_string')
vmap[dst] = builder.call(boxer, [p], name=f"concat_box_{dst}")
safe_vmap_write(vmap, dst, builder.call(boxer, [p], name=f"concat_box_{dst}"), "binop_concat_is")
# Tag result as string handle so subsequent '+' stays in string domain
try:
if resolver is not None and hasattr(resolver, 'mark_string'):
@ -353,6 +353,10 @@ def lower_binop(
else:
# Unknown op - return zero
result = ir.Constant(i64, 0)
# Phase 131-12-P1: Object identity trace before write
import sys # os already imported at module level
if os.environ.get('NYASH_LLVM_VMAP_TRACE') == '1':
print(f"[vmap/id] binop op={op} dst={dst} vmap id={id(vmap)} before_write", file=sys.stderr)
# Store result
vmap[dst] = result
safe_vmap_write(vmap, dst, result, f"binop_{op}")

View File

@ -5,7 +5,7 @@ Handles comparison operations (<, >, <=, >=, ==, !=)
import llvmlite.ir as ir
from typing import Dict, Optional, Any
from utils.values import resolve_i64_strict
from utils.values import resolve_i64_strict, safe_vmap_write
import os
from .externcall import lower_externcall
from trace import values as trace_values
@ -28,7 +28,7 @@ def _canonicalize_i64(builder: ir.IRBuilder, value, vid, vmap: Dict[int, ir.Valu
elif width > 64:
value = builder.trunc(value, target, name=f"{hint}_trunc_{vid}")
if isinstance(vid, int):
vmap[vid] = value
safe_vmap_write(vmap, vid, value, f"canonicalize_{hint}")
return value
def lower_compare(
@ -126,11 +126,11 @@ def lower_compare(
eqf = ir.Function(builder.module, ir.FunctionType(i64, [i64, i64]), name='nyash.string.eq_hh')
eq = builder.call(eqf, [lh, rh], name='str_eq')
if op == '==':
vmap[dst] = eq
safe_vmap_write(vmap, dst, eq, "compare_str_eq", resolver=resolver)
else:
one = ir.Constant(i64, 1)
ne = builder.sub(one, eq, name='str_ne')
vmap[dst] = ne
safe_vmap_write(vmap, dst, ne, "compare_str_ne", resolver=resolver)
return
# Default integer compare path
@ -159,7 +159,7 @@ def lower_compare(
# should explicitly cast at their use site (e.g., via resolver or
# instruction-specific lowering) to avoid emitting casts after
# terminators when used as branch conditions.
vmap[dst] = cmp_result
safe_vmap_write(vmap, dst, cmp_result, f"compare_{pred}")
def lower_fcmp(
builder: ir.IRBuilder,
@ -188,10 +188,10 @@ def lower_fcmp(
# Perform ordered comparison using canonical predicates
pred = op if op in ('<','>','<=','>=','==','!=') else '=='
cmp_result = builder.fcmp_ordered(pred, lhs_val, rhs_val, name=f"fcmp_{dst}")
# Convert i1 to i64
i64 = ir.IntType(64)
result = builder.zext(cmp_result, i64, name=f"fcmp_i64_{dst}")
# Store result
vmap[dst] = result
safe_vmap_write(vmap, dst, result, f"fcmp_{pred}")

View File

@ -5,6 +5,7 @@ Handles integer, float, string, and void constants
import llvmlite.ir as ir
from typing import Dict, Any
from utils.values import safe_vmap_write
def lower_const(
builder: ir.IRBuilder,
@ -31,13 +32,17 @@ def lower_const(
# Integer constant
i64 = ir.IntType(64)
llvm_val = ir.Constant(i64, int(const_val))
vmap[dst] = llvm_val
# Phase 131-12-P1: Object identity trace before write
import os, sys
if os.environ.get('NYASH_LLVM_VMAP_TRACE') == '1':
print(f"[vmap/id] const dst={dst} vmap id={id(vmap)} before_write", file=sys.stderr)
safe_vmap_write(vmap, dst, llvm_val, "const_i64", resolver=resolver)
elif const_type == 'f64':
# Float constant
f64 = ir.DoubleType()
llvm_val = ir.Constant(f64, float(const_val))
vmap[dst] = llvm_val
safe_vmap_write(vmap, dst, llvm_val, "const_f64", resolver=resolver)
elif const_type == 'string' or (isinstance(const_type, dict) and const_type.get('kind') in ('handle','ptr') and const_type.get('box_type') == 'StringBox'):
# String constant - create global and immediately box to i64 handle
@ -75,7 +80,7 @@ def lower_const(
if boxer is None:
boxer = ir.Function(module, boxer_ty, name='nyash.box.from_i8_string')
handle = builder.call(boxer, [gep], name=f"const_str_h_{dst}")
vmap[dst] = handle
safe_vmap_write(vmap, dst, handle, "const_string", resolver=resolver)
if resolver is not None:
if hasattr(resolver, 'string_literals'):
resolver.string_literals[dst] = str_val
@ -87,13 +92,13 @@ def lower_const(
resolver.string_ptrs[dst] = gep
except Exception:
pass
elif const_type == 'void':
# Void/null constant - use i64 zero
i64 = ir.IntType(64)
vmap[dst] = ir.Constant(i64, 0)
safe_vmap_write(vmap, dst, ir.Constant(i64, 0), "const_void", resolver=resolver)
else:
# Unknown type - default to i64 zero
i64 = ir.IntType(64)
vmap[dst] = ir.Constant(i64, 0)
safe_vmap_write(vmap, dst, ir.Constant(i64, 0), "const_unknown", resolver=resolver)

View File

@ -5,7 +5,7 @@ MIR13 PHI-off uses explicit copies along edges/blocks to model merges.
import llvmlite.ir as ir
from typing import Dict, Optional, Any
from utils.values import resolve_i64_strict
from utils.values import resolve_i64_strict, safe_vmap_write
def lower_copy(
builder: ir.IRBuilder,
@ -43,4 +43,8 @@ def lower_copy(
val = resolve_i64_strict(resolver, src, current_block, preds, block_end_values, vmap, bb_map)
if val is None:
val = ir.Constant(ir.IntType(64), 0)
vmap[dst] = val
# Phase 131-12-P1: Object identity trace before write
import os, sys
if os.environ.get('NYASH_LLVM_VMAP_TRACE') == '1':
print(f"[vmap/id] copy dst={dst} src={src} vmap id={id(vmap)} before_write", file=sys.stderr)
safe_vmap_write(vmap, dst, val, "copy", resolver=resolver)

View File

@ -6,6 +6,7 @@ Critical for SSA form - handles value merging from different control flow paths
import llvmlite.ir as ir
from phi_wiring import phi_at_block_head
from typing import Dict, List, Tuple, Optional
from utils.values import safe_vmap_write
def lower_phi(
builder: ir.IRBuilder,
@ -131,6 +132,18 @@ def lower_phi(
# Store PHI result
vmap[dst_vid] = phi
# Register PHI definition in def_blocks (critical for resolver dominance tracking)
if resolver is not None and hasattr(resolver, 'def_blocks') and cur_bid is not None:
resolver.def_blocks.setdefault(dst_vid, set()).add(cur_bid)
import os
if os.environ.get('NYASH_LLVM_PHI_DEBUG') == '1':
try:
from trace import phi as trace_phi_debug
trace_phi_debug(f"[PHI_DEBUG] Registered dst_vid={dst_vid} in def_blocks for block={cur_bid}")
except Exception:
pass
# Strict mode: fail fast on synthesized zeros (indicates incomplete incoming or dominance issue)
import os
if used_default_zero and os.environ.get('NYASH_LLVM_PHI_STRICT') == '1':

View File

@ -5,6 +5,7 @@ Handles type conversions and type checks
import llvmlite.ir as ir
from typing import Dict, Optional, Any
from utils.values import safe_vmap_write
def lower_typeop(
builder: ir.IRBuilder,
@ -57,8 +58,8 @@ def lower_typeop(
if op == "cast":
# Type casting - for now just pass through
# In real implementation, would check/convert box types
vmap[dst_vid] = src_val
safe_vmap_write(vmap, dst_vid, src_val, "typeop_cast", resolver=resolver)
elif op == "is":
# Type check - returns boolean (i64: 1 or 0)
# For now, simplified implementation
@ -75,17 +76,17 @@ def lower_typeop(
else:
# For other types, would need runtime type info
result = ir.Constant(ir.IntType(64), 0)
vmap[dst_vid] = result
safe_vmap_write(vmap, dst_vid, result, "typeop_is", resolver=resolver)
elif op == "as":
# Safe cast - returns value or null/0
# For now, same as cast
vmap[dst_vid] = src_val
safe_vmap_write(vmap, dst_vid, src_val, "typeop_as", resolver=resolver)
else:
# Unknown operation
vmap[dst_vid] = ir.Constant(ir.IntType(64), 0)
safe_vmap_write(vmap, dst_vid, ir.Constant(ir.IntType(64), 0), "typeop_unknown")
def lower_convert(
builder: ir.IRBuilder,
@ -134,12 +135,12 @@ def lower_convert(
if not src_val:
# Default based on target type
if to_type == "f64":
vmap[dst_vid] = ir.Constant(ir.DoubleType(), 0.0)
safe_vmap_write(vmap, dst_vid, ir.Constant(ir.DoubleType(), 0.0), "convert_default_f64")
elif to_type == "ptr":
i8 = ir.IntType(8)
vmap[dst_vid] = ir.Constant(i8.as_pointer(), None)
safe_vmap_write(vmap, dst_vid, ir.Constant(i8.as_pointer(), None), "convert_default_ptr")
else:
vmap[dst_vid] = ir.Constant(ir.IntType(64), 0)
safe_vmap_write(vmap, dst_vid, ir.Constant(ir.IntType(64), 0), "convert_default_i64")
return
# Perform conversion
@ -165,5 +166,5 @@ def lower_convert(
else:
# Unknown conversion - pass through
result = src_val
vmap[dst_vid] = result
safe_vmap_write(vmap, dst_vid, result, f"convert_{from_type}_to_{to_type}")

View File

@ -114,6 +114,9 @@ def setup_phi_placeholders(builder, blocks: List[Dict[str, Any]]):
# Definition hint: PHI defines dst in this block
try:
builder.def_blocks.setdefault(int(dst0), set()).add(int(bid0))
import os
if os.environ.get('NYASH_LLVM_PHI_DEBUG') == '1':
trace({"phi_def_blocks": "registered", "dst": int(dst0), "block": int(bid0)})
except Exception:
pass
try:

View File

@ -88,7 +88,20 @@ class Resolver:
Creates PHI at block start if needed, caches the result.
"""
cache_key = (current_block.name, value_id)
import os
if os.environ.get('NYASH_LLVM_PHI_DEBUG') == '1':
try:
bid = int(str(current_block.name).replace('bb',''))
in_def_blocks = value_id in self.def_blocks
if in_def_blocks:
def_in_blocks = list(self.def_blocks.get(value_id, set()))
else:
def_in_blocks = []
trace_phi(f"[resolve_i64/entry] bb{bid} v{value_id} in_def_blocks={in_def_blocks} def_in={def_in_blocks}")
except Exception as e:
trace_phi(f"[resolve_i64/entry] ERROR: {e}")
# Check cache
if cache_key in self.i64_cache:
return self.i64_cache[cache_key]
@ -158,10 +171,18 @@ class Resolver:
defined_here = False
if defined_here:
existing = vmap.get(value_id)
import os
if os.environ.get('NYASH_LLVM_PHI_DEBUG') == '1':
existing_type = type(existing).__name__ if existing is not None else "None"
trace_phi(f"[resolve/def_here] bb{bid} v{value_id} existing={existing_type} in vmap={value_id in vmap}")
if existing is not None and hasattr(existing, 'type') and isinstance(existing.type, ir.IntType) and existing.type.width == 64:
trace_values(f"[resolve] local reuse: bb{bid} v{value_id}")
self.i64_cache[cache_key] = existing
return existing
elif existing is not None:
if os.environ.get('NYASH_LLVM_PHI_DEBUG') == '1':
existing_llvm_type = str(existing.type) if hasattr(existing, 'type') else "no_type"
trace_phi(f"[resolve/def_here] bb{bid} v{value_id} existing has wrong type: {existing_llvm_type}")
else:
# Do NOT blindly reuse vmap across blocks: it may reference values defined
# in non-dominating predecessors (e.g., other branches). Only reuse when
@ -235,6 +256,24 @@ class Resolver:
result = placeholder if (placeholder is not None and hasattr(placeholder, 'add_incoming')) else ir.Constant(self.i64, 0)
else:
# No declared PHI and multi-pred: do not synthesize; fallback to zero
import os
if os.environ.get('NYASH_LLVM_STRICT') == '1':
# P0-2: STRICT mode - fail fast on undeclared PHI in multi-pred context
def_blocks_info = "not_in_def_blocks"
try:
if value_id in self.def_blocks:
def_blocks_info = f"def_blocks={sorted(list(self.def_blocks[value_id]))}"
except Exception:
pass
raise RuntimeError(
f"[LLVM_PY/STRICT] Undeclared PHI in multi-pred block:\n"
f" ValueId: {value_id}\n"
f" Block: bb{cur_bid}\n"
f" Predecessors: {pred_ids}\n"
f" {def_blocks_info}\n"
f" Hint: Value needs PHI but not declared in block_phi_incomings"
)
result = ir.Constant(self.i64, 0)
# Cache and return
@ -342,6 +381,31 @@ class Resolver:
preds_s = ','.join(str(x) for x in pred_ids)
trace_phi(f"[resolve] end_i64 miss: bb{block_id} v{value_id} preds=[{preds_s}] → 0")
# P0-2: STRICT mode - fail fast on resolution miss
import os
if os.environ.get('NYASH_LLVM_STRICT') == '1':
# Collect diagnostic information
def_blocks_info = "not_in_def_blocks"
try:
if value_id in self.def_blocks:
def_blocks_info = f"def_blocks={sorted(list(self.def_blocks[value_id]))}"
except Exception:
pass
# Build search path for diagnostics
search_path = [block_id] + pred_ids
raise RuntimeError(
f"[LLVM_PY/STRICT] PHI resolution miss:\n"
f" ValueId: {value_id}\n"
f" Target block: {block_id}\n"
f" Predecessors: [{preds_s}]\n"
f" Search path: {search_path}\n"
f" {def_blocks_info}\n"
f" Hint: PHI dst may not be registered in def_blocks, or dominance issue"
)
z = ir.Constant(self.i64, 0)
self._end_i64_cache[key] = z
return z

View File

@ -5,6 +5,8 @@ Centralize policies like "prefer same-block SSA; otherwise resolve with dominanc
from typing import Any, Dict, Optional
import llvmlite.ir as ir
import sys
import os
def resolve_i64_strict(
resolver,
@ -21,19 +23,87 @@ def resolve_i64_strict(
- If prefer_local and vmap has a same-block definition, reuse it.
- Otherwise, delegate to resolver to localize with PHI/casts as needed.
"""
import os
debug = os.environ.get('NYASH_LLVM_PHI_DEBUG') == '1'
# Prefer current vmap SSA first (block-local map is passed in vmap)
val = vmap.get(value_id)
if debug:
val_type = type(val).__name__ if val is not None else "None"
from trace import phi as trace_phi
trace_phi(f"[resolve_i64_strict] v{value_id} vmap={val_type}")
if prefer_local and val is not None:
if debug:
trace_phi(f"[resolve_i64_strict] v{value_id} -> local vmap")
return val
# If local map misses, try builder-global vmap (e.g., predeclared PHIs)
try:
if hasattr(resolver, 'global_vmap') and isinstance(resolver.global_vmap, dict):
gval = resolver.global_vmap.get(value_id)
if gval is not None:
if debug:
trace_phi(f"[resolve_i64_strict] v{value_id} -> global_vmap")
return gval
except Exception:
pass
# Fallback to resolver
if resolver is None:
if debug:
trace_phi(f"[resolve_i64_strict] v{value_id} -> 0 (no resolver)")
return ir.Constant(ir.IntType(64), 0)
if debug:
trace_phi(f"[resolve_i64_strict] v{value_id} -> resolver.resolve_i64")
return resolver.resolve_i64(value_id, current_block, preds, block_end_values, vmap, bb_map)
def safe_vmap_write(vmap: Dict[int, Any], dst: int, value: Any, context: str = "", resolver=None, block_id: Optional[int] = None) -> None:
"""
PHI overwrite protection for vmap writes + def_blocks registration (P0-1 unified).
Implements fail-fast protection against ValueId namespace collisions.
Args:
vmap: Value map to write to
dst: Destination ValueId
value: LLVM IR value to write
context: Context string for error messages (e.g., "const", "binop")
resolver: Optional resolver for def_blocks tracking (P0-1)
block_id: Optional block ID for def_blocks registration (P0-1)
Behavior:
- STRICT mode (NYASH_LLVM_STRICT=1): Raises error if overwriting PHI
- TRACE mode (NYASH_LLVM_TRACE_VMAP=1): Logs warning but skips overwrite
- Default: Silently skips PHI overwrite (SSOT: PHI defined once)
- P0-1: If resolver and block_id provided, register in def_blocks
"""
existing = vmap.get(dst)
if existing is not None and hasattr(existing, 'add_incoming'):
# PHI node detected - overwrite forbidden (SSOT principle)
if os.environ.get('NYASH_LLVM_STRICT') == '1':
raise RuntimeError(
f"[LLVM_PY/{context}] Cannot overwrite PHI dst={dst}. "
f"ValueId namespace collision detected. "
f"Existing: PHI node, Attempted: {type(value).__name__}"
)
# STRICT not enabled - warn and skip
if os.environ.get('NYASH_LLVM_TRACE_VMAP') == '1':
print(f"[vmap/warn] Skipping overwrite of PHI dst={dst} in context={context}", file=sys.stderr)
return # Do not overwrite PHI
# Safe to write
vmap[dst] = value
# Phase 131-12-P1: Trace successful write
if os.environ.get('NYASH_LLVM_VMAP_TRACE') == '1':
print(f"[vmap/write] dst={dst} written, vmap.keys()={sorted(vmap.keys())[:20]}", file=sys.stderr)
# P0-1: Register definition in def_blocks for dominance tracking (SSOT for all instructions)
if resolver is not None and hasattr(resolver, 'def_blocks'):
# Auto-detect block_id from resolver if not provided explicitly
bid = block_id
if bid is None and hasattr(resolver, 'current_block_id'):
bid = resolver.current_block_id
if bid is not None:
resolver.def_blocks.setdefault(dst, set()).add(bid)
if os.environ.get('NYASH_LLVM_TRACE_VMAP') == '1':
print(f"[vmap/def_blocks] Registered v{dst} in block {bid} (context={context})", file=sys.stderr)