phase29ab(p3): fix PromoteDecision contract and add negative smokes

This commit is contained in:
2025-12-28 10:57:55 +09:00
parent 280a5a8187
commit 84e1cd7c7b
8 changed files with 184 additions and 30 deletions

View File

@ -0,0 +1,29 @@
// Phase 29ab P3: Pattern2 LoopBodyLocal seg Freeze (read-only violation)
//
// Goal:
// - break condition uses LoopBodyLocal seg
// - seg is reassigned in the loop body (read-only contract violation)
// - Pattern2 promotion must Freeze (fail-fast)
//
// Expected: JoinIR freeze error (non-zero exit)
static box Main {
main() {
local s = "ab "
local i = 0
loop(i < s.length()) {
local seg = s.substring(i, i + 1)
if seg == " " || seg == "\t" {
break
}
seg = "x"
i = i + 1
}
print(i)
return i
}
}

View File

@ -0,0 +1,27 @@
// Phase 29ab P3: Pattern2 LoopBodyLocal seg NotApplicable (shape mismatch)
//
// Goal:
// - break condition uses no LoopBodyLocal vars
// - Pattern2 promotion should be NotApplicable and continue safely
//
// Expected: prints "2" and returns 2
static box Main {
main() {
local s = "ab "
local i = 0
loop(i < s.length()) {
local seg = s.substring(i, i + 1)
if i >= 2 {
break
}
i = i + 1
}
print(i)
return i
}
}

View File

@ -0,0 +1,31 @@
# Pattern2 Promotion API Contract (SSOT)
This directory is the single entry point for Pattern2 LoopBodyLocal promotion.
All callers must go through `try_promote()` and must honor the decision contract
below.
## PromoteDecision Contract
- `Promoted`
- All contract checks satisfied.
- Pattern2 continues in the JoinIR path.
- `NotApplicable`
- Promotion not applicable (no LoopBodyLocal in conditions).
- The caller continues Pattern2 with unchanged inputs.
- Example causes:
- No LoopBodyLocal variables in the break condition.
- `Freeze`
- Contract violation or unimplemented behavior.
- Fail-fast with a clear error tag, no fallback.
- Example causes:
- Read-only contract broken (assignment detected).
- Missing required metadata (loop scope/break guard).
## Reject Mapping Rules (PolicyDecision::Reject -> PromoteDecision)
The mapping lives in `promote_runner.rs` and must remain stable:
- Any `PolicyDecision::Reject` becomes `Freeze`
- Promotion not applicable (no LoopBodyLocal vars) uses `NotApplicable`

View File

@ -16,11 +16,11 @@ pub(crate) enum PromoteDecision {
/// Promotion succeeded - Pattern2 can proceed
Promoted(PromoteStepResult),
/// Pattern2 not applicable (e.g., reassigned LoopBodyLocal, no promotable pattern)
/// → Router should try next path (legacy binding, etc.)
NotApplicable,
/// Promotion not applicable (e.g., no LoopBodyLocal in conditions)
/// → Continue Pattern2 with unchanged inputs
NotApplicable(PromoteStepResult),
/// Pattern2 should handle this but implementation is missing
/// Contract violation or unimplemented behavior
/// → Fail-Fast with error message
Freeze(String),
}

View File

@ -42,7 +42,9 @@ pub(in crate::mir::builder) fn try_promote(
.map(|v| v.name.clone())
.collect();
if cond_scope.has_loop_body_local() {
let has_body_locals_in_conditions = cond_scope.has_loop_body_local();
if has_body_locals_in_conditions {
// Policy-controlled: some families must not run promotion/slot heuristics here.
// Example: balanced depth-scan uses derived vars and doesn't have a break-guard node.
if matches!(
@ -106,25 +108,11 @@ pub(in crate::mir::builder) fn try_promote(
}
PolicyDecision::Reject(reason) => {
// Phase 263 P0.1: Reject を PromoteDecision で二分化(型安全)
if reason.contains("not_readonly")
|| reason.contains("No promotable pattern detected")
{
// 対象外: Pattern2 で処理できない形 → NotApplicable で後続経路へ
#[cfg(debug_assertions)]
{
eprintln!(
"[pattern2/api/promote] Pattern2 対象外LoopBodyLocal {:?}: {}. 後続経路へfallback.",
cond_body_local_vars, reason
);
}
return Ok(PromoteDecision::NotApplicable);
} else {
// 対象だが未対応freeze級: 実装バグ or 将来実装予定 → Freeze で Fail-Fast
return Ok(PromoteDecision::Freeze(format!(
"[pattern2/api/promote] Pattern2 未対応エラーLoopBodyLocal {:?}: {}",
cond_body_local_vars, reason
)));
}
// 対象だが未対応freeze級: 実装バグ or 将来実装予定 → Freeze で Fail-Fast
return Ok(PromoteDecision::Freeze(format!(
"[pattern2/api/promote] Pattern2 未対応エラーLoopBodyLocal {:?}: {}",
cond_body_local_vars, reason
)));
}
PolicyDecision::None => {}
}
@ -176,5 +164,9 @@ pub(in crate::mir::builder) fn try_promote(
}
}
Ok(PromoteDecision::Promoted(PromoteStepResult { inputs }))
if has_body_locals_in_conditions {
Ok(PromoteDecision::Promoted(PromoteStepResult { inputs }))
} else {
Ok(PromoteDecision::NotApplicable(PromoteStepResult { inputs }))
}
}

View File

@ -67,11 +67,12 @@ impl Pattern2LoweringOrchestrator {
PromoteDecision::Promoted(result) => {
result.inputs
}
PromoteDecision::NotApplicable => {
// Pattern2 cannot handle this loop (e.g., reassigned LoopBodyLocal)
// Return Ok(None) to allow router to try next path (legacy binding)
super::super::trace::trace().debug("pattern2", "Pattern2 aborted (not applicable), allowing fallback");
return Ok(None);
PromoteDecision::NotApplicable(result) => {
super::super::trace::trace().debug(
"pattern2",
"Pattern2 promotion not applicable, continuing without promotion",
);
result.inputs
}
PromoteDecision::Freeze(reason) => {
// Pattern2 should handle this but implementation is missing → Fail-Fast

View File

@ -0,0 +1,40 @@
#!/bin/bash
# Phase 29ab P3: Pattern2 LoopBodyLocal seg Freeze (VM backend)
# Tests: read-only violation must fail-fast with joinir freeze tag
source "$(dirname "$0")/../../../lib/test_runner.sh"
export SMOKES_USE_PYVM=0
require_env || exit 2
INPUT="$NYASH_ROOT/apps/tests/phase29ab_pattern2_seg_freeze_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_seg_freeze_min_vm: hakorune timed out (>${RUN_TIMEOUT_SECS}s)"
exit 1
fi
if [ "$EXIT_CODE" -eq 0 ]; then
echo "[FAIL] Expected JoinIR freeze error, got exit 0"
echo "[INFO] Output:"
echo "$OUTPUT" | tail -n 40 || true
test_fail "phase29ab_pattern2_seg_freeze_min_vm: Unexpected success"
exit 1
fi
if echo "$OUTPUT" | grep -q "\[joinir/freeze\]"; then
test_pass "phase29ab_pattern2_seg_freeze_min_vm: joinir freeze detected"
exit 0
else
echo "[FAIL] Expected joinir freeze tag in output"
echo "[INFO] Exit code: $EXIT_CODE"
echo "[INFO] Output:"
echo "$OUTPUT" | tail -n 60 || true
test_fail "phase29ab_pattern2_seg_freeze_min_vm: Missing joinir freeze tag"
exit 1
fi

View File

@ -0,0 +1,34 @@
#!/bin/bash
# Phase 29ab P3: Pattern2 promotion NotApplicable (VM backend)
# Tests: no LoopBodyLocal in condition -> continue, output should be 2
source "$(dirname "$0")/../../../lib/test_runner.sh"
export SMOKES_USE_PYVM=0
require_env || exit 2
INPUT="$NYASH_ROOT/apps/tests/phase29ab_pattern2_seg_notapplicable_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_seg_notapplicable_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_seg_notapplicable_min_vm: promotion not applicable (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_seg_notapplicable_min_vm: Unexpected output"
exit 1
fi