From a824346e30dfb583b9eeffc58da6d28a85cd98fb Mon Sep 17 00:00:00 2001 From: tomoaki Date: Fri, 26 Dec 2025 02:03:22 +0900 Subject: [PATCH] =?UTF-8?q?feat(joinir):=20Phase=20286=20P2/P2.1/P2.2=20-?= =?UTF-8?q?=20JoinIR=20Line=20Absorption=20(Pattern1/4=20Plan=E5=8C=96=20P?= =?UTF-8?q?oC=20+=20hygiene)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 286 P2: Pattern4 (Loop with Continue) を Plan/Frag SSOT に移行 - DomainPlan::Pattern4Continue 追加 - PlanNormalizer::normalize_pattern4_continue() 実装(phi_bindings による PHI dst 優先参照) - Router integration(Plan line routing → legacy fallback) - Integration test PASS (output: 6), quick smoke 154/154 PASS Phase 286 P2.1: Pattern1 (SimpleWhile) を Plan/Frag SSOT に移行 - DomainPlan::Pattern1SimpleWhile 追加 - PlanNormalizer::normalize_pattern1_simple_while() 実装(4ブロック、1 PHI、phi_bindings 流用) - Router integration(Plan line routing → legacy fallback) - Integration test PASS (return: 3), quick smoke 154/154 PASS Phase 286 P2.2: hygiene(extractor重複排除 + router小整理) - extractor helper化: extract_loop_increment_plan を common_helpers.rs に統一 - Pattern1/Pattern4 が呼ぶだけに変更(重複排除 ~25行) - router helper化: lower_via_plan() を追加し Pattern6/7/4/1 で共用 - 3行パターン(normalize→verify→lower)を1関数に集約(ボイラープレート削減 ~40行) 成果物: - DomainPlan 2パターン新規追加(Pattern1SimpleWhile, Pattern4Continue) - Normalizer 2つの normalize 関数追加 - Router に Plan line ブロック追加 + lower_via_plan() helper - Extractor に extract_pattern1_plan() 追加 - Integration fixtures 2個 + smoke tests 2個 検証: - quick smoke: 154/154 PASS - integration: phase286_pattern1_frag_poc PASS, phase286_pattern4_frag_poc PASS - Plan line routing: route=plan strategy=extract で Pattern1/4 検出確認 🤖 Generated with Claude Code Co-Authored-By: Claude Haiku 4.5 --- apps/tests/phase286_pattern1_frag_poc.hako | 14 + apps/tests/phase286_pattern4_frag_poc.hako | 38 ++ .../patterns/extractors/common_helpers.rs | 39 +- .../joinir/patterns/extractors/pattern1.rs | 72 +++ .../joinir/patterns/extractors/pattern4.rs | 140 ++++ .../control_flow/joinir/patterns/router.rs | 86 ++- src/mir/builder/control_flow/plan/mod.rs | 38 +- .../builder/control_flow/plan/normalizer.rs | 610 +++++++++++++++++- src/mir/builder/utils.rs | 13 +- .../apps/phase286_pattern1_frag_poc.sh | 36 ++ .../apps/phase286_pattern4_frag_poc.sh | 44 ++ 11 files changed, 1108 insertions(+), 22 deletions(-) create mode 100644 apps/tests/phase286_pattern1_frag_poc.hako create mode 100644 apps/tests/phase286_pattern4_frag_poc.hako create mode 100644 tools/smokes/v2/profiles/integration/apps/phase286_pattern1_frag_poc.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase286_pattern4_frag_poc.sh diff --git a/apps/tests/phase286_pattern1_frag_poc.hako b/apps/tests/phase286_pattern1_frag_poc.hako new file mode 100644 index 00000000..904a86ea --- /dev/null +++ b/apps/tests/phase286_pattern1_frag_poc.hako @@ -0,0 +1,14 @@ +// Phase 286 P2.1: Pattern1 → Frag PoC minimal fixture +// Expected: return 3 (loop 3 times: i=0,1,2 → i=3 で終了) +// Design: 副作用なし・出力に依存しない(PoC向き) + +static box Main { + main() { + local i + i = 0 + loop(i < 3) { + i = i + 1 + } + return i + } +} diff --git a/apps/tests/phase286_pattern4_frag_poc.hako b/apps/tests/phase286_pattern4_frag_poc.hako new file mode 100644 index 00000000..fd8d6c3c --- /dev/null +++ b/apps/tests/phase286_pattern4_frag_poc.hako @@ -0,0 +1,38 @@ +// Phase 286 P2: Pattern4 → Frag PoC minimal fixture +// +// Purpose: Test Pattern4 (Loop with Continue) using Plan/Frag SSOT +// Expected output: 6 (sum of 1+2+3, skipping i==0) +// +// Structure: +// i = 0, sum = 0 // pre-loop init +// loop(i < 4) { // loop variable condition +// if (i == 0) { i = i + 1; continue } // continue at i==0 (SINGLE continue) +// sum = sum + i // carrier update (accumulator pattern) +// i = i + 1 // loop increment +// } +// return sum // return 1+2+3=6 +// +// PoC Goal: +// Pattern4 → DomainPlan → CorePlan → Frag → emit_frag() +// (Skip: JoinIR → bridge → merge) +// +// Design Note: +// - Uses only Compare (==, <) and Add (+) operators +// - Avoids Mod (%) which lacks Plan/Frag support guarantee +// - Single continue condition for Plan line PoC +// - Accumulator pattern for carrier (sum = sum + i) + +static box Main { + main() { + local i, sum + i = 0 + sum = 0 + loop(i < 4) { + if (i == 0) { i = i + 1; continue } + sum = sum + i + i = i + 1 + } + print(sum) + return 0 + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/extractors/common_helpers.rs b/src/mir/builder/control_flow/joinir/patterns/extractors/common_helpers.rs index 753d031b..dab65d88 100644 --- a/src/mir/builder/control_flow/joinir/patterns/extractors/common_helpers.rs +++ b/src/mir/builder/control_flow/joinir/patterns/extractors/common_helpers.rs @@ -194,7 +194,44 @@ pub(crate) fn is_true_literal(condition: &ASTNode) -> bool { } /// ============================================================ -/// Group 4: Pattern5-Specific Helpers (NOT generalized) +/// Group 4: Loop Increment Extraction (Common for Plan line) +/// ============================================================ + +/// Phase 286 P2.2: Extract loop increment for Plan line patterns +/// +/// Supports ` = + ` pattern only (PoC safety). +/// Used by Pattern1 and Pattern4 Plan line extractors. +/// +/// # Returns +/// - Ok(Some(ASTNode)) if valid increment found +/// - Ok(None) if no increment or unsupported pattern +/// - Err if malformed AST (rare) +pub(crate) fn extract_loop_increment_plan(body: &[ASTNode], loop_var: &str) -> Result, String> { + for stmt in body { + if let ASTNode::Assignment { target, value, .. } = stmt { + if let ASTNode::Variable { name, .. } = target.as_ref() { + if name == loop_var { + // Check for `i = i + ` + if let ASTNode::BinaryOp { operator, left, right, .. } = value.as_ref() { + if matches!(operator, BinaryOperator::Add) { + if let ASTNode::Variable { name: lname, .. } = left.as_ref() { + if lname == loop_var { + if matches!(right.as_ref(), ASTNode::Literal { .. }) { + return Ok(Some(value.as_ref().clone())); + } + } + } + } + } + } + } + } + } + Ok(None) +} + +/// ============================================================ +/// Group 5: Pattern5-Specific Helpers (NOT generalized) /// ============================================================ /// /// **IMPORTANT**: These helpers are Pattern5-specific and intentionally NOT generalized. diff --git a/src/mir/builder/control_flow/joinir/patterns/extractors/pattern1.rs b/src/mir/builder/control_flow/joinir/patterns/extractors/pattern1.rs index 60ed73d7..80ef9361 100644 --- a/src/mir/builder/control_flow/joinir/patterns/extractors/pattern1.rs +++ b/src/mir/builder/control_flow/joinir/patterns/extractors/pattern1.rs @@ -145,6 +145,78 @@ fn has_control_flow_statement(body: &[ASTNode]) -> bool { common_has_control_flow(body) } +// ============================================================================ +// Phase 286 P2.1: Pattern1 → Plan/Frag SSOT extractor +// ============================================================================ + +/// Phase 286 P2.1: Minimal subset extractor for Pattern1 Plan line +/// +/// Supported subset (PoC safety - Compare + Add only): +/// - Loop condition: ` < ` +/// - Loop increment: ` = + ` +/// +/// Returns Ok(None) for unsupported patterns → legacy fallback +pub(crate) fn extract_pattern1_plan( + condition: &ASTNode, + body: &[ASTNode], +) -> Result, String> { + use crate::mir::builder::control_flow::plan::{DomainPlan, Pattern1SimpleWhilePlan}; + + // Step 1: Validate via existing extractor + let parts = extract_simple_while_parts(condition, body)?; + if parts.is_none() { + return Ok(None); // Not Pattern1 → legacy fallback + } + let parts = parts.unwrap(); + + // Step 2: Validate loop condition is ` < ` + if !validate_loop_condition_plan(condition, &parts.loop_var) { + return Ok(None); // Unsupported condition format + } + + // Step 3: Extract loop increment ` = + ` + // Phase 286 P2.2: Use common helper from common_helpers + let loop_increment = match super::common_helpers::extract_loop_increment_plan(body, &parts.loop_var)? { + Some(inc) => inc, + None => return Ok(None), // No loop increment found + }; + + Ok(Some(DomainPlan::Pattern1SimpleWhile(Pattern1SimpleWhilePlan { + loop_var: parts.loop_var, + condition: condition.clone(), + loop_increment, + }))) +} + +/// Validate loop condition: supports ` < ` only +fn validate_loop_condition_plan(cond: &ASTNode, loop_var: &str) -> bool { + use crate::ast::LiteralValue; + + if let ASTNode::BinaryOp { operator, left, right, .. } = cond { + if !matches!(operator, BinaryOperator::Less) { + return false; // Only < supported for PoC + } + + // Left must be the loop variable + if let ASTNode::Variable { name, .. } = left.as_ref() { + if name != loop_var { + return false; + } + } else { + return false; + } + + // Right must be integer literal + if !matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(_), .. }) { + return false; + } + + true + } else { + false + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/mir/builder/control_flow/joinir/patterns/extractors/pattern4.rs b/src/mir/builder/control_flow/joinir/patterns/extractors/pattern4.rs index f3fd90f0..d64cb785 100644 --- a/src/mir/builder/control_flow/joinir/patterns/extractors/pattern4.rs +++ b/src/mir/builder/control_flow/joinir/patterns/extractors/pattern4.rs @@ -99,6 +99,146 @@ fn has_break_statement_recursive(body: &[ASTNode]) -> bool { // Phase 284 P1: has_return_statement_recursive removed // Return detection now handled by return_collector SSOT in conversion_pipeline.rs +// ============================================================================ +// Phase 286 P2: Pattern4 → Plan/Frag SSOT extractor +// ============================================================================ + +/// Phase 286 P2: Minimal subset extractor for Pattern4 Plan line +/// +/// Supported subset (PoC safety - Compare + Add only): +/// - Loop condition: ` < ` +/// - Continue condition: ` == ` (NO Mod operator) +/// - Carrier update: ` = + ` (accumulator pattern) +/// - Loop increment: ` = + ` +/// +/// Returns Ok(None) for unsupported patterns → legacy fallback +pub(crate) fn extract_pattern4_plan( + condition: &ASTNode, + body: &[ASTNode], +) -> Result, String> { + use crate::mir::builder::control_flow::plan::{DomainPlan, Pattern4ContinuePlan}; + + // Step 1: Validate via existing extractor + let parts = extract_loop_with_continue_parts(condition, body)?; + if parts.is_none() { + return Ok(None); // Not Pattern4 → legacy fallback + } + + // Step 2: Extract loop variable from condition `i < N` + let loop_var = match extract_loop_condition_plan(condition)? { + Some((var, _bound)) => var, + None => return Ok(None), // Unsupported condition + }; + + // Step 3: Find continue condition from body + let continue_cond = match find_continue_condition_plan(body)? { + Some(cond) => cond, + None => return Ok(None), // No continue condition found + }; + + // Step 4: Extract carrier updates (accumulator pattern) + let carrier_updates = extract_carrier_updates_plan(body, &loop_var)?; + if carrier_updates.is_empty() { + return Ok(None); // No carrier → unsupported + } + + // Step 5: Extract loop increment `i = i + 1` + // Phase 286 P2.2: Use common helper from common_helpers + let loop_increment = match super::common_helpers::extract_loop_increment_plan(body, &loop_var)? { + Some(inc) => inc, + None => return Ok(None), // No loop increment found + }; + + Ok(Some(DomainPlan::Pattern4Continue(Pattern4ContinuePlan { + loop_var: loop_var.clone(), + carrier_vars: carrier_updates.keys().cloned().collect(), + condition: condition.clone(), + continue_condition: continue_cond, + carrier_updates, + loop_increment, + }))) +} + +/// Extract loop condition: supports ` < ` only +fn extract_loop_condition_plan(cond: &ASTNode) -> Result, String> { + use crate::ast::{BinaryOperator, LiteralValue}; + + if let ASTNode::BinaryOp { operator, left, right, .. } = cond { + if !matches!(operator, BinaryOperator::Less) { + return Ok(None); // Only < supported + } + + let var_name = if let ASTNode::Variable { name, .. } = left.as_ref() { + name.clone() + } else { + return Ok(None); + }; + + let bound = if let ASTNode::Literal { value: LiteralValue::Integer(n), .. } = right.as_ref() { + *n + } else { + return Ok(None); + }; + + Ok(Some((var_name, bound))) + } else { + Ok(None) + } +} + +/// Find continue condition in if statement +fn find_continue_condition_plan(body: &[ASTNode]) -> Result, String> { + for stmt in body { + if let ASTNode::If { condition, then_body, .. } = stmt { + for then_stmt in then_body { + if matches!(then_stmt, ASTNode::Continue { .. }) { + return Ok(Some(condition.as_ref().clone())); + } + } + } + } + Ok(None) +} + +/// Extract carrier updates: ` = + ` pattern only +fn extract_carrier_updates_plan( + body: &[ASTNode], + loop_var: &str, +) -> Result, String> { + use crate::ast::BinaryOperator; + use std::collections::BTreeMap; + + let mut updates = BTreeMap::new(); + + for stmt in body { + if let ASTNode::Assignment { target, value, .. } = stmt { + let var_name = if let ASTNode::Variable { name, .. } = target.as_ref() { + name.clone() + } else { + continue; + }; + + // Skip loop variable itself + if var_name == loop_var { + continue; + } + + // Check for accumulator pattern: `var = var + x` + if let ASTNode::BinaryOp { operator, left, .. } = value.as_ref() { + if matches!(operator, BinaryOperator::Add) { + if let ASTNode::Variable { name, .. } = left.as_ref() { + if name == &var_name { + updates.insert(var_name, value.as_ref().clone()); + } + } + } + } + } + } + + Ok(updates) +} + // ============================================================================ // Unit Tests // ============================================================================ diff --git a/src/mir/builder/control_flow/joinir/patterns/router.rs b/src/mir/builder/control_flow/joinir/patterns/router.rs index b24d1e48..2e837e98 100644 --- a/src/mir/builder/control_flow/joinir/patterns/router.rs +++ b/src/mir/builder/control_flow/joinir/patterns/router.rs @@ -135,6 +135,24 @@ impl<'a> LoopPatternContext<'a> { } } +/// Phase 286 P2.2: Common helper for Plan line lowering +/// +/// Extracts the common 3-line pattern used by Pattern6/7/4/1: +/// 1. Normalize DomainPlan → CorePlan +/// 2. Verify CorePlan invariants (fail-fast) +/// 3. Lower CorePlan → MIR +/// +/// This eliminates repetition across 4 pattern routing blocks. +fn lower_via_plan( + builder: &mut MirBuilder, + domain_plan: crate::mir::builder::control_flow::plan::DomainPlan, + ctx: &LoopPatternContext, +) -> Result, String> { + let core_plan = PlanNormalizer::normalize(builder, domain_plan, ctx)?; + PlanVerifier::verify(&core_plan)?; + PlanLowerer::lower(builder, core_plan, ctx) +} + /// Phase 272 P0.2 Refactoring: can_lower() strategy classification /// /// Clarifies the two main detection strategies used across patterns: @@ -310,15 +328,8 @@ pub(crate) fn route_loop_pattern( Some(domain_plan) => { // DomainPlan extracted successfully trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern6_ScanWithInit (Phase 273)", true); - - // Step 1: Normalize DomainPlan → CorePlan - let core_plan = PlanNormalizer::normalize(builder, domain_plan, ctx)?; - - // Step 2: Verify CorePlan invariants (fail-fast) - PlanVerifier::verify(&core_plan)?; - - // Step 3: Lower CorePlan → MIR - return PlanLowerer::lower(builder, core_plan, ctx); + // Phase 286 P2.2: Use common helper + return lower_via_plan(builder, domain_plan, ctx); } None => { // Not Pattern6 - continue to other patterns @@ -341,15 +352,8 @@ pub(crate) fn route_loop_pattern( Some(domain_plan) => { // DomainPlan extracted successfully trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern7_SplitScan (Phase 273)", true); - - // Step 1: Normalize DomainPlan → CorePlan - let core_plan = PlanNormalizer::normalize(builder, domain_plan, ctx)?; - - // Step 2: Verify CorePlan invariants (fail-fast) - PlanVerifier::verify(&core_plan)?; - - // Step 3: Lower CorePlan → MIR - return PlanLowerer::lower(builder, core_plan, ctx); + // Phase 286 P2.2: Use common helper + return lower_via_plan(builder, domain_plan, ctx); } None => { // Not Pattern7 - continue to other patterns @@ -362,6 +366,52 @@ pub(crate) fn route_loop_pattern( } } + // Phase 286 P2: Try Plan-based Pattern4 (Loop with Continue) + // Flow: Extract → Normalize → Verify → Lower + match super::extractors::pattern4::extract_pattern4_plan( + ctx.condition, + ctx.body, + )? { + Some(domain_plan) => { + // DomainPlan extracted successfully + trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern4_Continue (Phase 286 P2)", true); + // Phase 286 P2.2: Use common helper + return lower_via_plan(builder, domain_plan, ctx); + } + None => { + // Not Pattern4 Plan - continue to legacy Pattern4 + if ctx.debug { + trace::trace().debug( + "route", + "Pattern4 Plan extraction returned None, trying legacy Pattern4", + ); + } + } + } + + // Phase 286 P2.1: Try Plan-based Pattern1 (Simple While Loop) + // Flow: Extract → Normalize → Verify → Lower + match super::extractors::pattern1::extract_pattern1_plan( + ctx.condition, + ctx.body, + )? { + Some(domain_plan) => { + // DomainPlan extracted successfully + trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern1_SimpleWhile (Phase 286 P2.1)", true); + // Phase 286 P2.2: Use common helper + return lower_via_plan(builder, domain_plan, ctx); + } + None => { + // Not Pattern1 Plan - continue to legacy Pattern1 + if ctx.debug { + trace::trace().debug( + "route", + "Pattern1 Plan extraction returned None, trying legacy Pattern1", + ); + } + } + } + // Phase 183: Route based on pre-classified pattern kind // Pattern kind was already determined by ctx.pattern_kind in LoopPatternContext::new() // This eliminates duplicate detection logic across routers. diff --git a/src/mir/builder/control_flow/plan/mod.rs b/src/mir/builder/control_flow/plan/mod.rs index 582f7bb7..36295c04 100644 --- a/src/mir/builder/control_flow/plan/mod.rs +++ b/src/mir/builder/control_flow/plan/mod.rs @@ -45,7 +45,10 @@ pub(in crate::mir::builder) enum DomainPlan { ScanWithInit(ScanWithInitPlan), /// Pattern7: split / tokenization scan SplitScan(SplitScanPlan), - // P2+: BoolPredicate(BoolPredicatePlan), etc. + /// Pattern4: Loop with Continue (Phase 286 P2) + Pattern4Continue(Pattern4ContinuePlan), + /// Pattern1: Simple While Loop (Phase 286 P2.1) + Pattern1SimpleWhile(Pattern1SimpleWhilePlan), } /// Phase 273 P0: Scan direction for forward/reverse scan @@ -98,6 +101,39 @@ pub(in crate::mir::builder) struct SplitScanPlan { pub start_var: String, } +/// Phase 286 P2: Extracted structure for Pattern4 (Loop with Continue) +/// +/// This structure contains all the information needed to lower a continue-style loop. +#[derive(Debug, Clone)] +pub(in crate::mir::builder) struct Pattern4ContinuePlan { + /// Loop variable name (e.g., "i") + pub loop_var: String, + /// Carrier variable names (e.g., ["sum"]) + pub carrier_vars: Vec, + /// Loop condition AST (e.g., `i < 6`) + pub condition: ASTNode, + /// Continue condition AST (e.g., `i == 0`) + pub continue_condition: ASTNode, + /// Carrier update expressions (var -> update AST) + pub carrier_updates: std::collections::BTreeMap, + /// Loop increment expression (e.g., `i + 1`) + pub loop_increment: ASTNode, +} + +/// Phase 286 P2.1: Extracted structure for Pattern1 (Simple While Loop) +/// +/// This structure contains all the information needed to lower a simple while loop. +/// Pattern1 is the simplest loop: no break, no continue, no if-else-phi. +#[derive(Debug, Clone)] +pub(in crate::mir::builder) struct Pattern1SimpleWhilePlan { + /// Loop variable name (e.g., "i") + pub loop_var: String, + /// Loop condition AST (e.g., `i < 3`) + pub condition: ASTNode, + /// Loop increment expression AST (e.g., `i + 1`) + pub loop_increment: ASTNode, +} + // ============================================================================ // CorePlan (固定語彙 - 構造ノードのみ) // ============================================================================ diff --git a/src/mir/builder/control_flow/plan/normalizer.rs b/src/mir/builder/control_flow/plan/normalizer.rs index 6d804d15..1c18935b 100644 --- a/src/mir/builder/control_flow/plan/normalizer.rs +++ b/src/mir/builder/control_flow/plan/normalizer.rs @@ -13,7 +13,7 @@ use super::{ CoreEffectPlan, CoreLoopPlan, CorePhiInfo, CorePlan, DomainPlan, - ScanWithInitPlan, SplitScanPlan, + ScanWithInitPlan, SplitScanPlan, Pattern1SimpleWhilePlan, }; use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext; use crate::mir::builder::MirBuilder; @@ -39,6 +39,8 @@ impl PlanNormalizer { match domain { DomainPlan::ScanWithInit(parts) => Self::normalize_scan_with_init(builder, parts, ctx), DomainPlan::SplitScan(parts) => Self::normalize_split_scan(builder, parts, ctx), + DomainPlan::Pattern4Continue(parts) => Self::normalize_pattern4_continue(builder, parts, ctx), + DomainPlan::Pattern1SimpleWhile(parts) => Self::normalize_pattern1_simple_while(builder, parts, ctx), } } @@ -856,4 +858,610 @@ impl PlanNormalizer { Ok(CorePlan::Loop(loop_plan)) } + + /// Phase 286 P2: Pattern4Continue → CorePlan 変換 + /// + /// Expands Pattern4 (Loop with Continue) semantics into generic CorePlan: + /// - CFG structure: 2-step branching + header PHI merge + /// - NO Select instruction (not in CoreEffectPlan) + /// - NO after PHI (header PHI handles all paths) + fn normalize_pattern4_continue( + builder: &mut MirBuilder, + parts: crate::mir::builder::control_flow::plan::Pattern4ContinuePlan, + ctx: &LoopPatternContext, + ) -> Result { + use crate::mir::builder::control_flow::joinir::trace; + + let trace_logger = trace::trace(); + let debug = ctx.debug; + + if debug { + trace_logger.debug( + "normalizer/pattern4_continue", + &format!( + "Phase 286 P2: Normalizing Pattern4Continue for {} (loop_var: {}, carriers: {:?})", + ctx.func_name, parts.loop_var, parts.carrier_vars + ), + ); + } + + // P2 PoC Scope: Single carrier only + if parts.carrier_vars.len() != 1 { + return Err(format!( + "[normalizer] P2 PoC scope: only single carrier supported (found: {})", + parts.carrier_vars.len() + )); + } + + let carrier_var = &parts.carrier_vars[0]; + + // Step 1: Get host ValueIds for variables + let loop_var_init = builder + .variable_ctx + .variable_map + .get(&parts.loop_var) + .copied() + .ok_or_else(|| format!("[normalizer] Loop variable {} not found", parts.loop_var))?; + + let carrier_init = builder + .variable_ctx + .variable_map + .get(carrier_var) + .copied() + .ok_or_else(|| format!("[normalizer] Carrier variable {} not found", carrier_var))?; + + // Step 2: Capture preheader block + let preheader_bb = builder + .current_block + .ok_or_else(|| "[normalizer] No current block for loop entry".to_string())?; + + // Step 3: Allocate BasicBlockIds for 8 blocks (2-step branching architecture) + let header_bb = builder.next_block_id(); + let body_bb = builder.next_block_id(); + let continue_path_bb = builder.next_block_id(); + let normal_path_bb = builder.next_block_id(); + let step_continue_bb = builder.next_block_id(); + let step_normal_bb = builder.next_block_id(); + let after_bb = builder.next_block_id(); + + if debug { + trace_logger.debug( + "normalizer/pattern4_continue", + &format!( + "Allocated: preheader={:?}, header={:?}, body={:?}, continue_path={:?}, normal_path={:?}, step_continue={:?}, step_normal={:?}, after={:?}", + preheader_bb, header_bb, body_bb, continue_path_bb, normal_path_bb, step_continue_bb, step_normal_bb, after_bb + ), + ); + } + + // Step 4: Allocate ValueIds for loop control and carriers + + // Loop variable PHI dst (header PHI) + let loop_var_current = builder.alloc_typed(MirType::Integer); + + // Carrier PHI dst (header PHI) + let carrier_current = builder.alloc_typed(MirType::Integer); + + // Step 4.5: Build phi_bindings - PHI dst takes precedence over variable_map + // This ensures loop condition/body reference PHI dst, not initial values + let mut phi_bindings: BTreeMap = BTreeMap::new(); + phi_bindings.insert(parts.loop_var.clone(), loop_var_current); + phi_bindings.insert(carrier_var.clone(), carrier_current); + + // Loop condition value + let cond_loop = builder.alloc_typed(MirType::Bool); + + // Continue condition value + let cond_continue = builder.alloc_typed(MirType::Bool); + + // Loop increment values (continue path) + let loop_var_cont_next = builder.alloc_typed(MirType::Integer); + + // Carrier update value (normal path) + let carrier_updated = builder.alloc_typed(MirType::Integer); + + // Loop increment values (normal path) + let loop_var_norm_next = builder.alloc_typed(MirType::Integer); + + // Step 5: Lower AST expressions to get operands and const definitions + // Pass phi_bindings to ensure loop variables reference PHI dst + + // Lower loop condition (e.g., `i < 6`) + let (loop_cond_lhs, loop_cond_op, loop_cond_rhs, loop_cond_consts) = + Self::lower_compare_ast(&parts.condition, builder, &phi_bindings)?; + + // Lower continue condition (e.g., `i == 0`) + let (cont_cond_lhs, cont_cond_op, cont_cond_rhs, cont_cond_consts) = + Self::lower_compare_ast(&parts.continue_condition, builder, &phi_bindings)?; + + // Lower carrier update expression (e.g., `sum = sum + i`) + let carrier_update_ast = parts.carrier_updates.get(carrier_var) + .ok_or_else(|| format!("[normalizer] Carrier update for {} not found", carrier_var))?; + let (carrier_update_lhs, carrier_update_op, carrier_update_rhs, carrier_update_consts) = + Self::lower_binop_ast(carrier_update_ast, builder, &phi_bindings)?; + + // Lower loop increment (e.g., `i = i + 1`) + let (loop_inc_lhs, loop_inc_op, loop_inc_rhs, loop_inc_consts) = + Self::lower_binop_ast(&parts.loop_increment, builder, &phi_bindings)?; + + // Step 6: Build header_effects (const definitions + loop condition check) + let mut header_effects = loop_cond_consts; + header_effects.push(CoreEffectPlan::Compare { + dst: cond_loop, + lhs: loop_cond_lhs, + op: loop_cond_op, + rhs: loop_cond_rhs, + }); + + // Step 7: Build body (as CorePlan items, NOT block_effects) + let mut body = Vec::new(); + // Add const definitions + for const_effect in cont_cond_consts { + body.push(CorePlan::Effect(const_effect)); + } + // Add continue condition check + body.push(CorePlan::Effect(CoreEffectPlan::Compare { + dst: cond_continue, + lhs: cont_cond_lhs, + op: cont_cond_op, + rhs: cont_cond_rhs, + })); + + // Step 8: Build continue_path effects (const definitions + increment loop var + passthrough carrier) + let mut continue_path_effects = loop_inc_consts.clone(); + continue_path_effects.push(CoreEffectPlan::BinOp { + dst: loop_var_cont_next, + lhs: loop_inc_lhs, + op: loop_inc_op, + rhs: loop_inc_rhs, + }); + + // Step 8.5: step_continue has NO effects + // Carrier passthrough: carrier PHI input directly references carrier_current + // No Add 0 workaround needed - PHI inputs can reference the PHI dst from another path + + // Step 9: Build normal_path effects (const definitions + carrier update + loop increment) + let mut normal_path_effects = carrier_update_consts; + normal_path_effects.push(CoreEffectPlan::BinOp { + dst: carrier_updated, + lhs: carrier_update_lhs, + op: carrier_update_op, + rhs: carrier_update_rhs, + }); + normal_path_effects.extend(loop_inc_consts); + normal_path_effects.push(CoreEffectPlan::BinOp { + dst: loop_var_norm_next, + lhs: loop_inc_lhs, + op: loop_inc_op, + rhs: loop_inc_rhs, + }); + + // Step 10: Build block_effects + let block_effects = vec![ + (preheader_bb, vec![]), + (header_bb, header_effects), + (body_bb, vec![]), // Body effects are in body CorePlan field + (continue_path_bb, continue_path_effects), + (normal_path_bb, normal_path_effects), + (step_continue_bb, vec![]), // No effects - carrier passthrough is handled by PHI + (step_normal_bb, vec![]), + ]; + + // Step 11: Build PHIs (header PHI with 3 inputs for 2-step branching) + let phis = vec![ + // Loop variable PHI: (preheader, init), (step_continue, cont), (step_normal, norm) + CorePhiInfo { + block: header_bb, + dst: loop_var_current, + inputs: vec![ + (preheader_bb, loop_var_init), + (step_continue_bb, loop_var_cont_next), + (step_normal_bb, loop_var_norm_next), + ], + tag: format!("loop_var_{}", parts.loop_var), + }, + // Carrier PHI: (preheader, init), (step_continue, current), (step_normal, updated) + // Continue path: carrier unchanged - directly reference carrier_current (no intermediate value) + CorePhiInfo { + block: header_bb, + dst: carrier_current, + inputs: vec![ + (preheader_bb, carrier_init), + (step_continue_bb, carrier_current), // Continue path: carrier unchanged (self-reference OK) + (step_normal_bb, carrier_updated), // Normal path: carrier updated + ], + tag: format!("carrier_{}", carrier_var), + }, + ]; + + // Step 12: Build Frag (2-step branching structure) + let empty_args = EdgeArgs { + layout: JumpArgsLayout::CarriersOnly, + values: vec![], + }; + + let branches = vec![ + // Header: loop condition check + BranchStub { + from: header_bb, + cond: cond_loop, + then_target: body_bb, + then_args: empty_args.clone(), + else_target: after_bb, + else_args: empty_args.clone(), + }, + // Body: continue condition check (2-step branching starts here) + BranchStub { + from: body_bb, + cond: cond_continue, + then_target: continue_path_bb, // Continue path + then_args: empty_args.clone(), + else_target: normal_path_bb, // Normal path + else_args: empty_args.clone(), + }, + ]; + + let wires = vec![ + // Continue path: continue_path → step_continue → header + EdgeStub { + from: continue_path_bb, + kind: ExitKind::Normal, + target: Some(step_continue_bb), + args: empty_args.clone(), + }, + EdgeStub { + from: step_continue_bb, + kind: ExitKind::Normal, + target: Some(header_bb), + args: empty_args.clone(), + }, + // Normal path: normal_path → step_normal → header + EdgeStub { + from: normal_path_bb, + kind: ExitKind::Normal, + target: Some(step_normal_bb), + args: empty_args.clone(), + }, + EdgeStub { + from: step_normal_bb, + kind: ExitKind::Normal, + target: Some(header_bb), + args: empty_args.clone(), + }, + ]; + + let frag = Frag { + entry: header_bb, + exits: BTreeMap::new(), + wires, + branches, + }; + + // Step 13: Build final_values + let final_values = vec![ + (parts.loop_var.clone(), loop_var_current), + (carrier_var.clone(), carrier_current), + ]; + + // Step 14: Build CoreLoopPlan + let loop_plan = CoreLoopPlan { + preheader_bb, + header_bb, + body_bb, + step_bb: step_normal_bb, // Use step_normal as primary step block + after_bb, + found_bb: after_bb, // No early exit, so found = after + body, // Body CorePlan with continue condition check + cond_loop, + cond_match: cond_continue, // Use continue condition as match condition + block_effects, + phis, + frag, + final_values, + }; + + if debug { + trace_logger.debug( + "normalizer/pattern4_continue", + "CorePlan construction complete (2-step branching with header PHI merge)", + ); + } + + Ok(CorePlan::Loop(loop_plan)) + } + + /// Helper: Lower Compare AST to (lhs ValueId, op, rhs ValueId, const_effects) + fn lower_compare_ast( + ast: &crate::ast::ASTNode, + builder: &mut MirBuilder, + phi_bindings: &BTreeMap, + ) -> Result<(crate::mir::ValueId, CompareOp, crate::mir::ValueId, Vec), String> { + use crate::ast::{ASTNode, BinaryOperator}; + + match ast { + ASTNode::BinaryOp { operator, left, right, .. } => { + let op = match operator { + BinaryOperator::Less => CompareOp::Lt, + BinaryOperator::LessEqual => CompareOp::Le, + BinaryOperator::Greater => CompareOp::Gt, + BinaryOperator::GreaterEqual => CompareOp::Ge, + BinaryOperator::Equal => CompareOp::Eq, + BinaryOperator::NotEqual => CompareOp::Ne, + _ => return Err(format!("[normalizer] Unsupported compare operator: {:?}", operator)), + }; + + let (lhs, mut lhs_consts) = Self::lower_value_ast(left, builder, phi_bindings)?; + let (rhs, rhs_consts) = Self::lower_value_ast(right, builder, phi_bindings)?; + + lhs_consts.extend(rhs_consts); + + Ok((lhs, op, rhs, lhs_consts)) + } + _ => Err(format!("[normalizer] Expected BinaryOp for compare, got: {:?}", ast)), + } + } + + /// Helper: Lower BinOp AST to (lhs ValueId, op, rhs ValueId, const_effects) + fn lower_binop_ast( + ast: &crate::ast::ASTNode, + builder: &mut MirBuilder, + phi_bindings: &BTreeMap, + ) -> Result<(crate::mir::ValueId, BinaryOp, crate::mir::ValueId, Vec), String> { + use crate::ast::{ASTNode, BinaryOperator}; + + match ast { + ASTNode::BinaryOp { operator, left, right, .. } => { + let op = match operator { + BinaryOperator::Add => BinaryOp::Add, + BinaryOperator::Subtract => BinaryOp::Sub, + BinaryOperator::Multiply => BinaryOp::Mul, + BinaryOperator::Divide => BinaryOp::Div, + _ => return Err(format!("[normalizer] Unsupported binary operator: {:?}", operator)), + }; + + let (lhs, mut lhs_consts) = Self::lower_value_ast(left, builder, phi_bindings)?; + let (rhs, rhs_consts) = Self::lower_value_ast(right, builder, phi_bindings)?; + + lhs_consts.extend(rhs_consts); + + Ok((lhs, op, rhs, lhs_consts)) + } + _ => Err(format!("[normalizer] Expected BinOp, got: {:?}", ast)), + } + } + + /// Helper: Lower value AST to (ValueId, const_effects) + /// Returns the ValueId and any Const instructions needed to define literals + /// + /// phi_bindings: PHI dst for loop variables (takes precedence over variable_map) + fn lower_value_ast( + ast: &crate::ast::ASTNode, + builder: &mut MirBuilder, + phi_bindings: &BTreeMap, + ) -> Result<(crate::mir::ValueId, Vec), String> { + use crate::ast::{ASTNode, LiteralValue}; + + match ast { + ASTNode::Variable { name, .. } => { + // PHI bindings take precedence (for loop variables in header/body) + if let Some(&phi_dst) = phi_bindings.get(name) { + return Ok((phi_dst, vec![])); + } + // Fallback to variable_map (for pre-loop init values) + if let Some(&value_id) = builder.variable_ctx.variable_map.get(name) { + Ok((value_id, vec![])) + } else { + Err(format!("[normalizer] Variable {} not found", name)) + } + } + ASTNode::Literal { value, .. } => { + // Allocate ValueId for literal and create Const instruction + let value_id = builder.next_value_id(); + let const_value = match value { + LiteralValue::Integer(n) => ConstValue::Integer(*n), + LiteralValue::String(s) => ConstValue::String(s.clone()), + LiteralValue::Bool(b) => ConstValue::Bool(*b), + _ => return Err(format!("[normalizer] Unsupported literal type: {:?}", value)), + }; + + builder.type_ctx.value_types.insert(value_id, MirType::Integer); + + // Return the ValueId and the Const instruction + let const_effect = CoreEffectPlan::Const { + dst: value_id, + value: const_value, + }; + + Ok((value_id, vec![const_effect])) + } + _ => Err(format!("[normalizer] Unsupported value AST: {:?}", ast)), + } + } + + /// Phase 286 P2.1: Pattern1SimpleWhile → CorePlan 変換 + /// + /// Expands Pattern1 (Simple While Loop) semantics into generic CorePlan: + /// - CFG structure: preheader → header → body → step → header (back-edge) + /// - 1 PHI for loop variable in header + /// - No 2-step branching (simpler than Pattern4) + fn normalize_pattern1_simple_while( + builder: &mut MirBuilder, + parts: Pattern1SimpleWhilePlan, + ctx: &LoopPatternContext, + ) -> Result { + use crate::mir::builder::control_flow::joinir::trace; + + let trace_logger = trace::trace(); + let debug = ctx.debug; + + if debug { + trace_logger.debug( + "normalizer/pattern1_simple_while", + &format!( + "Phase 286 P2.1: Normalizing Pattern1SimpleWhile for {} (loop_var: {})", + ctx.func_name, parts.loop_var + ), + ); + } + + // Step 1: Get host ValueId for loop variable + let loop_var_init = builder + .variable_ctx + .variable_map + .get(&parts.loop_var) + .copied() + .ok_or_else(|| format!("[normalizer] Loop variable {} not found", parts.loop_var))?; + + // Step 2: Capture preheader block + let preheader_bb = builder + .current_block + .ok_or_else(|| "[normalizer] No current block for loop entry".to_string())?; + + // Step 3: Allocate BasicBlockIds for 5 blocks + let header_bb = builder.next_block_id(); + let body_bb = builder.next_block_id(); + let step_bb = builder.next_block_id(); + let after_bb = builder.next_block_id(); + + if debug { + trace_logger.debug( + "normalizer/pattern1_simple_while", + &format!( + "Allocated: preheader={:?}, header={:?}, body={:?}, step={:?}, after={:?}", + preheader_bb, header_bb, body_bb, step_bb, after_bb + ), + ); + } + + // Step 4: Allocate ValueIds for loop control + let loop_var_current = builder.alloc_typed(MirType::Integer); // PHI destination + let cond_loop = builder.alloc_typed(MirType::Bool); // Loop condition + let loop_var_next = builder.alloc_typed(MirType::Integer); // Incremented loop var + + // Step 5: Build phi_bindings - PHI dst takes precedence over variable_map + let mut phi_bindings: BTreeMap = BTreeMap::new(); + phi_bindings.insert(parts.loop_var.clone(), loop_var_current); + + // Step 6: Lower AST expressions + // Lower loop condition (e.g., `i < 3`) + let (loop_cond_lhs, loop_cond_op, loop_cond_rhs, loop_cond_consts) = + Self::lower_compare_ast(&parts.condition, builder, &phi_bindings)?; + + // Lower loop increment (e.g., `i + 1`) + let (loop_inc_lhs, loop_inc_op, loop_inc_rhs, loop_inc_consts) = + Self::lower_binop_ast(&parts.loop_increment, builder, &phi_bindings)?; + + // Step 7: Build header_effects (const definitions + loop condition check) + let mut header_effects = loop_cond_consts; + header_effects.push(CoreEffectPlan::Compare { + dst: cond_loop, + lhs: loop_cond_lhs, + op: loop_cond_op, + rhs: loop_cond_rhs, + }); + + // Step 8: Build step_effects (const definitions + increment) + let mut step_effects = loop_inc_consts; + step_effects.push(CoreEffectPlan::BinOp { + dst: loop_var_next, + lhs: loop_inc_lhs, + op: loop_inc_op, + rhs: loop_inc_rhs, + }); + + // Step 9: Build block_effects + let block_effects = vec![ + (preheader_bb, vec![]), + (header_bb, header_effects), + (body_bb, vec![]), // Body is empty for simple increment-only loop + (step_bb, step_effects), + ]; + + // Step 10: Build PHI (single PHI for loop variable) + let phis = vec![ + CorePhiInfo { + block: header_bb, + dst: loop_var_current, + inputs: vec![ + (preheader_bb, loop_var_init), + (step_bb, loop_var_next), + ], + tag: format!("loop_var_{}", parts.loop_var), + }, + ]; + + // Step 11: Build Frag + let empty_args = EdgeArgs { + layout: JumpArgsLayout::CarriersOnly, + values: vec![], + }; + + let branches = vec![ + BranchStub { + from: header_bb, + cond: cond_loop, + then_target: body_bb, + then_args: empty_args.clone(), + else_target: after_bb, + else_args: empty_args.clone(), + }, + ]; + + let wires = vec![ + // body → step + EdgeStub { + from: body_bb, + kind: ExitKind::Normal, + target: Some(step_bb), + args: empty_args.clone(), + }, + // step → header (back-edge) + EdgeStub { + from: step_bb, + kind: ExitKind::Normal, + target: Some(header_bb), + args: empty_args.clone(), + }, + ]; + + let frag = Frag { + entry: header_bb, + exits: BTreeMap::new(), + wires, + branches, + }; + + // Step 12: Build final_values + let final_values = vec![ + (parts.loop_var.clone(), loop_var_current), + ]; + + // Step 13: Build CoreLoopPlan + // Note: found_bb = after_bb (no early exit in Pattern1) + // Note: cond_match unused but required by struct, use cond_loop as placeholder + let loop_plan = CoreLoopPlan { + preheader_bb, + header_bb, + body_bb, + step_bb, + after_bb, + found_bb: after_bb, // No early exit + body: vec![], // Body is empty for simple loop + cond_loop, + cond_match: cond_loop, // Placeholder (unused in Pattern1) + block_effects, + phis, + frag, + final_values, + }; + + if debug { + trace_logger.debug( + "normalizer/pattern1_simple_while", + "CorePlan construction complete (4 blocks, 1 PHI)", + ); + } + + Ok(CorePlan::Loop(loop_plan)) + } } diff --git a/src/mir/builder/utils.rs b/src/mir/builder/utils.rs index ce744f9e..6e2c66c7 100644 --- a/src/mir/builder/utils.rs +++ b/src/mir/builder/utils.rs @@ -1,5 +1,5 @@ use super::{BasicBlock, BasicBlockId}; -use crate::mir::{BarrierOp, SpannedInstruction, TypeOpKind, WeakRefOp}; +use crate::mir::{BarrierOp, MirType, SpannedInstruction, TypeOpKind, WeakRefOp}; use std::sync::atomic::{AtomicUsize, Ordering}; // include path resolver removed (using handles modules) @@ -56,6 +56,17 @@ impl super::MirBuilder { } } + /// Allocate a new ValueId and register its MIR type (convenience helper). + /// + /// This is a readability helper for code that repeatedly does: + /// `let v = next_value_id(); type_ctx.value_types.insert(v, ty);` + #[inline] + pub(crate) fn alloc_typed(&mut self, ty: MirType) -> super::ValueId { + let value_id = self.next_value_id(); + self.type_ctx.value_types.insert(value_id, ty); + value_id + } + /// Allocate a new BasicBlockId /// /// Phase 136 Step 2/7 + Phase 2-2: Uses core_ctx as SSOT (no sync needed). diff --git a/tools/smokes/v2/profiles/integration/apps/phase286_pattern1_frag_poc.sh b/tools/smokes/v2/profiles/integration/apps/phase286_pattern1_frag_poc.sh new file mode 100644 index 00000000..f46c83e4 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase286_pattern1_frag_poc.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Phase 286 P2.1: Pattern1 → Frag PoC test +# Tests: Pattern1 (SimpleWhile) using Plan/Frag SSOT +# +# PoC Goal: +# Pattern1 → DomainPlan → CorePlan → Frag → emit_frag() +# (Skip: JoinIR → bridge → merge) +# +# Expected: return 3 (loop 3 times: i=0,1,2 → i=3 で終了) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +INPUT="$NYASH_ROOT/apps/tests/phase286_pattern1_frag_poc.hako" +RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10} + +set +e +OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env NYASH_DISABLE_PLUGINS=1 "$NYASH_BIN" "$INPUT" 2>&1) +EXIT_CODE=$? +set -e + +if [ "$EXIT_CODE" -eq 124 ]; then + test_fail "phase286_pattern1_frag_poc: hakorune timed out (>${RUN_TIMEOUT_SECS}s)" + exit 1 +elif [ "$EXIT_CODE" -eq 3 ]; then + # Expected: return 3 (exit code) + test_pass "phase286_pattern1_frag_poc: Pattern1 Frag PoC succeeded (return: 3)" + exit 0 +else + echo "[FAIL] Unexpected exit code (expected: 3, got: $EXIT_CODE)" + echo "[INFO] Output:" + echo "$OUTPUT" | head -n 20 || true + test_fail "phase286_pattern1_frag_poc: Unexpected exit code" + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase286_pattern4_frag_poc.sh b/tools/smokes/v2/profiles/integration/apps/phase286_pattern4_frag_poc.sh new file mode 100644 index 00000000..07ada4d7 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase286_pattern4_frag_poc.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Phase 286 P2: Pattern4 → Frag PoC test +# Tests: Pattern4 (Loop with Continue) using Plan/Frag SSOT +# +# PoC Goal: +# Pattern4 → DomainPlan → CorePlan → Frag → emit_frag() +# (Skip: JoinIR → bridge → merge) +# +# Expected: Output "6" (sum of 1+2+3, loop skips i==0) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +INPUT="$NYASH_ROOT/apps/tests/phase286_pattern4_frag_poc.hako" +RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10} + +set +e +OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env NYASH_DISABLE_PLUGINS=1 "$NYASH_BIN" "$INPUT" 2>&1) +EXIT_CODE=$? +set -e + +if [ "$EXIT_CODE" -eq 124 ]; then + test_fail "phase286_pattern4_frag_poc: hakorune timed out (>${RUN_TIMEOUT_SECS}s)" + exit 1 +elif [ "$EXIT_CODE" -eq 0 ]; then + # Expected output: "6" + if echo "$OUTPUT" | grep -q "^6$"; then + test_pass "phase286_pattern4_frag_poc: Pattern4 Frag PoC succeeded (output: 6)" + exit 0 + else + echo "[FAIL] Unexpected output (expected: 6)" + echo "[INFO] Output:" + echo "$OUTPUT" | head -n 20 || true + test_fail "phase286_pattern4_frag_poc: Unexpected output" + exit 1 + fi +else + echo "[FAIL] hakorune failed with exit code $EXIT_CODE" + echo "[INFO] Output (tail):" + echo "$OUTPUT" | tail -n 20 || true + test_fail "phase286_pattern4_frag_poc: hakorune failed" + exit 1 +fi