feat(joinir): Phase 88 - Pattern4 continue + variable step increment

Continue Pattern 拡張:
- then側の i=i+const 差分加算 + acc更新を許可
- continue_pattern.rs:193 で可変ステップ検出

Dev Router 許可:
- ast_lowerer/mod.rs:92 で normalized_dev feature時に新パターンを有効化

Fixtures & Tests:
- jsonparser_unescape_string_step2_min fixture追加(submodule)
- normalized_joinir_min.rs に shape テスト追加
- shapes.rs に expected shape 定義

Documentation:
- joinir-architecture-overview.md に Phase 88 到達点を追記

Impact:
- Pattern4 continue + 可変インクリメント(i+=1 or i+=2)対応
- _unescape_string 制御構造の土台確立
- normalized_dev tests PASS

Next: _unescape_string 残り複合ループ対応

🤖 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 00:29:56 +09:00
parent 8a8c90fc74
commit b71a18495d
7 changed files with 142 additions and 5 deletions

View File

@ -833,6 +833,8 @@ Pattern2/4 への統合(実際に Body-local 更新を使うループを JoinI
P2 + P4 + P5 の組み合わせで表現可能なことを設計上確認済み)
- 低優先度だが理論上は P1P4 からの拡張で吸収可能:
- `_unescape_string` など、複雑な continue / 条件付き更新を含むループ
- Phase 88dev-onlyで、`i+=2 + continue`(かつ continue 分岐側で `acc` 更新)を最小フィクスチャとして抽出し、
frontend の continue pattern を「`i = i + const` の差分加算」に限定して段階拡張した。
方針:

View File

@ -83,6 +83,7 @@ pub fn lower(
&parsed.func_name,
loop_cond_expr,
continue_cond_expr,
continue_if_stmt,
loop_body,
)?;
@ -136,6 +137,7 @@ fn create_loop_step_function_continue(
func_name: &str,
loop_cond_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;
@ -188,6 +190,67 @@ fn create_loop_step_function_continue(
body.extend(i_insts);
step_ctx.register_param("i".to_string(), i_next);
// Phase 88: Continue 分岐で i を追加更新できるようにする(例: i += 2
//
// `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
}
let mut i_next_continue = i_next;
let continue_then = continue_if_stmt["then"]
.as_array()
.ok_or_else(|| LoweringError::InvalidLoopBody {
message: "Continue pattern If must have 'then' array".to_string(),
})?;
if let Some(then_i_local) = continue_then
.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(|| LoweringError::InvalidLoopBody {
message: "Continue pattern requires i update of form (i + const)".to_string(),
})?;
let then_k =
extract_add_i_const(&then_i_local["expr"]).ok_or_else(|| LoweringError::InvalidLoopBody {
message: "Continue pattern requires then i update of form (i + const)".to_string(),
})?;
let delta = 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;
}
}
// 3. Continue 条件を評価
let (continue_cond_var, continue_cond_insts) =
lowerer.extract_value(continue_cond_expr, &mut step_ctx);
@ -205,21 +268,41 @@ fn create_loop_step_function_continue(
let (acc_increment, acc_insts) = lowerer.extract_value(acc_expr, &mut step_ctx);
body.extend(acc_insts);
// 5. Select: Continue なら acc そのまま、そうでなければ更新
// Phase 88: 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;
}
// 5. Select: Continue/通常 で acc を切り替える
let acc_next = step_ctx.alloc_var();
body.push(JoinInst::Select {
dst: acc_next,
cond: continue_cond_var,
then_val: step_acc, // Continue: 更新しない
else_val: acc_increment, // 通常: 更新
then_val: acc_then_val,
else_val: acc_increment,
type_hint: None, // Phase 63-3
});
// Phase 88: Continue/通常 で次 i を切り替える
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,
});
// 6. 末尾再帰
let recurse_result = step_ctx.alloc_var();
body.push(JoinInst::Call {
func: ctx.loop_step_id,
args: vec![i_next, acc_next, step_n],
args: vec![i_next_selected, acc_next, step_n],
k_next: None,
dst: Some(recurse_result),
});

View File

@ -89,6 +89,11 @@ fn resolve_function_route(func_name: &str) -> Result<FunctionRoute, String> {
"jsonparser_parse_object_continue_skip_ws",
FunctionRoute::LoopFrontend,
),
// Phase 88: JsonParser _unescape_string core (step2 + continue) minimal fixture
(
"jsonparser_unescape_string_step2_min",
FunctionRoute::LoopFrontend,
),
];
if let Some((_, route)) = TABLE.iter().find(|(name, _)| *name == func_name) {

View File

@ -796,6 +796,33 @@ pub fn build_jsonparser_parse_object_continue_skip_ws_structured_for_normalized_
module
}
/// JsonParser _unescape_string の「i+=2 + continue」コアを Structured で組み立てるヘルパーdev-only
///
/// 実ループ(`tools/hako_shared/json_parser.hako::_unescape_string`)から、
/// 文字列処理を除いて制御構造continue + 可変ステップ更新)だけを抽出した最小フィクスチャ。
///
/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_unescape_string_step2_min.program.json
pub fn build_jsonparser_unescape_string_step2_min_structured_for_normalized_dev() -> JoinModule {
const FIXTURE: &str = include_str!(
"../../../../docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_unescape_string_step2_min.program.json"
);
let program_json: serde_json::Value = serde_json::from_str(FIXTURE)
.expect("jsonparser_unescape_string_step2_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] jsonparser_unescape_string_step2_min structured module: {:#?}",
module
);
}
module
}
/// まとめて import したいとき用のプレリュード。
pub mod prelude {
pub use super::{
@ -806,6 +833,7 @@ pub mod prelude {
build_jsonparser_parse_object_continue_skip_ws_structured_for_normalized_dev,
build_jsonparser_skip_ws_real_structured_for_normalized_dev,
build_jsonparser_skip_ws_structured_for_normalized_dev,
build_jsonparser_unescape_string_step2_min_structured_for_normalized_dev,
build_pattern2_break_fixture_structured, build_pattern2_minimal_structured,
build_pattern3_if_sum_min_structured_for_normalized_dev,
build_pattern3_if_sum_multi_min_structured_for_normalized_dev,

View File

@ -12,6 +12,7 @@ use nyash_rust::mir::join_ir::normalized::fixtures::{
build_jsonparser_parse_object_continue_skip_ws_structured_for_normalized_dev,
build_jsonparser_skip_ws_real_structured_for_normalized_dev,
build_jsonparser_skip_ws_structured_for_normalized_dev,
build_jsonparser_unescape_string_step2_min_structured_for_normalized_dev,
build_pattern2_break_fixture_structured, build_pattern2_minimal_structured,
build_pattern3_if_sum_min_structured_for_normalized_dev,
build_pattern3_if_sum_multi_min_structured_for_normalized_dev,

View File

@ -295,6 +295,24 @@ fn test_normalized_pattern4_jsonparser_parse_object_continue_skip_ws_vm_bridge_d
}
}
/// Phase 88: JsonParser _unescape_string コアi+=2 + continueを canonical route で固定する。
#[test]
fn test_phase88_jsonparser_unescape_string_step2_min_canonical_matches_structured() {
let structured = build_jsonparser_unescape_string_step2_min_structured_for_normalized_dev();
let entry = structured.entry.expect("structured entry required");
// n=10 → i=0,2,4,6,8 で acc++ → 5
let args = [JoinValue::Int(10)];
let structured_res = run_joinir_vm_bridge_structured_only(&structured, entry, &args);
let canonical = run_joinir_vm_bridge(&structured, entry, &args, false);
assert_eq!(
structured_res, canonical,
"canonical unescape(step2) mismatch"
);
assert_eq!(canonical, JoinValue::Int(5));
}
/// Phase 48-C: JsonParser _parse_object continue skip_ws canonical route should match Structured
#[test]
fn test_normalized_pattern4_jsonparser_parse_object_continue_skip_ws_canonical_matches_structured()