diff --git a/src/mir/builder/calls/lowering.rs b/src/mir/builder/calls/lowering.rs index 2cc2f541..707f25c4 100644 --- a/src/mir/builder/calls/lowering.rs +++ b/src/mir/builder/calls/lowering.rs @@ -176,9 +176,28 @@ impl MirBuilder { fn lower_function_body(&mut self, body: Vec) -> 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(|| "".to_string()); + + crate::mir::builder::control_flow::joinir::control_tree_capability_guard::check( + &tree, &func_name, strict, dev, + )?; } trace.emit_if( diff --git a/src/mir/builder/control_flow/joinir/control_tree_capability_guard.rs b/src/mir/builder/control_flow/joinir/control_tree_capability_guard.rs new file mode 100644 index 00000000..4156509e --- /dev/null +++ b/src/mir/builder/control_flow/joinir/control_tree_capability_guard.rs @@ -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/] 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::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()); + } +} diff --git a/src/mir/builder/control_flow/joinir/mod.rs b/src/mir/builder/control_flow/joinir/mod.rs index 050a9614..6bb1c80f 100644 --- a/src/mir/builder/control_flow/joinir/mod.rs +++ b/src/mir/builder/control_flow/joinir/mod.rs @@ -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; diff --git a/src/mir/control_tree/mod.rs b/src/mir/control_tree/mod.rs index fa5eed40..85be1f87 100644 --- a/src/mir/control_tree/mod.rs +++ b/src/mir/control_tree/mod.rs @@ -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, };