Phase 61: structural if-sum+break lowering (dev-only)
This commit is contained in:
@ -230,7 +230,7 @@
|
|||||||
14. **Phase 57-OWNERSHIP-ANALYZER-DEV(完了✅ 2025-12-12)**: OwnershipPlan を生成する解析箱の実装
|
14. **Phase 57-OWNERSHIP-ANALYZER-DEV(完了✅ 2025-12-12)**: OwnershipPlan を生成する解析箱の実装
|
||||||
- `OwnershipAnalyzer` を追加し、ネスト含む reads/writes/owned を集計→ carriers/relay/captures を plan 化。
|
- `OwnershipAnalyzer` を追加し、ネスト含む reads/writes/owned を集計→ carriers/relay/captures を plan 化。
|
||||||
- 既存 fixtures(pattern2/3, jsonparser, selfhost)で 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)
|
15. **Phase 58-OWNERSHIP-PLUMB-P2-DEV(完了✅ 2025-12-12)**: P2 conversion helper (dev-only)
|
||||||
- `plan_to_p2_inputs()` でOwnershipPlan→P2LoweringInputs変換
|
- `plan_to_p2_inputs()` でOwnershipPlan→P2LoweringInputs変換
|
||||||
- Fail-Fast: relay_writes 未対応(Phase 60で対応予定)
|
- Fail-Fast: relay_writes 未対応(Phase 60で対応予定)
|
||||||
@ -242,10 +242,19 @@
|
|||||||
- Fail-Fast: relay_writes 未対応(Phase 60で対応予定)
|
- Fail-Fast: relay_writes 未対応(Phase 60で対応予定)
|
||||||
- 4つのユニットテスト + 2つのintegrationテスト
|
- 4つのユニットテスト + 2つのintegrationテスト
|
||||||
- 設計詳細: [PHASE_59_SUMMARY.md](docs/development/current/main/PHASE_59_SUMMARY.md)
|
- 設計詳細: [PHASE_59_SUMMARY.md](docs/development/current/main/PHASE_59_SUMMARY.md)
|
||||||
17. **Phase 60-OWNERSHIP-RELAY-IMPL(次のフォーカス候補)**: Relay support for P2/P3
|
17. **Phase 60-OWNERSHIP-RELAY-IMPL(完了✅ 2025-12-12)**: Relay support for P2/P3 (dev-only)
|
||||||
- relay_writes対応実装
|
- `plan_to_p2_inputs_with_relay()` / `plan_to_p3_inputs_with_relay()` を追加(単一hopのみ許可、multi-hopはFail-Fast)
|
||||||
- P2/P3両方の変換器に統合
|
- P2 Break lowering を dev-only で ownership-with-relay に接続し、legacy 経路との VM 出力一致を比較テストで固定。
|
||||||
18. JoinIR Verify / 最適化まわり
|
- 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 ビルドで検証しているので、
|
- すでに PHI/ValueId 契約は debug ビルドで検証しているので、
|
||||||
必要なら SSA‑DFA や軽い最適化(Loop invariant / Strength reduction)を検討。
|
必要なら SSA‑DFA や軽い最適化(Loop invariant / Strength reduction)を検討。
|
||||||
|
|
||||||
|
|||||||
56
docs/development/current/main/PHASE_61_SUMMARY.md
Normal file
56
docs/development/current/main/PHASE_61_SUMMARY.md
Normal 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 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 と不変条件を明文化)。
|
||||||
@ -138,8 +138,11 @@ pub struct OwnershipPlan {
|
|||||||
- **Phase 57**: OwnershipAnalyzer implementation (dev-only)
|
- **Phase 57**: OwnershipAnalyzer implementation (dev-only)
|
||||||
- **Phase 58**: P2 plumbing (dev-only)
|
- **Phase 58**: P2 plumbing (dev-only)
|
||||||
- **Phase 59**: P3 plumbing (dev-only)
|
- **Phase 59**: P3 plumbing (dev-only)
|
||||||
- **Phase 60**: Cleanup dev heuristics
|
- **Phase 60**: Single-hop relay threading for fixtures (dev-only)
|
||||||
- **Phase 61**: Canonical promotion decision
|
- **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
|
## Module Boundary
|
||||||
|
|
||||||
@ -244,6 +247,13 @@ loop {
|
|||||||
|
|
||||||
### Implementation Details
|
### 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**:
|
**Body-Local Ownership Rule**:
|
||||||
```rust
|
```rust
|
||||||
// Example: local in if/block → enclosing loop owns it
|
// Example: local in if/block → enclosing loop owns it
|
||||||
@ -267,5 +277,9 @@ loop {
|
|||||||
## Status
|
## Status
|
||||||
|
|
||||||
- ✅ Phase 56: Design + interface skeleton completed
|
- ✅ Phase 56: Design + interface skeleton completed
|
||||||
- ✅ Phase 57: Analyzer implemented (dev-only, not connected to lowering yet)
|
- ✅ Phase 57: Analyzer implemented (dev-only)
|
||||||
- ⏳ Phase 58+: Plumbing integration pending
|
- ✅ 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+
|
||||||
|
|||||||
Submodule docs/private updated: 1e76c99f44...00e3e63780
@ -25,10 +25,17 @@ use super::common::{
|
|||||||
process_local_inits,
|
process_local_inits,
|
||||||
};
|
};
|
||||||
use super::param_guess::{build_param_order, compute_param_guess};
|
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 super::{AstToJoinIrLowerer, JoinModule, LoweringError};
|
||||||
use crate::mir::join_ir::{JoinFunction, JoinInst};
|
use crate::mir::join_ir::{JoinFunction, JoinInst};
|
||||||
use crate::mir::ValueId;
|
use crate::mir::ValueId;
|
||||||
|
|
||||||
|
#[cfg(feature = "normalized_dev")]
|
||||||
|
use crate::mir::join_ir::ownership::{
|
||||||
|
plan_to_p2_inputs_with_relay, OwnershipAnalyzer,
|
||||||
|
};
|
||||||
|
|
||||||
/// Break パターンを JoinModule に変換
|
/// Break パターンを JoinModule に変換
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@ -37,6 +44,26 @@ use crate::mir::ValueId;
|
|||||||
pub fn lower(
|
pub fn lower(
|
||||||
lowerer: &mut AstToJoinIrLowerer,
|
lowerer: &mut AstToJoinIrLowerer,
|
||||||
program_json: &serde_json::Value,
|
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> {
|
) -> Result<JoinModule, LoweringError> {
|
||||||
// 1. Program(JSON) をパース
|
// 1. Program(JSON) をパース
|
||||||
let parsed = parse_program_json(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))
|
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, ¶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<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 関数を生成
|
/// Break パターン用 entry 関数を生成
|
||||||
fn create_entry_function_break(
|
fn create_entry_function_break(
|
||||||
ctx: &super::common::LoopContext,
|
ctx: &super::common::LoopContext,
|
||||||
|
|||||||
@ -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,
|
||||||
|
¶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<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 ¶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<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))
|
||||||
|
}
|
||||||
@ -19,6 +19,8 @@ pub mod break_pattern;
|
|||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod continue_pattern;
|
pub mod continue_pattern;
|
||||||
pub mod filter;
|
pub mod filter;
|
||||||
|
#[cfg(feature = "normalized_dev")]
|
||||||
|
pub mod if_sum_break_pattern;
|
||||||
pub mod param_guess;
|
pub mod param_guess;
|
||||||
pub mod print_tokens;
|
pub mod print_tokens;
|
||||||
pub mod simple;
|
pub mod simple;
|
||||||
|
|||||||
@ -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 {
|
impl Default for AstToJoinIrLowerer {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
|
|||||||
@ -351,6 +351,10 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule {
|
|||||||
NormalizedDevShape::Pattern3IfSumMinimal
|
NormalizedDevShape::Pattern3IfSumMinimal
|
||||||
| NormalizedDevShape::Pattern3IfSumMulti
|
| NormalizedDevShape::Pattern3IfSumMulti
|
||||||
| NormalizedDevShape::Pattern3IfSumJson
|
| NormalizedDevShape::Pattern3IfSumJson
|
||||||
|
| NormalizedDevShape::SelfhostIfSumP3
|
||||||
|
| NormalizedDevShape::SelfhostIfSumP3Ext
|
||||||
|
| NormalizedDevShape::SelfhostStmtCountP3
|
||||||
|
| NormalizedDevShape::SelfhostDetectFormatP3
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
max = max.max(6);
|
max = max.max(6);
|
||||||
|
|||||||
@ -312,7 +312,10 @@ pub fn detect_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
|
|||||||
|| shapes.contains(&NormalizedDevShape::SelfhostArgsParseP2)
|
|| shapes.contains(&NormalizedDevShape::SelfhostArgsParseP2)
|
||||||
|| shapes.contains(&NormalizedDevShape::SelfhostVerifySchemaP2)
|
|| shapes.contains(&NormalizedDevShape::SelfhostVerifySchemaP2)
|
||||||
{
|
{
|
||||||
shapes.retain(|s| *s != NormalizedDevShape::Pattern2Mini);
|
shapes.retain(|s| {
|
||||||
|
*s != NormalizedDevShape::Pattern2Mini
|
||||||
|
&& *s != NormalizedDevShape::Pattern4ContinueMinimal
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if shapes.contains(&NormalizedDevShape::SelfhostIfSumP3)
|
if shapes.contains(&NormalizedDevShape::SelfhostIfSumP3)
|
||||||
|| shapes.contains(&NormalizedDevShape::SelfhostIfSumP3Ext)
|
|| shapes.contains(&NormalizedDevShape::SelfhostIfSumP3Ext)
|
||||||
@ -325,6 +328,7 @@ pub fn detect_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
|
|||||||
NormalizedDevShape::Pattern3IfSumMinimal
|
NormalizedDevShape::Pattern3IfSumMinimal
|
||||||
| NormalizedDevShape::Pattern3IfSumMulti
|
| NormalizedDevShape::Pattern3IfSumMulti
|
||||||
| NormalizedDevShape::Pattern3IfSumJson
|
| NormalizedDevShape::Pattern3IfSumJson
|
||||||
|
| NormalizedDevShape::Pattern4ContinueMinimal
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,11 +51,33 @@ impl OwnershipAnalyzer {
|
|||||||
self.scopes.clear();
|
self.scopes.clear();
|
||||||
self.next_scope_id = 0;
|
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()) {
|
if let Some(functions) = json.get("functions").and_then(|f| f.as_array()) {
|
||||||
for func in functions {
|
for func in functions {
|
||||||
self.analyze_function(func, None)?;
|
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
|
// Convert ScopeInfo to OwnershipPlan
|
||||||
@ -98,26 +120,45 @@ impl OwnershipAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn analyze_statement(&mut self, stmt: &Value, current_scope: ScopeId) -> Result<(), String> {
|
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 {
|
match kind {
|
||||||
"Local" => {
|
"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()) {
|
if let Some(name) = stmt.get("name").and_then(|n| n.as_str()) {
|
||||||
|
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
|
// Find enclosing loop (or function) for ownership
|
||||||
let owner_scope = self.find_enclosing_loop_or_function(current_scope);
|
let owner_scope = self.find_enclosing_loop_or_function(current_scope);
|
||||||
self.scopes.get_mut(&owner_scope).unwrap().defined.insert(name.to_string());
|
self.scopes
|
||||||
|
.get_mut(&owner_scope)
|
||||||
|
.unwrap()
|
||||||
|
.defined
|
||||||
|
.insert(name.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Analyze initializer if present
|
// 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)?;
|
self.analyze_expression(init, current_scope, false)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"Assign" => {
|
"Assign" | "Assignment" => {
|
||||||
if let Some(target) = stmt.get("target").and_then(|t| t.as_str()) {
|
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());
|
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)?;
|
self.analyze_expression(value, current_scope, false)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,7 +166,7 @@ impl OwnershipAnalyzer {
|
|||||||
let loop_scope = self.alloc_scope(ScopeKind::Loop, Some(current_scope));
|
let loop_scope = self.alloc_scope(ScopeKind::Loop, Some(current_scope));
|
||||||
|
|
||||||
// Analyze condition (mark as condition_reads)
|
// 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)?;
|
self.analyze_expression(cond, loop_scope, true)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +182,7 @@ impl OwnershipAnalyzer {
|
|||||||
let if_scope = self.alloc_scope(ScopeKind::If, Some(current_scope));
|
let if_scope = self.alloc_scope(ScopeKind::If, Some(current_scope));
|
||||||
|
|
||||||
// Analyze condition
|
// 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)?;
|
self.analyze_expression(cond, if_scope, true)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +199,11 @@ impl OwnershipAnalyzer {
|
|||||||
"Block" => {
|
"Block" => {
|
||||||
let block_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope));
|
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 {
|
for s in stmts {
|
||||||
self.analyze_statement(s, block_scope)?;
|
self.analyze_statement(s, block_scope)?;
|
||||||
}
|
}
|
||||||
@ -167,7 +212,7 @@ impl OwnershipAnalyzer {
|
|||||||
self.propagate_to_parent(block_scope);
|
self.propagate_to_parent(block_scope);
|
||||||
}
|
}
|
||||||
"Return" | "Break" | "Continue" => {
|
"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)?;
|
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> {
|
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 {
|
match kind {
|
||||||
"Var" | "Variable" | "Identifier" => {
|
"Var" | "Variable" | "Identifier" => {
|
||||||
@ -248,6 +297,21 @@ impl OwnershipAnalyzer {
|
|||||||
Ok(())
|
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)
|
/// Find enclosing Loop or Function scope (body-local ownership rule)
|
||||||
fn find_enclosing_loop_or_function(&self, scope_id: ScopeId) -> ScopeId {
|
fn find_enclosing_loop_or_function(&self, scope_id: ScopeId) -> ScopeId {
|
||||||
let scope = &self.scopes[&scope_id];
|
let scope = &self.scopes[&scope_id];
|
||||||
@ -629,4 +693,30 @@ mod tests {
|
|||||||
|
|
||||||
assert!(any_relay, "Some loop should relay 'total' to function");
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
/// Convert OwnershipPlan to P3 (if-sum) lowering inputs.
|
||||||
///
|
///
|
||||||
/// P3 patterns have multiple carriers (sum, count, etc.) updated conditionally.
|
/// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -220,6 +304,44 @@ mod tests {
|
|||||||
.contains("relay_writes not yet supported"));
|
.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]
|
#[test]
|
||||||
#[cfg(feature = "normalized_dev")]
|
#[cfg(feature = "normalized_dev")]
|
||||||
fn test_read_only_vars_not_carriers() {
|
fn test_read_only_vars_not_carriers() {
|
||||||
@ -402,4 +524,24 @@ mod tests {
|
|||||||
.unwrap_err()
|
.unwrap_err()
|
||||||
.contains("relay_writes not yet supported for P3"));
|
.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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -638,7 +638,7 @@ fn normalized_selfhost_if_sum_p3_vm_bridge_direct_matches_structured() {
|
|||||||
let _ctx = normalized_dev_test_ctx();
|
let _ctx = normalized_dev_test_ctx();
|
||||||
let structured = build_selfhost_if_sum_p3_structured_for_normalized_dev();
|
let structured = build_selfhost_if_sum_p3_structured_for_normalized_dev();
|
||||||
let entry = structured.entry.expect("structured entry required");
|
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 {
|
for n in cases {
|
||||||
let input = [JoinValue::Int(n)];
|
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);
|
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!(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 _ctx = normalized_dev_test_ctx();
|
||||||
let structured = build_selfhost_if_sum_p3_ext_structured_for_normalized_dev();
|
let structured = build_selfhost_if_sum_p3_ext_structured_for_normalized_dev();
|
||||||
let entry = structured.entry.expect("structured entry required");
|
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 {
|
for n in cases {
|
||||||
let input = [JoinValue::Int(n)];
|
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);
|
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!(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)
|
/// Phase 53: selfhost args-parse P2 (practical variation with string carrier)
|
||||||
#[test]
|
#[test]
|
||||||
fn normalized_selfhost_args_parse_p2_vm_bridge_direct_matches_structured() {
|
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
|
/// plan_to_p2_inputs remains Fail-Fast on relay_writes (legacy contract),
|
||||||
/// and plan_to_p2_inputs can convert the result to reasonable CarrierVar structures.
|
/// while plan_to_p2_inputs_with_relay accepts single-hop relay and promotes them
|
||||||
///
|
/// to carriers (dev-only).
|
||||||
/// 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.
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "normalized_dev")]
|
#[cfg(feature = "normalized_dev")]
|
||||||
fn test_phase58_ownership_p2_comparison() {
|
fn test_phase60_ownership_p2_with_relay_conversion() {
|
||||||
use nyash_rust::mir::join_ir::ownership::{plan_to_p2_inputs, OwnershipAnalyzer};
|
use nyash_rust::mir::join_ir::ownership::{
|
||||||
|
plan_to_p2_inputs, plan_to_p2_inputs_with_relay, OwnershipAnalyzer,
|
||||||
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
// Create a simple P2 fixture JSON (loop with i and sum)
|
// 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
|
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");
|
let result = plan_to_p2_inputs(loop_plan, "i");
|
||||||
assert!(
|
assert!(
|
||||||
result.is_err(),
|
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();
|
let err = result.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
@ -1167,7 +1189,22 @@ fn test_phase58_ownership_p2_comparison() {
|
|||||||
err
|
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)
|
// Also test the case where variables ARE owned by loop (future scenario)
|
||||||
// This would work once we support loop-local carriers
|
// 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.len(), 1, "Should have 1 carrier (sum)");
|
||||||
assert_eq!(inputs.carriers[0].name, "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
|
/// 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");
|
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"));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user