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>
149 lines
5.2 KiB
Python
149 lines
5.2 KiB
Python
"""
|
|
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)})"
|
|
)
|