feat(joinir): Phase 112 strict guard for StepTree required_caps
- Add control_tree_capability_guard.rs with check(tree, func_name, strict, dev) - Allowlist: If, NestedIf, Loop, Return, Break, Continue - Deny (strict): NestedLoop, TryCatch, Throw, Lambda, While, ForRange, Match, Arrow - Wire into lower_function_body() (strict-only check) - Error format: [joinir/control_tree/cap_missing/<Cap>] with 1-line Hint - Unit tests: nested_loop_rejects, if_only_passes, strict_false_passes - Default behavior unchanged (strict=false always Ok) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -176,9 +176,28 @@ impl MirBuilder {
|
|||||||
fn lower_function_body(&mut self, body: Vec<ASTNode>) -> Result<(), String> {
|
fn lower_function_body(&mut self, body: Vec<ASTNode>) -> Result<(), String> {
|
||||||
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
|
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
|
||||||
|
|
||||||
if crate::config::env::joinir_dev_enabled() {
|
// Phase 112: StepTree capability guard (strict-only)
|
||||||
|
let strict = crate::config::env::joinir_dev::strict_enabled();
|
||||||
|
let dev = crate::config::env::joinir_dev_enabled();
|
||||||
|
|
||||||
|
if strict || dev {
|
||||||
let tree = crate::mir::control_tree::StepTreeBuilderBox::build_from_block(&body);
|
let tree = crate::mir::control_tree::StepTreeBuilderBox::build_from_block(&body);
|
||||||
trace.dev("control_tree/step_tree", &tree.to_compact_string());
|
|
||||||
|
if dev {
|
||||||
|
trace.dev("control_tree/step_tree", &tree.to_compact_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 112: Guard check (strict mode only)
|
||||||
|
let func_name = self
|
||||||
|
.scope_ctx
|
||||||
|
.current_function
|
||||||
|
.as_ref()
|
||||||
|
.map(|f| f.signature.name.clone())
|
||||||
|
.unwrap_or_else(|| "<unknown>".to_string());
|
||||||
|
|
||||||
|
crate::mir::builder::control_flow::joinir::control_tree_capability_guard::check(
|
||||||
|
&tree, &func_name, strict, dev,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
trace.emit_if(
|
trace.emit_if(
|
||||||
|
|||||||
@ -0,0 +1,200 @@
|
|||||||
|
//! Phase 112: StepTree Capability Guard (strict-only)
|
||||||
|
//!
|
||||||
|
//! ## Purpose
|
||||||
|
//! Detects unsupported capabilities in StepTree required_caps during strict mode.
|
||||||
|
//! Provides actionable 1-line hints for developers to fix their code.
|
||||||
|
//!
|
||||||
|
//! ## Design
|
||||||
|
//! - **Allowlist**: If, NestedIf, Loop, Return, Break, Continue
|
||||||
|
//! - **Deny (strict)**: NestedLoop, TryCatch, Throw, Lambda, While, ForRange, Match, Arrow
|
||||||
|
//! - **Default behavior unchanged**: strict=false always returns Ok(())
|
||||||
|
//!
|
||||||
|
//! ## Integration
|
||||||
|
//! - Called from `lower_function_body()` in `calls/lowering.rs`
|
||||||
|
//! - Uses existing `HAKO_JOINIR_STRICT` / `NYASH_JOINIR_STRICT` env vars
|
||||||
|
//! - Error format: `[joinir/control_tree/cap_missing/<Cap>] <msg> Hint: <hint>`
|
||||||
|
|
||||||
|
use crate::mir::control_tree::{StepCapability, StepTree};
|
||||||
|
use crate::mir::join_ir::lowering::error_tags;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
/// Check StepTree capabilities against allowlist (Phase 112)
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - `tree`: StepTree to check
|
||||||
|
/// - `func_name`: Function name for error messages
|
||||||
|
/// - `strict`: If false, always returns Ok (default behavior unchanged)
|
||||||
|
/// - `dev`: If true, trace missing caps (debug info)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - Ok(()) if all required_caps are allowed (or strict=false)
|
||||||
|
/// - Err with freeze_with_hint format if unsupported cap found in strict mode
|
||||||
|
pub fn check(tree: &StepTree, func_name: &str, strict: bool, dev: bool) -> Result<(), String> {
|
||||||
|
if !strict {
|
||||||
|
return Ok(()); // Default behavior: always pass
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allowlist (supported capabilities)
|
||||||
|
let allowed: BTreeSet<StepCapability> = [
|
||||||
|
StepCapability::If,
|
||||||
|
StepCapability::NestedIf,
|
||||||
|
StepCapability::Loop,
|
||||||
|
StepCapability::Return,
|
||||||
|
StepCapability::Break,
|
||||||
|
StepCapability::Continue,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Check for unsupported caps
|
||||||
|
for cap in &tree.contract.required_caps {
|
||||||
|
if !allowed.contains(cap) {
|
||||||
|
let cap_name = format!("{:?}", cap);
|
||||||
|
let tag = format!("control_tree/cap_missing/{}", cap_name);
|
||||||
|
let msg = format!(
|
||||||
|
"{} detected in '{}' (step_tree_sig={})",
|
||||||
|
cap_name,
|
||||||
|
func_name,
|
||||||
|
tree.signature.to_hex()
|
||||||
|
);
|
||||||
|
let hint = get_hint_for_cap(cap);
|
||||||
|
|
||||||
|
if dev {
|
||||||
|
// Trace for debugging
|
||||||
|
eprintln!(
|
||||||
|
"[joinir/control_tree] missing cap: {} in {}",
|
||||||
|
cap_name, func_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(error_tags::freeze_with_hint(&tag, &msg, &hint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_hint_for_cap(cap: &StepCapability) -> String {
|
||||||
|
match cap {
|
||||||
|
StepCapability::NestedLoop => {
|
||||||
|
"refactor to avoid nested loops (not supported yet) or run without HAKO_JOINIR_STRICT=1".to_string()
|
||||||
|
}
|
||||||
|
StepCapability::TryCatch => {
|
||||||
|
"try/catch not supported in JoinIR yet, use HAKO_JOINIR_STRICT=0".to_string()
|
||||||
|
}
|
||||||
|
StepCapability::Throw => {
|
||||||
|
"throw not supported in JoinIR yet, use HAKO_JOINIR_STRICT=0".to_string()
|
||||||
|
}
|
||||||
|
StepCapability::Lambda => {
|
||||||
|
"lambda not supported in JoinIR yet, extract to named function".to_string()
|
||||||
|
}
|
||||||
|
StepCapability::While => {
|
||||||
|
"use 'loop(cond)' instead of 'while(cond)' syntax".to_string()
|
||||||
|
}
|
||||||
|
StepCapability::ForRange => {
|
||||||
|
"use 'loop(i < n)' instead of 'for i in range' syntax".to_string()
|
||||||
|
}
|
||||||
|
StepCapability::Match => {
|
||||||
|
"match expressions not supported in JoinIR yet, use if-else chain".to_string()
|
||||||
|
}
|
||||||
|
StepCapability::Arrow => {
|
||||||
|
"arrow functions not supported in JoinIR yet, use regular functions".to_string()
|
||||||
|
}
|
||||||
|
_ => format!("{:?} not supported in JoinIR yet", cap),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
|
||||||
|
use crate::mir::control_tree::StepTreeBuilderBox;
|
||||||
|
|
||||||
|
fn var(name: &str) -> ASTNode {
|
||||||
|
ASTNode::Variable {
|
||||||
|
name: name.to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn int_lit(v: i64) -> ASTNode {
|
||||||
|
ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(v),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bin_lt(lhs: ASTNode, rhs: ASTNode) -> ASTNode {
|
||||||
|
ASTNode::BinaryOp {
|
||||||
|
operator: BinaryOperator::Less,
|
||||||
|
left: Box::new(lhs),
|
||||||
|
right: Box::new(rhs),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nested_loop_strict_rejects() {
|
||||||
|
// AST: loop(i < 3) { loop(j < 2) { ... } }
|
||||||
|
let nested_loop_ast = vec![ASTNode::Loop {
|
||||||
|
condition: Box::new(bin_lt(var("i"), int_lit(3))),
|
||||||
|
body: vec![ASTNode::Loop {
|
||||||
|
condition: Box::new(bin_lt(var("j"), int_lit(2))),
|
||||||
|
body: vec![],
|
||||||
|
span: Span::unknown(),
|
||||||
|
}],
|
||||||
|
span: Span::unknown(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
let tree = StepTreeBuilderBox::build_from_block(&nested_loop_ast);
|
||||||
|
|
||||||
|
// strict=true should reject NestedLoop
|
||||||
|
let result = check(&tree, "test_func", true, false);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert!(err.contains("[joinir/control_tree/cap_missing/NestedLoop]"));
|
||||||
|
assert!(err.contains("Hint:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_if_only_strict_passes() {
|
||||||
|
// AST: if x == 1 { ... } else { ... }
|
||||||
|
let if_only_ast = vec![ASTNode::If {
|
||||||
|
condition: Box::new(ASTNode::BinaryOp {
|
||||||
|
operator: BinaryOperator::Equal,
|
||||||
|
left: Box::new(var("x")),
|
||||||
|
right: Box::new(int_lit(1)),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
then_body: vec![],
|
||||||
|
else_body: Some(vec![]),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
let tree = StepTreeBuilderBox::build_from_block(&if_only_ast);
|
||||||
|
|
||||||
|
// strict=true should pass (If is allowed)
|
||||||
|
let result = check(&tree, "test_func", true, false);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_strict_false_always_passes() {
|
||||||
|
// Even with NestedLoop, strict=false should pass
|
||||||
|
let nested_loop_ast = vec![ASTNode::Loop {
|
||||||
|
condition: Box::new(bin_lt(var("i"), int_lit(3))),
|
||||||
|
body: vec![ASTNode::Loop {
|
||||||
|
condition: Box::new(bin_lt(var("j"), int_lit(2))),
|
||||||
|
body: vec![],
|
||||||
|
span: Span::unknown(),
|
||||||
|
}],
|
||||||
|
span: Span::unknown(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
let tree = StepTreeBuilderBox::build_from_block(&nested_loop_ast);
|
||||||
|
|
||||||
|
// strict=false should always pass
|
||||||
|
let result = check(&tree, "test_func", false, false);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,7 +7,9 @@
|
|||||||
//! - Loop processing context (loop_context.rs) ✅ Phase 140-P5
|
//! - Loop processing context (loop_context.rs) ✅ Phase 140-P5
|
||||||
//! - MIR block merging (merge/) ✅ Phase 4
|
//! - MIR block merging (merge/) ✅ Phase 4
|
||||||
//! - Unified tracing (trace.rs) ✅ Phase 195
|
//! - Unified tracing (trace.rs) ✅ Phase 195
|
||||||
|
//! - Control tree capability guard (control_tree_capability_guard.rs) ✅ Phase 112
|
||||||
|
|
||||||
|
pub(in crate::mir::builder) mod control_tree_capability_guard;
|
||||||
pub(in crate::mir::builder) mod loop_context;
|
pub(in crate::mir::builder) mod loop_context;
|
||||||
pub(in crate::mir::builder) mod merge;
|
pub(in crate::mir::builder) mod merge;
|
||||||
pub(in crate::mir::builder) mod parity_checker;
|
pub(in crate::mir::builder) mod parity_checker;
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
mod step_tree;
|
mod step_tree;
|
||||||
|
|
||||||
pub use step_tree::{
|
pub use step_tree::{
|
||||||
AstSummary, StepNode, StepStmtKind, StepTree, StepTreeBuilderBox, StepTreeFeatures,
|
AstSummary, ExitKind, StepCapability, StepNode, StepStmtKind, StepTree, StepTreeBuilderBox,
|
||||||
|
StepTreeContract, StepTreeFeatures, StepTreeSignature,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user