fix(joinir): wire balanced depth-scan policy through Pattern2

This commit is contained in:
nyash-codex
2025-12-17 22:47:36 +09:00
parent 9ec2e28b6a
commit d8ce9fdb99
4 changed files with 121 additions and 29 deletions

View File

@ -17,29 +17,44 @@ impl ApplyPolicyStepBox {
// Phase 107: balanced depth-scan (return-in-loop) policy. // Phase 107: balanced depth-scan (return-in-loop) policy.
// This route provides its own break-cond + derived recipe + post-loop early return plan. // This route provides its own break-cond + derived recipe + post-loop early return plan.
if let PolicyDecision::Use(result) = let decision = balanced_depth_scan_policy::classify_balanced_depth_scan_array_end(condition, body);
balanced_depth_scan_policy::classify_balanced_depth_scan_array_end(condition, body) if crate::config::env::joinir_dev_enabled() {
{ use crate::mir::builder::control_flow::joinir::trace;
return Ok(Pattern2Inputs { let summary = match &decision {
loop_var_name: facts.loop_var_name, PolicyDecision::Use(_) => "Use".to_string(),
loop_var_id: facts.loop_var_id, PolicyDecision::Reject(reason) => format!("Reject: {}", reason),
carrier_info: facts.carrier_info, PolicyDecision::None => "None".to_string(),
scope: facts.scope, };
captured_env: facts.captured_env, trace::trace().dev("phase107/balanced_depth_scan_policy", &summary);
join_value_space: facts.join_value_space, }
env: facts.env,
condition_bindings: facts.condition_bindings, match decision {
body_local_env: facts.body_local_env, PolicyDecision::Use(result) => {
allowed_body_locals_for_conditions: result.allowed_body_locals_for_conditions, return Ok(Pattern2Inputs {
read_only_body_local_slot: None, loop_var_name: facts.loop_var_name,
break_condition_node: result.break_condition_node, loop_var_id: facts.loop_var_id,
is_loop_true_read_digits: false, carrier_info: facts.carrier_info,
condition_only_recipe: None, scope: facts.scope,
body_local_derived_recipe: None, captured_env: facts.captured_env,
balanced_depth_scan_recipe: Some(result.derived_recipe), join_value_space: facts.join_value_space,
carrier_updates_override: Some(result.carrier_updates_override), env: facts.env,
post_loop_early_return: Some(result.post_loop_early_return), condition_bindings: facts.condition_bindings,
}); body_local_env: facts.body_local_env,
allowed_body_locals_for_conditions: result.allowed_body_locals_for_conditions,
read_only_body_local_slot: None,
break_condition_node: result.break_condition_node,
is_loop_true_read_digits: false,
condition_only_recipe: None,
body_local_derived_recipe: None,
balanced_depth_scan_recipe: Some(result.derived_recipe),
carrier_updates_override: Some(result.carrier_updates_override),
post_loop_early_return: Some(result.post_loop_early_return),
});
}
PolicyDecision::Reject(reason) => {
return Err(reason);
}
PolicyDecision::None => {}
} }
let break_routing = Pattern2BreakConditionPolicyRouterBox::route(condition, body)?; let break_routing = Pattern2BreakConditionPolicyRouterBox::route(condition, body)?;

View File

@ -166,7 +166,11 @@ impl PromoteStepBox {
.collect(); .collect();
if cond_scope.has_loop_body_local() { if cond_scope.has_loop_body_local() {
if !inputs.is_loop_true_read_digits { // Phase 107: balanced depth-scan policy already provides an allow-list + derived recipe.
// Do not re-run Pattern2 body-local promotion/slot heuristics here (they assume break-guard shapes).
if inputs.balanced_depth_scan_recipe.is_some() {
// no-op: LoopBodyLocalInitLowerer + BalancedDepthScanEmitter will populate the env.
} else if !inputs.is_loop_true_read_digits {
match classify_for_pattern2( match classify_for_pattern2(
builder, builder,
&inputs.loop_var_name, &inputs.loop_var_name,

View File

@ -476,6 +476,7 @@ fn eq_int(left: ASTNode, n: i64) -> ASTNode {
mod tests { mod tests {
use super::*; use super::*;
use crate::ast::Span; use crate::ast::Span;
use crate::parser::NyashParser;
fn span() -> Span { fn span() -> Span {
Span::unknown() Span::unknown()
@ -520,6 +521,46 @@ mod tests {
} }
} }
fn find_first_loop<'a>(node: &'a ASTNode) -> Option<(&'a ASTNode, &'a [ASTNode])> {
match node {
ASTNode::Loop { condition, body, .. } => Some((condition.as_ref(), body.as_slice())),
ASTNode::Program { statements, .. } => statements.iter().find_map(find_first_loop),
ASTNode::BoxDeclaration {
methods,
constructors,
static_init,
..
} => {
for v in methods.values() {
if let Some(found) = find_first_loop(v) {
return Some(found);
}
}
for v in constructors.values() {
if let Some(found) = find_first_loop(v) {
return Some(found);
}
}
if let Some(init) = static_init {
if let Some(found) = init.iter().find_map(find_first_loop) {
return Some(found);
}
}
None
}
ASTNode::FunctionDeclaration { body, .. } => body.iter().find_map(find_first_loop),
ASTNode::If {
then_body,
else_body,
..
} => then_body
.iter()
.find_map(find_first_loop)
.or_else(|| else_body.as_ref()?.iter().find_map(find_first_loop)),
_ => None,
}
}
#[test] #[test]
fn detects_balanced_array_end_min_shape() { fn detects_balanced_array_end_min_shape() {
// loop(i < n) { // loop(i < n) {
@ -584,4 +625,29 @@ mod tests {
assert!(result.carrier_updates_override.contains_key("i")); assert!(result.carrier_updates_override.contains_key("i"));
assert!(result.carrier_updates_override.contains_key("depth")); assert!(result.carrier_updates_override.contains_key("depth"));
} }
#[test]
fn detects_balanced_array_end_min_shape_from_parser_ast() {
let src = r#"
static box Main {
f(s, idx) {
local n = s.length()
if s.substring(idx, idx+1) != "[" { return -1 }
local depth = 0
local i = idx
loop (i < n) {
local ch = s.substring(i, i+1)
if ch == "[" { depth = depth + 1 }
if ch == "]" { depth = depth - 1 if depth == 0 { return i } }
i = i + 1
}
return -1
}
}
"#;
let ast = NyashParser::parse_from_string(src).expect("parse ok");
let (condition, body) = find_first_loop(&ast).expect("find loop");
let decision = classify_balanced_depth_scan_array_end(condition, body);
assert!(matches!(decision, PolicyDecision::Use(_)), "got {:?}", decision);
}
} }

View File

@ -24,11 +24,18 @@ pub(in crate::mir::builder) fn choose_pattern_kind(
// Phase 107: Route balanced depth-scan (return-in-loop) to Pattern2 via policy. // Phase 107: Route balanced depth-scan (return-in-loop) to Pattern2 via policy.
// //
// This keeps Pattern routing structural: no by-name dispatch, no silent fallback. // This keeps Pattern routing structural: no by-name dispatch, no silent fallback.
if matches!( match classify_balanced_depth_scan_array_end(condition, body) {
classify_balanced_depth_scan_array_end(condition, body), PolicyDecision::Use(_) => {
PolicyDecision::Use(_) return loop_pattern_detection::LoopPatternKind::Pattern2Break;
) { }
return loop_pattern_detection::LoopPatternKind::Pattern2Break; PolicyDecision::Reject(_reason) => {
// In strict mode, treat "close-but-unsupported" as a fail-fast
// Pattern2 route so the policy can surface the precise contract violation.
if crate::config::env::joinir_dev::strict_enabled() {
return loop_pattern_detection::LoopPatternKind::Pattern2Break;
}
}
PolicyDecision::None => {}
} }
// Phase 193: Use AST Feature Extractor Box for break/continue detection // Phase 193: Use AST Feature Extractor Box for break/continue detection