phase29af(p0): pattern2 boundary hygiene ssot

This commit is contained in:
2025-12-29 05:12:15 +09:00
parent 62bd42b87f
commit 19f2c6b7f6
14 changed files with 244 additions and 98 deletions

View File

@ -17,7 +17,7 @@ Pattern61-level nested loopの JoinIR→bridge→merge 経路で発生し
- SSOTmergeの契約:
- latch_incoming を記録してよいのは `TailCallKind::BackEdge` のみLoopEntry は上書き禁止)
- `LoopEntry` 判定は “JoinIR main の entry block のみ” を entry-like とする(誤分類で latch が壊れるのを防止)
- entry-like 判定は “JoinIR MAIN のみ” を対象にするblock-id 推測はしない): `src/mir/builder/control_flow/joinir/merge/contract_checks/entry_like_policy.rs`
- latch 二重設定は `debug_assert!` で fail-fast回帰検知
- 変更箇所:
- `src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs`

View File

@ -1,6 +1,12 @@
# Self Current Task — Now (main)
## Current Focus: Phase 29ae (JoinIR Regression Pack)
## Current Focus: Phase 29af (Pattern2 Boundary Hygiene)
**2025-12-29: Phase 29af P0 完了**
- 目的: Pattern2 の boundary 情報の歪みを SSOT 化し、exit/header/latch の責務境界を固定(仕様不変)
- 入口: `docs/development/current/main/phases/phase-29af/README.md`
- 変更: exit_bindings は LoopState のみConditionOnly/LoopLocalZero は carrier_info→header PHI
- 検証: `cargo build --release` / `./tools/smokes/v2/run.sh --profile quick` / `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern2_"` / `./tools/smokes/v2/run.sh --profile integration --filter "phase1883_"` PASS
**2025-12-28: Phase 29ae P1 完了**
- 目的: Merge/Phi Contract SSOT + 回帰パック完全固定

View File

@ -8,6 +8,9 @@ Related:
## 直近JoinIR/selfhost
- **Phase 29af P0in progress: Pattern2 Boundary HygieneSSOT固定**
- 入口: `docs/development/current/main/phases/phase-29af/README.md`
- **Phase 29ae P1✅ COMPLETE: JoinIR Regression Pack (SSOT固定)**
- 入口: `docs/development/current/main/phases/phase-29ae/README.md`

View File

@ -0,0 +1,66 @@
# Phase 29af P1: Boundary Hygiene Contract Checks — Instructions
Status: Ready for execution
Scope: JoinIR merge の contract_checks に boundary hygiene を集約(仕様不変)
## Goal
Phase 29af P0 で確定した boundary hygienePattern2を、merge の `contract_checks` 側でも検証できる形に収束する。
- upstreamPattern2 lowerer 側)だけに Fail-Fast が散らばらないようにする
- future pattern / future refactor で boundary 構築の歪みが再発したときに、merge 入口で検知できるようにする
## Non-goals
- 挙動変更release ビルド既定挙動の変更)
- env var の追加
- fixture/smoke の増加(必要性が明確になった場合のみ)
## Contract (SSOT)
SSOT: `docs/development/current/main/phases/phase-29af/README.md`
- Exit reconnection (`boundary.exit_bindings`) は LoopState のみ
- Header PHI の対象は `boundary.carrier_info` の carriersLoopState + ConditionOnly + LoopLocalZero
- `CarrierInit::FromHost``host_id=0` は契約違反Fail-Fast
- `exit_bindings.carrier_name` の重複は禁止Fail-Fast
## Implementation Steps
1) **contract_checks に boundary hygiene チェックを追加**
- 追加ファイル案: `src/mir/builder/control_flow/joinir/merge/contract_checks/boundary_hygiene.rs`
- 関数案:
- `verify_boundary_hygiene(boundary: &JoinInlineBoundary) -> Result<(), String>`
- 検証項目:
- `exit_bindings``role` が全て LoopState であること
- `exit_bindings``carrier_name` が重複しないこと
- `carrier_info` がある場合:
- `CarrierInit::FromHost` の carrier は `host_id != ValueId(0)` であること
- `exit_bindings` の carrier が `carrier_info` と整合すること(最低限: 同名が存在すること)
2) **merge 入口のチェックに配線**
- `src/mir/builder/control_flow/joinir/merge/contract_checks/boundary_creation.rs`
- boundary の概要ログの直後Fail-Fast 位置)で `verify_boundary_hygiene()` を呼ぶ
- `src/mir/builder/control_flow/joinir/merge/contract_checks/mod.rs`
- module 宣言と re-export を追加
3) **docs を更新(入口と責務の明文化)**
- `docs/development/current/main/phases/phase-29af/README.md`
- P1: contract_checks に集約した旨を追記
- `docs/development/current/main/10-Now.md`
- Phase 29af P1 の進捗Started/Completeに反映
- `docs/development/current/main/30-Backlog.md`
- Phase 29af のステータスを更新
## Verification
- `cargo build --release`
- `./tools/smokes/v2/run.sh --profile quick`
- `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern2_"`
- `./tools/smokes/v2/run.sh --profile integration --filter "phase1883_"`
## Acceptance Criteria
- 既存 smokesquick / phase29ab_pattern2_ / phase1883_が PASS のまま
- release ビルドでの挙動は不変Fail-Fast は debug/strict のみ)
- boundary hygiene の契約が “merge 入口の SSOTcontract_checks” でも検証される

View File

@ -0,0 +1,35 @@
# Phase 29af P0: Pattern2 Boundary Hygiene (SSOT)
Goal: Pattern2 の boundary 情報の歪みを SSOT で整理し、将来の回帰を防ぐ(仕様不変)。
## Boundary Contract (SSOT)
- Header PHI 対象:
- `carrier_info` の carriersLoopState + ConditionOnly + LoopLocalZero
- Exit reconnection 対象:
- LoopState のみConditionOnly は exit_bindings に入れない)
- Host binding 対象:
- `CarrierInit::FromHost` のみBoolConst / LoopLocalZero は host slot 不要)
## Fail-Fast Rules
- exit_bindings の `carrier_name` 重複は禁止debug_assert
- `CarrierInit::FromHost``host_id=0` は Fail-Fast
## Entry Points
- boundary 構築: `src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs`
- header PHI 事前構築: `src/mir/builder/control_flow/joinir/merge/header_phi_prebuild.rs`
- exit_bindings 収集: `src/mir/builder/control_flow/joinir/merge/exit_line/meta_collector.rs`
- latch 記録: `src/mir/builder/control_flow/joinir/merge/rewriter/{tail_call_policy,latch_incoming_recorder}.rs`
## Verification
- `cargo build --release`
- `./tools/smokes/v2/run.sh --profile quick`
- `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern2_"`
- `./tools/smokes/v2/run.sh --profile integration --filter "phase1883_"`
## Notes
- Merge 側の Header PHI Entry/Latch contract は Phase 29ae で SSOT 化済み: `docs/development/current/main/phases/phase-29ae/README.md`

View File

@ -0,0 +1,12 @@
//! Entry-like policy (SSOT)
//!
//! Contract:
//! - "entry-like" は MAIN のみを対象にするby-name 以外の推測は禁止)
use crate::mir::join_ir::lowering::canonical_names;
pub(in crate::mir::builder::control_flow::joinir::merge) fn is_entry_like_source(
func_name: &str,
) -> bool {
func_name == canonical_names::MAIN
}

View File

@ -15,11 +15,13 @@ mod exit_bindings;
mod carrier_inputs;
mod boundary_creation;
mod entry_params;
mod entry_like_policy;
// Re-export public functions
pub(super) use terminator_targets::verify_all_terminator_targets_exist;
pub(super) use exit_bindings::verify_exit_bindings_have_exit_phis;
pub(super) use carrier_inputs::verify_carrier_inputs_complete;
pub(super) use entry_like_policy::is_entry_like_source;
pub(in crate::mir::builder::control_flow::joinir) use boundary_creation::verify_boundary_contract_at_creation;
pub(in crate::mir::builder::control_flow::joinir) use entry_params::run_all_pipeline_checks;

View File

@ -8,7 +8,6 @@
use crate::mir::builder::MirBuilder;
use crate::mir::join_ir::lowering::carrier_info::ExitMeta;
use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding;
use crate::mir::ValueId; // Phase 228-8: For ConditionOnly placeholder
/// ExitMetaCollector: A Box that builds exit_bindings from ExitMeta
///
@ -59,17 +58,15 @@ impl ExitMetaCollector {
/// 2. Create LoopExitBinding with carrier_name, join_exit_value, host_slot
/// 3. Collect into Vec<LoopExitBinding>
///
/// # Phase 228-8: ConditionOnly carrier handling
/// # Phase 29af: Boundary hygiene
///
/// ConditionOnly carriers are included in exit_bindings even if they're not
/// in variable_ctx.variable_map, because they need latch incoming values for header PHI.
/// The host_slot is set to ValueId(0) as a placeholder since ConditionOnly
/// carriers don't participate in exit PHI.
/// ConditionOnly / LoopLocalZero carriers do not participate in exit reconnection.
/// They are excluded from exit_bindings and are handled by header PHIs via carrier_info.
///
/// # Skipped carriers
///
/// Carriers not found in variable_ctx.variable_map AND not in carrier_info are silently skipped.
/// This is intentional: some carriers may not be relevant to the current pattern.
/// - ConditionOnly / LoopLocalZero carriers are excluded from exit_bindings.
/// - Other carriers not found in variable_ctx.variable_map are skipped (or strict-fail).
///
/// # Logging
///
@ -148,8 +145,6 @@ impl ExitMetaCollector {
bindings.push(binding);
} else {
// Phase 228-8: Check if this is a ConditionOnly carrier
// Phase 247-EX: Also check if this is a FromHost carrier (e.g., digit_value)
use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole};
let carrier_meta = if let Some(ci) = carrier_info {
ci.carriers
@ -161,80 +156,34 @@ impl ExitMetaCollector {
};
match carrier_meta {
Some((CarrierRole::ConditionOnly, _)) => {
// Phase 228-8: Include ConditionOnly carrier in exit_bindings
// (needed for latch incoming, not for exit PHI)
let binding = LoopExitBinding {
carrier_name: carrier_name.clone(),
join_exit_value: *join_exit_value,
host_slot: ValueId(0), // Placeholder - not used for ConditionOnly
role: CarrierRole::ConditionOnly,
};
Some((CarrierRole::ConditionOnly, _))
| Some((CarrierRole::LoopState, CarrierInit::LoopLocalZero)) => {
if verbose {
trace.emit_if(
"exit-line",
"collector",
&format!(
"collected ConditionOnly carrier '{}' JoinIR {:?} (not in variable_ctx.variable_map)",
"skipping non-exit carrier '{}' JoinIR {:?} (ConditionOnly/LoopLocalZero)",
carrier_name, join_exit_value
),
true,
);
}
bindings.push(binding);
}
Some((CarrierRole::LoopState, CarrierInit::FromHost)) => {
// Phase 247-EX: Include FromHost carrier in exit_bindings
// (needed for latch incoming, not for exit PHI or variable_ctx.variable_map)
let binding = LoopExitBinding {
carrier_name: carrier_name.clone(),
join_exit_value: *join_exit_value,
host_slot: ValueId(0), // Placeholder - not used for FromHost
role: CarrierRole::LoopState,
};
if verbose {
trace.emit_if(
"exit-line",
"collector",
&format!(
"collected FromHost carrier '{}' JoinIR {:?} (not in variable_ctx.variable_map)",
carrier_name, join_exit_value
),
true,
);
let msg = format!(
"[joinir/exit-line] carrier '{}' missing host slot for FromHost",
carrier_name
);
if strict {
panic!("{}", msg);
} else if verbose {
trace.emit_if("exit-line", "collector", &msg, true);
}
bindings.push(binding);
}
Some((CarrierRole::LoopState, CarrierInit::LoopLocalZero)) => {
// Loop-local derived carrier: include binding with placeholder host slot
let binding = LoopExitBinding {
carrier_name: carrier_name.clone(),
join_exit_value: *join_exit_value,
host_slot: ValueId(0), // No host slot; used for latch/PHI only
role: CarrierRole::LoopState,
};
if verbose {
trace.emit_if(
"exit-line",
"collector",
&format!(
"collected loop-local carrier '{}' JoinIR {:?} (no host slot)",
carrier_name, join_exit_value
),
true,
);
}
bindings.push(binding);
}
_ => {
let msg = format!(
"[joinir/exit-line] carrier '{}' not in variable_ctx.variable_map and not ConditionOnly/FromHost (skip)",
"[joinir/exit-line] carrier '{}' not in variable_ctx.variable_map (skip)",
carrier_name
);
if strict {

View File

@ -14,7 +14,7 @@
use super::{entry_selector, LoopHeaderPhiBuilder, LoopHeaderPhiInfo};
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
use crate::mir::{BasicBlockId, MirModule, ValueId};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::collections::{BTreeMap, HashSet};
/// Pre-build loop header PHIs and reserve ValueIds
///
@ -81,12 +81,6 @@ pub(super) fn prebuild_header_phis(
};
// Extract carriers with their initialization strategy
let exit_carrier_names: BTreeSet<&str> = boundary
.exit_bindings
.iter()
.map(|b| b.carrier_name.as_str())
.collect();
let other_carriers: Vec<(
String,
ValueId,
@ -97,7 +91,6 @@ pub(super) fn prebuild_header_phis(
.carriers
.iter()
.filter(|c| c.name != *loop_var_name)
.filter(|c| exit_carrier_names.contains(c.name.as_str()))
.map(|c| (c.name.clone(), c.host_id, c.init, c.role))
.collect()
} else {

View File

@ -62,16 +62,28 @@ pub(in crate::mir::builder::control_flow::joinir::merge) fn record_if_backedge(
// Other carriers (excluding loop_var)
let mut carrier_arg_idx = if boundary.loop_var_name.is_some() { 1 } else { 0 };
for binding in boundary.exit_bindings.iter() {
if let Some(ref loop_var) = boundary.loop_var_name {
if &binding.carrier_name == loop_var {
if let Some(ref carrier_info) = boundary.carrier_info {
for carrier in carrier_info.carriers.iter() {
if boundary.loop_var_name.as_deref() == Some(carrier.name.as_str()) {
continue;
}
if let Some(&latch_value) = args.get(carrier_arg_idx) {
loop_header_phi_info.set_latch_incoming(&carrier.name, new_block_id, latch_value);
carrier_arg_idx += 1;
}
}
} else {
for binding in boundary.exit_bindings.iter() {
if let Some(ref loop_var) = boundary.loop_var_name {
if &binding.carrier_name == loop_var {
continue;
}
}
if let Some(&latch_value) = args.get(carrier_arg_idx) {
loop_header_phi_info.set_latch_incoming(&binding.carrier_name, new_block_id, latch_value);
carrier_arg_idx += 1;
if let Some(&latch_value) = args.get(carrier_arg_idx) {
loop_header_phi_info.set_latch_incoming(&binding.carrier_name, new_block_id, latch_value);
carrier_arg_idx += 1;
}
}
}

View File

@ -4,20 +4,20 @@
//! - Identify entry-like source blocks (LoopEntry vs BackEdge)
//! - Record latch incoming in one place for BackEdge
use crate::mir::builder::control_flow::joinir::merge::contract_checks::is_entry_like_source;
use crate::mir::builder::control_flow::joinir::merge::loop_header_phi_info::LoopHeaderPhiInfo;
use crate::mir::builder::control_flow::joinir::merge::rewriter::latch_incoming_recorder;
use crate::mir::builder::control_flow::joinir::merge::tail_call_classifier::TailCallKind;
use crate::mir::join_ir::lowering::canonical_names;
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
use crate::mir::{BasicBlock, BasicBlockId, MirInstruction, ValueId};
/// Entry-like source is only MAIN's entry block.
/// Entry-like source is MAIN's blocks.
pub(super) fn is_loop_entry_source(
func_name: &str,
old_block_id: BasicBlockId,
func_entry_block: BasicBlockId,
_old_block_id: BasicBlockId,
_func_entry_block: BasicBlockId,
) -> bool {
func_name == canonical_names::MAIN && old_block_id == func_entry_block
is_entry_like_source(func_name)
}
/// Record latch incoming for BackEdge using copies in the rewritten block.
@ -38,7 +38,31 @@ pub(super) fn record_latch_incoming_if_backedge(
let mut latch_args: Vec<ValueId> = Vec::new();
let mut loop_var_updated = false;
for (idx, carrier_name) in loop_header_phi_info.carrier_order.iter().enumerate() {
let mut ordered_carriers: Vec<&str> = Vec::new();
let mut other_phi_dsts: std::collections::BTreeSet<ValueId> =
std::collections::BTreeSet::new();
if let Some(loop_var) = boundary.loop_var_name.as_deref() {
ordered_carriers.push(loop_var);
for (name, entry) in loop_header_phi_info.carrier_phis.iter() {
if name.as_str() != loop_var {
other_phi_dsts.insert(entry.phi_dst);
}
}
}
if let Some(ref carrier_info) = boundary.carrier_info {
for carrier in carrier_info.carriers.iter() {
ordered_carriers.push(carrier.name.as_str());
}
} else {
for binding in boundary.exit_bindings.iter() {
if boundary.loop_var_name.as_deref() == Some(binding.carrier_name.as_str()) {
continue;
}
ordered_carriers.push(binding.carrier_name.as_str());
}
}
for (idx, carrier_name) in ordered_carriers.iter().enumerate() {
let phi_dst = match loop_header_phi_info.get_carrier_phi(carrier_name) {
Some(dst) => dst,
None => continue,
@ -50,8 +74,8 @@ pub(super) fn record_latch_incoming_if_backedge(
if *dst == phi_dst {
chosen = Some(*src);
if *src != *dst {
if boundary.loop_var_name.as_deref() == Some(carrier_name) {
loop_var_updated = true;
if boundary.loop_var_name.as_deref() == Some(*carrier_name) {
loop_var_updated = !other_phi_dsts.contains(src);
}
break;
}
@ -62,7 +86,10 @@ pub(super) fn record_latch_incoming_if_backedge(
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 {
if boundary.loop_var_name.as_deref() == Some(*carrier_name)
&& *arg != phi_dst
&& !other_phi_dsts.contains(arg)
{
loop_var_updated = true;
}
latch_args.push(*arg);

View File

@ -27,6 +27,14 @@
- `CarrierInit::BoolConst(_)` / `CarrierInit::LoopLocalZero` -> host binding is skipped
- ConditionOnly carriers must not use `FromHost`
## Boundary hygiene (Phase 29af)
- Header PHI 対象: `carrier_info` の carriersLoopState + ConditionOnly + LoopLocalZero
- Exit reconnection 対象: LoopState のみConditionOnly は exit_bindings に入れない)
- Host binding 対象: `CarrierInit::FromHost` のみBoolConst / LoopLocalZero は host slot 不要)
- Fail-Fast: exit_bindings の `carrier_name` 重複は禁止debug_assert
- Fail-Fast: `CarrierInit::FromHost``host_id=0` の場合は Err
- SSOT: `docs/development/current/main/phases/phase-29af/README.md`
## Out of scope
- multiple breaks / continue / return in the loop body
- reassigned LoopBodyLocal outside the derived-slot shape

View File

@ -66,7 +66,25 @@ impl EmitJoinIRStepBox {
let exit_meta = &fragment_meta.exit_meta;
use crate::mir::builder::control_flow::joinir::merge::exit_line::ExitMetaCollector;
let exit_bindings = ExitMetaCollector::collect(builder, exit_meta, Some(&inputs.carrier_info), debug);
let mut exit_bindings =
ExitMetaCollector::collect(builder, exit_meta, Some(&inputs.carrier_info), debug);
// Phase 29af P0: Exit reconnection targets LoopState only.
exit_bindings.retain(|binding| {
binding.role == crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState
});
// Phase 29af P0: Reject duplicate carrier_name in exit_bindings.
#[cfg(debug_assertions)]
{
use std::collections::HashSet;
let mut seen = HashSet::new();
for binding in &exit_bindings {
debug_assert!(
seen.insert(&binding.carrier_name),
"Phase 29af Fail-Fast: duplicate exit_binding carrier '{}'",
binding.carrier_name
);
}
}
// Phase 256.8.5: Use JoinModule.entry.params as SSOT (no hardcoded ValueIds)
use super::super::common::get_entry_function;
@ -78,7 +96,22 @@ impl EmitJoinIRStepBox {
// Build host_input_values in same order (loop_var + carriers)
let mut host_input_values = vec![inputs.loop_var_id];
for carrier in inputs.carrier_info.carriers.iter() {
host_input_values.push(carrier.host_id);
use super::super::common::{decide_carrier_binding_policy, CarrierBindingPolicy};
match decide_carrier_binding_policy(carrier) {
CarrierBindingPolicy::BindFromHost => {
if carrier.host_id == crate::mir::ValueId(0) {
return Err(format!(
"[emit_joinir] Phase 29af Fail-Fast: FromHost carrier '{}' has host_id=0",
carrier.name
));
}
host_input_values.push(carrier.host_id);
}
CarrierBindingPolicy::SkipBinding => {
// Placeholder: SkipBinding does not require a host slot.
host_input_values.push(crate::mir::ValueId(0));
}
}
}
// Verify count consistency (fail-fast)

View File

@ -36,8 +36,8 @@ impl BoundaryInjector {
///
/// # Phase 33-20: All Carriers Header PHI Support
///
/// When `boundary.loop_var_name` is set, ALL carriers (loop var + other carriers
/// from exit_bindings) are handled by header PHIs. We skip ALL join_inputs
/// When `boundary.loop_var_name` is set, ALL carriers (loop var + carrier_info carriers)
/// are handled by header PHIs. We skip ALL join_inputs
/// Copy instructions to avoid overwriting the PHI results.
///
/// # Phase 177-3: PHI Collision Avoidance (Option B)
@ -62,7 +62,7 @@ impl BoundaryInjector {
) -> Result<BTreeMap<ValueId, ValueId>, String> {
// Phase 222.5-E: HashMap → BTreeMap for determinism
// Phase 33-20: When loop_var_name is set, ALL join_inputs are handled by header PHIs
// This includes the loop variable AND all other carriers from exit_bindings.
// This includes the loop variable AND all other carriers from carrier_info.
// We skip ALL join_inputs Copy instructions, only condition_bindings remain.
let skip_all_join_inputs = boundary.loop_var_name.is_some();