refactor: unify string helpers and pattern2 derived slot

This commit is contained in:
2025-12-28 13:22:02 +09:00
parent 84e1cd7c7b
commit 10e6a15552
41 changed files with 2044 additions and 585 deletions

View File

@ -6,7 +6,9 @@
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use crate::mir::builder::control_flow::joinir::patterns::policies::PolicyDecision;
use crate::mir::builder::control_flow::joinir::patterns::pattern2::contracts::derived_slot::extract_derived_slot_for_conditions;
use crate::mir::join_ir::lowering::carrier_info::CarrierInfo;
use crate::mir::join_ir::lowering::common::body_local_derived_slot_emitter::BodyLocalDerivedSlotRecipe;
use crate::mir::join_ir::lowering::common::body_local_slot::{
ReadOnlyBodyLocalSlot, ReadOnlyBodyLocalSlotBox,
};
@ -27,6 +29,7 @@ pub enum BodyLocalRoute {
carrier_name: String,
},
ReadOnlySlot(ReadOnlyBodyLocalSlot),
DerivedSlot(BodyLocalDerivedSlotRecipe),
}
pub fn classify_for_pattern2(
@ -60,17 +63,29 @@ pub fn classify_for_pattern2(
carrier_info: promoted_carrier,
promoted_var,
carrier_name,
} => PolicyDecision::Use(BodyLocalRoute::Promotion {
promoted_carrier,
promoted_var,
carrier_name,
}),
} => match extract_derived_slot_for_conditions(&vars, body) {
Ok(Some(recipe)) => PolicyDecision::Use(BodyLocalRoute::DerivedSlot(recipe)),
Ok(None) => PolicyDecision::Use(BodyLocalRoute::Promotion {
promoted_carrier,
promoted_var,
carrier_name,
}),
Err(slot_err) => PolicyDecision::Reject(format!(
"[pattern2/body_local_policy] derived-slot check failed: {slot_err}"
)),
},
ConditionPromotionResult::CannotPromote { reason, .. } => {
match extract_body_local_inits_for_conditions(&vars, body) {
Ok(Some(slot)) => PolicyDecision::Use(BodyLocalRoute::ReadOnlySlot(slot)),
Ok(None) => PolicyDecision::Reject(reason),
match extract_derived_slot_for_conditions(&vars, body) {
Ok(Some(recipe)) => PolicyDecision::Use(BodyLocalRoute::DerivedSlot(recipe)),
Ok(None) => match extract_body_local_inits_for_conditions(&vars, body) {
Ok(Some(slot)) => PolicyDecision::Use(BodyLocalRoute::ReadOnlySlot(slot)),
Ok(None) => PolicyDecision::Reject(reason),
Err(slot_err) => PolicyDecision::Reject(format!(
"{reason}; read-only-slot rejected: {slot_err}"
)),
},
Err(slot_err) => PolicyDecision::Reject(format!(
"{reason}; read-only-slot rejected: {slot_err}"
"{reason}; derived-slot rejected: {slot_err}"
)),
}
}

View File

@ -8,10 +8,11 @@
//! - 曖昧な loop(true) を **通さない**Fail-Fast で理由を返す)
//!
//! ## ContractFail-Fast
//! 許可(read_digits(loop(true)) 系で必要な最小):
//! 許可loop(true) 系で必要な最小):
//! - カウンタ候補が **ちょうど1つ**
//! - 更新が `i = i + 1` 形(定数 1 のみ)
//! - `s.substring(i, i + 1)` 形が body のどこかに存在(誤マッチ防止
//! - 更新が `i = i + 1` 形(定数 1 のみ) **または**
//! `i = j + K` 形(`j = s.indexOf(..., i)` 由来、K は整数定数
//! - `substring(i, ...)` が body のどこかに存在(誤マッチ防止)
//! - `i` が loop-outer var`variable_map` に存在)である
//!
//! 禁止:
@ -86,13 +87,6 @@ impl LoopTrueCounterExtractorBox {
}
}
fn extract_var_name(n: &ASTNode) -> Option<String> {
match n {
ASTNode::Variable { name, .. } => Some(name.clone()),
_ => None,
}
}
fn is_self_plus_const_one(value: &ASTNode, target: &ASTNode) -> bool {
let target_name = match extract_var_name(target) {
Some(n) => n,
@ -126,22 +120,122 @@ impl LoopTrueCounterExtractorBox {
candidates.sort();
candidates.dedup();
let loop_var_name = match candidates.len() {
0 => {
return Err(
"[pattern2/loop_true_counter/contract/no_candidate] Cannot find unique counter update `i = i + 1` in loop(true) body"
.to_string(),
);
}
1 => candidates[0].clone(),
_ => {
if candidates.len() > 1 {
return Err(format!(
"[pattern2/loop_true_counter/contract/multiple_candidates] Multiple counter candidates found in loop(true) body: {:?}",
candidates
));
}
if candidates.len() == 1 {
let loop_var_name = candidates[0].clone();
let host_id = variable_map.get(&loop_var_name).copied().ok_or_else(|| {
format!(
"[pattern2/loop_true_counter/contract/not_loop_outer] Counter '{}' not found in variable_map (loop-outer var required)",
loop_var_name
)
})?;
if !has_substring_read(body, &loop_var_name) {
return Err(format!(
"[pattern2/loop_true_counter/contract/multiple_candidates] Multiple counter candidates found in loop(true) body: {:?}",
candidates
"[pattern2/loop_true_counter/contract/missing_substring_guard] Counter '{}' found, but missing substring pattern `s.substring({}, {} + 1)`",
loop_var_name, loop_var_name, loop_var_name
));
}
};
return Ok((loop_var_name, host_id));
}
if let Some((loop_var_name, host_id)) =
extract_loop_counter_from_indexof_pattern(body, variable_map)?
{
return Ok((loop_var_name, host_id));
}
Err(
"[pattern2/loop_true_counter/contract/no_candidate] Cannot find unique counter update `i = i + 1` in loop(true) body"
.to_string(),
)
}
}
fn extract_var_name(n: &ASTNode) -> Option<String> {
match n {
ASTNode::Variable { name, .. } => Some(name.clone()),
_ => None,
}
}
fn extract_loop_counter_from_indexof_pattern(
body: &[ASTNode],
variable_map: &BTreeMap<String, ValueId>,
) -> Result<Option<(String, ValueId)>, String> {
let indexof_bindings = collect_indexof_bindings(body);
if indexof_bindings.is_empty() {
return Ok(None);
}
let mut candidates: Vec<String> = Vec::new();
fn walk_assign(
node: &ASTNode,
indexof_bindings: &[(String, String)],
candidates: &mut Vec<String>,
) {
match node {
ASTNode::Assignment { target, value, .. } => {
if let (Some(target_name), Some((index_var, const_val))) =
(extract_var_name(target.as_ref()), extract_add_var_const(value.as_ref()))
{
if const_val <= 0 {
return;
}
if indexof_bindings.iter().any(|(idx_var, start_var)| {
idx_var == &index_var && start_var == &target_name
}) {
candidates.push(target_name);
}
}
}
ASTNode::If {
then_body,
else_body,
..
} => {
for s in then_body {
walk_assign(s, indexof_bindings, candidates);
}
if let Some(eb) = else_body {
for s in eb {
walk_assign(s, indexof_bindings, candidates);
}
}
}
ASTNode::Loop { body, .. } => {
for s in body {
walk_assign(s, indexof_bindings, candidates);
}
}
_ => {}
}
}
for stmt in body {
walk_assign(stmt, &indexof_bindings, &mut candidates);
}
candidates.sort();
candidates.dedup();
if candidates.len() > 1 {
return Err(format!(
"[pattern2/loop_true_counter/contract/multiple_candidates] Multiple counter candidates found in loop(true) body: {:?}",
candidates
));
}
if candidates.len() == 1 {
let loop_var_name = candidates[0].clone();
let host_id = variable_map.get(&loop_var_name).copied().ok_or_else(|| {
format!(
"[pattern2/loop_true_counter/contract/not_loop_outer] Counter '{}' not found in variable_map (loop-outer var required)",
@ -149,14 +243,112 @@ impl LoopTrueCounterExtractorBox {
)
})?;
if !has_substring_read(body, &loop_var_name) {
if !has_substring_read_with_start(body, &loop_var_name) {
return Err(format!(
"[pattern2/loop_true_counter/contract/missing_substring_guard] Counter '{}' found, but missing substring pattern `s.substring({}, {} + 1)`",
loop_var_name, loop_var_name, loop_var_name
"[pattern2/loop_true_counter/contract/missing_substring_guard] Counter '{}' found, but missing substring pattern `substring({}, ...)`",
loop_var_name, loop_var_name
));
}
Ok((loop_var_name, host_id))
return Ok(Some((loop_var_name, host_id)));
}
Ok(None)
}
fn collect_indexof_bindings(body: &[ASTNode]) -> Vec<(String, String)> {
fn extract_indexof_binding(node: &ASTNode) -> Option<(String, String)> {
let (target_name, value_node) = match node {
ASTNode::Local {
variables,
initial_values,
..
} => {
if variables.len() != 1 {
return None;
}
let value = initial_values.get(0).and_then(|v| v.as_ref())?;
(variables[0].clone(), value.as_ref())
}
ASTNode::Assignment { target, value, .. } => {
let target_name = extract_var_name(target.as_ref())?;
(target_name, value.as_ref())
}
_ => return None,
};
if let ASTNode::MethodCall {
method,
arguments,
..
} = value_node
{
if method == "indexOf" && arguments.len() == 2 {
if let ASTNode::Variable { name, .. } = &arguments[1] {
return Some((target_name, name.clone()));
}
}
}
None
}
fn walk(node: &ASTNode, out: &mut Vec<(String, String)>) {
if let Some(binding) = extract_indexof_binding(node) {
out.push(binding);
}
match node {
ASTNode::If {
then_body,
else_body,
..
} => {
for s in then_body {
walk(s, out);
}
if let Some(eb) = else_body {
for s in eb {
walk(s, out);
}
}
}
ASTNode::Loop { body, .. } => {
for s in body {
walk(s, out);
}
}
_ => {}
}
}
let mut bindings = Vec::new();
for stmt in body {
walk(stmt, &mut bindings);
}
bindings
}
fn extract_add_var_const(value: &ASTNode) -> Option<(String, i64)> {
match value {
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} => {
if let ASTNode::Variable { name, .. } = left.as_ref() {
if let ASTNode::Literal {
value: LiteralValue::Integer(i),
..
} = right.as_ref()
{
return Some((name.clone(), *i));
}
}
None
}
_ => None,
}
}
@ -221,6 +413,55 @@ fn has_substring_read(body: &[ASTNode], counter: &str) -> bool {
body.iter().any(|s| walk(s, counter))
}
fn has_substring_read_with_start(body: &[ASTNode], counter: &str) -> bool {
fn walk(node: &ASTNode, counter: &str) -> bool {
match node {
ASTNode::Assignment { value, .. } => walk(value.as_ref(), counter),
ASTNode::Local { initial_values, .. } => initial_values
.iter()
.filter_map(|v| v.as_ref())
.any(|v| walk(v.as_ref(), counter)),
ASTNode::MethodCall {
method,
arguments,
..
} => {
if method == "substring" && arguments.len() == 2 {
if matches!(
&arguments[0],
ASTNode::Variable { name, .. } if name == counter
) {
return true;
}
}
arguments.iter().any(|a| walk(a, counter))
}
ASTNode::BinaryOp { left, right, .. } => {
walk(left.as_ref(), counter) || walk(right.as_ref(), counter)
}
ASTNode::If {
condition,
then_body,
else_body,
..
} => {
walk(condition.as_ref(), counter)
|| then_body.iter().any(|s| walk(s, counter))
|| else_body
.as_ref()
.map(|eb| eb.iter().any(|s| walk(s, counter)))
.unwrap_or(false)
}
ASTNode::Loop { body, condition, .. } => {
walk(condition.as_ref(), counter) || body.iter().any(|s| walk(s, counter))
}
_ => false,
}
}
body.iter().any(|s| walk(s, counter))
}
#[cfg(test)]
mod tests {
use super::*;
@ -244,6 +485,13 @@ mod tests {
}
}
fn lit_s(s: &str) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::String(s.to_string()),
span: span(),
}
}
fn add(left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
@ -270,6 +518,24 @@ mod tests {
}
}
fn substring_ij(s_var: &str, i_var: &str, j_var: &str) -> ASTNode {
ASTNode::MethodCall {
object: Box::new(var(s_var)),
method: "substring".to_string(),
arguments: vec![var(i_var), var(j_var)],
span: span(),
}
}
fn indexof(s_var: &str, needle: &str, start_var: &str) -> ASTNode {
ASTNode::MethodCall {
object: Box::new(var(s_var)),
method: "indexOf".to_string(),
arguments: vec![lit_s(needle), var(start_var)],
span: span(),
}
}
fn local_one(name: &str, init: ASTNode) -> ASTNode {
ASTNode::Local {
variables: vec![name.to_string()],
@ -332,4 +598,21 @@ mod tests {
.unwrap_err();
assert!(err.contains("missing_substring_guard"));
}
#[test]
fn extract_indexof_candidate_ok() {
let body = vec![
local_one("j", indexof("table", "|||", "i")),
local_one("seg", substring_ij("table", "i", "j")),
assign(var("i"), add(var("j"), lit_i(3))),
];
let mut variable_map = BTreeMap::new();
variable_map.insert("i".to_string(), ValueId(7));
let (name, host_id) =
LoopTrueCounterExtractorBox::extract_loop_counter_from_body(&body, &variable_map)
.unwrap();
assert_eq!(name, "i");
assert_eq!(host_id, ValueId(7));
}
}

View File

@ -4,6 +4,7 @@
- `loop(...) { ... break ... }` (break present, no continue/return)
- break condition is normalized to "break when <cond> is true"
- loop variable comes from header condition or loop(true) counter extraction
- loop(true): `i = i + 1` + `substring(i, i + 1)` or `i = j + K` with `j = indexOf(..., i)` + `substring(i, ...)`
## LoopBodyLocal promotion
- SSOT entry: `pattern2::api::try_promote`
@ -15,6 +16,12 @@
- Break guard: `if seg == " " || seg == "\\t" { break }`
- seg is read-only (no reassignment in the loop body)
## Derived slot minimal shape (seg)
- Example shape (Derived): `local seg = ""` then `if cond { seg = expr1 } else { seg = expr2 }`
- Break guard: `if seg == "" { break }` (seg used in break condition)
- seg is recomputed per-iteration (Select), no promotion
- Contract SSOT: `pattern2/contracts/derived_slot.rs`
## Carrier binding rules (Pattern2)
- `CarrierInit::FromHost` -> host binding required
- `CarrierInit::BoolConst(_)` / `CarrierInit::LoopLocalZero` -> host binding is skipped
@ -22,9 +29,9 @@
## Out of scope
- multiple breaks / continue / return in the loop body
- reassigned LoopBodyLocal or ReadOnlySlot contract violations
- reassigned LoopBodyLocal outside the derived-slot shape
- break conditions with unsupported AST shapes
- seg reassignment or non-substring init (e.g., `seg = other_call()`)
- non-substring init for Trim promotion (e.g., `seg = other_call()`)
## Fail-Fast policy
- `PromoteDecision::Freeze` -> Err (missing implementation or contract violation)
@ -32,4 +39,4 @@
## `Ok(None)` meaning
- not Pattern2 (extractor returns None)
- promotion NotApplicable (router fallback)
- promotion NotApplicable (continue Pattern2 without promotion)

View File

@ -106,6 +106,10 @@ pub(in crate::mir::builder) fn try_promote(
inputs.allowed_body_locals_for_conditions = vec![slot.name.clone()];
inputs.read_only_body_local_slot = Some(slot);
}
PolicyDecision::Use(BodyLocalRoute::DerivedSlot(recipe)) => {
inputs.allowed_body_locals_for_conditions = vec![recipe.name.clone()];
inputs.body_local_derived_slot_recipe = Some(recipe);
}
PolicyDecision::Reject(reason) => {
// Phase 263 P0.1: Reject を PromoteDecision で二分化(型安全)
// 対象だが未対応freeze級: 実装バグ or 将来実装予定 → Freeze で Fail-Fast

View File

@ -0,0 +1,230 @@
//! Phase 29ab P4: Derived slot contract for Pattern2
//!
//! Responsibility:
//! - Extract a minimal derived-slot recipe for a single LoopBodyLocal variable
//! used in Pattern2 break conditions.
//! - No JoinIR emission; detection only.
use crate::ast::ASTNode;
use crate::mir::join_ir::lowering::common::body_local_derived_slot_emitter::BodyLocalDerivedSlotRecipe;
pub(crate) fn extract_body_local_derived_slot(
name: &str,
body: &[ASTNode],
) -> Result<Option<BodyLocalDerivedSlotRecipe>, String> {
let break_guard_idx = match find_first_top_level_break_guard_if(body) {
Some(idx) => idx,
None => return Ok(None),
};
let (decl_idx, base_init_expr) = match find_top_level_local_init(body, name) {
Some(result) => result,
None => return Ok(None),
};
if decl_idx >= break_guard_idx {
return Ok(None);
}
let (assign_idx, assign_cond, then_expr, else_expr) =
match find_top_level_conditional_assignment(body, name, break_guard_idx) {
Some(result) => result,
None => return Ok(None),
};
if assign_idx <= decl_idx {
return Ok(None);
}
if has_other_assignments(body, name, assign_idx) {
return Ok(None);
}
Ok(Some(BodyLocalDerivedSlotRecipe {
name: name.to_string(),
base_init_expr,
assign_cond,
then_expr,
else_expr: Some(else_expr),
}))
}
pub(crate) fn extract_derived_slot_for_conditions(
body_local_names_in_conditions: &[String],
body: &[ASTNode],
) -> Result<Option<BodyLocalDerivedSlotRecipe>, String> {
if body_local_names_in_conditions.len() != 1 {
return Ok(None);
}
extract_body_local_derived_slot(&body_local_names_in_conditions[0], body)
}
fn find_first_top_level_break_guard_if(body: &[ASTNode]) -> Option<usize> {
for (idx, stmt) in body.iter().enumerate() {
if let ASTNode::If {
then_body,
else_body,
..
} = stmt
{
if then_body.iter().any(|n| matches!(n, ASTNode::Break { .. })) {
return Some(idx);
}
if let Some(else_body) = else_body {
if else_body.iter().any(|n| matches!(n, ASTNode::Break { .. })) {
return Some(idx);
}
}
}
}
None
}
fn find_top_level_local_init(body: &[ASTNode], name: &str) -> Option<(usize, ASTNode)> {
for (idx, stmt) in body.iter().enumerate() {
if let ASTNode::Local {
variables,
initial_values,
..
} = stmt
{
if variables.len() != 1 {
continue;
}
if variables[0] != name {
continue;
}
let init = initial_values
.get(0)
.and_then(|v| v.as_ref())
.map(|b| (*b.clone()).clone())?;
return Some((idx, init));
}
}
None
}
fn find_top_level_conditional_assignment(
body: &[ASTNode],
name: &str,
break_guard_idx: usize,
) -> Option<(usize, ASTNode, ASTNode, ASTNode)> {
for (idx, stmt) in body.iter().enumerate() {
if idx >= break_guard_idx {
break;
}
let ASTNode::If {
condition,
then_body,
else_body,
..
} = stmt else {
continue;
};
let Some(else_body) = else_body.as_ref() else {
continue;
};
let Some(then_expr) = extract_single_assignment_expr(then_body, name) else {
continue;
};
let Some(else_expr) = extract_single_assignment_expr(else_body, name) else {
continue;
};
return Some((idx, (*condition.clone()), then_expr, else_expr));
}
None
}
fn extract_single_assignment_expr(stmts: &[ASTNode], name: &str) -> Option<ASTNode> {
if stmts.len() != 1 {
return None;
}
match &stmts[0] {
ASTNode::Assignment { target, value, .. } => {
if matches!(&**target, ASTNode::Variable { name: n, .. } if n == name) {
Some((**value).clone())
} else {
None
}
}
_ => None,
}
}
fn has_other_assignments(body: &[ASTNode], name: &str, assign_if_idx: usize) -> bool {
body.iter().enumerate().any(|(idx, stmt)| {
if idx == assign_if_idx {
return false;
}
contains_assignment_to_name_in_node(stmt, name)
})
}
fn contains_assignment_to_name_in_node(node: &ASTNode, name: &str) -> bool {
match node {
ASTNode::Assignment { target, value, .. } => {
if matches!(&**target, ASTNode::Variable { name: n, .. } if n == name) {
return true;
}
contains_assignment_to_name_in_node(target, name)
|| contains_assignment_to_name_in_node(value, name)
}
ASTNode::Nowait { variable, .. } => variable == name,
ASTNode::If {
condition,
then_body,
else_body,
..
} => {
contains_assignment_to_name_in_node(condition, name)
|| then_body
.iter()
.any(|n| contains_assignment_to_name_in_node(n, name))
|| else_body.as_ref().is_some_and(|e| {
e.iter()
.any(|n| contains_assignment_to_name_in_node(n, name))
})
}
ASTNode::Loop { condition, body, .. } => {
contains_assignment_to_name_in_node(condition, name)
|| body
.iter()
.any(|n| contains_assignment_to_name_in_node(n, name))
}
ASTNode::While { condition, body, .. } => {
contains_assignment_to_name_in_node(condition, name)
|| body
.iter()
.any(|n| contains_assignment_to_name_in_node(n, name))
}
ASTNode::ForRange { body, .. } => body
.iter()
.any(|n| contains_assignment_to_name_in_node(n, name)),
ASTNode::TryCatch {
try_body,
catch_clauses,
finally_body,
..
} => {
try_body
.iter()
.any(|n| contains_assignment_to_name_in_node(n, name))
|| catch_clauses.iter().any(|c| {
c.body
.iter()
.any(|n| contains_assignment_to_name_in_node(n, name))
})
|| finally_body.as_ref().is_some_and(|b| {
b.iter()
.any(|n| contains_assignment_to_name_in_node(n, name))
})
}
ASTNode::ScopeBox { body, .. } => body
.iter()
.any(|n| contains_assignment_to_name_in_node(n, name)),
_ => false,
}
}

View File

@ -0,0 +1,3 @@
//! Phase 29ab P4: Pattern2 contract modules (SSOT)
pub(crate) mod derived_slot;

View File

@ -4,3 +4,4 @@
//! - `api/` - Public entry point for promotion logic (SSOT)
pub(in crate::mir::builder) mod api;
pub(in crate::mir::builder) mod contracts;

View File

@ -87,6 +87,10 @@ pub(in crate::mir::builder) struct Pattern2Inputs {
/// Phase 94: BodyLocalDerived recipe for P5b "ch" reassignment + escape counter.
pub body_local_derived_recipe:
Option<crate::mir::join_ir::lowering::common::body_local_derived_emitter::BodyLocalDerivedRecipe>,
/// Phase 29ab P4: Derived slot recipe for seg-like conditional assignments.
pub body_local_derived_slot_recipe: Option<
crate::mir::join_ir::lowering::common::body_local_derived_slot_emitter::BodyLocalDerivedSlotRecipe,
>,
/// Phase 107: Balanced depth-scan (find_balanced_*) derived recipe.
pub balanced_depth_scan_recipe:
Option<crate::mir::join_ir::lowering::common::balanced_depth_scan_emitter::BalancedDepthScanRecipe>,

View File

@ -30,6 +30,7 @@ impl ApplyPolicyStepBox {
is_loop_true_read_digits: policy.is_loop_true_read_digits,
condition_only_recipe: None,
body_local_derived_recipe: None,
body_local_derived_slot_recipe: None,
balanced_depth_scan_recipe: policy.balanced_depth_scan_recipe,
carrier_updates_override: policy.carrier_updates_override,
post_loop_early_return: policy.post_loop_early_return,

View File

@ -51,6 +51,7 @@ impl EmitJoinIRStepBox {
skeleton,
condition_only_recipe: inputs.condition_only_recipe.as_ref(),
body_local_derived_recipe: inputs.body_local_derived_recipe.as_ref(),
body_local_derived_slot_recipe: inputs.body_local_derived_slot_recipe.as_ref(),
balanced_depth_scan_recipe: inputs.balanced_depth_scan_recipe.as_ref(),
current_static_box_name: inputs.current_static_box_name.clone(), // Phase 252
};

View File

@ -4,6 +4,7 @@
//! 生成loweringは従来通り TrimLoopLowerer 側が担当する。
use crate::ast::ASTNode;
use crate::mir::builder::control_flow::joinir::patterns::pattern2::contracts::derived_slot::extract_body_local_derived_slot;
use crate::mir::builder::control_flow::joinir::patterns::trim_loop_lowering::TrimLoopLowerer;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::loop_pattern_detection::loop_condition_scope::{
@ -23,6 +24,7 @@ pub fn classify_trim_like_loop(
scope: &LoopScopeShape,
loop_cond: &ASTNode,
break_cond: &ASTNode,
body: &[ASTNode],
loop_var_name: &str,
) -> PolicyDecision<TrimPolicyResult> {
let cond_scope =
@ -44,6 +46,18 @@ pub fn classify_trim_like_loop(
return PolicyDecision::None;
}
if condition_body_locals.len() == 1 {
match extract_body_local_derived_slot(&condition_body_locals[0].name, body) {
Ok(Some(_)) => return PolicyDecision::None,
Ok(None) => {}
Err(reason) => {
return PolicyDecision::Reject(format!(
"[trim_policy] derived-slot check failed: {reason}"
))
}
}
}
PolicyDecision::Use(TrimPolicyResult {
cond_scope,
condition_body_locals,

View File

@ -187,7 +187,7 @@ impl TrimLoopLowerer {
let TrimPolicyResult {
cond_scope,
condition_body_locals,
} = match classify_trim_like_loop(scope, loop_cond, break_cond, loop_var_name) {
} = match classify_trim_like_loop(scope, loop_cond, break_cond, body, loop_var_name) {
PolicyDecision::Use(res) => res,
PolicyDecision::None => return Ok(None),
PolicyDecision::Reject(reason) => return Err(reason),