stage3: unify to cleanup; MIR return-defer; docs+smokes updated; LLVM(harness): finalize_phis ownership, ret.py simplified, uses-predeclare; cleanup return override green; method-postfix cleanup return WIP (PHI head)

This commit is contained in:
Selfhosting Dev
2025-09-19 02:07:38 +09:00
parent 951a050592
commit 5e818eeb7e
205 changed files with 9671 additions and 1849 deletions

View File

@ -9,7 +9,8 @@ from typing import Dict, Optional
def lower_barrier(
builder: ir.IRBuilder,
barrier_type: str,
ordering: Optional[str] = None
ordering: Optional[str] = None,
ctx=None,
) -> None:
"""
Lower MIR Barrier instruction

View File

@ -5,6 +5,7 @@ Experimental loop normalization following paper-e-loop-signal-ir
import os
import llvmlite.ir as ir
from phi_wiring import phi_at_block_head
from dataclasses import dataclass
from typing import Dict, Tuple, List, Optional, Any
from instructions.safepoint import insert_automatic_safepoint
@ -109,9 +110,9 @@ def lower_while_loopform(
i8 = ir.IntType(8)
i64 = ir.IntType(64)
# Create PHI nodes
tag_phi = builder.phi(i8, name=f"lf{loop_id}_tag")
payload_phi = builder.phi(i64, name=f"lf{loop_id}_payload")
# Create PHI nodes at the block head (LLVM requires PHIs grouped at top)
tag_phi = phi_at_block_head(lf.dispatch, i8, name=f"lf{loop_id}_tag")
payload_phi = phi_at_block_head(lf.dispatch, i64, name=f"lf{loop_id}_payload")
# Add incoming values
# From header (condition false): Break signal

View File

@ -4,6 +4,7 @@ Critical for SSA form - handles value merging from different control flow paths
"""
import llvmlite.ir as ir
from phi_wiring import phi_at_block_head
from typing import Dict, List, Tuple, Optional
def lower_phi(
@ -123,8 +124,8 @@ def lower_phi(
vmap[dst_vid] = ir.Constant(phi_type, 0)
return
# Create PHI instruction now and add incoming
phi = builder.phi(phi_type, name=f"phi_{dst_vid}")
# Create PHI instruction at the block head and add incoming
phi = phi_at_block_head(current_block, phi_type, name=f"phi_{dst_vid}")
for block, val in incoming_pairs:
phi.add_incoming(val, block)

View File

@ -45,83 +45,73 @@ def lower_return(
else:
# Get return value (prefer resolver)
ret_val = None
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
# Fast path: if vmap has a concrete non-PHI value defined in this block, use it directly
if isinstance(value_id, int):
tmp0 = vmap.get(value_id)
try:
# If this block has a declared PHI for the return value, force using the
# local PHI placeholder to ensure dominance and let finalize_phis wire it.
try:
block_name = builder.block.name
cur_bid = int(str(block_name).replace('bb',''))
except Exception:
cur_bid = -1
try:
bm = getattr(resolver, 'block_phi_incomings', {}) or {}
except Exception:
bm = {}
if isinstance(value_id, int) and isinstance(bm.get(cur_bid), dict) and value_id in bm.get(cur_bid):
# Reuse predeclared ret-phi when available
cur = None
is_phi0 = hasattr(tmp0, 'add_incoming')
except Exception:
is_phi0 = False
if tmp0 is not None and not is_phi0:
ret_val = tmp0
if ret_val is None:
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
# Multi-pred special case: construct a PHI at block head and wire per-pred values
# to keep PHIs grouped at top and avoid late synthesis that violates ordering.
cur_bid = int(str(builder.block.name).replace('bb','')) if hasattr(builder.block, 'name') else -1
pred_ids = [p for p in preds.get(cur_bid, []) if p != cur_bid] if isinstance(preds, dict) else []
if isinstance(value_id, int) and len(pred_ids) > 1 and isinstance(return_type, ir.IntType):
# Create PHI at block head
btop = ir.IRBuilder(builder.block)
try:
rp = getattr(resolver, 'ret_phi_map', {}) or {}
key = (int(cur_bid), int(value_id))
if key in rp:
cur = rp[key]
except Exception:
cur = None
if cur is None:
btop = ir.IRBuilder(builder.block)
try:
btop.position_at_start(builder.block)
except Exception:
pass
# Reuse existing local phi if present; otherwise create
cur = vmap.get(value_id)
need_new = True
try:
need_new = not (cur is not None and hasattr(cur, 'add_incoming') and getattr(getattr(cur, 'basic_block', None), 'name', None) == builder.block.name)
except Exception:
need_new = True
if need_new:
cur = btop.phi(ir.IntType(64), name=f"phi_ret_{value_id}")
# Bind to maps
vmap[value_id] = cur
try:
if hasattr(resolver, 'global_vmap') and isinstance(resolver.global_vmap, dict):
resolver.global_vmap[value_id] = cur
btop.position_at_start(builder.block)
except Exception:
pass
ret_val = cur
if ret_val is not None:
builder.ret(ret_val)
return
if isinstance(return_type, ir.PointerType):
ret_val = resolver.resolve_ptr(value_id, builder.block, preds, block_end_values, vmap)
ph = btop.phi(return_type, name=f"res_phi_{value_id}_{cur_bid}")
# Wire per-pred end values
for pred_bid in pred_ids:
pred_bb = bb_map.get(pred_bid) if isinstance(bb_map, dict) else None
if pred_bb is None:
continue
val = resolver._value_at_end_i64(value_id, pred_bid, preds, block_end_values, vmap, bb_map)
if not hasattr(val, 'type'):
val = ir.Constant(return_type, 0)
ph.add_incoming(val, pred_bb)
ret_val = ph
else:
# Prefer pointer→handle reboxing for string-ish returns even if function return type is i64
is_stringish = False
if hasattr(resolver, 'is_stringish'):
try:
is_stringish = resolver.is_stringish(int(value_id))
except Exception:
is_stringish = False
if is_stringish and hasattr(resolver, 'string_ptrs') and int(value_id) in getattr(resolver, 'string_ptrs'):
# Re-box known string pointer to handle
p = resolver.string_ptrs[int(value_id)]
i8p = ir.IntType(8).as_pointer()
i64 = ir.IntType(64)
boxer = None
for f in builder.module.functions:
if f.name == 'nyash.box.from_i8_string':
boxer = f; break
if boxer is None:
boxer = ir.Function(builder.module, ir.FunctionType(i64, [i8p]), name='nyash.box.from_i8_string')
ret_val = builder.call(boxer, [p], name='ret_ptr2h')
# Resolve direct value
if isinstance(return_type, ir.PointerType):
ret_val = resolver.resolve_ptr(value_id, builder.block, preds, block_end_values, vmap)
else:
ret_val = resolver.resolve_i64(value_id, builder.block, preds, block_end_values, vmap, bb_map)
except Exception:
ret_val = None
is_stringish = False
if hasattr(resolver, 'is_stringish'):
try:
is_stringish = resolver.is_stringish(int(value_id))
except Exception:
is_stringish = False
if is_stringish and hasattr(resolver, 'string_ptrs') and int(value_id) in getattr(resolver, 'string_ptrs'):
p = resolver.string_ptrs[int(value_id)]
i8p = ir.IntType(8).as_pointer()
i64 = ir.IntType(64)
boxer = None
for f in builder.module.functions:
if f.name == 'nyash.box.from_i8_string':
boxer = f; break
if boxer is None:
boxer = ir.Function(builder.module, ir.FunctionType(i64, [i8p]), name='nyash.box.from_i8_string')
ret_val = builder.call(boxer, [p], name='ret_ptr2h')
else:
ret_val = resolver.resolve_i64(value_id, builder.block, preds, block_end_values, vmap, bb_map)
if ret_val is None:
ret_val = vmap.get(value_id)
# Default to vmap (non-PHI) if available
tmp = vmap.get(value_id)
try:
is_phi = hasattr(tmp, 'add_incoming')
except Exception:
is_phi = False
if tmp is not None and not is_phi:
ret_val = tmp
if not ret_val:
# Default based on return type
if isinstance(return_type, ir.IntType):

View File

@ -32,6 +32,12 @@ from instructions.controlflow.while_ import lower_while_regular
from phi_wiring import setup_phi_placeholders as _setup_phi_placeholders, finalize_phis as _finalize_phis
from trace import debug as trace_debug
from trace import phi as trace_phi
try:
# Structured JSON trace for PHI wiring (shared with phi_wiring)
from phi_wiring.common import trace as trace_phi_json
except Exception:
def trace_phi_json(_msg):
pass
from prepass.loops import detect_simple_while
from prepass.if_merge import plan_ret_phi_predeclare
from build_ctx import BuildCtx
@ -397,6 +403,90 @@ class NyashLLVMBuilder:
except Exception:
pass
# Predeclare PHIs for values used in a block but defined in predecessors (multi-pred only).
# This keeps PHI nodes grouped at the top and avoids late synthesis during operand resolution.
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 []:
# Minimal keys: lhs/rhs (binop), value (ret/copy), cond (branch), box_val (boxcall)
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
# Ensure map for declared incomings exists
if not hasattr(self, 'block_phi_incomings') or self.block_phi_incomings is None:
self.block_phi_incomings = {}
for bid, blk in block_by_id.items():
# Only multi-pred blocks need PHIs
try:
preds_raw = [p for p in local_preds.get(int(bid), []) if p != int(bid)]
except Exception:
preds_raw = []
# Dedup preds preserve order
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
bb0 = self.bb_map.get(int(bid))
if bb0 is None:
continue
b0 = ir.IRBuilder(bb0)
try:
b0.position_at_start(bb0)
except Exception:
pass
for vid in need:
# Skip if we already have a PHI mapped for (bid, vid)
cur = self.vmap.get(int(vid))
has_phi_here = False
try:
has_phi_here = (
cur is not None and hasattr(cur, 'add_incoming') and
getattr(getattr(cur, 'basic_block', None), 'name', None) == bb0.name
)
except Exception:
has_phi_here = False
if not has_phi_here:
ph = b0.phi(self.i64, name=f"phi_{vid}")
self.vmap[int(vid)] = ph
# Record incoming metadata for finalize_phis (pred -> same vid)
try:
self.block_phi_incomings.setdefault(int(bid), {}).setdefault(int(vid), [])
# Overwrite with dedup list of (pred, vid)
self.block_phi_incomings[int(bid)][int(vid)] = [(int(p), int(vid)) for p in preds_list]
except Exception:
pass
# Expose to resolver
try:
self.resolver.block_phi_incomings = self.block_phi_incomings
except Exception:
pass
except Exception:
pass
# Optional: simple loop prepass → synthesize a structured while body
loop_plan = None
try:
@ -444,9 +534,19 @@ class NyashLLVMBuilder:
try:
# Use a clean per-while vmap context seeded from global placeholders
self._current_vmap = dict(self.vmap)
ok = lower_while_loopform(builder, func, cond_vid, body_insts,
self.loop_count, self.vmap, self.bb_map,
self.resolver, self.preds, self.block_end_values)
ok = lower_while_loopform(
builder,
func,
cond_vid,
body_insts,
self.loop_count,
self.vmap,
self.bb_map,
self.resolver,
self.preds,
self.block_end_values,
getattr(self, 'ctx', None),
)
except Exception:
ok = False
if not ok:
@ -810,7 +910,11 @@ class NyashLLVMBuilder:
try:
import os
keys = sorted(list(snap.keys()))
trace_phi(f"[builder] snapshot bb{bid} keys={keys[:20]}...")
# Emit structured snapshot event for up to first 20 keys
try:
trace_phi_json({"phi": "snapshot", "block": int(bid), "keys": [int(k) for k in keys[:20]]})
except Exception:
pass
except Exception:
pass
# Record block-local definitions for lifetime hinting
@ -941,7 +1045,7 @@ class NyashLLVMBuilder:
elif op == "barrier":
barrier_type = inst.get("type", "memory")
lower_barrier(builder, barrier_type)
lower_barrier(builder, barrier_type, ctx=getattr(self, 'ctx', None))
elif op == "while":
# Experimental LoopForm lowering
@ -1001,7 +1105,10 @@ class NyashLLVMBuilder:
for fr in from_list:
succs.setdefault(fr, []).append(to_bid)
for block_id, dst_map in (getattr(self, 'block_phi_incomings', {}) or {}).items():
trace_phi(f"[finalize] bb{block_id} dsts={list(dst_map.keys())}")
try:
trace_phi_json({"phi": "finalize_begin", "block": int(block_id), "dsts": [int(k) for k in (dst_map or {}).keys()]})
except Exception:
pass
bb = self.bb_map.get(block_id)
if bb is None:
continue
@ -1011,7 +1118,10 @@ class NyashLLVMBuilder:
except Exception:
pass
for dst_vid, incoming in (dst_map or {}).items():
trace_phi(f"[finalize] dst v{dst_vid} incoming={incoming}")
try:
trace_phi_json({"phi": "finalize_dst", "block": int(block_id), "dst": int(dst_vid), "incoming": [(int(v), int(b)) for (b, v) in [(b, v) for (v, b) in (incoming or [])]]})
except Exception:
pass
# Ensure placeholder exists at block head
# Prefer predeclared ret-phi when available and force using it.
predecl = getattr(self, 'predeclared_ret_phis', {}) if hasattr(self, 'predeclared_ret_phis') else {}
@ -1038,7 +1148,10 @@ class NyashLLVMBuilder:
phi = b.phi(self.i64, name=f"phi_{dst_vid}")
self.vmap[dst_vid] = phi
n = getattr(phi, 'name', b'').decode() if hasattr(getattr(phi, 'name', None), 'decode') else str(getattr(phi, 'name', ''))
trace_phi(f"[finalize] target phi={n}")
try:
trace_phi_json({"phi": "finalize_target", "block": int(block_id), "dst": int(dst_vid), "ir": str(n)})
except Exception:
pass
# Wire incoming per CFG predecessor; map src_vid when provided
preds_raw = [p for p in self.preds.get(block_id, []) if p != block_id]
# Deduplicate while preserving order
@ -1214,8 +1327,7 @@ def main():
if dummy:
# Emit dummy ny_main
ir_text = builder._create_dummy_main()
if os.environ.get('NYASH_CLI_VERBOSE') == '1':
print(f"[Python LLVM] Generated dummy IR:\n{ir_text}")
trace_debug(f"[Python LLVM] Generated dummy IR:\n{ir_text}")
builder.compile_to_object(output_file)
print(f"Compiled to {output_file}")
return
@ -1229,8 +1341,7 @@ def main():
mir_json = json.load(f)
llvm_ir = builder.build_from_mir(mir_json)
if os.environ.get('NYASH_CLI_VERBOSE') == '1':
print(f"[Python LLVM] Generated LLVM IR (see NYASH_LLVM_DUMP_IR or tmp/nyash_harness.ll)")
trace_debug("[Python LLVM] Generated LLVM IR (see NYASH_LLVM_DUMP_IR or tmp/nyash_harness.ll)")
builder.compile_to_object(output_file)
print(f"Compiled to {output_file}")

View File

@ -1,270 +0,0 @@
"""
PHI wiring helpers
- setup_phi_placeholders: Predeclare PHIs and collect incoming metadata
- finalize_phis: Wire PHI incomings using end-of-block snapshots and resolver
These operate on the NyashLLVMBuilder instance to keep changes minimal.
Refactor note: core responsibilities are split into smaller helpers so they
can be unit-tested in isolation.
"""
from typing import Dict, List, Any, Optional, Tuple
import os
import json
import llvmlite.ir as ir
# ---- Small helpers (analyzable/testable) ----
def _collect_produced_stringish(blocks: List[Dict[str, Any]]) -> Dict[int, bool]:
produced_str: Dict[int, bool] = {}
for block_data in blocks:
for inst in block_data.get("instructions", []) or []:
try:
opx = inst.get("op")
dstx = inst.get("dst")
if dstx is None:
continue
is_str = False
if opx == "const":
v = inst.get("value", {}) or {}
t = v.get("type")
if t == "string" or (isinstance(t, dict) and t.get("kind") in ("handle", "ptr") and t.get("box_type") == "StringBox"):
is_str = True
elif opx in ("binop", "boxcall", "externcall"):
t = inst.get("dst_type")
if isinstance(t, dict) and t.get("kind") == "handle" and t.get("box_type") == "StringBox":
is_str = True
if is_str:
produced_str[int(dstx)] = True
except Exception:
pass
return produced_str
def _trace(msg: Any):
if os.environ.get("NYASH_LLVM_TRACE_PHI", "0") == "1":
out = os.environ.get("NYASH_LLVM_TRACE_OUT")
# Format as single-line JSON for machine parsing
if not isinstance(msg, (str, bytes)):
try:
msg = json.dumps(msg, ensure_ascii=False, separators=(",", ":"))
except Exception:
msg = str(msg)
if out:
try:
with open(out, "a", encoding="utf-8") as f:
f.write(msg.rstrip() + "\n")
except Exception:
pass
else:
try:
print(msg)
except Exception:
pass
def analyze_incomings(blocks: List[Dict[str, Any]]) -> Dict[int, Dict[int, List[Tuple[int, int]]]]:
"""Return block_phi_incomings map: block_id -> { dst_vid -> [(decl_b, v_src), ...] }"""
result: Dict[int, Dict[int, List[Tuple[int, int]]]] = {}
for block_data in blocks:
bid0 = block_data.get("id", 0)
for inst in block_data.get("instructions", []) or []:
if inst.get("op") == "phi":
try:
dst0 = int(inst.get("dst"))
incoming0 = inst.get("incoming", []) or []
except Exception:
dst0 = None; incoming0 = []
if dst0 is None:
continue
try:
pairs = [(int(b), int(v)) for (v, b) in incoming0]
result.setdefault(int(bid0), {})[dst0] = pairs
_trace({
"phi": "analyze",
"block": int(bid0),
"dst": dst0,
"incoming": pairs,
})
except Exception:
pass
return result
def ensure_phi(builder, block_id: int, dst_vid: int, bb: ir.Block) -> ir.Instruction:
"""Ensure a PHI placeholder exists at the block head for dst_vid and return it."""
b = ir.IRBuilder(bb)
try:
b.position_at_start(bb)
except Exception:
pass
# Prefer predeclared PHIs (e.g., from if-merge prepass)
predecl = getattr(builder, 'predeclared_ret_phis', {}) if hasattr(builder, 'predeclared_ret_phis') else {}
phi = predecl.get((int(block_id), int(dst_vid))) if predecl else None
if phi is not None:
builder.vmap[dst_vid] = phi
_trace({"phi": "ensure_predecl", "block": int(block_id), "dst": int(dst_vid)})
return phi
# Reuse current if it is a PHI in the correct block
cur = builder.vmap.get(dst_vid)
try:
if cur is not None and hasattr(cur, 'add_incoming') and getattr(getattr(cur, 'basic_block', None), 'name', None) == bb.name:
return cur
except Exception:
pass
# Create a new placeholder
ph = b.phi(builder.i64, name=f"phi_{dst_vid}")
builder.vmap[dst_vid] = ph
_trace({"phi": "ensure_create", "block": int(block_id), "dst": int(dst_vid)})
return ph
def _build_succs(preds: Dict[int, List[int]]) -> Dict[int, List[int]]:
succs: Dict[int, List[int]] = {}
for to_bid, from_list in (preds or {}).items():
for fr in from_list:
succs.setdefault(fr, []).append(to_bid)
return succs
def _nearest_pred_on_path(succs: Dict[int, List[int]], preds_list: List[int], decl_b: int, target_bid: int) -> Optional[int]:
from collections import deque
q = deque([decl_b])
visited = set([decl_b])
parent: Dict[int, Any] = {decl_b: None}
while q:
cur = q.popleft()
if cur == target_bid:
par = parent.get(target_bid)
return par if par in preds_list else None
for nx in succs.get(cur, []):
if nx not in visited:
visited.add(nx)
parent[nx] = cur
q.append(nx)
return None
def wire_incomings(builder, block_id: int, dst_vid: int, incoming: List[Tuple[int, int]]):
"""Wire PHI incoming edges for (block_id, dst_vid) using declared (decl_b, v_src) pairs."""
bb = builder.bb_map.get(block_id)
if bb is None:
return
phi = ensure_phi(builder, block_id, dst_vid, bb)
# Normalize predecessor list
preds_raw = [p for p in builder.preds.get(block_id, []) if p != block_id]
seen = set()
preds_list: List[int] = []
for p in preds_raw:
if p not in seen:
preds_list.append(p)
seen.add(p)
succs = _build_succs(builder.preds)
# Precompute a non-self initial source for self-carry
init_src_vid = None
for (_bd0, vs0) in incoming:
try:
vi = int(vs0)
except Exception:
continue
if vi != int(dst_vid):
init_src_vid = vi
break
chosen: Dict[int, ir.Value] = {}
for (b_decl, v_src) in incoming:
try:
bd = int(b_decl); vs = int(v_src)
except Exception:
continue
pred_match = _nearest_pred_on_path(succs, preds_list, bd, block_id)
if pred_match is None:
_trace({
"phi": "wire_skip_no_path",
"decl_b": bd,
"target": int(block_id),
"src": vs,
})
continue
if vs == int(dst_vid) and init_src_vid is not None:
vs = int(init_src_vid)
try:
val = builder.resolver._value_at_end_i64(vs, pred_match, builder.preds, builder.block_end_values, builder.vmap, builder.bb_map)
except Exception:
val = None
if val is None:
val = ir.Constant(builder.i64, 0)
chosen[pred_match] = val
_trace({
"phi": "wire_choose",
"pred": int(pred_match),
"dst": int(dst_vid),
"src": int(vs),
})
for pred_bid, val in chosen.items():
pred_bb = builder.bb_map.get(pred_bid)
if pred_bb is None:
continue
phi.add_incoming(val, pred_bb)
_trace({"phi": "add_incoming", "dst": int(dst_vid), "pred": int(pred_bid)})
# ---- Public API (used by llvm_builder) ----
def setup_phi_placeholders(builder, blocks: List[Dict[str, Any]]):
"""Predeclare PHIs and collect incoming metadata for finalize_phis.
This pass is function-local and must be invoked after basic blocks are
created and before lowering individual blocks. It also tags string-ish
values eagerly to help downstream resolvers choose correct intrinsics.
"""
try:
produced_str = _collect_produced_stringish(blocks)
builder.block_phi_incomings = analyze_incomings(blocks)
_trace({"phi": "setup", "produced_str_keys": list(produced_str.keys())})
# Materialize placeholders and propagate stringish tags
for block_data in blocks:
bid0 = block_data.get("id", 0)
bb0 = builder.bb_map.get(bid0)
for inst in block_data.get("instructions", []) or []:
if inst.get("op") != "phi":
continue
try:
dst0 = int(inst.get("dst"))
incoming0 = inst.get("incoming", []) or []
except Exception:
dst0 = None; incoming0 = []
if dst0 is None or bb0 is None:
continue
_ = ensure_phi(builder, bid0, dst0, bb0)
# Tag propagation
try:
dst_type0 = inst.get("dst_type")
mark_str = isinstance(dst_type0, dict) and dst_type0.get("kind") == "handle" and dst_type0.get("box_type") == "StringBox"
if not mark_str:
for (_b_decl_i, v_src_i) in incoming0:
try:
if produced_str.get(int(v_src_i)):
mark_str = True; break
except Exception:
pass
if mark_str and hasattr(builder.resolver, 'mark_string'):
builder.resolver.mark_string(int(dst0))
except Exception:
pass
# Definition hint: PHI defines dst in this block
try:
builder.def_blocks.setdefault(int(dst0), set()).add(int(bid0))
except Exception:
pass
# Sync to resolver
try:
builder.resolver.block_phi_incomings = builder.block_phi_incomings
except Exception:
pass
except Exception:
pass
def finalize_phis(builder):
"""Finalize PHIs declared in JSON by wiring incoming edges at block heads.
Uses resolver._value_at_end_i64 to materialize values at predecessor ends.
"""
for block_id, dst_map in (getattr(builder, 'block_phi_incomings', {}) or {}).items():
for dst_vid, incoming in (dst_map or {}).items():
wire_incomings(builder, int(block_id), int(dst_vid), incoming)
_trace({"phi": "finalize", "block": int(block_id), "dst": int(dst_vid)})

View File

@ -0,0 +1,33 @@
"""
PHI wiring package
Submodules
- analysis: analyze_incomings and stringish production scan
- wiring: ensure_phi, wire_incomings, finalize_phis
- tagging: setup_phi_placeholders (predeclare + tagging + sync)
This package re-exports the primary helpers for backward compatibility with
`from phi_wiring import ...` and `from src.llvm_py import phi_wiring` usage.
"""
from .analysis import analyze_incomings, collect_produced_stringish
from .wiring import ensure_phi, wire_incomings, finalize_phis, build_succs, nearest_pred_on_path, phi_at_block_head
from .tagging import setup_phi_placeholders
# Backward-compatible aliases for tests that used private helpers
_build_succs = build_succs
_nearest_pred_on_path = nearest_pred_on_path
__all__ = [
"analyze_incomings",
"collect_produced_stringish",
"ensure_phi",
"wire_incomings",
"finalize_phis",
"phi_at_block_head",
"build_succs",
"nearest_pred_on_path",
"setup_phi_placeholders",
"_build_succs",
"_nearest_pred_on_path",
]

View File

@ -0,0 +1,68 @@
from __future__ import annotations
from typing import Dict, List, Any, Tuple
from .common import trace
def collect_produced_stringish(blocks: List[Dict[str, Any]]) -> Dict[int, bool]:
produced_str: Dict[int, bool] = {}
for block_data in blocks:
for inst in block_data.get("instructions", []) or []:
try:
opx = inst.get("op")
dstx = inst.get("dst")
if dstx is None:
continue
is_str = False
if opx == "const":
v = inst.get("value", {}) or {}
t = v.get("type")
if t == "string" or (
isinstance(t, dict)
and t.get("kind") in ("handle", "ptr")
and t.get("box_type") == "StringBox"
):
is_str = True
elif opx in ("binop", "boxcall", "externcall"):
t = inst.get("dst_type")
if (
isinstance(t, dict)
and t.get("kind") == "handle"
and t.get("box_type") == "StringBox"
):
is_str = True
if is_str:
produced_str[int(dstx)] = True
except Exception:
pass
return produced_str
def analyze_incomings(blocks: List[Dict[str, Any]]) -> Dict[int, Dict[int, List[Tuple[int, int]]]]:
"""Return block_phi_incomings: block_id -> { dst_vid -> [(decl_b, v_src), ...] }"""
result: Dict[int, Dict[int, List[Tuple[int, int]]]] = {}
for block_data in blocks:
bid0 = block_data.get("id", 0)
for inst in block_data.get("instructions", []) or []:
if inst.get("op") == "phi":
try:
dst0 = int(inst.get("dst"))
incoming0 = inst.get("incoming", []) or []
except Exception:
dst0 = None
incoming0 = []
if dst0 is None:
continue
try:
pairs = [(int(b), int(v)) for (v, b) in incoming0]
result.setdefault(int(bid0), {})[dst0] = pairs
trace({
"phi": "analyze",
"block": int(bid0),
"dst": dst0,
"incoming": pairs,
})
except Exception:
pass
return result

View File

@ -0,0 +1,26 @@
from __future__ import annotations
from typing import Any
import os
import json
def trace(msg: Any):
if os.environ.get("NYASH_LLVM_TRACE_PHI", "0") != "1":
return
out = os.environ.get("NYASH_LLVM_TRACE_OUT")
if not isinstance(msg, (str, bytes)):
try:
msg = json.dumps(msg, ensure_ascii=False, separators=(",", ":"))
except Exception:
msg = str(msg)
if out:
try:
with open(out, "a", encoding="utf-8") as f:
f.write(msg.rstrip() + "\n")
except Exception:
pass
else:
try:
print(msg)
except Exception:
pass

View File

@ -0,0 +1,66 @@
from __future__ import annotations
from typing import Dict, List, Any
from .common import trace
from .analysis import analyze_incomings, collect_produced_stringish
from .wiring import ensure_phi
def setup_phi_placeholders(builder, blocks: List[Dict[str, Any]]):
"""Predeclare PHIs and collect incoming metadata for finalize_phis.
Function-local: must be invoked after basic blocks are created and before
lowering individual blocks. Also tags string-ish values to help downstream
resolvers.
"""
try:
produced_str = collect_produced_stringish(blocks)
builder.block_phi_incomings = analyze_incomings(blocks)
trace({"phi": "setup", "produced_str_keys": list(produced_str.keys())})
for block_data in blocks:
bid0 = block_data.get("id", 0)
bb0 = builder.bb_map.get(bid0)
for inst in block_data.get("instructions", []) or []:
if inst.get("op") != "phi":
continue
try:
dst0 = int(inst.get("dst"))
incoming0 = inst.get("incoming", []) or []
except Exception:
dst0 = None
incoming0 = []
if dst0 is None or bb0 is None:
continue
_ = ensure_phi(builder, bid0, dst0, bb0)
# Tag propagation
try:
dst_type0 = inst.get("dst_type")
mark_str = (
isinstance(dst_type0, dict)
and dst_type0.get("kind") == "handle"
and dst_type0.get("box_type") == "StringBox"
)
if not mark_str:
# JSON v0 incoming pairs are (value, block)
for (v_src_i, _b_decl_i) in incoming0:
try:
if produced_str.get(int(v_src_i)):
mark_str = True
break
except Exception:
pass
if mark_str and hasattr(builder.resolver, "mark_string"):
builder.resolver.mark_string(int(dst0))
except Exception:
pass
# Definition hint: PHI defines dst in this block
try:
builder.def_blocks.setdefault(int(dst0), set()).add(int(bid0))
except Exception:
pass
try:
builder.resolver.block_phi_incomings = builder.block_phi_incomings
except Exception:
pass
except Exception:
pass

View File

@ -0,0 +1,143 @@
from __future__ import annotations
from typing import Dict, List, Any, Optional, Tuple
import llvmlite.ir as ir
from .common import trace
def ensure_phi(builder, block_id: int, dst_vid: int, bb: ir.Block) -> ir.Instruction:
"""Ensure a PHI placeholder exists at the block head for dst_vid and return it."""
b = ir.IRBuilder(bb)
try:
b.position_at_start(bb)
except Exception:
pass
predecl = getattr(builder, "predeclared_ret_phis", {}) if hasattr(builder, "predeclared_ret_phis") else {}
phi = predecl.get((int(block_id), int(dst_vid))) if predecl else None
if phi is not None:
builder.vmap[dst_vid] = phi
trace({"phi": "ensure_predecl", "block": int(block_id), "dst": int(dst_vid)})
return phi
cur = builder.vmap.get(dst_vid)
try:
if cur is not None and hasattr(cur, "add_incoming") and getattr(getattr(cur, "basic_block", None), "name", None) == bb.name:
return cur
except Exception:
pass
ph = b.phi(builder.i64, name=f"phi_{dst_vid}")
builder.vmap[dst_vid] = ph
trace({"phi": "ensure_create", "block": int(block_id), "dst": int(dst_vid)})
return ph
def phi_at_block_head(block: ir.Block, ty: ir.Type, name: str | None = None) -> ir.Instruction:
"""Create a PHI at the very start of `block` and return it.
Keeps LLVM's requirement that PHI nodes are grouped at the top of a block.
"""
b = ir.IRBuilder(block)
try:
b.position_at_start(block)
except Exception:
pass
return b.phi(ty, name=name) if name is not None else b.phi(ty)
def build_succs(preds: Dict[int, List[int]]) -> Dict[int, List[int]]:
succs: Dict[int, List[int]] = {}
for to_bid, from_list in (preds or {}).items():
for fr in from_list:
succs.setdefault(fr, []).append(to_bid)
return succs
def nearest_pred_on_path(
succs: Dict[int, List[int]], preds_list: List[int], decl_b: int, target_bid: int
) -> Optional[int]:
from collections import deque
q = deque([decl_b])
visited = set([decl_b])
parent: Dict[int, Any] = {decl_b: None}
while q:
cur = q.popleft()
if cur == target_bid:
par = parent.get(target_bid)
return par if par in preds_list else None
for nx in succs.get(cur, []):
if nx not in visited:
visited.add(nx)
parent[nx] = cur
q.append(nx)
return None
def wire_incomings(builder, block_id: int, dst_vid: int, incoming: List[Tuple[int, int]]):
"""Wire PHI incoming edges for (block_id, dst_vid) using declared (decl_b, v_src) pairs."""
bb = builder.bb_map.get(block_id)
if bb is None:
return
phi = ensure_phi(builder, block_id, dst_vid, bb)
preds_raw = [p for p in builder.preds.get(block_id, []) if p != block_id]
seen = set()
preds_list: List[int] = []
for p in preds_raw:
if p not in seen:
preds_list.append(p)
seen.add(p)
succs = build_succs(builder.preds)
init_src_vid = None
for (_bd0, vs0) in incoming:
try:
vi = int(vs0)
except Exception:
continue
if vi != int(dst_vid):
init_src_vid = vi
break
chosen: Dict[int, ir.Value] = {}
for (b_decl, v_src) in incoming:
try:
bd = int(b_decl)
vs = int(v_src)
except Exception:
continue
pred_match = nearest_pred_on_path(succs, preds_list, bd, block_id)
if pred_match is None:
trace({"phi": "wire_skip_no_path", "decl_b": bd, "target": int(block_id), "src": vs})
continue
if vs == int(dst_vid) and init_src_vid is not None:
vs = int(init_src_vid)
try:
val = builder.resolver._value_at_end_i64(
vs, pred_match, builder.preds, builder.block_end_values, builder.vmap, builder.bb_map
)
except Exception:
val = None
if val is None:
val = ir.Constant(builder.i64, 0)
chosen[pred_match] = val
trace({"phi": "wire_choose", "pred": int(pred_match), "dst": int(dst_vid), "src": int(vs)})
wired = 0
for pred_bid, val in chosen.items():
pred_bb = builder.bb_map.get(pred_bid)
if pred_bb is None:
continue
phi.add_incoming(val, pred_bb)
trace({"phi": "add_incoming", "dst": int(dst_vid), "pred": int(pred_bid)})
wired += 1
return wired
def finalize_phis(builder):
total_blocks = 0
total_dsts = 0
total_wired = 0
for block_id, dst_map in (getattr(builder, "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)
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)})

View File

@ -28,8 +28,20 @@ def plan_ret_phi_predeclare(block_by_id: Dict[int, Dict[str, Any]]) -> Optional[
val = term.get('value')
if not isinstance(val, int):
continue
# Heuristic: skip when the return value is freshly defined in this block
# (e.g., returning a const computed in cleanup). Predeclaring a PHI for such
# values is unnecessary and may violate PHI grouping/order.
try:
defined_here = False
for ins in blk.get('instructions') or []:
if isinstance(ins, dict) and ins.get('dst') == int(val):
defined_here = True
break
if defined_here:
continue
except Exception:
pass
pred_list = [p for p in preds.get(int(bid), []) if p != int(bid)]
if len(pred_list) > 1:
plan[int(bid)] = int(val)
return plan or None

View File

@ -222,37 +222,8 @@ class Resolver:
placeholder = vmap.get(value_id)
result = placeholder if (placeholder is not None and hasattr(placeholder, 'add_incoming')) else ir.Constant(self.i64, 0)
else:
# Synthesize a PHI to localize the value and dominate its uses.
try:
bb = bb_map.get(cur_bid) if isinstance(bb_map, dict) else current_block
except Exception:
bb = current_block
b = ir.IRBuilder(bb)
try:
b.position_at_start(bb)
except Exception:
pass
existing = vmap.get(value_id)
if existing is not None and hasattr(existing, 'add_incoming'):
phi = existing
else:
phi = b.phi(self.i64, name=f"res_phi_{value_id}_{cur_bid}")
vmap[value_id] = phi
# Wire end-of-block values from each predecessor
for pred_bid in pred_ids:
try:
pred_bb = bb_map.get(pred_bid) if isinstance(bb_map, dict) else None
except Exception:
pred_bb = None
val = self._value_at_end_i64(value_id, pred_bid, preds, block_end_values, vmap, bb_map)
if pred_bb is None:
# If we cannot map to a real basic block (shouldn't happen),
# fallback to a zero to keep IR consistent.
val = val if hasattr(val, 'type') else ir.Constant(self.i64, 0)
# llvmlite requires a real BasicBlock; skip if missing.
continue
phi.add_incoming(val, pred_bb)
result = phi
# No declared PHI and multi-pred: do not synthesize; fallback to zero
result = ir.Constant(self.i64, 0)
# Cache and return
self.i64_cache[cache_key] = result

View File

@ -0,0 +1,80 @@
import unittest
import llvmlite.ir as ir
from src.llvm_py.phi_wiring import setup_phi_placeholders
class DummyResolver:
def __init__(self):
self.marked = set()
def mark_string(self, vid: int):
self.marked.add(int(vid))
class DummyBuilder:
def __init__(self, bb_map):
self.i64 = ir.IntType(64)
self.vmap = {}
self.def_blocks = {}
self.resolver = DummyResolver()
self.bb_map = bb_map
class TestPhiTagging(unittest.TestCase):
def _mk_blocks_and_bbs(self):
mod = ir.Module(name="m")
fnty = ir.FunctionType(ir.VoidType(), [])
fn = ir.Function(mod, fnty, name="f")
b0 = fn.append_basic_block(name="b0")
b1 = fn.append_basic_block(name="b1")
return mod, {0: b0, 1: b1}
def test_mark_by_dst_type(self):
_mod, bb_map = self._mk_blocks_and_bbs()
builder = DummyBuilder(bb_map)
blocks = [
{
"id": 1,
"instructions": [
{
"op": "phi",
"dst": 42,
"dst_type": {"kind": "handle", "box_type": "StringBox"},
"incoming": [[7, 0]],
}
],
}
]
setup_phi_placeholders(builder, blocks)
self.assertIn(42, builder.resolver.marked)
def test_mark_by_incoming_stringish(self):
_mod, bb_map = self._mk_blocks_and_bbs()
builder = DummyBuilder(bb_map)
blocks = [
{
"id": 0,
"instructions": [
{"op": "const", "dst": 7, "value": {"type": "string", "value": "hi"}}
],
},
{
"id": 1,
"instructions": [
{
"op": "phi",
"dst": 43,
# no dst_type string; inference should happen via incoming
"incoming": [[7, 0]],
}
],
},
]
setup_phi_placeholders(builder, blocks)
self.assertIn(43, builder.resolver.marked)
if __name__ == "__main__":
unittest.main()

View File

@ -14,28 +14,40 @@ Import and use:
"""
import os
import json
_TRACE_OUT = os.environ.get('NYASH_LLVM_TRACE_OUT')
def _write(msg: str) -> None:
if _TRACE_OUT:
try:
with open(_TRACE_OUT, 'a', encoding='utf-8') as f:
f.write(msg.rstrip() + "\n")
return
except Exception:
pass
try:
print(msg, flush=True)
except Exception:
pass
def _enabled(env_key: str) -> bool:
return os.environ.get(env_key) == '1'
def debug(msg: str) -> None:
if _enabled('NYASH_CLI_VERBOSE'):
try:
print(msg, flush=True)
except Exception:
pass
_write(msg)
def phi(msg: str) -> None:
def phi(msg) -> None:
if _enabled('NYASH_LLVM_TRACE_PHI'):
try:
print(msg, flush=True)
except Exception:
pass
# Accept raw strings or arbitrary objects; non-strings are JSON-encoded
if not isinstance(msg, (str, bytes)):
try:
msg = json.dumps(msg, ensure_ascii=False, separators=(",", ":"))
except Exception:
msg = str(msg)
_write(msg)
def values(msg: str) -> None:
if _enabled('NYASH_LLVM_TRACE_VALUES'):
try:
print(msg, flush=True)
except Exception:
pass
_write(msg)