Files
hakorune/src/mir/join_ir/verify.rs
nyash-codex c63e6deb32 feat(joinir): Task 200-3 - JoinIRVerifier for LoopHeader PHI and ExitLine contracts
Debug-only verification module to catch JoinIR contract violations early:
- verify_loop_header_phis: Checks loop_var_name → PHI exists in header block
- verify_exit_line: Checks exit_bindings → values exist, exit block in range
- verify_joinir_contracts: Main entry point, runs all checks

Implementation:
- Added verification functions to merge/mod.rs (private module has type access)
- Called from merge_joinir_mir_blocks after exit block setup
- Only active in debug builds (#[cfg(debug_assertions)])

Benefits:
- Catches "动くけど header PHI 無い" bugs immediately
- Validates exit_bindings before variable_map reconnection
- Prevents silent contract violations during development
2025-12-08 04:33:33 +09:00

394 lines
13 KiB
Rust
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.

//! JoinIR Progress Carrier Verification (Phase 29 L-5.2)
//!
//! # Purpose
//!
//! ループの「進捗キャリア」が backedge で必ず更新されていることを検証する。
//! ゼロ進捗 backedgeprogress carrier が変化しない再帰呼び出し)を検出し、
//! 無限ループの可能性を早期に警告する。
//!
//! # Phase 29 Strategy
//!
//! - まずは minimal skip_ws JoinIR のみを対象に、dev モードで警告を出す。
//! - 将来Phase 30では Verifier エラーに格上げする予定。
//!
//! # Usage
//!
//! ```ignore
//! // NYASH_JOINIR_EXPERIMENT=1 のときのみ有効
//! if let Err(e) = verify_progress_for_skip_ws(&join_module) {
//! eprintln!("[joinir/progress] warning: {:?}", e);
//! }
//! ```
use crate::mir::join_ir::{BinOpKind, JoinFuncId, JoinInst, JoinModule, MirLikeInst};
use crate::mir::ValueId;
/// Progress verification error
#[derive(Debug, Clone)]
pub enum ProgressError {
/// Progress carrier is not updated before backedge (recursive call)
ZeroProgressBackedge {
/// Progress variable name (for diagnostics)
progress_var: String,
/// Loop function ID
loop_func_id: JoinFuncId,
/// The recursive call that doesn't update progress
call_index: usize,
},
/// No recursive call found in loop function
NoRecursiveCall { loop_func_id: JoinFuncId },
/// Progress carrier not found
ProgressCarrierNotFound {
expected_param_index: usize,
loop_func_id: JoinFuncId,
},
}
/// Verify progress carrier for skip_ws JoinIR (Phase 29 minimal version)
///
/// This function checks that:
/// 1. The loop_step function has a recursive call
/// 2. The progress carrier (i, typically the 3rd parameter) is updated via BinOp::Add
/// before the recursive call
///
/// # Arguments
///
/// * `join_module` - The JoinModule containing skip_ws functions
///
/// # Returns
///
/// Ok(()) if progress carrier is properly updated, Err(ProgressError) otherwise.
///
/// # Skip_ws Structure Expected
///
/// ```text
/// fn loop_step(s: Str, n: Int, i: Int) -> Int {
/// if i >= n { return i }
/// let ch = s.substring(i, i+1)
/// if ch != " " { return i }
/// let next_i = i + 1 // ← progress update
/// loop_step(s, n, next_i) // ← recursive call with updated progress
/// }
/// ```
pub fn verify_progress_for_skip_ws(join_module: &JoinModule) -> Result<(), ProgressError> {
// skip_ws では loop_step が JoinFuncId(1)
let loop_func_id = JoinFuncId::new(1);
let loop_func = match join_module.functions.get(&loop_func_id) {
Some(f) => f,
None => {
// loop_step 関数が無ければ検証スキップ(エラーではない)
return Ok(());
}
};
// skip_ws の progress carrier は 3番目のパラメータ (i)
// params = [s, n, i] → index 2
let progress_param_index = 2;
let progress_param = match loop_func.params.get(progress_param_index) {
Some(&p) => p,
None => {
return Err(ProgressError::ProgressCarrierNotFound {
expected_param_index: progress_param_index,
loop_func_id,
});
}
};
// 1. 再帰呼び出しloop_step への Callを探す
let mut recursive_call_indices: Vec<usize> = Vec::new();
for (idx, inst) in loop_func.body.iter().enumerate() {
if let JoinInst::Call { func, .. } = inst {
if *func == loop_func_id {
recursive_call_indices.push(idx);
}
}
}
if recursive_call_indices.is_empty() {
return Err(ProgressError::NoRecursiveCall { loop_func_id });
}
// 2. 再帰呼び出しの前に progress carrier が更新されているか確認
// 「BinOp::Add で progress_param を使った演算がある」ことを保守的にチェック
for &call_idx in &recursive_call_indices {
let has_progress_update =
check_progress_update_before(&loop_func.body[..call_idx], progress_param);
if !has_progress_update {
return Err(ProgressError::ZeroProgressBackedge {
progress_var: format!("param[{}]", progress_param_index),
loop_func_id,
call_index: call_idx,
});
}
}
Ok(())
}
/// Check if there's a BinOp::Add that uses the progress parameter before the call
///
/// This is a conservative check: we look for any BinOp::Add where progress_param
/// is the LHS. The result should be used as an argument to the recursive call.
fn check_progress_update_before(instructions: &[JoinInst], progress_param: ValueId) -> bool {
for inst in instructions {
if let JoinInst::Compute(MirLikeInst::BinOp {
op: BinOpKind::Add,
lhs,
..
}) = inst
{
if *lhs == progress_param {
return true;
}
}
}
false
}
/// Verify progress carrier for a generic loop (future extension)
///
/// This is a placeholder for Phase 30 where we'll support arbitrary loops.
#[allow(dead_code)]
pub fn verify_progress_generic(
_join_module: &JoinModule,
_loop_func_id: JoinFuncId,
_progress_param_index: usize,
) -> Result<(), ProgressError> {
// Phase 30: 汎用ループの progress チェック
// 現在は未実装skip_ws 専用版のみ)
Ok(())
}
// ============================================================================
// Phase 33-3.2: Select Minimal Invariant Verification
// ============================================================================
/// JoinIR Verify Error for Select verification
#[derive(Debug)]
pub struct JoinIrVerifyError {
message: String,
}
impl JoinIrVerifyError {
pub fn new(message: String) -> Self {
Self { message }
}
}
impl std::fmt::Display for JoinIrVerifyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "JoinIR Verify Error: {}", self.message)
}
}
impl std::error::Error for JoinIrVerifyError {}
/// Phase 33-3.2: Select 命令の最小 invariant チェック
/// IfSelectTest.* 専用の軽い検証
///
/// # 検証項目
///
/// 1. **型一貫性**: then_val と else_val の型が一致(将来拡張)
/// 2. **単一 PHI**: 1 つの Select のみが存在
/// 3. **完全性**: then_val と else_val が両方存在
///
/// # phi_invariants.rs からの移譲
///
/// - `ensure_if_values_exist()` のエッセンス: then/else/pre のいずれかに値が必須
/// → Select では then_val/else_val が必須(完全性チェック)
///
/// # conservative.rs からの移譲
///
/// - `ConservativeMerge::analyze()` の最小部分: 単一変数のみ対象
/// → Select は1つの dst のみ(単一 PHI 保証)
///
/// # Arguments
///
/// * `join_func` - 検証対象の JoinFunction
/// * `debug` - デバッグログ出力フラグ
///
/// # Returns
///
/// Ok(()) if verification passes, Err(JoinIrVerifyError) otherwise.
pub fn verify_select_minimal(
join_func: &crate::mir::join_ir::JoinFunction,
debug: bool,
) -> Result<(), JoinIrVerifyError> {
if debug {
eprintln!("[verify_select_minimal] checking {}", join_func.name);
}
// 1. Select 命令を探す
let mut select_count = 0;
let mut select_inst: Option<&JoinInst> = None;
for inst in &join_func.body {
if let JoinInst::Select { .. } = inst {
select_count += 1;
select_inst = Some(inst);
}
}
// 2. Select が 1 個だけか確認(単一 PHI チェック)
if select_count != 1 {
return Err(JoinIrVerifyError::new(format!(
"verify_select_minimal: expected exactly 1 Select, found {}. \
This violates the 'single PHI' invariant from conservative.rs",
select_count
)));
}
let JoinInst::Select {
dst,
cond,
then_val,
else_val,
type_hint: _, // Phase 63-3: 検証では未使用
} = select_inst.unwrap()
else {
unreachable!()
};
// 3. ValueId の妥当性チェック(基本的な存在確認)
// Phase 33-3.2 では最小チェックのみ
// 将来的に型情報があれば型一貫性チェック追加可能
// 完全性チェック: then_val と else_val が両方存在
// (ValueId は常に存在するので、ここでは簡易チェックのみ)
// phi_invariants.rs::ensure_if_values_exist() のエッセンス:
// - then/else のいずれかに値が必須
// - Select 構造上、then_val/else_val は常に存在するのでOK
if debug {
eprintln!(
"[verify_select_minimal] OK: Select {{ dst: {:?}, cond: {:?}, then: {:?}, else: {:?} }}",
dst, cond, then_val, else_val
);
eprintln!(
"[verify_select_minimal] Invariants verified: single PHI (from conservative.rs), \
completeness (from phi_invariants.rs)"
);
}
Ok(())
}
// ============================================================================
// Unit Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
use crate::mir::join_ir::{ConstValue, JoinFunction, JoinModule};
fn build_valid_skip_ws_loop() -> JoinModule {
let mut module = JoinModule::new();
// loop_step(s, n, i) with proper progress update
let loop_func_id = JoinFuncId::new(1);
let s_param = ValueId(4000);
let n_param = ValueId(4001);
let i_param = ValueId(4002);
let const_1 = ValueId(4010);
let next_i = ValueId(4011);
let mut loop_func = JoinFunction::new(
loop_func_id,
"loop_step".to_string(),
vec![s_param, n_param, i_param],
);
// const 1
loop_func.body.push(JoinInst::Compute(MirLikeInst::Const {
dst: const_1,
value: ConstValue::Integer(1),
}));
// next_i = i + 1 (progress update)
loop_func.body.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: next_i,
op: BinOpKind::Add,
lhs: i_param, // progress carrier as LHS
rhs: const_1,
}));
// recursive call: loop_step(s, n, next_i)
loop_func.body.push(JoinInst::Call {
func: loop_func_id,
args: vec![s_param, n_param, next_i],
k_next: None,
dst: None,
});
module.add_function(loop_func);
module
}
fn build_invalid_skip_ws_loop() -> JoinModule {
let mut module = JoinModule::new();
// loop_step(s, n, i) WITHOUT progress update
let loop_func_id = JoinFuncId::new(1);
let s_param = ValueId(4000);
let n_param = ValueId(4001);
let i_param = ValueId(4002);
let mut loop_func = JoinFunction::new(
loop_func_id,
"loop_step".to_string(),
vec![s_param, n_param, i_param],
);
// recursive call with SAME i (no progress!)
loop_func.body.push(JoinInst::Call {
func: loop_func_id,
args: vec![s_param, n_param, i_param], // i is not updated!
k_next: None,
dst: None,
});
module.add_function(loop_func);
module
}
#[test]
fn test_valid_progress_passes() {
let module = build_valid_skip_ws_loop();
let result = verify_progress_for_skip_ws(&module);
assert!(result.is_ok(), "Valid progress should pass: {:?}", result);
}
#[test]
fn test_zero_progress_detected() {
let module = build_invalid_skip_ws_loop();
let result = verify_progress_for_skip_ws(&module);
assert!(result.is_err(), "Zero progress should be detected");
match result {
Err(ProgressError::ZeroProgressBackedge { .. }) => {
// Expected
}
other => panic!("Expected ZeroProgressBackedge, got {:?}", other),
}
}
#[test]
fn test_empty_module_ok() {
let module = JoinModule::new();
let result = verify_progress_for_skip_ws(&module);
assert!(
result.is_ok(),
"Empty module should pass (no loop_step to verify)"
);
}
}
// ============================================================================
// Phase 200-3: JoinIR Contract Verification (Loop Header PHI / Exit Line)
// ============================================================================
// Note: Verification functions are moved to merge/mod.rs to avoid circular dependencies
// with private control_flow module. This file only contains progress verification.