diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index e0fa1f52..f9f93d57 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -230,7 +230,7 @@ 14. **Phase 57-OWNERSHIP-ANALYZER-DEV(完了✅ 2025-12-12)**: OwnershipPlan を生成する解析箱の実装 - `OwnershipAnalyzer` を追加し、ネスト含む reads/writes/owned を集計→ carriers/relay/captures を plan 化。 - 既存 fixtures(pattern2/3, jsonparser, selfhost)で plan の回帰テストを追加。 - - 設計詳細: [phase57-ownership-analyzer.md](docs/development/current/main/phase57-ownership-analyzer.md) + - 設計詳細: [PHASE_57_SUMMARY.md](docs/development/current/main/PHASE_57_SUMMARY.md) 15. **Phase 58-OWNERSHIP-PLUMB-P2-DEV(完了✅ 2025-12-12)**: P2 conversion helper (dev-only) - `plan_to_p2_inputs()` でOwnershipPlan→P2LoweringInputs変換 - Fail-Fast: relay_writes 未対応(Phase 60で対応予定) @@ -242,10 +242,19 @@ - Fail-Fast: relay_writes 未対応(Phase 60で対応予定) - 4つのユニットテスト + 2つのintegrationテスト - 設計詳細: [PHASE_59_SUMMARY.md](docs/development/current/main/PHASE_59_SUMMARY.md) -17. **Phase 60-OWNERSHIP-RELAY-IMPL(次のフォーカス候補)**: Relay support for P2/P3 - - relay_writes対応実装 - - P2/P3両方の変換器に統合 -18. JoinIR Verify / 最適化まわり +17. **Phase 60-OWNERSHIP-RELAY-IMPL(完了✅ 2025-12-12)**: Relay support for P2/P3 (dev-only) + - `plan_to_p2_inputs_with_relay()` / `plan_to_p3_inputs_with_relay()` を追加(単一hopのみ許可、multi-hopはFail-Fast) + - P2 Break lowering を dev-only で ownership-with-relay に接続し、legacy 経路との VM 出力一致を比較テストで固定。 + - shape_guard の selfhost family 分離を最小更新(selfhost shapes 優先時の混線を遮断)。 +18. **Phase 61-IFSUM-BREAK-STRUCTURAL(完了✅ 2025-12-12)**: if-sum + break を別箱で構造的に導入(dev-only) + - Break(P2) から P3 固有ロジック(by-name)を撤去し、責務混線を解消。 + - 新箱 `if_sum_break_pattern` を追加し、`return Var+Var` を含む if-sum+break を構造判定→Fail-Fast で lowering。 + - OwnershipPlan を param order/carriers の SSOT に使い、carriers!=return vars の混線を遮断。 + - 詳細: [PHASE_61_SUMMARY.md](docs/development/current/main/PHASE_61_SUMMARY.md) +19. **Phase 62-OWNERSHIP-P3-ROUTE-DESIGN(次のフォーカス候補)**: P3 本番ルートへ OwnershipPlan を渡す設計 + - MIR→JoinIR の `pattern3_with_if_phi.rs` は OwnershipPlan を受け取らないため、AST-based ownership 解析の接続点を設計する。 + - dev-only で段階接続し、legacy と stdout/exit 一致の比較で回帰を固定(既定挙動は不変)。 +20. JoinIR Verify / 最適化まわり - すでに PHI/ValueId 契約は debug ビルドで検証しているので、 必要なら SSA‑DFA や軽い最適化(Loop invariant / Strength reduction)を検討。 diff --git a/docs/development/current/main/PHASE_61_SUMMARY.md b/docs/development/current/main/PHASE_61_SUMMARY.md new file mode 100644 index 00000000..f787eff9 --- /dev/null +++ b/docs/development/current/main/PHASE_61_SUMMARY.md @@ -0,0 +1,56 @@ +# Phase 61 Summary: IF-SUM + BREAK (dev-only, structural) + +## Goal + +Phase 61 は「Break(P2) に P3 固有ロジックを混ぜない」を達成しつつ、selfhost の `if-sum + break` 形状を **by-name なし**で JoinIR Frontend に載せる。 + +## Problem (Phase 61 前) + +- `break_pattern.rs`(Break/P2 箱)の param-order 決定で、`selfhost_if_sum_p3*` を名指しして P3 helper を優先する案は、 + - 責務混線(Break 箱が P3(if-sum) の意味論まで背負う) + - dev-only でも撤去困難な by-name 分岐になりやすい + という理由で採用しない。 + +## Solution (Phase 61) + +### 1) Break(P2) を構造ベースに戻す + +- `src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs` + - Break(P2) の ownership 経路は `plan_to_p2_inputs_with_relay` のみを使用し、P3 専用分岐を排除する。 + +### 2) 新箱 `if_sum_break_pattern` を追加(dev-only) + +- `src/mir/join_ir/frontend/ast_lowerer/loop_patterns/if_sum_break_pattern.rs` + - 対象: `break-if + update-if + counter-update + return Var+Var` + - 検出は構造のみ(by-name なし)。 + - lowering は Select ベースで `sum`/`count` を更新し、exit では `sum + count` を 1 値にして `k_exit` に渡す。 + +### 3) Ownership を SSOT に使う(param order / carriers) + +- `OwnershipAnalyzer` → `plan_to_p3_inputs_with_relay` で carriers/captures を得る。 +- Fail-Fast: carriers が Return の 2 変数と一致しない場合は拒否(canonical P3 群との混線防止)。 +- relay は単一hopのみ許可(multi-hop は Fail-Fast 維持)。 + +## Fail-Fast Contract + +- Return が `Var + Var` 以外なら対象外。 +- loop body が `[break-if, update-if, counter-update]` の最小形状から外れるなら対象外。 +- counter update が `i = i + 1` 形で検出できないなら Err。 +- relay_path.len() > 1 は Err。 +- carriers != return vars は Err。 + +## Tests + +- `tests/normalized_joinir_min.rs` + - selfhost P3 fixture を Program(JSON v0) として解析し、relay_writes→carriers 変換が成立することを固定。 + - selfhost P3(NormalizedDev 経路)については stdout/exit + 期待値(意味論)で固定し、比較テスト依存を避ける。 + +## Notes (Reproducibility) + +- `docs/private` は submodule のため、fixture JSON を参照する場合は **submodule 側で追跡されていること**を前提とする。 + - 未追跡のままローカルだけで存在すると、clean checkout で `include_str!` が壊れる。 + +## Next Candidates + +- Phase 62: P3(if-sum) の「本番 MIR→JoinIR ルート」へ OwnershipPlan を渡す設計(AST-based ownership 解析の接続点を設計 → dev-only で段階接続)。 +- Phase 63+: multi-hop / merge relay の意味論設計(Fail-Fast を解除する前に SSOT と不変条件を明文化)。 diff --git a/docs/development/current/main/phase56-ownership-relay-design.md b/docs/development/current/main/phase56-ownership-relay-design.md index 80006f30..7cbee573 100644 --- a/docs/development/current/main/phase56-ownership-relay-design.md +++ b/docs/development/current/main/phase56-ownership-relay-design.md @@ -138,8 +138,11 @@ pub struct OwnershipPlan { - **Phase 57**: OwnershipAnalyzer implementation (dev-only) - **Phase 58**: P2 plumbing (dev-only) - **Phase 59**: P3 plumbing (dev-only) -- **Phase 60**: Cleanup dev heuristics -- **Phase 61**: Canonical promotion decision +- **Phase 60**: Single-hop relay threading for fixtures (dev-only) +- **Phase 61**: P3 側の接続点を決めて段階接続(dev-only) + - まずは fixtures ルート(Program(JSON v0))で、if-sum+break を別箱として構造的に接続する + - 詳細: `docs/development/current/main/PHASE_61_SUMMARY.md` + - MIR→JoinIR の本番ルート(`pattern3_with_if_phi.rs`)へ寄せるのは別フェーズで設計→接続 ## Module Boundary @@ -244,6 +247,13 @@ loop { ### Implementation Details +**Input JSON Compatibility**: +- テスト用の簡易スキーマ: top-level `functions` + stmt/expr の `kind` を解釈 +- Program(JSON v0): top-level `defs` + stmt/expr の `type` を解釈 +- `Local` は JSON v0 で「新規束縛」と「rebind/update」が混在し得るため、 + 解析では「scope chain で既に定義済みなら write / 未定義なら define」として扱う(dev-only 前提)。 + - Note: `docs/private` は submodule のため、fixture JSON を参照する場合は submodule 側で追跡されていることを前提とする。 + **Body-Local Ownership Rule**: ```rust // Example: local in if/block → enclosing loop owns it @@ -267,5 +277,9 @@ loop { ## Status - ✅ Phase 56: Design + interface skeleton completed -- ✅ Phase 57: Analyzer implemented (dev-only, not connected to lowering yet) -- ⏳ Phase 58+: Plumbing integration pending +- ✅ Phase 57: Analyzer implemented (dev-only) +- ✅ Phase 58-59: plan_to_lowering helpers (P2/P3) with Fail-Fast relay +- ✅ Phase 60 (dev-only): single-hop relay threading for P2 fixtures + - `plan_to_p2_inputs_with_relay` promotes relay_writes to carriers (relay_path.len()<=1 only) + - Frontend Break(P2) lowering uses ownership-with-relay; legacy path preserved for comparison + - P3 stays analysis-only; real threading is Phase 61+ diff --git a/docs/private b/docs/private index 1e76c99f..00e3e637 160000 --- a/docs/private +++ b/docs/private @@ -1 +1 @@ -Subproject commit 1e76c99f441fca6fab7ec6cda97773f5f3aa9361 +Subproject commit 00e3e63780ecccd5e8c9a4773f07c54629431eb8 diff --git a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs index a36793ea..f89a2cbe 100644 --- a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs +++ b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs @@ -25,10 +25,17 @@ use super::common::{ process_local_inits, }; use super::param_guess::{build_param_order, compute_param_guess}; +#[cfg(feature = "normalized_dev")] +use super::if_sum_break_pattern; use super::{AstToJoinIrLowerer, JoinModule, LoweringError}; use crate::mir::join_ir::{JoinFunction, JoinInst}; use crate::mir::ValueId; +#[cfg(feature = "normalized_dev")] +use crate::mir::join_ir::ownership::{ + plan_to_p2_inputs_with_relay, OwnershipAnalyzer, +}; + /// Break パターンを JoinModule に変換 /// /// # Arguments @@ -37,6 +44,26 @@ use crate::mir::ValueId; pub fn lower( lowerer: &mut AstToJoinIrLowerer, program_json: &serde_json::Value, +) -> Result { + #[cfg(feature = "normalized_dev")] + { + if let Some(module) = if_sum_break_pattern::try_lower_if_sum_break(lowerer, program_json)? { + return Ok(module); + } + if let Ok(module) = lower_with_ownership_relay(lowerer, program_json) { + return Ok(module); + } + } + + lower_legacy_param_guess(lowerer, program_json) +} + +/// Legacy Break lowering (Phase P4) using param_guess heuristics. +/// +/// This remains as a fallback and is also used for Phase 60 comparison tests. +fn lower_legacy_param_guess( + lowerer: &mut AstToJoinIrLowerer, + program_json: &serde_json::Value, ) -> Result { // 1. Program(JSON) をパース let parsed = parse_program_json(program_json); @@ -101,6 +128,136 @@ pub fn lower( Ok(build_join_module(entry_func, loop_step_func, k_exit_func)) } +/// Phase 60 dev-only Break lowering using OwnershipAnalyzer + relay threading. +/// +/// This function is only compiled with `normalized_dev` and is fail-fast on +/// multi-hop relay (relay_path.len()>1). +#[cfg(feature = "normalized_dev")] +fn lower_with_ownership_relay( + lowerer: &mut AstToJoinIrLowerer, + program_json: &serde_json::Value, +) -> Result { + // Parse and build contexts similarly to legacy path. + let parsed = parse_program_json(program_json); + let (ctx, mut entry_ctx) = create_loop_context(lowerer, &parsed); + let init_insts = process_local_inits(lowerer, &parsed, &mut entry_ctx); + + let loop_node = &parsed.stmts[parsed.loop_node_idx]; + let loop_body = loop_node["body"] + .as_array() + .ok_or_else(|| LoweringError::InvalidLoopBody { + message: "Loop must have 'body' array".to_string(), + })?; + + let (break_if_idx, break_if_stmt) = loop_body + .iter() + .enumerate() + .find(|(_, stmt)| { + stmt["type"].as_str() == Some("If") + && stmt["then"].as_array().map_or(false, |then| { + then.iter().any(|s| s["type"].as_str() == Some("Break")) + }) + }) + .ok_or_else(|| LoweringError::InvalidLoopBody { + message: "Break pattern must have If + Break".to_string(), + })?; + + let break_cond_expr = &break_if_stmt["cond"]; + let loop_cond_expr = &loop_node["cond"]; + + // Use legacy guess only to stabilize loop_var/acc names. + let legacy_guess = compute_param_guess(&entry_ctx); + let loop_var_name = legacy_guess.loop_var.0.clone(); + let acc_name = legacy_guess.acc.0.clone(); + + let param_order = compute_param_order_from_ownership(program_json, &entry_ctx, &loop_var_name) + .unwrap_or_else(|| build_param_order(&legacy_guess, &entry_ctx)); + + // Ensure accumulator is present in param list (avoid missing carrier ordering). + let mut param_order = param_order; + if !param_order.iter().any(|(n, _)| n == &acc_name) { + if let Some(id) = entry_ctx.get_var(&acc_name) { + param_order.push((acc_name.clone(), id)); + } + } + + let entry_func = + create_entry_function_break(&ctx, &parsed, init_insts, &mut entry_ctx, ¶m_order); + + let loop_step_func = create_loop_step_function_break( + lowerer, + &ctx, + &parsed.func_name, + loop_cond_expr, + break_cond_expr, + loop_body, + ¶m_order, + &loop_var_name, + &acc_name, + break_if_idx, + )?; + + let k_exit_func = create_k_exit_function(&ctx, &parsed.func_name); + Ok(build_join_module(entry_func, loop_step_func, k_exit_func)) +} + +#[cfg(feature = "normalized_dev")] +fn compute_param_order_from_ownership( + program_json: &serde_json::Value, + entry_ctx: &super::super::context::ExtractCtx, + loop_var_name: &str, +) -> Option> { + let mut analyzer = OwnershipAnalyzer::new(); + let plans = analyzer.analyze_json(program_json).ok()?; + + let loop_plan = plans + .iter() + // Prefer the actual loop scope (loop var is re-bound inside loop => relay_writes) + .find(|p| p.relay_writes.iter().any(|r| r.name == loop_var_name)) + // Fallback: any loop plan with relay_writes + .or_else(|| plans.iter().find(|p| !p.relay_writes.is_empty())) + // Last resort: any plan that owns loop_var_name (loop-local case) + .or_else(|| plans.iter().find(|p| p.owned_vars.iter().any(|v| v.name == loop_var_name)))?; + + let inputs = plan_to_p2_inputs_with_relay(loop_plan, loop_var_name).ok()?; + + let mut order: Vec<(String, ValueId)> = Vec::new(); + let mut seen = std::collections::BTreeSet::::new(); + + if let Some(id) = entry_ctx.get_var(loop_var_name) { + order.push((loop_var_name.to_string(), id)); + seen.insert(loop_var_name.to_string()); + } + + for carrier in inputs.carriers { + if seen.contains(&carrier.name) { + continue; + } + if let Some(id) = entry_ctx.get_var(&carrier.name) { + order.push((carrier.name.clone(), id)); + seen.insert(carrier.name); + } + } + + for (name, var_id) in &entry_ctx.var_map { + if !seen.contains(name) { + order.push((name.clone(), *var_id)); + seen.insert(name.clone()); + } + } + + Some(order) +} + +/// Expose legacy Break lowering for Phase 60 comparison tests (dev-only). +#[cfg(feature = "normalized_dev")] +pub fn lower_break_legacy_for_comparison( + lowerer: &mut AstToJoinIrLowerer, + program_json: &serde_json::Value, +) -> Result { + lower_legacy_param_guess(lowerer, program_json) +} + /// Break パターン用 entry 関数を生成 fn create_entry_function_break( ctx: &super::common::LoopContext, diff --git a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/if_sum_break_pattern.rs b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/if_sum_break_pattern.rs new file mode 100644 index 00000000..9f9f4663 --- /dev/null +++ b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/if_sum_break_pattern.rs @@ -0,0 +1,503 @@ +//! Phase 61: If-Sum + Break pattern (dev-only) +//! +//! ## Responsibility +//! Break 付き if-sum ループを、sum/count の複数キャリアで k_exit に渡し、 +//! k_exit で `sum + count` を返す。 +//! +//! ## Fail-Fast Boundary +//! - Return が `Var + Var` 以外 → not matched +//! - ループ末尾の counter update が `i = i + 1` 形で検出できない → Err +//! - Ownership relay が single-hop 以外 → Err +//! - loop-carried carriers が Return の 2 変数と一致しない → Err + +#![cfg(feature = "normalized_dev")] + +use super::common::{ + build_join_module, create_k_exit_function, create_loop_context, parse_program_json, + process_local_inits, +}; +use super::{AstToJoinIrLowerer, JoinModule, LoweringError}; +use crate::mir::join_ir::{BinOpKind, JoinFunction, JoinInst, MirLikeInst}; +use crate::mir::ValueId; +use crate::mir::join_ir::ownership::{plan_to_p3_inputs_with_relay, OwnershipAnalyzer}; + +pub fn try_lower_if_sum_break( + lowerer: &mut AstToJoinIrLowerer, + program_json: &serde_json::Value, +) -> Result, LoweringError> { + let parsed = parse_program_json(program_json); + + let return_expr = parsed.stmts.last().and_then(|s| { + if s["type"].as_str() == Some("Return") { + s.get("expr") + } else { + None + } + }); + + let Some((ret_lhs, ret_rhs)) = parse_return_var_plus_var(return_expr) else { + return Ok(None); + }; + + let loop_node = &parsed.stmts[parsed.loop_node_idx]; + let loop_body = loop_node["body"] + .as_array() + .ok_or_else(|| LoweringError::InvalidLoopBody { + message: "Loop must have 'body' array".to_string(), + })?; + + let (break_if_idx, break_if_stmt) = match find_break_if(loop_body) { + Some(v) => v, + None => return Ok(None), + }; + if break_if_idx != 0 { + return Ok(None); + } + let break_cond_expr = &break_if_stmt["cond"]; + + // Limit scope (Phase 61 dev-only): [break-if, update-if, counter-update] only. + if loop_body.len() != 3 { + return Ok(None); + } + + let update_if_stmt = match find_single_update_if(loop_body, break_if_idx) { + Some(v) => v, + None => return Ok(None), + }; + + let counter_update_stmt = loop_body + .last() + .expect("loop_body len checked") + .clone(); + + let loop_var_name = detect_counter_update_loop_var(loop_body).ok_or_else(|| { + LoweringError::InvalidLoopBody { + message: "if-sum-break requires trailing counter update like i = i + 1".to_string(), + } + })?; + + if ret_lhs == loop_var_name || ret_rhs == loop_var_name || ret_lhs == ret_rhs { + return Ok(None); + } + + // === Ownership SSOT: param order = [loop_var] + carriers + captures === + let (ctx, mut entry_ctx) = create_loop_context(lowerer, &parsed); + let init_insts = process_local_inits(lowerer, &parsed, &mut entry_ctx); + + let mut analyzer = OwnershipAnalyzer::new(); + let plans = analyzer + .analyze_json(program_json) + .map_err(|e| LoweringError::JsonParseError { message: e })?; + + let loop_plan = plans + .iter() + .find(|p| p.relay_writes.iter().any(|r| r.name == loop_var_name)) + .or_else(|| plans.iter().find(|p| p.owned_vars.iter().any(|v| v.name == loop_var_name))) + .ok_or_else(|| LoweringError::InvalidLoopBody { + message: "if-sum-break: failed to find loop ownership plan".to_string(), + })?; + + let inputs = plan_to_p3_inputs_with_relay(loop_plan, &loop_var_name).map_err(|e| { + LoweringError::JsonParseError { message: e } + })?; + + // Ensure carriers are exactly the return vars (fail-fast mixing protection). + let carrier_names: std::collections::BTreeSet = + inputs.carriers.iter().map(|c| c.name.clone()).collect(); + let expected: std::collections::BTreeSet = + [ret_lhs.clone(), ret_rhs.clone()].into_iter().collect(); + + if carrier_names != expected { + return Err(LoweringError::InvalidLoopBody { + message: format!( + "if-sum-break: carriers {:?} must equal return vars {:?}", + carrier_names, expected + ), + }); + } + + let mut param_order: Vec<(String, ValueId)> = Vec::new(); + let mut seen = std::collections::BTreeSet::::new(); + + let loop_var_id = entry_ctx + .get_var(&loop_var_name) + .ok_or_else(|| LoweringError::InvalidLoopBody { + message: format!("loop var '{}' must be initialized before loop", loop_var_name), + })?; + param_order.push((loop_var_name.clone(), loop_var_id)); + seen.insert(loop_var_name.clone()); + + for carrier in &inputs.carriers { + let id = entry_ctx + .get_var(&carrier.name) + .ok_or_else(|| LoweringError::InvalidLoopBody { + message: format!("carrier '{}' must be initialized before loop", carrier.name), + })?; + param_order.push((carrier.name.clone(), id)); + seen.insert(carrier.name.clone()); + } + + for cap_name in &inputs.captures { + if seen.contains(cap_name) { + continue; + } + if let Some(id) = entry_ctx.get_var(cap_name) { + param_order.push((cap_name.clone(), id)); + seen.insert(cap_name.clone()); + } + } + + // Include remaining params/vars deterministically + for (name, var_id) in &entry_ctx.var_map { + if !seen.contains(name) { + param_order.push((name.clone(), *var_id)); + seen.insert(name.clone()); + } + } + + let entry_func = create_entry_function_if_sum_break( + &ctx, + &parsed, + init_insts, + &mut entry_ctx, + ¶m_order, + ); + + let loop_step_func = create_loop_step_function_if_sum_break( + lowerer, + &ctx, + &parsed.func_name, + &loop_node["cond"], + break_cond_expr, + update_if_stmt, + &counter_update_stmt, + ¶m_order, + &loop_var_name, + &ret_lhs, + &ret_rhs, + )?; + + let k_exit_func = create_k_exit_function(&ctx, &parsed.func_name); + + Ok(Some(build_join_module(entry_func, loop_step_func, k_exit_func))) +} + +fn parse_return_var_plus_var( + expr: Option<&serde_json::Value>, +) -> Option<(String, String)> { + let expr = expr?; + if expr["type"].as_str()? != "Binary" { + return None; + } + if expr["op"].as_str()? != "+" { + return None; + } + let lhs = expr["lhs"].as_object()?; + let rhs = expr["rhs"].as_object()?; + if lhs.get("type")?.as_str()? != "Var" || rhs.get("type")?.as_str()? != "Var" { + return None; + } + Some(( + lhs.get("name")?.as_str()?.to_string(), + rhs.get("name")?.as_str()?.to_string(), + )) +} + +fn find_break_if(loop_body: &[serde_json::Value]) -> Option<(usize, &serde_json::Value)> { + loop_body.iter().enumerate().find(|(_, stmt)| { + stmt["type"].as_str() == Some("If") + && stmt["then"].as_array().map_or(false, |then| { + then.iter().any(|s| s["type"].as_str() == Some("Break")) + }) + }) +} + +fn detect_counter_update_loop_var(loop_body: &[serde_json::Value]) -> Option { + let last = loop_body.last()?; + if last["type"].as_str()? != "Local" { + return None; + } + let name = last["name"].as_str()?.to_string(); + let expr = last.get("expr")?; + if expr["type"].as_str()? != "Binary" || expr["op"].as_str()? != "+" { + return None; + } + let lhs = &expr["lhs"]; + let rhs = &expr["rhs"]; + if lhs["type"].as_str()? != "Var" || lhs["name"].as_str()? != name { + return None; + } + if rhs["type"].as_str()? != "Int" || rhs["value"].as_i64()? != 1 { + return None; + } + Some(name) +} + +fn create_entry_function_if_sum_break( + ctx: &super::common::LoopContext, + parsed: &super::common::ParsedProgram, + init_insts: Vec, + entry_ctx: &mut super::super::context::ExtractCtx, + param_order: &[(String, ValueId)], +) -> JoinFunction { + let loop_args: Vec = param_order.iter().map(|(_, id)| *id).collect(); + let loop_result = entry_ctx.alloc_var(); + let mut body = init_insts; + body.push(JoinInst::Call { + func: ctx.loop_step_id, + args: loop_args, + 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, + } +} + +fn create_loop_step_function_if_sum_break( + lowerer: &mut AstToJoinIrLowerer, + ctx: &super::common::LoopContext, + func_name: &str, + loop_cond_expr: &serde_json::Value, + break_cond_expr: &serde_json::Value, + update_if_stmt: &serde_json::Value, + counter_update_stmt: &serde_json::Value, + param_order: &[(String, ValueId)], + loop_var_name: &str, + sum_name: &str, + count_name: &str, +) -> Result { + use super::super::context::ExtractCtx; + + let param_names: Vec = param_order.iter().map(|(name, _)| name.clone()).collect(); + let mut step_ctx = ExtractCtx::new(param_names.len() as u32); + for (idx, name) in param_names.iter().enumerate() { + step_ctx.register_param(name.clone(), ValueId(idx as u32)); + } + + let mut body = Vec::new(); + + // Header condition: if !loop_cond -> exit with (sum, count) + let (loop_cond_var, loop_cond_insts) = lowerer.extract_value(loop_cond_expr, &mut step_ctx); + body.extend(loop_cond_insts); + let header_exit_flag = step_ctx.alloc_var(); + body.push(JoinInst::Compute(MirLikeInst::UnaryOp { + dst: header_exit_flag, + op: crate::mir::join_ir::UnaryOp::Not, + operand: loop_cond_var, + })); + let sum_before = step_ctx + .get_var(sum_name) + .ok_or_else(|| LoweringError::InvalidLoopBody { + message: format!("{} must exist", sum_name), + })?; + let count_before = step_ctx + .get_var(count_name) + .ok_or_else(|| LoweringError::InvalidLoopBody { + message: format!("{} must exist", count_name), + })?; + let acc_before = step_ctx.alloc_var(); + body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: acc_before, + op: BinOpKind::Add, + lhs: sum_before, + rhs: count_before, + })); + body.push(JoinInst::Jump { + cont: ctx.k_exit_id.as_cont(), + args: vec![acc_before], + cond: Some(header_exit_flag), + }); + + // Break condition: if break_cond -> exit with (sum, count) + let (break_cond_var, break_cond_insts) = lowerer.extract_value(break_cond_expr, &mut step_ctx); + body.extend(break_cond_insts); + body.push(JoinInst::Jump { + cont: ctx.k_exit_id.as_cont(), + args: vec![acc_before], + cond: Some(break_cond_var), + }); + + // Update-if: cond ? then_update : else_update (Select-based, no if-in-loop lowering) + let update_cond_expr = &update_if_stmt["cond"]; + let (update_cond_var, update_cond_insts) = + lowerer.extract_value(update_cond_expr, &mut step_ctx); + body.extend(update_cond_insts); + + let (sum_then_expr, sum_else_expr) = + extract_if_branch_assignment(update_if_stmt, sum_name)?; + let (count_then_expr, count_else_expr) = + extract_if_branch_assignment(update_if_stmt, count_name)?; + + let (sum_then_val, sum_then_insts) = + lowerer.extract_value(&sum_then_expr, &mut step_ctx); + let (sum_else_val, sum_else_insts) = + lowerer.extract_value(&sum_else_expr, &mut step_ctx); + let (count_then_val, count_then_insts) = + lowerer.extract_value(&count_then_expr, &mut step_ctx); + let (count_else_val, count_else_insts) = + lowerer.extract_value(&count_else_expr, &mut step_ctx); + body.extend(sum_then_insts); + body.extend(sum_else_insts); + body.extend(count_then_insts); + body.extend(count_else_insts); + + let sum_next = step_ctx.alloc_var(); + body.push(JoinInst::Compute(MirLikeInst::Select { + dst: sum_next, + cond: update_cond_var, + then_val: sum_then_val, + else_val: sum_else_val, + })); + step_ctx.register_param(sum_name.to_string(), sum_next); + + let count_next = step_ctx.alloc_var(); + body.push(JoinInst::Compute(MirLikeInst::Select { + dst: count_next, + cond: update_cond_var, + then_val: count_then_val, + else_val: count_else_val, + })); + step_ctx.register_param(count_name.to_string(), count_next); + + // Counter update (must update loop var) + let counter_expr = counter_update_stmt.get("expr").ok_or_else(|| { + LoweringError::InvalidLoopBody { + message: "counter update must have 'expr'".to_string(), + } + })?; + let (i_next, i_insts) = lowerer.extract_value(counter_expr, &mut step_ctx); + body.extend(i_insts); + step_ctx.register_param(loop_var_name.to_string(), i_next); + + // Recurse with updated params. + let recurse_result = step_ctx.alloc_var(); + let mut recurse_args = Vec::new(); + for name in ¶m_names { + let arg = step_ctx + .get_var(name) + .unwrap_or_else(|| panic!("param {} must exist", name)); + recurse_args.push(arg); + } + body.push(JoinInst::Call { + func: ctx.loop_step_id, + args: recurse_args, + 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: (0..param_names.len()) + .map(|i| ValueId(i as u32)) + .collect(), + body, + exit_cont: None, + }) +} + +fn find_single_update_if<'a>( + loop_body: &'a [serde_json::Value], + break_if_idx: usize, +) -> Option<&'a serde_json::Value> { + let mut found: Option<&serde_json::Value> = None; + for (idx, stmt) in loop_body.iter().enumerate() { + if idx == break_if_idx { + continue; + } + if stmt["type"].as_str() == Some("If") { + if found.is_some() { + return None; + } + found = Some(stmt); + } + } + found +} + +fn extract_if_branch_assignment( + if_stmt: &serde_json::Value, + target: &str, +) -> Result<(serde_json::Value, serde_json::Value), LoweringError> { + fn find_assignment_expr(branch: &[serde_json::Value], target: &str) -> Result, LoweringError> { + let mut found: Option = None; + for stmt in branch { + match stmt["type"].as_str() { + Some("Local") => { + let name = stmt["name"].as_str().ok_or_else(|| LoweringError::InvalidLoopBody { + message: "Local must have 'name'".to_string(), + })?; + if name != target { + continue; + } + if found.is_some() { + return Err(LoweringError::InvalidLoopBody { + message: format!("if-sum-break: multiple assignments to '{}'", target), + }); + } + let expr = stmt.get("expr").ok_or_else(|| LoweringError::InvalidLoopBody { + message: "Local must have 'expr'".to_string(), + })?; + found = Some(expr.clone()); + } + Some("Assignment") | Some("Assign") => { + let name = stmt["target"].as_str().ok_or_else(|| LoweringError::InvalidLoopBody { + message: "Assignment must have 'target'".to_string(), + })?; + if name != target { + continue; + } + if found.is_some() { + return Err(LoweringError::InvalidLoopBody { + message: format!("if-sum-break: multiple assignments to '{}'", target), + }); + } + let expr = stmt.get("expr").or_else(|| stmt.get("value")).ok_or_else(|| { + LoweringError::InvalidLoopBody { + message: "Assignment must have 'expr' or 'value'".to_string(), + } + })?; + found = Some(expr.clone()); + } + _ => { + return Err(LoweringError::InvalidLoopBody { + message: "if-sum-break: unsupported statement in update if".to_string(), + }); + } + } + } + Ok(found) + } + + let then_branch = if_stmt["then"] + .as_array() + .ok_or_else(|| LoweringError::InvalidLoopBody { + message: "If must have 'then' array".to_string(), + })?; + let else_branch = if_stmt["else"] + .as_array() + .ok_or_else(|| LoweringError::InvalidLoopBody { + message: "If must have 'else' array".to_string(), + })?; + + let then_expr = find_assignment_expr(then_branch, target)?.unwrap_or_else(|| { + serde_json::json!({"type":"Var","name":target}) + }); + let else_expr = find_assignment_expr(else_branch, target)?.unwrap_or_else(|| { + serde_json::json!({"type":"Var","name":target}) + }); + Ok((then_expr, else_expr)) +} diff --git a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/mod.rs b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/mod.rs index 0ba80901..5ef2b79a 100644 --- a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/mod.rs +++ b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/mod.rs @@ -19,6 +19,8 @@ pub mod break_pattern; pub mod common; pub mod continue_pattern; pub mod filter; +#[cfg(feature = "normalized_dev")] +pub mod if_sum_break_pattern; pub mod param_guess; pub mod print_tokens; pub mod simple; diff --git a/src/mir/join_ir/frontend/ast_lowerer/mod.rs b/src/mir/join_ir/frontend/ast_lowerer/mod.rs index 578b2eef..284b6a94 100644 --- a/src/mir/join_ir/frontend/ast_lowerer/mod.rs +++ b/src/mir/join_ir/frontend/ast_lowerer/mod.rs @@ -189,6 +189,18 @@ impl AstToJoinIrLowerer { } } +/// Phase 60 dev-only helper: legacy Break(P2) lowering for comparison tests. +/// +/// `loop_patterns` is private, so this wrapper is exposed at the ast_lowerer boundary. +#[cfg(feature = "normalized_dev")] +pub fn lower_break_legacy_for_comparison( + lowerer: &mut AstToJoinIrLowerer, + program_json: &serde_json::Value, +) -> JoinModule { + loop_patterns::break_pattern::lower_break_legacy_for_comparison(lowerer, program_json) + .unwrap_or_else(|e| panic!("legacy break lowering failed: {:?}", e)) +} + impl Default for AstToJoinIrLowerer { fn default() -> Self { Self::new() diff --git a/src/mir/join_ir/normalized.rs b/src/mir/join_ir/normalized.rs index 6aa272f0..c12197e4 100644 --- a/src/mir/join_ir/normalized.rs +++ b/src/mir/join_ir/normalized.rs @@ -351,6 +351,10 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule { NormalizedDevShape::Pattern3IfSumMinimal | NormalizedDevShape::Pattern3IfSumMulti | NormalizedDevShape::Pattern3IfSumJson + | NormalizedDevShape::SelfhostIfSumP3 + | NormalizedDevShape::SelfhostIfSumP3Ext + | NormalizedDevShape::SelfhostStmtCountP3 + | NormalizedDevShape::SelfhostDetectFormatP3 ) }) { max = max.max(6); diff --git a/src/mir/join_ir/normalized/shape_guard.rs b/src/mir/join_ir/normalized/shape_guard.rs index 1563c0ee..2f7c3ba4 100644 --- a/src/mir/join_ir/normalized/shape_guard.rs +++ b/src/mir/join_ir/normalized/shape_guard.rs @@ -312,7 +312,10 @@ pub fn detect_shapes(module: &JoinModule) -> Vec { || shapes.contains(&NormalizedDevShape::SelfhostArgsParseP2) || shapes.contains(&NormalizedDevShape::SelfhostVerifySchemaP2) { - shapes.retain(|s| *s != NormalizedDevShape::Pattern2Mini); + shapes.retain(|s| { + *s != NormalizedDevShape::Pattern2Mini + && *s != NormalizedDevShape::Pattern4ContinueMinimal + }); } if shapes.contains(&NormalizedDevShape::SelfhostIfSumP3) || shapes.contains(&NormalizedDevShape::SelfhostIfSumP3Ext) @@ -325,6 +328,7 @@ pub fn detect_shapes(module: &JoinModule) -> Vec { NormalizedDevShape::Pattern3IfSumMinimal | NormalizedDevShape::Pattern3IfSumMulti | NormalizedDevShape::Pattern3IfSumJson + | NormalizedDevShape::Pattern4ContinueMinimal ) }); } diff --git a/src/mir/join_ir/ownership/analyzer.rs b/src/mir/join_ir/ownership/analyzer.rs index 67ac3741..24dacde4 100644 --- a/src/mir/join_ir/ownership/analyzer.rs +++ b/src/mir/join_ir/ownership/analyzer.rs @@ -51,11 +51,33 @@ impl OwnershipAnalyzer { self.scopes.clear(); self.next_scope_id = 0; - // Find functions and analyze each + // Find functions and analyze each. + // + // Supported inputs: + // - "functions": [...], with statement nodes using "kind" (test schema) + // - "defs": [...], where FunctionDef nodes contain "params"/"body" (Program(JSON v0)) if let Some(functions) = json.get("functions").and_then(|f| f.as_array()) { for func in functions { self.analyze_function(func, None)?; } + } else if let Some(defs) = json.get("defs").and_then(|d| d.as_array()) { + let mut found = false; + for def in defs { + let def_kind = def + .get("type") + .or_else(|| def.get("kind")) + .and_then(|k| k.as_str()) + .unwrap_or(""); + if def_kind == "FunctionDef" { + found = true; + self.analyze_function(def, None)?; + } + } + if !found { + return Err("OwnershipAnalyzer: no FunctionDef found in 'defs'".to_string()); + } + } else { + return Err("OwnershipAnalyzer: expected top-level 'functions' or 'defs' array".to_string()); } // Convert ScopeInfo to OwnershipPlan @@ -98,26 +120,45 @@ impl OwnershipAnalyzer { } fn analyze_statement(&mut self, stmt: &Value, current_scope: ScopeId) -> Result<(), String> { - let kind = stmt.get("kind").and_then(|k| k.as_str()).unwrap_or(""); + let kind = stmt + .get("kind") + .or_else(|| stmt.get("type")) + .and_then(|k| k.as_str()) + .unwrap_or(""); match kind { "Local" => { - // Variable definition + // NOTE: Program(JSON v0) historically uses Local for both "new binding" + // and "rebind/update". This analyzer treats Local as: + // - definition when the name is not yet defined in scope chain + // - write when the name is already defined (rebind) if let Some(name) = stmt.get("name").and_then(|n| n.as_str()) { - // Find enclosing loop (or function) for ownership - let owner_scope = self.find_enclosing_loop_or_function(current_scope); - self.scopes.get_mut(&owner_scope).unwrap().defined.insert(name.to_string()); + if self.is_defined_in_scope_chain(current_scope, name) { + self.scopes + .get_mut(¤t_scope) + .unwrap() + .writes + .insert(name.to_string()); + } else { + // Find enclosing loop (or function) for ownership + let owner_scope = self.find_enclosing_loop_or_function(current_scope); + self.scopes + .get_mut(&owner_scope) + .unwrap() + .defined + .insert(name.to_string()); + } } // Analyze initializer if present - if let Some(init) = stmt.get("init") { + if let Some(init) = stmt.get("init").or_else(|| stmt.get("expr")) { self.analyze_expression(init, current_scope, false)?; } } - "Assign" => { + "Assign" | "Assignment" => { if let Some(target) = stmt.get("target").and_then(|t| t.as_str()) { self.scopes.get_mut(¤t_scope).unwrap().writes.insert(target.to_string()); } - if let Some(value) = stmt.get("value") { + if let Some(value) = stmt.get("value").or_else(|| stmt.get("expr")) { self.analyze_expression(value, current_scope, false)?; } } @@ -125,7 +166,7 @@ impl OwnershipAnalyzer { let loop_scope = self.alloc_scope(ScopeKind::Loop, Some(current_scope)); // Analyze condition (mark as condition_reads) - if let Some(cond) = stmt.get("condition") { + if let Some(cond) = stmt.get("condition").or_else(|| stmt.get("cond")) { self.analyze_expression(cond, loop_scope, true)?; } @@ -141,7 +182,7 @@ impl OwnershipAnalyzer { let if_scope = self.alloc_scope(ScopeKind::If, Some(current_scope)); // Analyze condition - if let Some(cond) = stmt.get("condition") { + if let Some(cond) = stmt.get("condition").or_else(|| stmt.get("cond")) { self.analyze_expression(cond, if_scope, true)?; } @@ -158,7 +199,11 @@ impl OwnershipAnalyzer { "Block" => { let block_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope)); - if let Some(stmts) = stmt.get("statements").and_then(|s| s.as_array()) { + let stmts = stmt + .get("statements") + .or_else(|| stmt.get("body")) + .and_then(|s| s.as_array()); + if let Some(stmts) = stmts { for s in stmts { self.analyze_statement(s, block_scope)?; } @@ -167,7 +212,7 @@ impl OwnershipAnalyzer { self.propagate_to_parent(block_scope); } "Return" | "Break" | "Continue" => { - if let Some(value) = stmt.get("value") { + if let Some(value) = stmt.get("value").or_else(|| stmt.get("expr")) { self.analyze_expression(value, current_scope, false)?; } } @@ -190,7 +235,11 @@ impl OwnershipAnalyzer { } fn analyze_expression(&mut self, expr: &Value, current_scope: ScopeId, is_condition: bool) -> Result<(), String> { - let kind = expr.get("kind").and_then(|k| k.as_str()).unwrap_or(""); + let kind = expr + .get("kind") + .or_else(|| expr.get("type")) + .and_then(|k| k.as_str()) + .unwrap_or(""); match kind { "Var" | "Variable" | "Identifier" => { @@ -248,6 +297,21 @@ impl OwnershipAnalyzer { Ok(()) } + fn is_defined_in_scope_chain(&self, from_scope: ScopeId, name: &str) -> bool { + let mut current = Some(from_scope); + while let Some(id) = current { + let scope = match self.scopes.get(&id) { + Some(scope) => scope, + None => break, + }; + if scope.defined.contains(name) { + return true; + } + current = scope.parent; + } + false + } + /// Find enclosing Loop or Function scope (body-local ownership rule) fn find_enclosing_loop_or_function(&self, scope_id: ScopeId) -> ScopeId { let scope = &self.scopes[&scope_id]; @@ -629,4 +693,30 @@ mod tests { assert!(any_relay, "Some loop should relay 'total' to function"); } + + #[test] + fn test_program_json_v0_break_fixture_relay_and_capture() { + let program_json: serde_json::Value = serde_json::from_str(include_str!( + "../../../../docs/private/roadmap2/phases/phase-34-joinir-frontend/fixtures/loop_frontend_break.program.json" + )) + .expect("fixture json"); + + let mut analyzer = OwnershipAnalyzer::new(); + let plans = analyzer + .analyze_json(&program_json) + .expect("Program(JSON v0) analysis should succeed"); + + let loop_plan = plans + .iter() + .find(|p| !p.relay_writes.is_empty()) + .expect("expected a loop plan with relay_writes"); + + // i/acc are defined outside the loop but rebound inside loop body -> relay_writes + assert!(loop_plan.relay_writes.iter().any(|r| r.name == "i")); + assert!(loop_plan.relay_writes.iter().any(|r| r.name == "acc")); + + // n is read-only in loop condition -> capture + condition_capture + assert!(loop_plan.captures.iter().any(|c| c.name == "n")); + assert!(loop_plan.condition_captures.iter().any(|c| c.name == "n")); + } } diff --git a/src/mir/join_ir/ownership/plan_to_lowering.rs b/src/mir/join_ir/ownership/plan_to_lowering.rs index db669dca..7e8ac3a8 100644 --- a/src/mir/join_ir/ownership/plan_to_lowering.rs +++ b/src/mir/join_ir/ownership/plan_to_lowering.rs @@ -89,6 +89,74 @@ pub fn plan_to_p2_inputs( }) } +/// Convert OwnershipPlan to P2 lowering inputs, allowing relay_writes (dev-only Phase 60). +/// +/// Rules: +/// - carriers = owned_vars where is_written && name != loop_var +/// + relay_writes where name != loop_var +/// - relay_path.len() > 1 is rejected (single-hop only) +#[cfg(feature = "normalized_dev")] +pub fn plan_to_p2_inputs_with_relay( + plan: &OwnershipPlan, + loop_var: &str, +) -> Result { + let mut carriers = Vec::new(); + + for var in &plan.owned_vars { + if var.name == loop_var || !var.is_written { + continue; + } + + let role = if var.is_condition_only { + CarrierRole::ConditionOnly + } else { + CarrierRole::LoopState + }; + + carriers.push(CarrierVar { + name: var.name.clone(), + role, + init: CarrierInit::FromHost, + host_id: crate::mir::ValueId(0), + join_id: None, + }); + } + + for relay in &plan.relay_writes { + if relay.name == loop_var { + continue; + } + if relay.relay_path.len() > 1 { + return Err(format!( + "Phase 60 limitation: only single-hop relay supported for P2. Var='{}' relay_path_len={}", + relay.name, + relay.relay_path.len() + )); + } + + carriers.push(CarrierVar { + name: relay.name.clone(), + role: CarrierRole::LoopState, + init: CarrierInit::FromHost, + host_id: crate::mir::ValueId(0), + join_id: None, + }); + } + + let captures: Vec = plan.captures.iter().map(|c| c.name.clone()).collect(); + let condition_captures: Vec = plan + .condition_captures + .iter() + .map(|c| c.name.clone()) + .collect(); + + Ok(P2LoweringInputs { + carriers, + captures, + condition_captures, + }) +} + /// Convert OwnershipPlan to P3 (if-sum) lowering inputs. /// /// P3 patterns have multiple carriers (sum, count, etc.) updated conditionally. @@ -152,6 +220,22 @@ pub fn plan_to_p3_inputs( }) } +/// Convert OwnershipPlan to P3 lowering inputs, allowing relay_writes (dev-only Phase 60). +/// +/// Rules are identical to `plan_to_p2_inputs_with_relay`, but output type is P3LoweringInputs. +#[cfg(feature = "normalized_dev")] +pub fn plan_to_p3_inputs_with_relay( + plan: &OwnershipPlan, + loop_var: &str, +) -> Result { + let p2 = plan_to_p2_inputs_with_relay(plan, loop_var)?; + Ok(P3LoweringInputs { + carriers: p2.carriers, + captures: p2.captures, + condition_captures: p2.condition_captures, + }) +} + #[cfg(test)] mod tests { use super::*; @@ -220,6 +304,44 @@ mod tests { .contains("relay_writes not yet supported")); } + #[test] + #[cfg(feature = "normalized_dev")] + fn test_relay_single_hop_accepted_in_with_relay() { + let mut plan = OwnershipPlan::new(ScopeId(1)); + plan.owned_vars.push(ScopeOwnedVar { + name: "i".to_string(), + is_written: true, + is_condition_only: false, + }); + plan.relay_writes.push(RelayVar { + name: "sum".to_string(), + owner_scope: ScopeId(0), + relay_path: vec![ScopeId(42)], + }); + + let inputs = plan_to_p2_inputs_with_relay(&plan, "i").expect("with_relay should accept"); + assert_eq!(inputs.carriers.len(), 1); + assert_eq!(inputs.carriers[0].name, "sum"); + assert_eq!(inputs.carriers[0].role, CarrierRole::LoopState); + } + + #[test] + #[cfg(feature = "normalized_dev")] + fn test_relay_multi_hop_rejected_in_with_relay() { + let mut plan = OwnershipPlan::new(ScopeId(1)); + plan.relay_writes.push(RelayVar { + name: "outer_var".to_string(), + owner_scope: ScopeId(0), + relay_path: vec![ScopeId(1), ScopeId(2)], + }); + + let result = plan_to_p2_inputs_with_relay(&plan, "i"); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .contains("only single-hop relay supported")); + } + #[test] #[cfg(feature = "normalized_dev")] fn test_read_only_vars_not_carriers() { @@ -402,4 +524,24 @@ mod tests { .unwrap_err() .contains("relay_writes not yet supported for P3")); } + + #[test] + #[cfg(feature = "normalized_dev")] + fn test_p3_with_relay_accepts_single_hop() { + let mut plan = OwnershipPlan::new(ScopeId(1)); + plan.owned_vars.push(ScopeOwnedVar { + name: "i".to_string(), + is_written: true, + is_condition_only: false, + }); + plan.relay_writes.push(RelayVar { + name: "sum".to_string(), + owner_scope: ScopeId(0), + relay_path: vec![], + }); + + let inputs = plan_to_p3_inputs_with_relay(&plan, "i").expect("P3 with_relay should accept"); + assert_eq!(inputs.carriers.len(), 1); + assert_eq!(inputs.carriers[0].name, "sum"); + } } diff --git a/tests/normalized_joinir_min.rs b/tests/normalized_joinir_min.rs index 5624b0a4..fa9b1400 100644 --- a/tests/normalized_joinir_min.rs +++ b/tests/normalized_joinir_min.rs @@ -638,7 +638,7 @@ fn normalized_selfhost_if_sum_p3_vm_bridge_direct_matches_structured() { let _ctx = normalized_dev_test_ctx(); let structured = build_selfhost_if_sum_p3_structured_for_normalized_dev(); let entry = structured.entry.expect("structured entry required"); - let cases = [0, 1, 3, 4]; + let cases = [0, 1, 3, 4, 5]; for n in cases { let input = [JoinValue::Int(n)]; @@ -646,6 +646,7 @@ fn normalized_selfhost_if_sum_p3_vm_bridge_direct_matches_structured() { let dev = run_joinir_vm_bridge(&structured, entry, &input, true); assert_eq!(base, dev, "vm bridge mismatch for selfhost_if_sum_p3 n={}", n); + assert_eq!(dev, JoinValue::Int(expected_selfhost_if_sum_p3(n))); } } @@ -654,7 +655,7 @@ fn normalized_selfhost_if_sum_p3_ext_vm_bridge_direct_matches_structured() { let _ctx = normalized_dev_test_ctx(); let structured = build_selfhost_if_sum_p3_ext_structured_for_normalized_dev(); let entry = structured.entry.expect("structured entry required"); - let cases = [0, 1, 3, 4]; + let cases = [0, 1, 3, 4, 5]; for n in cases { let input = [JoinValue::Int(n)]; @@ -662,9 +663,30 @@ fn normalized_selfhost_if_sum_p3_ext_vm_bridge_direct_matches_structured() { let dev = run_joinir_vm_bridge(&structured, entry, &input, true); assert_eq!(base, dev, "vm bridge mismatch for selfhost_if_sum_p3_ext n={}", n); + assert_eq!(dev, JoinValue::Int(expected_selfhost_if_sum_p3_ext(n))); } } +fn expected_selfhost_if_sum_p3(n: i64) -> i64 { + if n <= 1 { + return 0; + } + let sum = (n - 1) * n / 2; + let count = n - 1; + sum + count +} + +fn expected_selfhost_if_sum_p3_ext(n: i64) -> i64 { + if n <= 0 { + return 0; + } + // i=0: sum += 1 + // i=1..n-1: sum += i, count += 1 + let sum = 1 + (n - 1) * n / 2; + let count = n - 1; + sum + count +} + /// Phase 53: selfhost args-parse P2 (practical variation with string carrier) #[test] fn normalized_selfhost_args_parse_p2_vm_bridge_direct_matches_structured() { @@ -1067,17 +1089,17 @@ fn test_phase54_structural_axis_discrimination_p3() { } } -/// Phase 58: Compare ownership-based P2 lowering with existing path +/// Phase 60: Ownership relay threading helpers for P2 (analysis + contract) /// -/// This test demonstrates that OwnershipAnalyzer can analyze P2 fixtures -/// and plan_to_p2_inputs can convert the result to reasonable CarrierVar structures. -/// -/// Note: This is analysis-only testing. We're NOT modifying the actual lowering path yet. -/// Full integration will come in Phase 60+ after validating the analysis is correct. +/// plan_to_p2_inputs remains Fail-Fast on relay_writes (legacy contract), +/// while plan_to_p2_inputs_with_relay accepts single-hop relay and promotes them +/// to carriers (dev-only). #[test] #[cfg(feature = "normalized_dev")] -fn test_phase58_ownership_p2_comparison() { - use nyash_rust::mir::join_ir::ownership::{plan_to_p2_inputs, OwnershipAnalyzer}; +fn test_phase60_ownership_p2_with_relay_conversion() { + use nyash_rust::mir::join_ir::ownership::{ + plan_to_p2_inputs, plan_to_p2_inputs_with_relay, OwnershipAnalyzer, + }; use serde_json::json; // Create a simple P2 fixture JSON (loop with i and sum) @@ -1154,11 +1176,11 @@ fn test_phase58_ownership_p2_comparison() { loop_plan.relay_writes ); - // Phase 58 Fail-Fast: relay_writes should be rejected + // Legacy Fail-Fast: relay_writes should be rejected let result = plan_to_p2_inputs(loop_plan, "i"); assert!( result.is_err(), - "Phase 58: relay_writes should be rejected (not supported yet)" + "Legacy contract: relay_writes should be rejected" ); let err = result.unwrap_err(); assert!( @@ -1167,7 +1189,22 @@ fn test_phase58_ownership_p2_comparison() { err ); - eprintln!("[phase58/test] Phase 58 Fail-Fast verified: relay_writes correctly rejected"); + // Phase 60 dev-only: with_relay should accept and include relay vars as carriers + let inputs_with_relay = + plan_to_p2_inputs_with_relay(loop_plan, "i").expect("with_relay should accept"); + assert!( + inputs_with_relay.carriers.iter().any(|c| c.name == "sum"), + "relay carrier sum should be promoted" + ); + + eprintln!( + "[phase60/test] with_relay carriers={:?}", + inputs_with_relay + .carriers + .iter() + .map(|c| &c.name) + .collect::>() + ); // Also test the case where variables ARE owned by loop (future scenario) // This would work once we support loop-local carriers @@ -1239,7 +1276,41 @@ fn test_phase58_ownership_p2_comparison() { assert_eq!(inputs.carriers.len(), 1, "Should have 1 carrier (sum)"); assert_eq!(inputs.carriers[0].name, "sum"); - eprintln!("[phase58/test] Phase 58 conversion verified: sum correctly extracted as carrier"); + eprintln!("[phase60/test] Loop-local conversion verified"); +} + +/// Phase 60: P2 dev-only ownership relay route matches legacy Break lowering. +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase60_break_lowering_ownership_matches_legacy() { + use nyash_rust::mir::join_ir::frontend::ast_lowerer::lower_break_legacy_for_comparison; + use nyash_rust::mir::join_ir::frontend::ast_lowerer::AstToJoinIrLowerer; + + let _ctx = normalized_dev_test_ctx(); + + let program_json: serde_json::Value = serde_json::from_str(include_str!( + "../docs/private/roadmap2/phases/phase-34-joinir-frontend/fixtures/loop_frontend_break.program.json" + )) + .expect("fixture json"); + + let mut lowerer_new = AstToJoinIrLowerer::new(); + let structured_new = lowerer_new.lower_program_json(&program_json); + + let mut lowerer_old = AstToJoinIrLowerer::new(); + let structured_old = + lower_break_legacy_for_comparison(&mut lowerer_old, &program_json); + + let entry_new = structured_new.entry.expect("new entry"); + let entry_old = structured_old.entry.expect("old entry"); + let input = vec![JoinValue::Int(5)]; + + let out_old = run_joinir_vm_bridge(&structured_old, entry_old, &input, false); + let out_new = run_joinir_vm_bridge(&structured_new, entry_new, &input, false); + + assert_eq!( + out_old, out_new, + "ownership relay dev route must match legacy output" + ); } /// Phase 59: P3 with outer-owned carriers (relay case) should fail-fast @@ -1412,3 +1483,40 @@ fn test_phase59_ownership_p3_loop_local_success() { eprintln!("[phase59/test] P3 loop-local conversion verified: sum and count correctly extracted as carriers"); } + +/// Phase 60: Program(JSON v0) fixture (selfhost_if_sum_p3) should produce relay_writes and convert with single-hop relay. +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase60_ownership_p3_program_json_fixture_with_relay() { + use nyash_rust::mir::join_ir::ownership::{plan_to_p3_inputs_with_relay, OwnershipAnalyzer}; + + let program_json: serde_json::Value = serde_json::from_str(include_str!( + "../docs/private/roadmap2/phases/normalized_dev/fixtures/selfhost_if_sum_p3.program.json" + )) + .expect("fixture json"); + + let mut analyzer = OwnershipAnalyzer::new(); + let plans = analyzer + .analyze_json(&program_json) + .expect("Program(JSON v0) analysis should succeed"); + + let loop_plan = plans + .iter() + .find(|p| !p.relay_writes.is_empty()) + .expect("expected a loop plan with relay_writes"); + + // i/sum/count are defined outside the loop but updated in the loop body -> relay_writes + assert!(loop_plan.relay_writes.iter().any(|r| r.name == "i")); + assert!(loop_plan.relay_writes.iter().any(|r| r.name == "sum")); + assert!(loop_plan.relay_writes.iter().any(|r| r.name == "count")); + + let inputs = plan_to_p3_inputs_with_relay(loop_plan, "i").expect("with_relay should succeed"); + + let mut carriers: Vec<&str> = inputs.carriers.iter().map(|c| c.name.as_str()).collect(); + carriers.sort(); + assert_eq!(carriers, vec!["count", "sum"]); + + // n is read-only in loop condition -> capture + condition_capture + assert!(inputs.captures.iter().any(|n| n == "n")); + assert!(inputs.condition_captures.iter().any(|n| n == "n")); +}