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

@ -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())}"