refactor: unify string helpers and pattern2 derived slot
This commit is contained in:
@ -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}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,10 +8,11 @@
|
||||
//! - 曖昧な loop(true) を **通さない**(Fail-Fast で理由を返す)
|
||||
//!
|
||||
//! ## Contract(Fail-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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
//! Phase 29ab P4: Pattern2 contract modules (SSOT)
|
||||
|
||||
pub(crate) mod derived_slot;
|
||||
@ -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;
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
|
||||
Reference in New Issue
Block a user