diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index e1ad9519..9a70bd88 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -1,6 +1,6 @@ # Self Current Task — Now (main) -## Current Focus: Phase 29ab P8(Pattern6/7 ok path PASS) +## Current Focus: Phase 29ab COMPLETE(closeout) **2025-12-28: Phase 29ab P1 完了** ✅ - 目的: Pattern2 の LoopBodyLocal promotion の最小ケースを fixture+integration smoke で固定 @@ -53,6 +53,10 @@ - Pattern7 OK: `apps/tests/phase29ab_pattern7_splitscan_ok_min.hako` - Smokes: `phase29ab_pattern6_scan_with_init_ok_min_vm`, `phase29ab_pattern7_splitscan_ok_min_vm` +**2025-12-28: Phase 29ab P9 完了** ✅ +- 目的: Phase 29ab closeout(入口SSOTの集約) +- 入口: `docs/development/current/main/phases/phase-29ab/README.md` + **2025-12-28: Phase 29aa P5 完了** ✅ - 目的: Return block が複数 predecessor のとき、incoming state が完全一致する場合のみ ReturnCleanup を成立させる - 入口: `docs/development/current/main/phases/phase-29aa/README.md` diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md index c256f778..dee242b4 100644 --- a/docs/development/current/main/30-Backlog.md +++ b/docs/development/current/main/30-Backlog.md @@ -8,16 +8,8 @@ Related: ## 直近(JoinIR/selfhost) -- **Phase 29ab(P4 ✅ done / P5 planned): JoinIR completion triage** - - P1: Pattern2 LoopBodyLocal minimal fixture+smoke fixed(`phase29ab_pattern2_loopbodylocal_min_vm`) - - P2: Pattern2 Trim seg minimal fixture+smoke fixed(`phase29ab_pattern2_loopbodylocal_seg_min_vm`) - - P3: PromoteDecision の “NotApplicable/Freeze” 境界を contract+smoke で固定(JoinIR-only前提) - - P4: Phase 263(Stage‑B 実ログ seg)を Derived slot 方針で通し、fixture+smoke で固定 - - P5: Pattern7 SplitScan first-fail(契約違反)を freeze で固定して SSOT 化 - - P6: Pattern6 ScanWithInit first-fail(契約違反)を freeze で固定して SSOT 化 - - P7: Pattern6 reverse scan / MatchScan の near-miss を追加で freeze 固定(負債を先に顕在化) - - P8: Pattern6/7 “正常系” を Plan/Frag/compose へ寄せて PASS 固定(freeze を減らす本丸)✅ - - P9: Phase 29ab closeout(Now/Backlog/phase README を更新して完了宣言) +- **Phase 29ab(✅ COMPLETE): JoinIR completion triage** + - 入口: `docs/development/current/main/phases/phase-29ab/README.md` - **Phase 288(✅ P0–P3 + 288.1 complete): REPL mode** - 入口: `docs/development/current/main/phases/phase-288/README.md` diff --git a/docs/development/current/main/design/edgecfg-fragments.md b/docs/development/current/main/design/edgecfg-fragments.md index 64fba4e9..828a88c3 100644 --- a/docs/development/current/main/design/edgecfg-fragments.md +++ b/docs/development/current/main/design/edgecfg-fragments.md @@ -361,6 +361,13 @@ MIR Terminator Instructions - loop の EdgeCFG 化は、まず **BasicBlockId 層で持っている箇所(Phase 268 の if_form のような場所)**から適用を進める。 - JoinIR 側の loop は Phase 270 で **fixture/smoke による SSOT 固定**を先に行い、壊れたら最小差分で直す。 +## compose SSOT(使い分けガイド) + +- `compose::if_`: then/else が **Normal exit** で step/merge に合流する形(Pattern7 split scan の if/else) +- `compose::cleanup`: main frag に cleanup frag(Return/Normal)を合成して早期離脱を明示する形(Pattern6 scan-with-init) +- どちらも **Frag の validity を前提** にし、Normalizer が入口で妥当性を保証する +- Pattern6/7 contract SSOT: `docs/development/current/main/design/pattern6-7-contracts.md` + 補足(Phase 270): - Pattern1(simple_while_minimal)は test-only stub のため、一般ループの “基準” には使えない。 - Phase 270 では “最小の固定形” を Pattern9(AccumConstLoop)として追加し、後で Frag 合成側へ吸収される前提で橋渡しにする。 diff --git a/docs/development/current/main/design/pattern6-7-contracts.md b/docs/development/current/main/design/pattern6-7-contracts.md new file mode 100644 index 00000000..301f2f00 --- /dev/null +++ b/docs/development/current/main/design/pattern6-7-contracts.md @@ -0,0 +1,39 @@ +# Pattern6/7 Contracts (SSOT) + +This document defines the **contract boundary** for Pattern6/7 extractors. +It is the SSOT for `NotApplicable` vs `Freeze` decisions. + +## Common Rule + +- **NotApplicable**: shape mismatch (another pattern may apply) +- **Freeze**: shape matches but contract is violated (fail-fast) + +Tags: +- Pattern6: `[joinir/phase29ab/pattern6/contract]` +- Pattern7: `[joinir/phase29ab/pattern7/contract]` + +## Pattern6 (ScanWithInit / MatchScan) + +Accepted shape: +- `loop(i < s.length())` or `loop(i <= s.length() - needle.length())` +- `if s.substring(i, i + 1) == needle { return i }` +- step update exists and matches direction + +Freeze conditions: +- step update missing +- forward scan with step != `i = i + 1` +- reverse scan with step != `i = i - 1` +Note: +- plan line is forward-only; reverse scans are currently treated as NotApplicable + +## Pattern7 (SplitScan) + +Accepted shape: +- `loop(i <= s.length() - separator.length())` +- `if s.substring(i, i + separator.length()) == separator` +- then: `result.push(...)`, `start = i + separator.length()`, `i = start` +- else: `i = i + 1` + +Freeze conditions: +- then/else update contracts broken (start/i updates) +- separator literal length != 1 (P0 scope) diff --git a/docs/development/current/main/phases/phase-29ab/README.md b/docs/development/current/main/phases/phase-29ab/README.md new file mode 100644 index 00000000..31eb9b0a --- /dev/null +++ b/docs/development/current/main/phases/phase-29ab/README.md @@ -0,0 +1,54 @@ +# Phase 29ab: JoinIR completion triage (P1–P9) + +Goal: Fix near-miss vs OK boundaries for Pattern2/6/7 and provide a single entry point for fixtures, smokes, and contracts. + +## Contracts (SSOT) + +- Pattern2 promotion: `src/mir/builder/control_flow/joinir/patterns/pattern2/api/README.md` +- Pattern6/7 contracts: `docs/development/current/main/design/pattern6-7-contracts.md` +- compose SSOT: `docs/development/current/main/design/edgecfg-fragments.md` + +## Fixtures and Smokes + +### Freeze-fixed (contract violations) +- Pattern2 seg freeze: + - `apps/tests/phase29ab_pattern2_seg_freeze_min.hako` + - `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern2_seg_freeze_min_vm.sh` +- Pattern7 SplitScan first-fail: + - `apps/tests/phase29ab_pattern7_firstfail_min.hako` + - `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern7_firstfail_min_vm.sh` +- Pattern6 ScanWithInit first-fail: + - `apps/tests/phase29ab_pattern6_firstfail_min.hako` + - `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern6_firstfail_min_vm.sh` +- Pattern6 reverse scan near-miss: + - `apps/tests/phase29ab_pattern6_reverse_firstfail_min.hako` + - `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern6_reverse_firstfail_min_vm.sh` +- Pattern6 matchscan near-miss: + - `apps/tests/phase29ab_pattern6_matchscan_firstfail_min.hako` + - `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern6_matchscan_firstfail_min_vm.sh` + +### OK (PASS fixed) +- Pattern2 LoopBodyLocal min: + - `apps/tests/phase29ab_pattern2_loopbodylocal_min.hako` + - `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern2_loopbodylocal_min_vm.sh` +- Pattern2 LoopBodyLocal seg: + - `apps/tests/phase29ab_pattern2_loopbodylocal_seg_min.hako` + - `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern2_loopbodylocal_seg_min_vm.sh` +- Pattern2 seg notapplicable: + - `apps/tests/phase29ab_pattern2_seg_notapplicable_min.hako` + - `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern2_seg_notapplicable_min_vm.sh` +- Phase 263 realworld seg (Derived slot path): + - `apps/tests/phase263_pattern2_seg_realworld_min.hako` + - `tools/smokes/v2/profiles/integration/apps/phase263_pattern2_seg_realworld_min_vm.sh` +- Pattern6 ScanWithInit OK: + - `apps/tests/phase29ab_pattern6_scan_with_init_ok_min.hako` + - `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern6_scan_with_init_ok_min_vm.sh` +- Pattern7 SplitScan OK: + - `apps/tests/phase29ab_pattern7_splitscan_ok_min.hako` + - `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern7_splitscan_ok_min_vm.sh` + +## Commands + +- `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern2_*"` +- `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern6_*"` +- `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern7_*"` diff --git a/src/mir/builder/control_flow/joinir/patterns/common/contract_error.rs b/src/mir/builder/control_flow/joinir/patterns/common/contract_error.rs new file mode 100644 index 00000000..ebb8c760 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/common/contract_error.rs @@ -0,0 +1,43 @@ +use crate::mir::join_ir::lowering::error_tags; + +#[derive(Debug, Clone)] +pub(crate) struct ContractViolation { + pub(crate) msg: String, + pub(crate) hint: String, +} + +impl ContractViolation { + pub(crate) fn new(msg: &str, hint: &str) -> Self { + Self { + msg: msg.to_string(), + hint: hint.to_string(), + } + } +} + +pub(crate) fn freeze_contract(tag: &str, violation: &ContractViolation) -> String { + error_tags::freeze_with_hint(tag, &violation.msg, &violation.hint) +} + +pub(crate) enum ExtractDecision { + Match(T), + NotApplicable, + Contract(ContractViolation), +} + +impl ExtractDecision { + pub(crate) fn contract(msg: &str, hint: &str) -> Self { + Self::Contract(ContractViolation::new(msg, hint)) + } +} + +pub(crate) fn finalize_extract( + decision: ExtractDecision, + tag: &str, +) -> Result, String> { + match decision { + ExtractDecision::Match(value) => Ok(Some(value)), + ExtractDecision::NotApplicable => Ok(None), + ExtractDecision::Contract(violation) => Err(freeze_contract(tag, &violation)), + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/common/mod.rs b/src/mir/builder/control_flow/joinir/patterns/common/mod.rs index a0c403bb..d6b0f75b 100644 --- a/src/mir/builder/control_flow/joinir/patterns/common/mod.rs +++ b/src/mir/builder/control_flow/joinir/patterns/common/mod.rs @@ -5,8 +5,10 @@ mod ast_helpers; mod carrier_binding_policy; +mod contract_error; mod joinir_helpers; // Phase 256.8.5: JoinModule helpers pub(crate) use ast_helpers::var; pub(crate) use carrier_binding_policy::{decide_carrier_binding_policy, CarrierBindingPolicy}; +pub(crate) use contract_error::{finalize_extract, ContractViolation, ExtractDecision}; pub(crate) use joinir_helpers::get_entry_function; // Phase 256.8.5 diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs b/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs index 5044d1f7..0d1bb30b 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs @@ -35,55 +35,14 @@ // Phase 255 P2: Use shared var() helper use crate::ast::ASTNode; -use crate::mir::join_ir::lowering::error_tags; +use crate::mir::builder::control_flow::joinir::patterns::common::{finalize_extract, ExtractDecision}; // Phase 273 P1: Import DomainPlan types (Plan renamed to DomainPlan) use crate::mir::builder::control_flow::plan::{DomainPlan, ScanDirection as PlanScanDirection, ScanWithInitPlan}; -// Phase 273 P0.1: Local ScanDirection and ScanParts removed (migrated to plan/mod.rs) -// Only keeping internal helper enum for legacy extract_scan_with_init_parts() +// Phase 273 P0.1: ScanDirection/ScanWithInitPlan is SSOT in plan/mod.rs -#[derive(Debug, Clone)] -struct ScanWithInitContractViolation { - msg: String, - hint: String, -} - -#[derive(Debug, Clone)] -enum ScanWithInitExtractError { - Contract(ScanWithInitContractViolation), -} - -impl ScanWithInitExtractError { - fn contract(msg: &str, hint: &str) -> Self { - Self::Contract(ScanWithInitContractViolation { - msg: msg.to_string(), - hint: hint.to_string(), - }) - } -} - -/// Phase 273 P0.1: Internal scan direction for legacy helper (temporary) -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ScanDirection { - /// Forward scan: i < s.length(), i = i + 1 - Forward, - /// Reverse scan: i >= 0, i = i - 1 - Reverse, -} - -/// Phase 273 P0.1: Internal structure for legacy helper (temporary) -#[derive(Debug, Clone)] -struct ScanParts { - loop_var: String, - haystack: String, - needle: String, - step_lit: i64, - early_return_expr: ASTNode, - not_found_return_lit: i64, - scan_direction: ScanDirection, - dynamic_needle: bool, -} +// Phase 273 P0.1: extract_scan_with_init_parts() returns ScanWithInitPlan directly /// Phase 273 P1: Pure extractor that returns DomainPlan (SSOT) /// @@ -101,41 +60,21 @@ pub(crate) fn extract_scan_with_init_plan( fn_body: Option<&[ASTNode]>, ) -> Result, String> { // Call internal extraction helper - let parts = match extract_scan_with_init_parts(condition, body, fn_body) { - Ok(parts) => parts, - Err(ScanWithInitExtractError::Contract(err)) => { - return Err(error_tags::freeze_with_hint( - "phase29ab/pattern6/contract", - &err.msg, - &err.hint, - )); - } - }; + let parts = finalize_extract( + extract_scan_with_init_parts(condition, body, fn_body), + "phase29ab/pattern6/contract", + )?; // Phase 273 P1: Filter out patterns not supported by Plan-based normalizer if let Some(ref p) = parts { // P1 scope: Only forward scan (step=1) supported if p.step_lit != 1 { - return Ok(None); // Let legacy path handle reverse scans + return Ok(None); } } // Wrap in DomainPlan if extracted successfully - Ok(parts.map(|p| { - DomainPlan::ScanWithInit(ScanWithInitPlan { - loop_var: p.loop_var, - haystack: p.haystack, - needle: p.needle, - step_lit: p.step_lit, - early_return_expr: p.early_return_expr, - not_found_return_lit: p.not_found_return_lit, - scan_direction: match p.scan_direction { - ScanDirection::Forward => PlanScanDirection::Forward, - ScanDirection::Reverse => PlanScanDirection::Reverse, - }, - dynamic_needle: p.dynamic_needle, - }) - })) + Ok(parts.map(DomainPlan::ScanWithInit)) } // Phase 273 P0.1: can_lower() removed (router now calls extract_scan_with_init_plan() directly) @@ -224,9 +163,9 @@ fn extract_substring_window( /// /// # Returns /// -/// * `Ok(Some(ScanParts))` - Successfully extracted the pattern -/// * `Ok(None)` - Not a scan-with-init pattern (different pattern) -/// * `Err(String)` - Contract violation +/// * `ExtractDecision::Match(ScanWithInitPlan)` - Successfully extracted the pattern +/// * `ExtractDecision::NotApplicable` - Not a scan-with-init pattern (different pattern) +/// * `ExtractDecision::Contract` - Contract violation /// /// # P0 Restrictions /// @@ -238,7 +177,7 @@ fn extract_scan_with_init_parts( condition: &ASTNode, body: &[ASTNode], _fn_body: Option<&[ASTNode]>, -) -> Result, ScanWithInitExtractError> { +) -> ExtractDecision { use crate::ast::{BinaryOperator, LiteralValue}; // 1. Check loop condition: i < s.length() (forward) or i >= 0 (reverse) @@ -253,7 +192,7 @@ fn extract_scan_with_init_parts( } => { let loop_var = match left.as_ref() { ASTNode::Variable { name, .. } => name.clone(), - _ => return Ok(None), + _ => return ExtractDecision::NotApplicable, }; let haystack = match right.as_ref() { @@ -261,12 +200,12 @@ fn extract_scan_with_init_parts( object, method, .. } if method == "length" => match object.as_ref() { ASTNode::Variable { name, .. } => name.clone(), - _ => return Ok(None), + _ => return ExtractDecision::NotApplicable, }, - _ => return Ok(None), + _ => return ExtractDecision::NotApplicable, }; - (loop_var, Some(haystack), ScanDirection::Forward) + (loop_var, Some(haystack), PlanScanDirection::Forward) } // Forward (Dynamic): i <= s.length() - substr.length() // Phase 258 P0: Accept dynamic needle form for index_of_string @@ -278,7 +217,7 @@ fn extract_scan_with_init_parts( } => { let loop_var = match left.as_ref() { ASTNode::Variable { name, .. } => name.clone(), - _ => return Ok(None), + _ => return ExtractDecision::NotApplicable, }; // Right side must be: s.length() - substr.length() @@ -295,9 +234,9 @@ fn extract_scan_with_init_parts( object, method, .. } if method == "length" => match object.as_ref() { ASTNode::Variable { name, .. } => name.clone(), - _ => return Ok(None), + _ => return ExtractDecision::NotApplicable, }, - _ => return Ok(None), + _ => return ExtractDecision::NotApplicable, }; // Right of subtraction: substr.length() @@ -306,13 +245,13 @@ fn extract_scan_with_init_parts( // Valid: s.length() - substr.length() haystack } - _ => return Ok(None), + _ => return ExtractDecision::NotApplicable, } } - _ => return Ok(None), + _ => return ExtractDecision::NotApplicable, }; - (loop_var, Some(haystack), ScanDirection::Forward) + (loop_var, Some(haystack), PlanScanDirection::Forward) } // Reverse: i >= 0 ASTNode::BinaryOp { @@ -323,7 +262,7 @@ fn extract_scan_with_init_parts( } => { let loop_var = match left.as_ref() { ASTNode::Variable { name, .. } => name.clone(), - _ => return Ok(None), + _ => return ExtractDecision::NotApplicable, }; // Check right is Literal(0) @@ -332,13 +271,13 @@ fn extract_scan_with_init_parts( value: LiteralValue::Integer(0), .. } => {} - _ => return Ok(None), + _ => return ExtractDecision::NotApplicable, } // For reverse, haystack will be extracted from substring call in body - (loop_var, None, ScanDirection::Reverse) + (loop_var, None, PlanScanDirection::Reverse) } - _ => return Ok(None), + _ => return ExtractDecision::NotApplicable, }; // 2. Find if statement with substring == needle and return loop_var @@ -421,30 +360,36 @@ fn extract_scan_with_init_parts( } } - // Phase 273 P2: Return Ok(None) if pattern doesn't match (allow Pattern7 to try) + // Phase 273 P2: Return NotApplicable if pattern doesn't match (allow Pattern7 to try) let needle = match needle_opt { Some(n) => n, - None => return Ok(None), // Not Pattern6, try next pattern + None => return ExtractDecision::NotApplicable, // Not Pattern6, try next pattern }; let early_return_expr = match early_return_expr_opt { Some(e) => e, - None => return Ok(None), // Not Pattern6, try next pattern + None => return ExtractDecision::NotApplicable, // Not Pattern6, try next pattern }; // Phase 257 P0: Determine haystack based on scan direction let haystack = match scan_direction { - ScanDirection::Forward => haystack_opt.ok_or_else(|| { - ScanWithInitExtractError::contract( - "scan-with-init contract: forward scan missing haystack", - "use `i < s.length()` or `i <= s.length() - needle.length()` for forward scans", - ) - })?, - ScanDirection::Reverse => haystack_from_substring_opt.ok_or_else(|| { - ScanWithInitExtractError::contract( - "scan-with-init contract: reverse scan missing haystack", - "use `s.substring(i, i + 1)` (or dynamic) in the match condition", - ) - })?, + PlanScanDirection::Forward => match haystack_opt { + Some(value) => value, + None => { + return ExtractDecision::contract( + "scan-with-init contract: forward scan missing haystack", + "use `i < s.length()` or `i <= s.length() - needle.length()` for forward scans", + ); + } + }, + PlanScanDirection::Reverse => match haystack_from_substring_opt { + Some(value) => value, + None => { + return ExtractDecision::contract( + "scan-with-init contract: reverse scan missing haystack", + "use `s.substring(i, i + 1)` (or dynamic) in the match condition", + ); + } + }, }; // 3. Check for step: i = i + 1 (forward) or i = i - 1 (reverse) @@ -486,29 +431,32 @@ fn extract_scan_with_init_parts( } } - let step_lit = step_lit_opt.ok_or_else(|| { - ScanWithInitExtractError::contract( - "scan-with-init contract: missing step update", - "add `i = i + 1` (forward) or `i = i - 1` (reverse) inside the loop", - ) - })?; + let step_lit = match step_lit_opt { + Some(value) => value, + None => { + return ExtractDecision::contract( + "scan-with-init contract: missing step update", + "add `i = i + 1` (forward) or `i = i - 1` (reverse) inside the loop", + ); + } + }; // Phase 257 P0: Verify step matches scan direction match scan_direction { - ScanDirection::Forward => { + PlanScanDirection::Forward => { if step_lit != 1 { - return Err(ScanWithInitExtractError::contract( + return ExtractDecision::contract( "scan-with-init contract: forward step must be `i = i + 1`", "change the step update to `i = i + 1`", - )); + ); } } - ScanDirection::Reverse => { + PlanScanDirection::Reverse => { if step_lit != -1 { - return Err(ScanWithInitExtractError::contract( + return ExtractDecision::contract( "scan-with-init contract: reverse step must be `i = i - 1`", "change the step update to `i = i - 1`", - )); + ); } } } @@ -519,7 +467,7 @@ fn extract_scan_with_init_parts( // Phase 258 P0: Extract dynamic_needle (default to false for backward compat) let dynamic_needle = dynamic_needle_opt.unwrap_or(false); - Ok(Some(ScanParts { + ExtractDecision::Match(ScanWithInitPlan { loop_var, haystack, needle, @@ -528,7 +476,7 @@ fn extract_scan_with_init_parts( not_found_return_lit, scan_direction, dynamic_needle, - })) + }) } // Phase 273 P0.1: lower() removed (router now uses PlanLowerer::lower_scan_with_init()) diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs b/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs index 3743d13c..72b08c09 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs @@ -28,32 +28,9 @@ //! - P0 restriction: 1-char separator only use crate::ast::ASTNode; -use crate::mir::join_ir::lowering::error_tags; - -#[derive(Debug, Clone)] -struct SplitScanContractViolation { - msg: String, - hint: String, -} - -#[derive(Debug, Clone)] -enum SplitScanExtractError { - NotApplicable(String), - Contract(SplitScanContractViolation), -} - -impl SplitScanExtractError { - fn not_applicable(msg: &str) -> Self { - Self::NotApplicable(msg.to_string()) - } - - fn contract(msg: &str, hint: &str) -> Self { - Self::Contract(SplitScanContractViolation { - msg: msg.to_string(), - hint: hint.to_string(), - }) - } -} +use crate::mir::builder::control_flow::joinir::patterns::common::{ + finalize_extract, ContractViolation, ExtractDecision, +}; /// Phase 256 P0: Split/Scan pattern parts extractor /// @@ -89,24 +66,18 @@ pub(crate) fn extract_split_scan_plan( use crate::mir::builder::control_flow::plan::{DomainPlan, SplitScanPlan}; // Try to extract using existing implementation - match extract_split_scan_parts(condition, body, &[]) { - Ok(parts) => { - let plan = SplitScanPlan { - s_var: parts.s_var, - sep_var: parts.sep_var, - result_var: parts.result_var, - i_var: parts.i_var, - start_var: parts.start_var, - }; - Ok(Some(DomainPlan::SplitScan(plan))) - } - Err(SplitScanExtractError::NotApplicable(_)) => Ok(None), // Pattern doesn't match - Err(SplitScanExtractError::Contract(err)) => Err(error_tags::freeze_with_hint( - "phase29ab/pattern7/contract", - &err.msg, - &err.hint, - )), - } + let parts = finalize_extract( + extract_split_scan_parts(condition, body, &[]), + "phase29ab/pattern7/contract", + )?; + let plan = parts.map(|parts| SplitScanPlan { + s_var: parts.s_var, + sep_var: parts.sep_var, + result_var: parts.result_var, + i_var: parts.i_var, + start_var: parts.start_var, + }); + Ok(plan.map(DomainPlan::SplitScan)) } /// Phase 256 P0: Extract SplitScanParts from AST @@ -119,17 +90,19 @@ fn extract_split_scan_parts( condition: &ASTNode, body: &[ASTNode], post_loop_code: &[ASTNode], -) -> Result { +) -> ExtractDecision { // Step 1: Extract variables from loop condition // Expected: i <= s.length() - separator.length() - let (i_var, s_var, sep_var) = extract_loop_condition_vars(condition) - .map_err(SplitScanExtractError::NotApplicable)?; + let (i_var, s_var, sep_var) = match extract_loop_condition_vars(condition) { + Ok(values) => values, + Err(_) => return ExtractDecision::NotApplicable, + }; // Step 2: Find the if statement in loop body - let if_stmt = body - .iter() - .find(|stmt| matches!(stmt, ASTNode::If { .. })) - .ok_or_else(|| SplitScanExtractError::not_applicable("extract_split_scan_parts: No if statement found in loop body"))?; + let if_stmt = match body.iter().find(|stmt| matches!(stmt, ASTNode::If { .. })) { + Some(stmt) => stmt, + None => return ExtractDecision::NotApplicable, + }; let (match_if_cond_ast, then_body, else_body) = match if_stmt { ASTNode::If { @@ -138,57 +111,68 @@ fn extract_split_scan_parts( else_body, .. } => (condition.as_ref().clone(), then_body, else_body), - _ => return Err(SplitScanExtractError::not_applicable("extract_split_scan_parts: Invalid if statement")), + _ => return ExtractDecision::NotApplicable, }; + if let Err(err) = validate_separator_literal_len(&match_if_cond_ast) { + return ExtractDecision::Contract(err); + } + // Step 3: Extract push operation from then branch - let then_push_ast = then_body + let then_push_ast = match then_body .iter() .find(|stmt| matches!(stmt, ASTNode::MethodCall { method, .. } if method == "push")) - .ok_or_else(|| SplitScanExtractError::not_applicable("extract_split_scan_parts: No push() found in then branch"))? - .clone(); + { + Some(stmt) => stmt.clone(), + None => return ExtractDecision::NotApplicable, + }; // Step 4: Extract start assignment (start = i + separator.length()) - let then_start_next_ast = then_body + let then_start_next_ast = match then_body .iter() .find(|stmt| { matches!(stmt, ASTNode::Assignment { target, .. } if { matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "start") }) - }) - .ok_or_else(|| SplitScanExtractError::not_applicable("extract_split_scan_parts: No 'start = ...' assignment in then branch"))? - .clone(); + }) { + Some(stmt) => stmt.clone(), + None => return ExtractDecision::NotApplicable, + }; // Step 5: Extract i assignment (i = start or i = variable) - let then_i_next_ast = then_body - .iter() - .find(|stmt| { - matches!(stmt, ASTNode::Assignment { target, value, .. } if { - matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "i") - && matches!(value.as_ref(), ASTNode::Variable { .. }) - }) + let then_i_next_ast = match then_body.iter().find(|stmt| { + matches!(stmt, ASTNode::Assignment { target, value, .. } if { + matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "i") + && matches!(value.as_ref(), ASTNode::Variable { .. }) }) - .ok_or_else(|| SplitScanExtractError::not_applicable("extract_split_scan_parts: No 'i = variable' assignment in then branch"))? - .clone(); + }) { + Some(stmt) => stmt.clone(), + None => return ExtractDecision::NotApplicable, + }; // Step 6: Extract else branch assignment (i = i + 1) let else_i_next_ast = if let Some(else_statements) = else_body { - else_statements - .iter() - .find(|stmt| { - matches!(stmt, ASTNode::Assignment { target, .. } if { - matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "i") - }) + match else_statements.iter().find(|stmt| { + matches!(stmt, ASTNode::Assignment { target, .. } if { + matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "i") }) - .ok_or_else(|| SplitScanExtractError::not_applicable("extract_split_scan_parts: No 'i = ...' assignment in else branch"))? - .clone() + }) { + Some(stmt) => stmt.clone(), + None => return ExtractDecision::NotApplicable, + } } else { - return Err(SplitScanExtractError::not_applicable("extract_split_scan_parts: No else branch found")); + return ExtractDecision::NotApplicable; }; - validate_start_update(&then_start_next_ast, &i_var, &sep_var)?; - validate_i_set_to_start(&then_i_next_ast, &i_var)?; - validate_i_increment_by_one(&else_i_next_ast, &i_var)?; + if let Err(err) = validate_start_update(&then_start_next_ast, &i_var, &sep_var) { + return ExtractDecision::Contract(err); + } + if let Err(err) = validate_i_set_to_start(&then_i_next_ast, &i_var) { + return ExtractDecision::Contract(err); + } + if let Err(err) = validate_i_increment_by_one(&else_i_next_ast, &i_var) { + return ExtractDecision::Contract(err); + } // Step 7: Extract post-loop push (result.push(...)) let post_push_ast = post_loop_code @@ -197,10 +181,12 @@ fn extract_split_scan_parts( .cloned(); // Step 8: Extract result variable from push statements - let result_var = extract_result_var(&then_push_ast) - .map_err(SplitScanExtractError::NotApplicable)?; + let result_var = match extract_result_var(&then_push_ast) { + Ok(value) => value, + Err(_) => return ExtractDecision::NotApplicable, + }; - Ok(SplitScanParts { + ExtractDecision::Match(SplitScanParts { s_var, sep_var, result_var, @@ -292,14 +278,14 @@ fn validate_start_update( assign: &ASTNode, i_var: &str, sep_var: &str, -) -> Result<(), SplitScanExtractError> { +) -> Result<(), ContractViolation> { use crate::ast::BinaryOperator; let hint = "use `start = i + separator.length()` in the then-branch"; match assign { ASTNode::Assignment { target, value, .. } => { if !matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "start") { - return Err(SplitScanExtractError::contract( + return Err(ContractViolation::new( "split scan contract: start target must be `start`", hint, )); @@ -322,26 +308,26 @@ fn validate_start_update( if left_ok && right_ok { Ok(()) } else { - Err(SplitScanExtractError::contract( + Err(ContractViolation::new( "split scan contract: start update must be `i + separator.length()`", hint, )) } } - _ => Err(SplitScanExtractError::contract( + _ => Err(ContractViolation::new( "split scan contract: start update must be `i + separator.length()`", hint, )), } } - _ => Err(SplitScanExtractError::contract( + _ => Err(ContractViolation::new( "split scan contract: expected start assignment", hint, )), } } -fn validate_i_set_to_start(assign: &ASTNode, i_var: &str) -> Result<(), SplitScanExtractError> { +fn validate_i_set_to_start(assign: &ASTNode, i_var: &str) -> Result<(), ContractViolation> { let hint = "use `i = start` in the then-branch"; match assign { ASTNode::Assignment { target, value, .. } => { @@ -350,13 +336,13 @@ fn validate_i_set_to_start(assign: &ASTNode, i_var: &str) -> Result<(), SplitSca if target_ok && value_ok { Ok(()) } else { - Err(SplitScanExtractError::contract( + Err(ContractViolation::new( "split scan contract: then i update must be `i = start`", hint, )) } } - _ => Err(SplitScanExtractError::contract( + _ => Err(ContractViolation::new( "split scan contract: expected then i assignment", hint, )), @@ -366,7 +352,7 @@ fn validate_i_set_to_start(assign: &ASTNode, i_var: &str) -> Result<(), SplitSca fn validate_i_increment_by_one( assign: &ASTNode, i_var: &str, -) -> Result<(), SplitScanExtractError> { +) -> Result<(), ContractViolation> { use crate::ast::{BinaryOperator, LiteralValue}; let hint = "use `i = i + 1` in the else-branch"; @@ -385,19 +371,51 @@ fn validate_i_increment_by_one( if target_ok && value_ok { Ok(()) } else { - Err(SplitScanExtractError::contract( + Err(ContractViolation::new( "split scan contract: else i update must be `i = i + 1`", hint, )) } } - _ => Err(SplitScanExtractError::contract( + _ => Err(ContractViolation::new( "split scan contract: expected else i assignment", hint, )), } } +fn validate_separator_literal_len(cond: &ASTNode) -> Result<(), ContractViolation> { + use crate::ast::{BinaryOperator, LiteralValue}; + + let hint = "use a 1-char separator (e.g. \",\") or avoid split-scan patterns"; + let (left, right) = match cond { + ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left, + right, + .. + } => (left.as_ref(), right.as_ref()), + _ => return Ok(()), + }; + + let literal = match (left, right) { + (ASTNode::Literal { value: LiteralValue::String(s), .. }, _) => Some(s), + (_, ASTNode::Literal { value: LiteralValue::String(s), .. }) => Some(s), + _ => None, + }; + + if let Some(value) = literal { + if value.chars().count() != 1 { + return Err(ContractViolation::new( + "split scan contract: separator must be 1 char (P0)", + hint, + )); + } + } + + Ok(()) +} + /// Extract result variable name from push call /// Expected: result.push(...) fn extract_result_var(push_stmt: &ASTNode) -> Result { diff --git a/src/mir/builder/control_flow/plan/normalizer/common.rs b/src/mir/builder/control_flow/plan/normalizer/common.rs new file mode 100644 index 00000000..c1e3168e --- /dev/null +++ b/src/mir/builder/control_flow/plan/normalizer/common.rs @@ -0,0 +1,62 @@ +use crate::mir::basic_block::EdgeArgs; +use crate::mir::builder::control_flow::edgecfg::api::{compose, EdgeStub, ExitKind, Frag}; +use crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout; +use std::collections::BTreeMap; + +pub(super) fn empty_args() -> EdgeArgs { + EdgeArgs { + layout: JumpArgsLayout::CarriersOnly, + values: vec![], + } +} + +pub(super) fn normal_exit_frag( + entry: crate::mir::BasicBlockId, + args: &EdgeArgs, +) -> Frag { + let mut exits = BTreeMap::new(); + exits.insert( + ExitKind::Normal, + vec![EdgeStub { + from: entry, + kind: ExitKind::Normal, + target: None, + args: args.clone(), + }], + ); + Frag { + entry, + exits, + wires: vec![], + branches: vec![], + } +} + +pub(super) fn build_body_if_frag( + body_bb: crate::mir::BasicBlockId, + cond: crate::mir::ValueId, + then_bb: crate::mir::BasicBlockId, + else_bb: crate::mir::BasicBlockId, + step_bb: crate::mir::BasicBlockId, + empty_args: &EdgeArgs, +) -> Frag { + let then_frag = normal_exit_frag(then_bb, empty_args); + let else_frag = normal_exit_frag(else_bb, empty_args); + + let step_frag = Frag { + entry: step_bb, + exits: BTreeMap::new(), + wires: vec![], + branches: vec![], + }; + + compose::if_( + body_bb, + cond, + then_frag, + empty_args.clone(), + else_frag, + empty_args.clone(), + step_frag, + ) +} diff --git a/src/mir/builder/control_flow/plan/normalizer/mod.rs b/src/mir/builder/control_flow/plan/normalizer/mod.rs index affb12cc..66e46927 100644 --- a/src/mir/builder/control_flow/plan/normalizer/mod.rs +++ b/src/mir/builder/control_flow/plan/normalizer/mod.rs @@ -21,6 +21,7 @@ mod pattern8_bool_predicate_scan; mod pattern9_accum_const_loop; mod pattern_scan_with_init; mod pattern_split_scan; +mod common; use super::{ CoreEffectPlan, CoreLoopPlan, CorePhiInfo, CorePlan, DomainPlan, Pattern1SimpleWhilePlan, diff --git a/src/mir/builder/control_flow/plan/normalizer/pattern_split_scan.rs b/src/mir/builder/control_flow/plan/normalizer/pattern_split_scan.rs index 77fd929b..0582e60c 100644 --- a/src/mir/builder/control_flow/plan/normalizer/pattern_split_scan.rs +++ b/src/mir/builder/control_flow/plan/normalizer/pattern_split_scan.rs @@ -1,11 +1,10 @@ use super::{CoreEffectPlan, CoreLoopPlan, CorePhiInfo, CorePlan, SplitScanPlan}; -use crate::mir::basic_block::EdgeArgs; use crate::mir::builder::control_flow::edgecfg::api::{BranchStub, EdgeStub, ExitKind, Frag}; use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext; use crate::mir::builder::MirBuilder; -use crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout; use crate::mir::{BinaryOp, CompareOp, ConstValue, Effect, EffectMask, MirType}; use std::collections::BTreeMap; +use super::common::{build_body_if_frag, empty_args}; impl super::PlanNormalizer { /// SplitScan → CorePlan 変換 @@ -21,8 +20,6 @@ impl super::PlanNormalizer { ctx: &LoopPatternContext, ) -> Result { use crate::mir::builder::control_flow::joinir::trace; - use crate::mir::builder::control_flow::edgecfg::api::compose; - let trace_logger = trace::trace(); let debug = ctx.debug; @@ -279,61 +276,9 @@ impl super::PlanNormalizer { ]; // Step 9.5: Build Frags for compose::if_() (Phase 281 P0) - let empty_args = EdgeArgs { - layout: JumpArgsLayout::CarriersOnly, - values: vec![], - }; + let empty_args = empty_args(); - let mut then_exits = BTreeMap::new(); - then_exits.insert( - ExitKind::Normal, - vec![EdgeStub { - from: then_bb, - kind: ExitKind::Normal, - target: None, - args: empty_args.clone(), - }], - ); - let then_frag = Frag { - entry: then_bb, - exits: then_exits, - wires: vec![], - branches: vec![], - }; - - let mut else_exits = BTreeMap::new(); - else_exits.insert( - ExitKind::Normal, - vec![EdgeStub { - from: else_bb, - kind: ExitKind::Normal, - target: None, - args: empty_args.clone(), - }], - ); - let else_frag = Frag { - entry: else_bb, - exits: else_exits, - wires: vec![], - branches: vec![], - }; - - let step_frag = Frag { - entry: step_bb, - exits: BTreeMap::new(), - wires: vec![], - branches: vec![], - }; - - let body_if_frag = compose::if_( - body_bb, - cond_match, - then_frag, - empty_args.clone(), - else_frag, - empty_args.clone(), - step_frag, - ); + let body_if_frag = build_body_if_frag(body_bb, cond_match, then_bb, else_bb, step_bb, &empty_args); // Step 10: Build block_effects let block_effects = vec![