refactor(llvm): Phase 132-P1 - FunctionLowerContext Box isolation

Structural fix for cross-function state leakage bugs discovered in Phase 131-132.

Problem:
- Function-local state (block_end_values, def_blocks, phi_manager, caches)
  was globally managed, causing collisions across functions
- Required manual clearing via _clear_function_local_state() (~80 lines)
- Tuple-key workaround (func_name, block_id) added complexity

Solution: FunctionLowerContext Box
- Encapsulates all function-scoped state in a dedicated Box
- Automatic lifetime management (created at entry, destroyed at exit)
- Eliminates manual state clearing
- Simplifies from tuple-key to simple block_id access

Implementation:
1. Created src/llvm_py/context/function_lower_context.py (150+ lines)
   - block_end_values, def_blocks, jump_only_blocks
   - phi_manager, resolver_caches
   - Helper methods: get/set_block_snapshot(), register_def(), etc.

2. Updated function_lower.py
   - Creates context at function entry
   - Binds resolver to context for cache isolation
   - Removed _clear_function_local_state() (~80 lines)

3. Updated block_lower.py
   - Changed tuple-key (func_name, block_id) to simple block_id
   - Access via context.get_block_snapshot() / context.set_block_snapshot()

4. Updated resolver.py, phi_wiring/wiring.py, phi_wiring/tagging.py
   - All state access now through context

5. Fixed critical bug in phi_wiring/tagging.py
   - setup_phi_placeholders() was breaking context connection
   - Changed reassignment to .clear()/.update() to preserve reference

Benefits:
-  Automatic state isolation (no manual clearing)
-  Clear ownership (one context per function)
-  Eliminated tuple-keys (simpler code)
-  Prevents bugs by design (cross-function leakage impossible)

Test results:
-  Phase 87 smoke test: PASS
-  Phase 132 smoke test: PASS (both cases)
-  STRICT mode: Works with multi-function inputs
-  No regression

Files modified: 8 (2 new, 6 updated)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-15 11:26:10 +09:00
parent 42f7eaa215
commit e0fb6fecf6
8 changed files with 279 additions and 128 deletions

View File

@ -58,9 +58,9 @@ class DeferredTerminator(NamedTuple):
vmap_snapshot: Dict[int, ir.Value]
def resolve_jump_only_snapshots(builder, block_by_id: Dict[int, Dict[str, Any]], func_name: str = "unknown"):
def resolve_jump_only_snapshots(builder, block_by_id: Dict[int, Dict[str, Any]], context):
"""Phase 131-14-B P0-2: Resolve jump-only block snapshots (Pass B).
Phase 132-P0: Use tuple-key (func_name, block_id) to prevent cross-function collision.
Phase 132-P1: Use context Box for function-local state isolation.
This function runs AFTER all blocks have been lowered (Pass A) but BEFORE
PHI finalization. It resolves snapshots for jump-only blocks by following
@ -71,14 +71,14 @@ def resolve_jump_only_snapshots(builder, block_by_id: Dict[int, Dict[str, Any]],
SSOT: Snapshots are based on CFG structure, not processing order.
Args:
func_name: Function name for tuple-key (func_name, block_id) in block_end_values
context: FunctionLowerContext Box containing function-local state
"""
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', {})
jump_only = context.jump_only_blocks
if not jump_only:
if trace_vmap:
print("[vmap/resolve/passB] No jump-only blocks to resolve", file=sys.stderr)
@ -114,9 +114,9 @@ def resolve_jump_only_snapshots(builder, block_by_id: Dict[int, Dict[str, Any]],
return resolved[bid]
# Normal block - already has snapshot from Pass A
# Phase 132-P0: Use tuple-key (func_name, block_id)
if (func_name, bid) in builder.block_end_values:
snapshot = builder.block_end_values[(func_name, bid)]
# Phase 132-P1: Use context.block_end_values (simple block_id key)
snapshot = context.get_block_snapshot(bid)
if snapshot:
if trace_vmap:
print(
f"[vmap/resolve/passB] bb{bid} is normal block with snapshot "
@ -171,10 +171,10 @@ def resolve_jump_only_snapshots(builder, block_by_id: Dict[int, Dict[str, Any]],
return {}
# Resolve all jump-only blocks
# Phase 132-P0: Use tuple-key (func_name, block_id)
# Phase 132-P1: Use context.set_block_snapshot (simple block_id key)
for bid in sorted(jump_only.keys()):
snapshot = resolve(bid)
builder.block_end_values[(func_name, bid)] = snapshot
context.set_block_snapshot(bid, snapshot)
if trace_vmap:
print(
@ -187,12 +187,12 @@ def resolve_jump_only_snapshots(builder, block_by_id: Dict[int, Dict[str, Any]],
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, func_name: str = "unknown"):
def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, Any]], order: List[int], loop_plan: Dict[str, Any] | None, context):
"""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
Phase 132-P0: Tuple-key (func_name, block_id) to prevent cross-function collision
Phase 132-P1: Use context Box for function-local state isolation
- 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
@ -203,7 +203,7 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
other instructions, and terminators must be last.
Args:
func_name: Function name for tuple-key (func_name, block_id) in block_end_values
context: FunctionLowerContext Box containing function-local state
"""
skipped: set[int] = set()
if loop_plan is not None:
@ -329,10 +329,10 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
else:
body_ops.append(inst)
# Per-block SSA map
# Phase 132-Post: Use PhiManager Box for PHI filtering (Box-First principle)
# Phase 132-P1: Use context.phi_manager for PHI filtering (Box-First principle)
vmap_cur: Dict[int, ir.Value] = {}
try:
vmap_cur = builder.phi_manager.filter_vmap_preserve_phis(
vmap_cur = context.phi_manager.filter_vmap_preserve_phis(
builder.vmap or {},
int(bid)
)
@ -414,8 +414,9 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
created_ids.append(dst)
# P0-1.5: Update def_blocks IMMEDIATELY after instruction lowering
# This ensures resolver can detect defined_here for same-block uses
# Phase 132-P1: Use context.add_def_block
try:
builder.def_blocks.setdefault(dst, set()).add(block_data.get("id", 0))
context.add_def_block(dst, block_data.get("id", 0))
except Exception:
pass
except Exception:
@ -434,12 +435,12 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
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
# Phase 132-Post: Use PhiManager Box for PHI protection (Box-First principle)
# Phase 132-P1: Use context.phi_manager for PHI protection (Box-First principle)
try:
# Create sync dict from created values only
sync_dict = {vid: vmap_cur[vid] for vid in created_ids if vid in vmap_cur}
# PhiManager.sync_protect_phis ensures PHIs are never overwritten (SSOT)
builder.phi_manager.sync_protect_phis(builder.vmap, sync_dict)
context.phi_manager.sync_protect_phis(builder.vmap, sync_dict)
if os.environ.get('NYASH_LLVM_VMAP_TRACE') == '1':
print(f"[vmap/sync] bb{bid} synced {len(sync_dict)} values to builder.vmap (PHIs protected)", file=sys.stderr)
except Exception:
@ -449,10 +450,6 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
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(
@ -483,7 +480,7 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
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
context.register_jump_only_block(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"
@ -516,7 +513,7 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
snap = dict(vmap_cur)
# Phase 131-14-B: Only store snapshot if not deferred (snap is not None)
# Phase 132-P0: Use tuple-key (func_name, block_id)
# Phase 132-P1: Use context.set_block_snapshot (simple block_id key)
if snap is not None:
try:
keys = sorted(list(snap.keys()))
@ -525,8 +522,8 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
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[(func_name, bid)] = snap
context.add_def_block(vid, block_data.get("id", 0))
context.set_block_snapshot(bid, snap)
else:
# Jump-only block with deferred snapshot - don't store yet
if trace_vmap:

View File

@ -9,74 +9,8 @@ from phi_wiring import (
finalize_phis as _finalize_phis,
build_succs as _build_succs,
)
def _clear_function_local_state(builder):
"""
Phase 132-P1: Clear all function-local state at function boundary.
Box-First Principle: Prevents cross-function ValueId/BlockId collisions
by clearing all function-scoped state when entering a new function.
Cleared state:
1. PhiManager.predeclared - (bid, vid) -> phi_value
2. Resolver caches - i64_cache, ptr_cache, f64_cache, _end_i64_cache
3. Jump-only blocks - _jump_only_blocks
4. PHI incomings - block_phi_incomings, def_blocks
5. String/NewBox caches - function-local string handling
"""
try:
# 1. PhiManager predeclared PHIs (bid, vid) -> phi_value
if hasattr(builder, 'phi_manager') and hasattr(builder.phi_manager, 'predeclared'):
builder.phi_manager.predeclared.clear()
except Exception:
pass
try:
# 2. Resolver caches (all keyed by block/value IDs)
builder.resolver.i64_cache.clear()
builder.resolver.ptr_cache.clear()
builder.resolver.f64_cache.clear()
if hasattr(builder.resolver, '_end_i64_cache'):
builder.resolver._end_i64_cache.clear()
# 3. String-related caches (function-local)
if hasattr(builder.resolver, 'string_ids'):
builder.resolver.string_ids.clear()
if hasattr(builder.resolver, 'string_literals'):
builder.resolver.string_literals.clear()
if hasattr(builder.resolver, 'string_ptrs'):
builder.resolver.string_ptrs.clear()
if hasattr(builder.resolver, 'length_cache'):
builder.resolver.length_cache.clear()
# 4. NewBox→string-arg hints (function-local)
if hasattr(builder.resolver, 'newbox_string_args') and isinstance(builder.resolver.newbox_string_args, dict):
builder.resolver.newbox_string_args.clear()
except Exception:
pass
try:
# 5. Jump-only blocks (bid -> pred_bid)
if hasattr(builder, '_jump_only_blocks'):
builder._jump_only_blocks.clear()
except Exception:
pass
try:
# 6. PHI incomings and def_blocks (function-local)
if hasattr(builder, 'block_phi_incomings'):
builder.block_phi_incomings.clear()
if hasattr(builder, 'def_blocks'):
builder.def_blocks.clear()
# Also clear resolver's references to these
if hasattr(builder.resolver, 'block_phi_incomings'):
builder.resolver.block_phi_incomings = {}
if hasattr(builder.resolver, 'def_blocks'):
builder.resolver.def_blocks.clear()
except Exception:
pass
from context import FunctionLowerContext
from phi_manager import PhiManager
def lower_function(builder, func_data: Dict[str, Any]):
@ -117,8 +51,24 @@ def lower_function(builder, func_data: Dict[str, Any]):
builder.bb_map.clear()
except Exception:
builder.bb_map = {}
# Phase 132-P1: Clear all function-local state (prevent cross-function collision)
_clear_function_local_state(builder)
# Phase 132-P1: Create function-local context Box
# This automatically isolates all function-scoped state
context = FunctionLowerContext(name)
# Initialize PHI manager within context
context.phi_manager = PhiManager()
# Connect builder attributes to context storage (for backward compatibility)
builder.phi_manager = context.phi_manager
builder.block_phi_incomings = context.block_phi_incomings
builder.def_blocks = context.def_blocks
# Bind resolver to context (redirects caches to context storage)
builder.resolver.bind_context(context)
# Store context in builder for access by sub-components
builder.context = context
# Phase 131-15-P1: Load value_types metadata from JSON into resolver
try:
@ -248,8 +198,9 @@ def lower_function(builder, func_data: Dict[str, Any]):
if os.environ.get('NYASH_LLVM_PREPASS_IFMERGE') == '1':
plan = plan_ret_phi_predeclare(block_by_id)
if plan:
if not hasattr(builder, 'block_phi_incomings') or builder.block_phi_incomings is None:
builder.block_phi_incomings = {}
# Phase 132-P1: block_phi_incomings already points to context storage
# No need to reassign - just ensure it exists
pass
for bbid, ret_vid in plan.items():
try:
preds_raw = [p for p in builder.preds.get(bbid, []) if p != bbid]
@ -297,8 +248,8 @@ def lower_function(builder, func_data: Dict[str, Any]):
except Exception:
pass
return uses
if not hasattr(builder, 'block_phi_incomings') or builder.block_phi_incomings is None:
builder.block_phi_incomings = {}
# Phase 132-P1: block_phi_incomings already points to context storage
# No need to reassign - it's already initialized
for bid, blk in block_by_id.items():
try:
preds_raw = [p for p in local_preds.get(int(bid), []) if p != int(bid)]
@ -339,14 +290,14 @@ def lower_function(builder, func_data: Dict[str, Any]):
loop_plan = None
# Phase 131-4 Pass A: Lower non-terminator instructions (terminators deferred)
# Phase 132-P0: Pass func.name for tuple-key (func_name, block_id)
# Phase 132-P1: Pass context Box for function-local state isolation
from builders.block_lower import lower_blocks as _lower_blocks
_lower_blocks(builder, func, block_by_id, order, loop_plan, func_name=name)
_lower_blocks(builder, func, block_by_id, order, loop_plan, context)
# Phase 131-14-B Pass B: Resolve jump-only block snapshots (BEFORE PHI finalization)
# Phase 132-P0: Pass func_name for tuple-key
# Phase 132-P1: Pass context Box for function-local state isolation
from builders.block_lower import resolve_jump_only_snapshots as _resolve_jump_only_snapshots
_resolve_jump_only_snapshots(builder, block_by_id, func_name=name)
_resolve_jump_only_snapshots(builder, block_by_id, context)
# Optional: capture lowering ctx for downstream helpers
try:
@ -370,8 +321,8 @@ def lower_function(builder, func_data: Dict[str, Any]):
pass
# Phase 131-4 Pass B (now Pass B2): Finalize PHIs (wires incoming edges)
# Phase 132-P0: Pass func_name for tuple-key resolution
_finalize_phis(builder, func_name=name)
# Phase 132-P1: Pass context Box for function-local state isolation
_finalize_phis(builder, context)
# Phase 131-4 Pass C: Lower deferred terminators (after PHIs are placed)
from builders.block_lower import lower_terminators as _lower_terminators

View File

@ -0,0 +1,9 @@
"""
Phase 132-P1: Function-local context module
Box-First principle: Isolate function-level state to prevent cross-function collisions.
"""
from .function_lower_context import FunctionLowerContext
__all__ = ['FunctionLowerContext']

View File

@ -0,0 +1,148 @@
"""
Function-local state container for LLVM lowering.
Phase 132-P1: Box-First isolation of function-level state.
Prevents cross-function state leakage and collisions.
"""
from typing import Dict, Set, Any
from llvmlite import ir
class FunctionLowerContext:
"""
Box containing all function-local lowering state.
Lifetime: Created at function entry, destroyed at function exit.
Scope: Single MIR function lowering.
Benefits:
- Automatic state isolation (no manual clearing needed)
- Clear ownership (context belongs to one function)
- Easy to test (create context, lower, verify)
Design Principle (Box-First):
All function-scoped state lives here. This Box is created fresh for each
function and automatically destroyed when the function lowering completes,
ensuring zero cross-function contamination.
"""
def __init__(self, func_name: str):
"""Initialize function-local context.
Args:
func_name: Name of the function being lowered (for debugging/tracing)
"""
self.func_name = func_name
# Block snapshot state (was: builder.block_end_values with tuple-key)
# Maps: block_id -> {value_id -> ir.Value}
# SSOT: End-of-block value snapshots for PHI wiring
self.block_end_values: Dict[int, Dict[int, ir.Value]] = {}
# Definition tracking (was: builder.def_blocks)
# Maps: value_id -> set of block_ids where it's defined
# Used by resolver to determine if value is defined in current block
self.def_blocks: Dict[int, Set[int]] = {}
# Jump-only blocks (was: builder._jump_only_blocks)
# Maps: jump_only_block_id -> predecessor_block_id
# Used for snapshot resolution in Pass B
self.jump_only_blocks: Dict[int, int] = {}
# PHI management (was: builder.phi_manager)
# Will be set to PhiManager instance
self.phi_manager: Any = None # Type: PhiManager (avoid circular import)
# Resolver caches (function-local)
# These caches are keyed by (block_name, value_id) and must be
# cleared between functions to prevent cross-function collisions
self.resolver_i64_cache: Dict = {}
self.resolver_ptr_cache: Dict = {}
self.resolver_f64_cache: Dict = {}
self.resolver_end_i64_cache: Dict = {}
# String-related caches (function-local)
self.resolver_string_ids: Set[int] = set()
self.resolver_string_literals: Dict[int, str] = {}
self.resolver_string_ptrs: Dict[int, ir.Value] = {}
self.resolver_length_cache: Dict[int, ir.Value] = {}
# NewBox→string-arg hints (function-local)
self.resolver_newbox_string_args: Dict = {}
# PHI incomings metadata (function-local)
# Maps: block_id -> {value_id -> [(pred_bid, val_vid), ...]}
self.block_phi_incomings: Dict[int, Dict[int, Any]] = {}
def get_block_snapshot(self, block_id: int) -> Dict[int, ir.Value]:
"""Get end-of-block value snapshot for a block.
Args:
block_id: Block ID to get snapshot for
Returns:
Dictionary mapping value_id -> ir.Value (empty dict if not found)
"""
return self.block_end_values.get(block_id, {})
def set_block_snapshot(self, block_id: int, snapshot: Dict[int, ir.Value]) -> None:
"""Set end-of-block value snapshot for a block.
Args:
block_id: Block ID to set snapshot for
snapshot: Dictionary mapping value_id -> ir.Value
"""
self.block_end_values[block_id] = snapshot
def register_jump_only_block(self, block_id: int, pred_id: int) -> None:
"""Register a block as jump-only (trampoline block).
Args:
block_id: ID of jump-only block
pred_id: ID of predecessor block to copy snapshot from
"""
self.jump_only_blocks[block_id] = pred_id
def is_jump_only(self, block_id: int) -> bool:
"""Check if a block is registered as jump-only.
Args:
block_id: Block ID to check
Returns:
True if block is jump-only, False otherwise
"""
return block_id in self.jump_only_blocks
def add_def_block(self, value_id: int, block_id: int) -> None:
"""Record that a value is defined in a block.
Args:
value_id: Value ID
block_id: Block ID where value is defined
"""
if value_id not in self.def_blocks:
self.def_blocks[value_id] = set()
self.def_blocks[value_id].add(block_id)
def is_defined_in_block(self, value_id: int, block_id: int) -> bool:
"""Check if a value is defined in a specific block.
Args:
value_id: Value ID to check
block_id: Block ID to check
Returns:
True if value is defined in the block, False otherwise
"""
return block_id in self.def_blocks.get(value_id, set())
def __repr__(self) -> str:
"""String representation for debugging."""
return (
f"FunctionLowerContext(func_name={self.func_name!r}, "
f"blocks={len(self.block_end_values)}, "
f"jump_only={len(self.jump_only_blocks)}, "
f"defs={len(self.def_blocks)})"
)

View File

@ -112,8 +112,9 @@ class NyashLLVMBuilder:
self.phi_deferrals: List[Tuple[int, int, List[Tuple[int, int]]]] = []
# Predecessor map and per-block end snapshots
self.preds: Dict[int, List[int]] = {}
# Phase 132-P0: Tuple-key (func_name, block_id) to prevent cross-function collision
self.block_end_values: Dict[Tuple[str, int], Dict[int, ir.Value]] = {}
# Phase 132-P1: Legacy storage (replaced by FunctionLowerContext Box per-function)
# These are now only used as fallback/backward compatibility
self.block_end_values: Dict[int, Dict[int, ir.Value]] = {}
# Definition map: value_id -> set(block_id) where the value is defined
# Used as a lightweight lifetime hint to avoid over-localization
self.def_blocks: Dict[int, set] = {}

View File

@ -18,7 +18,11 @@ def setup_phi_placeholders(builder, blocks: List[Dict[str, Any]]):
"""
try:
produced_str = collect_produced_stringish(blocks)
builder.block_phi_incomings = analyze_incomings(blocks)
# Phase 132-P1: Update existing block_phi_incomings dict (points to context storage)
# Don't replace it with assignment, as that breaks the connection to context
analyzed = analyze_incomings(blocks)
builder.block_phi_incomings.clear()
builder.block_phi_incomings.update(analyzed)
trace({"phi": "setup", "produced_str_keys": list(produced_str.keys())})
# Phase 132: Create all PHI placeholders FIRST, before any other operations

View File

@ -137,12 +137,12 @@ def nearest_pred_on_path(
return None
def wire_incomings(builder, block_id: int, dst_vid: int, incoming: List[Tuple[int, int]], func_name: str = "unknown"):
def wire_incomings(builder, block_id: int, dst_vid: int, incoming: List[Tuple[int, int]], context=None):
"""Wire PHI incoming edges for (block_id, dst_vid) using declared (decl_b, v_src) pairs.
Phase 132-P0: Accept func_name for tuple-key (func_name, block_id) resolution.
Phase 132-P1: Use context Box for function-local state isolation.
Args:
func_name: Function name for tuple-key (func_name, block_id) in block_end_values
context: FunctionLowerContext Box containing function-local state
"""
bb = builder.bb_map.get(block_id)
if bb is None:
@ -150,9 +150,14 @@ def wire_incomings(builder, block_id: int, dst_vid: int, incoming: List[Tuple[in
# Prefer an existing PHI already materialized in this block (e.g., by resolver)
phi = None
try:
snap = getattr(builder, 'block_end_values', {}) or {}
# Phase 132-P0: Use tuple-key (func_name, block_id)
cur = (snap.get((func_name, int(block_id)), {}) or {}).get(int(dst_vid))
# Phase 132-P1: Use context.get_block_snapshot (simple block_id key)
if context is not None:
snapshot = context.get_block_snapshot(int(block_id))
cur = snapshot.get(int(dst_vid))
else:
# Fallback for backward compatibility
snap = getattr(builder, 'block_end_values', {}) or {}
cur = snap.get(int(block_id), {}).get(int(dst_vid))
if cur is not None and hasattr(cur, 'add_incoming'):
# Ensure it belongs to the same block
cur_bb_name = getattr(getattr(cur, 'basic_block', None), 'name', None)
@ -216,8 +221,8 @@ def wire_incomings(builder, block_id: int, dst_vid: int, incoming: List[Tuple[in
trace({"phi": "wire_replaced_src", "original": original_vs, "replaced": vs})
try:
# P0-4: Use resolve_incoming for PHI incoming values
# Phase 132-P0: Pass func_name for tuple-key resolution
val = builder.resolver.resolve_incoming(pred_match, vs, func_name=func_name)
# Phase 132-P1: Pass context for function-local state isolation
val = builder.resolver.resolve_incoming(pred_match, vs, context=context)
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)})
@ -246,21 +251,21 @@ def wire_incomings(builder, block_id: int, dst_vid: int, incoming: List[Tuple[in
return wired
def finalize_phis(builder, func_name: str = "unknown"):
def finalize_phis(builder, context):
"""Finalize PHI nodes by wiring their incoming edges.
Phase 132-P0: Pass func_name for tuple-key (func_name, block_id) resolution.
Phase 132-P1: Use context Box for function-local state isolation.
Args:
func_name: Function name for tuple-key (func_name, block_id) in block_end_values
context: FunctionLowerContext Box containing function-local state
"""
total_blocks = 0
total_dsts = 0
total_wired = 0
for block_id, dst_map in (getattr(builder, "block_phi_incomings", {}) or {}).items():
for block_id, dst_map in (context.block_phi_incomings or {}).items():
total_blocks += 1
for dst_vid, incoming in (dst_map or {}).items():
total_dsts += 1
wired = wire_incomings(builder, int(block_id), int(dst_vid), incoming, func_name=func_name)
wired = wire_incomings(builder, int(block_id), int(dst_vid), incoming, context=context)
total_wired += int(wired or 0)
trace({"phi": "finalize", "block": int(block_id), "dst": int(dst_vid), "wired": int(wired or 0)})
trace({"phi": "finalize_summary", "blocks": int(total_blocks), "dsts": int(total_dsts), "incoming_wired": int(total_wired)})

View File

@ -35,8 +35,12 @@ class Resolver:
except Exception:
self.global_vmap = None
self.global_bb_map = None
# Phase 132-P1: Context reference (will be set by bind_context)
self.context = None
# Caches: (block_name, value_id) -> llvm value
# Phase 132-P1: These are now managed via context, but kept for backward compatibility
self.i64_cache: Dict[Tuple[str, int], ir.Value] = {}
self.ptr_cache: Dict[Tuple[str, int], ir.Value] = {}
self.f64_cache: Dict[Tuple[str, int], ir.Value] = {}
@ -49,7 +53,7 @@ class Resolver:
self.string_ids: set[int] = set()
# Cache for repeated string length queries when immutably known
self.length_cache: Dict[int, ir.Value] = {}
# Type shortcuts
self.i64 = ir.IntType(64)
self.i8p = ir.IntType(8).as_pointer()
@ -58,14 +62,39 @@ class Resolver:
self._end_i64_cache: Dict[Tuple[int, int], ir.Value] = {}
# Lifetime hint: value_id -> set(block_id) where it's known to be defined
# Populated by the builder when available.
# Phase 132-P1: Now managed via context, but kept for backward compatibility
self.def_blocks = {}
# Optional: block -> { dst_vid -> [(pred_bid, val_vid), ...] } for PHIs from MIR JSON
self.block_phi_incomings = {}
# P0-1: SSOT for end-of-block values (snapshots)
# Phase 132-P1: Now managed via context, but kept for backward compatibility
self.block_end_values = {}
# P0-3: Circular reference detection (hang prevention)
self._visited: Set[Tuple[int, int]] = set()
def bind_context(self, context):
"""Phase 132-P1: Bind resolver to function-local context Box.
This connects the resolver's caches to the context's function-local storage,
enabling automatic isolation between functions.
Args:
context: FunctionLowerContext instance
"""
self.context = context
# Redirect to context-managed storage
self.i64_cache = context.resolver_i64_cache
self.ptr_cache = context.resolver_ptr_cache
self.f64_cache = context.resolver_f64_cache
self._end_i64_cache = context.resolver_end_i64_cache
self.string_ids = context.resolver_string_ids
self.string_literals = context.resolver_string_literals
self.string_ptrs = context.resolver_string_ptrs
self.length_cache = context.resolver_length_cache
self.def_blocks = context.def_blocks
self.block_phi_incomings = context.block_phi_incomings
# Note: block_end_values access goes through context methods
def mark_string(self, value_id: int) -> None:
try:
self.string_ids.add(int(value_id))
@ -124,9 +153,9 @@ class Resolver:
# Non-STRICT: fallback to 0
return ir.Constant(ir.IntType(64), 0)
def resolve_incoming(self, pred_block_id: int, value_id: int, func_name: str = "unknown") -> ir.Value:
def resolve_incoming(self, pred_block_id: int, value_id: int, context=None) -> ir.Value:
"""P0-2: PHI incoming resolution (snapshot-only reference)
Phase 132-P0: Use tuple-key (func_name, block_id) to prevent cross-function collision
Phase 132-P1: Use context Box for function-local state isolation
Used for resolving PHI incoming values from predecessor blocks.
Only looks at block_end_values snapshot, never vmap_cur.
@ -134,19 +163,26 @@ class Resolver:
Args:
pred_block_id: Predecessor block ID
value_id: Value ID to resolve from predecessor
func_name: Function name for tuple-key (func_name, block_id) in block_end_values
context: FunctionLowerContext Box (if None, uses self.context)
Returns:
LLVM IR value (i64)
"""
# Phase 132-P0: Use tuple-key (func_name, block_id)
snapshot = self.block_end_values.get((func_name, pred_block_id), {})
# Phase 132-P1: Use context.get_block_snapshot (simple block_id key)
ctx = context if context is not None else self.context
if ctx is not None:
snapshot = ctx.get_block_snapshot(pred_block_id)
else:
# Fallback for backward compatibility (legacy code path)
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':
func_name = ctx.func_name if ctx else "unknown"
raise RuntimeError(
f"[LLVM_PY/STRICT] resolve_incoming: v{value_id} not in {func_name}:bb{pred_block_id} snapshot. "
f"Available: {sorted(snapshot.keys())}"