feat(joinir): Phase 224-C - MethodCallLowerer with argument support

- 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 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-10 18:19:14 +09:00
parent 250555bfc0
commit 8e3b55ddec
3 changed files with 237 additions and 50 deletions

View File

@ -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!(

View File

@ -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<JoinInst>,
) -> Result<ValueId, String>
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<F>(
recv_val: ValueId,
method_name: &str,
args: &[ASTNode],
alloc_value: &mut F,
env: &ConditionEnv,
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String>
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"),
}
}
}

View File

@ -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,