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,9 +17,19 @@ 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;
let summary = match &decision {
PolicyDecision::Use(_) => "Use".to_string(),
PolicyDecision::Reject(reason) => format!("Reject: {}", reason),
PolicyDecision::None => "None".to_string(),
};
trace::trace().dev("phase107/balanced_depth_scan_policy", &summary);
}
match decision {
PolicyDecision::Use(result) => {
return Ok(Pattern2Inputs { return Ok(Pattern2Inputs {
loop_var_name: facts.loop_var_name, loop_var_name: facts.loop_var_name,
loop_var_id: facts.loop_var_id, loop_var_id: facts.loop_var_id,
@ -41,6 +51,11 @@ impl ApplyPolicyStepBox {
post_loop_early_return: Some(result.post_loop_early_return), 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,12 +24,19 @@ 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
let has_continue = ast_features::detect_continue_in_body(body); let has_continue = ast_features::detect_continue_in_body(body);