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

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