diff --git a/apps/tests/phase29ab_pattern2_loopbodylocal_min.hako b/apps/tests/phase29ab_pattern2_loopbodylocal_min.hako new file mode 100644 index 00000000..3379af23 --- /dev/null +++ b/apps/tests/phase29ab_pattern2_loopbodylocal_min.hako @@ -0,0 +1,30 @@ +// Phase 29ab P1: Pattern2 LoopBodyLocal promotion minimal case +// +// Goal: +// - Break condition uses LoopBodyLocal (digit_pos) that must be promoted +// - JoinIR -> merge path should handle A-4 DigitPos promotion +// +// Expected (before fix): FAIL/out-of-scope +// Expected (after fix): prints "2" and returns 2 + +static box Main { + main() { + local s = "12a" + local digits = "0123456789" + local p = 0 + + loop(p < s.length()) { + local ch = s.substring(p, p + 1) + local digit_pos = digits.indexOf(ch) + + if digit_pos < 0 { + break + } + + p = p + 1 + } + + print(p) + return p + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/common/carrier_binding_policy.rs b/src/mir/builder/control_flow/joinir/patterns/common/carrier_binding_policy.rs new file mode 100644 index 00000000..e45dbd6b --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/common/carrier_binding_policy.rs @@ -0,0 +1,26 @@ +//! Carrier binding policy for Pattern2 inputs +//! +//! Responsibility: +//! - Decide whether a carrier should bind to a host ValueId +//! - Keep ConditionOnly / loop-local carriers out of ConditionBindings + +use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole, CarrierVar}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CarrierBindingPolicy { + BindFromHost, + SkipBinding, +} + +pub(crate) fn decide_carrier_binding_policy(carrier: &CarrierVar) -> CarrierBindingPolicy { + // ConditionOnly carriers should never be sourced from host values. + debug_assert!( + !(carrier.role == CarrierRole::ConditionOnly && matches!(carrier.init, CarrierInit::FromHost)), + "ConditionOnly carriers must not use FromHost init" + ); + + match carrier.init { + CarrierInit::FromHost => CarrierBindingPolicy::BindFromHost, + CarrierInit::BoolConst(_) | CarrierInit::LoopLocalZero => CarrierBindingPolicy::SkipBinding, + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/common/mod.rs b/src/mir/builder/control_flow/joinir/patterns/common/mod.rs index b3ddb305..a0c403bb 100644 --- a/src/mir/builder/control_flow/joinir/patterns/common/mod.rs +++ b/src/mir/builder/control_flow/joinir/patterns/common/mod.rs @@ -4,7 +4,9 @@ //! lowering implementations, eliminating code duplication and ensuring consistency. mod ast_helpers; +mod carrier_binding_policy; mod joinir_helpers; // Phase 256.8.5: JoinModule helpers pub(crate) use ast_helpers::var; +pub(crate) use carrier_binding_policy::{decide_carrier_binding_policy, CarrierBindingPolicy}; pub(crate) use joinir_helpers::get_entry_function; // Phase 256.8.5 diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2/README.md b/src/mir/builder/control_flow/joinir/patterns/pattern2/README.md new file mode 100644 index 00000000..49c6a056 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2/README.md @@ -0,0 +1,29 @@ +# Pattern2 (Loop with Break) - JoinIR + +## Scope / Criteria +- `loop(...) { ... break ... }` (break present, no continue/return) +- break condition is normalized to "break when is true" +- loop variable comes from header condition or loop(true) counter extraction + +## LoopBodyLocal promotion +- SSOT entry: `pattern2::api::try_promote` +- Supported: A-3 Trim / A-4 DigitPos (promote LoopBodyLocal to carrier) +- ConditionOnly carriers are recalculated per iteration (no host binding) + +## Carrier binding rules (Pattern2) +- `CarrierInit::FromHost` -> host binding required +- `CarrierInit::BoolConst(_)` / `CarrierInit::LoopLocalZero` -> host binding is skipped +- ConditionOnly carriers must not use `FromHost` + +## Out of scope +- multiple breaks / continue / return in the loop body +- reassigned LoopBodyLocal or ReadOnlySlot contract violations +- break conditions with unsupported AST shapes + +## Fail-Fast policy +- `PromoteDecision::Freeze` -> Err (missing implementation or contract violation) +- JoinIR lowering/merge contract violations -> Err + +## `Ok(None)` meaning +- not Pattern2 (extractor returns None) +- promotion NotApplicable (router fallback) diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/carrier_updates_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/carrier_updates_step_box.rs index 1b4388ec..6bfcee3d 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/carrier_updates_step_box.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/carrier_updates_step_box.rs @@ -10,6 +10,7 @@ use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierInit, Carr use crate::mir::join_ir::lowering::condition_env::ConditionBinding; use crate::mir::join_ir::lowering::loop_update_analyzer::{LoopUpdateAnalyzer, UpdateExpr}; +use super::super::common::{decide_carrier_binding_policy, CarrierBindingPolicy}; use super::super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs}; use std::collections::BTreeMap; @@ -51,20 +52,23 @@ impl CarrierUpdatesStepBox { .unwrap_or_else(|| inputs.join_value_space.alloc_param()); inputs.env.insert(carrier.name.clone(), join_value); - if carrier.init != CarrierInit::LoopLocalZero { - inputs.condition_bindings.push(ConditionBinding { - name: carrier.name.clone(), - host_value: carrier.host_id, - join_value, - }); - } else { - Pattern2DebugLog::new(verbose).log( - "updates", - format!( - "Phase 247-EX: Skipping host binding for loop-local carrier '{}' (init=LoopLocalZero)", - carrier.name - ), - ); + match decide_carrier_binding_policy(carrier) { + CarrierBindingPolicy::BindFromHost => { + inputs.condition_bindings.push(ConditionBinding { + name: carrier.name.clone(), + host_value: carrier.host_id, + join_value, + }); + } + CarrierBindingPolicy::SkipBinding => { + Pattern2DebugLog::new(verbose).log( + "updates", + format!( + "Phase 29ab: Skipping host binding for carrier '{}' (init={:?}, role={:?})", + carrier.name, carrier.init, carrier.role + ), + ); + } } } } diff --git a/tools/smokes/v2/profiles/integration/apps/phase29ab_pattern2_loopbodylocal_min_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase29ab_pattern2_loopbodylocal_min_vm.sh new file mode 100644 index 00000000..52a25939 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase29ab_pattern2_loopbodylocal_min_vm.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Phase 29ab P1: Pattern2 LoopBodyLocal promotion minimal (VM backend) +# Tests: LoopBodyLocal digit_pos in break condition promoted to carrier (DigitPos A-4) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +INPUT="$NYASH_ROOT/apps/tests/phase29ab_pattern2_loopbodylocal_min.hako" +RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10} + +set +e +OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env NYASH_DISABLE_PLUGINS=1 HAKO_JOINIR_STRICT=1 "$NYASH_BIN" "$INPUT" 2>&1) +EXIT_CODE=$? +set -e + +if [ "$EXIT_CODE" -eq 124 ]; then + test_fail "phase29ab_pattern2_loopbodylocal_min_vm: hakorune timed out (>${RUN_TIMEOUT_SECS}s)" + exit 1 +fi + +OUTPUT_CLEAN=$(echo "$OUTPUT" | filter_noise) + +if echo "$OUTPUT_CLEAN" | grep -q "^2$" || echo "$OUTPUT" | grep -q "^RC: 2$"; then + test_pass "phase29ab_pattern2_loopbodylocal_min_vm: Pattern2 LoopBodyLocal promotion succeeded (output: 2)" + exit 0 +else + echo "[FAIL] Unexpected output (expected: 2)" + echo "[INFO] Exit code: $EXIT_CODE" + echo "[INFO] Output (clean):" + echo "$OUTPUT_CLEAN" | tail -n 20 || true + test_fail "phase29ab_pattern2_loopbodylocal_min_vm: Unexpected output" + exit 1 +fi