fix(joinir): stabilize phase1883 latch/entry preds

This commit is contained in:
2025-12-28 23:39:51 +09:00
parent d8786ebab9
commit dd8c2709bd
11 changed files with 202 additions and 79 deletions

View File

@ -1,6 +1,10 @@
# Self Current Task — Now (main)
## Current Focus: Phase 29ad COMPLETENaming SSOT
## Current Focus: Phase 29ae (JoinIR Regression Pack)
**2025-12-28: Phase 29ae P0 完了**
- 目的: JoinIR の最小回帰セットを SSOT で固定
- 入口: `docs/development/current/main/phases/phase-29ae/README.md`
**2025-12-28: Phase 29ad 完了**
- 目的: Pattern6/7 fixture/smoke の命名規約を SSOT 化し、variant 名を明示して迷いを消す

View File

@ -8,6 +8,9 @@ Related:
## 直近JoinIR/selfhost
- **Phase 29ae✅ COMPLETE: JoinIR Regression Pack (docs-first)**
- 入口: `docs/development/current/main/phases/phase-29ae/README.md`
- **Phase 29ad✅ COMPLETE: Naming SSOT for Pattern6/7 fixtures**
- 入口: `docs/development/current/main/phases/phase-29ad/README.md`

View File

@ -0,0 +1,21 @@
# Phase 29ae: JoinIR Regression Pack (docs-first)
Goal: JoinIR の最小回帰セットを SSOT として固定する。
## Regression pack (SSOT)
- Pattern2: `phase29ab_pattern2_*`
- Pattern6: `phase29ab_pattern6_*`
- Pattern7: `phase29ab_pattern7_*`
- Merge/Phi代表: `apps/tests/phase1883_nested_minimal.hako`RC=9
## Commands
- `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern2_"`
- `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern6_"`
- `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern7_"`
- `./tools/smokes/v2/run.sh --profile integration --filter "phase1883_"`
## Status
- phase1883: PASSRC=9 を成功扱い)

View File

@ -424,11 +424,22 @@ impl LoopHeaderPhiBuilder {
})?
};
// Step 3: Compute entry predecessors (header_preds - latch_block)
// Step 3: Compute entry predecessors (entry_incoming blocks + host entry).
// Phase 257 P1.2-FIX: Multiple entry preds are OK (bb0 host + bb10 JoinIR main)
let mut entry_pred_set: std::collections::BTreeSet<BasicBlockId> =
std::collections::BTreeSet::new();
for entry in info.carrier_phis.values() {
entry_pred_set.insert(entry.entry_incoming.0);
}
if let Some(host_entry_block) = host_entry_block_opt {
entry_pred_set.insert(host_entry_block);
}
// Latch block is never an entry predecessor.
entry_pred_set.remove(&latch_block);
let mut entry_preds: Vec<BasicBlockId> = header_preds
.iter()
.filter(|&&pred| pred != latch_block)
.filter(|&&pred| entry_pred_set.contains(&pred))
.copied()
.collect();
@ -450,6 +461,13 @@ impl LoopHeaderPhiBuilder {
}
}
// Latch preds are all header predecessors that are not entry preds.
let latch_preds: Vec<BasicBlockId> = header_preds
.iter()
.filter(|&&pred| !entry_pred_set.contains(&pred))
.copied()
.collect();
// Step 4: Validate at least one entry predecessor
if entry_preds.is_empty() {
return Err(format!(
@ -463,8 +481,8 @@ impl LoopHeaderPhiBuilder {
let host_desc = host_entry_block_opt.map_or_else(|| "None".to_string(), |bb| format!("bb{}", bb.0));
trace.stderr_if(
&format!(
"[joinir/header-phi] Entry predecessors: {:?} (latch=bb{}, host={}, total_preds={})",
entry_preds, latch_block.0, host_desc, header_preds.len()
"[joinir/header-phi] Entry predecessors: {:?} (latch=bb{}, host={}, total_preds={}, latch_preds={:?})",
entry_preds, latch_block.0, host_desc, header_preds.len(), latch_preds
),
true,
);
@ -499,14 +517,16 @@ impl LoopHeaderPhiBuilder {
// Phase 257 P1.2-FIX: Handle multiple entry predecessors (bb0 host + bb10 JoinIR main)
for (name, entry) in &info.carrier_phis {
let (_stored_entry_block, entry_val) = entry.entry_incoming; // Use value only
let (latch_block_stored, latch_val) = entry.latch_incoming.unwrap();
let (_latch_block_stored, latch_val) = entry.latch_incoming.unwrap();
// Build PHI inputs: all entry preds use same init value, latch uses next value
// Build PHI inputs: entry preds use init value, latch preds use next value
let mut phi_inputs = Vec::new();
for &entry_pred in &entry_preds {
phi_inputs.push((entry_pred, entry_val));
}
phi_inputs.push((latch_block_stored, latch_val));
for &latch_pred in &latch_preds {
phi_inputs.push((latch_pred, latch_val));
}
let phi = MirInstruction::Phi {
dst: entry.phi_dst,

View File

@ -10,6 +10,8 @@ use crate::mir::builder::control_flow::joinir::merge::loop_header_phi_info::Loop
use crate::mir::builder::control_flow::joinir::merge::tail_call_classifier::TailCallKind;
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
use crate::mir::{BasicBlockId, ValueId};
use crate::config::env::joinir_dev_enabled;
use crate::mir::builder::control_flow::joinir::trace;
pub(in crate::mir::builder::control_flow::joinir::merge) fn record_if_backedge(
tail_call_kind: TailCallKind,
@ -24,7 +26,35 @@ pub(in crate::mir::builder::control_flow::joinir::merge) fn record_if_backedge(
let Some(boundary) = boundary else { return };
if loop_header_phi_info
.carrier_phis
.values()
.any(|entry| entry.latch_incoming.is_some())
{
return;
}
if let Some(loop_var_name) = &boundary.loop_var_name {
debug_assert!(
!args.is_empty(),
"Phase 29ae Fail-Fast: BackEdge latch args empty for loop var '{}'",
loop_var_name
);
if joinir_dev_enabled() {
if let Some(entry) = loop_header_phi_info.carrier_phis.get(loop_var_name) {
if let Some(&arg0) = args.first() {
if arg0 == entry.entry_incoming.1 {
trace::trace().stderr_if(
&format!(
"[joinir/latch] warn: loop_var '{}' latch arg matches entry_incoming {:?}",
loop_var_name, arg0
),
true,
);
}
}
}
}
if let Some(&latch_value) = args.first() {
loop_header_phi_info.set_latch_incoming(loop_var_name, new_block_id, latch_value);
}
@ -52,4 +82,3 @@ pub(in crate::mir::builder::control_flow::joinir::merge) fn record_if_backedge(
}
}
}

View File

@ -54,7 +54,8 @@ pub(super) fn resolve_target_func_name<'a>(
/// Used to classify LoopEntry for tail-call handling.
pub(super) fn is_joinir_loop_entry_source(
func_name: &str,
entry_func_name: Option<&str>,
old_block_id: BasicBlockId,
func_entry_block: BasicBlockId,
) -> bool {
entry_func_name.map(|name| name != func_name).unwrap_or(false)
func_name == canonical_names::MAIN && old_block_id == func_entry_block
}

View File

@ -55,10 +55,13 @@ pub(super) fn process_block_instructions(
// First pass: Filter instructions
for inst in &old_block.instructions {
// Skip Copy instructions that overwrite PHI dsts
if is_loop_header_with_phi {
if let MirInstruction::Copy { dst, src: _ } = inst {
if is_loop_header_with_phi && is_loop_header_entry_block {
if let MirInstruction::Copy { dst, src } = inst {
let dst_remapped = remapper.get_value(*dst).unwrap_or(*dst);
if InstructionFilterBox::should_skip_copy_overwriting_phi(dst_remapped, phi_dst_ids_for_block) {
let is_boundary_input = boundary_input_set.contains(src);
if is_boundary_input
&& InstructionFilterBox::should_skip_copy_overwriting_phi(dst_remapped, phi_dst_ids_for_block)
{
log!(
verbose,
"[plan_rewrites] Skipping loop header Copy to PHI dst {:?}",
@ -112,20 +115,23 @@ pub(super) fn process_block_instructions(
}
// Skip Copy instructions that overwrite header PHI dsts
if let MirInstruction::Copy { dst, src: _ } = inst {
let remapped_dst = remapper.get_value(*dst).unwrap_or(*dst);
let is_header_phi_dst = loop_header_phi_info
.carrier_phis
.values()
.any(|entry| entry.phi_dst == remapped_dst);
if is_loop_header_entry_block {
if let MirInstruction::Copy { dst, src } = inst {
let remapped_dst = remapper.get_value(*dst).unwrap_or(*dst);
let is_header_phi_dst = loop_header_phi_info
.carrier_phis
.values()
.any(|entry| entry.phi_dst == remapped_dst);
let is_boundary_input = boundary_input_set.contains(src);
if is_header_phi_dst {
log!(
verbose,
"[plan_rewrites] Skipping Copy that overwrites header PHI dst {:?}",
remapped_dst
);
continue;
if is_header_phi_dst && is_boundary_input {
log!(
verbose,
"[plan_rewrites] Skipping Copy that overwrites header PHI dst {:?}",
remapped_dst
);
continue;
}
}
}

View File

@ -153,13 +153,12 @@ pub(in crate::mir::builder::control_flow::joinir::merge) fn plan_rewrites(
let mut blocks_merge: Vec<_> = func.blocks.iter().collect();
blocks_merge.sort_by_key(|(id, _)| id.0);
// Determine if this is the loop header entry block (loop_step entry).
let is_loop_header_entry_block = entry_func_name == Some(func_name.as_str())
&& blocks_merge.first().map(|(id, _)| **id) == Some(func.entry_block);
// Determine if this function is the loop header (loop_step).
let is_loop_header_func = entry_func_name == Some(func_name.as_str());
// Check if loop header has PHIs
let is_loop_header_with_phi =
is_loop_header_entry_block && !loop_header_phi_info.carrier_phis.is_empty();
is_loop_header_func && !loop_header_phi_info.carrier_phis.is_empty();
// Collect PHI dst IDs for this block (if loop header)
let phi_dst_ids_for_block: HashSet<ValueId> =
@ -191,6 +190,9 @@ pub(in crate::mir::builder::control_flow::joinir::merge) fn plan_rewrites(
let mut new_block = BasicBlock::new(new_block_id);
// PHASE 2: Instruction rewriting (extracted)
let is_loop_header_entry_block =
is_loop_header_func && *old_block_id == func.entry_block;
let (filtered_insts, tail_target) = instruction_rewrite::process_block_instructions(
old_block,
remapper,

View File

@ -8,17 +8,13 @@
//! - Record latch incoming for loop header PHI
// Import helpers from entry_resolver
use super::entry_resolver::{resolve_target_func_name, is_joinir_loop_entry_source};
use super::entry_resolver::resolve_target_func_name;
// Rewriter siblings (2 super:: up from plan/ to stages/, then 1 more to rewriter/)
use super::super::super::{
rewrite_context::RewriteContext,
latch_incoming_recorder,
};
use super::super::super::rewrite_context::RewriteContext;
// Merge level (3 super:: up from plan/ to stages/, then 1 more to rewriter/, then 1 more to merge/)
use super::super::super::super::{
tail_call_classifier::classify_tail_call,
loop_header_phi_info::LoopHeaderPhiInfo,
trace,
};
@ -50,11 +46,11 @@ pub(super) fn process_tail_call_params(
continuation_candidates: &BTreeSet<String>,
is_loop_header_entry_block: bool,
is_loop_header_with_phi: bool,
boundary: Option<&JoinInlineBoundary>,
_boundary: Option<&JoinInlineBoundary>,
loop_header_phi_info: &mut LoopHeaderPhiInfo,
remapper: &mut JoinIrIdRemapper,
ctx: &RewriteContext,
new_block_id: BasicBlockId,
_new_block_id: BasicBlockId,
verbose: bool,
) -> Result<(), String> {
let trace_obj = trace::trace();
@ -80,20 +76,6 @@ pub(super) fn process_tail_call_params(
.map(|name| entry_func_name == Some(name))
.unwrap_or(false);
// Phase 287 P2: Calculate tail_call_kind early for latch incoming logic
// Only treat MAIN's entry block as entry-like (not loop_step's entry block)
let is_entry_like_block_for_latch =
is_joinir_loop_entry_source(func_name, entry_func_name);
// CRITICAL: Argument order must match merge level classify_tail_call()
let tail_call_kind = classify_tail_call(
is_entry_like_block_for_latch,
!loop_header_phi_info.carrier_phis.is_empty(),
boundary.is_some(),
is_target_continuation,
is_target_loop_entry,
);
if let Some(target_func_name) = target_func_name {
if let Some(target_params) = function_params.get(target_func_name) {
@ -114,7 +96,10 @@ pub(super) fn process_tail_call_params(
verbose,
"[plan_rewrites] Skip param bindings in header block (PHIs define carriers)"
);
} else if (is_recursive_call || is_target_loop_entry) && is_loop_header_with_phi {
} else if (is_recursive_call || is_target_loop_entry)
&& is_loop_header_with_phi
&& is_loop_header_entry_block
{
// Update remapper mappings for continuation instructions
for (i, arg_val_remapped) in args.iter().enumerate() {
if i < target_params.len() {
@ -157,7 +142,7 @@ pub(super) fn process_tail_call_params(
.values()
.any(|entry| entry.phi_dst == param_val_dst);
if is_header_phi_dst {
if is_loop_header_entry_block && is_header_phi_dst {
log!(
verbose,
"[plan_rewrites] Skip param binding to PHI dst {:?}",
@ -176,16 +161,5 @@ pub(super) fn process_tail_call_params(
}
}
// Record latch incoming for loop header PHI (SSOT)
// Phase 287 P2: BackEdge のみ latch 記録LoopEntry main → loop_step を除外)
// CRITICAL: Do not move this call - exact location matters
latch_incoming_recorder::record_if_backedge(
tail_call_kind,
boundary,
new_block_id,
args,
loop_header_phi_info,
);
Ok(())
}

View File

@ -17,6 +17,7 @@ use super::super::super::{
plan_box::RewrittenBlocks,
return_converter_box::ReturnConverterBox,
carrier_inputs_collector::CarrierInputsCollector,
latch_incoming_recorder,
terminator::{remap_branch, remap_jump},
};
@ -51,8 +52,8 @@ pub(super) fn process_block_terminator(
found_tail_call: bool,
tail_call_target: Option<(BasicBlockId, &[ValueId])>,
func_name: &str,
_func: &MirFunction,
_old_block_id: BasicBlockId,
func: &MirFunction,
old_block_id: BasicBlockId,
new_block_id: BasicBlockId,
remapper: &JoinIrIdRemapper,
local_block_map: &BTreeMap<BasicBlockId, BasicBlockId>,
@ -62,7 +63,7 @@ pub(super) fn process_block_terminator(
is_continuation_candidate: bool,
is_skippable_continuation: bool,
boundary: Option<&JoinInlineBoundary>,
loop_header_phi_info: &LoopHeaderPhiInfo,
loop_header_phi_info: &mut LoopHeaderPhiInfo,
ctx: &RewriteContext,
result: &mut RewrittenBlocks,
verbose: bool,
@ -243,7 +244,7 @@ pub(super) fn process_block_terminator(
}
}
}
} else if let Some((target_block, _args)) = tail_call_target {
} else if let Some((target_block, args)) = tail_call_target {
// Tail call: Set Jump terminator
// Classify tail call and determine actual target
let target_func_name = resolve_target_func_name(&ctx.function_entry_map, target_block);
@ -259,7 +260,7 @@ pub(super) fn process_block_terminator(
// Phase 287 P2: host entry block からの呼び出しを LoopEntry 扱いにする
// (loop header func の entry block は含めない)
let is_entry_like_block = is_joinir_loop_entry_source(func_name, entry_func_name);
let is_entry_like_block = is_joinir_loop_entry_source(func_name, old_block_id, func.entry_block);
// CRITICAL: Argument order must match merge level classify_tail_call()
let tail_call_kind = classify_tail_call(
@ -270,6 +271,65 @@ pub(super) fn process_block_terminator(
is_target_loop_entry,
);
// SSOT: record latch incoming only when BackEdge is confirmed
if tail_call_kind == TailCallKind::BackEdge {
let mut latch_args: Vec<ValueId> = Vec::new();
let mut loop_var_updated = false;
if let Some(boundary) = boundary {
for (idx, carrier_name) in loop_header_phi_info.carrier_order.iter().enumerate() {
let phi_dst = match loop_header_phi_info.get_carrier_phi(carrier_name) {
Some(dst) => dst,
None => continue,
};
let mut chosen = None;
for inst in new_block.instructions.iter().rev() {
if let MirInstruction::Copy { dst, src } = inst {
if *dst == phi_dst {
chosen = Some(*src);
if *src != *dst {
if boundary.loop_var_name.as_deref() == Some(carrier_name) {
loop_var_updated = true;
}
break;
}
}
}
}
if let Some(val) = chosen {
latch_args.push(val);
} else if let Some(arg) = args.get(idx) {
if boundary.loop_var_name.as_deref() == Some(carrier_name) && *arg != phi_dst {
loop_var_updated = true;
}
latch_args.push(*arg);
}
}
if !loop_var_updated && boundary.loop_var_name.is_some() {
// Skip latch record if we only see self-copies for loop var.
} else {
latch_incoming_recorder::record_if_backedge(
tail_call_kind,
Some(boundary),
new_block_id,
&latch_args,
loop_header_phi_info,
);
}
} else {
latch_incoming_recorder::record_if_backedge(
tail_call_kind,
boundary,
new_block_id,
args,
loop_header_phi_info,
);
}
}
let actual_target = match tail_call_kind {
TailCallKind::BackEdge => {
log!(

View File

@ -7,18 +7,21 @@ require_env || exit 2
# Test: Nested loop (3x3 iterations, sum = 9)
FIXTURE="$NYASH_ROOT/apps/tests/phase1883_nested_minimal.hako"
RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10}
if ! output=$(NYASH_DISABLE_PLUGINS=1 "$NYASH_BIN" --backend vm "$FIXTURE" 2>&1); then
exit_code=$?
log_error "phase1883_nested_minimal_vm: fixture failed to execute"
echo "$output"
set +e
OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env NYASH_DISABLE_PLUGINS=1 HAKO_JOINIR_STRICT=1 "$NYASH_BIN" --backend vm "$FIXTURE" 2>&1)
EXIT_CODE=$?
set -e
if [ "$EXIT_CODE" -eq 124 ]; then
log_error "phase1883_nested_minimal_vm: hakorune timed out (>${RUN_TIMEOUT_SECS}s)"
exit 1
fi
# Check exit code == 9 (expected sum result)
exit_code=$?
if [ "$exit_code" != "9" ]; then
log_error "phase1883_nested_minimal_vm: expected exit code 9, got $exit_code"
if [ "$EXIT_CODE" -ne 9 ]; then
log_error "phase1883_nested_minimal_vm: expected exit code 9, got $EXIT_CODE"
echo "$OUTPUT"
exit 1
fi