feat(joinir): Phase 92 P0-2 - Skeleton to LoopPatternContext (Option A)

SSOT principle: Pass canonicalizer-derived ConditionalStep info to Pattern2 lowerer
without re-detecting from AST.

Changes:
- router.rs: Add skeleton: Option<&'a LoopSkeleton> to LoopPatternContext
- parity_checker.rs: Return (Result, Option<LoopSkeleton>) to reuse skeleton
- routing.rs: Pass skeleton to context when joinir_dev_enabled()
- pattern2_with_break.rs: Detect ConditionalStep in can_lower()

Next: P0-3 implements actual JoinIR generation for ConditionalStep.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-16 15:22:29 +09:00
parent 6169ae03d6
commit 51017a0f9a
5 changed files with 266 additions and 9 deletions

View File

@ -13,13 +13,16 @@ impl MirBuilder {
/// pattern_kind. On mismatch:
/// - Debug mode (HAKO_JOINIR_DEBUG=1): Log warning
/// - Strict mode (HAKO_JOINIR_STRICT=1 or NYASH_JOINIR_STRICT=1): Return error
///
/// Phase 92 P0-2: Now returns (Result<(), String>, Option<LoopSkeleton>)
/// The skeleton can be used by Pattern2 lowerer for ConditionalStep handling.
pub(super) fn verify_router_parity(
&self,
condition: &ASTNode,
body: &[ASTNode],
func_name: &str,
ctx: &super::patterns::LoopPatternContext,
) -> Result<(), String> {
) -> (Result<(), String>, Option<crate::mir::loop_canonicalizer::LoopSkeleton>) {
use crate::mir::loop_canonicalizer::canonicalize_loop_expr;
// Reconstruct loop AST for canonicalizer
@ -30,11 +33,16 @@ impl MirBuilder {
};
// Run canonicalizer
let (_, decision) = canonicalize_loop_expr(&loop_ast)
.map_err(|e| format!("[loop_canonicalizer/PARITY] Canonicalizer error: {}", e))?;
let (skeleton, decision) = match canonicalize_loop_expr(&loop_ast) {
Ok((skel, dec)) => (Some(skel), dec),
Err(e) => {
let err_msg = format!("[loop_canonicalizer/PARITY] Canonicalizer error: {}", e);
return (Err(err_msg), None);
}
};
// Compare patterns only if canonicalizer succeeded
if let Some(canonical_pattern) = decision.chosen {
let result = if let Some(canonical_pattern) = decision.chosen {
let actual_pattern = ctx.pattern_kind;
if canonical_pattern != actual_pattern {
@ -51,10 +59,11 @@ impl MirBuilder {
if is_strict {
// Strict mode: fail fast
return Err(msg);
Err(msg)
} else {
// Debug mode: log only
eprintln!("{}", msg);
Ok(())
}
} else {
// Patterns match - success!
@ -63,6 +72,7 @@ impl MirBuilder {
canonical and actual agree on {:?}",
func_name, canonical_pattern
);
Ok(())
}
} else {
// Canonicalizer failed (Fail-Fast)
@ -72,9 +82,11 @@ impl MirBuilder {
func_name,
decision.notes.join("; ")
);
}
Ok(())
};
Ok(())
// Phase 92 P0-2: Return both parity result and skeleton
(result, skeleton)
}
}

View File

@ -647,10 +647,12 @@ fn collect_body_local_variables(
/// Phase 192: Updated to structure-based detection
/// Phase 178: Added string carrier rejection (unsupported by Pattern 2)
/// Phase 187-2: No legacy fallback - rejection means error
/// Phase 92 P0-2: Added ConditionalStep detection from skeleton (Option A)
///
/// Pattern 2 matches:
/// - Pattern kind is Pattern2Break (has break, no continue)
/// - No string/complex carrier updates (JoinIR doesn't support string concat)
/// - ConditionalStep updates are supported (if skeleton provides them)
pub(crate) fn can_lower(builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool {
use super::common_init::CommonPatternInitializer;
use crate::mir::loop_pattern_detection::LoopPatternKind;
@ -671,6 +673,32 @@ pub(crate) fn can_lower(builder: &MirBuilder, ctx: &super::router::LoopPatternCo
);
}
// Phase 92 P0-2: Check skeleton for ConditionalStep support
if let Some(skeleton) = ctx.skeleton {
use crate::mir::loop_canonicalizer::UpdateKind;
// Count ConditionalStep carriers
let conditional_step_count = skeleton.carriers.iter()
.filter(|c| matches!(c.update_kind, UpdateKind::ConditionalStep { .. }))
.count();
if conditional_step_count > 0 {
if ctx.debug {
trace::trace().debug(
"pattern2/can_lower",
&format!(
"Phase 92 P0-2: Found {} ConditionalStep carriers in skeleton",
conditional_step_count
),
);
}
// Phase 92 P0-2: ConditionalStep support enabled
// Pattern2 can handle these via if-else JoinIR generation
// TODO: Implement actual lowering in cf_loop_pattern2_with_break_impl
}
}
// Phase 188/Refactor: Use common carrier update validation
// Extracts loop variable for dummy carrier creation (not used but required by API)
let loop_var_name = match builder.extract_loop_variable_from_condition(ctx.condition) {

View File

@ -29,6 +29,9 @@ use crate::mir::loop_pattern_detection::{LoopFeatures, LoopPatternKind};
/// (declared in mod.rs as pub module, import from parent)
use super::ast_feature_extractor as ast_features;
/// Phase 92 P0-2: Import LoopSkeleton for Option A
use crate::mir::loop_canonicalizer::LoopSkeleton;
/// Context passed to pattern detect/lower functions
pub(crate) struct LoopPatternContext<'a> {
/// Loop condition AST node
@ -61,6 +64,13 @@ pub(crate) struct LoopPatternContext<'a> {
/// Phase 200-C: Optional function body AST for capture analysis
/// None if not available, Some(&[ASTNode]) if function body is accessible
pub fn_body: Option<&'a [ASTNode]>,
/// Phase 92 P0-2: Optional LoopSkeleton from canonicalizer
/// This provides ConditionalStep information for Pattern2 lowering.
/// None if canonicalizer hasn't run yet (backward compatibility).
/// SSOT Principle: Avoid re-detecting ConditionalStep in lowering phase.
#[allow(dead_code)]
pub skeleton: Option<&'a LoopSkeleton>,
}
impl<'a> LoopPatternContext<'a> {
@ -71,6 +81,7 @@ impl<'a> LoopPatternContext<'a> {
/// Phase 193: Feature extraction delegated to ast_feature_extractor module
/// Phase 131-11: Detects infinite loop condition
/// Phase 137-6-S1: Use choose_pattern_kind() SSOT entry point
/// Phase 92 P0-2: Added skeleton parameter (None for backward compatibility)
pub(crate) fn new(
condition: &'a ASTNode,
body: &'a [ASTNode],
@ -99,6 +110,7 @@ impl<'a> LoopPatternContext<'a> {
features,
pattern_kind,
fn_body: None, // Phase 200-C: Default to None
skeleton: None, // Phase 92 P0-2: Default to None
}
}
@ -114,6 +126,13 @@ impl<'a> LoopPatternContext<'a> {
ctx.fn_body = Some(fn_body);
ctx
}
/// Phase 92 P0-2: Set skeleton (for canonicalizer integration)
#[allow(dead_code)]
pub(crate) fn with_skeleton(mut self, skeleton: &'a LoopSkeleton) -> Self {
self.skeleton = Some(skeleton);
self
}
}
/// Phase 193: Feature extraction moved to ast_feature_extractor module

View File

@ -292,7 +292,7 @@ impl MirBuilder {
}
),
);
let ctx = if let Some(ref fn_body) = fn_body_clone {
let mut ctx = if let Some(ref fn_body) = fn_body_clone {
trace::trace().routing(
"router",
func_name,
@ -304,8 +304,19 @@ impl MirBuilder {
};
// Phase 137-4: Router parity verification (after ctx is created)
// Phase 92 P0-2: Get skeleton from canonicalizer for Option A
let skeleton_holder: Option<crate::mir::loop_canonicalizer::LoopSkeleton>;
if crate::config::env::joinir_dev_enabled() {
self.verify_router_parity(condition, body, func_name, &ctx)?;
let (result, skeleton_opt) = self.verify_router_parity(condition, body, func_name, &ctx);
result?;
skeleton_holder = skeleton_opt;
if skeleton_holder.is_some() {
// Phase 92 P0-2: Set skeleton reference in context (must use holder lifetime)
// Note: This is safe because skeleton_holder lives for the entire scope
ctx.skeleton = skeleton_holder.as_ref();
}
} else {
skeleton_holder = None;
}
if let Some(result) = route_loop_pattern(self, &ctx)? {