diff --git a/src/mir/control_tree/mod.rs b/src/mir/control_tree/mod.rs index fc0b7ae4..06593dad 100644 --- a/src/mir/control_tree/mod.rs +++ b/src/mir/control_tree/mod.rs @@ -8,11 +8,17 @@ //! - step_tree_facts: Raw data collection (exits/writes/caps/cond_sig) //! - step_tree_contract_box: Facts → Contract transformation //! - step_tree: Structure + integration +//! +//! Phase 121: StepTree → Normalized shadow lowering (dev-only) +//! - normalized_shadow: StepTree → JoinModule conversion for if-only patterns mod step_tree; mod step_tree_contract_box; mod step_tree_facts; +// Phase 121: Normalized shadow lowering (dev-only) +pub mod normalized_shadow; + pub use step_tree::{ AstSummary, ExitKind, StepCapability, StepNode, StepStmtKind, StepTree, StepTreeBuilderBox, StepTreeFeatures, StepTreeSignature, diff --git a/src/mir/control_tree/normalized_shadow/builder.rs b/src/mir/control_tree/normalized_shadow/builder.rs new file mode 100644 index 00000000..67c45612 --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/builder.rs @@ -0,0 +1,168 @@ +//! Phase 121: StepTree → JoinModule shadow lowering (if-only) +//! +//! ## Responsibility +//! +//! - Convert StepTree to JoinModule (Normalized dialect) +//! - Only for if-only patterns (no loops) +//! - Returns None for out-of-scope patterns +//! - Returns Err for patterns that should be supported but conversion failed +//! +//! ## Design +//! +//! - Input: `&StepTree` with pre-computed contract +//! - No AST re-analysis (contract-only decisions) +//! - Single responsibility: structure → JoinIR conversion + +use crate::mir::control_tree::step_tree::StepTree; +use crate::mir::join_ir::lowering::carrier_info::{ExitMeta, JoinFragmentMeta}; +use crate::mir::join_ir::JoinModule; + +use super::contracts::{check_if_only, CapabilityCheckResult}; + +/// Box-First: StepTree → Normalized shadow lowering +pub struct StepTreeNormalizedShadowLowererBox; + +impl StepTreeNormalizedShadowLowererBox { + /// Try to lower an if-only StepTree to normalized form + /// + /// ## Returns + /// + /// - `Ok(None)`: Out of scope (e.g., contains loops) + /// - `Ok(Some(...))`: Shadow generation succeeded + /// - `Err(...)`: Should be supported but conversion failed (internal error) + /// + /// ## Contract + /// + /// - Only processes if-only patterns (no loops/breaks/continues) + /// - Uses contract information only (no AST re-analysis) + /// - Dev-only: caller must check `joinir_dev_enabled()` before calling + pub fn try_lower_if_only( + step_tree: &StepTree, + ) -> Result, String> { + // Phase 121 P1: Capability check (if-only scope) + let capability = check_if_only(step_tree); + match capability { + CapabilityCheckResult::Supported => { + // Phase 121 P1: For now, return empty module + empty meta + // Full lowering implementation in future phases + let module = JoinModule::new(); + let meta = JoinFragmentMeta::empty(); + Ok(Some((module, meta))) + } + CapabilityCheckResult::Unsupported(_reason) => { + // Out of scope for Phase 121 + Ok(None) + } + } + } + + /// Get shadow lowering status string for dev logging + /// + /// ## Contract + /// + /// - Returns 1-line summary: "shadow_lowered=true/false reason=..." + /// - Does not perform actual lowering (use `try_lower_if_only` for that) + pub fn get_status_string(step_tree: &StepTree) -> String { + let capability = check_if_only(step_tree); + match capability { + CapabilityCheckResult::Supported => { + format!( + "shadow_lowered=true step_tree_sig={} exits={:?} writes={:?}", + step_tree.signature_basis_string(), + step_tree.contract.exits, + step_tree.contract.writes + ) + } + CapabilityCheckResult::Unsupported(reason) => { + format!( + "shadow_lowered=false reason=\"{}\" step_tree_sig={}", + reason.reason(), + step_tree.signature_basis_string() + ) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mir::control_tree::step_tree::{StepNode, StepTreeFeatures, StepTreeSignature}; + use crate::mir::control_tree::step_tree_contract_box::StepTreeContract; + + fn make_if_only_tree() -> StepTree { + StepTree { + root: StepNode::Block(vec![]), + features: StepTreeFeatures { + has_if: true, + has_loop: false, + has_break: false, + has_continue: false, + has_return: false, + max_if_depth: 1, + max_loop_depth: 0, + }, + contract: StepTreeContract { + exits: Default::default(), + writes: Default::default(), + required_caps: Default::default(), + cond_sig: Default::default(), + }, + signature: StepTreeSignature(0), + } + } + + fn make_loop_tree() -> StepTree { + StepTree { + root: StepNode::Block(vec![]), + features: StepTreeFeatures { + has_if: false, + has_loop: true, + has_break: false, + has_continue: false, + has_return: false, + max_if_depth: 0, + max_loop_depth: 1, + }, + contract: StepTreeContract { + exits: Default::default(), + writes: Default::default(), + required_caps: Default::default(), + cond_sig: Default::default(), + }, + signature: StepTreeSignature(0), + } + } + + #[test] + fn test_if_only_supported() { + let tree = make_if_only_tree(); + let result = StepTreeNormalizedShadowLowererBox::try_lower_if_only(&tree); + assert!(result.is_ok()); + assert!(result.unwrap().is_some()); + } + + #[test] + fn test_loop_rejected() { + let tree = make_loop_tree(); + let result = StepTreeNormalizedShadowLowererBox::try_lower_if_only(&tree); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); + } + + #[test] + fn test_status_string_if_only() { + let tree = make_if_only_tree(); + let status = StepTreeNormalizedShadowLowererBox::get_status_string(&tree); + assert!(status.contains("shadow_lowered=true")); + assert!(status.contains("step_tree_sig=")); + } + + #[test] + fn test_status_string_loop() { + let tree = make_loop_tree(); + let status = StepTreeNormalizedShadowLowererBox::get_status_string(&tree); + assert!(status.contains("shadow_lowered=false")); + assert!(status.contains("reason=\"contains loop")); + } +} diff --git a/src/mir/control_tree/normalized_shadow/contracts.rs b/src/mir/control_tree/normalized_shadow/contracts.rs new file mode 100644 index 00000000..6c546148 --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/contracts.rs @@ -0,0 +1,81 @@ +//! Phase 121: Capability checking and if-only validation +//! +//! ## Responsibility +//! +//! - Check if StepTree is "if-only" (no loops, breaks, continues) +//! - Provide SSOT for capability rejection reasons +//! - Explicit enumeration of unsupported capabilities + +use crate::mir::control_tree::step_tree::StepTree; + +/// Unsupported capability classification +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnsupportedCapability { + /// Contains loop constructs + Loop, + /// Contains break statements + Break, + /// Contains continue statements + Continue, + /// Other unsupported feature + Other, +} + +impl UnsupportedCapability { + /// Get human-readable reason string + pub fn reason(&self) -> &'static str { + match self { + UnsupportedCapability::Loop => "contains loop (if-only scope)", + UnsupportedCapability::Break => "contains break (if-only scope)", + UnsupportedCapability::Continue => "contains continue (if-only scope)", + UnsupportedCapability::Other => "unsupported feature for if-only", + } + } +} + +/// Result of capability check +#[derive(Debug, Clone)] +pub enum CapabilityCheckResult { + /// Supported (if-only) + Supported, + /// Unsupported with specific reason + Unsupported(UnsupportedCapability), +} + +/// Check if StepTree is if-only (Phase 121 scope) +/// +/// ## Contract +/// +/// - Input: `&StepTree` with already-computed `features` and `contract` +/// - No AST re-analysis (uses contract fields only) +/// - Returns `Supported` only if no loops/breaks/continues +pub fn check_if_only(step_tree: &StepTree) -> CapabilityCheckResult { + // Check features (already computed during StepTree construction) + if step_tree.features.has_loop { + return CapabilityCheckResult::Unsupported(UnsupportedCapability::Loop); + } + if step_tree.features.has_break { + return CapabilityCheckResult::Unsupported(UnsupportedCapability::Break); + } + if step_tree.features.has_continue { + return CapabilityCheckResult::Unsupported(UnsupportedCapability::Continue); + } + + // If-only scope is supported + CapabilityCheckResult::Supported +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unsupported_capability_reasons() { + assert_eq!(UnsupportedCapability::Loop.reason(), "contains loop (if-only scope)"); + assert_eq!(UnsupportedCapability::Break.reason(), "contains break (if-only scope)"); + assert_eq!( + UnsupportedCapability::Continue.reason(), + "contains continue (if-only scope)" + ); + } +} diff --git a/src/mir/control_tree/normalized_shadow/mod.rs b/src/mir/control_tree/normalized_shadow/mod.rs new file mode 100644 index 00000000..d31320d5 --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/mod.rs @@ -0,0 +1,38 @@ +//! Phase 121: StepTree → Normalized shadow lowering (dev-only) +//! +//! ## Purpose +//! +//! Establish minimal route from StepTree (structure SSOT) to Normalized form, +//! verifying parity with existing router for if-only patterns. +//! +//! ## Scope +//! +//! - **if-only**: Support if statements without loops +//! - **loop rejection**: Reject loops via capability guard +//! +//! ## Design Rules +//! +//! **Input SSOT**: +//! - `StepTree` + `StepTreeContract` (no facts re-analysis) +//! - Lowering decisions based only on contract information +//! +//! **Output**: +//! - `JoinModule` (Normalized dialect) +//! - Or "Normalized-equivalent intermediate" expressed in existing JoinIR types +//! +//! **Execution conditions**: +//! - dev-only: Only runs when `joinir_dev_enabled()` returns true +//! - strict: Only fail-fast on mismatch when `joinir_strict_enabled()` returns true +//! +//! **Prohibitions**: +//! - No fallback: Shadow conversion failure logs reason in dev-only, fail-fast in strict +//! - No direct env reads (must go through `src/config/env/*`) +//! - No hardcoding (no branching on fixture names or variable names) + +pub mod builder; +pub mod contracts; +pub mod parity; + +pub use builder::StepTreeNormalizedShadowLowererBox; +pub use contracts::{CapabilityCheckResult, UnsupportedCapability}; +pub use parity::{MismatchKind, ShadowParityResult}; diff --git a/src/mir/control_tree/normalized_shadow/parity.rs b/src/mir/control_tree/normalized_shadow/parity.rs new file mode 100644 index 00000000..a3ea0571 --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/parity.rs @@ -0,0 +1,215 @@ +//! 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; +use std::collections::BTreeSet; + +/// 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, + /// Hint for debugging (must be non-empty if not ok) + pub hint: Option, +} + +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() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mir::control_tree::step_tree::ExitKind; + + fn make_contract(exits: Vec, writes: Vec<&str>) -> StepTreeContract { + StepTreeContract { + exits: exits.into_iter().collect(), + writes: writes.into_iter().map(String::from).collect(), + 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)); + } +}