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