Files
hakorune/src/llvm_py/builders/function_lower.py
nyash-codex aad01a1079 feat(llvm): Phase 132-P2 - Dict ctx removal (FunctionLowerContext SSOT completion)
Completed SSOT unification for FunctionLowerContext by removing the manual
dict ctx creation and assignment in function_lower.py.

Changes:
- Removed builder.ctx = dict(...) creation (18 lines, lines 313-330)
- Removed builder.resolver.ctx assignment (no longer needed)
- Confirmed instruction_lower.py uses context=owner.context throughout
- Added phase132_multifunc_isolation_min.hako test for multi-function isolation
- Extended phase132_exit_phi_parity.sh with Case C (Rust VM context test)

Testing:
- Phase 132 smoke test: All 3 cases PASS
- Phase 87 LLVM exe test: PASS (Result: 42)
- STRICT mode: PASS
- No regressions: Behavior identical before/after (RC:6 maintained)

Impact:
- Reduced manual context management complexity
- FunctionLowerContext now sole source of truth (SSOT)
- Per-function state properly isolated, no cross-function collisions
- Cleaner architecture: context parameter passed explicitly vs manual dict

🤖 Generated with Claude Code
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2025-12-15 12:12:54 +09:00

388 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from typing import Dict, Any, List
from llvmlite import ir
from trace import debug as trace_debug
from prepass.if_merge import plan_ret_phi_predeclare
from prepass.loops import detect_simple_while
from phi_wiring import (
setup_phi_placeholders as _setup_phi_placeholders,
finalize_phis as _finalize_phis,
build_succs as _build_succs,
)
from context import FunctionLowerContext
from phi_manager import PhiManager
def lower_function(builder, func_data: Dict[str, Any]):
"""Lower a single MIR function to LLVM IR using the given builder context.
This is a faithful extraction of NyashLLVMBuilder.lower_function.
"""
import os, re
name = func_data.get("name", "unknown")
builder.current_function_name = name
params = func_data.get("params", [])
blocks = func_data.get("blocks", [])
# Determine function signature
if name == "ny_main":
# Special case: ny_main returns i64 to match runtime (nyrt) expectations
func_ty = ir.FunctionType(builder.i64, [])
else:
# Default: i64(i64, ...) signature; derive arity from '/N' suffix when params missing
m = re.search(r"/(\d+)$", name)
arity = int(m.group(1)) if m else len(params)
# Dev fallback: when params are missing for global (Box.method) functions,
# use observed call-site arity if available (scanned in builder.build_from_mir)
if arity == 0 and '.' in name:
try:
arity = int(builder.call_arities.get(name, 0))
except Exception:
pass
param_types = [builder.i64] * arity
func_ty = ir.FunctionType(builder.i64, param_types)
# Reset per-function maps and resolver caches to avoid cross-function collisions
try:
builder.vmap.clear()
except Exception:
builder.vmap = {}
try:
builder.bb_map.clear()
except Exception:
builder.bb_map = {}
# Phase 132-P1: Clear per-function predeclared PHI placeholders (avoid cross-function leakage)
try:
builder.predeclared_ret_phis.clear()
except Exception:
try:
builder.predeclared_ret_phis = {}
except Exception:
pass
# 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
builder.block_end_values = context.block_end_values
# 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:
metadata = func_data.get('metadata', {})
value_types_json = metadata.get('value_types', {})
# Convert string keys to integers and store in resolver
builder.resolver.value_types = {}
for vid_str, vtype in value_types_json.items():
try:
vid = int(vid_str)
builder.resolver.value_types[vid] = vtype
except (ValueError, TypeError):
pass
except Exception:
# If metadata loading fails, ensure value_types exists (empty fallback)
builder.resolver.value_types = {}
# Create or reuse function
func = None
for f in builder.module.functions:
if f.name == name:
func = f
break
if func is None:
func = ir.Function(builder.module, func_ty, name=name)
# Map parameters to vmap. Prefer mapping by referenced value-ids that have no
# local definition (common in v0 JSON where params appear as lhs/rhs ids).
try:
arity = len(func.args)
# Collect defined and used ids
defs = set()
uses = set()
for bb in (blocks or []):
for ins in (bb.get('instructions') or []):
try:
dstv = ins.get('dst')
if isinstance(dstv, int):
defs.add(int(dstv))
except Exception:
pass
for k in ('lhs','rhs','value','cond','box_val'):
try:
v = ins.get(k)
if isinstance(v, int):
uses.add(int(v))
except Exception:
pass
cand = [vid for vid in uses if vid not in defs]
cand.sort()
mapped = 0
for i in range(min(arity, len(cand))):
builder.vmap[int(cand[i])] = func.args[i]
mapped += 1
# Fallback: also map positional 0..arity-1 to args if not already mapped
for i in range(arity):
if i not in builder.vmap:
builder.vmap[i] = func.args[i]
except Exception:
pass
# Build predecessor map from control-flow edges
builder.preds = {}
for block_data in blocks:
bid = block_data.get("id", 0)
builder.preds.setdefault(bid, [])
for block_data in blocks:
src = block_data.get("id", 0)
for inst in block_data.get("instructions", []):
op = inst.get("op")
if op == "jump":
t = inst.get("target")
if t is not None:
builder.preds.setdefault(t, []).append(src)
elif op == "branch":
th = inst.get("then")
el = inst.get("else")
if th is not None:
builder.preds.setdefault(th, []).append(src)
if el is not None:
builder.preds.setdefault(el, []).append(src)
# Create all blocks first
for block_data in blocks:
bid = block_data.get("id", 0)
block_name = f"bb{bid}"
bb = func.append_basic_block(block_name)
builder.bb_map[bid] = bb
# Build quick lookup for blocks by id
block_by_id: Dict[int, Dict[str, Any]] = {}
for block_data in blocks:
block_by_id[block_data.get("id", 0)] = block_data
# Determine entry block: first with no predecessors; fallback to first block
entry_bid = None
for bid, preds in builder.preds.items():
if len(preds) == 0:
entry_bid = bid
break
if entry_bid is None and blocks:
entry_bid = blocks[0].get("id", 0)
# Compute approx preds-first order
visited = set()
order: List[int] = []
def visit(bid: int):
if bid in visited:
return
visited.add(bid)
for p in builder.preds.get(bid, []):
visit(p)
order.append(bid)
if entry_bid is not None:
visit(entry_bid)
for bid in block_by_id.keys():
if bid not in visited:
visit(bid)
# Prepass: collect PHI metadata and placeholders
_setup_phi_placeholders(builder, blocks)
# Optional: if-merge prepass (gate NYASH_LLVM_PREPASS_IFMERGE)
try:
if os.environ.get('NYASH_LLVM_PREPASS_IFMERGE') == '1':
plan = plan_ret_phi_predeclare(block_by_id)
if plan:
# 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]
except Exception:
preds_raw = []
seen = set(); preds_list = []
for p in preds_raw:
if p not in seen:
preds_list.append(p); seen.add(p)
try:
builder.block_phi_incomings.setdefault(int(bbid), {})[int(ret_vid)] = [
(int(p), int(ret_vid)) for p in preds_list
]
except Exception:
pass
try:
trace_debug(f"[prepass] if-merge: plan metadata at bb{bbid} for v{ret_vid} preds={preds_list}")
except Exception:
pass
except Exception:
pass
# Predeclare PHIs for used-in-block values defined in predecessors (multi-pred only)
try:
from cfg.utils import build_preds_succs
local_preds, _ = build_preds_succs(block_by_id)
def _collect_defs(block):
defs = set()
for ins in block.get('instructions') or []:
try:
dstv = ins.get('dst')
if isinstance(dstv, int):
defs.add(int(dstv))
except Exception:
pass
return defs
def _collect_uses(block):
uses = set()
for ins in block.get('instructions') or []:
for k in ('lhs','rhs','value','cond','box_val'):
try:
v = ins.get(k)
if isinstance(v, int):
uses.add(int(v))
except Exception:
pass
return uses
# 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)]
except Exception:
preds_raw = []
seen = set(); preds_list = []
for p in preds_raw:
if p not in seen:
preds_list.append(p); seen.add(p)
if len(preds_list) <= 1:
continue
defs = _collect_defs(blk)
uses = _collect_uses(blk)
need = [u for u in uses if u not in defs]
if not need:
continue
for vid in need:
try:
builder.block_phi_incomings.setdefault(int(bid), {}).setdefault(int(vid), [])
builder.block_phi_incomings[int(bid)][int(vid)] = [(int(p), int(vid)) for p in preds_list]
except Exception:
pass
try:
builder.resolver.block_phi_incomings = builder.block_phi_incomings
except Exception:
pass
except Exception:
pass
# Optional: simple loop prepass
loop_plan = None
try:
if os.environ.get('NYASH_LLVM_PREPASS_LOOP') == '1':
loop_plan = detect_simple_while(block_by_id)
if loop_plan is not None:
trace_debug(f"[prepass] detect loop header=bb{loop_plan['header']} then=bb{loop_plan['then']} latch=bb{loop_plan['latch']} exit=bb{loop_plan['exit']}")
except Exception:
loop_plan = None
# Phase 131-4 Pass A: Lower non-terminator instructions (terminators deferred)
# 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, context)
# Phase 131-14-B Pass B: Resolve jump-only block snapshots (BEFORE PHI finalization)
# 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, context)
# Phase 132-P2: Dict ctx removed; FunctionLowerContext is now SSOT
# All context access goes through owner.context (passed to instruction handlers)
# Phase 131-4 Pass B (now Pass B2): Finalize PHIs (wires incoming edges)
# 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
_lower_terminators(builder, func)
# Safety pass: ensure every basic block ends with a terminator.
# This avoids llvmlite IR parse errors like "expected instruction opcode" on empty blocks.
try:
_enforce_terminators(builder, func, block_by_id)
except Exception:
# Non-fatal in bring-up; better to emit IR than crash
pass
def _enforce_terminators(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, Any]]):
import re
succs = _build_succs(getattr(builder, 'preds', {}) or {})
for bb in func.blocks:
try:
if bb.terminator is not None:
continue
except Exception:
# If property access fails, try to add a branch/ret anyway
pass
# Parse block id from name like "bb123"
bid = None
try:
m = re.match(r"bb(\d+)$", str(bb.name))
bid = int(m.group(1)) if m else None
except Exception:
bid = None
# Choose a reasonable successor if any
target_bb = None
if bid is not None:
for s in (succs.get(int(bid), []) or []):
try:
cand = builder.bb_map.get(int(s))
except Exception:
cand = None
if cand is not None and cand is not bb:
target_bb = cand
break
ib = ir.IRBuilder(bb)
if target_bb is not None:
try:
ib.position_at_end(bb)
except Exception:
pass
ib.branch(target_bb)
try:
trace_debug(f"[llvm-py] enforce_terminators: br from {bb.name} -> {target_bb.name}")
except Exception:
pass
continue
# Fallback: insert a return of 0 matching function return type (i32 for ny_main, else i64)
try:
rty = func.function_type.return_type
if str(rty) == str(builder.i32):
ib.ret(ir.Constant(builder.i32, 0))
elif str(rty) == str(builder.i64):
ib.ret(ir.Constant(builder.i64, 0))
else:
# Unknown/void synthesize a dummy br to self to keep parser happy (unreachable in practice)
ib.branch(bb)
try:
trace_debug(f"[llvm-py] enforce_terminators: ret/br injected in {bb.name}")
except Exception:
pass
except Exception:
# Last resort: do nothing
pass