Phase 61: structural if-sum+break lowering (dev-only)

This commit is contained in:
nyash-codex
2025-12-12 22:15:41 +09:00
parent 6aba138950
commit acb6720d9b
13 changed files with 1140 additions and 39 deletions

View File

@ -230,7 +230,7 @@
14. **Phase 57-OWNERSHIP-ANALYZER-DEV完了✅ 2025-12-12**: OwnershipPlan を生成する解析箱の実装
- `OwnershipAnalyzer` を追加し、ネスト含む reads/writes/owned を集計→ carriers/relay/captures を plan 化。
- 既存 fixturespattern2/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 ビルドで検証しているので、
必要なら SSADFA や軽い最適化Loop invariant / Strength reductionを検討。

View File

@ -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 P3NormalizedDev 経路)については 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 と不変条件を明文化)。

View File

@ -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+

View File

@ -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<JoinModule, LoweringError> {
#[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<JoinModule, LoweringError> {
// 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<JoinModule, LoweringError> {
// 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, &param_order);
let loop_step_func = create_loop_step_function_break(
lowerer,
&ctx,
&parsed.func_name,
loop_cond_expr,
break_cond_expr,
loop_body,
&param_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<Vec<(String, ValueId)>> {
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::<String>::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<JoinModule, LoweringError> {
lower_legacy_param_guess(lowerer, program_json)
}
/// Break パターン用 entry 関数を生成
fn create_entry_function_break(
ctx: &super::common::LoopContext,

View File

@ -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<Option<JoinModule>, 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<String> =
inputs.carriers.iter().map(|c| c.name.clone()).collect();
let expected: std::collections::BTreeSet<String> =
[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::<String>::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,
&param_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,
&param_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<String> {
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<JoinInst>,
entry_ctx: &mut super::super::context::ExtractCtx,
param_order: &[(String, ValueId)],
) -> JoinFunction {
let loop_args: Vec<ValueId> = 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<JoinFunction, LoweringError> {
use super::super::context::ExtractCtx;
let param_names: Vec<String> = 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 &param_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<Option<serde_json::Value>, LoweringError> {
let mut found: Option<serde_json::Value> = 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))
}

View File

@ -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;

View File

@ -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()

View File

@ -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);

View File

@ -312,7 +312,10 @@ pub fn detect_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
|| 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> {
NormalizedDevShape::Pattern3IfSumMinimal
| NormalizedDevShape::Pattern3IfSumMulti
| NormalizedDevShape::Pattern3IfSumJson
| NormalizedDevShape::Pattern4ContinueMinimal
)
});
}

View File

@ -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(&current_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(&current_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"));
}
}

View File

@ -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<P2LoweringInputs, String> {
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<String> = plan.captures.iter().map(|c| c.name.clone()).collect();
let condition_captures: Vec<String> = 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<P3LoweringInputs, String> {
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");
}
}

View File

@ -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::<Vec<_>>()
);
// 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"));
}