feat(joinir): Phase 224-B - MethodCallLowerer + CoreMethodId extension

- Extend CoreMethodId with is_pure(), allowed_in_condition(), allowed_in_init()
- New MethodCallLowerer box for metadata-driven MethodCall lowering
- Integrate MethodCall handling in condition_lowerer
- P0: Zero-argument methods (length) supported
- Design principle: NO method name hardcoding, CoreMethodId metadata only

🤖 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 17:59:24 +09:00
parent 4cddca1cae
commit 250555bfc0
8 changed files with 881 additions and 0 deletions

View File

@ -0,0 +1,363 @@
//! 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<F>(
recv_val: ValueId,
method_name: &str,
args: &[ASTNode],
alloc_value: &mut F,
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()
.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<F>(
recv_val: ValueId,
method_name: &str,
args: &[ASTNode],
alloc_value: &mut F,
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)
.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"));
}
}