feat(joinir): Phase 89 P1 - ContinueReturn lowering implementation

## ContinueReturn 独立箱
- continue_return_pattern.rs (457行)
- Continue + Early Return の JoinIR lowering
- StepCalculator 再利用(3箇所)
- Fail-Fast 原則徹底(5箇所検証)

## Dispatcher 配線
- mod.rs: LoopPattern::ContinueReturn 対応
- allowlist: pattern_continue_return_minimal 追加

## Fixture & Tests
- fixtures.rs: build_pattern_continue_return_min
- shapes.rs: +2 tests (vm_bridge + 期待値 n=10→acc=4)

Impact:
- normalized_dev: 61 passed (+2)
- lib tests: 993 passed (回帰なし)
- 箱化原則:単一責任・境界明確・再利用性高

Next: Phase 90 - _parse_string 合成 fixture

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-14 02:02:47 +09:00
parent 39affbf00d
commit 730a80f33f
6 changed files with 521 additions and 4 deletions

View File

@ -0,0 +1,457 @@
//! Phase 89 P1: ContinueReturn パターン lowering
//!
//! ## 責務1行で表現
//! **continue + early return 両方を持つループを複合処理で JoinIR に落とす**
//!
//! ## パターン例
//! ```nyash
//! loop(i < n) {
//! if i == 5 { return acc } // Early return
//! if i == 2 {
//! i = i + 1
//! continue
//! }
//! acc = acc + 1
//! i = i + 1
//! }
//! ```
//!
//! ## 生成する JoinIR 構造
//! - entry 関数: Call(loop_step)
//! - loop_step 関数:
//! - exit 条件チェック (i >= n で k_exit)
//! - early return 条件チェック (i == 5 で Jump(k_exit, acc))
//! - continue 条件チェック
//! - Select: continue なら acc そのまま、そうでなければ更新
//! - Select: continue なら i 追加更新、そうでなければ通常更新
//! - 再帰
//! - k_exit 関数: Return(acc)
//!
//! ## 設計原則
//! - **StepCalculator 再利用**: Phase 89-2 の成果活用
//! - **Fail-Fast**: 複数 Return/Continue、Return が then 以外などは明示エラー
//! - **境界明確**: continue_pattern.rs の設計を継承
use super::common::{
build_join_module, create_k_exit_function, create_loop_context, parse_program_json,
process_local_inits,
};
use super::step_calculator::StepCalculator;
use super::{AstToJoinIrLowerer, JoinModule, LoweringError};
use crate::mir::join_ir::{CompareOp, ConstValue, JoinFunction, JoinInst, MirLikeInst};
use crate::mir::ValueId;
/// ContinueReturn パターンを JoinModule に変換
///
/// # Arguments
/// * `lowerer` - AstToJoinIrLowerer インスタンス
/// * `program_json` - Program(JSON v0)
pub fn lower(
lowerer: &mut AstToJoinIrLowerer,
program_json: &serde_json::Value,
) -> Result<JoinModule, LoweringError> {
// 1. Program(JSON) をパース
let parsed = parse_program_json(program_json);
// 2. LoopContext と entry_ctx を作成
let (ctx, mut entry_ctx) = create_loop_context(lowerer, &parsed);
// 3. Local 初期化を処理
let init_insts = process_local_inits(lowerer, &parsed, &mut entry_ctx);
// 4. Loop の cond を取得
let loop_node = &parsed.stmts[parsed.loop_node_idx];
let loop_cond_expr = &loop_node["cond"];
// 5. Loop body から Return If と Continue If を探す
let loop_body = loop_node["body"]
.as_array()
.ok_or_else(|| LoweringError::InvalidLoopBody {
message: "Loop must have 'body' array".to_string(),
})?;
// 5-1. Return If を探すFail-Fast: 複数あればエラー)
let return_if_stmts: Vec<_> = loop_body
.iter()
.filter(|stmt| {
stmt["type"].as_str() == Some("If")
&& stmt["then"].as_array().map_or(false, |then| {
then.iter().any(|s| s["type"].as_str() == Some("Return"))
})
})
.collect();
if return_if_stmts.is_empty() {
return Err(LoweringError::InvalidLoopBody {
message: "ContinueReturn pattern must have If + Return".to_string(),
});
}
if return_if_stmts.len() > 1 {
return Err(LoweringError::InvalidLoopBody {
message: format!(
"ContinueReturn pattern validation failed: multiple Return statements found.\n\
Expected: Exactly one 'if {{ return ... }}' statement in loop body.\n\
Found: {} Return statements.\n\
Hint: Combine multiple return conditions into a single if statement.",
return_if_stmts.len()
),
});
}
let return_if_stmt = return_if_stmts[0];
// 5-2. Return が then にあることを確認Fail-Fast
let return_then = return_if_stmt["then"]
.as_array()
.ok_or_else(|| LoweringError::InvalidLoopBody {
message: "Return If must have 'then' array".to_string(),
})?;
let return_stmt = return_then
.iter()
.find(|s| s["type"].as_str() == Some("Return"))
.ok_or_else(|| LoweringError::InvalidLoopBody {
message: "Return If 'then' must contain Return statement".to_string(),
})?;
// else に Return があったら Fail-Fast
if let Some(else_branch) = return_if_stmt["else"].as_array() {
if else_branch
.iter()
.any(|s| s["type"].as_str() == Some("Return"))
{
return Err(LoweringError::InvalidLoopBody {
message: "ContinueReturn pattern validation failed: Return in 'else' branch.\n\
Expected: Return statement only in 'then' branch.\n\
Found: Return in 'else' branch.\n\
Hint: Move Return statement to 'then' branch or invert condition."
.to_string(),
});
}
}
let return_cond_expr = &return_if_stmt["cond"];
let return_expr = &return_stmt["expr"];
// 5-3. Continue If を探すFail-Fast: 複数あればエラー)
let continue_if_stmts: Vec<_> = loop_body
.iter()
.filter(|stmt| {
stmt["type"].as_str() == Some("If")
&& stmt["then"].as_array().map_or(false, |then| {
then.iter().any(|s| s["type"].as_str() == Some("Continue"))
})
})
.collect();
if continue_if_stmts.is_empty() {
return Err(LoweringError::InvalidLoopBody {
message: "ContinueReturn pattern must have If + Continue".to_string(),
});
}
if continue_if_stmts.len() > 1 {
return Err(LoweringError::InvalidLoopBody {
message: format!(
"ContinueReturn pattern validation failed: multiple Continue statements found.\n\
Expected: Exactly one 'if {{ continue }}' statement in loop body.\n\
Found: {} Continue statements.\n\
Hint: Combine multiple continue conditions into a single if statement.",
continue_if_stmts.len()
),
});
}
let continue_if_stmt = continue_if_stmts[0];
let continue_cond_expr = &continue_if_stmt["cond"];
// 6. entry 関数を生成
let entry_func =
create_entry_function_continue_return(&ctx, &parsed, init_insts, &mut entry_ctx);
// 7. loop_step 関数を生成
let loop_step_func = create_loop_step_function_continue_return(
lowerer,
&ctx,
&parsed.func_name,
loop_cond_expr,
return_cond_expr,
return_expr,
continue_cond_expr,
continue_if_stmt,
loop_body,
)?;
// 8. k_exit 関数を生成
let k_exit_func = create_k_exit_function(&ctx, &parsed.func_name);
// 9. JoinModule を構築
Ok(build_join_module(entry_func, loop_step_func, k_exit_func))
}
/// ContinueReturn パターン用 entry 関数を生成
fn create_entry_function_continue_return(
ctx: &super::common::LoopContext,
parsed: &super::common::ParsedProgram,
init_insts: Vec<JoinInst>,
entry_ctx: &mut super::super::context::ExtractCtx,
) -> JoinFunction {
// i, acc, n を取得
let i_init = entry_ctx.get_var("i").expect("i must be initialized");
let acc_init = entry_ctx.get_var("acc").expect("acc must be initialized");
let n_param = entry_ctx.get_var("n").expect("n must be parameter");
let loop_result = entry_ctx.alloc_var();
let mut body = init_insts;
body.push(JoinInst::Call {
func: ctx.loop_step_id,
args: vec![i_init, acc_init, n_param],
k_next: None,
dst: Some(loop_result),
});
body.push(JoinInst::Ret {
value: Some(loop_result),
});
JoinFunction {
id: ctx.entry_id,
name: parsed.func_name.clone(),
params: (0..parsed.param_names.len())
.map(|i| ValueId(i as u32))
.collect(),
body,
exit_cont: None,
}
}
/// ContinueReturn パターン用 loop_step 関数を生成
#[allow(clippy::too_many_arguments)]
fn create_loop_step_function_continue_return(
lowerer: &mut AstToJoinIrLowerer,
ctx: &super::common::LoopContext,
func_name: &str,
loop_cond_expr: &serde_json::Value,
return_cond_expr: &serde_json::Value,
return_expr: &serde_json::Value,
continue_cond_expr: &serde_json::Value,
continue_if_stmt: &serde_json::Value,
loop_body: &[serde_json::Value],
) -> Result<JoinFunction, LoweringError> {
use super::super::context::ExtractCtx;
let step_i = ValueId(0);
let step_acc = ValueId(1);
let step_n = ValueId(2);
let mut step_ctx = ExtractCtx::new(3);
step_ctx.register_param("i".to_string(), step_i);
step_ctx.register_param("acc".to_string(), step_acc);
step_ctx.register_param("n".to_string(), step_n);
let mut body = Vec::new();
// 1. exit 条件チェック(!(i < n) = i >= n で抜ける)
let (cond_var, cond_insts) = lowerer.extract_value(loop_cond_expr, &mut step_ctx);
body.extend(cond_insts);
let false_const = step_ctx.alloc_var();
let exit_cond = step_ctx.alloc_var();
body.push(JoinInst::Compute(MirLikeInst::Const {
dst: false_const,
value: ConstValue::Bool(false),
}));
body.push(JoinInst::Compute(MirLikeInst::Compare {
dst: exit_cond,
op: CompareOp::Eq,
lhs: cond_var,
rhs: false_const,
}));
body.push(JoinInst::Jump {
cont: ctx.k_exit_id.as_cont(),
args: vec![step_acc],
cond: Some(exit_cond),
});
// 2. Early return 条件チェックi == 5 で Jump(k_exit, return_val)
let (return_cond_var, return_cond_insts) =
lowerer.extract_value(return_cond_expr, &mut step_ctx);
body.extend(return_cond_insts);
let (return_val, return_val_insts) = lowerer.extract_value(return_expr, &mut step_ctx);
body.extend(return_val_insts);
body.push(JoinInst::Jump {
cont: ctx.k_exit_id.as_cont(),
args: vec![return_val],
cond: Some(return_cond_var),
});
// 3. Continue pattern: i のインクリメント処理
// Continue If の then 内に i の更新がある場合、それを使う(例: i = i + 1
let continue_then = continue_if_stmt["then"]
.as_array()
.ok_or_else(|| LoweringError::InvalidLoopBody {
message: "Continue If must have 'then' array".to_string(),
})?;
let i_update_in_continue = continue_then
.iter()
.find(|stmt| stmt["type"].as_str() == Some("Local") && stmt["name"].as_str() == Some("i"))
.ok_or_else(|| LoweringError::InvalidLoopBody {
message: format!(
"ContinueReturn pattern validation failed: missing 'i' increment in continue block.\n\
Expected: Continue block must contain 'local i = i + K' where K is a constant.\n\
Found: Continue block does not contain 'i' update.\n\
Hint: Add 'i = i + 1' inside the 'if {{ continue }}' block."
),
})?;
let i_expr = &i_update_in_continue["expr"];
let (i_next, i_insts) = lowerer.extract_value(i_expr, &mut step_ctx);
body.extend(i_insts);
step_ctx.register_param("i".to_string(), i_next);
// 通常の i 更新を探す(ループ末尾)
let i_update_normal = loop_body
.iter()
.filter(|stmt| stmt["type"].as_str() == Some("Local") && stmt["name"].as_str() == Some("i"))
.last() // Continue の後にある最後の i 更新を使う
.ok_or_else(|| LoweringError::InvalidLoopBody {
message: format!(
"ContinueReturn pattern validation failed: missing normal 'i' increment.\n\
Expected: Loop body must contain 'local i = i + K' outside continue block.\n\
Found: No 'i' update found in loop body.\n\
Hint: Add 'i = i + 1' at the end of the loop body."
),
})?;
// Continue パスと通常パスの i 更新の差分計算StepCalculator 活用)
let mut i_next_continue = i_next;
let base_k = StepCalculator::extract_linear_increment(&i_update_normal["expr"], "i")
.ok_or_else(|| {
let expr_debug = serde_json::to_string(&i_update_normal["expr"])
.unwrap_or_else(|_| "<invalid JSON>".to_string());
LoweringError::InvalidLoopBody {
message: format!(
"ContinueReturn pattern validation failed: invalid step increment form.\n\
Expected: i = i + const (e.g., 'i = i + 1', 'i = i + 2').\n\
Found: {}\n\
Hint: Change the 'i' update to addition form 'i = i + K' where K is a constant integer.",
expr_debug
),
}
})?;
let then_k = StepCalculator::extract_linear_increment(i_expr, "i").ok_or_else(|| {
let expr_debug =
serde_json::to_string(i_expr).unwrap_or_else(|_| "<invalid JSON>".to_string());
LoweringError::InvalidLoopBody {
message: format!(
"ContinueReturn pattern validation failed: invalid continue branch step increment.\n\
Expected: In continue block, 'i = i + const' (e.g., 'i = i + 2').\n\
Found: {}\n\
Hint: Ensure the continue block updates 'i' using addition form 'i = i + K'.",
expr_debug
),
}
})?;
let delta = StepCalculator::calculate_step_difference(then_k, base_k);
if delta != 0 {
let delta_const = step_ctx.alloc_var();
body.push(JoinInst::Compute(MirLikeInst::Const {
dst: delta_const,
value: ConstValue::Integer(delta),
}));
let bumped = step_ctx.alloc_var();
body.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: bumped,
op: crate::mir::join_ir::BinOpKind::Add,
lhs: i_next,
rhs: delta_const,
}));
i_next_continue = bumped;
}
// 4. Continue 条件を評価
let (continue_cond_var, continue_cond_insts) =
lowerer.extract_value(continue_cond_expr, &mut step_ctx);
body.extend(continue_cond_insts);
// 5. acc の更新値を計算(通常パス)
let acc_update_local = loop_body
.iter()
.find(|stmt| {
stmt["type"].as_str() == Some("Local") && stmt["name"].as_str() == Some("acc")
})
.ok_or_else(|| LoweringError::InvalidLoopBody {
message: format!(
"ContinueReturn pattern validation failed: missing accumulator update.\n\
Expected: Loop body must contain 'local acc = ...' statement.\n\
Found: No 'acc' update found in loop body.\n\
Hint: Add 'acc = acc + <value>' or similar accumulator update."
),
})?;
let acc_expr = &acc_update_local["expr"];
let (acc_increment, acc_insts) = lowerer.extract_value(acc_expr, &mut step_ctx);
body.extend(acc_insts);
// Continue 分岐側でも acc を更新できる場合(例: acc += 1
let mut acc_then_val = step_acc;
if let Some(then_acc_local) = continue_then.iter().find(|stmt| {
stmt["type"].as_str() == Some("Local") && stmt["name"].as_str() == Some("acc")
}) {
let (acc_then, acc_then_insts) =
lowerer.extract_value(&then_acc_local["expr"], &mut step_ctx);
body.extend(acc_then_insts);
acc_then_val = acc_then;
}
// 6. Select: Continue/通常 で acc を切り替える
let acc_next = step_ctx.alloc_var();
body.push(JoinInst::Select {
dst: acc_next,
cond: continue_cond_var,
then_val: acc_then_val,
else_val: acc_increment,
type_hint: None,
});
// 7. Select: Continue/通常 で次 i を切り替える
// Note: 通常パスは i_next (continue の i 更新ベース)、continue パスは i_next_continue (差分加算済み)
// しかし、この fixture では continue 時に i = i + 1、通常時も i = i + 1 なので差分なし
// → 本実装では差分があっても動作するよう設計
let i_next_selected = step_ctx.alloc_var();
body.push(JoinInst::Select {
dst: i_next_selected,
cond: continue_cond_var,
then_val: i_next_continue,
else_val: i_next,
type_hint: None,
});
// 8. 末尾再帰
let recurse_result = step_ctx.alloc_var();
body.push(JoinInst::Call {
func: ctx.loop_step_id,
args: vec![i_next_selected, acc_next, step_n],
k_next: None,
dst: Some(recurse_result),
});
body.push(JoinInst::Ret {
value: Some(recurse_result),
});
Ok(JoinFunction {
id: ctx.loop_step_id,
name: format!("{}_loop_step", func_name),
params: vec![step_i, step_acc, step_n],
body,
exit_cont: None,
})
}

View File

@ -18,6 +18,7 @@ use super::{AstToJoinIrLowerer, JoinModule};
pub mod break_pattern;
pub mod common;
pub mod continue_pattern;
pub mod continue_return_pattern;
pub mod filter;
#[cfg(feature = "normalized_dev")]
pub mod if_sum_break_pattern;
@ -128,9 +129,6 @@ pub fn lower_loop_with_pattern(
LoopPattern::Simple => simple::lower(lowerer, program_json),
LoopPattern::Break => break_pattern::lower(lowerer, program_json),
LoopPattern::Continue => continue_pattern::lower(lowerer, program_json),
LoopPattern::ContinueReturn => Err(LoweringError::UnimplementedPattern {
pattern: LoopPattern::ContinueReturn,
reason: "ContinueReturn pattern Phase 89 implementation in progress (Step 3)".to_string(),
}),
LoopPattern::ContinueReturn => continue_return_pattern::lower(lowerer, program_json),
}
}

View File

@ -94,6 +94,11 @@ fn resolve_function_route(func_name: &str) -> Result<FunctionRoute, String> {
"jsonparser_unescape_string_step2_min",
FunctionRoute::LoopFrontend,
),
// Phase 89 P1: ContinueReturn pattern minimal fixture
(
"pattern_continue_return_minimal",
FunctionRoute::LoopFrontend,
),
];
if let Some((_, route)) = TABLE.iter().find(|(name, _)| *name == func_name) {

View File

@ -823,6 +823,30 @@ pub fn build_jsonparser_unescape_string_step2_min_structured_for_normalized_dev(
module
}
/// Pattern Continue + Return minimal を Structured で組み立てるヘルパー
///
/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/pattern_continue_return_min.program.json
pub fn build_pattern_continue_return_min_structured_for_normalized_dev() -> JoinModule {
const FIXTURE: &str = include_str!(
"../../../../docs/private/roadmap2/phases/normalized_dev/fixtures/pattern_continue_return_min.program.json"
);
let program_json: serde_json::Value =
serde_json::from_str(FIXTURE).expect("pattern_continue_return_min fixture should be valid JSON");
let mut lowerer = AstToJoinIrLowerer::new();
let module = lowerer.lower_program_json(&program_json);
if joinir_dev_enabled() && joinir_test_debug_enabled() {
eprintln!(
"[joinir/normalized-dev] pattern_continue_return_min structured module: {:#?}",
module
);
}
module
}
/// まとめて import したいとき用のプレリュード。
pub mod prelude {
pub use super::{
@ -839,6 +863,7 @@ pub mod prelude {
build_pattern3_if_sum_multi_min_structured_for_normalized_dev,
build_pattern3_json_if_sum_min_structured_for_normalized_dev,
build_pattern4_continue_min_structured_for_normalized_dev,
build_pattern_continue_return_min_structured_for_normalized_dev,
build_selfhost_if_sum_p3_ext_structured_for_normalized_dev,
build_selfhost_if_sum_p3_structured_for_normalized_dev,
build_selfhost_token_scan_p2_accum_structured_for_normalized_dev,

View File

@ -18,6 +18,7 @@ use nyash_rust::mir::join_ir::normalized::fixtures::{
build_pattern3_if_sum_multi_min_structured_for_normalized_dev,
build_pattern3_json_if_sum_min_structured_for_normalized_dev,
build_pattern4_continue_min_structured_for_normalized_dev,
build_pattern_continue_return_min_structured_for_normalized_dev,
build_selfhost_args_parse_p2_structured_for_normalized_dev,
build_selfhost_if_sum_p3_ext_structured_for_normalized_dev,
build_selfhost_if_sum_p3_structured_for_normalized_dev,

View File

@ -537,3 +537,34 @@ fn test_phase54_structural_axis_discrimination_p3() {
);
}
}
/// Phase 89 P1: Pattern Continue + Return minimal - vm_bridge direct vs structured
#[test]
fn test_normalized_pattern_continue_return_min_vm_bridge_direct_matches_structured() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern_continue_return_min_structured_for_normalized_dev();
let entry = structured.entry.expect("structured entry required");
let input = [JoinValue::Int(10)]; // n = 10
let base = run_joinir_vm_bridge(&structured, entry, &input, false);
let dev = run_joinir_vm_bridge(&structured, entry, &input, true);
assert_eq!(base, dev, "vm bridge mismatch for pattern continue+return min");
}
/// Phase 89 P1: Pattern Continue + Return minimal - expected output test
#[test]
fn test_normalized_pattern_continue_return_min_expected_output() {
let _ctx = normalized_dev_test_ctx();
let structured = build_pattern_continue_return_min_structured_for_normalized_dev();
let entry = structured.entry.expect("structured entry required");
let input = [JoinValue::Int(10)]; // n = 10
let result = run_joinir_vm_bridge(&structured, entry, &input, true);
assert_eq!(
result,
JoinValue::Int(4),
"Expected acc=4 for n=10 (i=0,1,3,4 increments acc, i=2 skipped by continue, i=5 early return)"
);
}