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

@ -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