From 8e3b55ddeca8c851fa9e0156b2b00007d1745aa2 Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Wed, 10 Dec 2025 18:19:14 +0900 Subject: [PATCH] feat(joinir): Phase 224-C - MethodCallLowerer with argument support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add StringIndexOf to CoreMethodId (arity=1, return IntegerBox) - Extend MethodCallLowerer to handle methods with arguments - Add arity checking against CoreMethodId metadata - Recursive argument lowering via lower_value_expression - 8 unit tests PASS (indexOf, substring, arity mismatch) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/mir/join_ir/lowering/condition_lowerer.rs | 6 +- .../join_ir/lowering/method_call_lowerer.rs | 259 +++++++++++++++--- src/runtime/core_box_ids.rs | 22 +- 3 files changed, 237 insertions(+), 50 deletions(-) diff --git a/src/mir/join_ir/lowering/condition_lowerer.rs b/src/mir/join_ir/lowering/condition_lowerer.rs index d13e7851..b8d7840f 100644 --- a/src/mir/join_ir/lowering/condition_lowerer.rs +++ b/src/mir/join_ir/lowering/condition_lowerer.rs @@ -306,7 +306,7 @@ where .. } => lower_arithmetic_binop(operator, left, right, alloc_value, env, instructions), - // Phase 224-B: MethodCall support (e.g., s.length()) + // Phase 224-C: MethodCall support with arguments (e.g., s.length(), s.indexOf(ch)) ASTNode::MethodCall { object, method, @@ -316,8 +316,8 @@ where // 1. Lower receiver (object) to ValueId let recv_val = lower_value_expression(object, alloc_value, env, instructions)?; - // 2. Lower method call using MethodCallLowerer - MethodCallLowerer::lower_for_condition(recv_val, method, arguments, alloc_value, instructions) + // 2. Lower method call using MethodCallLowerer (will lower arguments internally) + MethodCallLowerer::lower_for_condition(recv_val, method, arguments, alloc_value, env, instructions) } _ => Err(format!( diff --git a/src/mir/join_ir/lowering/method_call_lowerer.rs b/src/mir/join_ir/lowering/method_call_lowerer.rs index 7d0026b7..22090b1e 100644 --- a/src/mir/join_ir/lowering/method_call_lowerer.rs +++ b/src/mir/join_ir/lowering/method_call_lowerer.rs @@ -38,6 +38,8 @@ use crate::mir::join_ir::{JoinInst, MirLikeInst}; use crate::mir::ValueId; use crate::runtime::core_box_ids::CoreMethodId; +use super::condition_env::ConditionEnv; + /// Phase 224-B: MethodCall Lowerer Box /// /// Provides metadata-driven lowering of MethodCall AST nodes to JoinIR instructions. @@ -59,11 +61,12 @@ impl MethodCallLowerer { /// * `Ok(ValueId)` - Result of method call /// * `Err(String)` - If method not found or not allowed in condition /// - /// # Phase 224-B P0 Restrictions + /// # Phase 224-C: Argument Support /// - /// - Only zero-argument methods supported (e.g., `s.length()`) - /// - Only whitelisted methods (StringLength, ArrayLength, etc.) - /// - Arguments must be empty slice + /// - Supports zero-argument methods (e.g., `s.length()`) + /// - Supports methods with arguments (e.g., `s.substring(0, 5)`, `s.indexOf("x")`) + /// - Only whitelisted methods (StringLength, ArrayLength, StringIndexOf, etc.) + /// - Arity is checked against CoreMethodId metadata /// /// # Example /// @@ -83,21 +86,12 @@ impl MethodCallLowerer { method_name: &str, args: &[ASTNode], alloc_value: &mut F, + env: &ConditionEnv, 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() @@ -117,14 +111,40 @@ impl MethodCallLowerer { )); } + // Phase 224-C: Check arity + let expected_arity = method_id.arity(); + if args.len() != expected_arity { + return Err(format!( + "Arity mismatch: {}.{}() expects {} args, got {}", + recv_val.0, method_name, expected_arity, args.len() + )); + } + + // Phase 224-C: Lower arguments using condition lowerer + let mut lowered_args = Vec::new(); + for arg_ast in args { + let arg_val = super::condition_lowerer::lower_value_expression( + arg_ast, + alloc_value, + env, + instructions + )?; + lowered_args.push(arg_val); + } + // Emit BoxCall instruction let dst = alloc_value(); let box_name = method_id.box_id().name().to_string(); + + // Build complete args: receiver + method args + let mut full_args = vec![recv_val]; + full_args.extend(lowered_args); + 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 + args: full_args, })); Ok(dst) @@ -135,30 +155,22 @@ impl MethodCallLowerer { /// Similar to `lower_for_condition` but uses `allowed_in_init()` whitelist. /// More permissive - allows methods like `substring`, `indexOf`, etc. /// - /// # Phase 224-B P0 Restrictions + /// # Phase 224-C: Argument Support /// - /// - Only zero-argument methods supported - /// - Arguments will be supported in Phase 224-C + /// - Supports zero-argument methods + /// - Supports methods with arguments (e.g., `substring(0, 5)`, `indexOf(ch)`) + /// - Arity is checked against CoreMethodId metadata pub fn lower_for_init( recv_val: ValueId, method_name: &str, args: &[ASTNode], alloc_value: &mut F, + env: &ConditionEnv, 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) @@ -177,14 +189,40 @@ impl MethodCallLowerer { )); } + // Phase 224-C: Check arity + let expected_arity = method_id.arity(); + if args.len() != expected_arity { + return Err(format!( + "Arity mismatch: {}.{}() expects {} args, got {}", + recv_val.0, method_name, expected_arity, args.len() + )); + } + + // Phase 224-C: Lower arguments using condition lowerer + let mut lowered_args = Vec::new(); + for arg_ast in args { + let arg_val = super::condition_lowerer::lower_value_expression( + arg_ast, + alloc_value, + env, + instructions + )?; + lowered_args.push(arg_val); + } + // Emit BoxCall instruction let dst = alloc_value(); let box_name = method_id.box_id().name().to_string(); + + // Build complete args: receiver + method args + let mut full_args = vec![recv_val]; + full_args.extend(lowered_args); + 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 + args: full_args, })); Ok(dst) @@ -216,11 +254,15 @@ mod tests { }; let mut instructions = Vec::new(); + // Phase 224-C: Use ConditionEnv + let env = ConditionEnv::new(); + let result = MethodCallLowerer::lower_for_condition( recv_val, "length", &[], &mut alloc_value, + &env, &mut instructions, ); @@ -257,12 +299,14 @@ mod tests { id }; let mut instructions = Vec::new(); + let env = ConditionEnv::new(); let result = MethodCallLowerer::lower_for_condition( recv_val, "upper", &[], &mut alloc_value, + &env, &mut instructions, ); @@ -281,12 +325,14 @@ mod tests { id }; let mut instructions = Vec::new(); + let env = ConditionEnv::new(); let result = MethodCallLowerer::lower_for_condition( recv_val, "unknownMethod", &[], &mut alloc_value, + &env, &mut instructions, ); @@ -298,8 +344,10 @@ mod tests { #[test] fn test_lower_substring_for_init() { - // Test: s.substring() in LoopBodyLocal init (allowed in init, not condition) + // Phase 224-C: substring is not allowed in condition but IS allowed in init let recv_val = ValueId(10); + let i_val = ValueId(11); + let j_val = ValueId(12); let mut value_counter = 100u32; let mut alloc_value = || { let id = ValueId(value_counter); @@ -308,23 +356,41 @@ mod tests { }; let mut instructions = Vec::new(); - // substring is allowed in init but NOT in condition + // Create ConditionEnv with i and j variables + let mut env = ConditionEnv::new(); + env.insert("i".to_string(), i_val); + env.insert("j".to_string(), j_val); + + // Create argument ASTs for substring(i, j) + let arg1_ast = ASTNode::Variable { + name: "i".to_string(), + span: crate::ast::Span::unknown(), + }; + let arg2_ast = ASTNode::Variable { + name: "j".to_string(), + span: crate::ast::Span::unknown(), + }; + + // substring is NOT in condition whitelist let cond_result = MethodCallLowerer::lower_for_condition( recv_val, "substring", - &[], + &[arg1_ast.clone(), arg2_ast.clone()], &mut alloc_value, + &env, &mut instructions, ); assert!(cond_result.is_err()); + assert!(cond_result.unwrap_err().contains("not allowed in loop condition")); - // But allowed in init context + // But IS allowed in init context instructions.clear(); let init_result = MethodCallLowerer::lower_for_init( recv_val, "substring", - &[], + &[arg1_ast, arg2_ast], &mut alloc_value, + &env, &mut instructions, ); assert!(init_result.is_ok()); @@ -332,8 +398,8 @@ mod tests { } #[test] - fn test_phase224b_p0_no_arguments() { - // Test: P0 restriction - no arguments supported yet + fn test_phase224c_arity_mismatch() { + // Phase 224-C Test: Arity mismatch error let recv_val = ValueId(10); let mut value_counter = 100u32; let mut alloc_value = || { @@ -342,6 +408,7 @@ mod tests { id }; let mut instructions = Vec::new(); + let env = ConditionEnv::new(); // Create dummy argument let dummy_arg = ASTNode::Literal { @@ -354,10 +421,128 @@ mod tests { "length", &[dummy_arg], &mut alloc_value, + &env, &mut instructions, ); assert!(result.is_err()); - assert!(result.unwrap_err().contains("P0: MethodCall with arguments not supported")); + assert!(result.unwrap_err().contains("Arity mismatch")); + } + + #[test] + fn test_lower_indexOf_with_arg() { + // Phase 224-C Test: s.indexOf(ch) with 1 argument + let recv_val = ValueId(10); + let ch_val = ValueId(11); + let mut value_counter = 100u32; + let mut alloc_value = || { + let id = ValueId(value_counter); + value_counter += 1; + id + }; + let mut instructions = Vec::new(); + + // Create ConditionEnv with ch variable + let mut env = ConditionEnv::new(); + env.insert("ch".to_string(), ch_val); + + // Create argument AST + let arg_ast = ASTNode::Variable { + name: "ch".to_string(), + span: crate::ast::Span::unknown(), + }; + + let result = MethodCallLowerer::lower_for_init( + recv_val, + "indexOf", + &[arg_ast], + &mut alloc_value, + &env, + &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, "indexOf"); + assert_eq!(args.len(), 2); // Receiver + 1 arg + assert_eq!(args[0], ValueId(10)); // Receiver + assert_eq!(args[1], ValueId(11)); // ch argument + } + _ => panic!("Expected BoxCall instruction"), + } + } + + #[test] + fn test_lower_substring_with_args() { + // Phase 224-C Test: s.substring(i, j) with 2 arguments + let recv_val = ValueId(10); + let i_val = ValueId(11); + let j_val = ValueId(12); + let mut value_counter = 100u32; + let mut alloc_value = || { + let id = ValueId(value_counter); + value_counter += 1; + id + }; + let mut instructions = Vec::new(); + + // Create ConditionEnv with i and j variables + let mut env = ConditionEnv::new(); + env.insert("i".to_string(), i_val); + env.insert("j".to_string(), j_val); + + // Create argument ASTs + let arg1_ast = ASTNode::Variable { + name: "i".to_string(), + span: crate::ast::Span::unknown(), + }; + let arg2_ast = ASTNode::Variable { + name: "j".to_string(), + span: crate::ast::Span::unknown(), + }; + + let result = MethodCallLowerer::lower_for_init( + recv_val, + "substring", + &[arg1_ast, arg2_ast], + &mut alloc_value, + &env, + &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, "substring"); + assert_eq!(args.len(), 3); // Receiver + 2 args + assert_eq!(args[0], ValueId(10)); // Receiver + assert_eq!(args[1], ValueId(11)); // i argument + assert_eq!(args[2], ValueId(12)); // j argument + } + _ => panic!("Expected BoxCall instruction"), + } } } diff --git a/src/runtime/core_box_ids.rs b/src/runtime/core_box_ids.rs index f2ebcdd7..79afbab5 100644 --- a/src/runtime/core_box_ids.rs +++ b/src/runtime/core_box_ids.rs @@ -166,6 +166,7 @@ pub enum CoreMethodId { StringLower, StringConcat, StringSubstring, + StringIndexOf, StringReplace, StringTrim, StringSplit, @@ -213,8 +214,8 @@ impl CoreMethodId { use CoreMethodId::*; match self { StringLength | StringUpper | StringLower | - StringConcat | StringSubstring | StringReplace | - StringTrim | StringSplit => CoreBoxId::String, + StringConcat | StringSubstring | StringIndexOf | + StringReplace | StringTrim | StringSplit => CoreBoxId::String, IntegerAbs | IntegerMin | IntegerMax => CoreBoxId::Integer, @@ -241,6 +242,7 @@ impl CoreMethodId { StringLower => "lower", StringConcat => "concat", StringSubstring => "substring", + StringIndexOf => "indexOf", StringReplace => "replace", StringTrim => "trim", StringSplit => "split", @@ -287,7 +289,7 @@ impl CoreMethodId { MapKeys | ResultIsOk | ResultGetValue => 0, - StringConcat | StringSubstring | StringReplace | StringSplit | + StringConcat | StringIndexOf | StringReplace | StringSplit | IntegerMin | IntegerMax | BoolAnd | BoolOr | ArrayGet | ArrayPush | @@ -295,7 +297,7 @@ impl CoreMethodId { ConsolePrintln | ConsoleLog | ConsoleError | FileRead | FileWrite | FileOpen => 1, - MapSet => 2, + StringSubstring | MapSet => 2, } } @@ -303,7 +305,7 @@ impl CoreMethodId { pub fn return_type_name(&self) -> &'static str { use CoreMethodId::*; match self { - StringLength | ArrayLength => "IntegerBox", + StringLength | StringIndexOf | ArrayLength => "IntegerBox", StringUpper | StringLower | StringConcat | StringSubstring | StringReplace | StringTrim => "StringBox", @@ -329,7 +331,7 @@ impl CoreMethodId { use CoreMethodId::*; [ StringLength, StringUpper, StringLower, StringConcat, StringSubstring, - StringReplace, StringTrim, StringSplit, + StringIndexOf, StringReplace, StringTrim, StringSplit, IntegerAbs, IntegerMin, IntegerMax, BoolNot, BoolAnd, BoolOr, ArrayLength, ArrayPush, ArrayPop, ArrayGet, @@ -360,8 +362,8 @@ impl CoreMethodId { match self { // String methods (pure - return new values, don't mutate) StringLength | StringUpper | StringLower | - StringConcat | StringSubstring | StringReplace | - StringTrim | StringSplit => true, + StringConcat | StringSubstring | StringIndexOf | + StringReplace | StringTrim | StringSplit => true, // Integer/Bool methods (pure - mathematical operations) IntegerAbs | IntegerMin | IntegerMax | @@ -407,7 +409,7 @@ impl CoreMethodId { // Not yet whitelisted - be conservative StringUpper | StringLower | StringConcat | - StringSubstring | StringReplace | StringTrim | StringSplit => false, + StringSubstring | StringIndexOf | StringReplace | StringTrim | StringSplit => false, IntegerAbs | IntegerMin | IntegerMax => false, BoolNot | BoolAnd | BoolOr => false, @@ -431,7 +433,7 @@ impl CoreMethodId { use CoreMethodId::*; match self { // String operations - allowed - StringLength | StringSubstring => true, + StringLength | StringSubstring | StringIndexOf => true, // String transformations - allowed for init StringUpper | StringLower | StringTrim => true,