phase29ab(p5): freeze pattern7 split-scan near-miss with fixture+smoke

This commit is contained in:
2025-12-28 14:32:19 +09:00
parent 7a790a27cb
commit bea2a8d9bb
6 changed files with 249 additions and 14 deletions

View File

@ -0,0 +1,37 @@
// Phase 29ab P5: Pattern7 first-fail minimal (contract violation)
// Expect: JoinIR freeze (else step is not i = i + 1)
static box StringUtils {
split_bad_step(s, separator) {
local result = new ArrayBox()
if separator.length() == 0 {
result.push(s)
return result
}
local start = 0
local i = 0
loop(i <= s.length() - separator.length()) {
if s.substring(i, i + separator.length()) == separator {
result.push(s.substring(start, i))
start = i + separator.length()
i = start
} else {
i = i + 2
}
}
if start <= s.length() {
result.push(s.substring(start, s.length()))
}
return result
}
}
static box Main {
main() {
local result = StringUtils.split_bad_step("a,b,c", ",")
return result.length()
}
}

View File

@ -1,6 +1,6 @@
# Self Current Task — Now (main)
## Current Focus: Phase 29ab P4Phase 263 realworld seg: Derived slot
## Current Focus: Phase 29ab P5Pattern7 SplitScan contract fail-fast
**2025-12-28: Phase 29ab P1 完了**
- 目的: Pattern2 の LoopBodyLocal promotion の最小ケースを fixture+integration smoke で固定
@ -27,6 +27,13 @@
- Smoke: `tools/smokes/v2/profiles/integration/apps/phase263_pattern2_seg_realworld_min_vm.sh`
- 追加/調整: `String.indexOf(search, fromIndex)` の2引数対応arity解決を含むを VM/JoinIR lowering 側で整備
**2025-12-28: Phase 29ab P5 完了**
- 目的: Pattern7 SplitScan の「形は近いが契約違反」ケースを extractor 段で freeze に固定SSOT化
- 実装: `src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs``freeze_with_hint("phase29ab/pattern7/contract", ...)`
- fixture: `apps/tests/phase29ab_pattern7_firstfail_min.hako`
- smoke: `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern7_firstfail_min_vm.sh`
- 検証: `./tools/smokes/v2/run.sh --profile integration --filter "phase29ab_pattern7_*"` PASS
**2025-12-28: Phase 29aa P5 完了**
- 目的: Return block が複数 predecessor のとき、incoming state が完全一致する場合のみ ReturnCleanup を成立させる
- 入口: `docs/development/current/main/phases/phase-29aa/README.md`

View File

@ -13,7 +13,8 @@ Related:
- P2: Pattern2 Trim seg minimal fixture+smoke fixed`phase29ab_pattern2_loopbodylocal_seg_min_vm`
- P3: PromoteDecision の “NotApplicable/Freeze” 境界を contract+smoke で固定JoinIR-only前提
- P4: Phase 263StageB 実ログ segを Derived slot 方針で通し、fixture+smoke で固定
- P5: Pattern6/7joinir/edgecfgfirst-fail を fixture で固定して、Frag/compose へ収束方針を決める
- P5: Pattern7 SplitScan first-fail契約違反を freeze で固定して SSOT 化
- P6: Pattern6 MatchScan first-fail を同様に fixture+smoke で固定freeze/NotApplicableの境界を決める
- **Phase 288✅ P0P3 + 288.1 complete: REPL mode**
- 入口: `docs/development/current/main/phases/phase-288/README.md`

View File

@ -346,6 +346,7 @@ MIR Terminator Instructions
- **NormalizeBox 直後**: terminator 語彙固定・edge-args 長さ一致・cond付きJump禁止など “意味SSOT” を確定
- **merge直前**: boundary/ABI/edge-args の矛盾を即死させ “配線SSOT” を確定
- **--verify**: PHI predecessor / CFG cache 整合 / edge-args の長さ一致を常設
- **Pattern6/7 extractor**: 形は近いが契約違反のケースは `Ok(None)` で流さず freeze例: SplitScan の `else i = i + 1` 破り)
## 直近の導入ステップ(最小で始める)

View File

@ -28,6 +28,32 @@
//! - P0 restriction: 1-char separator only
use crate::ast::ASTNode;
use crate::mir::join_ir::lowering::error_tags;
#[derive(Debug, Clone)]
struct SplitScanContractViolation {
msg: String,
hint: String,
}
#[derive(Debug, Clone)]
enum SplitScanExtractError {
NotApplicable(String),
Contract(SplitScanContractViolation),
}
impl SplitScanExtractError {
fn not_applicable(msg: &str) -> Self {
Self::NotApplicable(msg.to_string())
}
fn contract(msg: &str, hint: &str) -> Self {
Self::Contract(SplitScanContractViolation {
msg: msg.to_string(),
hint: hint.to_string(),
})
}
}
/// Phase 256 P0: Split/Scan pattern parts extractor
///
@ -74,7 +100,12 @@ pub(crate) fn extract_split_scan_plan(
};
Ok(Some(DomainPlan::SplitScan(plan)))
}
Err(_) => Ok(None), // Pattern doesn't match
Err(SplitScanExtractError::NotApplicable(_)) => Ok(None), // Pattern doesn't match
Err(SplitScanExtractError::Contract(err)) => Err(error_tags::freeze_with_hint(
"phase29ab/pattern7/contract",
&err.msg,
&err.hint,
)),
}
}
@ -88,16 +119,17 @@ fn extract_split_scan_parts(
condition: &ASTNode,
body: &[ASTNode],
post_loop_code: &[ASTNode],
) -> Result<SplitScanParts, String> {
) -> Result<SplitScanParts, SplitScanExtractError> {
// Step 1: Extract variables from loop condition
// Expected: i <= s.length() - separator.length()
let (i_var, s_var, sep_var) = extract_loop_condition_vars(condition)?;
let (i_var, s_var, sep_var) = extract_loop_condition_vars(condition)
.map_err(SplitScanExtractError::NotApplicable)?;
// Step 2: Find the if statement in loop body
let if_stmt = body
.iter()
.find(|stmt| matches!(stmt, ASTNode::If { .. }))
.ok_or("extract_split_scan_parts: No if statement found in loop body")?;
.ok_or_else(|| SplitScanExtractError::not_applicable("extract_split_scan_parts: No if statement found in loop body"))?;
let (match_if_cond_ast, then_body, else_body) = match if_stmt {
ASTNode::If {
@ -106,14 +138,14 @@ fn extract_split_scan_parts(
else_body,
..
} => (condition.as_ref().clone(), then_body, else_body),
_ => return Err("extract_split_scan_parts: Invalid if statement".to_string()),
_ => return Err(SplitScanExtractError::not_applicable("extract_split_scan_parts: Invalid if statement")),
};
// Step 3: Extract push operation from then branch
let then_push_ast = then_body
.iter()
.find(|stmt| matches!(stmt, ASTNode::MethodCall { method, .. } if method == "push"))
.ok_or("extract_split_scan_parts: No push() found in then branch")?
.ok_or_else(|| SplitScanExtractError::not_applicable("extract_split_scan_parts: No push() found in then branch"))?
.clone();
// Step 4: Extract start assignment (start = i + separator.length())
@ -124,7 +156,7 @@ fn extract_split_scan_parts(
matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "start")
})
})
.ok_or("extract_split_scan_parts: No 'start = ...' assignment in then branch")?
.ok_or_else(|| SplitScanExtractError::not_applicable("extract_split_scan_parts: No 'start = ...' assignment in then branch"))?
.clone();
// Step 5: Extract i assignment (i = start or i = variable)
@ -136,7 +168,7 @@ fn extract_split_scan_parts(
&& matches!(value.as_ref(), ASTNode::Variable { .. })
})
})
.ok_or("extract_split_scan_parts: No 'i = variable' assignment in then branch")?
.ok_or_else(|| SplitScanExtractError::not_applicable("extract_split_scan_parts: No 'i = variable' assignment in then branch"))?
.clone();
// Step 6: Extract else branch assignment (i = i + 1)
@ -148,12 +180,16 @@ fn extract_split_scan_parts(
matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "i")
})
})
.ok_or("extract_split_scan_parts: No 'i = ...' assignment in else branch")?
.ok_or_else(|| SplitScanExtractError::not_applicable("extract_split_scan_parts: No 'i = ...' assignment in else branch"))?
.clone()
} else {
return Err("extract_split_scan_parts: No else branch found".to_string());
return Err(SplitScanExtractError::not_applicable("extract_split_scan_parts: No else branch found"));
};
validate_start_update(&then_start_next_ast, &i_var, &sep_var)?;
validate_i_set_to_start(&then_i_next_ast, &i_var)?;
validate_i_increment_by_one(&else_i_next_ast, &i_var)?;
// Step 7: Extract post-loop push (result.push(...))
let post_push_ast = post_loop_code
.iter()
@ -161,7 +197,8 @@ fn extract_split_scan_parts(
.cloned();
// Step 8: Extract result variable from push statements
let result_var = extract_result_var(&then_push_ast)?;
let result_var = extract_result_var(&then_push_ast)
.map_err(SplitScanExtractError::NotApplicable)?;
Ok(SplitScanParts {
s_var,
@ -197,7 +234,9 @@ fn extract_loop_condition_vars(condition: &ASTNode) -> Result<(String, String, S
// Left should be: Variable { name: "i" }
let i_var = match left.as_ref() {
ASTNode::Variable { name, .. } => name.clone(),
_ => return Err("extract_loop_condition_vars: Left side not a variable".to_string()),
_ => {
return Err("extract_loop_condition_vars: Left side not a variable".to_string());
}
};
// Right should be: s.length() - separator.length()
@ -249,6 +288,116 @@ fn extract_subtraction_vars(expr: &ASTNode) -> Result<(String, String), String>
}
}
fn validate_start_update(
assign: &ASTNode,
i_var: &str,
sep_var: &str,
) -> Result<(), SplitScanExtractError> {
use crate::ast::BinaryOperator;
let hint = "use `start = i + separator.length()` in the then-branch";
match assign {
ASTNode::Assignment { target, value, .. } => {
if !matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == "start") {
return Err(SplitScanExtractError::contract(
"split scan contract: start target must be `start`",
hint,
));
}
match value.as_ref() {
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} => {
let left_ok = matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == i_var);
let right_ok = matches!(right.as_ref(), ASTNode::MethodCall { object, method, arguments, .. }
if method == "length"
&& arguments.is_empty()
&& matches!(object.as_ref(), ASTNode::Variable { name, .. } if name == sep_var)
);
if left_ok && right_ok {
Ok(())
} else {
Err(SplitScanExtractError::contract(
"split scan contract: start update must be `i + separator.length()`",
hint,
))
}
}
_ => Err(SplitScanExtractError::contract(
"split scan contract: start update must be `i + separator.length()`",
hint,
)),
}
}
_ => Err(SplitScanExtractError::contract(
"split scan contract: expected start assignment",
hint,
)),
}
}
fn validate_i_set_to_start(assign: &ASTNode, i_var: &str) -> Result<(), SplitScanExtractError> {
let hint = "use `i = start` in the then-branch";
match assign {
ASTNode::Assignment { target, value, .. } => {
let target_ok = matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == i_var);
let value_ok = matches!(value.as_ref(), ASTNode::Variable { name, .. } if name == "start");
if target_ok && value_ok {
Ok(())
} else {
Err(SplitScanExtractError::contract(
"split scan contract: then i update must be `i = start`",
hint,
))
}
}
_ => Err(SplitScanExtractError::contract(
"split scan contract: expected then i assignment",
hint,
)),
}
}
fn validate_i_increment_by_one(
assign: &ASTNode,
i_var: &str,
) -> Result<(), SplitScanExtractError> {
use crate::ast::{BinaryOperator, LiteralValue};
let hint = "use `i = i + 1` in the else-branch";
match assign {
ASTNode::Assignment { target, value, .. } => {
let target_ok = matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == i_var);
let value_ok = matches!(value.as_ref(), ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} if matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == i_var)
&& matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(1), .. })
);
if target_ok && value_ok {
Ok(())
} else {
Err(SplitScanExtractError::contract(
"split scan contract: else i update must be `i = i + 1`",
hint,
))
}
}
_ => Err(SplitScanExtractError::contract(
"split scan contract: expected else i assignment",
hint,
)),
}
}
/// Extract result variable name from push call
/// Expected: result.push(...)
fn extract_result_var(push_stmt: &ASTNode) -> Result<String, String> {

View File

@ -0,0 +1,40 @@
#!/bin/bash
# Phase 29ab P5: Pattern7 first-fail minimal (contract violation)
# Tests: split scan with else step != i + 1 must fail-fast
source "$(dirname "$0")/../../../lib/test_runner.sh"
export SMOKES_USE_PYVM=0
require_env || exit 2
INPUT="$NYASH_ROOT/apps/tests/phase29ab_pattern7_firstfail_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_pattern7_firstfail_min_vm: hakorune timed out (>${RUN_TIMEOUT_SECS}s)"
exit 1
fi
if [ "$EXIT_CODE" -eq 0 ]; then
echo "[FAIL] Expected JoinIR contract freeze error, got exit 0"
echo "[INFO] Output:"
echo "$OUTPUT" | tail -n 40 || true
test_fail "phase29ab_pattern7_firstfail_min_vm: Unexpected success"
exit 1
fi
if echo "$OUTPUT" | grep -q "\[joinir/phase29ab/pattern7/contract\]"; then
test_pass "phase29ab_pattern7_firstfail_min_vm: joinir contract freeze detected"
exit 0
else
echo "[FAIL] Expected joinir contract freeze tag in output"
echo "[INFO] Exit code: $EXIT_CODE"
echo "[INFO] Output:"
echo "$OUTPUT" | tail -n 60 || true
test_fail "phase29ab_pattern7_firstfail_min_vm: Missing joinir contract freeze tag"
exit 1
fi