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> {
|
||||
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);
|
||||
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(
|
||||
|
||||
@ -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
|
||||
//! - MIR block merging (merge/) ✅ Phase 4
|
||||
//! - 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 merge;
|
||||
pub(in crate::mir::builder) mod parity_checker;
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
mod 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