Files
hakorune/src/mir/builder/control_flow/joinir/loop_context.rs
nyash-codex d2972c1437 feat(joinir): Phase 92完了 - ConditionalStep + body-local変数サポート
## Phase 92全体の成果

**Phase 92 P0-P2**: ConditionalStep JoinIR生成とbody-local変数サポート
- ConditionalStep(条件付きキャリア更新)のJoinIR生成実装
- Body-local変数(ch等)の条件式での参照サポート
- 変数解決優先度: ConditionEnv → LoopBodyLocalEnv

**Phase 92 P3**: BodyLocalPolicyBox + 安全ガード
- BodyLocalPolicyDecision実装(Accept/Reject判定)
- BodyLocalSlot + DualValueRewriter(JoinIR/MIR二重書き込み)
- Fail-Fast契約(Cannot promote LoopBodyLocal検出)

**Phase 92 P4**: E2E固定+回帰最小化 (本コミット)
- Unit test 3本追加(body-local変数解決検証)
- Integration smoke追加(phase92_pattern2_baseline.sh、2ケースPASS)
- P4-E2E-PLAN.md、P4-COMPLETION.md作成

## 主要な実装

### ConditionalStep(条件付きキャリア更新)
- `conditional_step_emitter.rs`: JoinIR Select命令生成
- `loop_with_break_minimal.rs`: ConditionalStep検出と統合
- `loop_with_continue_minimal.rs`: Pattern4対応

### Body-local変数サポート
- `condition_lowerer.rs`: body-local変数解決機能
  - `lower_condition_to_joinir`: body_local_env パラメータ追加
  - 変数解決優先度実装(ConditionEnv優先)
  - Unit test 3本追加: 変数解決/優先度/エラー
- `header_break_lowering.rs`: break条件でbody-local変数参照
- 7ファイルで後方互換ラッパー(lower_condition_to_joinir_no_body_locals)

### Body-local Policy & Safety
- `body_local_policy.rs`: BodyLocalPolicyDecision(Accept/Reject)
- `body_local_slot.rs`: JoinIR/MIR二重書き込み
- `dual_value_rewriter.rs`: ValueId書き換えヘルパー

## テスト体制

### Unit Tests (+3)
- `test_body_local_variable_resolution`: body-local変数解決
- `test_variable_resolution_priority`: 変数解決優先度(ConditionEnv優先)
- `test_undefined_variable_error`: 未定義変数エラー
- 全7テストPASS(cargo test --release condition_lowerer::tests)

### Integration Smoke (+1)
- `phase92_pattern2_baseline.sh`:
  - Case A: loop_min_while.hako (Pattern2 baseline)
  - Case B: phase92_conditional_step_minimal.hako (条件付きインクリメント)
  - 両ケースPASS、integration profileで発見可能

### 退行確認
-  既存Pattern2Breakテスト正常(退行なし)
-  Phase 135 smoke正常(MIR検証PASS)

## アーキテクチャ設計

### 変数解決メカニズム
```rust
// Priority 1: ConditionEnv (loop params, captured)
if let Some(value_id) = env.get(name) { return Ok(value_id); }
// Priority 2: LoopBodyLocalEnv (body-local like `ch`)
if let Some(body_env) = body_local_env {
    if let Some(value_id) = body_env.get(name) { return Ok(value_id); }
}
```

### Fail-Fast契約
- Delta equality check (conditional_step_emitter.rs)
- Variable resolution error messages (ConditionEnv)
- Body-local promotion rejection (BodyLocalPolicyDecision::Reject)

## ドキュメント

- `P4-E2E-PLAN.md`: 3レベルテスト戦略(Level 1-2完了、Level 3延期)
- `P4-COMPLETION.md`: Phase 92完了報告
- `README.md`: Phase 92全体のまとめ

## 将来の拡張(Phase 92スコープ外)

- Body-local promotionシステム拡張
- P5bパターン認識の汎化(flagベース条件サポート)
- 完全なP5b E2Eテスト(body-local promotion実装後)

🎯 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 21:37:07 +09:00

326 lines
10 KiB
Rust

//! Loop Processing Context - SSOT for AST + Skeleton + Pattern
//!
//! Phase 140-P5: Unified context for loop processing that integrates:
//! - AST information (condition, body, span)
//! - Canonicalizer output (LoopSkeleton, RoutingDecision)
//! - Router information (LoopPatternKind, LoopFeatures)
//!
//! This eliminates duplicate AST reconstruction and centralizes loop processing state.
use crate::ast::{ASTNode, Span};
use crate::mir::loop_canonicalizer::{LoopSkeleton, RoutingDecision};
use crate::mir::loop_pattern_detection::{LoopFeatures, LoopPatternKind};
/// Loop processing context - SSOT for AST + Skeleton + Pattern
///
/// This context integrates all information needed for loop processing:
/// - AST: The source structure (condition, body, span)
/// - Canonicalizer: Normalized skeleton and routing decision (optional)
/// - Router: Pattern classification and features
///
/// # Lifecycle
///
/// 1. Create with `new()` - AST + Router info only
/// 2. Call `set_canonicalizer_result()` - Add Canonicalizer output
/// 3. Call `verify_parity()` - Check consistency (dev-only)
#[derive(Debug)]
pub struct LoopProcessingContext<'a> {
// ========================================================================
// AST Information
// ========================================================================
/// Loop condition AST node
pub condition: &'a ASTNode,
/// Loop body statements
pub body: &'a [ASTNode],
/// Source location for debugging
pub span: Span,
// ========================================================================
// Canonicalizer Output (Optional - set after canonicalization)
// ========================================================================
/// Normalized loop skeleton (None = canonicalizer not run yet)
pub skeleton: Option<LoopSkeleton>,
/// Routing decision from canonicalizer (None = canonicalizer not run yet)
pub decision: Option<RoutingDecision>,
// ========================================================================
// Router Information (Always Present)
// ========================================================================
/// Pattern kind determined by router
pub pattern_kind: LoopPatternKind,
/// Loop features extracted from AST
pub features: LoopFeatures,
}
impl<'a> LoopProcessingContext<'a> {
/// Create new context (canonicalizer not run yet)
///
/// # Parameters
///
/// - `condition`: Loop condition AST node
/// - `body`: Loop body statements
/// - `span`: Source location
/// - `pattern_kind`: Pattern classification from router
/// - `features`: Loop features extracted from AST
pub fn new(
condition: &'a ASTNode,
body: &'a [ASTNode],
span: Span,
pattern_kind: LoopPatternKind,
features: LoopFeatures,
) -> Self {
Self {
condition,
body,
span,
skeleton: None,
decision: None,
pattern_kind,
features,
}
}
/// Set canonicalizer result (skeleton + decision)
///
/// This should be called after running the canonicalizer.
/// After this call, `verify_parity()` can be used to check consistency.
pub fn set_canonicalizer_result(&mut self, skeleton: LoopSkeleton, decision: RoutingDecision) {
self.skeleton = Some(skeleton);
self.decision = Some(decision);
}
/// Reconstruct loop AST for canonicalizer input
///
/// This is used for parity verification when we need to run the canonicalizer
/// on the same AST that the router processed.
pub fn to_loop_ast(&self) -> ASTNode {
ASTNode::Loop {
condition: Box::new(self.condition.clone()),
body: self.body.to_vec(),
span: self.span.clone(),
}
}
/// Verify parity between canonicalizer and router (dev-only)
///
/// Checks that the canonicalizer's pattern choice matches the router's
/// pattern_kind. On mismatch:
/// - Strict mode (HAKO_JOINIR_STRICT=1): Returns error
/// - Debug mode: Logs warning
///
/// Returns Ok(()) if:
/// - Canonicalizer not run yet (decision is None)
/// - Patterns match
/// - Non-strict mode (mismatch logged only)
pub fn verify_parity(&self) -> Result<(), String> {
use crate::config::env::joinir_dev;
// Canonicalizer not run yet - skip verification
let decision = match &self.decision {
Some(d) => d,
None => return Ok(()),
};
// Canonicalizer failed (Fail-Fast) - no pattern to compare
let canonical_pattern = match decision.chosen {
Some(p) => p,
None => return Ok(()), // Router might still handle it
};
// Compare patterns
let actual_pattern = self.pattern_kind;
if canonical_pattern != actual_pattern {
let msg = format!(
"[loop_canonicalizer/PARITY] MISMATCH: canonical={:?}, actual={:?}",
canonical_pattern, actual_pattern
);
if joinir_dev::strict_enabled() {
// Strict mode: fail fast
return Err(msg);
} else {
// Debug mode: log only
crate::mir::builder::control_flow::joinir::trace::trace()
.dev("loop_canonicalizer/parity", &msg);
}
} else {
// Patterns match - success!
crate::mir::builder::control_flow::joinir::trace::trace().dev(
"loop_canonicalizer/parity",
&format!(
"[loop_canonicalizer/PARITY] OK: canonical and actual agree on {:?}",
canonical_pattern
),
);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::LiteralValue;
/// Helper: Create a simple loop context for testing
fn make_simple_context<'a>(
condition: &'a ASTNode,
body: &'a [ASTNode],
) -> LoopProcessingContext<'a> {
LoopProcessingContext::new(
condition,
body,
Span::unknown(),
LoopPatternKind::Pattern1SimpleWhile,
LoopFeatures {
has_if: false,
has_break: false,
has_continue: false,
has_if_else_phi: false,
carrier_count: 0,
break_count: 0,
continue_count: 0,
is_infinite_loop: false,
update_summary: None,
},
)
}
#[test]
fn test_context_creation() {
let condition = ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
};
let body = vec![];
let ctx = make_simple_context(&condition, &body);
// Check AST fields
assert!(matches!(
ctx.condition,
ASTNode::Literal {
value: LiteralValue::Integer(1),
..
}
));
assert_eq!(ctx.body.len(), 0);
assert_eq!(ctx.span, Span::unknown());
// Check canonicalizer fields (not set yet)
assert!(ctx.skeleton.is_none());
assert!(ctx.decision.is_none());
// Check router fields
assert_eq!(ctx.pattern_kind, LoopPatternKind::Pattern1SimpleWhile);
}
#[test]
fn test_to_loop_ast_reconstruction() {
let condition = ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
};
let body = vec![];
let ctx = make_simple_context(&condition, &body);
let loop_ast = ctx.to_loop_ast();
// Check reconstructed AST
match loop_ast {
ASTNode::Loop {
condition,
body,
span,
} => {
assert!(matches!(
*condition,
ASTNode::Literal {
value: LiteralValue::Integer(1),
..
}
));
assert_eq!(body.len(), 0);
assert_eq!(span, Span::unknown());
}
_ => panic!("Expected Loop node"),
}
}
#[test]
fn test_verify_parity_without_canonicalizer() {
let condition = ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
};
let body = vec![];
let ctx = make_simple_context(&condition, &body);
// Should succeed (canonicalizer not run yet)
assert!(ctx.verify_parity().is_ok());
}
#[test]
fn test_verify_parity_with_matching_patterns() {
use crate::mir::loop_canonicalizer::{ExitContract, RoutingDecision};
let condition = ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
};
let body = vec![];
let mut ctx = make_simple_context(&condition, &body);
// Set canonicalizer result with matching pattern
let decision = RoutingDecision::success(LoopPatternKind::Pattern1SimpleWhile);
ctx.set_canonicalizer_result(
LoopSkeleton {
steps: vec![],
carriers: vec![],
exits: ExitContract::none(),
captured: None,
span: Span::unknown(),
},
decision,
);
// Should succeed (patterns match)
assert!(ctx.verify_parity().is_ok());
}
#[test]
fn test_verify_parity_with_fail_fast() {
use crate::mir::loop_canonicalizer::{CapabilityTag, ExitContract, RoutingDecision};
let condition = ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
};
let body = vec![];
let mut ctx = make_simple_context(&condition, &body);
// Set canonicalizer result with fail-fast decision
let decision = RoutingDecision::fail_fast(
vec![CapabilityTag::ConstStep],
"Test fail-fast".to_string(),
);
ctx.set_canonicalizer_result(
LoopSkeleton {
steps: vec![],
carriers: vec![],
exits: ExitContract::none(),
captured: None,
span: Span::unknown(),
},
decision,
);
// Should succeed (canonicalizer failed, no pattern to compare)
assert!(ctx.verify_parity().is_ok());
}
}