phase29ab(p5): freeze pattern7 split-scan near-miss with fixture+smoke
This commit is contained in:
37
apps/tests/phase29ab_pattern7_firstfail_min.hako
Normal file
37
apps/tests/phase29ab_pattern7_firstfail_min.hako
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
# Self Current Task — Now (main)
|
||||
|
||||
## Current Focus: Phase 29ab P4(Phase 263 realworld seg: Derived slot)
|
||||
## Current Focus: Phase 29ab P5(Pattern7 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`
|
||||
|
||||
@ -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 263(Stage‑B 実ログ seg)を Derived slot 方針で通し、fixture+smoke で固定
|
||||
- P5: Pattern6/7(joinir/edgecfg)first-fail を fixture で固定して、Frag/compose へ収束方針を決める
|
||||
- P5: Pattern7 SplitScan first-fail(契約違反)を freeze で固定して SSOT 化
|
||||
- P6: Pattern6 MatchScan first-fail を同様に fixture+smoke で固定(freeze/NotApplicableの境界を決める)
|
||||
|
||||
- **Phase 288(✅ P0–P3 + 288.1 complete): REPL mode**
|
||||
- 入口: `docs/development/current/main/phases/phase-288/README.md`
|
||||
|
||||
@ -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` 破り)
|
||||
|
||||
## 直近の導入ステップ(最小で始める)
|
||||
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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
|
||||
Reference in New Issue
Block a user