diff --git a/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md b/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md index ba41fb2f..24e794ca 100644 --- a/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md +++ b/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md @@ -45,6 +45,8 @@ JoinIR の箱構造と責務、ループ/if の lowering パターンを把握 - 実装(Phase 137-2): `src/mir/loop_canonicalizer/mod.rs` 6. Phase 93: ConditionOnly Derived Slot(Trim / body-local) - `docs/development/current/main/phases/phase-93/README.md` +7. Phase 94: P5b “完全E2E”(escape skip / derived) + - `docs/development/current/main/phases/phase-94/README.md` 6. MIR Builder(Context 分割の入口) - `src/mir/builder/README.md` 7. Scope/BindingId(shadowing・束縛同一性の段階移行) diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index 4a988e17..1ab04098 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -49,6 +49,13 @@ - schedule: `body-init → derived → break` を評価順SSOTとして強制 - Phase 記録(入口): `docs/development/current/main/phases/phase-93/README.md` +## 2025‑12‑16:Phase 94(短報) + +- P5b escape(`ch` 再代入 + `i` の +1/+2)を “derived(Select)” として扱い、VM E2E を固定。 + - 新箱: `BodyLocalDerivedEmitter` + 明示ポリシー(strict で理由付き Fail-Fast) + - integration smoke: `tools/smokes/v2/profiles/integration/apps/phase94_p5b_escape_e2e.sh` + - Phase 記録(入口): `docs/development/current/main/phases/phase-94/README.md` + ## 2025‑12‑14:現状サマリ (補足)docs が増えて迷子になったときの「置き場所ルール(SSOT)」: diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md index 275a2404..c60f976f 100644 --- a/docs/development/current/main/30-Backlog.md +++ b/docs/development/current/main/30-Backlog.md @@ -9,8 +9,8 @@ Related: ## 直近(JoinIR/selfhost) - **P5b “完全E2E”**(escape skip の実ループを end-to-end で固定) - - 現状: 認識(Phase 91)+ lowering基盤(Phase 92)は完了、promotion が未整備で E2E を保留 - - 入口: `docs/development/current/main/phases/phase-92/README.md` + - 現状: Phase 94 で VM E2E まで固定済み。次は selfhost 実コード(`apps/selfhost-vm/json_loader.hako`)へ横展開して回帰を減らす。 + - 入口: `docs/development/current/main/phases/phase-94/README.md` - **制御の再帰合成(docs-only → dev-only段階投入)** - ねらい: `loop/if` ネストの “構造” を SSOT(ControlTree/StepTree)で表せるようにする - 注意: canonicalizer は観測/構造SSOTまで(ValueId/PHI配線は Normalized 側へ) diff --git a/docs/development/current/main/design/joinir-design-map.md b/docs/development/current/main/design/joinir-design-map.md index 217b9d50..baaff08d 100644 --- a/docs/development/current/main/design/joinir-design-map.md +++ b/docs/development/current/main/design/joinir-design-map.md @@ -107,6 +107,7 @@ flowchart LR - Error tags(SSOT): [`src/mir/join_ir/lowering/error_tags.rs`](../../../../../src/mir/join_ir/lowering/error_tags.rs) - Loop Canonicalizer(前処理 SSOT): [`src/mir/loop_canonicalizer/mod.rs`](../../../../../src/mir/loop_canonicalizer/mod.rs) - ConditionOnly Derived Slot(Phase 93): [`src/mir/join_ir/lowering/common/condition_only_emitter.rs`](../../../../../src/mir/join_ir/lowering/common/condition_only_emitter.rs) +- BodyLocalDerived Slot(Phase 94 / P5b): [`src/mir/join_ir/lowering/common/body_local_derived_emitter.rs`](../../../../../src/mir/join_ir/lowering/common/body_local_derived_emitter.rs) --- diff --git a/docs/development/current/main/phases/phase-93/README.md b/docs/development/current/main/phases/phase-93/README.md index 2acdd2c8..a8695776 100644 --- a/docs/development/current/main/phases/phase-93/README.md +++ b/docs/development/current/main/phases/phase-93/README.md @@ -1,6 +1,6 @@ # Phase 93: ConditionOnly Derived Slot(Trim / body-local) -Status: Active +Status: ✅ Done(P0+P1) Scope: Pattern2(Loop with Break)で「ConditionOnly(PHIで運ばない派生値)」を毎イテレーション再計算できるようにする。 Related: - 設計地図(入口): `docs/development/current/main/design/joinir-design-map.md` @@ -27,6 +27,19 @@ JoinIR で “初回の計算値が固定される” 事故を避ける。 - Trim: `src/mir/builder/control_flow/joinir/patterns/trim_loop_lowering.rs` - ConditionOnly 用 break 生成(反転の有無を明示) +## 成果(P1): 箱化・語彙のSSOT化 + +コミット: `c213ecc3 refactor(mir): Phase 93 リファクタリング - 箱化モジュール化` + +- schedule: + - `decide_pattern2_schedule()` に判定を集約し、理由(ConditionOnly / body-local / loop-local / default)をSSOT化 + - 決定→生成を分離(decision→build)してテスト容易性を上げた +- ConditionOnlyRecipe: + - `BreakSemantics`(WhenMatch / WhenNotMatch)を recipe に保持し、break 条件生成の責務を recipe 側へ移動 + - `trim_loop_lowering.rs` 側の重複ヘルパーを削除 +- Debug(新 env 追加なし): + - 既存の `NYASH_JOINIR_DEBUG=1` の範囲で、`[phase93/*]` prefix に統一 + ## 受け入れ基準(P0) - `apps/tests/loop_min_while.hako` が退行しない(Pattern2 baseline) diff --git a/docs/development/current/main/phases/phase-94/README.md b/docs/development/current/main/phases/phase-94/README.md new file mode 100644 index 00000000..a6004688 --- /dev/null +++ b/docs/development/current/main/phases/phase-94/README.md @@ -0,0 +1,6 @@ +# Phase 94: P5b “完全E2E” のための `ch` 再代入対応 + +- 目的: `tools/selfhost/test_pattern5b_escape_minimal.hako` を JoinIR(Pattern2Break)で VM E2E PASS に固定する。 +- 新箱: `BodyLocalDerivedEmitter`(`src/mir/join_ir/lowering/common/body_local_derived_emitter.rs`)で `ch` を Select ベースの derived 値として表現する。 +- 契約: `escape_cond` は base 値で評価し、override は副作用なし・評価順を SSOT 化。`HAKO_JOINIR_STRICT=1` では未対応形を理由付き Fail-Fast。 + diff --git a/src/mir/builder/control_flow/joinir/merge/README.md b/src/mir/builder/control_flow/joinir/merge/README.md new file mode 100644 index 00000000..e7d6fad0 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/merge/README.md @@ -0,0 +1,20 @@ +# JoinIR Merge Coordinator(箱の地図) + +責務: JoinIR 生成物をホスト MIR に統合するフェーズ(block/ValueId remap → rewrite → PHI/ExitLine 再接続)。 + +主な箱と入口: +- `instruction_rewriter.rs` — JoinIR→MIR 命令を書き換えつつブロックを組み立てるメイン導線 +- `phi_block_remapper.rs` — PHI の block-id だけを再マップする専用箱(ValueId は remapper 済みを前提) +- `loop_header_phi_builder.rs` / `exit_phi_builder.rs` — ヘッダ/出口 PHI の組み立て +- `inline_boundary_injector.rs` — Host↔JoinIR の ValueId 接続(Boundary 注入) +- `value_collector.rs` / `block_allocator.rs` — 再マップ前の収集と ID 割り当て +- `tail_call_classifier.rs` — tail call 判定(loop_step の末尾呼び出し検出) + +Fail-Fast の基本: +- block/ValueId 衝突や PHI 契約違反は握りつぶさず `Err` で止める +- remap は「ValueId remap」→「block-id remap」の順で一貫させる(PHI 二度 remap は禁止) + +拡張時のチェックリスト: +1. 新しい JoinInst → MIR 変換を追加する場合、`instruction_rewriter` に閉じて追加し、PHI/ExitLine 契約が壊れないか確認。 +2. PHI の入力ブロックを触るときは `phi_block_remapper` 経由に寄せて二重 remap を防ぐ。 +3. 増やした box/契約はここ(README)に一言追記して入口を明示。 diff --git a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs index 7b6d8345..1c545336 100644 --- a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs +++ b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs @@ -348,25 +348,10 @@ pub(super) fn merge_and_rewrite( then_bb: local_block_map.get(&then_bb).copied().unwrap_or(then_bb), else_bb: local_block_map.get(&else_bb).copied().unwrap_or(else_bb), }, - MirInstruction::Phi { - dst, - inputs, - type_hint: None, - } => MirInstruction::Phi { - dst, - // Phase 196: Fix Select expansion PHI - ValueIds are ALREADY remapped by remap_instruction() - // We only need to remap block IDs here (not ValueIds!) - inputs: inputs - .iter() - .map(|(bb, val)| { - let remapped_bb = local_block_map.get(bb).copied().unwrap_or(*bb); - // Phase 196 FIX: Don't double-remap values! - // remapper.remap_instruction() already remapped *val - (remapped_bb, *val) - }) - .collect(), - type_hint: None, - }, + MirInstruction::Phi { dst, inputs, type_hint } => { + use super::phi_block_remapper::remap_phi_instruction; + remap_phi_instruction(dst, &inputs, type_hint, &local_block_map) + } other => other, }; diff --git a/src/mir/builder/control_flow/joinir/merge/mod.rs b/src/mir/builder/control_flow/joinir/merge/mod.rs index d928b65d..df8a83bc 100644 --- a/src/mir/builder/control_flow/joinir/merge/mod.rs +++ b/src/mir/builder/control_flow/joinir/merge/mod.rs @@ -23,6 +23,7 @@ mod instruction_rewriter; mod loop_header_phi_builder; mod loop_header_phi_info; mod merge_result; +mod phi_block_remapper; // Phase 94: Phi block-id remap box mod tail_call_classifier; mod value_collector; diff --git a/src/mir/builder/control_flow/joinir/merge/phi_block_remapper.rs b/src/mir/builder/control_flow/joinir/merge/phi_block_remapper.rs new file mode 100644 index 00000000..e0d12b3c --- /dev/null +++ b/src/mir/builder/control_flow/joinir/merge/phi_block_remapper.rs @@ -0,0 +1,86 @@ +//! Box: PHI block remapper (Phase 94 tidy-up) +//! +//! Responsibility: remap only the block ids of MIR `Phi` inputs using the +//! caller-provided `local_block_map`, leaving ValueIds untouched (already +//! remapped upstream). Handles both `type_hint = None` and `Some(...)`. + +use std::collections::BTreeMap; + +use crate::mir::{BasicBlockId, MirInstruction, MirType, ValueId}; + +/// Remap a single PHI instruction's block ids. +pub(crate) fn remap_phi_instruction( + dst: ValueId, + inputs: &[(BasicBlockId, ValueId)], + type_hint: Option, + local_block_map: &BTreeMap, +)-> MirInstruction { + MirInstruction::Phi { + dst, + inputs: remap_phi_inputs(inputs, local_block_map), + type_hint, + } +} + +fn remap_phi_inputs( + inputs: &[(BasicBlockId, ValueId)], + local_block_map: &BTreeMap, +) -> Vec<(BasicBlockId, ValueId)> { + inputs + .iter() + .map(|(bb, val)| { + let remapped_bb = local_block_map.get(bb).copied().unwrap_or(*bb); + (remapped_bb, *val) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn bb(id: u32) -> BasicBlockId { + BasicBlockId(id) + } + + #[test] + fn remaps_blocks_preserves_values_and_type_none() { + let inputs = vec![(bb(1), ValueId(10)), (bb(2), ValueId(11))]; + let mut map = BTreeMap::new(); + map.insert(bb(1), bb(10)); + map.insert(bb(99), bb(100)); // unused entry should not matter + + let inst = remap_phi_instruction(ValueId(5), &inputs, None, &map); + + match inst { + MirInstruction::Phi { dst, inputs, type_hint } => { + assert_eq!(dst, ValueId(5)); + assert_eq!( + inputs, + vec![(bb(10), ValueId(10)), (bb(2), ValueId(11))] + ); + assert!(type_hint.is_none()); + } + other => panic!("expected Phi, got {:?}", other), + } + } + + #[test] + fn remaps_blocks_preserves_type_hint() { + let inputs = vec![(bb(3), ValueId(20))]; + let mut map = BTreeMap::new(); + map.insert(bb(3), bb(30)); + + let inst = remap_phi_instruction(ValueId(7), &inputs, Some(MirType::Integer), &map); + + match inst { + MirInstruction::Phi { dst, inputs, type_hint } => { + assert_eq!(dst, ValueId(7)); + assert_eq!(inputs, vec![(bb(30), ValueId(20))]); + assert_eq!(type_hint, Some(MirType::Integer)); + } + other => panic!("expected Phi, got {:?}", other), + } + } +} + diff --git a/src/mir/builder/control_flow/joinir/patterns/escape_pattern_recognizer.rs b/src/mir/builder/control_flow/joinir/patterns/escape_pattern_recognizer.rs index 6cc94042..1feb3c93 100644 --- a/src/mir/builder/control_flow/joinir/patterns/escape_pattern_recognizer.rs +++ b/src/mir/builder/control_flow/joinir/patterns/escape_pattern_recognizer.rs @@ -20,6 +20,10 @@ pub struct EscapeSkipPatternInfo { pub counter_name: String, pub normal_delta: i64, pub escape_delta: i64, + /// Index of the break-guard `if ... { break }` within the loop body. + pub break_idx: usize, + /// Index of the escape `if` within the loop body. + pub escape_idx: usize, /// Phase 92 P0-3: The condition expression for conditional increment /// e.g., `ch == '\\'` for escape sequence handling pub escape_cond: Box, @@ -74,6 +78,8 @@ pub fn detect_escape_skip_pattern(body: &[ASTNode]) -> Option` exists at top level +/// - escape if exists (detected by EscapeSkipPatternInfo) +/// - inside the escape if's then-body, after pre-increment: +/// - optional bounds `if i < n { ch = }` +/// - or direct `ch = ` +pub fn classify_p5b_escape_derived( + body: &[ASTNode], + loop_var_name: &str, +) -> P5bEscapeDerivedDecision { + let Some(info) = super::ast_feature_extractor::detect_escape_skip_pattern(body) else { + return P5bEscapeDerivedDecision::None; + }; + + if info.counter_name != loop_var_name { + // Not the loop counter we lower as the JoinIR loop var; ignore to avoid misrouting. + return P5bEscapeDerivedDecision::None; + } + + match build_recipe_from_info(body, &info) { + Ok(Some(recipe)) => P5bEscapeDerivedDecision::UseDerived(recipe), + Ok(None) => { + // Escape pattern exists but there is no body-local reassignment to cover. + P5bEscapeDerivedDecision::None + } + Err(e) => { + if joinir_dev::strict_enabled() { + P5bEscapeDerivedDecision::Reject(error_tags::freeze(&e)) + } else { + // Non-strict mode: keep legacy behavior (no derived slot); still loggable via dev. + P5bEscapeDerivedDecision::None + } + } + } +} + +fn build_recipe_from_info( + body: &[ASTNode], + info: &EscapeSkipPatternInfo, +) -> Result, String> { + // 1) Find base init: `local ch = ` + let Some(base_init_expr) = find_local_init_expr(body, "ch") else { + return Err("[phase94/body_local_derived/contract/missing_local_init] Missing `local ch = `".to_string()); + }; + + // 2) Locate escape if and find override assignment to ch + let escape_if = body.get(info.escape_idx).ok_or_else(|| { + format!( + "[phase94/body_local_derived/contract/escape_idx_oob] escape_idx={} out of bounds (body.len={})", + info.escape_idx, + body.len() + ) + })?; + let (escape_cond, then_body) = match escape_if { + ASTNode::If { condition, then_body, else_body: _, .. } => (condition.as_ref().clone(), then_body.as_slice()), + other => { + return Err(format!( + "[phase94/body_local_derived/contract/escape_node_kind] escape_idx points to non-If: {:?}", + other + )); + } + }; + + let override_assignment = find_ch_override_in_escape_then(then_body)?; + let Some((bounds_check, override_expr)) = override_assignment else { + return Ok(None); + }; + + // EscapeSkipPatternInfo uses "escape_delta" for the then-body increment, and "normal_delta" for the unconditional tail. + // For the common P5b shape: + // - escape iteration total delta = escape_delta + normal_delta + // - normal iteration total delta = normal_delta + let recipe = BodyLocalDerivedRecipe { + name: "ch".to_string(), + base_init_expr, + escape_cond, + loop_counter_name: info.counter_name.clone(), + pre_delta: info.escape_delta, + post_delta: info.normal_delta, + bounds_check, + override_expr, + }; + Ok(Some(recipe)) +} + +fn find_local_init_expr(body: &[ASTNode], name: &str) -> Option { + for node in body { + if let ASTNode::Local { variables, initial_values, .. } = node { + for (var_name, maybe_expr) in variables.iter().zip(initial_values.iter()) { + if var_name == name { + if let Some(expr) = maybe_expr.as_ref() { + return Some((**expr).clone()); + } + } + } + } + } + None +} + +/// Find `ch = ` either directly or under an inner bounds `if`. +/// +/// Returns: +/// - Ok(Some((bounds_opt, override_expr))) when an override assignment exists +/// - Ok(None) when no override assignment exists (no derived slot needed) +/// - Err when an override exists but violates minimal contract +fn find_ch_override_in_escape_then( + then_body: &[ASTNode], +) -> Result, ASTNode)>, String> { + // Direct assignment form: `ch = ` + for stmt in then_body { + if let ASTNode::Assignment { target, value, .. } = stmt { + if is_var_named(target.as_ref(), "ch") { + return Ok(Some((None, value.as_ref().clone()))); + } + } + } + + // Nested bounds form: `if { ch = }` + for stmt in then_body { + if let ASTNode::If { condition, then_body, else_body: None, .. } = stmt { + if then_body.len() != 1 { + continue; + } + if let ASTNode::Assignment { target, value, .. } = &then_body[0] { + if is_var_named(target.as_ref(), "ch") { + return Ok(Some((Some(condition.as_ref().clone()), value.as_ref().clone()))); + } + } + } + } + + Ok(None) +} + +fn is_var_named(node: &ASTNode, name: &str) -> bool { + matches!(node, ASTNode::Variable { name: n, .. } if n == name) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{BinaryOperator, Span}; + + fn var(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: Span::unknown(), + } + } + + fn str_lit(s: &str) -> ASTNode { + ASTNode::Literal { + value: crate::ast::LiteralValue::String(s.to_string()), + span: Span::unknown(), + } + } + + fn int_lit(v: i64) -> ASTNode { + ASTNode::Literal { + value: crate::ast::LiteralValue::Integer(v), + span: Span::unknown(), + } + } + + fn binop(op: BinaryOperator, lhs: ASTNode, rhs: ASTNode) -> ASTNode { + ASTNode::BinaryOp { + operator: op, + left: Box::new(lhs), + right: Box::new(rhs), + span: Span::unknown(), + } + } + + fn assignment(target: ASTNode, value: ASTNode) -> ASTNode { + ASTNode::Assignment { + target: Box::new(target), + value: Box::new(value), + span: Span::unknown(), + } + } + + fn method_call(obj: &str, method: &str, args: Vec) -> ASTNode { + ASTNode::MethodCall { + object: Box::new(var(obj)), + method: method.to_string(), + arguments: args, + span: Span::unknown(), + } + } + + #[test] + fn detects_p5b_shape_and_builds_recipe() { + // Body layout: + // 0: local ch = s.substring(i, i+1) + // 1: if ch == "\"" { break } + // 2: if ch == "\\" { i = i + 1; ch = s.substring(i, i+1) } + // 3: i = i + 1 + let body = vec![ + ASTNode::Local { + variables: vec!["ch".to_string()], + initial_values: vec![Some(Box::new(method_call( + "s", + "substring", + vec![var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))], + )))], + span: Span::unknown(), + }, + ASTNode::If { + condition: Box::new(binop(BinaryOperator::Equal, var("ch"), str_lit("\""))), + then_body: vec![ASTNode::Break { span: Span::unknown() }], + else_body: None, + span: Span::unknown(), + }, + ASTNode::If { + condition: Box::new(binop(BinaryOperator::Equal, var("ch"), str_lit("\\"))), + then_body: vec![ + assignment(var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))), + assignment( + var("ch"), + method_call( + "s", + "substring", + vec![var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))], + ), + ), + ], + else_body: None, + span: Span::unknown(), + }, + assignment(var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))), + ]; + + match classify_p5b_escape_derived(&body, "i") { + P5bEscapeDerivedDecision::UseDerived(recipe) => { + assert_eq!(recipe.name, "ch"); + assert_eq!(recipe.loop_counter_name, "i"); + assert_eq!(recipe.pre_delta, 1); + assert_eq!(recipe.post_delta, 1); + match recipe.override_expr { + ASTNode::MethodCall { ref method, .. } => assert_eq!(method, "substring"), + other => panic!("expected override MethodCall, got {:?}", other), + } + } + other => panic!("expected UseDerived recipe, got {:?}", other), + } + } + + #[test] + fn strict_rejects_when_local_init_missing() { + // escape pattern exists, but `local ch = ...` is absent -> strict should reject + let body = vec![ + ASTNode::If { + condition: Box::new(binop(BinaryOperator::Equal, var("ch"), str_lit("\""))), + then_body: vec![ASTNode::Break { span: Span::unknown() }], + else_body: None, + span: Span::unknown(), + }, + ASTNode::If { + condition: Box::new(binop(BinaryOperator::Equal, var("ch"), str_lit("\\"))), + then_body: vec![ + assignment(var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))), + assignment( + var("ch"), + method_call( + "s", + "substring", + vec![var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))], + ), + ), + ], + else_body: None, + span: Span::unknown(), + }, + assignment(var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))), + ]; + + let prev = std::env::var("HAKO_JOINIR_STRICT").ok(); + std::env::set_var("HAKO_JOINIR_STRICT", "1"); + let decision = classify_p5b_escape_derived(&body, "i"); + if let Some(v) = prev { + std::env::set_var("HAKO_JOINIR_STRICT", v); + } else { + std::env::remove_var("HAKO_JOINIR_STRICT"); + } + + match decision { + P5bEscapeDerivedDecision::Reject(reason) => { + assert!( + reason.contains("missing_local_init"), + "unexpected reason: {}", + reason + ); + } + other => panic!("expected Reject, got {:?}", other), + } + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs index 95ab5f04..48b24e4a 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs @@ -7,6 +7,7 @@ use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierInit}; use crate::mir::join_ir::lowering::condition_env::{ConditionBinding, ConditionEnv}; use super::body_local_policy::{classify_for_pattern2, BodyLocalPolicyDecision}; use crate::mir::join_ir::lowering::common::body_local_slot::ReadOnlyBodyLocalSlot; +use crate::mir::join_ir::lowering::common::body_local_derived_emitter::BodyLocalDerivedRecipe; use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox; use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; @@ -16,6 +17,7 @@ use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr; use crate::mir::loop_pattern_detection::error_messages; use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv; use crate::mir::ValueId; +use super::p5b_escape_derived_policy::{classify_p5b_escape_derived, P5bEscapeDerivedDecision}; use std::collections::BTreeMap; struct Pattern2DebugLog { @@ -56,6 +58,8 @@ struct Pattern2Inputs { break_condition_node: ASTNode, /// Phase 93 P0: ConditionOnly recipe for derived slot recalculation condition_only_recipe: Option, + /// Phase 94: BodyLocalDerived recipe for P5b "ch" reassignment + escape counter. + body_local_derived_recipe: Option, } fn prepare_pattern2_inputs( @@ -254,6 +258,7 @@ fn prepare_pattern2_inputs( read_only_body_local_slot: None, break_condition_node, condition_only_recipe: None, // Phase 93 P0: Will be set by apply_trim_and_normalize + body_local_derived_recipe: None, // Phase 94: Will be set after normalization }) } @@ -807,6 +812,42 @@ impl MirBuilder { apply_trim_and_normalize(self, condition, _body, &mut inputs, verbose)?; let analysis_body = normalized_body.as_deref().unwrap_or(_body); + // Phase 94: Detect P5b escape-derived (`ch` reassignment + escape counter). + // Route is explicit; in strict mode, partial matches that require derived support must fail-fast. + match classify_p5b_escape_derived(analysis_body, &inputs.loop_var_name) { + P5bEscapeDerivedDecision::UseDerived(recipe) => { + log.log( + "phase94", + format!( + "Phase 94: Enabled BodyLocalDerived for '{}' (counter='{}', pre_delta={}, post_delta={})", + recipe.name, recipe.loop_counter_name, recipe.pre_delta, recipe.post_delta + ), + ); + inputs.body_local_derived_recipe = Some(recipe); + } + P5bEscapeDerivedDecision::Reject(reason) => { + return Err(format!("[cf_loop/pattern2] {}", reason)); + } + P5bEscapeDerivedDecision::None => { + // Strict-only guard: if we see a body-local reassignment to `ch`, don't silently miscompile. + let has_ch_reassign = analysis_body.iter().any(|n| match n { + ASTNode::Assignment { target, .. } => matches!( + target.as_ref(), + ASTNode::Variable { name, .. } if name == "ch" + ), + _ => false, + }); + if crate::config::env::joinir_dev::strict_enabled() && has_ch_reassign { + return Err(format!( + "[cf_loop/pattern2] {}", + crate::mir::join_ir::lowering::error_tags::freeze( + "[phase94/body_local_derived/contract/unhandled_reassign] Body-local reassignment to 'ch' detected but not supported by Phase 94 recipe" + ) + )); + } + } + } + use crate::mir::join_ir::lowering::loop_update_analyzer::LoopUpdateAnalyzer; let carrier_updates = LoopUpdateAnalyzer::analyze_carrier_updates( analysis_body, @@ -894,6 +935,7 @@ impl MirBuilder { join_value_space, skeleton, condition_only_recipe: inputs.condition_only_recipe.as_ref(), // Phase 93 P0 + body_local_derived_recipe: inputs.body_local_derived_recipe.as_ref(), // Phase 94 }; let (join_module, fragment_meta) = match lower_loop_with_break_minimal(lowering_inputs) { diff --git a/src/mir/builder/control_flow/joinir/patterns/policies/README.md b/src/mir/builder/control_flow/joinir/patterns/policies/README.md new file mode 100644 index 00000000..cad994dc --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/policies/README.md @@ -0,0 +1,235 @@ +# JoinIR Pattern Policies - ルーティング箱の責務 + +## 概要 +このディレクトリには、Pattern認識とルーティング(policy決定)を行う「箱」が格納されています。 + +## Policy箱の責務 + +### ルーティング決定 +- 入力: LoopSkeleton、break条件、carrier情報 +- 出力: 適用可能なPattern(Pattern2, Pattern3, etc.)とLoweringResult +- 判断基準: パターンマッチング条件(Trim, ConditionalStep, Escape, etc.) + +### Lowering Result生成 +- Pattern固有の情報(ConditionOnlyRecipe, ConditionalStepInfo, etc.)を生成 +- CarrierInfo拡張(promoted variables, trim_helper, etc.) +- ConditionBinding設定 + +## Policy箱の候補(将来の整理対象) + +### trim_loop_lowering.rs (Phase 180/92/93) +**現在の場所**: `patterns/trim_loop_lowering.rs` + +**責務**: Trimパターン認識とConditionOnlyルーティング + +**判断基準**: +- LoopBodyLocal変数がTrim条件(whitespace check) +- break条件がConditionOnly(毎イテレーション再計算) + +**出力**: +- `TrimLoweringResult` - condition, carrier_info, condition_only_recipe + +**将来的な整理**: +- Trimパターン判断ロジックをpolicies/へ移動検討 +- 現在はpatterns/直下に配置(Phase 180統合済み) + +--- + +### p5b_escape_derived_policy.rs (Phase 94) +**現在の場所**: `patterns/p5b_escape_derived_policy.rs` + +**責務**: P5b escapeパターン認識とBodyLocalDerivedルーティング + +**判断基準**: +- body-local変数の再代入(例: `ch = s.substring(...)`) +- escape skip条件(例: `if ch == "\\" { i = i + 2 }`) +- loop変数の条件付き更新 + +**出力**: +- `P5bEscapeDerivedDecision::UseDerived(BodyLocalDerivedRecipe)` - escape recipe +- `P5bEscapeDerivedDecision::Reject(String)` - 検出失敗理由 +- `P5bEscapeDerivedDecision::None` - 該当なし + +**将来的な整理**: +- policies/へ移動してpolicy箱として統一 +- 現在はpatterns/直下に配置 + +--- + +### body_local_policy.rs +**現在の場所**: `patterns/body_local_policy.rs` + +**責務**: Body-local変数の昇格判断 + +**判断基準**: +- body-local変数がloop carrier候補か +- 昇格可能なパターンか(Trim, Escape, etc.) + +**将来的な整理**: +- policies/へ移動してpolicy箱として統一 + +--- + +## 設計原則 + +### 単一判断の原則 +- 各policy箱は1つのパターン判断のみ +- 複数パターンの判断は別のpolicy箱に委譲 + +### 非破壊的判断 +- 入力を変更しない +- 判断結果をResultで返す(`Ok(Some(result))` / `Ok(None)` / `Err(msg)`) + +### Fail-Fast +- パターンマッチング失敗は即座に`Ok(None)`を返す +- エラーは明示的に`Err(msg)` +- Reject理由は`error_tags::freeze()`でタグ付与 + +### Decision型の統一 +- `Decision` enum(None / Use(...) / Reject(String))を統一パターンとして使用 +- 例: `P5bEscapeDerivedDecision`, `TrimDecision`(将来) + +## 使用パターン + +### Pattern2ルーティング(現在のパターン) +```rust +// Step 1: P5b escapeパターン判断 +match classify_p5b_escape_derived(body, loop_var_name) { + P5bEscapeDerivedDecision::UseDerived(recipe) => { + // P5b escape適用 + return lower_with_p5b_escape(recipe, ...); + } + P5bEscapeDerivedDecision::Reject(reason) => { + return Err(reason); + } + P5bEscapeDerivedDecision::None => { + // 次のパターンへ + } +} + +// Step 2: Trimパターン判断 +if let Some(trim_result) = TrimLoopLowerer::try_lower_trim_pattern(...)? { + // Trimパターン適用 + return Ok(trim_result); +} + +// Step 3: デフォルトパターン +Ok(default_pattern2_lowering(...)) +``` + +### 将来の統一パターン(policies/移動後) +```rust +use patterns::policies::{P5bEscapePolicy, TrimPolicy, ConditionalStepPolicy}; + +// Policy boxの統一インターフェース +trait PatternPolicy { + type Decision; + fn classify(&self, context: &PatternContext) -> Self::Decision; +} + +// ルーティングパイプライン +let decision = P5bEscapePolicy.classify(&ctx) + .or_else(|| TrimPolicy.classify(&ctx)) + .or_else(|| ConditionalStepPolicy.classify(&ctx)) + .unwrap_or(DefaultDecision); +``` + +## デバッグ + +### ログprefixの統一 +- `[policy/p5b-escape]` - P5b escape判断 +- `[policy/trim]` - Trim判断 +- `[policy/conditional-step]` - ConditionalStep判断 + +### 環境変数 +- `HAKO_JOINIR_DEBUG=1` - JoinIR全般のデバッグログ +- `joinir_dev_enabled()` - 既存の制御機構 + +### デバッグ出力例 +```rust +if joinir_dev_enabled() { + eprintln!("[policy/p5b-escape] Detected escape pattern: {:?}", info); +} +``` + +## 命名規則 + +### ファイル命名 +- `<パターン名>_policy.rs` - パターン判断policy箱 +- 例: `trim_policy.rs`, `escape_policy.rs`, `conditional_step_policy.rs` + +### 型命名 +- `<パターン名>Decision` - 判断結果型 +- `<パターン名>Policy` - policy箱の構造体(将来) + +### 列挙型パターン +```rust +pub enum Decision { + None, // 該当なし + Use(Recipe), // 適用可能 + Reject(String), // 検出失敗 +} +``` + +## 将来の拡張 + +### Phase 95以降の候補 +- `trim_policy.rs` - Trimパターン判断をpolicies/へ移動 +- `escape_policy.rs` - P5b escapeを統一インターフェースに +- `array_loop_policy.rs` - 配列ループパターン判断 +- `map_loop_policy.rs` - Mapループパターン判断 +- `conditional_step_policy.rs` - ConditionalStep独立箱化 + +### 段階的な移行計画 + +#### Phase 1: ディレクトリ準備(今回) +- policies/ディレクトリ作成 ✅ +- README.md作成 ✅ +- mod.rs作成 ✅ + +#### Phase 2: 既存policy箱の移動(将来) +- `p5b_escape_derived_policy.rs` → `policies/escape_policy.rs` +- `body_local_policy.rs` → `policies/body_local_policy.rs` +- trim関連ロジック抽出 → `policies/trim_policy.rs` + +#### Phase 3: インターフェース統一(将来) +- `PatternPolicy` trait定義 +- Decision型の統一 +- ルーティングパイプラインの実装 + +### 拡張時の注意 +- README.mdにpolicy箱の責務を追加 +- Decision型を統一パターンに従う +- 既存のpolicy箱との一貫性を保つ +- Fail-Fastタグを明記 + +## 参考資料 + +### 関連ドキュメント +- [JoinIR アーキテクチャ概要](../../../../development/current/main/joinir-architecture-overview.md) +- [JoinIR 設計マップ](../../../../development/current/main/design/joinir-design-map.md) +- [Pattern Pipeline](../pattern_pipeline.rs) - パターン判断の統合処理 +- [Escape Pattern Recognizer](../escape_pattern_recognizer.rs) - escape検出ロジック + +### Phase Log +- [Phase 92 Log](../../../../development/current/main/phases/phase-92/) - ConditionalStep +- [Phase 93 Log](../../../../development/current/main/phases/phase-93/) - ConditionOnly +- [Phase 94 Log](../../../../development/current/main/phases/phase-94/) - BodyLocalDerived + +## 設計哲学(Box Theory) + +### 箱理論の適用 +- **箱にする**: Policy判断を独立した箱に分離 +- **境界を作る**: Decisionを統一インターフェースに +- **戻せる**: 段階的移行で既存コードを壊さない +- **見える化**: ログprefixで判断過程を可視化 + +### Fail-Fast原則の徹底 +- フォールバック処理は原則禁止 +- Reject理由を明示的に返す +- エラータグで根本原因を追跡可能に + +### 単一責任の箱 +- 1つのpolicy箱 = 1つのパターン判断 +- 複数パターンの組み合わせは上位層で制御 +- policy箱同士は独立・疎結合 diff --git a/src/mir/builder/control_flow/joinir/patterns/policies/mod.rs b/src/mir/builder/control_flow/joinir/patterns/policies/mod.rs new file mode 100644 index 00000000..67142e8a --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/policies/mod.rs @@ -0,0 +1,29 @@ +//! JoinIR Pattern Policies - パターン認識とルーティング決定 +//! +//! ## 概要 +//! このモジュールには、Pattern認識とルーティング(policy決定)を行う「箱」が格納されています。 +//! +//! ## Policy箱の責務 +//! - パターン認識: LoopSkeletonから特定のパターン(Trim, Escape, etc.)を検出 +//! - ルーティング決定: 適用可能なLoweringパターンを決定 +//! - Recipe生成: Pattern固有の情報(ConditionOnlyRecipe, BodyLocalDerivedRecipe, etc.)を生成 +//! +//! ## 設計原則 +//! - **単一判断の原則**: 各policy箱は1つのパターン判断のみ +//! - **非破壊的判断**: 入力を変更せず、Decision型で結果を返す +//! - **Fail-Fast**: パターンマッチング失敗は即座にReject/Noneを返す +//! +//! ## 将来の拡張 +//! 現在はpolicies/ディレクトリの準備段階です。 +//! 既存のpolicy関連ファイル(p5b_escape_derived_policy.rs, body_local_policy.rs等)は +//! patterns/直下に配置されていますが、将来的にこのディレクトリへ移動する予定です。 +//! +//! ### 段階的な移行計画 +//! - Phase 1: ディレクトリ準備(今回) ✅ +//! - Phase 2: 既存policy箱の移動(将来) +//! - Phase 3: インターフェース統一(将来) +//! +//! 詳細は [README.md](README.md) を参照してください。 + +// 現在は空モジュール(将来の拡張用) +// 既存のpolicy関連ファイルは親モジュール(patterns/)に配置されています diff --git a/src/mir/join_ir/lowering/common.rs b/src/mir/join_ir/lowering/common.rs index a9bf6a4d..460dd46d 100644 --- a/src/mir/join_ir/lowering/common.rs +++ b/src/mir/join_ir/lowering/common.rs @@ -7,6 +7,7 @@ pub mod conditional_step_emitter; // Phase 92 P1-1: ConditionalStep emission mod pub mod body_local_slot; // Phase 92 P3: Read-only body-local slot for conditions pub mod dual_value_rewriter; // Phase 246-EX/247-EX: name-based dual-value rewrites pub mod condition_only_emitter; // Phase 93 P0: ConditionOnly (Derived Slot) recalculation +pub mod body_local_derived_emitter; // Phase 94: Derived body-local (P5b escape "ch" reassignment) use crate::mir::loop_form::LoopForm; use crate::mir::query::{MirQuery, MirQueryBox}; diff --git a/src/mir/join_ir/lowering/common/README.md b/src/mir/join_ir/lowering/common/README.md new file mode 100644 index 00000000..4e8fd4fa --- /dev/null +++ b/src/mir/join_ir/lowering/common/README.md @@ -0,0 +1,13 @@ +# JoinIR Lowering Common Boxes + +このディレクトリは「JoinIR lowerer の縫い目」を担う小箱の入口だよ。責務を混ぜないためのメモ。 + +- `conditional_step_emitter.rs` — ConditionalStep 専用の更新生成(P5b の i+=1/2 など) +- `condition_only_emitter.rs` — ConditionOnly derived slot の再計算(Phase 93) +- `body_local_slot.rs` — 読み取り専用 body-local を条件式で使うためのガード付き抽出(Phase 92) +- `body_local_derived_emitter.rs` — 再代入される body-local(P5b `ch`)を Select で統合し、loop-var の +1/+2 も同時に出す(Phase 94) +- `dual_value_rewriter.rs` — name ベースの dual-value 書き換え(BodyLocal vs Carrier)を一箇所に閉じ込める + +Fail-Fast 原則: +- 未対応 shape は error_tags::freeze などで理由付き停止(サイレント回避禁止)。 +- フォールバック臭を出さず、ポリシーで「使う/使わない/拒否」を明示する。 diff --git a/src/mir/join_ir/lowering/common/body_local_derived_emitter.rs b/src/mir/join_ir/lowering/common/body_local_derived_emitter.rs new file mode 100644 index 00000000..0600d386 --- /dev/null +++ b/src/mir/join_ir/lowering/common/body_local_derived_emitter.rs @@ -0,0 +1,445 @@ +//! Phase 94: BodyLocalDerivedEmitter (P5b "complete E2E" for body-local reassignment) +//! +//! Goal: represent a loop body-local that is conditionally overridden (e.g. `ch`) +//! as a pure derived JoinIR value, without PHI-carrying it across iterations. +//! +//! This is intentionally minimal and fail-fast: +//! - Supports a single derived variable recipe (typically `ch`) +//! - Supports the P5b escape shape where: +//! - `ch_top` is computed by a top-level `local ch = ...` +//! - `escape_cond` is computed from `ch_top` (e.g. `ch == "\\"`) +//! - `ch` is conditionally overridden after a pre-increment of the loop counter +//! - loop counter update is `else_delta` always + `pre_delta` when `escape_cond` +//! +//! The extraction/policy lives on the Pattern2 builder side; this module only +//! emits JoinIR given a validated recipe. + +use crate::ast::ASTNode; +use crate::mir::join_ir::lowering::condition_env::ConditionEnv; +use crate::mir::join_ir::lowering::condition_lowerer::lower_condition_to_joinir; +use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; +use crate::mir::join_ir::lowering::method_call_lowerer::MethodCallLowerer; +use crate::mir::join_ir::{BinOpKind, ConstValue, JoinInst, MirLikeInst}; +use crate::mir::{MirType, ValueId}; + +/// SSOT recipe: "derived body-local" + loop-counter pre/post increment semantics. +#[derive(Debug, Clone)] +pub struct BodyLocalDerivedRecipe { + /// Derived variable name to register into LoopBodyLocalEnv (e.g. "ch"). + pub name: String, + /// Base init expression from `local name = ` (diagnostics only; lowering is done elsewhere). + pub base_init_expr: ASTNode, + /// Escape condition evaluated on the *base* value (e.g. `ch == "\\"`). + pub escape_cond: ASTNode, + /// Loop counter variable name (typically the loop var, e.g. "i"). + pub loop_counter_name: String, + /// Pre-increment applied inside escape branch before override expr (e.g. `i = i + 1`). + pub pre_delta: i64, + /// Post-increment applied at the loop tail (e.g. the unconditional `i = i + 1`). + pub post_delta: i64, + /// Optional bounds check after the pre-increment (e.g. `i < n` in the escape branch). + /// This is evaluated with `loop_counter_name` bound to `i_pre` (= i + pre_delta). + pub bounds_check: Option, + /// Override expression (e.g. `s.substring(i, i + 1)` in the escape branch), + /// evaluated with `loop_counter_name` bound to `i_pre`. + pub override_expr: ASTNode, +} + +#[derive(Debug, Clone, Copy)] +pub struct BodyLocalDerivedEmission { + /// ValueId of the escape condition (truthy) evaluated on the base body-local value. + pub escape_cond_id: ValueId, + /// ValueId of the loop counter next value (includes conditional pre-delta + post-delta). + pub loop_counter_next: ValueId, +} + +/// Generation box: emits JoinIR for the derived slot and the conditional loop-counter next. +pub struct BodyLocalDerivedEmitter; + +impl BodyLocalDerivedEmitter { + pub fn emit( + recipe: &BodyLocalDerivedRecipe, + alloc_value: &mut dyn FnMut() -> ValueId, + env: &ConditionEnv, + body_local_env: &mut LoopBodyLocalEnv, + instructions: &mut Vec, + ) -> Result { + let base_value = body_local_env.get(&recipe.name).ok_or_else(|| { + format!( + "[phase94/body_local_derived/contract/missing_base_value] Missing base ValueId for body-local '{}' in LoopBodyLocalEnv", + recipe.name + ) + })?; + + if recipe.pre_delta < 0 || recipe.post_delta < 0 { + return Err(format!( + "[phase94/body_local_derived/contract/negative_delta] Invalid deltas: pre_delta={}, post_delta={} (must be >= 0)", + recipe.pre_delta, recipe.post_delta + )); + } + if recipe.post_delta == 0 { + return Err(format!( + "[phase94/body_local_derived/contract/post_delta_zero] post_delta=0 is not supported (would stall loop counter '{}')", + recipe.loop_counter_name + )); + } + + // ------------------------------------------------------------ + // 1) escape_cond evaluated on base `name` (ch_top) + // ------------------------------------------------------------ + let (escape_cond_id, escape_cond_insts) = lower_condition_to_joinir( + &recipe.escape_cond, + alloc_value, + env, + Some(body_local_env), + )?; + instructions.extend(escape_cond_insts); + + // ------------------------------------------------------------ + // 2) i_pre = i + pre_delta (used for bounds + override expr) + // ------------------------------------------------------------ + let counter_cur = env.get(&recipe.loop_counter_name).ok_or_else(|| { + format!( + "[phase94/body_local_derived/contract/missing_loop_counter] ConditionEnv missing loop counter '{}'", + recipe.loop_counter_name + ) + })?; + + let counter_pre = if recipe.pre_delta == 0 { + counter_cur + } else { + let pre_const = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::Const { + dst: pre_const, + value: ConstValue::Integer(recipe.pre_delta), + })); + let pre_sum = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: pre_sum, + op: BinOpKind::Add, + lhs: counter_cur, + rhs: pre_const, + })); + pre_sum + }; + + // ------------------------------------------------------------ + // 3) override_guard = escape_cond && bounds_check(i_pre) (if present) + // ------------------------------------------------------------ + let override_guard = if let Some(bounds_ast) = &recipe.bounds_check { + let mut env_pre = env.clone(); + env_pre.insert(recipe.loop_counter_name.clone(), counter_pre); + let (bounds_ok, bounds_insts) = + lower_condition_to_joinir(bounds_ast, alloc_value, &env_pre, Some(body_local_env))?; + instructions.extend(bounds_insts); + + let guard = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: guard, + op: BinOpKind::And, + lhs: escape_cond_id, + rhs: bounds_ok, + })); + guard + } else { + escape_cond_id + }; + + // ------------------------------------------------------------ + // 4) override_val (evaluated with loop_counter_name := i_pre) + // ------------------------------------------------------------ + let mut env_pre = env.clone(); + env_pre.insert(recipe.loop_counter_name.clone(), counter_pre); + let override_val = Self::lower_override_expr( + &recipe.override_expr, + alloc_value, + &env_pre, + body_local_env, + instructions, + )?; + + // ------------------------------------------------------------ + // 5) derived = Select(override_guard, override_val, base_value) + // ------------------------------------------------------------ + let derived = alloc_value(); + instructions.push(JoinInst::Select { + dst: derived, + cond: override_guard, + then_val: override_val, + else_val: base_value, + type_hint: Some(MirType::String), + }); + body_local_env.insert(recipe.name.clone(), derived); + + // ------------------------------------------------------------ + // 6) loop_counter_next = Select(escape_cond, i + (pre+post), i + post) + // ------------------------------------------------------------ + let then_delta = recipe.pre_delta + recipe.post_delta; + let else_delta = recipe.post_delta; + if then_delta == else_delta { + return Err(format!( + "[phase94/body_local_derived/contract/equal_total_deltas] then_delta == else_delta == {} for loop counter '{}' (pre_delta={}, post_delta={})", + then_delta, recipe.loop_counter_name, recipe.pre_delta, recipe.post_delta + )); + } + + let then_const = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::Const { + dst: then_const, + value: ConstValue::Integer(then_delta), + })); + let then_sum = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: then_sum, + op: BinOpKind::Add, + lhs: counter_cur, + rhs: then_const, + })); + + let else_const = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::Const { + dst: else_const, + value: ConstValue::Integer(else_delta), + })); + let else_sum = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: else_sum, + op: BinOpKind::Add, + lhs: counter_cur, + rhs: else_const, + })); + + let counter_next = alloc_value(); + instructions.push(JoinInst::Select { + dst: counter_next, + cond: escape_cond_id, + then_val: then_sum, + else_val: else_sum, + type_hint: Some(MirType::Integer), + }); + + Ok(BodyLocalDerivedEmission { + escape_cond_id, + loop_counter_next: counter_next, + }) + } + + fn lower_override_expr( + expr: &ASTNode, + alloc_value: &mut dyn FnMut() -> ValueId, + env: &ConditionEnv, + body_local_env: &LoopBodyLocalEnv, + instructions: &mut Vec, + ) -> Result { + match expr { + ASTNode::MethodCall { + object, + method, + arguments, + .. + } => { + let recv_name = match object.as_ref() { + ASTNode::Variable { name, .. } => name, + _ => { + return Err(format!( + "[phase94/body_local_derived/contract/override_receiver] Override receiver must be a variable: {:?}", + object + )); + } + }; + + let recv_val = body_local_env + .get(recv_name) + .or_else(|| env.get(recv_name)) + .ok_or_else(|| { + format!( + "[phase94/body_local_derived/contract/override_receiver_missing] Receiver '{}' not found in envs", + recv_name + ) + })?; + + MethodCallLowerer::lower_for_init( + recv_val, + method, + arguments, + alloc_value, + env, + body_local_env, + instructions, + ) + } + _ => Err(format!( + "[phase94/body_local_derived/contract/override_expr_kind] Override expr must be MethodCall (pure): {:?}", + expr + )), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{BinaryOperator, LiteralValue, Span}; + use crate::mir::join_ir::lowering::condition_env::ConditionEnv; + + fn var(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: Span::unknown(), + } + } + + fn str_lit(s: &str) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::String(s.to_string()), + span: Span::unknown(), + } + } + + fn int_lit(i: i64) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::Integer(i), + span: Span::unknown(), + } + } + + fn binop(op: BinaryOperator, left: ASTNode, right: ASTNode) -> ASTNode { + ASTNode::BinaryOp { + operator: op, + left: Box::new(left), + right: Box::new(right), + span: Span::unknown(), + } + } + + fn method_call(obj: &str, method: &str, args: Vec) -> ASTNode { + ASTNode::MethodCall { + object: Box::new(var(obj)), + method: method.to_string(), + arguments: args, + span: Span::unknown(), + } + } + + #[test] + fn derived_emits_select_and_updates_env() { + // Pattern: + // base: ch_top already computed and in body_local_env + // escape_cond: ch == "\\" + // bounds: i < n (evaluated with i_pre) + // override_expr: s.substring(i, i + 1) (evaluated with i_pre) + let recipe = BodyLocalDerivedRecipe { + name: "ch".to_string(), + base_init_expr: method_call( + "s", + "substring", + vec![var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))], + ), + escape_cond: binop(BinaryOperator::Equal, var("ch"), str_lit("\\")), + loop_counter_name: "i".to_string(), + pre_delta: 1, + post_delta: 1, + bounds_check: Some(binop(BinaryOperator::Less, var("i"), var("n"))), + override_expr: method_call( + "s", + "substring", + vec![var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))], + ), + }; + + let mut env = ConditionEnv::new(); + env.insert("i".to_string(), ValueId(10)); + env.insert("n".to_string(), ValueId(11)); + env.insert("s".to_string(), ValueId(12)); + + let mut body_env = LoopBodyLocalEnv::new(); + body_env.insert("ch".to_string(), ValueId(20)); + + let mut next = 100u32; + let mut alloc_value = || { + let id = ValueId(next); + next += 1; + id + }; + + let mut insts = Vec::new(); + let out = BodyLocalDerivedEmitter::emit(&recipe, &mut alloc_value, &env, &mut body_env, &mut insts) + .expect("emit should succeed"); + + // Env must now point `ch` to the derived value (not the base 20). + let ch_now = body_env.get("ch").expect("ch should exist"); + assert_ne!(ch_now, ValueId(20)); + assert!(insts.iter().any(|i| matches!(i, JoinInst::Select { .. }))); + assert_ne!(out.loop_counter_next, ValueId(10)); + } + + #[test] + fn fails_fast_on_equal_total_deltas() { + let recipe = BodyLocalDerivedRecipe { + name: "ch".to_string(), + base_init_expr: str_lit("x"), + escape_cond: binop(BinaryOperator::Equal, var("ch"), str_lit("\\")), + loop_counter_name: "i".to_string(), + pre_delta: 0, + post_delta: 1, + bounds_check: None, + override_expr: method_call("s", "substring", vec![var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))]), + }; + + let mut env = ConditionEnv::new(); + env.insert("i".to_string(), ValueId(10)); + env.insert("s".to_string(), ValueId(12)); + + let mut body_env = LoopBodyLocalEnv::new(); + body_env.insert("ch".to_string(), ValueId(20)); + + let mut next = 200u32; + let mut alloc_value = || { + let id = ValueId(next); + next += 1; + id + }; + + let mut insts = Vec::new(); + let err = BodyLocalDerivedEmitter::emit(&recipe, &mut alloc_value, &env, &mut body_env, &mut insts) + .unwrap_err(); + assert!(err.contains("equal_total_deltas")); + } + + #[test] + fn fails_fast_on_unsupported_override_expr_kind() { + let recipe = BodyLocalDerivedRecipe { + name: "ch".to_string(), + base_init_expr: str_lit("x"), + escape_cond: binop(BinaryOperator::Equal, var("ch"), str_lit("\\")), + loop_counter_name: "i".to_string(), + pre_delta: 1, + post_delta: 1, + bounds_check: None, + // Literal is not allowed (must be MethodCall) + override_expr: str_lit("!"), + }; + + let mut env = ConditionEnv::new(); + env.insert("i".to_string(), ValueId(1)); + env.insert("s".to_string(), ValueId(2)); + + let mut body_env = LoopBodyLocalEnv::new(); + body_env.insert("ch".to_string(), ValueId(3)); + + let mut next = 10u32; + let mut alloc_value = || { + let id = ValueId(next); + next += 1; + id + }; + + let mut insts = Vec::new(); + let err = BodyLocalDerivedEmitter::emit(&recipe, &mut alloc_value, &env, &mut body_env, &mut insts) + .unwrap_err(); + assert!( + err.contains("override_expr_kind"), + "expected override_expr_kind contract violation, got: {err}" + ); + } +} diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal.rs b/src/mir/join_ir/lowering/loop_with_break_minimal.rs index 1d86b576..118bedc1 100644 --- a/src/mir/join_ir/lowering/loop_with_break_minimal.rs +++ b/src/mir/join_ir/lowering/loop_with_break_minimal.rs @@ -67,6 +67,7 @@ use crate::mir::join_ir::lowering::carrier_update_emitter::{ }; // Phase 92 P2-1: Import ConditionalStep emitter from dedicated module use crate::mir::join_ir::lowering::common::conditional_step_emitter::emit_conditional_step_update; +use crate::mir::join_ir::lowering::common::body_local_derived_emitter::BodyLocalDerivedRecipe; use crate::mir::join_ir::lowering::condition_to_joinir::ConditionEnv; use crate::mir::loop_canonicalizer::UpdateKind; use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; @@ -74,7 +75,7 @@ use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr; use crate::mir::join_ir::lowering::step_schedule::{ - build_pattern2_schedule, Pattern2ScheduleContext, Pattern2StepKind, + build_pattern2_schedule_from_decision, decide_pattern2_schedule, Pattern2StepKind, }; use crate::mir::join_ir::lowering::update_env::UpdateEnv; use crate::mir::loop_canonicalizer::LoopSkeleton; @@ -107,6 +108,8 @@ pub(crate) struct LoopWithBreakLoweringInputs<'a> { pub skeleton: Option<&'a LoopSkeleton>, /// Phase 93 P0: ConditionOnly recipe for derived slot recalculation pub condition_only_recipe: Option<&'a crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyRecipe>, + /// Phase 94: BodyLocalDerived recipe (P5b escape `ch` reassignment + conditional counter). + pub body_local_derived_recipe: Option<&'a BodyLocalDerivedRecipe>, } /// Lower Pattern 2 (Loop with Conditional Break) to JoinIR @@ -184,6 +187,7 @@ pub(crate) fn lower_loop_with_break_minimal( join_value_space, skeleton, condition_only_recipe, + body_local_derived_recipe, } = inputs; let mut body_local_env = body_local_env; @@ -377,12 +381,13 @@ pub(crate) fn lower_loop_with_break_minimal( // Decide evaluation order (header/body-init/break/updates/tail) up-front. // Phase 93 P0: Pass condition_only_recipe existence to schedule context. // When recipe exists, body-init must happen before break check. - let schedule_ctx = Pattern2ScheduleContext::from_env( + let schedule_decision = decide_pattern2_schedule( body_local_env.as_ref().map(|env| &**env), carrier_info, condition_only_recipe.is_some(), + body_local_derived_recipe.is_some(), ); - let schedule = build_pattern2_schedule(&schedule_ctx); + let schedule = build_pattern2_schedule_from_decision(&schedule_decision); // Collect fragments per step; append them according to the schedule below. let mut header_block: Vec = Vec::new(); @@ -390,6 +395,7 @@ pub(crate) fn lower_loop_with_break_minimal( let mut break_block: Vec = Vec::new(); let mut carrier_update_block: Vec = Vec::new(); let mut tail_block: Vec = Vec::new(); + let mut loop_var_next_override: Option = None; // Phase 94: conditional loop-var step (P5b) // ------------------------------------------------------------------ // Natural Exit Condition Check (Phase 169: from AST) @@ -523,6 +529,28 @@ pub(crate) fn lower_loop_with_break_minimal( cond: Some(break_cond_value), // Phase 170-B: Use lowered condition }); + // ------------------------------------------------------------------ + // Phase 94: P5b escape derived body-local + conditional loop-var update + // ------------------------------------------------------------------ + if let (Some(recipe), Some(ref mut body_env)) = (body_local_derived_recipe, body_local_env.as_mut()) + { + use crate::mir::join_ir::lowering::common::body_local_derived_emitter::BodyLocalDerivedEmitter; + let emission = BodyLocalDerivedEmitter::emit( + recipe, + &mut alloc_value, + env, + body_env, + &mut carrier_update_block, + )?; + loop_var_next_override = Some(emission.loop_counter_next); + dev_log.log_if_enabled(|| { + format!( + "[phase94/body_local_derived] enabled: name='{}', loop_counter='{}', loop_counter_next={:?}", + recipe.name, recipe.loop_counter_name, emission.loop_counter_next + ) + }); + } + // ------------------------------------------------------------------ // Loop Body: Compute updated values for all carriers // ------------------------------------------------------------------ @@ -684,20 +712,25 @@ pub(crate) fn lower_loop_with_break_minimal( // Phase 176-3: Multi-carrier support - tail call includes all updated carriers // Call(loop_step, [i_next, carrier1_next, carrier2_next, ...]) // tail recursion - // Note: We need to emit i_next = i + 1 first for the loop variable - let const_1 = alloc_value(); - tail_block.push(JoinInst::Compute(MirLikeInst::Const { - dst: const_1, - value: ConstValue::Integer(1), - })); + // Phase 94: i_next may be overridden (escape skip: +2 vs +1). + let i_next = if let Some(i_next) = loop_var_next_override { + i_next + } else { + let const_1 = alloc_value(); + tail_block.push(JoinInst::Compute(MirLikeInst::Const { + dst: const_1, + value: ConstValue::Integer(1), + })); - let i_next = alloc_value(); - tail_block.push(JoinInst::Compute(MirLikeInst::BinOp { - dst: i_next, - op: BinOpKind::Add, - lhs: i_param, - rhs: const_1, - })); + let i_next = alloc_value(); + tail_block.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: i_next, + op: BinOpKind::Add, + lhs: i_param, + rhs: const_1, + })); + i_next + }; let mut tail_call_args = vec![i_next]; tail_call_args.extend(updated_carrier_values.iter().copied()); diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal/tests.rs b/src/mir/join_ir/lowering/loop_with_break_minimal/tests.rs index c004e2eb..53202056 100644 --- a/src/mir/join_ir/lowering/loop_with_break_minimal/tests.rs +++ b/src/mir/join_ir/lowering/loop_with_break_minimal/tests.rs @@ -147,6 +147,7 @@ fn test_pattern2_header_condition_via_exprlowerer() { join_value_space: &mut join_value_space, skeleton: None, // Phase 92 P0-3: skeleton=None for backward compatibility condition_only_recipe: None, // Phase 93 P0: None for normal loops + body_local_derived_recipe: None, // Phase 94: None for normal loops }); assert!(result.is_ok(), "ExprLowerer header path should succeed"); diff --git a/src/mir/join_ir/lowering/method_call_lowerer.rs b/src/mir/join_ir/lowering/method_call_lowerer.rs index 8e7b74bb..32b09aeb 100644 --- a/src/mir/join_ir/lowering/method_call_lowerer.rs +++ b/src/mir/join_ir/lowering/method_call_lowerer.rs @@ -39,6 +39,60 @@ use crate::mir::ValueId; use crate::runtime::core_box_ids::CoreMethodId; use super::condition_env::ConditionEnv; +use super::loop_body_local_env::LoopBodyLocalEnv; +use super::debug_output_box::DebugOutputBox; + +/// Box: resolves method call arguments with cascading lookup (body-local → condition). +struct CascadingArgResolver<'a> { + cond_env: &'a ConditionEnv, + body_local_env: &'a LoopBodyLocalEnv, + debug: DebugOutputBox, +} + +impl<'a> CascadingArgResolver<'a> { + fn new(cond_env: &'a ConditionEnv, body_local_env: &'a LoopBodyLocalEnv) -> Self { + Self { + cond_env, + body_local_env, + debug: DebugOutputBox::new_dev("method_call_lowerer"), + } + } + + fn resolve( + &self, + expr: &ASTNode, + alloc_value: &mut dyn FnMut() -> ValueId, + instructions: &mut Vec, + ) -> Result { + match expr { + // Variables - check body_local_env first, then cond_env + ASTNode::Variable { name, .. } => { + if let Some(vid) = self.body_local_env.get(name) { + self.debug + .log_if_enabled(|| format!("Arg '{}' found in LoopBodyLocalEnv → {:?}", name, vid)); + Ok(vid) + } else if let Some(vid) = self.cond_env.get(name) { + self.debug + .log_if_enabled(|| format!("Arg '{}' found in ConditionEnv → {:?}", name, vid)); + Ok(vid) + } else { + Err(format!( + "Variable '{}' not found in LoopBodyLocalEnv or ConditionEnv", + name + )) + } + } + // Non-variables delegate to value expression lowering (body-local not needed) + _ => super::condition_lowerer::lower_value_expression( + expr, + alloc_value, + self.cond_env, + None, // body-local not used for generic expressions + instructions, + ), + } + } +} /// Phase 224-B: MethodCall Lowerer Box /// @@ -167,18 +221,15 @@ impl MethodCallLowerer { /// - Arguments can reference previously defined body-local variables /// - Checks `body_local_env` first, then `cond_env` for variable resolution /// - Example: `local digit_pos = digits.indexOf(ch)` where `ch` is body-local - pub fn lower_for_init( + pub fn lower_for_init( recv_val: ValueId, method_name: &str, args: &[ASTNode], - alloc_value: &mut F, + alloc_value: &mut dyn FnMut() -> ValueId, cond_env: &ConditionEnv, - body_local_env: &super::loop_body_local_env::LoopBodyLocalEnv, + body_local_env: &LoopBodyLocalEnv, instructions: &mut Vec, - ) -> Result - where - F: FnMut() -> ValueId, - { + ) -> Result { // Resolve method name to CoreMethodId let method_id = CoreMethodId::iter() .find(|m| m.name() == method_name) @@ -211,15 +262,10 @@ impl MethodCallLowerer { // Phase 226: Lower arguments with cascading LoopBodyLocal support // Check body_local_env first, then cond_env + let resolver = CascadingArgResolver::new(cond_env, body_local_env); let mut lowered_args = Vec::new(); for arg_ast in args { - let arg_val = Self::lower_arg_with_cascading( - arg_ast, - alloc_value, - cond_env, - body_local_env, - instructions, - )?; + let arg_val = resolver.resolve(arg_ast, alloc_value, instructions)?; lowered_args.push(arg_val); } @@ -241,81 +287,12 @@ impl MethodCallLowerer { Ok(dst) } - /// Phase 226: Lower an argument expression with cascading LoopBodyLocal support - /// - /// This function extends `condition_lowerer::lower_value_expression` to support - /// cascading LoopBodyLocal variables. It checks both environments: - /// 1. LoopBodyLocalEnv first (for previously defined body-local variables) - /// 2. ConditionEnv as fallback (for loop condition variables) - /// - /// # Example - /// - /// ```nyash - /// local ch = s.substring(p, p+1) // ch stored in body_local_env - /// local digit_pos = digits.indexOf(ch) // ch resolved from body_local_env - /// ``` - /// - /// # Arguments - /// - /// * `expr` - Argument AST node - /// * `alloc_value` - ValueId allocator - /// * `cond_env` - Condition environment (fallback) - /// * `body_local_env` - LoopBodyLocal environment (priority) - /// * `instructions` - Instruction buffer - /// - /// # Returns - /// - /// * `Ok(ValueId)` - Lowered argument value - /// * `Err(String)` - If variable not found in either environment - fn lower_arg_with_cascading( - expr: &ASTNode, - alloc_value: &mut F, - cond_env: &ConditionEnv, - body_local_env: &super::loop_body_local_env::LoopBodyLocalEnv, - instructions: &mut Vec, - ) -> Result - where - F: FnMut() -> ValueId, - { - match expr { - // Variables - check body_local_env first, then cond_env - ASTNode::Variable { name, .. } => { - if let Some(vid) = body_local_env.get(name) { - eprintln!( - "[method_call_lowerer] Arg '{}' found in LoopBodyLocalEnv → {:?}", - name, vid - ); - Ok(vid) - } else if let Some(vid) = cond_env.get(name) { - eprintln!( - "[method_call_lowerer] Arg '{}' found in ConditionEnv → {:?}", - name, vid - ); - Ok(vid) - } else { - Err(format!( - "Variable '{}' not found in LoopBodyLocalEnv or ConditionEnv", - name - )) - } - } - - // For non-variables (literals, binops, etc.), delegate to condition_lowerer - // These don't need cascading lookup since they don't reference variables directly - _ => super::condition_lowerer::lower_value_expression( - expr, - alloc_value, - cond_env, - None, // Phase 92 P2-2: No body-local for method call expressions - instructions, - ), - } - } } #[cfg(test)] mod tests { use super::*; + use crate::ast::Span; use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; use crate::mir::join_ir::JoinInst; @@ -577,9 +554,53 @@ mod tests { assert_eq!(args[1], ValueId(11)); // ch argument } _ => panic!("Expected BoxCall instruction"), - } } + #[test] + fn test_cascading_resolves_body_local_first() { + // receiver: s, method: indexOf(ch) where ch is body-local + let mut body_env = LoopBodyLocalEnv::new(); + body_env.insert("ch".to_string(), ValueId(2)); + + let mut cond_env = ConditionEnv::new(); + cond_env.insert("s".to_string(), ValueId(1)); + + let recv_val = ValueId(1); + let mut next = 100u32; + let mut alloc_value = || { + let id = ValueId(next); + next += 1; + id + }; + let mut instructions = Vec::new(); + + let result = MethodCallLowerer::lower_for_init( + recv_val, + "indexOf", + &[ASTNode::Variable { + name: "ch".to_string(), + span: Span::unknown(), + }], + &mut alloc_value, + &cond_env, + &body_env, + &mut instructions, + ) + .expect("lower_for_init should succeed"); + + // Ensure BoxCall args include receiver + body-local resolved value (ValueId(2)) + let boxcall = instructions + .iter() + .find_map(|inst| match inst { + JoinInst::Compute(MirLikeInst::BoxCall { args, .. }) => Some(args.clone()), + _ => None, + }) + .expect("BoxCall not emitted"); + assert_eq!(boxcall, vec![ValueId(1), ValueId(2)]); + assert!(result.0 >= 100); + } +} + #[test] fn test_lower_substring_with_args() { // Phase 226 Test: s.substring(i, j) with 2 arguments (cascading support) diff --git a/src/mir/join_ir/lowering/step_schedule.rs b/src/mir/join_ir/lowering/step_schedule.rs index b6945a81..314d947a 100644 --- a/src/mir/join_ir/lowering/step_schedule.rs +++ b/src/mir/join_ir/lowering/step_schedule.rs @@ -71,9 +71,7 @@ impl Pattern2StepSchedule { } } -/// Schedule decision result with reasoning -/// -/// Phase 93 Refactoring: Unified schedule decision with explicit reasons +/// Schedule decision result with reasoning (SSOT) #[derive(Debug, Clone)] pub(crate) struct ScheduleDecision { /// Whether body-init should come before break check @@ -90,6 +88,7 @@ pub(crate) struct ScheduleDebugContext { pub has_body_local_init: bool, pub has_loop_local_carrier: bool, pub has_condition_only_recipe: bool, + pub has_body_local_derived_recipe: bool, } /// Decide Pattern2 schedule based on loop characteristics @@ -116,6 +115,7 @@ pub(crate) fn decide_pattern2_schedule( body_local_env: Option<&LoopBodyLocalEnv>, carrier_info: &CarrierInfo, has_condition_only_recipe: bool, + has_body_local_derived_recipe: bool, ) -> ScheduleDecision { let has_body_local_init = body_local_env.map(|env| !env.is_empty()).unwrap_or(false); let has_loop_local_carrier = carrier_info @@ -123,10 +123,15 @@ pub(crate) fn decide_pattern2_schedule( .iter() .any(|c| matches!(c.init, CarrierInit::LoopLocalZero)); - let body_init_first = has_condition_only_recipe || has_body_local_init || has_loop_local_carrier; + let body_init_first = has_condition_only_recipe + || has_body_local_derived_recipe + || has_body_local_init + || has_loop_local_carrier; let reason = if has_condition_only_recipe { "ConditionOnly requires body-init before break" + } else if has_body_local_derived_recipe { + "BodyLocalDerived requires body-init before break" } else if has_body_local_init { "body-local variables require init before break" } else if has_loop_local_carrier { @@ -142,51 +147,13 @@ pub(crate) fn decide_pattern2_schedule( has_body_local_init, has_loop_local_carrier, has_condition_only_recipe, + has_body_local_derived_recipe, }, } } -/// Minimal context for deciding the step order. -/// -/// Phase 93 Refactoring: Kept for backward compatibility, delegates to `decide_pattern2_schedule` -#[derive(Debug, Clone, Copy)] -pub(crate) struct Pattern2ScheduleContext { - pub(crate) has_body_local_init: bool, - pub(crate) has_loop_local_carrier: bool, -} - -impl Pattern2ScheduleContext { - /// Build schedule context from environment. - /// - /// # Phase 93 Refactoring: Backward compatibility wrapper - /// - /// Delegates to `decide_pattern2_schedule()` for actual decision making. - /// Note: When `has_condition_only_recipe` is true, we set `has_body_local_init` to true - /// to ensure the correct schedule is generated. - pub(crate) fn from_env( - body_local_env: Option<&LoopBodyLocalEnv>, - carrier_info: &CarrierInfo, - has_condition_only_recipe: bool, - ) -> Self { - let decision = decide_pattern2_schedule(body_local_env, carrier_info, has_condition_only_recipe); - Self { - // Phase 93 Refactoring: Include condition_only_recipe in has_body_local_init - // to maintain backward compatibility with requires_body_init_before_break() - has_body_local_init: decision.debug_ctx.has_body_local_init - || decision.debug_ctx.has_condition_only_recipe, - has_loop_local_carrier: decision.debug_ctx.has_loop_local_carrier, - } - } - - fn requires_body_init_before_break(&self) -> bool { - self.has_body_local_init || self.has_loop_local_carrier - } -} - /// Build a schedule for Pattern 2 lowering. /// -/// Phase 93 Refactoring: Now accepts `ScheduleDecision` for explicit reasoning -/// /// - Default P2: header → break → body-init → updates → tail /// - Body-local break dependency (DigitPos/_atoi style): /// header → body-init → break → updates → tail @@ -221,42 +188,6 @@ pub(crate) fn build_pattern2_schedule_from_decision( schedule } -/// Build a schedule for Pattern 2 lowering (legacy wrapper). -/// -/// Phase 93 Refactoring: Kept for backward compatibility -/// -/// - Default P2: header → break → body-init → updates → tail -/// - Body-local break dependency (DigitPos/_atoi style): -/// header → body-init → break → updates → tail -pub(crate) fn build_pattern2_schedule(ctx: &Pattern2ScheduleContext) -> Pattern2StepSchedule { - let schedule = if ctx.requires_body_init_before_break() { - Pattern2StepSchedule { - steps: vec![ - Pattern2StepKind::HeaderCond, - Pattern2StepKind::BodyInit, - Pattern2StepKind::BreakCheck, - Pattern2StepKind::Updates, - Pattern2StepKind::Tail, - ], - reason: "body-local break dependency", - } - } else { - Pattern2StepSchedule { - steps: vec![ - Pattern2StepKind::HeaderCond, - Pattern2StepKind::BreakCheck, - Pattern2StepKind::BodyInit, - Pattern2StepKind::Updates, - Pattern2StepKind::Tail, - ], - reason: "default", - } - }; - - log_schedule(ctx, &schedule); - schedule -} - fn log_schedule_from_decision(decision: &ScheduleDecision, schedule: &Pattern2StepSchedule) { if !(env::joinir_dev_enabled() || joinir_test_debug_enabled()) { return; @@ -270,32 +201,13 @@ fn log_schedule_from_decision(decision: &ScheduleDecision, schedule: &Pattern2St .join(" -> "); eprintln!( - "[phase93/schedule] steps={} reason={} ctx={{body_local_init={}, loop_local_carrier={}, condition_only={}}}", + "[phase93/schedule] steps={} reason={} ctx={{body_local_init={}, loop_local_carrier={}, condition_only={}, body_local_derived={}}}", steps_desc, schedule.reason(), decision.debug_ctx.has_body_local_init, decision.debug_ctx.has_loop_local_carrier, - decision.debug_ctx.has_condition_only_recipe - ); -} - -fn log_schedule(ctx: &Pattern2ScheduleContext, schedule: &Pattern2StepSchedule) { - if !(env::joinir_dev_enabled() || joinir_test_debug_enabled()) { - return; - } - - let steps_desc = schedule - .steps() - .iter() - .map(Pattern2StepKind::as_str) - .collect::>() - .join(" -> "); - - eprintln!( - "[joinir/p2-sched] steps={steps_desc} reason={} ctx={{body_local_init={}, loop_local_carrier={}}}", - schedule.reason(), - ctx.has_body_local_init, - ctx.has_loop_local_carrier + decision.debug_ctx.has_condition_only_recipe, + decision.debug_ctx.has_body_local_derived_recipe ); } @@ -351,8 +263,8 @@ mod tests { #[test] fn default_schedule_break_before_body_init() { - let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![]), false); - let schedule = build_pattern2_schedule(&ctx); + let decision = decide_pattern2_schedule(None, &carrier_info(vec![]), false, false); + let schedule = build_pattern2_schedule_from_decision(&decision); assert_eq!( schedule.steps(), &[ @@ -363,7 +275,7 @@ mod tests { Pattern2StepKind::Tail ] ); - assert_eq!(schedule.reason(), "default"); + assert_eq!(schedule.reason(), "default schedule"); } #[test] @@ -371,9 +283,13 @@ mod tests { let mut body_env = LoopBodyLocalEnv::new(); body_env.insert("tmp".to_string(), ValueId(5)); - let ctx = - Pattern2ScheduleContext::from_env(Some(&body_env), &carrier_info(vec![carrier(false)]), false); - let schedule = build_pattern2_schedule(&ctx); + let decision = decide_pattern2_schedule( + Some(&body_env), + &carrier_info(vec![carrier(false)]), + false, + false, + ); + let schedule = build_pattern2_schedule_from_decision(&decision); assert_eq!( schedule.steps(), &[ @@ -384,13 +300,18 @@ mod tests { Pattern2StepKind::Tail ] ); - assert_eq!(schedule.reason(), "body-local break dependency"); + assert_eq!(schedule.reason(), "body-local variables require init before break"); } #[test] fn loop_local_carrier_triggers_body_first() { - let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![carrier(true)]), false); - let schedule = build_pattern2_schedule(&ctx); + let decision = decide_pattern2_schedule( + None, + &carrier_info(vec![carrier(true)]), + false, + false, + ); + let schedule = build_pattern2_schedule_from_decision(&decision); assert_eq!( schedule.steps(), &[ @@ -401,15 +322,15 @@ mod tests { Pattern2StepKind::Tail ] ); - assert_eq!(schedule.reason(), "body-local break dependency"); + assert_eq!(schedule.reason(), "loop-local carrier requires init before break"); } /// Phase 93 P0: ConditionOnly recipe triggers body-init before break #[test] fn condition_only_recipe_triggers_body_first() { // Empty body_local_env but has condition_only_recipe - let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![]), true); - let schedule = build_pattern2_schedule(&ctx); + let decision = decide_pattern2_schedule(None, &carrier_info(vec![]), true, false); + let schedule = build_pattern2_schedule_from_decision(&decision); assert_eq!( schedule.steps(), &[ @@ -420,8 +341,7 @@ mod tests { Pattern2StepKind::Tail ] ); - // Phase 93 Refactoring: Reason is now preserved from backward compat wrapper - assert_eq!(schedule.reason(), "body-local break dependency"); + assert_eq!(schedule.reason(), "ConditionOnly requires body-init before break"); } #[test] diff --git a/src/mir/join_ir/normalized/fixtures.rs b/src/mir/join_ir/normalized/fixtures.rs index 6adc76fb..54c7cc2a 100644 --- a/src/mir/join_ir/normalized/fixtures.rs +++ b/src/mir/join_ir/normalized/fixtures.rs @@ -136,6 +136,7 @@ pub fn build_pattern2_minimal_structured() -> JoinModule { join_value_space: &mut join_value_space, skeleton: None, // Phase 92 P0-3: skeleton=None for backward compatibility condition_only_recipe: None, // Phase 93 P0: None for normal loops + body_local_derived_recipe: None, // Phase 94: None for fixture }, ) .expect("pattern2 minimal lowering should succeed"); diff --git a/tools/smokes/v2/profiles/integration/apps/phase94_p5b_escape_e2e.sh b/tools/smokes/v2/profiles/integration/apps/phase94_p5b_escape_e2e.sh new file mode 100644 index 00000000..8ac917f3 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase94_p5b_escape_e2e.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Phase 94: P5b escape handling complete E2E (VM) +# +# Verifies: +# - Pattern2Break JoinIR lowering supports: +# - body-local `ch` conditional override (derived Select) +# - loop counter skip on escape (i += 2 when ch == "\\") +# - Fixture prints: hello" world +# +# Notes: +# - We keep this in `integration` (not `quick`) to avoid adding more output-heavy cases +# to the fastest profile. + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +PASS_COUNT=0 +FAIL_COUNT=0 +RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10} + +INPUT="$NYASH_ROOT/tools/selfhost/test_pattern5b_escape_minimal.hako" + +echo "[INFO] Phase 94: P5b escape E2E (VM) - $INPUT" + +set +e +OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env \ + NYASH_DISABLE_PLUGINS=1 \ + HAKO_JOINIR_STRICT=1 \ + "$NYASH_BIN" --backend vm "$INPUT" 2>&1) +EXIT_CODE=$? +set -e + +if [ "$EXIT_CODE" -eq 124 ]; then + echo "[FAIL] hakorune timed out (>${RUN_TIMEOUT_SECS}s)" + FAIL_COUNT=$((FAIL_COUNT + 1)) +elif [ "$EXIT_CODE" -eq 0 ]; then + if echo "$OUTPUT" | grep -q '^hello" world$'; then + echo "[PASS] Output verified: hello\" world" + PASS_COUNT=$((PASS_COUNT + 1)) + else + echo "[FAIL] Unexpected output (expected line: hello\" world)" + echo "[INFO] output (tail):" + echo "$OUTPUT" | tail -n 50 || true + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi +else + echo "[FAIL] hakorune failed with exit code $EXIT_CODE" + echo "[INFO] output (tail):" + echo "$OUTPUT" | tail -n 50 || true + FAIL_COUNT=$((FAIL_COUNT + 1)) +fi + +echo "[INFO] PASS: $PASS_COUNT, FAIL: $FAIL_COUNT" + +if [ "$FAIL_COUNT" -eq 0 ]; then + test_pass "phase94_p5b_escape_e2e: All tests passed" + exit 0 +else + test_fail "phase94_p5b_escape_e2e: $FAIL_COUNT test(s) failed" + exit 1 +fi +