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] 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 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 This function runs AFTER all blocks have been lowered (Pass A) but BEFORE
PHI finalization. It resolves snapshots for jump-only blocks by following 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. SSOT: Snapshots are based on CFG structure, not processing order.
Args: 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 import sys
strict_mode = os.environ.get('NYASH_LLVM_STRICT') == '1' strict_mode = os.environ.get('NYASH_LLVM_STRICT') == '1'
trace_vmap = os.environ.get('NYASH_LLVM_TRACE_VMAP') == '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 not jump_only:
if trace_vmap: if trace_vmap:
print("[vmap/resolve/passB] No jump-only blocks to resolve", file=sys.stderr) 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] return resolved[bid]
# Normal block - already has snapshot from Pass A # Normal block - already has snapshot from Pass A
# Phase 132-P0: Use tuple-key (func_name, block_id) # Phase 132-P1: Use context.block_end_values (simple block_id key)
if (func_name, bid) in builder.block_end_values: snapshot = context.get_block_snapshot(bid)
snapshot = builder.block_end_values[(func_name, bid)] if snapshot:
if trace_vmap: if trace_vmap:
print( print(
f"[vmap/resolve/passB] bb{bid} is normal block with snapshot " 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 {} return {}
# Resolve all jump-only blocks # 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()): for bid in sorted(jump_only.keys()):
snapshot = resolve(bid) snapshot = resolve(bid)
builder.block_end_values[(func_name, bid)] = snapshot context.set_block_snapshot(bid, snapshot)
if trace_vmap: if trace_vmap:
print( 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) 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. """Lower blocks in multi-pass to ensure PHIs are always before terminators.
Phase 131-4: Multi-pass block lowering architecture Phase 131-4: Multi-pass block lowering architecture
Phase 131-14-B: Two-pass snapshot resolution 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) - Pass A: Lower non-terminator instructions only (terminators deferred)
- jump-only blocks: record metadata only, NO snapshot resolution - jump-only blocks: record metadata only, NO snapshot resolution
- Pass B: PHI finalization happens in function_lower.py - 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. other instructions, and terminators must be last.
Args: 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() skipped: set[int] = set()
if loop_plan is not None: 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: else:
body_ops.append(inst) body_ops.append(inst)
# Per-block SSA map # 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] = {} vmap_cur: Dict[int, ir.Value] = {}
try: try:
vmap_cur = builder.phi_manager.filter_vmap_preserve_phis( vmap_cur = context.phi_manager.filter_vmap_preserve_phis(
builder.vmap or {}, builder.vmap or {},
int(bid) 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) created_ids.append(dst)
# P0-1.5: Update def_blocks IMMEDIATELY after instruction lowering # P0-1.5: Update def_blocks IMMEDIATELY after instruction lowering
# This ensures resolver can detect defined_here for same-block uses # This ensures resolver can detect defined_here for same-block uses
# Phase 132-P1: Use context.add_def_block
try: 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: except Exception:
pass pass
except Exception: 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) 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) # 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 # 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: try:
# Create sync dict from created values only # Create sync dict from created values only
sync_dict = {vid: vmap_cur[vid] for vid in created_ids if vid in vmap_cur} 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) # 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': 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) print(f"[vmap/sync] bb{bid} synced {len(sync_dict)} values to builder.vmap (PHIs protected)", file=sys.stderr)
except Exception: 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' strict_mode = os.environ.get('NYASH_LLVM_STRICT') == '1'
trace_vmap = os.environ.get('NYASH_LLVM_TRACE_VMAP') == '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) is_jump_only = is_jump_only_block(block_data)
if trace_vmap: if trace_vmap:
print( 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: elif len(preds_list) == 1:
# Single predecessor - record metadata for Pass B resolution # Single predecessor - record metadata for Pass B resolution
pred_bid = preds_list[0] 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 # DO NOT create snapshot here - will be resolved in Pass B
# Set snap to None to indicate "skip storing in block_end_values" # 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) snap = dict(vmap_cur)
# Phase 131-14-B: Only store snapshot if not deferred (snap is not None) # 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: if snap is not None:
try: try:
keys = sorted(list(snap.keys())) 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]]}) trace_phi_json({"phi": "snapshot", "block": int(bid), "keys": [int(k) for k in keys[:20]]})
for vid in created_ids: for vid in created_ids:
if vid in vmap_cur: if vid in vmap_cur:
builder.def_blocks.setdefault(vid, set()).add(block_data.get("id", 0)) context.add_def_block(vid, block_data.get("id", 0))
builder.block_end_values[(func_name, bid)] = snap context.set_block_snapshot(bid, snap)
else: else:
# Jump-only block with deferred snapshot - don't store yet # Jump-only block with deferred snapshot - don't store yet
if trace_vmap: if trace_vmap:

View File

@ -9,74 +9,8 @@ from phi_wiring import (
finalize_phis as _finalize_phis, finalize_phis as _finalize_phis,
build_succs as _build_succs, build_succs as _build_succs,
) )
from context import FunctionLowerContext
from phi_manager import PhiManager
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
def lower_function(builder, func_data: Dict[str, Any]): 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() builder.bb_map.clear()
except Exception: except Exception:
builder.bb_map = {} 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 # Phase 131-15-P1: Load value_types metadata from JSON into resolver
try: try:
@ -248,8 +198,9 @@ def lower_function(builder, func_data: Dict[str, Any]):
if os.environ.get('NYASH_LLVM_PREPASS_IFMERGE') == '1': if os.environ.get('NYASH_LLVM_PREPASS_IFMERGE') == '1':
plan = plan_ret_phi_predeclare(block_by_id) plan = plan_ret_phi_predeclare(block_by_id)
if plan: if plan:
if not hasattr(builder, 'block_phi_incomings') or builder.block_phi_incomings is None: # Phase 132-P1: block_phi_incomings already points to context storage
builder.block_phi_incomings = {} # No need to reassign - just ensure it exists
pass
for bbid, ret_vid in plan.items(): for bbid, ret_vid in plan.items():
try: try:
preds_raw = [p for p in builder.preds.get(bbid, []) if p != bbid] 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: except Exception:
pass pass
return uses return uses
if not hasattr(builder, 'block_phi_incomings') or builder.block_phi_incomings is None: # Phase 132-P1: block_phi_incomings already points to context storage
builder.block_phi_incomings = {} # No need to reassign - it's already initialized
for bid, blk in block_by_id.items(): for bid, blk in block_by_id.items():
try: try:
preds_raw = [p for p in local_preds.get(int(bid), []) if p != int(bid)] 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 loop_plan = None
# Phase 131-4 Pass A: Lower non-terminator instructions (terminators deferred) # 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 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 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 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 # Optional: capture lowering ctx for downstream helpers
try: try:
@ -370,8 +321,8 @@ def lower_function(builder, func_data: Dict[str, Any]):
pass pass
# Phase 131-4 Pass B (now Pass B2): Finalize PHIs (wires incoming edges) # Phase 131-4 Pass B (now Pass B2): Finalize PHIs (wires incoming edges)
# Phase 132-P0: Pass func_name for tuple-key resolution # Phase 132-P1: Pass context Box for function-local state isolation
_finalize_phis(builder, func_name=name) _finalize_phis(builder, context)
# Phase 131-4 Pass C: Lower deferred terminators (after PHIs are placed) # Phase 131-4 Pass C: Lower deferred terminators (after PHIs are placed)
from builders.block_lower import lower_terminators as _lower_terminators 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]]]] = [] self.phi_deferrals: List[Tuple[int, int, List[Tuple[int, int]]]] = []
# Predecessor map and per-block end snapshots # Predecessor map and per-block end snapshots
self.preds: Dict[int, List[int]] = {} self.preds: Dict[int, List[int]] = {}
# Phase 132-P0: Tuple-key (func_name, block_id) to prevent cross-function collision # Phase 132-P1: Legacy storage (replaced by FunctionLowerContext Box per-function)
self.block_end_values: Dict[Tuple[str, int], Dict[int, ir.Value]] = {} # 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 # Definition map: value_id -> set(block_id) where the value is defined
# Used as a lightweight lifetime hint to avoid over-localization # Used as a lightweight lifetime hint to avoid over-localization
self.def_blocks: Dict[int, set] = {} self.def_blocks: Dict[int, set] = {}

View File

@ -18,7 +18,11 @@ def setup_phi_placeholders(builder, blocks: List[Dict[str, Any]]):
""" """
try: try:
produced_str = collect_produced_stringish(blocks) 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())}) trace({"phi": "setup", "produced_str_keys": list(produced_str.keys())})
# Phase 132: Create all PHI placeholders FIRST, before any other operations # Phase 132: Create all PHI placeholders FIRST, before any other operations

View File

@ -137,12 +137,12 @@ def nearest_pred_on_path(
return None 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. """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: 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) bb = builder.bb_map.get(block_id)
if bb is None: 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) # Prefer an existing PHI already materialized in this block (e.g., by resolver)
phi = None phi = None
try: try:
# 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 {} snap = getattr(builder, 'block_end_values', {}) or {}
# Phase 132-P0: Use tuple-key (func_name, block_id) cur = snap.get(int(block_id), {}).get(int(dst_vid))
cur = (snap.get((func_name, int(block_id)), {}) or {}).get(int(dst_vid))
if cur is not None and hasattr(cur, 'add_incoming'): if cur is not None and hasattr(cur, 'add_incoming'):
# Ensure it belongs to the same block # Ensure it belongs to the same block
cur_bb_name = getattr(getattr(cur, 'basic_block', None), 'name', None) 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}) trace({"phi": "wire_replaced_src", "original": original_vs, "replaced": vs})
try: try:
# P0-4: Use resolve_incoming for PHI incoming values # P0-4: Use resolve_incoming for PHI incoming values
# Phase 132-P0: Pass func_name for tuple-key resolution # Phase 132-P1: Pass context for function-local state isolation
val = builder.resolver.resolve_incoming(pred_match, vs, func_name=func_name) val = builder.resolver.resolve_incoming(pred_match, vs, context=context)
trace({"phi": "wire_resolved", "vs": vs, "pred": pred_match, "val_type": type(val).__name__}) trace({"phi": "wire_resolved", "vs": vs, "pred": pred_match, "val_type": type(val).__name__})
except Exception as e: except Exception as e:
trace({"phi": "wire_resolve_fail", "vs": vs, "pred": pred_match, "error": str(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 return wired
def finalize_phis(builder, func_name: str = "unknown"): def finalize_phis(builder, context):
"""Finalize PHI nodes by wiring their incoming edges. """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: 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_blocks = 0
total_dsts = 0 total_dsts = 0
total_wired = 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 total_blocks += 1
for dst_vid, incoming in (dst_map or {}).items(): for dst_vid, incoming in (dst_map or {}).items():
total_dsts += 1 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) 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", "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)}) trace({"phi": "finalize_summary", "blocks": int(total_blocks), "dsts": int(total_dsts), "incoming_wired": int(total_wired)})

View File

@ -36,7 +36,11 @@ class Resolver:
self.global_vmap = None self.global_vmap = None
self.global_bb_map = 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 # 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.i64_cache: Dict[Tuple[str, int], ir.Value] = {}
self.ptr_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] = {} self.f64_cache: Dict[Tuple[str, int], ir.Value] = {}
@ -58,14 +62,39 @@ class Resolver:
self._end_i64_cache: Dict[Tuple[int, int], ir.Value] = {} self._end_i64_cache: Dict[Tuple[int, int], ir.Value] = {}
# Lifetime hint: value_id -> set(block_id) where it's known to be defined # Lifetime hint: value_id -> set(block_id) where it's known to be defined
# Populated by the builder when available. # Populated by the builder when available.
# Phase 132-P1: Now managed via context, but kept for backward compatibility
self.def_blocks = {} self.def_blocks = {}
# Optional: block -> { dst_vid -> [(pred_bid, val_vid), ...] } for PHIs from MIR JSON # Optional: block -> { dst_vid -> [(pred_bid, val_vid), ...] } for PHIs from MIR JSON
self.block_phi_incomings = {} self.block_phi_incomings = {}
# P0-1: SSOT for end-of-block values (snapshots) # 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 = {} self.block_end_values = {}
# P0-3: Circular reference detection (hang prevention) # P0-3: Circular reference detection (hang prevention)
self._visited: Set[Tuple[int, int]] = set() 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: def mark_string(self, value_id: int) -> None:
try: try:
self.string_ids.add(int(value_id)) self.string_ids.add(int(value_id))
@ -124,9 +153,9 @@ class Resolver:
# Non-STRICT: fallback to 0 # Non-STRICT: fallback to 0
return ir.Constant(ir.IntType(64), 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) """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. Used for resolving PHI incoming values from predecessor blocks.
Only looks at block_end_values snapshot, never vmap_cur. Only looks at block_end_values snapshot, never vmap_cur.
@ -134,19 +163,26 @@ class Resolver:
Args: Args:
pred_block_id: Predecessor block ID pred_block_id: Predecessor block ID
value_id: Value ID to resolve from predecessor 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: Returns:
LLVM IR value (i64) LLVM IR value (i64)
""" """
# Phase 132-P0: Use tuple-key (func_name, block_id) # Phase 132-P1: Use context.get_block_snapshot (simple block_id key)
snapshot = self.block_end_values.get((func_name, pred_block_id), {}) 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) val = snapshot.get(value_id)
if val is not None: if val is not None:
return val return val
# Fail-Fast: snapshot miss → structural bug # Fail-Fast: snapshot miss → structural bug
if os.environ.get('NYASH_LLVM_STRICT') == '1': if os.environ.get('NYASH_LLVM_STRICT') == '1':
func_name = ctx.func_name if ctx else "unknown"
raise RuntimeError( raise RuntimeError(
f"[LLVM_PY/STRICT] resolve_incoming: v{value_id} not in {func_name}:bb{pred_block_id} snapshot. " f"[LLVM_PY/STRICT] resolve_incoming: v{value_id} not in {func_name}:bb{pred_block_id} snapshot. "
f"Available: {sorted(snapshot.keys())}" f"Available: {sorted(snapshot.keys())}"