phase29ab(p5): freeze pattern7 split-scan near-miss with fixture+smoke
This commit is contained in:
@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user