refactor(joinir): Phase 89-2 - StepCalculator Box extraction

Extract step increment logic from Continue pattern into reusable Box:

New Module:
- step_calculator.rs (161 lines)
  - extract_linear_increment(): Detect i + const patterns
  - calculate_step_difference(): Compute step delta
  - 6 comprehensive unit tests (100% coverage)

Changes:
- continue_pattern.rs: Use StepCalculator instead of local function
  - Removed extract_add_i_const() (20 lines)
  - Cleaner separation of concerns
- mod.rs: Register step_calculator module

Benefits:
- Reusability: Available for ContinueReturn and future patterns
- Testability: Independent pure functions with unit tests
- Extensibility: Easy to add i *= 2 support later
- SSOT: Single source of truth for step increment logic

Code Metrics:
- continue_pattern.rs: 321 → 305 lines (5% reduction)
- New tests: +6 (993 total passed)
- Complexity: Reduced (pure function extraction)

Tests: 993 passed (no regression)

🤖 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 01:49:45 +09:00
parent 6d61e8f578
commit 39affbf00d
3 changed files with 180 additions and 35 deletions

View File

@ -26,6 +26,7 @@ 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;
@ -200,26 +201,8 @@ fn create_loop_step_function_continue(
// `continue_if` の then 内に `Local i = i + K` がある場合、
// loop_body の通常更新 `Local i = i + K0` との差分 (K-K0) を i_next に加算して
// continue パスの次 i を構成するLinear increment のみ対応)。
fn extract_add_i_const(expr: &serde_json::Value) -> Option<i64> {
if expr["type"].as_str()? != "Binary" || expr["op"].as_str()? != "+" {
return None;
}
let lhs = &expr["lhs"];
let rhs = &expr["rhs"];
// i + const
if lhs["type"].as_str()? == "Var" && lhs["name"].as_str()? == "i" {
if rhs["type"].as_str()? == "Int" {
return rhs["value"].as_i64();
}
}
// const + i
if rhs["type"].as_str()? == "Var" && rhs["name"].as_str()? == "i" {
if lhs["type"].as_str()? == "Int" {
return lhs["value"].as_i64();
}
}
None
}
//
// Phase 89-2: StepCalculator Box に抽出済み(再利用性向上)
let mut i_next_continue = i_next;
let continue_then = continue_if_stmt["then"]
@ -231,7 +214,7 @@ fn create_loop_step_function_continue(
.iter()
.find(|stmt| stmt["type"].as_str() == Some("Local") && stmt["name"].as_str() == Some("i"))
{
let base_k = extract_add_i_const(i_expr).ok_or_else(|| {
let base_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 {
@ -244,20 +227,21 @@ fn create_loop_step_function_continue(
),
}
})?;
let then_k = extract_add_i_const(&then_i_local["expr"]).ok_or_else(|| {
let expr_debug = serde_json::to_string(&then_i_local["expr"])
.unwrap_or_else(|_| "<invalid JSON>".to_string());
LoweringError::InvalidLoopBody {
message: format!(
"Continue pattern validation failed: invalid 'then' branch step increment.\n\
Expected: In 'if ... {{ 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 = then_k - base_k;
let then_k = StepCalculator::extract_linear_increment(&then_i_local["expr"], "i")
.ok_or_else(|| {
let expr_debug = serde_json::to_string(&then_i_local["expr"])
.unwrap_or_else(|_| "<invalid JSON>".to_string());
LoweringError::InvalidLoopBody {
message: format!(
"Continue pattern validation failed: invalid 'then' branch step increment.\n\
Expected: In 'if ... {{ 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 {

View File

@ -24,6 +24,7 @@ pub mod if_sum_break_pattern;
pub mod param_guess;
pub mod print_tokens;
pub mod simple;
pub mod step_calculator;
/// ループパターンの分類(ユースケースベース + 制御構造)
///

View File

@ -0,0 +1,160 @@
//! Phase P4 副産物: Step increment 抽出・計算ユーティリティ
//!
//! ## 責務1行で表現
//! **ループ step increment の線形パターンi = i + constを検出・計算する**
//!
//! ## 使用例
//! ```rust
//! // Continue パターンで通常/continue パスの step 差分を計算
//! let base_k = StepCalculator::extract_linear_increment(i_expr, "i")?;
//! let then_k = StepCalculator::extract_linear_increment(&then_i_expr, "i")?;
//! let delta = StepCalculator::calculate_step_difference(then_k, base_k);
//! ```
use serde_json::Value;
/// Step increment 計算器(純関数集合)
pub struct StepCalculator;
impl StepCalculator {
/// `expr` が `i + const` または `const + i` 形式なら K を返す
///
/// # Arguments
/// * `expr` - JSON v0 形式の式Binary op想定
/// * `var_name` - ターゲット変数名(通常は "i"
///
/// # Returns
/// * `Some(K)`: 線形インクリメント定数
/// * `None`: パターン不一致
///
/// # 対応形式
/// - `i + 1`, `i + 2`, `i + K`
/// - `1 + i`, `2 + i`, `K + i`
///
/// # 非対応形式
/// - `i - K` (減算は未対応)
/// - `i * K` (乗算は未対応)
/// - `i + j` (変数同士の加算)
pub fn extract_linear_increment(expr: &Value, var_name: &str) -> Option<i64> {
// Binary + 演算子チェック
if expr["type"].as_str()? != "Binary" || expr["op"].as_str()? != "+" {
return None;
}
let lhs = &expr["lhs"];
let rhs = &expr["rhs"];
// パターン1: var + const
if Self::is_var_with_name(lhs, var_name) {
return Self::extract_int_const(rhs);
}
// パターン2: const + var
if Self::is_var_with_name(rhs, var_name) {
return Self::extract_int_const(lhs);
}
None
}
/// Step 差分を計算
///
/// # Arguments
/// * `then_step`: Continue パスの step 値(例: i = i + 2
/// * `normal_step`: 通常パスの step 値(例: i = i + 1
///
/// # Returns
/// * `delta`: Continue パスの追加 step例: 2 - 1 = 1
///
/// # 使用例
/// ```rust
/// let delta = StepCalculator::calculate_step_difference(3, 1); // → 2
/// // i_next_continue = i_next + delta
/// ```
pub fn calculate_step_difference(then_step: i64, normal_step: i64) -> i64 {
then_step - normal_step
}
/// 変数が指定名かチェック
fn is_var_with_name(expr: &Value, var_name: &str) -> bool {
expr["type"].as_str() == Some("Var") && expr["name"].as_str() == Some(var_name)
}
/// 整数定数を抽出
fn extract_int_const(expr: &Value) -> Option<i64> {
if expr["type"].as_str()? == "Int" {
expr["value"].as_i64()
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_extract_linear_increment_i_plus_const() {
let expr = json!({
"type": "Binary",
"op": "+",
"lhs": {"type": "Var", "name": "i"},
"rhs": {"type": "Int", "value": 2}
});
assert_eq!(StepCalculator::extract_linear_increment(&expr, "i"), Some(2));
}
#[test]
fn test_extract_linear_increment_const_plus_i() {
let expr = json!({
"type": "Binary",
"op": "+",
"lhs": {"type": "Int", "value": 3},
"rhs": {"type": "Var", "name": "i"}
});
assert_eq!(StepCalculator::extract_linear_increment(&expr, "i"), Some(3));
}
#[test]
fn test_extract_linear_increment_non_addition() {
let expr = json!({
"type": "Binary",
"op": "*",
"lhs": {"type": "Var", "name": "i"},
"rhs": {"type": "Int", "value": 2}
});
assert_eq!(StepCalculator::extract_linear_increment(&expr, "i"), None);
}
#[test]
fn test_extract_linear_increment_wrong_var_name() {
let expr = json!({
"type": "Binary",
"op": "+",
"lhs": {"type": "Var", "name": "j"},
"rhs": {"type": "Int", "value": 1}
});
assert_eq!(StepCalculator::extract_linear_increment(&expr, "i"), None);
}
#[test]
fn test_extract_linear_increment_var_plus_var() {
let expr = json!({
"type": "Binary",
"op": "+",
"lhs": {"type": "Var", "name": "i"},
"rhs": {"type": "Var", "name": "j"}
});
assert_eq!(StepCalculator::extract_linear_increment(&expr, "i"), None);
}
#[test]
fn test_calculate_step_difference() {
assert_eq!(StepCalculator::calculate_step_difference(3, 1), 2);
assert_eq!(StepCalculator::calculate_step_difference(1, 1), 0);
assert_eq!(StepCalculator::calculate_step_difference(5, 2), 3);
assert_eq!(StepCalculator::calculate_step_difference(2, 5), -3);
}
}