//! Phase 224-B: MethodCall Lowering Box //! //! This box provides metadata-driven lowering of MethodCall AST nodes to JoinIR. //! //! ## Design Philosophy //! //! **Box-First Design**: MethodCallLowerer is a single-responsibility box that //! answers one question: "Can this MethodCall be lowered to JoinIR, and if so, how?" //! //! **Metadata-Driven**: Uses CoreMethodId metadata exclusively - NO method name hardcoding. //! All decisions based on `is_pure()`, `allowed_in_condition()`, `allowed_in_init()`. //! //! **Fail-Fast**: If a method is not whitelisted, immediately returns Err. //! No silent fallbacks or guessing. //! //! ## Supported Contexts //! //! - **Condition context**: Methods allowed in loop conditions (e.g., `s.length()`) //! - **Init context**: Methods allowed in LoopBodyLocal initialization (e.g., `s.substring(0, 1)`) //! //! ## Example Usage //! //! ```ignore //! // Loop condition: loop(i < s.length()) //! let recv_val = ValueId(0); // 's' //! let result = MethodCallLowerer::lower_for_condition( //! recv_val, //! "length", //! &[], //! &mut alloc_value, //! &mut instructions, //! )?; //! // Result: BoxCall instruction emitted, returns result ValueId //! ``` use crate::ast::ASTNode; use crate::mir::join_ir::{JoinInst, MirLikeInst}; use crate::mir::ValueId; use crate::runtime::core_box_ids::CoreMethodId; /// Phase 224-B: MethodCall Lowerer Box /// /// Provides metadata-driven lowering of MethodCall AST nodes to JoinIR instructions. pub struct MethodCallLowerer; impl MethodCallLowerer { /// Lower a MethodCall for use in loop condition expressions /// /// # Arguments /// /// * `recv_val` - Receiver ValueId (already lowered) /// * `method_name` - Method name from AST (e.g., "length") /// * `args` - Argument AST nodes (not yet supported in P0) /// * `alloc_value` - ValueId allocator function /// * `instructions` - Instruction buffer to append to /// /// # Returns /// /// * `Ok(ValueId)` - Result of method call /// * `Err(String)` - If method not found or not allowed in condition /// /// # Phase 224-B P0 Restrictions /// /// - Only zero-argument methods supported (e.g., `s.length()`) /// - Only whitelisted methods (StringLength, ArrayLength, etc.) /// - Arguments must be empty slice /// /// # Example /// /// ```ignore /// // Loop condition: loop(i < s.length()) /// let recv_val = env.get("s").unwrap(); /// let result = MethodCallLowerer::lower_for_condition( /// recv_val, /// "length", /// &[], /// &mut alloc_value, /// &mut instructions, /// )?; /// ``` pub fn lower_for_condition( recv_val: ValueId, method_name: &str, args: &[ASTNode], alloc_value: &mut F, instructions: &mut Vec, ) -> Result where F: FnMut() -> ValueId, { // Phase 224-B P0: Only zero-argument methods if !args.is_empty() { return Err(format!( "Phase 224-B P0: MethodCall with arguments not supported yet: {}.{}({} args)", recv_val.0, method_name, args.len() )); } // Resolve method name to CoreMethodId // Note: We don't know receiver type at this point, so we try all methods let method_id = CoreMethodId::iter() .find(|m| m.name() == method_name) .ok_or_else(|| { format!( "MethodCall not recognized as CoreMethodId: {}.{}()", recv_val.0, method_name ) })?; // Check if allowed in condition context if !method_id.allowed_in_condition() { return Err(format!( "MethodCall not allowed in loop condition: {}.{}() (not whitelisted)", recv_val.0, method_name )); } // Emit BoxCall instruction let dst = alloc_value(); let box_name = method_id.box_id().name().to_string(); instructions.push(JoinInst::Compute(MirLikeInst::BoxCall { dst: Some(dst), box_name, method: method_name.to_string(), args: vec![recv_val], // First arg is the receiver })); Ok(dst) } /// Lower a MethodCall for use in LoopBodyLocal initialization /// /// Similar to `lower_for_condition` but uses `allowed_in_init()` whitelist. /// More permissive - allows methods like `substring`, `indexOf`, etc. /// /// # Phase 224-B P0 Restrictions /// /// - Only zero-argument methods supported /// - Arguments will be supported in Phase 224-C pub fn lower_for_init( recv_val: ValueId, method_name: &str, args: &[ASTNode], alloc_value: &mut F, instructions: &mut Vec, ) -> Result where F: FnMut() -> ValueId, { // Phase 224-B P0: Only zero-argument methods if !args.is_empty() { return Err(format!( "Phase 224-B P0: MethodCall with arguments not supported yet: {}.{}({} args)", recv_val.0, method_name, args.len() )); } // Resolve method name to CoreMethodId let method_id = CoreMethodId::iter() .find(|m| m.name() == method_name) .ok_or_else(|| { format!( "MethodCall not recognized as CoreMethodId: {}.{}()", recv_val.0, method_name ) })?; // Check if allowed in init context if !method_id.allowed_in_init() { return Err(format!( "MethodCall not allowed in LoopBodyLocal init: {}.{}() (not whitelisted)", recv_val.0, method_name )); } // Emit BoxCall instruction let dst = alloc_value(); let box_name = method_id.box_id().name().to_string(); instructions.push(JoinInst::Compute(MirLikeInst::BoxCall { dst: Some(dst), box_name, method: method_name.to_string(), args: vec![recv_val], // First arg is the receiver })); Ok(dst) } } #[cfg(test)] mod tests { use super::*; use crate::mir::join_ir::JoinInst; #[test] fn test_resolve_string_length() { // Test: "length" → CoreMethodId::StringLength let method_id = CoreMethodId::iter().find(|m| m.name() == "length"); assert!(method_id.is_some()); assert!(method_id.unwrap().allowed_in_condition()); } #[test] fn test_lower_string_length_for_condition() { // Test: s.length() in loop condition let recv_val = ValueId(10); let mut value_counter = 100u32; let mut alloc_value = || { let id = ValueId(value_counter); value_counter += 1; id }; let mut instructions = Vec::new(); let result = MethodCallLowerer::lower_for_condition( recv_val, "length", &[], &mut alloc_value, &mut instructions, ); assert!(result.is_ok()); let result_val = result.unwrap(); assert_eq!(result_val, ValueId(100)); assert_eq!(instructions.len(), 1); match &instructions[0] { JoinInst::Compute(MirLikeInst::BoxCall { dst, box_name, method, args, }) => { assert_eq!(*dst, Some(ValueId(100))); assert_eq!(box_name, "StringBox"); assert_eq!(method, "length"); assert_eq!(args.len(), 1); // Receiver is first arg assert_eq!(args[0], ValueId(10)); } _ => panic!("Expected BoxCall instruction"), } } #[test] fn test_not_allowed_in_condition() { // Test: s.upper() not whitelisted for conditions let recv_val = ValueId(10); let mut value_counter = 100u32; let mut alloc_value = || { let id = ValueId(value_counter); value_counter += 1; id }; let mut instructions = Vec::new(); let result = MethodCallLowerer::lower_for_condition( recv_val, "upper", &[], &mut alloc_value, &mut instructions, ); assert!(result.is_err()); assert!(result.unwrap_err().contains("not allowed in loop condition")); } #[test] fn test_unknown_method() { // Test: s.unknownMethod() not in CoreMethodId let recv_val = ValueId(10); let mut value_counter = 100u32; let mut alloc_value = || { let id = ValueId(value_counter); value_counter += 1; id }; let mut instructions = Vec::new(); let result = MethodCallLowerer::lower_for_condition( recv_val, "unknownMethod", &[], &mut alloc_value, &mut instructions, ); assert!(result.is_err()); assert!(result .unwrap_err() .contains("not recognized as CoreMethodId")); } #[test] fn test_lower_substring_for_init() { // Test: s.substring() in LoopBodyLocal init (allowed in init, not condition) let recv_val = ValueId(10); let mut value_counter = 100u32; let mut alloc_value = || { let id = ValueId(value_counter); value_counter += 1; id }; let mut instructions = Vec::new(); // substring is allowed in init but NOT in condition let cond_result = MethodCallLowerer::lower_for_condition( recv_val, "substring", &[], &mut alloc_value, &mut instructions, ); assert!(cond_result.is_err()); // But allowed in init context instructions.clear(); let init_result = MethodCallLowerer::lower_for_init( recv_val, "substring", &[], &mut alloc_value, &mut instructions, ); assert!(init_result.is_ok()); assert_eq!(instructions.len(), 1); } #[test] fn test_phase224b_p0_no_arguments() { // Test: P0 restriction - no arguments supported yet let recv_val = ValueId(10); let mut value_counter = 100u32; let mut alloc_value = || { let id = ValueId(value_counter); value_counter += 1; id }; let mut instructions = Vec::new(); // Create dummy argument let dummy_arg = ASTNode::Literal { value: crate::ast::LiteralValue::Integer(1), span: crate::ast::Span::unknown(), }; let result = MethodCallLowerer::lower_for_condition( recv_val, "length", &[dummy_arg], &mut alloc_value, &mut instructions, ); assert!(result.is_err()); assert!(result.unwrap_err().contains("P0: MethodCall with arguments not supported")); } }