Files
hakorune/src/mir/control_tree/normalized_shadow/parity.rs

381 lines
13 KiB
Rust

//! Phase 121: Parity verification between shadow and existing router
//!
//! ## Responsibility
//!
//! - Compare exit contracts and writes between shadow and existing paths
//! - Log mismatches in dev mode
//! - Fail-fast in strict mode with `freeze_with_hint`
//!
//! ## Comparison Strategy (Minimal & Robust)
//!
//! - Compare structural contracts (exits, writes)
//! - Do NOT compare actual values (too fragile)
//! - Focus on "did we extract the same information?"
use crate::mir::control_tree::step_tree_contract_box::StepTreeContract;
/// Mismatch classification
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MismatchKind {
/// Exit contract mismatch
ExitMismatch,
/// Writes contract mismatch
WritesMismatch,
/// Unsupported kind (should not happen for if-only)
UnsupportedKind,
}
impl MismatchKind {
/// Get human-readable description
pub fn description(&self) -> &'static str {
match self {
MismatchKind::ExitMismatch => "exit contract mismatch",
MismatchKind::WritesMismatch => "writes contract mismatch",
MismatchKind::UnsupportedKind => "unsupported pattern for parity check",
}
}
}
/// Result of parity check
#[derive(Debug, Clone)]
pub struct ShadowParityResult {
/// Whether parity check passed
pub ok: bool,
/// Mismatch kind if not ok
pub mismatch_kind: Option<MismatchKind>,
/// Hint for debugging (must be non-empty if not ok)
pub hint: Option<String>,
}
impl ShadowParityResult {
/// Create successful parity result
pub fn ok() -> Self {
Self {
ok: true,
mismatch_kind: None,
hint: None,
}
}
/// Create failed parity result with hint
pub fn mismatch(kind: MismatchKind, hint: String) -> Self {
assert!(!hint.is_empty(), "hint must not be empty for mismatch");
Self {
ok: false,
mismatch_kind: Some(kind),
hint: Some(hint),
}
}
}
/// Compare exit contracts between shadow and existing path
///
/// ## Contract
///
/// - Input: Two `StepTreeContract` instances (shadow vs existing)
/// - Compares `exits` field (BTreeSet for deterministic ordering)
/// - Returns mismatch with specific hint if different
pub fn compare_exit_contracts(
shadow: &StepTreeContract,
existing: &StepTreeContract,
) -> ShadowParityResult {
if shadow.exits != existing.exits {
let hint = format!(
"exit mismatch: shadow={:?}, existing={:?}",
shadow.exits, existing.exits
);
return ShadowParityResult::mismatch(MismatchKind::ExitMismatch, hint);
}
ShadowParityResult::ok()
}
/// Compare writes contracts between shadow and existing path
///
/// ## Contract
///
/// - Input: Two `StepTreeContract` instances (shadow vs existing)
/// - Compares `writes` field (BTreeSet for deterministic ordering)
/// - Returns mismatch with specific hint if different
pub fn compare_writes_contracts(
shadow: &StepTreeContract,
existing: &StepTreeContract,
) -> ShadowParityResult {
if shadow.writes != existing.writes {
let hint = format!(
"writes mismatch: shadow={:?}, existing={:?}",
shadow.writes, existing.writes
);
return ShadowParityResult::mismatch(MismatchKind::WritesMismatch, hint);
}
ShadowParityResult::ok()
}
/// Full parity check (exits + writes)
///
/// ## Contract
///
/// - Combines exit and writes checks
/// - Returns first mismatch found
/// - Returns ok only if all checks pass
pub fn check_full_parity(
shadow: &StepTreeContract,
existing: &StepTreeContract,
) -> ShadowParityResult {
// Check exits first
let exit_result = compare_exit_contracts(shadow, existing);
if !exit_result.ok {
return exit_result;
}
// Check writes second
let writes_result = compare_writes_contracts(shadow, existing);
if !writes_result.ok {
return writes_result;
}
ShadowParityResult::ok()
}
/// Phase 122: Verify Normalized JoinModule structure
///
/// ## Contract
///
/// - Checks function count, continuation count, tail-call form, env args
/// - Does NOT check actual execution (RC comparison is optional)
/// - Returns Err(freeze_with_hint) if structure is invalid
pub fn verify_normalized_structure(
module: &crate::mir::join_ir::JoinModule,
expected_env_fields: usize,
) -> Result<(), String> {
use crate::mir::join_ir::{JoinFuncId, JoinInst};
use crate::mir::join_ir::lowering::error_tags;
// Check phase
if !module.is_normalized() {
return Err(error_tags::freeze_with_hint(
"phase129/join_k/not_normalized",
&format!("module phase is not Normalized: {:?}", module.phase),
"ensure the shadow lowering marks module as Normalized",
));
}
if module.functions.is_empty() {
return Err(error_tags::freeze_with_hint(
"phase129/join_k/no_functions",
"no functions in module",
"ensure the shadow lowering emits at least the entry function",
));
}
// Check entry point
let entry_id = module.entry.ok_or_else(|| {
error_tags::freeze_with_hint(
"phase129/join_k/no_entry",
"no entry point in module",
"ensure the shadow lowering sets JoinModule.entry",
)
})?;
// Check entry function exists
let entry_func = module.functions.get(&entry_id).ok_or_else(|| {
error_tags::freeze_with_hint(
"phase129/join_k/entry_missing",
&format!("entry function {:?} not found", entry_id),
"ensure the emitted module includes the entry function id",
)
})?;
// Env layout: writes + inputs (SSOT)
if entry_func.params.len() != expected_env_fields {
return Err(error_tags::freeze_with_hint(
"phase129/join_k/env_arity_mismatch",
&format!(
"env args mismatch: expected {}, got {}",
expected_env_fields,
entry_func.params.len()
),
"ensure env params are built from (writes + inputs) SSOT",
));
}
// All functions in this shadow module must share the same env param arity.
for (fid, func) in &module.functions {
if func.params.len() != expected_env_fields {
return Err(error_tags::freeze_with_hint(
"phase129/join_k/env_arity_mismatch",
&format!(
"env args mismatch in {:?}: expected {}, got {}",
fid,
expected_env_fields,
func.params.len()
),
"ensure all continuations share the same env layout (writes + inputs)",
));
}
}
// PHI prohibition (Phase 129-B scope): no IfMerge/NestedIfMerge in shadow output.
for (fid, func) in &module.functions {
for inst in &func.body {
if matches!(inst, JoinInst::IfMerge { .. } | JoinInst::NestedIfMerge { .. }) {
return Err(error_tags::freeze_with_hint(
"phase129/join_k/phi_forbidden",
&format!("PHI-like merge instruction found in {:?}", fid),
"Phase 129-B join_k path forbids IfMerge/NestedIfMerge; use join_k tailcall merge instead",
));
}
}
}
// Detect join_k tailcall form (if present) and validate it.
fn tailcall_target(func: &crate::mir::join_ir::JoinFunction) -> Option<(JoinFuncId, usize)> {
match func.body.last()? {
JoinInst::Call {
func,
args,
k_next: None,
dst: None,
} => Some((*func, args.len())),
_ => None,
}
}
let mut tailcall_targets: Vec<(JoinFuncId, usize)> = Vec::new();
for func in module.functions.values() {
if let Some((target, argc)) = tailcall_target(func) {
tailcall_targets.push((target, argc));
}
}
if tailcall_targets.is_empty() {
return Ok(());
}
// join_k merge should have at least two branch continuations tailcalling the same target.
if tailcall_targets.len() < 2 {
return Err(error_tags::freeze_with_hint(
"phase129/join_k/tailcall_count",
&format!(
"join_k tailcall form requires >=2 tailcalls, got {}",
tailcall_targets.len()
),
"ensure both then/else branches tailcall join_k as the last instruction",
));
}
let first_target = tailcall_targets[0].0;
for (target, argc) in &tailcall_targets {
if *target != first_target {
return Err(error_tags::freeze_with_hint(
"phase129/join_k/tailcall_target_mismatch",
"tailcalls do not target a single join_k function",
"ensure then/else both tailcall the same join_k function id",
));
}
if *argc != expected_env_fields {
return Err(error_tags::freeze_with_hint(
"phase129/join_k/tailcall_arg_arity_mismatch",
&format!(
"tailcall env arg count mismatch: expected {}, got {}",
expected_env_fields, argc
),
"ensure join_k is called with the full env fields list (writes + inputs)",
));
}
}
let join_k_func = module.functions.get(&first_target).ok_or_else(|| {
error_tags::freeze_with_hint(
"phase129/join_k/join_k_missing",
"tailcall target join_k function not found in module",
"ensure join_k is registered in JoinModule.functions",
)
})?;
match join_k_func.body.last() {
Some(JoinInst::Ret { value: Some(_) }) => Ok(()),
_ => Err(error_tags::freeze_with_hint(
"phase129/join_k/join_k_not_ret",
"join_k must end with Ret(Some(value))",
"ensure join_k returns the merged env variable and has no post-if continuation in Phase 129-B",
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mir::control_tree::step_tree::ExitKind;
fn make_contract(exits: Vec<ExitKind>, writes: Vec<&str>) -> StepTreeContract {
StepTreeContract {
exits: exits.into_iter().collect(),
writes: writes.into_iter().map(String::from).collect(),
reads: Default::default(), // Phase 124
required_caps: Default::default(),
cond_sig: Default::default(),
}
}
#[test]
fn test_exit_parity_match() {
let c1 = make_contract(vec![ExitKind::Return], vec!["x"]);
let c2 = make_contract(vec![ExitKind::Return], vec!["x"]);
let result = compare_exit_contracts(&c1, &c2);
assert!(result.ok);
}
#[test]
fn test_exit_parity_mismatch() {
let c1 = make_contract(vec![ExitKind::Return], vec!["x"]);
let c2 = make_contract(vec![ExitKind::Break], vec!["x"]);
let result = compare_exit_contracts(&c1, &c2);
assert!(!result.ok);
assert_eq!(result.mismatch_kind, Some(MismatchKind::ExitMismatch));
assert!(result.hint.is_some());
}
#[test]
fn test_writes_parity_match() {
let c1 = make_contract(vec![ExitKind::Return], vec!["x", "y"]);
let c2 = make_contract(vec![ExitKind::Return], vec!["x", "y"]);
let result = compare_writes_contracts(&c1, &c2);
assert!(result.ok);
}
#[test]
fn test_writes_parity_mismatch() {
let c1 = make_contract(vec![ExitKind::Return], vec!["x"]);
let c2 = make_contract(vec![ExitKind::Return], vec!["x", "y"]);
let result = compare_writes_contracts(&c1, &c2);
assert!(!result.ok);
assert_eq!(result.mismatch_kind, Some(MismatchKind::WritesMismatch));
assert!(result.hint.is_some());
}
#[test]
fn test_full_parity_ok() {
let c1 = make_contract(vec![ExitKind::Return], vec!["x"]);
let c2 = make_contract(vec![ExitKind::Return], vec!["x"]);
let result = check_full_parity(&c1, &c2);
assert!(result.ok);
}
#[test]
fn test_full_parity_exit_mismatch() {
let c1 = make_contract(vec![ExitKind::Return], vec!["x"]);
let c2 = make_contract(vec![ExitKind::Break], vec!["x"]);
let result = check_full_parity(&c1, &c2);
assert!(!result.ok);
assert_eq!(result.mismatch_kind, Some(MismatchKind::ExitMismatch));
}
#[test]
fn test_full_parity_writes_mismatch() {
let c1 = make_contract(vec![ExitKind::Return], vec!["x"]);
let c2 = make_contract(vec![ExitKind::Return], vec!["x", "y"]);
let result = check_full_parity(&c1, &c2);
assert!(!result.ok);
assert_eq!(result.mismatch_kind, Some(MismatchKind::WritesMismatch));
}
}