Phase 33 NORM canon test: enforce normalized dev route for P1/P2/JP mini

This commit is contained in:
nyash-codex
2025-12-11 20:54:33 +09:00
parent 59a985b7fa
commit af6f95cd4b
170 changed files with 4423 additions and 1897 deletions

View File

@ -251,9 +251,7 @@ mod tests {
span: Span::unknown(),
};
assert!(BreakConditionAnalyzer::has_break_in_else_clause(&[
if_stmt
]));
assert!(BreakConditionAnalyzer::has_break_in_else_clause(&[if_stmt]));
}
#[test]
@ -448,9 +446,7 @@ mod tests {
// Should be wrapped in UnaryOp::Not
if let ASTNode::UnaryOp {
operator,
operand,
..
operator, operand, ..
} = negated
{
assert!(matches!(operator, UnaryOperator::Not));

View File

@ -124,10 +124,7 @@ pub fn extract_all_variables(node: &ASTNode) -> HashSet<String> {
/// Future versions may include:
/// - Dominance tree analysis
/// - More sophisticated scope inference
pub(crate) fn is_outer_scope_variable(
var_name: &str,
scope: Option<&LoopScopeShape>,
) -> bool {
pub(crate) fn is_outer_scope_variable(var_name: &str, scope: Option<&LoopScopeShape>) -> bool {
match scope {
// No scope information: be conservative but *not* overstrict.
// We treat unknown as body-local only when we have a LoopScopeShape
@ -162,7 +159,10 @@ pub(crate) fn is_outer_scope_variable(
// ...
// i = i + 1 (latch)
// }
if def_blocks.iter().all(|b| *b == scope.header || *b == scope.latch) {
if def_blocks
.iter()
.all(|b| *b == scope.header || *b == scope.latch)
{
return true;
}
@ -423,7 +423,10 @@ mod tests {
let vars = extract_all_variables(&literal_node);
assert!(vars.is_empty(), "Literal-only condition should extract no variables");
assert!(
vars.is_empty(),
"Literal-only condition should extract no variables"
);
}
#[test]
@ -453,14 +456,16 @@ mod tests {
};
// Phase 170-ultrathink: header+latch ONLY → OuterLocal (carrier variable)
assert!(is_outer_scope_variable("i", Some(&scope)),
"Carrier variable (header+latch only) should be classified as OuterLocal");
assert!(
is_outer_scope_variable("i", Some(&scope)),
"Carrier variable (header+latch only) should be classified as OuterLocal"
);
}
#[test]
fn test_scope_priority_in_add_var() {
// Test Phase 170-ultrathink scope priority system
use super::super::loop_condition_scope::{LoopConditionScope, CondVarScope};
use super::super::loop_condition_scope::{CondVarScope, LoopConditionScope};
let mut scope = LoopConditionScope::new();
@ -472,19 +477,28 @@ mod tests {
// Add same variable as OuterLocal (more restrictive)
scope.add_var("x".to_string(), CondVarScope::OuterLocal);
assert_eq!(scope.vars.len(), 1, "Should not duplicate variable");
assert_eq!(scope.vars[0].scope, CondVarScope::OuterLocal,
"Should upgrade to more restrictive OuterLocal");
assert_eq!(
scope.vars[0].scope,
CondVarScope::OuterLocal,
"Should upgrade to more restrictive OuterLocal"
);
// Try to downgrade to LoopBodyLocal (should be rejected)
scope.add_var("x".to_string(), CondVarScope::LoopBodyLocal);
assert_eq!(scope.vars.len(), 1);
assert_eq!(scope.vars[0].scope, CondVarScope::OuterLocal,
"Should NOT downgrade from OuterLocal to LoopBodyLocal");
assert_eq!(
scope.vars[0].scope,
CondVarScope::OuterLocal,
"Should NOT downgrade from OuterLocal to LoopBodyLocal"
);
// Add same variable as LoopParam (most restrictive)
scope.add_var("x".to_string(), CondVarScope::LoopParam);
assert_eq!(scope.vars.len(), 1);
assert_eq!(scope.vars[0].scope, CondVarScope::LoopParam,
"Should upgrade to most restrictive LoopParam");
assert_eq!(
scope.vars[0].scope,
CondVarScope::LoopParam,
"Should upgrade to most restrictive LoopParam"
);
}
}

View File

@ -43,7 +43,10 @@ pub fn format_unsupported_condition_error(
Consider using Pattern 5+ for complex loop conditions.",
pattern_name,
body_local_names,
pattern_name.chars().filter(|c| c.is_numeric()).collect::<String>()
pattern_name
.chars()
.filter(|c| c.is_numeric())
.collect::<String>()
)
}

View File

@ -30,9 +30,9 @@
//! Phase 200-A creates the infrastructure to capture such variables.
//! Phase 200-B will implement the actual detection logic.
use crate::mir::ValueId;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::ast::ASTNode;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::ValueId;
use std::collections::BTreeSet;
/// A variable captured from function scope for use in loop conditions/body.
@ -155,7 +155,9 @@ pub(crate) fn analyze_captured_vars(
Some(idx) => idx,
None => {
if debug {
eprintln!("[capture/debug] Loop not found in function body, returning empty CapturedEnv");
eprintln!(
"[capture/debug] Loop not found in function body, returning empty CapturedEnv"
);
}
return CapturedEnv::new();
}
@ -169,7 +171,10 @@ pub(crate) fn analyze_captured_vars(
let pre_loop_locals = collect_local_declarations(&fn_body[..loop_index]);
if debug {
eprintln!("[capture/debug] Found {} pre-loop local declarations", pre_loop_locals.len());
eprintln!(
"[capture/debug] Found {} pre-loop local declarations",
pre_loop_locals.len()
);
}
let mut env = CapturedEnv::new();
@ -230,7 +235,10 @@ pub(crate) fn analyze_captured_vars(
// Note: We don't have access to variable_map here, so we use a placeholder ValueId
// The actual host_id will be resolved in ConditionEnvBuilder
if debug {
eprintln!("[capture/accept] '{}': ALL CHECKS PASSED, adding to CapturedEnv", name);
eprintln!(
"[capture/accept] '{}': ALL CHECKS PASSED, adding to CapturedEnv",
name
);
}
env.add_var(CapturedVar {
@ -241,7 +249,8 @@ pub(crate) fn analyze_captured_vars(
}
if debug {
eprintln!("[capture/result] Captured {} variables: {:?}",
eprintln!(
"[capture/result] Captured {} variables: {:?}",
env.vars.len(),
env.vars.iter().map(|v| &v.name).collect::<Vec<_>>()
);
@ -301,7 +310,10 @@ pub(crate) fn analyze_captured_vars_v2(
};
if debug {
eprintln!("[capture/debug] Found {} pre-loop local declarations", pre_loop_locals.len());
eprintln!(
"[capture/debug] Found {} pre-loop local declarations",
pre_loop_locals.len()
);
}
let mut env = CapturedEnv::new();
@ -360,7 +372,10 @@ pub(crate) fn analyze_captured_vars_v2(
// All checks passed: add to CapturedEnv
if debug {
eprintln!("[capture/accept] '{}': ALL CHECKS PASSED, adding to CapturedEnv", name);
eprintln!(
"[capture/accept] '{}': ALL CHECKS PASSED, adding to CapturedEnv",
name
);
}
env.add_var(CapturedVar {
@ -374,8 +389,10 @@ pub(crate) fn analyze_captured_vars_v2(
let names_in_loop = collect_names_in_loop_parts(loop_condition, loop_body);
// pre-loop local names (already processed above)
let pre_loop_local_names: BTreeSet<String> =
pre_loop_locals.iter().map(|(name, _)| name.clone()).collect();
let pre_loop_local_names: BTreeSet<String> = pre_loop_locals
.iter()
.map(|(name, _)| name.clone())
.collect();
// Check each variable used in loop
for name in names_in_loop {
@ -402,7 +419,10 @@ pub(crate) fn analyze_captured_vars_v2(
// This is a function parameter-like variable - add to CapturedEnv
if debug {
eprintln!("[capture/param/accept] '{}': function parameter used in loop", name);
eprintln!(
"[capture/param/accept] '{}': function parameter used in loop",
name
);
}
env.add_var(CapturedVar {
@ -413,7 +433,8 @@ pub(crate) fn analyze_captured_vars_v2(
}
if debug {
eprintln!("[capture/result] Captured {} variables: {:?}",
eprintln!(
"[capture/result] Captured {} variables: {:?}",
env.vars.len(),
env.vars.iter().map(|v| &v.name).collect::<Vec<_>>()
);
@ -428,9 +449,9 @@ pub(crate) fn analyze_captured_vars_v2(
#[allow(dead_code)]
fn find_stmt_index(fn_body: &[ASTNode], loop_ast: &ASTNode) -> Option<usize> {
// Compare by pointer address (same AST node instance)
fn_body.iter().position(|stmt| {
std::ptr::eq(stmt as *const ASTNode, loop_ast as *const ASTNode)
})
fn_body
.iter()
.position(|stmt| std::ptr::eq(stmt as *const ASTNode, loop_ast as *const ASTNode))
}
/// Phase 200-C: Find loop index by structure matching (condition + body comparison)
@ -443,7 +464,10 @@ fn find_loop_index_by_structure(
target_body: &[ASTNode],
) -> Option<usize> {
for (idx, stmt) in fn_body.iter().enumerate() {
if let ASTNode::Loop { condition, body, .. } = stmt {
if let ASTNode::Loop {
condition, body, ..
} = stmt
{
// Compare condition and body by structure
if ast_matches(condition, target_condition) && body_matches(body, target_body) {
return Some(idx);
@ -476,7 +500,12 @@ fn collect_local_declarations(stmts: &[ASTNode]) -> Vec<(String, Option<Box<ASTN
let mut locals = Vec::new();
for stmt in stmts {
if let ASTNode::Local { variables, initial_values, .. } = stmt {
if let ASTNode::Local {
variables,
initial_values,
..
} = stmt
{
// Local declaration can have multiple variables (e.g., local a, b, c)
for (i, name) in variables.iter().enumerate() {
let init_expr = initial_values.get(i).and_then(|opt| opt.clone());
@ -528,29 +557,43 @@ fn is_reassigned_in_fn(fn_body: &[ASTNode], name: &str) -> bool {
}
// Grouped assignment expression: (x = expr)
ASTNode::GroupedAssignmentExpr { lhs, rhs, .. } => {
lhs == name || check_node(rhs, name)
}
ASTNode::GroupedAssignmentExpr { lhs, rhs, .. } => lhs == name || check_node(rhs, name),
// Recursive cases
ASTNode::If { condition, then_body, else_body, .. } => {
ASTNode::If {
condition,
then_body,
else_body,
..
} => {
check_node(condition, name)
|| then_body.iter().any(|n| check_node(n, name))
|| else_body.as_ref().map_or(false, |body| body.iter().any(|n| check_node(n, name)))
|| else_body
.as_ref()
.map_or(false, |body| body.iter().any(|n| check_node(n, name)))
}
ASTNode::Loop { condition, body, .. } => {
check_node(condition, name) || body.iter().any(|n| check_node(n, name))
}
ASTNode::Loop {
condition, body, ..
} => check_node(condition, name) || body.iter().any(|n| check_node(n, name)),
ASTNode::While { condition, body, .. } => {
check_node(condition, name) || body.iter().any(|n| check_node(n, name))
}
ASTNode::While {
condition, body, ..
} => check_node(condition, name) || body.iter().any(|n| check_node(n, name)),
ASTNode::TryCatch { try_body, catch_clauses, finally_body, .. } => {
ASTNode::TryCatch {
try_body,
catch_clauses,
finally_body,
..
} => {
try_body.iter().any(|n| check_node(n, name))
|| catch_clauses.iter().any(|clause| clause.body.iter().any(|n| check_node(n, name)))
|| finally_body.as_ref().map_or(false, |body| body.iter().any(|n| check_node(n, name)))
|| catch_clauses
.iter()
.any(|clause| clause.body.iter().any(|n| check_node(n, name)))
|| finally_body
.as_ref()
.map_or(false, |body| body.iter().any(|n| check_node(n, name)))
}
ASTNode::UnaryOp { operand, .. } => check_node(operand, name),
@ -559,9 +602,9 @@ fn is_reassigned_in_fn(fn_body: &[ASTNode], name: &str) -> bool {
check_node(left, name) || check_node(right, name)
}
ASTNode::MethodCall { object, arguments, .. } => {
check_node(object, name) || arguments.iter().any(|arg| check_node(arg, name))
}
ASTNode::MethodCall {
object, arguments, ..
} => check_node(object, name) || arguments.iter().any(|arg| check_node(arg, name)),
ASTNode::FunctionCall { arguments, .. } => {
arguments.iter().any(|arg| check_node(arg, name))
@ -573,9 +616,7 @@ fn is_reassigned_in_fn(fn_body: &[ASTNode], name: &str) -> bool {
check_node(target, name) || check_node(index, name)
}
ASTNode::Return { value, .. } => {
value.as_ref().map_or(false, |v| check_node(v, name))
}
ASTNode::Return { value, .. } => value.as_ref().map_or(false, |v| check_node(v, name)),
ASTNode::Local { .. } => {
// Local declarations are not reassignments
@ -598,14 +639,21 @@ fn is_used_in_loop(loop_ast: &ASTNode, name: &str) -> bool {
match node {
ASTNode::Variable { name: var_name, .. } => var_name == name,
ASTNode::Loop { condition, body, .. } => {
check_usage(condition, name) || body.iter().any(|n| check_usage(n, name))
}
ASTNode::Loop {
condition, body, ..
} => check_usage(condition, name) || body.iter().any(|n| check_usage(n, name)),
ASTNode::If { condition, then_body, else_body, .. } => {
ASTNode::If {
condition,
then_body,
else_body,
..
} => {
check_usage(condition, name)
|| then_body.iter().any(|n| check_usage(n, name))
|| else_body.as_ref().map_or(false, |body| body.iter().any(|n| check_usage(n, name)))
|| else_body
.as_ref()
.map_or(false, |body| body.iter().any(|n| check_usage(n, name)))
}
ASTNode::Assignment { target, value, .. } => {
@ -618,9 +666,9 @@ fn is_used_in_loop(loop_ast: &ASTNode, name: &str) -> bool {
check_usage(left, name) || check_usage(right, name)
}
ASTNode::MethodCall { object, arguments, .. } => {
check_usage(object, name) || arguments.iter().any(|arg| check_usage(arg, name))
}
ASTNode::MethodCall {
object, arguments, ..
} => check_usage(object, name) || arguments.iter().any(|arg| check_usage(arg, name)),
ASTNode::FunctionCall { arguments, .. } => {
arguments.iter().any(|arg| check_usage(arg, name))
@ -632,15 +680,11 @@ fn is_used_in_loop(loop_ast: &ASTNode, name: &str) -> bool {
check_usage(target, name) || check_usage(index, name)
}
ASTNode::Return { value, .. } => {
value.as_ref().map_or(false, |v| check_usage(v, name))
}
ASTNode::Return { value, .. } => value.as_ref().map_or(false, |v| check_usage(v, name)),
ASTNode::Local { initial_values, .. } => {
initial_values.iter().any(|opt| {
opt.as_ref().map_or(false, |init| check_usage(init, name))
})
}
ASTNode::Local { initial_values, .. } => initial_values
.iter()
.any(|opt| opt.as_ref().map_or(false, |init| check_usage(init, name))),
_ => false,
}
@ -657,14 +701,21 @@ fn is_used_in_loop_parts(condition: &ASTNode, body: &[ASTNode], name: &str) -> b
match node {
ASTNode::Variable { name: var_name, .. } => var_name == name,
ASTNode::Loop { condition, body, .. } => {
check_usage(condition, name) || body.iter().any(|n| check_usage(n, name))
}
ASTNode::Loop {
condition, body, ..
} => check_usage(condition, name) || body.iter().any(|n| check_usage(n, name)),
ASTNode::If { condition, then_body, else_body, .. } => {
ASTNode::If {
condition,
then_body,
else_body,
..
} => {
check_usage(condition, name)
|| then_body.iter().any(|n| check_usage(n, name))
|| else_body.as_ref().map_or(false, |body| body.iter().any(|n| check_usage(n, name)))
|| else_body
.as_ref()
.map_or(false, |body| body.iter().any(|n| check_usage(n, name)))
}
ASTNode::Assignment { target, value, .. } => {
@ -677,9 +728,9 @@ fn is_used_in_loop_parts(condition: &ASTNode, body: &[ASTNode], name: &str) -> b
check_usage(left, name) || check_usage(right, name)
}
ASTNode::MethodCall { object, arguments, .. } => {
check_usage(object, name) || arguments.iter().any(|arg| check_usage(arg, name))
}
ASTNode::MethodCall {
object, arguments, ..
} => check_usage(object, name) || arguments.iter().any(|arg| check_usage(arg, name)),
ASTNode::FunctionCall { arguments, .. } => {
arguments.iter().any(|arg| check_usage(arg, name))
@ -691,15 +742,11 @@ fn is_used_in_loop_parts(condition: &ASTNode, body: &[ASTNode], name: &str) -> b
check_usage(target, name) || check_usage(index, name)
}
ASTNode::Return { value, .. } => {
value.as_ref().map_or(false, |v| check_usage(v, name))
}
ASTNode::Return { value, .. } => value.as_ref().map_or(false, |v| check_usage(v, name)),
ASTNode::Local { initial_values, .. } => {
initial_values.iter().any(|opt| {
opt.as_ref().map_or(false, |init| check_usage(init, name))
})
}
ASTNode::Local { initial_values, .. } => initial_values
.iter()
.any(|opt| opt.as_ref().map_or(false, |init| check_usage(init, name))),
_ => false,
}
@ -718,7 +765,12 @@ fn collect_names_in_loop_parts(condition: &ASTNode, body: &[ASTNode]) -> BTreeSe
ASTNode::Variable { name, .. } => {
acc.insert(name.clone());
}
ASTNode::If { condition, then_body, else_body, .. } => {
ASTNode::If {
condition,
then_body,
else_body,
..
} => {
collect(condition, acc);
for stmt in then_body {
collect(stmt, acc);
@ -736,14 +788,19 @@ fn collect_names_in_loop_parts(condition: &ASTNode, body: &[ASTNode]) -> BTreeSe
ASTNode::UnaryOp { operand, .. } => {
collect(operand, acc);
}
ASTNode::Return { value: Some(operand), .. } => {
ASTNode::Return {
value: Some(operand),
..
} => {
collect(operand, acc);
}
ASTNode::BinaryOp { left, right, .. } => {
collect(left, acc);
collect(right, acc);
}
ASTNode::MethodCall { object, arguments, .. } => {
ASTNode::MethodCall {
object, arguments, ..
} => {
collect(object, acc);
for arg in arguments {
collect(arg, acc);
@ -768,7 +825,9 @@ fn collect_names_in_loop_parts(condition: &ASTNode, body: &[ASTNode]) -> BTreeSe
collect(target, acc);
collect(index, acc);
}
ASTNode::Loop { condition, body, .. } => {
ASTNode::Loop {
condition, body, ..
} => {
collect(condition, acc);
for stmt in body {
collect(stmt, acc);
@ -890,8 +949,8 @@ mod tests {
let fn_body = vec![digits_decl, loop_node.clone()];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
use std::collections::{BTreeMap, BTreeSet};
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
@ -982,8 +1041,8 @@ mod tests {
let fn_body = vec![digits_decl, reassignment, loop_node.clone()];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
use std::collections::{BTreeMap, BTreeSet};
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
@ -1040,8 +1099,8 @@ mod tests {
let fn_body = vec![loop_node.clone(), digits_decl];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
use std::collections::{BTreeMap, BTreeSet};
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
@ -1119,8 +1178,8 @@ mod tests {
let fn_body = vec![result_decl, loop_node.clone()];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
use std::collections::{BTreeMap, BTreeSet};
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
@ -1173,14 +1232,14 @@ mod tests {
}),
span: Span::unknown(),
}),
body: vec![], // empty body, no usage of digits
body: vec![], // empty body, no usage of digits
span: Span::unknown(),
};
let fn_body = vec![digits_decl, loop_node.clone()];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
use std::collections::{BTreeMap, BTreeSet};
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
@ -1205,7 +1264,7 @@ mod tests {
#[test]
fn test_capture_function_param_used_in_condition() {
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
// Simulate: fn parse_number(s, p, len) { loop(p < len) { ... } }
// Expected: 'len' should be captured (used in condition, not reassigned)
@ -1223,30 +1282,28 @@ mod tests {
span: Span::unknown(),
});
let body = vec![
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
let body = vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
}),
span: Span::unknown(),
}];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
use std::collections::{BTreeMap, BTreeSet};
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
@ -1274,7 +1331,7 @@ mod tests {
#[test]
fn test_capture_function_param_used_in_method_call() {
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
// Simulate: fn parse_number(s, p) { loop(p < s.length()) { ch = s.charAt(p) } }
// Expected: 's' should be captured (used in condition and body, not reassigned)
@ -1335,8 +1392,8 @@ mod tests {
},
];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
use std::collections::{BTreeMap, BTreeSet};
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
@ -1364,7 +1421,7 @@ mod tests {
#[test]
fn test_capture_function_param_reassigned_rejected() {
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
// Simulate: fn bad_func(x) { x = 5; loop(x < 10) { x = x + 1 } }
// Expected: 'x' should NOT be captured (reassigned in function)
@ -1382,45 +1439,41 @@ mod tests {
span: Span::unknown(),
});
let body = vec![
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
let body = vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
}),
span: Span::unknown(),
}];
// fn_body includes reassignment before loop
let fn_body = vec![
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(5),
span: Span::unknown(),
}),
let fn_body = vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
},
];
}),
value: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(5),
span: Span::unknown(),
}),
span: Span::unknown(),
}];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
use std::collections::{BTreeMap, BTreeSet};
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
@ -1444,7 +1497,7 @@ mod tests {
#[test]
fn test_capture_mixed_locals_and_params() {
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
// Simulate: fn parse(s, len) { local digits = "0123"; loop(p < len) { ch = digits.indexOf(...); s.charAt(...) } }
// Expected: 'len', 's', and 'digits' should all be captured
@ -1498,19 +1551,17 @@ mod tests {
];
// fn_body includes local declaration before loop
let fn_body = vec![
ASTNode::Local {
variables: vec!["digits".to_string()],
initial_values: vec![Some(Box::new(ASTNode::Literal {
value: LiteralValue::String("0123".to_string()),
span: Span::unknown(),
}))],
let fn_body = vec![ASTNode::Local {
variables: vec!["digits".to_string()],
initial_values: vec![Some(Box::new(ASTNode::Literal {
value: LiteralValue::String("0123".to_string()),
span: Span::unknown(),
},
];
}))],
span: Span::unknown(),
}];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
use std::collections::{BTreeMap, BTreeSet};
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),

View File

@ -68,16 +68,16 @@ impl TrimPatternInfo {
/// - The actual host ValueId will be assigned during merge_joinir_mir_blocks
/// - JoinInlineBoundary will handle the boundary mapping
pub fn to_carrier_info(&self) -> crate::mir::join_ir::lowering::carrier_info::CarrierInfo {
use super::trim_loop_helper::TrimLoopHelper;
use crate::mir::join_ir::lowering::carrier_info::CarrierInfo;
use crate::mir::ValueId;
use super::trim_loop_helper::TrimLoopHelper;
// Phase 171-C-4/5: Create CarrierInfo with promoted carrier as loop variable
// and attach TrimLoopHelper for future lowering
let mut carrier_info = CarrierInfo::with_carriers(
self.carrier_name.clone(), // "is_ch_match" becomes the loop variable
ValueId(0), // Placeholder (will be remapped)
vec![], // No additional carriers
self.carrier_name.clone(), // "is_ch_match" becomes the loop variable
ValueId(0), // Placeholder (will be remapped)
vec![], // No additional carriers
);
// Phase 171-C-5: Attach TrimLoopHelper for pattern-specific lowering logic
@ -85,7 +85,9 @@ impl TrimPatternInfo {
// Phase 229: Record promoted variable (no need for condition_aliases)
// Dynamic resolution uses promoted_loopbodylocals + naming convention
carrier_info.promoted_loopbodylocals.push(self.var_name.clone());
carrier_info
.promoted_loopbodylocals
.push(self.var_name.clone());
carrier_info
}
@ -104,7 +106,7 @@ pub enum PromotionResult {
/// 昇格不可: 理由を説明
CannotPromote {
reason: String,
vars: Vec<String>, // 問題の LoopBodyLocal
vars: Vec<String>, // 問題の LoopBodyLocal
},
}
@ -125,7 +127,10 @@ impl LoopBodyCarrierPromoter {
use crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope;
// 1. LoopBodyLocal を抽出
let body_locals: Vec<&String> = request.cond_scope.vars.iter()
let body_locals: Vec<&String> = request
.cond_scope
.vars
.iter()
.filter(|v| v.scope == CondVarScope::LoopBodyLocal)
.map(|v| &v.name)
.collect();
@ -230,7 +235,11 @@ impl LoopBodyCarrierPromoter {
}
// If: then_body と else_body を探索
ASTNode::If { then_body, else_body, .. } => {
ASTNode::If {
then_body,
else_body,
..
} => {
for stmt in then_body {
worklist.push(stmt);
}
@ -242,7 +251,9 @@ impl LoopBodyCarrierPromoter {
}
// Loop: body を探索(ネストループ)
ASTNode::Loop { body: loop_body, .. } => {
ASTNode::Loop {
body: loop_body, ..
} => {
for stmt in loop_body {
worklist.push(stmt);
}
@ -250,7 +261,11 @@ impl LoopBodyCarrierPromoter {
// Phase 171-impl-Trim: Handle Local with initial values
// local ch = s.substring(...)
ASTNode::Local { variables, initial_values, .. } if initial_values.len() == variables.len() => {
ASTNode::Local {
variables,
initial_values,
..
} if initial_values.len() == variables.len() => {
for (i, var) in variables.iter().enumerate() {
if var == var_name {
if let Some(Some(init_expr)) = initial_values.get(i) {
@ -303,7 +318,12 @@ impl LoopBodyCarrierPromoter {
while let Some(node) = worklist.pop() {
match node {
// BinaryOp: Or で分岐、Eq で比較
ASTNode::BinaryOp { operator, left, right, .. } => {
ASTNode::BinaryOp {
operator,
left,
right,
..
} => {
match operator {
// Or: 両側を探索
BinaryOperator::Or => {
@ -317,7 +337,11 @@ impl LoopBodyCarrierPromoter {
if let ASTNode::Variable { name, .. } = left.as_ref() {
if name == var_name {
// right が String リテラル
if let ASTNode::Literal { value: LiteralValue::String(s), .. } = right.as_ref() {
if let ASTNode::Literal {
value: LiteralValue::String(s),
..
} = right.as_ref()
{
result.push(s.clone());
}
}
@ -325,7 +349,11 @@ impl LoopBodyCarrierPromoter {
// right が Variable で var_name に一致(逆順)
if let ASTNode::Variable { name, .. } = right.as_ref() {
if name == var_name {
if let ASTNode::Literal { value: LiteralValue::String(s), .. } = left.as_ref() {
if let ASTNode::Literal {
value: LiteralValue::String(s),
..
} = left.as_ref()
{
result.push(s.clone());
}
}
@ -353,10 +381,10 @@ impl LoopBodyCarrierPromoter {
mod tests {
use super::*;
use crate::ast::Span;
use crate::mir::BasicBlockId;
use crate::mir::loop_pattern_detection::loop_condition_scope::{
CondVarScope, LoopConditionScope,
};
use crate::mir::BasicBlockId;
use std::collections::{BTreeMap, BTreeSet};
fn minimal_scope() -> LoopScopeShape {
@ -489,9 +517,7 @@ mod tests {
#[test]
fn test_find_definition_in_body_simple() {
// Test: local ch = s.substring(...)
let body = vec![
assignment("ch", method_call("s", "substring")),
];
let body = vec![assignment("ch", method_call("s", "substring"))];
let result = LoopBodyCarrierPromoter::find_definition_in_body(&body, "ch");
@ -507,20 +533,19 @@ mod tests {
#[test]
fn test_find_definition_in_body_nested_if() {
// Test: Definition inside if-else block
let body = vec![
ASTNode::If {
condition: Box::new(var_node("flag")),
then_body: vec![
assignment("ch", method_call("s", "substring")),
],
else_body: None,
span: Span::unknown(),
},
];
let body = vec![ASTNode::If {
condition: Box::new(var_node("flag")),
then_body: vec![assignment("ch", method_call("s", "substring"))],
else_body: None,
span: Span::unknown(),
}];
let result = LoopBodyCarrierPromoter::find_definition_in_body(&body, "ch");
assert!(result.is_some(), "Definition should be found inside if block");
assert!(
result.is_some(),
"Definition should be found inside if block"
);
}
#[test]
@ -528,8 +553,12 @@ mod tests {
let substring_call = method_call("s", "substring");
let other_call = method_call("s", "length");
assert!(LoopBodyCarrierPromoter::is_substring_method_call(&substring_call));
assert!(!LoopBodyCarrierPromoter::is_substring_method_call(&other_call));
assert!(LoopBodyCarrierPromoter::is_substring_method_call(
&substring_call
));
assert!(!LoopBodyCarrierPromoter::is_substring_method_call(
&other_call
));
}
#[test]
@ -547,10 +576,7 @@ mod tests {
fn test_extract_equality_literals_or_chain() {
// Test: ch == " " || ch == "\t" || ch == "\n"
let cond = or_expr(
or_expr(
eq_cmp("ch", " "),
eq_cmp("ch", "\t"),
),
or_expr(eq_cmp("ch", " "), eq_cmp("ch", "\t")),
eq_cmp("ch", "\n"),
);
@ -569,7 +595,10 @@ mod tests {
let result = LoopBodyCarrierPromoter::extract_equality_literals(&cond, "ch");
assert!(result.is_empty(), "Should not extract literals for wrong variable");
assert!(
result.is_empty(),
"Should not extract literals for wrong variable"
);
}
#[test]
@ -582,14 +611,9 @@ mod tests {
let scope = minimal_scope();
let cond_scope = cond_scope_with_body_local("ch");
let loop_body = vec![
assignment("ch", method_call("s", "substring")),
];
let loop_body = vec![assignment("ch", method_call("s", "substring"))];
let break_cond = or_expr(
eq_cmp("ch", " "),
eq_cmp("ch", "\t"),
);
let break_cond = or_expr(eq_cmp("ch", " "), eq_cmp("ch", "\t"));
let request = PromotionRequest {
scope: &scope,
@ -620,19 +644,11 @@ mod tests {
let scope = minimal_scope();
let cond_scope = cond_scope_with_body_local("ch");
let loop_body = vec![
assignment("ch", method_call("s", "substring")),
];
let loop_body = vec![assignment("ch", method_call("s", "substring"))];
let break_cond = or_expr(
or_expr(
eq_cmp("ch", " "),
eq_cmp("ch", "\t"),
),
or_expr(
eq_cmp("ch", "\n"),
eq_cmp("ch", "\r"),
),
or_expr(eq_cmp("ch", " "), eq_cmp("ch", "\t")),
or_expr(eq_cmp("ch", "\n"), eq_cmp("ch", "\r")),
);
let request = PromotionRequest {

View File

@ -149,7 +149,7 @@ impl LoopBodyCondPromoter {
/// - LoopBodyCondPromoter: Detection + metadata only (no code generation)
pub fn try_promote_for_condition(req: ConditionPromotionRequest) -> ConditionPromotionResult {
use crate::mir::loop_pattern_detection::loop_body_digitpos_promoter::{
DigitPosPromotionRequest, DigitPosPromotionResult, DigitPosPromoter,
DigitPosPromoter, DigitPosPromotionRequest, DigitPosPromotionResult,
};
use crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope;

View File

@ -187,19 +187,21 @@ impl DigitPosPromoter {
let bool_carrier_name = format!("is_{}", var_in_cond);
// Extract the base name for integer carrier (e.g., "digit_pos" → "digit")
let base_name = if var_in_cond.ends_with("_pos") {
&var_in_cond[..var_in_cond.len() - 4] // Remove "_pos" suffix
&var_in_cond[..var_in_cond.len() - 4] // Remove "_pos" suffix
} else {
var_in_cond.as_str()
};
let int_carrier_name = format!("{}_value", base_name);
use crate::mir::join_ir::lowering::carrier_info::{CarrierVar, CarrierRole, CarrierInit};
use crate::mir::join_ir::lowering::carrier_info::{
CarrierInit, CarrierRole, CarrierVar,
};
// Boolean carrier (condition-only, for break)
let promoted_carrier_bool = CarrierVar {
name: bool_carrier_name.clone(),
host_id: ValueId(0), // Placeholder (will be remapped)
join_id: None, // Will be allocated later
join_id: None, // Will be allocated later
role: CarrierRole::ConditionOnly, // Phase 227: DigitPos is condition-only
init: CarrierInit::BoolConst(false), // Phase 228: Initialize with false
};
@ -208,7 +210,7 @@ impl DigitPosPromoter {
let promoted_carrier_int = CarrierVar {
name: int_carrier_name.clone(),
host_id: ValueId(0), // Placeholder (loop-local; no host slot)
join_id: None, // Will be allocated later
join_id: None, // Will be allocated later
role: CarrierRole::LoopState, // Phase 247-EX: LoopState for accumulation
init: CarrierInit::LoopLocalZero, // Derived in-loop carrier (no host binding)
};
@ -216,13 +218,15 @@ impl DigitPosPromoter {
// Create CarrierInfo with a dummy loop_var_name (will be ignored during merge)
let mut carrier_info = CarrierInfo::with_carriers(
"__dummy_loop_var__".to_string(), // Placeholder, not used
ValueId(0), // Placeholder
ValueId(0), // Placeholder
vec![promoted_carrier_bool, promoted_carrier_int],
);
// Phase 229: Record promoted variable (no need for condition_aliases)
// Dynamic resolution uses promoted_loopbodylocals + naming convention
carrier_info.promoted_loopbodylocals.push(var_in_cond.clone());
carrier_info
.promoted_loopbodylocals
.push(var_in_cond.clone());
eprintln!(
"[digitpos_promoter] Phase 247-EX: A-4 DigitPos pattern promoted: {}{} (bool) + {} (i64)",
@ -335,9 +339,7 @@ impl DigitPosPromoter {
/// Handles: `if digit_pos < 0`, `if digit_pos >= 0`, etc.
fn extract_comparison_var(cond: &ASTNode) -> Option<String> {
match cond {
ASTNode::BinaryOp {
operator, left, ..
} => {
ASTNode::BinaryOp { operator, left, .. } => {
// Check if it's a comparison operator (not equality)
match operator {
BinaryOperator::Less

View File

@ -46,7 +46,9 @@ impl LoopConditionScope {
/// Check if this scope contains any loop-body-local variables
pub fn has_loop_body_local(&self) -> bool {
self.vars.iter().any(|v| v.scope == CondVarScope::LoopBodyLocal)
self.vars
.iter()
.any(|v| v.scope == CondVarScope::LoopBodyLocal)
}
/// Check if all variables in this scope are in the allowed set

View File

@ -19,8 +19,8 @@
//!
//! Reference: docs/private/roadmap2/phases/phase-188-joinir-loop-pattern-expansion/design.md
use crate::mir::loop_form::LoopForm;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::loop_form::LoopForm;
// ============================================================================
// Pattern Classification System (Phase 194+)
@ -142,7 +142,8 @@ pub struct LoopFeatures {
/// Contains UpdateKind (CounterLike/AccumulationLike/Other) for each carrier.
/// Used by CaseALoweringShape for more precise shape detection.
/// None if carrier names are not available.
pub update_summary: Option<crate::mir::join_ir::lowering::loop_update_summary::LoopUpdateSummary>,
pub update_summary:
Option<crate::mir::join_ir::lowering::loop_update_summary::LoopUpdateSummary>,
}
impl LoopFeatures {
@ -196,7 +197,10 @@ impl LoopFeatures {
///
/// # Returns
/// * `LoopFeatures` - Feature vector for pattern classification
pub(crate) fn extract_features(loop_form: &LoopForm, scope: Option<&LoopScopeShape>) -> LoopFeatures {
pub(crate) fn extract_features(
loop_form: &LoopForm,
scope: Option<&LoopScopeShape>,
) -> LoopFeatures {
// Phase 194: Basic feature extraction from LoopForm
let has_break = !loop_form.break_targets.is_empty();
let has_continue = !loop_form.continue_targets.is_empty();
@ -219,7 +223,9 @@ pub(crate) fn extract_features(loop_form: &LoopForm, scope: Option<&LoopScopeSha
// Note: carriers is BTreeSet<String>, so each item is already a String
let update_summary = scope.map(|s| {
let carrier_names: Vec<String> = s.carriers.iter().cloned().collect();
crate::mir::join_ir::lowering::loop_update_summary::analyze_loop_updates_by_name(&carrier_names)
crate::mir::join_ir::lowering::loop_update_summary::analyze_loop_updates_by_name(
&carrier_names,
)
});
LoopFeatures {
@ -275,7 +281,11 @@ pub fn classify(features: &LoopFeatures) -> LoopPatternKind {
// Pattern 3: If-PHI (check before Pattern 1)
// Phase 212.5: Structural if detection - route to P3 if has_if && carrier_count >= 1
if features.has_if && features.carrier_count >= 1 && !features.has_break && !features.has_continue {
if features.has_if
&& features.carrier_count >= 1
&& !features.has_break
&& !features.has_continue
{
return LoopPatternKind::Pattern3IfPhi;
}
@ -326,10 +336,7 @@ pub fn classify_with_diagnosis(features: &LoopFeatures) -> (LoopPatternKind, Str
"Simple while loop with no special control flow".to_string()
}
LoopPatternKind::Unknown => {
format!(
"Unknown pattern: {}",
features.debug_stats()
)
format!("Unknown pattern: {}", features.debug_stats())
}
};
@ -666,8 +673,8 @@ fn has_simple_condition(_loop_form: &LoopForm) -> bool {
mod tests;
// Phase 170-D: Loop Condition Scope Analysis Boxes
pub mod loop_condition_scope;
pub mod condition_var_analyzer;
pub mod loop_condition_scope;
// Phase 170-ultrathink: Error Message Utilities
pub mod error_messages;

View File

@ -40,8 +40,15 @@ fn assignment(target: ASTNode, value: ASTNode) -> ASTNode {
fn has_continue(node: &ASTNode) -> bool {
match node {
ASTNode::Continue { .. } => true,
ASTNode::If { then_body, else_body, .. } => {
then_body.iter().any(has_continue) || else_body.as_ref().map_or(false, |b| b.iter().any(has_continue))
ASTNode::If {
then_body,
else_body,
..
} => {
then_body.iter().any(has_continue)
|| else_body
.as_ref()
.map_or(false, |b| b.iter().any(has_continue))
}
ASTNode::Loop { body, .. } => body.iter().any(has_continue),
_ => false,
@ -51,8 +58,15 @@ fn has_continue(node: &ASTNode) -> bool {
fn has_break(node: &ASTNode) -> bool {
match node {
ASTNode::Break { .. } => true,
ASTNode::If { then_body, else_body, .. } => {
then_body.iter().any(has_break) || else_body.as_ref().map_or(false, |b| b.iter().any(has_break))
ASTNode::If {
then_body,
else_body,
..
} => {
then_body.iter().any(has_break)
|| else_body
.as_ref()
.map_or(false, |b| b.iter().any(has_break))
}
ASTNode::Loop { body, .. } => body.iter().any(has_break),
_ => false,
@ -69,7 +83,11 @@ fn carrier_count(body: &[ASTNode]) -> usize {
for n in nodes {
match n {
ASTNode::Assignment { .. } => c += 1,
ASTNode::If { then_body, else_body, .. } => {
ASTNode::If {
then_body,
else_body,
..
} => {
c += count(then_body);
if let Some(else_body) = else_body {
c += count(else_body);
@ -80,7 +98,11 @@ fn carrier_count(body: &[ASTNode]) -> usize {
}
c
}
if count(body) > 0 { 1 } else { 0 }
if count(body) > 0 {
1
} else {
0
}
}
fn classify_body(body: &[ASTNode]) -> LoopPatternKind {
@ -102,7 +124,10 @@ fn classify_body(body: &[ASTNode]) -> LoopPatternKind {
#[test]
fn pattern1_simple_while_is_detected() {
// loop(i < len) { i = i + 1 }
let body = vec![assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1)))];
let body = vec![assignment(
var("i"),
bin(BinaryOperator::Add, var("i"), lit_i(1)),
)];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern1SimpleWhile);
}
@ -211,7 +236,7 @@ fn test_atoi_loop_classified_as_pattern2() {
let mul_expr = bin(BinaryOperator::Multiply, var("result"), lit_i(10));
let result_update = assignment(
var("result"),
bin(BinaryOperator::Add, mul_expr, var("digit_pos"))
bin(BinaryOperator::Add, mul_expr, var("digit_pos")),
);
// i = i + 1
@ -229,6 +254,9 @@ fn test_atoi_loop_classified_as_pattern2() {
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern2Break,
"_atoi loop should be classified as Pattern2 (Break) due to if-break structure");
assert_eq!(
kind,
LoopPatternKind::Pattern2Break,
"_atoi loop should be classified as Pattern2 (Break) due to if-break structure"
);
}

View File

@ -275,7 +275,12 @@ mod tests {
let helper = TrimLoopHelper {
original_var: "ch".to_string(),
carrier_name: "is_whitespace".to_string(),
whitespace_chars: vec![" ".to_string(), "\t".to_string(), "\n".to_string(), "\r".to_string()],
whitespace_chars: vec![
" ".to_string(),
"\t".to_string(),
"\n".to_string(),
"\r".to_string(),
],
};
assert_eq!(helper.whitespace_count(), 4);