fix(joinir): wire balanced depth-scan policy through Pattern2
This commit is contained in:
@ -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)?;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user