feat(control_tree): add StepTree→Normalized shadow lowerer (if-only, dev-only)

This commit is contained in:
nyash-codex
2025-12-18 04:31:41 +09:00
parent 8d930d2dcc
commit 1e5432f61a
5 changed files with 508 additions and 0 deletions

View File

@ -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,

View 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"));
}
}

View 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)"
);
}
}

View 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};

View 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));
}
}