fix(joinir): wire balanced depth-scan policy through Pattern2
This commit is contained in:
@ -17,9 +17,19 @@ impl ApplyPolicyStepBox {
|
||||
|
||||
// Phase 107: balanced depth-scan (return-in-loop) policy.
|
||||
// This route provides its own break-cond + derived recipe + post-loop early return plan.
|
||||
if let PolicyDecision::Use(result) =
|
||||
balanced_depth_scan_policy::classify_balanced_depth_scan_array_end(condition, body)
|
||||
{
|
||||
let decision = 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 {
|
||||
loop_var_name: facts.loop_var_name,
|
||||
loop_var_id: facts.loop_var_id,
|
||||
@ -41,6 +51,11 @@ impl ApplyPolicyStepBox {
|
||||
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)?;
|
||||
|
||||
|
||||
@ -166,7 +166,11 @@ impl PromoteStepBox {
|
||||
.collect();
|
||||
|
||||
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(
|
||||
builder,
|
||||
&inputs.loop_var_name,
|
||||
|
||||
@ -476,6 +476,7 @@ fn eq_int(left: ASTNode, n: i64) -> ASTNode {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::Span;
|
||||
use crate::parser::NyashParser;
|
||||
|
||||
fn span() -> Span {
|
||||
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]
|
||||
fn detects_balanced_array_end_min_shape() {
|
||||
// loop(i < n) {
|
||||
@ -584,4 +625,29 @@ mod tests {
|
||||
assert!(result.carrier_updates_override.contains_key("i"));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
//
|
||||
// This keeps Pattern routing structural: no by-name dispatch, no silent fallback.
|
||||
if matches!(
|
||||
classify_balanced_depth_scan_array_end(condition, body),
|
||||
PolicyDecision::Use(_)
|
||||
) {
|
||||
match classify_balanced_depth_scan_array_end(condition, body) {
|
||||
PolicyDecision::Use(_) => {
|
||||
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
|
||||
let has_continue = ast_features::detect_continue_in_body(body);
|
||||
|
||||
Reference in New Issue
Block a user