feat(joinir): Phase 286 P2.3 + Phase 287 - Pattern9 Plan化 + Router table-driven
## Phase 286 P2.3: Pattern9 AccumConstLoop Plan化 PoC - DomainPlan::Pattern9AccumConstLoop 追加 - PlanNormalizer::normalize_pattern9_accum_const_loop() 実装 - PHI 2本(loop_var, acc_var) - const/var 両方 OK(sum = sum + 1 または sum = sum + i) - Pattern9 は Pattern1 より優先(より具体的なパターン) - Integration test: phase286_pattern9_frag_poc PASS (return: 3) - Regression: quick 154 PASS ## Phase 287: Router table-driven Plan extraction - PLAN_EXTRACTORS static table で Pattern6/7/4/9/1 を統一管理 - PlanExtractorEntry/PlanExtractorVariant 構造体追加 - try_plan_extractors() で ~100行 → 3行に集約 - メンテナンス性向上(新 Pattern 追加はテーブル1行追加のみ) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -15,8 +15,8 @@ Scope: Repo root の旧リンク互換。現行の入口は `docs/development/cu
|
||||
**2025-12-26: Phase 286 P2.2 COMPLETE (Hygiene: extractor重複排除 + router小整理)**
|
||||
Pattern1/Pattern4 の Plan/Frag PoC 完了後、extractor の `extract_loop_increment_plan` を `common_helpers.rs` に統一、router の 3行パターン(normalize→verify→lower)を `lower_via_plan()` ヘルパーで共通化。~65行削減、quick 154 PASS 維持。
|
||||
|
||||
**2025-12-26: Phase 286 P2.3 準備中 (Pattern9 Plan化 PoC 準備)**
|
||||
次のPoC対象は Pattern9 (AccumConstLoop) に決定。橋渡しパターンで粒度が小さく、Plan化の流れが確立しているため早期完了が期待できる。P2/P2.1 と同じ戦略:extract PoC サブセット → normalize CorePlan → router 統合。
|
||||
**2025-12-26: Phase 286 P2.3 COMPLETE (Pattern9 AccumConstLoop Plan化 PoC)**
|
||||
Pattern9 (AccumConstLoop) を Plan/Frag SSOT に移行完了。PHI 2本(loop_var, acc_var)、const/var 両方 OK。Pattern9 は Pattern1 より先にチェック(より具体的なパターン)。quick 154 PASS 維持。
|
||||
|
||||
- 現行の入口: `docs/development/current/main/10-Now.md`
|
||||
- Phase 286 詳細: `docs/development/current/main/phases/phase-286/README.md`
|
||||
|
||||
17
apps/tests/phase286_pattern9_frag_poc.hako
Normal file
17
apps/tests/phase286_pattern9_frag_poc.hako
Normal file
@ -0,0 +1,17 @@
|
||||
// Phase 286 P2.3: Pattern9 → Frag PoC minimal fixture
|
||||
// Expected: return 3 (sum = 0 + 1 + 1 + 1 = 3)
|
||||
// Design: Pattern1 + carrier1本 + const加算の最小固定
|
||||
|
||||
static box Main {
|
||||
main() {
|
||||
local i
|
||||
local sum
|
||||
i = 0
|
||||
sum = 0
|
||||
loop(i < 3) {
|
||||
sum = sum + 1
|
||||
i = i + 1
|
||||
}
|
||||
return sum
|
||||
}
|
||||
}
|
||||
@ -8,12 +8,12 @@ Related:
|
||||
|
||||
## 直近(JoinIR/selfhost)
|
||||
|
||||
- **Phase 288(✅ P0–P3 complete): REPL mode**
|
||||
- **Phase 288(✅ P0–P3 + 288.1 complete): REPL mode**
|
||||
- 入口: `docs/development/current/main/phases/phase-288/README.md`
|
||||
- SSOT: `docs/reference/language/repl.md`
|
||||
- 次: Phase 288.1(session persistence / auto-display) `docs/development/current/main/phases/phase-288/P1-INSTRUCTIONS.md`
|
||||
- 次: Phase 288.2+(任意): REPL UX improvements(history / multiline / load 等)
|
||||
|
||||
- **Phase 287(active, stabilization): ビルド/テスト軽量化(quick が重すぎる)**
|
||||
- **Phase 287(✅ complete): ビルド/テスト軽量化(quick gate の責務分離)**
|
||||
- 目的: `tools/smokes/v2/run.sh --profile quick` を 45秒以内(目安)へ戻し、開発サイクルを軽くする
|
||||
- 入口: `docs/development/current/main/phases/phase-287/README.md`
|
||||
- 手順: `docs/development/current/main/phases/phase-287/P1-INSTRUCTIONS.md`
|
||||
@ -64,6 +64,7 @@ Related:
|
||||
- ねらい: `return/break/continue` のような “大きな出口語彙” の実装場所が揺れない状態にする
|
||||
- 入口: `docs/development/current/main/phases/phase-286/README.md`
|
||||
- P0(docs-only): `docs/development/current/main/phases/phase-286/P0-INSTRUCTIONS.md`
|
||||
- 状況: P2(Pattern4 PoC)/P2.1(Pattern1 PoC)完了。次は P2.2(hygiene)→ P2.3(Pattern2 Plan化の再開条件が揃えば)
|
||||
- SSOT:
|
||||
- Plan/Frag: `compose::*` + `emit_frag()`(Phase 280/281)
|
||||
- JoinIR line 共通入口: `src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs`
|
||||
|
||||
@ -900,6 +900,11 @@ Pattern6/7 は **Plan line(Extractor → Normalizer → Verifier → Lowerer
|
||||
- 許可: `UpdateRhs::Const`, `UpdateRhs::Variable`, `UpdateRhs::StringLiteral`
|
||||
- 拒否: `UpdateRhs::Other`(method call / nested BinOp 等の複雑パターンのみ)
|
||||
- Pattern2/4 の can_lower() で選別、carrier_update_emitter.rs で JoinIR 生成
|
||||
- 現状制約(重要 / PoC の前提):
|
||||
- Pattern2 の「出口で値が持ち帰られる変数」は、基本的に **carrier として認識されたもの**のみが `exit_bindings` 経由で再接続される。
|
||||
- 現状の carrier 認識は “mutable-acc” 形(例: `result = result + rhs`)を中心に成立しており、`result = 42` のような **direct assignment** は carrier として扱われないケースがある。
|
||||
- その場合、break 後に `return result` しても「ループ前の値」が返りうる(再接続されないため)。
|
||||
- Phase 286 P2 の PoC fixture は、この制約を踏んで **mutable-acc 形で固定**する(direct assignment 対応は別フェーズで箱化して拡張する)。
|
||||
|
||||
- **ExitMeta / JoinFragmentMeta**
|
||||
- ファイル: `carrier_info.rs`
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Phase 286: JoinIR Line Absorption(JoinIR→CorePlan/Frag 収束)
|
||||
|
||||
Status: In Progress (P0, P1, P2, P3, 286C-2 COMPLETE)
|
||||
Status: In Progress (P0, P1, P2, P2.1, P2.2, P2.3, P3, 286C-2 COMPLETE)
|
||||
|
||||
## Goal
|
||||
|
||||
@ -147,6 +147,33 @@ Phase 286 では JoinIR line を “第2の lowerer” として放置せず、*
|
||||
- Regression test: quick smoke 154 PASS
|
||||
- Pattern1 PoC: PASS, Pattern4 PoC: PASS
|
||||
|
||||
### P2.3 (Pattern9 AccumConstLoop Plan化 PoC) ✅ COMPLETE (2025-12-26)
|
||||
|
||||
**完了内容**:
|
||||
- **Pattern9 (AccumConstLoop) を Plan/Frag SSOT に移行**
|
||||
- DomainPlan::Pattern9AccumConstLoop 追加
|
||||
- PlanNormalizer::normalize_pattern9_accum_const_loop() 実装(PHI 2本: loop_var, acc_var)
|
||||
- Router integration(Plan line routing → legacy fallback)
|
||||
- Pattern9 は Pattern1 より優先(より具体的なパターン)
|
||||
|
||||
**設計決定**:
|
||||
- **PoC は const/var 両方 OK**: `sum = sum + 1`(定数)または `sum = sum + i`(変数)
|
||||
- **本体の順序固定**: 1行目=累積更新, 2行目=ループ変数更新
|
||||
- **CFG 構造**: Pattern1 と同じ骨格、PHI 2本(i_current, sum_current)
|
||||
|
||||
**成果物**:
|
||||
- `apps/tests/phase286_pattern9_frag_poc.hako` (最小fixture: const accumulation)
|
||||
- `tools/smokes/v2/profiles/integration/apps/phase286_pattern9_frag_poc.sh` (integration smoke)
|
||||
- `src/mir/builder/control_flow/plan/mod.rs` (Pattern9AccumConstLoopPlan + DomainPlan variant)
|
||||
- `src/mir/builder/control_flow/joinir/patterns/extractors/pattern9.rs` (extract_pattern9_plan() 新規)
|
||||
- `src/mir/builder/control_flow/joinir/patterns/extractors/mod.rs` (pattern9 モジュール追加)
|
||||
- `src/mir/builder/control_flow/plan/normalizer.rs` (normalize_pattern9_accum_const_loop())
|
||||
- `src/mir/builder/control_flow/joinir/patterns/router.rs` (Pattern9 Plan line routing)
|
||||
|
||||
**検証結果**:
|
||||
- Integration test: `phase286_pattern9_frag_poc` PASS (return: 3)
|
||||
- Regression test: quick smoke 154 PASS, 0 FAILED
|
||||
|
||||
### P3 (error context enrichment) ✅ COMPLETE (2025-12-25)
|
||||
|
||||
**完了内容**:
|
||||
|
||||
@ -25,6 +25,7 @@ pub(crate) mod pattern2; // Phase 282 P4: Pattern2 extraction
|
||||
pub(crate) mod pattern3; // Phase 282 P5: Pattern3 extraction
|
||||
pub(crate) mod pattern4; // Phase 282 P6: Pattern4 extraction
|
||||
pub(crate) mod pattern5; // Phase 282 P7: Pattern5 extraction
|
||||
pub(crate) mod pattern9; // Phase 286 P2.3: Pattern9 Plan extraction
|
||||
|
||||
// Phase 282 P9a: Common extraction helpers
|
||||
// Provides shared utilities for Pattern1, 2, 4, 5 (~400 lines reduction):
|
||||
|
||||
@ -0,0 +1,318 @@
|
||||
//! Phase 286 P2.3: Pattern9 (AccumConstLoop) Extraction
|
||||
//!
|
||||
//! Minimal subset extractor for Pattern9 Plan line.
|
||||
//!
|
||||
//! # Supported subset (PoC safety)
|
||||
//!
|
||||
//! - Loop condition: `<var> < <int_lit>`
|
||||
//! - Body: 2 assignments only (順序固定)
|
||||
//! 1. `<acc_var> = <acc_var> + <expr>` (const OR var accumulation)
|
||||
//! 2. `<loop_var> = <loop_var> + <int_lit>` (increment)
|
||||
//! - No break/continue/if/return
|
||||
//!
|
||||
//! Returns Ok(None) for unsupported patterns → legacy fallback
|
||||
|
||||
use crate::ast::{ASTNode, BinaryOperator, LiteralValue};
|
||||
use crate::mir::builder::control_flow::plan::{DomainPlan, Pattern9AccumConstLoopPlan};
|
||||
|
||||
// Phase 286 P2.3: Use common_helpers
|
||||
use super::common_helpers::{extract_loop_increment_plan, has_control_flow_statement};
|
||||
|
||||
/// Phase 286 P2.3: Minimal subset extractor for Pattern9 Plan line
|
||||
///
|
||||
/// # Detection Criteria
|
||||
///
|
||||
/// 1. **Condition**: `<var> < <int_lit>`
|
||||
/// 2. **Body**: Exactly 2 assignments in fixed order
|
||||
/// - 1st: `<acc_var> = <acc_var> + <expr>` (accumulation)
|
||||
/// - 2nd: `<loop_var> = <loop_var> + <int_lit>` (increment)
|
||||
/// 3. **No control flow**: No break/continue/if-else
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// - `Ok(Some(plan))`: Pattern9 match confirmed
|
||||
/// - `Ok(None)`: Not Pattern9 (構造不一致 or unsupported)
|
||||
/// - `Err(msg)`: Logic bug (malformed AST)
|
||||
pub(crate) fn extract_pattern9_plan(
|
||||
condition: &ASTNode,
|
||||
body: &[ASTNode],
|
||||
) -> Result<Option<DomainPlan>, String> {
|
||||
// Step 1: Validate loop condition is `<var> < <int_lit>`
|
||||
let loop_var = match validate_loop_condition_plan(condition) {
|
||||
Some(var) => var,
|
||||
None => return Ok(None), // Unsupported condition format
|
||||
};
|
||||
|
||||
// Step 2: Reject control flow (break/continue/if-else)
|
||||
if has_control_flow_statement(body) {
|
||||
return Ok(None); // Has break/continue → Not Pattern9 for Plan line
|
||||
}
|
||||
|
||||
// Step 3: Validate body has exactly 2 assignments
|
||||
if body.len() != 2 {
|
||||
return Ok(None); // Must be exactly 2 statements
|
||||
}
|
||||
|
||||
// Step 4: Extract accumulator update (1st statement)
|
||||
// Supports: `<acc_var> = <acc_var> + <expr>` (const OR var)
|
||||
let (acc_var, acc_update) = match extract_accum_update(&body[0], &loop_var) {
|
||||
Some((var, update)) => (var, update),
|
||||
None => return Ok(None), // Not a valid accumulator update
|
||||
};
|
||||
|
||||
// Step 5: Extract loop increment (2nd statement)
|
||||
// Uses common_helpers::extract_loop_increment_plan
|
||||
let loop_increment = match extract_loop_increment_plan(&body[1..], &loop_var)? {
|
||||
Some(inc) => inc,
|
||||
None => return Ok(None), // No loop increment found
|
||||
};
|
||||
|
||||
Ok(Some(DomainPlan::Pattern9AccumConstLoop(Pattern9AccumConstLoopPlan {
|
||||
loop_var,
|
||||
acc_var,
|
||||
condition: condition.clone(),
|
||||
acc_update,
|
||||
loop_increment,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Validate loop condition: supports `<var> < <int_lit>` only
|
||||
fn validate_loop_condition_plan(cond: &ASTNode) -> Option<String> {
|
||||
if let ASTNode::BinaryOp { operator, left, right, .. } = cond {
|
||||
if !matches!(operator, BinaryOperator::Less) {
|
||||
return None; // Only < supported for PoC
|
||||
}
|
||||
|
||||
// Left must be a variable
|
||||
let var_name = if let ASTNode::Variable { name, .. } = left.as_ref() {
|
||||
name.clone()
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Right must be integer literal
|
||||
if !matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(_), .. }) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(var_name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract accumulator update from assignment
|
||||
///
|
||||
/// Supports: `<acc_var> = <acc_var> + <expr>` where <expr> is const OR var
|
||||
///
|
||||
/// Returns: Some((acc_var, update_expr)) or None
|
||||
fn extract_accum_update(stmt: &ASTNode, loop_var: &str) -> Option<(String, ASTNode)> {
|
||||
if let ASTNode::Assignment { target, value, .. } = stmt {
|
||||
// target must be a variable (different from loop_var)
|
||||
if let ASTNode::Variable { name: target_var, .. } = target.as_ref() {
|
||||
if target_var == loop_var {
|
||||
return None; // This is the loop increment, not accumulator
|
||||
}
|
||||
|
||||
// value must be `<acc_var> + <expr>`
|
||||
if let ASTNode::BinaryOp { operator, left, right, .. } = value.as_ref() {
|
||||
if !matches!(operator, BinaryOperator::Add) {
|
||||
return None; // Only + supported
|
||||
}
|
||||
|
||||
// Left must be same as target (acc_var = acc_var + ...)
|
||||
if let ASTNode::Variable { name: left_var, .. } = left.as_ref() {
|
||||
if left_var != target_var {
|
||||
return None; // Not a self-update
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Right can be const OR var (ChatGPT feedback: both OK)
|
||||
// Accept: Literal (const) or Variable (var)
|
||||
match right.as_ref() {
|
||||
ASTNode::Literal { value: LiteralValue::Integer(_), .. } => {
|
||||
// const accumulation: sum = sum + 1
|
||||
return Some((target_var.clone(), value.as_ref().clone()));
|
||||
}
|
||||
ASTNode::Variable { .. } => {
|
||||
// var accumulation: sum = sum + i
|
||||
return Some((target_var.clone(), value.as_ref().clone()));
|
||||
}
|
||||
_ => return None, // Complex expression not supported
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::Span;
|
||||
|
||||
fn make_condition(var: &str, bound: i64) -> ASTNode {
|
||||
ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: var.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(bound),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_accum_const_update(acc_var: &str, val: i64) -> ASTNode {
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: acc_var.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: acc_var.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(val),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_accum_var_update(acc_var: &str, rhs_var: &str) -> ASTNode {
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: acc_var.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: acc_var.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Variable {
|
||||
name: rhs_var.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_loop_increment(var: &str, step: i64) -> ASTNode {
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: var.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: var.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(step),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pattern9_const_accum_success() {
|
||||
// loop(i < 3) { sum = sum + 1; i = i + 1 }
|
||||
let condition = make_condition("i", 3);
|
||||
let body = vec![
|
||||
make_accum_const_update("sum", 1),
|
||||
make_loop_increment("i", 1),
|
||||
];
|
||||
|
||||
let result = extract_pattern9_plan(&condition, &body);
|
||||
assert!(result.is_ok());
|
||||
let plan = result.unwrap();
|
||||
assert!(plan.is_some());
|
||||
|
||||
if let Some(DomainPlan::Pattern9AccumConstLoop(p)) = plan {
|
||||
assert_eq!(p.loop_var, "i");
|
||||
assert_eq!(p.acc_var, "sum");
|
||||
} else {
|
||||
panic!("Expected Pattern9AccumConstLoop");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pattern9_var_accum_success() {
|
||||
// loop(i < 3) { sum = sum + i; i = i + 1 }
|
||||
let condition = make_condition("i", 3);
|
||||
let body = vec![
|
||||
make_accum_var_update("sum", "i"),
|
||||
make_loop_increment("i", 1),
|
||||
];
|
||||
|
||||
let result = extract_pattern9_plan(&condition, &body);
|
||||
assert!(result.is_ok());
|
||||
let plan = result.unwrap();
|
||||
assert!(plan.is_some());
|
||||
|
||||
if let Some(DomainPlan::Pattern9AccumConstLoop(p)) = plan {
|
||||
assert_eq!(p.loop_var, "i");
|
||||
assert_eq!(p.acc_var, "sum");
|
||||
} else {
|
||||
panic!("Expected Pattern9AccumConstLoop");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pattern9_wrong_order_returns_none() {
|
||||
// loop(i < 3) { i = i + 1; sum = sum + 1 } <- wrong order
|
||||
let condition = make_condition("i", 3);
|
||||
let body = vec![
|
||||
make_loop_increment("i", 1),
|
||||
make_accum_const_update("sum", 1),
|
||||
];
|
||||
|
||||
let result = extract_pattern9_plan(&condition, &body);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_none()); // Wrong order → None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pattern9_with_break_returns_none() {
|
||||
let condition = make_condition("i", 3);
|
||||
let body = vec![
|
||||
ASTNode::Break { span: Span::unknown() },
|
||||
make_accum_const_update("sum", 1),
|
||||
make_loop_increment("i", 1),
|
||||
];
|
||||
|
||||
let result = extract_pattern9_plan(&condition, &body);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_none()); // Has break → None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pattern9_single_stmt_returns_none() {
|
||||
let condition = make_condition("i", 3);
|
||||
let body = vec![make_loop_increment("i", 1)];
|
||||
|
||||
let result = extract_pattern9_plan(&condition, &body);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_none()); // Only 1 statement → None
|
||||
}
|
||||
}
|
||||
@ -153,6 +153,109 @@ fn lower_via_plan(
|
||||
PlanLowerer::lower(builder, core_plan, ctx)
|
||||
}
|
||||
|
||||
/// Phase 287: Plan extractor function type for routing table
|
||||
///
|
||||
/// Extractors have two signatures:
|
||||
/// - Simple: (condition, body) - Pattern1/4/9
|
||||
/// - WithFnBody: (condition, body, fn_body) - Pattern6/7
|
||||
type SimplePlanExtractor = fn(&ASTNode, &[ASTNode]) -> Result<Option<crate::mir::builder::control_flow::plan::DomainPlan>, String>;
|
||||
|
||||
/// Phase 287: Plan extractor table entry
|
||||
struct PlanExtractorEntry {
|
||||
/// Pattern name for debug logging
|
||||
name: &'static str,
|
||||
|
||||
/// Extractor function
|
||||
extractor: PlanExtractorVariant,
|
||||
}
|
||||
|
||||
/// Phase 287: Extractor function variant (handles different signatures)
|
||||
enum PlanExtractorVariant {
|
||||
/// Simple extractor: (condition, body)
|
||||
Simple(SimplePlanExtractor),
|
||||
|
||||
/// Extractor with fn_body: (condition, body, fn_body) - Pattern6
|
||||
WithFnBody(fn(&ASTNode, &[ASTNode], Option<&[ASTNode]>) -> Result<Option<crate::mir::builder::control_flow::plan::DomainPlan>, String>),
|
||||
|
||||
/// Extractor with post_loop_code: (condition, body, post_loop_code) - Pattern7
|
||||
WithPostLoop(fn(&ASTNode, &[ASTNode], &[ASTNode]) -> Result<Option<crate::mir::builder::control_flow::plan::DomainPlan>, String>),
|
||||
}
|
||||
|
||||
/// Phase 287: Plan extractor routing table (SSOT)
|
||||
///
|
||||
/// Order is important: more specific patterns first
|
||||
/// - Pattern6/7: Need fn_body for capture analysis
|
||||
/// - Pattern4: Continue loop (more specific than Pattern1)
|
||||
/// - Pattern9: Accum const loop (2 carriers, more specific than Pattern1)
|
||||
/// - Pattern1: Simple while (fallback)
|
||||
static PLAN_EXTRACTORS: &[PlanExtractorEntry] = &[
|
||||
PlanExtractorEntry {
|
||||
name: "Pattern6_ScanWithInit (Phase 273)",
|
||||
extractor: PlanExtractorVariant::WithFnBody(super::pattern6_scan_with_init::extract_scan_with_init_plan),
|
||||
},
|
||||
PlanExtractorEntry {
|
||||
name: "Pattern7_SplitScan (Phase 273)",
|
||||
extractor: PlanExtractorVariant::WithPostLoop(super::pattern7_split_scan::extract_split_scan_plan),
|
||||
},
|
||||
PlanExtractorEntry {
|
||||
name: "Pattern4_Continue (Phase 286 P2)",
|
||||
extractor: PlanExtractorVariant::Simple(super::extractors::pattern4::extract_pattern4_plan),
|
||||
},
|
||||
PlanExtractorEntry {
|
||||
name: "Pattern9_AccumConstLoop (Phase 286 P2.3)",
|
||||
extractor: PlanExtractorVariant::Simple(super::extractors::pattern9::extract_pattern9_plan),
|
||||
},
|
||||
PlanExtractorEntry {
|
||||
name: "Pattern1_SimpleWhile (Phase 286 P2.1)",
|
||||
extractor: PlanExtractorVariant::Simple(super::extractors::pattern1::extract_pattern1_plan),
|
||||
},
|
||||
];
|
||||
|
||||
/// Phase 287: Try all plan extractors in priority order
|
||||
///
|
||||
/// Returns Ok(Some(value_id)) if any extractor succeeds
|
||||
/// Returns Ok(None) if no extractor matches
|
||||
/// Returns Err if extraction or lowering fails
|
||||
fn try_plan_extractors(
|
||||
builder: &mut MirBuilder,
|
||||
ctx: &LoopPatternContext,
|
||||
) -> Result<Option<ValueId>, String> {
|
||||
use super::super::trace;
|
||||
|
||||
for entry in PLAN_EXTRACTORS {
|
||||
// Try extraction based on variant
|
||||
let plan_opt = match &entry.extractor {
|
||||
PlanExtractorVariant::Simple(extractor) => {
|
||||
extractor(ctx.condition, ctx.body)?
|
||||
}
|
||||
PlanExtractorVariant::WithFnBody(extractor) => {
|
||||
// Pattern6: needs fn_body for capture analysis
|
||||
extractor(ctx.condition, ctx.body, ctx.fn_body)?
|
||||
}
|
||||
PlanExtractorVariant::WithPostLoop(extractor) => {
|
||||
// Pattern7: uses empty slice for post_loop_code (3rd param)
|
||||
extractor(ctx.condition, ctx.body, &[])?
|
||||
}
|
||||
};
|
||||
|
||||
// If extraction succeeded, lower and return
|
||||
if let Some(domain_plan) = plan_opt {
|
||||
let log_msg = format!("route=plan strategy=extract pattern={}", entry.name);
|
||||
trace::trace().pattern("route", &log_msg, true);
|
||||
return lower_via_plan(builder, domain_plan, ctx);
|
||||
}
|
||||
|
||||
// Extraction returned None - try next extractor
|
||||
if ctx.debug {
|
||||
let debug_msg = format!("{} extraction returned None, trying next pattern", entry.name);
|
||||
trace::trace().debug("route", &debug_msg);
|
||||
}
|
||||
}
|
||||
|
||||
// No extractor matched
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Phase 272 P0.2 Refactoring: can_lower() strategy classification
|
||||
///
|
||||
/// Clarifies the two main detection strategies used across patterns:
|
||||
@ -318,98 +421,10 @@ pub(crate) fn route_loop_pattern(
|
||||
) -> Result<Option<ValueId>, String> {
|
||||
use super::super::trace;
|
||||
|
||||
// Phase 273 P1: Try Plan-based Pattern6 first (before table iteration)
|
||||
// Flow: Extract → Normalize → Verify → Lower
|
||||
match super::pattern6_scan_with_init::extract_scan_with_init_plan(
|
||||
ctx.condition,
|
||||
ctx.body,
|
||||
ctx.fn_body,
|
||||
)? {
|
||||
Some(domain_plan) => {
|
||||
// DomainPlan extracted successfully
|
||||
trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern6_ScanWithInit (Phase 273)", true);
|
||||
// Phase 286 P2.2: Use common helper
|
||||
return lower_via_plan(builder, domain_plan, ctx);
|
||||
}
|
||||
None => {
|
||||
// Not Pattern6 - continue to other patterns
|
||||
if ctx.debug {
|
||||
trace::trace().debug(
|
||||
"route",
|
||||
"Pattern6 Plan extraction returned None, trying other patterns",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 273 P2: Try Plan-based Pattern7 (SplitScan)
|
||||
// Flow: Extract → Normalize → Verify → Lower
|
||||
match super::pattern7_split_scan::extract_split_scan_plan(
|
||||
ctx.condition,
|
||||
ctx.body,
|
||||
&[],
|
||||
)? {
|
||||
Some(domain_plan) => {
|
||||
// DomainPlan extracted successfully
|
||||
trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern7_SplitScan (Phase 273)", true);
|
||||
// Phase 286 P2.2: Use common helper
|
||||
return lower_via_plan(builder, domain_plan, ctx);
|
||||
}
|
||||
None => {
|
||||
// Not Pattern7 - continue to other patterns
|
||||
if ctx.debug {
|
||||
trace::trace().debug(
|
||||
"route",
|
||||
"Pattern7 Plan extraction returned None, trying other patterns",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 287: Try all Plan extractors in priority order (Pattern6/7/4/9/1)
|
||||
// This replaces ~100 lines of repetitive extraction blocks
|
||||
if let Some(value_id) = try_plan_extractors(builder, ctx)? {
|
||||
return Ok(Some(value_id));
|
||||
}
|
||||
|
||||
// Phase 183: Route based on pre-classified pattern kind
|
||||
|
||||
@ -49,6 +49,8 @@ pub(in crate::mir::builder) enum DomainPlan {
|
||||
Pattern4Continue(Pattern4ContinuePlan),
|
||||
/// Pattern1: Simple While Loop (Phase 286 P2.1)
|
||||
Pattern1SimpleWhile(Pattern1SimpleWhilePlan),
|
||||
/// Pattern9: Accumulator Const Loop (Phase 286 P2.3)
|
||||
Pattern9AccumConstLoop(Pattern9AccumConstLoopPlan),
|
||||
}
|
||||
|
||||
/// Phase 273 P0: Scan direction for forward/reverse scan
|
||||
@ -134,6 +136,24 @@ pub(in crate::mir::builder) struct Pattern1SimpleWhilePlan {
|
||||
pub loop_increment: ASTNode,
|
||||
}
|
||||
|
||||
/// Phase 286 P2.3: Extracted structure for Pattern9 (Accumulator Const Loop)
|
||||
///
|
||||
/// This structure contains all the information needed to lower an accumulator loop.
|
||||
/// Pattern9 extends Pattern1 with an accumulator variable (e.g., sum = sum + 1).
|
||||
#[derive(Debug, Clone)]
|
||||
pub(in crate::mir::builder) struct Pattern9AccumConstLoopPlan {
|
||||
/// Loop variable name (e.g., "i")
|
||||
pub loop_var: String,
|
||||
/// Accumulator variable name (e.g., "sum")
|
||||
pub acc_var: String,
|
||||
/// Loop condition AST (e.g., `i < 3`)
|
||||
pub condition: ASTNode,
|
||||
/// Accumulator update expression AST (e.g., `sum + 1` or `sum + i`)
|
||||
pub acc_update: ASTNode,
|
||||
/// Loop increment expression AST (e.g., `i + 1`)
|
||||
pub loop_increment: ASTNode,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CorePlan (固定語彙 - 構造ノードのみ)
|
||||
// ============================================================================
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
use super::{
|
||||
CoreEffectPlan, CoreLoopPlan, CorePhiInfo, CorePlan, DomainPlan,
|
||||
ScanWithInitPlan, SplitScanPlan, Pattern1SimpleWhilePlan,
|
||||
ScanWithInitPlan, SplitScanPlan, Pattern1SimpleWhilePlan, Pattern9AccumConstLoopPlan,
|
||||
};
|
||||
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
|
||||
use crate::mir::builder::MirBuilder;
|
||||
@ -41,6 +41,7 @@ impl PlanNormalizer {
|
||||
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),
|
||||
DomainPlan::Pattern9AccumConstLoop(parts) => Self::normalize_pattern9_accum_const_loop(builder, parts, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1464,4 +1465,224 @@ impl PlanNormalizer {
|
||||
|
||||
Ok(CorePlan::Loop(loop_plan))
|
||||
}
|
||||
|
||||
/// Phase 286 P2.3: Pattern9AccumConstLoop → CorePlan 変換
|
||||
///
|
||||
/// Expands Pattern9 (Accumulator Const Loop) semantics into generic CorePlan:
|
||||
/// - CFG structure: preheader → header → body → step → header (back-edge)
|
||||
/// - 2 PHIs in header: loop variable (i) and accumulator (sum)
|
||||
/// - Similar to Pattern1 but with an additional carrier
|
||||
fn normalize_pattern9_accum_const_loop(
|
||||
builder: &mut MirBuilder,
|
||||
parts: Pattern9AccumConstLoopPlan,
|
||||
ctx: &LoopPatternContext,
|
||||
) -> Result<CorePlan, String> {
|
||||
use crate::mir::builder::control_flow::joinir::trace;
|
||||
|
||||
let trace_logger = trace::trace();
|
||||
let debug = ctx.debug;
|
||||
|
||||
if debug {
|
||||
trace_logger.debug(
|
||||
"normalizer/pattern9_accum_const_loop",
|
||||
&format!(
|
||||
"Phase 286 P2.3: Normalizing Pattern9AccumConstLoop for {} (loop_var: {}, acc_var: {})",
|
||||
ctx.func_name, parts.loop_var, parts.acc_var
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 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 acc_var_init = builder
|
||||
.variable_ctx
|
||||
.variable_map
|
||||
.get(&parts.acc_var)
|
||||
.copied()
|
||||
.ok_or_else(|| format!("[normalizer] Accumulator variable {} not found", parts.acc_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/pattern9_accum_const_loop",
|
||||
&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 for loop var
|
||||
let acc_var_current = builder.alloc_typed(MirType::Integer); // PHI destination for accumulator
|
||||
let cond_loop = builder.alloc_typed(MirType::Bool); // Loop condition
|
||||
let acc_var_next = builder.alloc_typed(MirType::Integer); // Updated accumulator
|
||||
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<String, crate::mir::ValueId> = BTreeMap::new();
|
||||
phi_bindings.insert(parts.loop_var.clone(), loop_var_current);
|
||||
phi_bindings.insert(parts.acc_var.clone(), acc_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 accumulator update (e.g., `sum + 1` or `sum + i`)
|
||||
let (acc_update_lhs, acc_update_op, acc_update_rhs, acc_update_consts) =
|
||||
Self::lower_binop_ast(&parts.acc_update, 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 (accumulator update + loop increment)
|
||||
let mut step_effects = acc_update_consts;
|
||||
step_effects.push(CoreEffectPlan::BinOp {
|
||||
dst: acc_var_next,
|
||||
lhs: acc_update_lhs,
|
||||
op: acc_update_op,
|
||||
rhs: acc_update_rhs,
|
||||
});
|
||||
step_effects.extend(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 (effects are in step)
|
||||
(step_bb, step_effects),
|
||||
];
|
||||
|
||||
// Step 10: Build PHIs (2 PHIs: loop variable + accumulator)
|
||||
let phis = vec![
|
||||
// Loop variable PHI
|
||||
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),
|
||||
},
|
||||
// Accumulator PHI
|
||||
CorePhiInfo {
|
||||
block: header_bb,
|
||||
dst: acc_var_current,
|
||||
inputs: vec![
|
||||
(preheader_bb, acc_var_init),
|
||||
(step_bb, acc_var_next),
|
||||
],
|
||||
tag: format!("acc_var_{}", parts.acc_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 (both loop var and accumulator)
|
||||
let final_values = vec![
|
||||
(parts.loop_var.clone(), loop_var_current),
|
||||
(parts.acc_var.clone(), acc_var_current),
|
||||
];
|
||||
|
||||
// Step 13: Build CoreLoopPlan
|
||||
// Note: found_bb = after_bb (no early exit in Pattern9)
|
||||
// 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 (effects are in step)
|
||||
cond_loop,
|
||||
cond_match: cond_loop, // Placeholder (unused in Pattern9)
|
||||
block_effects,
|
||||
phis,
|
||||
frag,
|
||||
final_values,
|
||||
};
|
||||
|
||||
if debug {
|
||||
trace_logger.debug(
|
||||
"normalizer/pattern9_accum_const_loop",
|
||||
"CorePlan construction complete (4 blocks, 2 PHIs)",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(CorePlan::Loop(loop_plan))
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# Phase 286 P2.3: Pattern9 → Frag PoC test
|
||||
# Tests: Pattern9 (AccumConstLoop) using Plan/Frag SSOT
|
||||
#
|
||||
# PoC Goal:
|
||||
# Pattern9 → DomainPlan → CorePlan → Frag → emit_frag()
|
||||
# (Skip: JoinIR → bridge → merge)
|
||||
#
|
||||
# Expected: return 3 (sum = 0 + 1 + 1 + 1 = 3)
|
||||
|
||||
source "$(dirname "$0")/../../../lib/test_runner.sh"
|
||||
export SMOKES_USE_PYVM=0
|
||||
require_env || exit 2
|
||||
|
||||
INPUT="$NYASH_ROOT/apps/tests/phase286_pattern9_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_pattern9_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_pattern9_frag_poc: Pattern9 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_pattern9_frag_poc: Unexpected exit code"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user