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

@ -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> {