feat(control_tree): add StepTree→Normalized shadow lowerer (if-only, dev-only)
This commit is contained in:
@ -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,
|
||||
|
||||
168
src/mir/control_tree/normalized_shadow/builder.rs
Normal file
168
src/mir/control_tree/normalized_shadow/builder.rs
Normal file
@ -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<Option<(JoinModule, JoinFragmentMeta)>, 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"));
|
||||
}
|
||||
}
|
||||
81
src/mir/control_tree/normalized_shadow/contracts.rs
Normal file
81
src/mir/control_tree/normalized_shadow/contracts.rs
Normal file
@ -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)"
|
||||
);
|
||||
}
|
||||
}
|
||||
38
src/mir/control_tree/normalized_shadow/mod.rs
Normal file
38
src/mir/control_tree/normalized_shadow/mod.rs
Normal file
@ -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};
|
||||
215
src/mir/control_tree/normalized_shadow/parity.rs
Normal file
215
src/mir/control_tree/normalized_shadow/parity.rs
Normal file
@ -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<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()
|
||||
}
|
||||
|
||||
#[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(),
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user