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:
@ -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!(
|
||||
|
||||
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user