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`
|
||||
6. Phase 93: ConditionOnly Derived Slot(Trim / body-local)
|
||||
- `docs/development/current/main/phases/phase-93/README.md`
|
||||
7. Phase 94: P5b “完全E2E”(escape skip / derived)
|
||||
- `docs/development/current/main/phases/phase-94/README.md`
|
||||
6. MIR Builder(Context 分割の入口)
|
||||
- `src/mir/builder/README.md`
|
||||
7. Scope/BindingId(shadowing・束縛同一性の段階移行)
|
||||
|
||||
@ -49,6 +49,13 @@
|
||||
- schedule: `body-init → derived → break` を評価順SSOTとして強制
|
||||
- Phase 記録(入口): `docs/development/current/main/phases/phase-93/README.md`
|
||||
|
||||
## 2025‑12‑16:Phase 94(短報)
|
||||
|
||||
- P5b escape(`ch` 再代入 + `i` の +1/+2)を “derived(Select)” として扱い、VM E2E を固定。
|
||||
- 新箱: `BodyLocalDerivedEmitter` + 明示ポリシー(strict で理由付き Fail-Fast)
|
||||
- integration smoke: `tools/smokes/v2/profiles/integration/apps/phase94_p5b_escape_e2e.sh`
|
||||
- Phase 記録(入口): `docs/development/current/main/phases/phase-94/README.md`
|
||||
|
||||
## 2025‑12‑14:現状サマリ
|
||||
|
||||
(補足)docs が増えて迷子になったときの「置き場所ルール(SSOT)」:
|
||||
|
||||
@ -9,8 +9,8 @@ Related:
|
||||
## 直近(JoinIR/selfhost)
|
||||
|
||||
- **P5b “完全E2E”**(escape skip の実ループを end-to-end で固定)
|
||||
- 現状: 認識(Phase 91)+ lowering基盤(Phase 92)は完了、promotion が未整備で E2E を保留
|
||||
- 入口: `docs/development/current/main/phases/phase-92/README.md`
|
||||
- 現状: Phase 94 で VM E2E まで固定済み。次は selfhost 実コード(`apps/selfhost-vm/json_loader.hako`)へ横展開して回帰を減らす。
|
||||
- 入口: `docs/development/current/main/phases/phase-94/README.md`
|
||||
- **制御の再帰合成(docs-only → dev-only段階投入)**
|
||||
- ねらい: `loop/if` ネストの “構造” を SSOT(ControlTree/StepTree)で表せるようにする
|
||||
- 注意: canonicalizer は観測/構造SSOTまで(ValueId/PHI配線は Normalized 側へ)
|
||||
|
||||
@ -107,6 +107,7 @@ flowchart LR
|
||||
- Error tags(SSOT): [`src/mir/join_ir/lowering/error_tags.rs`](../../../../../src/mir/join_ir/lowering/error_tags.rs)
|
||||
- Loop Canonicalizer(前処理 SSOT): [`src/mir/loop_canonicalizer/mod.rs`](../../../../../src/mir/loop_canonicalizer/mod.rs)
|
||||
- ConditionOnly Derived Slot(Phase 93): [`src/mir/join_ir/lowering/common/condition_only_emitter.rs`](../../../../../src/mir/join_ir/lowering/common/condition_only_emitter.rs)
|
||||
- BodyLocalDerived Slot(Phase 94 / P5b): [`src/mir/join_ir/lowering/common/body_local_derived_emitter.rs`](../../../../../src/mir/join_ir/lowering/common/body_local_derived_emitter.rs)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Phase 93: ConditionOnly Derived Slot(Trim / body-local)
|
||||
|
||||
Status: Active
|
||||
Status: ✅ Done(P0+P1)
|
||||
Scope: Pattern2(Loop with Break)で「ConditionOnly(PHIで運ばない派生値)」を毎イテレーション再計算できるようにする。
|
||||
Related:
|
||||
- 設計地図(入口): `docs/development/current/main/design/joinir-design-map.md`
|
||||
@ -27,6 +27,19 @@ JoinIR で “初回の計算値が固定される” 事故を避ける。
|
||||
- Trim: `src/mir/builder/control_flow/joinir/patterns/trim_loop_lowering.rs`
|
||||
- ConditionOnly 用 break 生成(反転の有無を明示)
|
||||
|
||||
## 成果(P1): 箱化・語彙のSSOT化
|
||||
|
||||
コミット: `c213ecc3 refactor(mir): Phase 93 リファクタリング - 箱化モジュール化`
|
||||
|
||||
- schedule:
|
||||
- `decide_pattern2_schedule()` に判定を集約し、理由(ConditionOnly / body-local / loop-local / default)をSSOT化
|
||||
- 決定→生成を分離(decision→build)してテスト容易性を上げた
|
||||
- ConditionOnlyRecipe:
|
||||
- `BreakSemantics`(WhenMatch / WhenNotMatch)を recipe に保持し、break 条件生成の責務を recipe 側へ移動
|
||||
- `trim_loop_lowering.rs` 側の重複ヘルパーを削除
|
||||
- Debug(新 env 追加なし):
|
||||
- 既存の `NYASH_JOINIR_DEBUG=1` の範囲で、`[phase93/*]` prefix に統一
|
||||
|
||||
## 受け入れ基準(P0)
|
||||
|
||||
- `apps/tests/loop_min_while.hako` が退行しない(Pattern2 baseline)
|
||||
|
||||
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),
|
||||
else_bb: local_block_map.get(&else_bb).copied().unwrap_or(else_bb),
|
||||
},
|
||||
MirInstruction::Phi {
|
||||
dst,
|
||||
inputs,
|
||||
type_hint: None,
|
||||
} => MirInstruction::Phi {
|
||||
dst,
|
||||
// Phase 196: Fix Select expansion PHI - ValueIds are ALREADY remapped by remap_instruction()
|
||||
// We only need to remap block IDs here (not ValueIds!)
|
||||
inputs: inputs
|
||||
.iter()
|
||||
.map(|(bb, val)| {
|
||||
let remapped_bb = local_block_map.get(bb).copied().unwrap_or(*bb);
|
||||
// Phase 196 FIX: Don't double-remap values!
|
||||
// remapper.remap_instruction() already remapped *val
|
||||
(remapped_bb, *val)
|
||||
})
|
||||
.collect(),
|
||||
type_hint: None,
|
||||
},
|
||||
MirInstruction::Phi { dst, inputs, type_hint } => {
|
||||
use super::phi_block_remapper::remap_phi_instruction;
|
||||
remap_phi_instruction(dst, &inputs, type_hint, &local_block_map)
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ mod instruction_rewriter;
|
||||
mod loop_header_phi_builder;
|
||||
mod loop_header_phi_info;
|
||||
mod merge_result;
|
||||
mod phi_block_remapper; // Phase 94: Phi block-id remap box
|
||||
mod tail_call_classifier;
|
||||
mod value_collector;
|
||||
|
||||
|
||||
@ -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 normal_delta: i64,
|
||||
pub escape_delta: i64,
|
||||
/// Index of the break-guard `if ... { break }` within the loop body.
|
||||
pub break_idx: usize,
|
||||
/// Index of the escape `if` within the loop body.
|
||||
pub escape_idx: usize,
|
||||
/// Phase 92 P0-3: The condition expression for conditional increment
|
||||
/// e.g., `ch == '\\'` for escape sequence handling
|
||||
pub escape_cond: Box<ASTNode>,
|
||||
@ -74,6 +78,8 @@ pub fn detect_escape_skip_pattern(body: &[ASTNode]) -> Option<EscapeSkipPatternI
|
||||
counter_name,
|
||||
normal_delta,
|
||||
escape_delta,
|
||||
break_idx,
|
||||
escape_idx,
|
||||
escape_cond, // Phase 92 P0-3: Condition for JoinIR Select
|
||||
body_stmts,
|
||||
})
|
||||
|
||||
@ -44,10 +44,16 @@
|
||||
//! Phase 91 P5b: Escape Pattern Recognizer
|
||||
//! - escape_pattern_recognizer.rs: P5b (escape sequence handling) pattern detection
|
||||
//! - 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 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 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 condition_env_builder;
|
||||
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 super::body_local_policy::{classify_for_pattern2, BodyLocalPolicyDecision};
|
||||
use crate::mir::join_ir::lowering::common::body_local_slot::ReadOnlyBodyLocalSlot;
|
||||
use crate::mir::join_ir::lowering::common::body_local_derived_emitter::BodyLocalDerivedRecipe;
|
||||
use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox;
|
||||
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
|
||||
@ -16,6 +17,7 @@ use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr;
|
||||
use crate::mir::loop_pattern_detection::error_messages;
|
||||
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
|
||||
use crate::mir::ValueId;
|
||||
use super::p5b_escape_derived_policy::{classify_p5b_escape_derived, P5bEscapeDerivedDecision};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
struct Pattern2DebugLog {
|
||||
@ -56,6 +58,8 @@ struct Pattern2Inputs {
|
||||
break_condition_node: ASTNode,
|
||||
/// Phase 93 P0: ConditionOnly recipe for derived slot recalculation
|
||||
condition_only_recipe: Option<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(
|
||||
@ -254,6 +258,7 @@ fn prepare_pattern2_inputs(
|
||||
read_only_body_local_slot: None,
|
||||
break_condition_node,
|
||||
condition_only_recipe: None, // Phase 93 P0: Will be set by apply_trim_and_normalize
|
||||
body_local_derived_recipe: None, // Phase 94: Will be set after normalization
|
||||
})
|
||||
}
|
||||
|
||||
@ -807,6 +812,42 @@ impl MirBuilder {
|
||||
apply_trim_and_normalize(self, condition, _body, &mut inputs, verbose)?;
|
||||
let analysis_body = normalized_body.as_deref().unwrap_or(_body);
|
||||
|
||||
// Phase 94: Detect P5b escape-derived (`ch` reassignment + escape counter).
|
||||
// Route is explicit; in strict mode, partial matches that require derived support must fail-fast.
|
||||
match classify_p5b_escape_derived(analysis_body, &inputs.loop_var_name) {
|
||||
P5bEscapeDerivedDecision::UseDerived(recipe) => {
|
||||
log.log(
|
||||
"phase94",
|
||||
format!(
|
||||
"Phase 94: Enabled BodyLocalDerived for '{}' (counter='{}', pre_delta={}, post_delta={})",
|
||||
recipe.name, recipe.loop_counter_name, recipe.pre_delta, recipe.post_delta
|
||||
),
|
||||
);
|
||||
inputs.body_local_derived_recipe = Some(recipe);
|
||||
}
|
||||
P5bEscapeDerivedDecision::Reject(reason) => {
|
||||
return Err(format!("[cf_loop/pattern2] {}", reason));
|
||||
}
|
||||
P5bEscapeDerivedDecision::None => {
|
||||
// Strict-only guard: if we see a body-local reassignment to `ch`, don't silently miscompile.
|
||||
let has_ch_reassign = analysis_body.iter().any(|n| match n {
|
||||
ASTNode::Assignment { target, .. } => matches!(
|
||||
target.as_ref(),
|
||||
ASTNode::Variable { name, .. } if name == "ch"
|
||||
),
|
||||
_ => false,
|
||||
});
|
||||
if crate::config::env::joinir_dev::strict_enabled() && has_ch_reassign {
|
||||
return Err(format!(
|
||||
"[cf_loop/pattern2] {}",
|
||||
crate::mir::join_ir::lowering::error_tags::freeze(
|
||||
"[phase94/body_local_derived/contract/unhandled_reassign] Body-local reassignment to 'ch' detected but not supported by Phase 94 recipe"
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::mir::join_ir::lowering::loop_update_analyzer::LoopUpdateAnalyzer;
|
||||
let carrier_updates = LoopUpdateAnalyzer::analyze_carrier_updates(
|
||||
analysis_body,
|
||||
@ -894,6 +935,7 @@ impl MirBuilder {
|
||||
join_value_space,
|
||||
skeleton,
|
||||
condition_only_recipe: inputs.condition_only_recipe.as_ref(), // Phase 93 P0
|
||||
body_local_derived_recipe: inputs.body_local_derived_recipe.as_ref(), // Phase 94
|
||||
};
|
||||
|
||||
let (join_module, fragment_meta) = match lower_loop_with_break_minimal(lowering_inputs) {
|
||||
|
||||
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 dual_value_rewriter; // Phase 246-EX/247-EX: name-based dual-value rewrites
|
||||
pub mod condition_only_emitter; // Phase 93 P0: ConditionOnly (Derived Slot) recalculation
|
||||
pub mod body_local_derived_emitter; // Phase 94: Derived body-local (P5b escape "ch" reassignment)
|
||||
|
||||
use crate::mir::loop_form::LoopForm;
|
||||
use crate::mir::query::{MirQuery, MirQueryBox};
|
||||
|
||||
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
|
||||
use crate::mir::join_ir::lowering::common::conditional_step_emitter::emit_conditional_step_update;
|
||||
use crate::mir::join_ir::lowering::common::body_local_derived_emitter::BodyLocalDerivedRecipe;
|
||||
use crate::mir::join_ir::lowering::condition_to_joinir::ConditionEnv;
|
||||
use crate::mir::loop_canonicalizer::UpdateKind;
|
||||
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||
@ -74,7 +75,7 @@ use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
|
||||
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
|
||||
use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr;
|
||||
use crate::mir::join_ir::lowering::step_schedule::{
|
||||
build_pattern2_schedule, Pattern2ScheduleContext, Pattern2StepKind,
|
||||
build_pattern2_schedule_from_decision, decide_pattern2_schedule, Pattern2StepKind,
|
||||
};
|
||||
use crate::mir::join_ir::lowering::update_env::UpdateEnv;
|
||||
use crate::mir::loop_canonicalizer::LoopSkeleton;
|
||||
@ -107,6 +108,8 @@ pub(crate) struct LoopWithBreakLoweringInputs<'a> {
|
||||
pub skeleton: Option<&'a LoopSkeleton>,
|
||||
/// Phase 93 P0: ConditionOnly recipe for derived slot recalculation
|
||||
pub condition_only_recipe: Option<&'a crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyRecipe>,
|
||||
/// Phase 94: BodyLocalDerived recipe (P5b escape `ch` reassignment + conditional counter).
|
||||
pub body_local_derived_recipe: Option<&'a BodyLocalDerivedRecipe>,
|
||||
}
|
||||
|
||||
/// Lower Pattern 2 (Loop with Conditional Break) to JoinIR
|
||||
@ -184,6 +187,7 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
join_value_space,
|
||||
skeleton,
|
||||
condition_only_recipe,
|
||||
body_local_derived_recipe,
|
||||
} = inputs;
|
||||
|
||||
let mut body_local_env = body_local_env;
|
||||
@ -377,12 +381,13 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
// Decide evaluation order (header/body-init/break/updates/tail) up-front.
|
||||
// Phase 93 P0: Pass condition_only_recipe existence to schedule context.
|
||||
// When recipe exists, body-init must happen before break check.
|
||||
let schedule_ctx = Pattern2ScheduleContext::from_env(
|
||||
let schedule_decision = decide_pattern2_schedule(
|
||||
body_local_env.as_ref().map(|env| &**env),
|
||||
carrier_info,
|
||||
condition_only_recipe.is_some(),
|
||||
body_local_derived_recipe.is_some(),
|
||||
);
|
||||
let schedule = build_pattern2_schedule(&schedule_ctx);
|
||||
let schedule = build_pattern2_schedule_from_decision(&schedule_decision);
|
||||
|
||||
// Collect fragments per step; append them according to the schedule below.
|
||||
let mut header_block: Vec<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 carrier_update_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)
|
||||
@ -523,6 +529,28 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
cond: Some(break_cond_value), // Phase 170-B: Use lowered condition
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Phase 94: P5b escape derived body-local + conditional loop-var update
|
||||
// ------------------------------------------------------------------
|
||||
if let (Some(recipe), Some(ref mut body_env)) = (body_local_derived_recipe, body_local_env.as_mut())
|
||||
{
|
||||
use crate::mir::join_ir::lowering::common::body_local_derived_emitter::BodyLocalDerivedEmitter;
|
||||
let emission = BodyLocalDerivedEmitter::emit(
|
||||
recipe,
|
||||
&mut alloc_value,
|
||||
env,
|
||||
body_env,
|
||||
&mut carrier_update_block,
|
||||
)?;
|
||||
loop_var_next_override = Some(emission.loop_counter_next);
|
||||
dev_log.log_if_enabled(|| {
|
||||
format!(
|
||||
"[phase94/body_local_derived] enabled: name='{}', loop_counter='{}', loop_counter_next={:?}",
|
||||
recipe.name, recipe.loop_counter_name, emission.loop_counter_next
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Loop Body: Compute updated values for all carriers
|
||||
// ------------------------------------------------------------------
|
||||
@ -684,7 +712,10 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
|
||||
// Phase 176-3: Multi-carrier support - tail call includes all updated carriers
|
||||
// Call(loop_step, [i_next, carrier1_next, carrier2_next, ...]) // tail recursion
|
||||
// Note: We need to emit i_next = i + 1 first for the loop variable
|
||||
// Phase 94: i_next may be overridden (escape skip: +2 vs +1).
|
||||
let i_next = if let Some(i_next) = loop_var_next_override {
|
||||
i_next
|
||||
} else {
|
||||
let const_1 = alloc_value();
|
||||
tail_block.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: const_1,
|
||||
@ -698,6 +729,8 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
lhs: i_param,
|
||||
rhs: const_1,
|
||||
}));
|
||||
i_next
|
||||
};
|
||||
|
||||
let mut tail_call_args = vec![i_next];
|
||||
tail_call_args.extend(updated_carrier_values.iter().copied());
|
||||
|
||||
@ -147,6 +147,7 @@ fn test_pattern2_header_condition_via_exprlowerer() {
|
||||
join_value_space: &mut join_value_space,
|
||||
skeleton: None, // Phase 92 P0-3: skeleton=None for backward compatibility
|
||||
condition_only_recipe: None, // Phase 93 P0: None for normal loops
|
||||
body_local_derived_recipe: None, // Phase 94: None for normal loops
|
||||
});
|
||||
|
||||
assert!(result.is_ok(), "ExprLowerer header path should succeed");
|
||||
|
||||
@ -39,6 +39,60 @@ use crate::mir::ValueId;
|
||||
use crate::runtime::core_box_ids::CoreMethodId;
|
||||
|
||||
use super::condition_env::ConditionEnv;
|
||||
use super::loop_body_local_env::LoopBodyLocalEnv;
|
||||
use super::debug_output_box::DebugOutputBox;
|
||||
|
||||
/// Box: resolves method call arguments with cascading lookup (body-local → condition).
|
||||
struct CascadingArgResolver<'a> {
|
||||
cond_env: &'a ConditionEnv,
|
||||
body_local_env: &'a LoopBodyLocalEnv,
|
||||
debug: DebugOutputBox,
|
||||
}
|
||||
|
||||
impl<'a> CascadingArgResolver<'a> {
|
||||
fn new(cond_env: &'a ConditionEnv, body_local_env: &'a LoopBodyLocalEnv) -> Self {
|
||||
Self {
|
||||
cond_env,
|
||||
body_local_env,
|
||||
debug: DebugOutputBox::new_dev("method_call_lowerer"),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve(
|
||||
&self,
|
||||
expr: &ASTNode,
|
||||
alloc_value: &mut dyn FnMut() -> ValueId,
|
||||
instructions: &mut Vec<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
|
||||
///
|
||||
@ -167,18 +221,15 @@ impl MethodCallLowerer {
|
||||
/// - Arguments can reference previously defined body-local variables
|
||||
/// - Checks `body_local_env` first, then `cond_env` for variable resolution
|
||||
/// - Example: `local digit_pos = digits.indexOf(ch)` where `ch` is body-local
|
||||
pub fn lower_for_init<F>(
|
||||
pub fn lower_for_init(
|
||||
recv_val: ValueId,
|
||||
method_name: &str,
|
||||
args: &[ASTNode],
|
||||
alloc_value: &mut F,
|
||||
alloc_value: &mut dyn FnMut() -> ValueId,
|
||||
cond_env: &ConditionEnv,
|
||||
body_local_env: &super::loop_body_local_env::LoopBodyLocalEnv,
|
||||
body_local_env: &LoopBodyLocalEnv,
|
||||
instructions: &mut Vec<JoinInst>,
|
||||
) -> Result<ValueId, String>
|
||||
where
|
||||
F: FnMut() -> ValueId,
|
||||
{
|
||||
) -> Result<ValueId, String> {
|
||||
// Resolve method name to CoreMethodId
|
||||
let method_id = CoreMethodId::iter()
|
||||
.find(|m| m.name() == method_name)
|
||||
@ -211,15 +262,10 @@ impl MethodCallLowerer {
|
||||
|
||||
// Phase 226: Lower arguments with cascading LoopBodyLocal support
|
||||
// Check body_local_env first, then cond_env
|
||||
let resolver = CascadingArgResolver::new(cond_env, body_local_env);
|
||||
let mut lowered_args = Vec::new();
|
||||
for arg_ast in args {
|
||||
let arg_val = Self::lower_arg_with_cascading(
|
||||
arg_ast,
|
||||
alloc_value,
|
||||
cond_env,
|
||||
body_local_env,
|
||||
instructions,
|
||||
)?;
|
||||
let arg_val = resolver.resolve(arg_ast, alloc_value, instructions)?;
|
||||
lowered_args.push(arg_val);
|
||||
}
|
||||
|
||||
@ -241,81 +287,12 @@ impl MethodCallLowerer {
|
||||
Ok(dst)
|
||||
}
|
||||
|
||||
/// Phase 226: Lower an argument expression with cascading LoopBodyLocal support
|
||||
///
|
||||
/// This function extends `condition_lowerer::lower_value_expression` to support
|
||||
/// cascading LoopBodyLocal variables. It checks both environments:
|
||||
/// 1. LoopBodyLocalEnv first (for previously defined body-local variables)
|
||||
/// 2. ConditionEnv as fallback (for loop condition variables)
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```nyash
|
||||
/// local ch = s.substring(p, p+1) // ch stored in body_local_env
|
||||
/// local digit_pos = digits.indexOf(ch) // ch resolved from body_local_env
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `expr` - Argument AST node
|
||||
/// * `alloc_value` - ValueId allocator
|
||||
/// * `cond_env` - Condition environment (fallback)
|
||||
/// * `body_local_env` - LoopBodyLocal environment (priority)
|
||||
/// * `instructions` - Instruction buffer
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(ValueId)` - Lowered argument value
|
||||
/// * `Err(String)` - If variable not found in either environment
|
||||
fn lower_arg_with_cascading<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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::Span;
|
||||
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
|
||||
use crate::mir::join_ir::JoinInst;
|
||||
|
||||
@ -578,7 +555,51 @@ mod tests {
|
||||
}
|
||||
_ => panic!("Expected BoxCall instruction"),
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cascading_resolves_body_local_first() {
|
||||
// receiver: s, method: indexOf(ch) where ch is body-local
|
||||
let mut body_env = LoopBodyLocalEnv::new();
|
||||
body_env.insert("ch".to_string(), ValueId(2));
|
||||
|
||||
let mut cond_env = ConditionEnv::new();
|
||||
cond_env.insert("s".to_string(), ValueId(1));
|
||||
|
||||
let recv_val = ValueId(1);
|
||||
let mut next = 100u32;
|
||||
let mut alloc_value = || {
|
||||
let id = ValueId(next);
|
||||
next += 1;
|
||||
id
|
||||
};
|
||||
let mut instructions = Vec::new();
|
||||
|
||||
let result = MethodCallLowerer::lower_for_init(
|
||||
recv_val,
|
||||
"indexOf",
|
||||
&[ASTNode::Variable {
|
||||
name: "ch".to_string(),
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
&mut alloc_value,
|
||||
&cond_env,
|
||||
&body_env,
|
||||
&mut instructions,
|
||||
)
|
||||
.expect("lower_for_init should succeed");
|
||||
|
||||
// Ensure BoxCall args include receiver + body-local resolved value (ValueId(2))
|
||||
let boxcall = instructions
|
||||
.iter()
|
||||
.find_map(|inst| match inst {
|
||||
JoinInst::Compute(MirLikeInst::BoxCall { args, .. }) => Some(args.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.expect("BoxCall not emitted");
|
||||
assert_eq!(boxcall, vec![ValueId(1), ValueId(2)]);
|
||||
assert!(result.0 >= 100);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lower_substring_with_args() {
|
||||
|
||||
@ -71,9 +71,7 @@ impl Pattern2StepSchedule {
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedule decision result with reasoning
|
||||
///
|
||||
/// Phase 93 Refactoring: Unified schedule decision with explicit reasons
|
||||
/// Schedule decision result with reasoning (SSOT)
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ScheduleDecision {
|
||||
/// Whether body-init should come before break check
|
||||
@ -90,6 +88,7 @@ pub(crate) struct ScheduleDebugContext {
|
||||
pub has_body_local_init: bool,
|
||||
pub has_loop_local_carrier: bool,
|
||||
pub has_condition_only_recipe: bool,
|
||||
pub has_body_local_derived_recipe: bool,
|
||||
}
|
||||
|
||||
/// Decide Pattern2 schedule based on loop characteristics
|
||||
@ -116,6 +115,7 @@ pub(crate) fn decide_pattern2_schedule(
|
||||
body_local_env: Option<&LoopBodyLocalEnv>,
|
||||
carrier_info: &CarrierInfo,
|
||||
has_condition_only_recipe: bool,
|
||||
has_body_local_derived_recipe: bool,
|
||||
) -> ScheduleDecision {
|
||||
let has_body_local_init = body_local_env.map(|env| !env.is_empty()).unwrap_or(false);
|
||||
let has_loop_local_carrier = carrier_info
|
||||
@ -123,10 +123,15 @@ pub(crate) fn decide_pattern2_schedule(
|
||||
.iter()
|
||||
.any(|c| matches!(c.init, CarrierInit::LoopLocalZero));
|
||||
|
||||
let body_init_first = has_condition_only_recipe || has_body_local_init || has_loop_local_carrier;
|
||||
let body_init_first = has_condition_only_recipe
|
||||
|| has_body_local_derived_recipe
|
||||
|| has_body_local_init
|
||||
|| has_loop_local_carrier;
|
||||
|
||||
let reason = if has_condition_only_recipe {
|
||||
"ConditionOnly requires body-init before break"
|
||||
} else if has_body_local_derived_recipe {
|
||||
"BodyLocalDerived requires body-init before break"
|
||||
} else if has_body_local_init {
|
||||
"body-local variables require init before break"
|
||||
} else if has_loop_local_carrier {
|
||||
@ -142,51 +147,13 @@ pub(crate) fn decide_pattern2_schedule(
|
||||
has_body_local_init,
|
||||
has_loop_local_carrier,
|
||||
has_condition_only_recipe,
|
||||
has_body_local_derived_recipe,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal context for deciding the step order.
|
||||
///
|
||||
/// Phase 93 Refactoring: Kept for backward compatibility, delegates to `decide_pattern2_schedule`
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct Pattern2ScheduleContext {
|
||||
pub(crate) has_body_local_init: bool,
|
||||
pub(crate) has_loop_local_carrier: bool,
|
||||
}
|
||||
|
||||
impl Pattern2ScheduleContext {
|
||||
/// Build schedule context from environment.
|
||||
///
|
||||
/// # Phase 93 Refactoring: Backward compatibility wrapper
|
||||
///
|
||||
/// Delegates to `decide_pattern2_schedule()` for actual decision making.
|
||||
/// Note: When `has_condition_only_recipe` is true, we set `has_body_local_init` to true
|
||||
/// to ensure the correct schedule is generated.
|
||||
pub(crate) fn from_env(
|
||||
body_local_env: Option<&LoopBodyLocalEnv>,
|
||||
carrier_info: &CarrierInfo,
|
||||
has_condition_only_recipe: bool,
|
||||
) -> Self {
|
||||
let decision = decide_pattern2_schedule(body_local_env, carrier_info, has_condition_only_recipe);
|
||||
Self {
|
||||
// Phase 93 Refactoring: Include condition_only_recipe in has_body_local_init
|
||||
// to maintain backward compatibility with requires_body_init_before_break()
|
||||
has_body_local_init: decision.debug_ctx.has_body_local_init
|
||||
|| decision.debug_ctx.has_condition_only_recipe,
|
||||
has_loop_local_carrier: decision.debug_ctx.has_loop_local_carrier,
|
||||
}
|
||||
}
|
||||
|
||||
fn requires_body_init_before_break(&self) -> bool {
|
||||
self.has_body_local_init || self.has_loop_local_carrier
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a schedule for Pattern 2 lowering.
|
||||
///
|
||||
/// Phase 93 Refactoring: Now accepts `ScheduleDecision` for explicit reasoning
|
||||
///
|
||||
/// - Default P2: header → break → body-init → updates → tail
|
||||
/// - Body-local break dependency (DigitPos/_atoi style):
|
||||
/// header → body-init → break → updates → tail
|
||||
@ -221,42 +188,6 @@ pub(crate) fn build_pattern2_schedule_from_decision(
|
||||
schedule
|
||||
}
|
||||
|
||||
/// Build a schedule for Pattern 2 lowering (legacy wrapper).
|
||||
///
|
||||
/// Phase 93 Refactoring: Kept for backward compatibility
|
||||
///
|
||||
/// - Default P2: header → break → body-init → updates → tail
|
||||
/// - Body-local break dependency (DigitPos/_atoi style):
|
||||
/// header → body-init → break → updates → tail
|
||||
pub(crate) fn build_pattern2_schedule(ctx: &Pattern2ScheduleContext) -> Pattern2StepSchedule {
|
||||
let schedule = if ctx.requires_body_init_before_break() {
|
||||
Pattern2StepSchedule {
|
||||
steps: vec![
|
||||
Pattern2StepKind::HeaderCond,
|
||||
Pattern2StepKind::BodyInit,
|
||||
Pattern2StepKind::BreakCheck,
|
||||
Pattern2StepKind::Updates,
|
||||
Pattern2StepKind::Tail,
|
||||
],
|
||||
reason: "body-local break dependency",
|
||||
}
|
||||
} else {
|
||||
Pattern2StepSchedule {
|
||||
steps: vec![
|
||||
Pattern2StepKind::HeaderCond,
|
||||
Pattern2StepKind::BreakCheck,
|
||||
Pattern2StepKind::BodyInit,
|
||||
Pattern2StepKind::Updates,
|
||||
Pattern2StepKind::Tail,
|
||||
],
|
||||
reason: "default",
|
||||
}
|
||||
};
|
||||
|
||||
log_schedule(ctx, &schedule);
|
||||
schedule
|
||||
}
|
||||
|
||||
fn log_schedule_from_decision(decision: &ScheduleDecision, schedule: &Pattern2StepSchedule) {
|
||||
if !(env::joinir_dev_enabled() || joinir_test_debug_enabled()) {
|
||||
return;
|
||||
@ -270,32 +201,13 @@ fn log_schedule_from_decision(decision: &ScheduleDecision, schedule: &Pattern2St
|
||||
.join(" -> ");
|
||||
|
||||
eprintln!(
|
||||
"[phase93/schedule] steps={} reason={} ctx={{body_local_init={}, loop_local_carrier={}, condition_only={}}}",
|
||||
"[phase93/schedule] steps={} reason={} ctx={{body_local_init={}, loop_local_carrier={}, condition_only={}, body_local_derived={}}}",
|
||||
steps_desc,
|
||||
schedule.reason(),
|
||||
decision.debug_ctx.has_body_local_init,
|
||||
decision.debug_ctx.has_loop_local_carrier,
|
||||
decision.debug_ctx.has_condition_only_recipe
|
||||
);
|
||||
}
|
||||
|
||||
fn log_schedule(ctx: &Pattern2ScheduleContext, schedule: &Pattern2StepSchedule) {
|
||||
if !(env::joinir_dev_enabled() || joinir_test_debug_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let steps_desc = schedule
|
||||
.steps()
|
||||
.iter()
|
||||
.map(Pattern2StepKind::as_str)
|
||||
.collect::<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
|
||||
decision.debug_ctx.has_condition_only_recipe,
|
||||
decision.debug_ctx.has_body_local_derived_recipe
|
||||
);
|
||||
}
|
||||
|
||||
@ -351,8 +263,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn default_schedule_break_before_body_init() {
|
||||
let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![]), false);
|
||||
let schedule = build_pattern2_schedule(&ctx);
|
||||
let decision = decide_pattern2_schedule(None, &carrier_info(vec![]), false, false);
|
||||
let schedule = build_pattern2_schedule_from_decision(&decision);
|
||||
assert_eq!(
|
||||
schedule.steps(),
|
||||
&[
|
||||
@ -363,7 +275,7 @@ mod tests {
|
||||
Pattern2StepKind::Tail
|
||||
]
|
||||
);
|
||||
assert_eq!(schedule.reason(), "default");
|
||||
assert_eq!(schedule.reason(), "default schedule");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -371,9 +283,13 @@ mod tests {
|
||||
let mut body_env = LoopBodyLocalEnv::new();
|
||||
body_env.insert("tmp".to_string(), ValueId(5));
|
||||
|
||||
let ctx =
|
||||
Pattern2ScheduleContext::from_env(Some(&body_env), &carrier_info(vec![carrier(false)]), false);
|
||||
let schedule = build_pattern2_schedule(&ctx);
|
||||
let decision = decide_pattern2_schedule(
|
||||
Some(&body_env),
|
||||
&carrier_info(vec![carrier(false)]),
|
||||
false,
|
||||
false,
|
||||
);
|
||||
let schedule = build_pattern2_schedule_from_decision(&decision);
|
||||
assert_eq!(
|
||||
schedule.steps(),
|
||||
&[
|
||||
@ -384,13 +300,18 @@ mod tests {
|
||||
Pattern2StepKind::Tail
|
||||
]
|
||||
);
|
||||
assert_eq!(schedule.reason(), "body-local break dependency");
|
||||
assert_eq!(schedule.reason(), "body-local variables require init before break");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loop_local_carrier_triggers_body_first() {
|
||||
let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![carrier(true)]), false);
|
||||
let schedule = build_pattern2_schedule(&ctx);
|
||||
let decision = decide_pattern2_schedule(
|
||||
None,
|
||||
&carrier_info(vec![carrier(true)]),
|
||||
false,
|
||||
false,
|
||||
);
|
||||
let schedule = build_pattern2_schedule_from_decision(&decision);
|
||||
assert_eq!(
|
||||
schedule.steps(),
|
||||
&[
|
||||
@ -401,15 +322,15 @@ mod tests {
|
||||
Pattern2StepKind::Tail
|
||||
]
|
||||
);
|
||||
assert_eq!(schedule.reason(), "body-local break dependency");
|
||||
assert_eq!(schedule.reason(), "loop-local carrier requires init before break");
|
||||
}
|
||||
|
||||
/// Phase 93 P0: ConditionOnly recipe triggers body-init before break
|
||||
#[test]
|
||||
fn condition_only_recipe_triggers_body_first() {
|
||||
// Empty body_local_env but has condition_only_recipe
|
||||
let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![]), true);
|
||||
let schedule = build_pattern2_schedule(&ctx);
|
||||
let decision = decide_pattern2_schedule(None, &carrier_info(vec![]), true, false);
|
||||
let schedule = build_pattern2_schedule_from_decision(&decision);
|
||||
assert_eq!(
|
||||
schedule.steps(),
|
||||
&[
|
||||
@ -420,8 +341,7 @@ mod tests {
|
||||
Pattern2StepKind::Tail
|
||||
]
|
||||
);
|
||||
// Phase 93 Refactoring: Reason is now preserved from backward compat wrapper
|
||||
assert_eq!(schedule.reason(), "body-local break dependency");
|
||||
assert_eq!(schedule.reason(), "ConditionOnly requires body-init before break");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -136,6 +136,7 @@ pub fn build_pattern2_minimal_structured() -> JoinModule {
|
||||
join_value_space: &mut join_value_space,
|
||||
skeleton: None, // Phase 92 P0-3: skeleton=None for backward compatibility
|
||||
condition_only_recipe: None, // Phase 93 P0: None for normal loops
|
||||
body_local_derived_recipe: None, // Phase 94: None for fixture
|
||||
},
|
||||
)
|
||||
.expect("pattern2 minimal lowering should succeed");
|
||||
|
||||
@ -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