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:
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
9
src/llvm_py/context/__init__.py
Normal file
9
src/llvm_py/context/__init__.py
Normal 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']
|
||||
148
src/llvm_py/context/function_lower_context.py
Normal file
148
src/llvm_py/context/function_lower_context.py
Normal 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)})"
|
||||
)
|
||||
@ -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] = {}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)})
|
||||
|
||||
@ -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())}"
|
||||
|
||||
Reference in New Issue
Block a user