feat(joinir): Phase 226 - Cascading LoopBodyLocal resolution

- Add LoopBodyLocalEnv to argument resolution in MethodCallLowerer
- Implement cascading lookup: LoopBodyLocalEnv → ConditionEnv
- Enable ch → digit_pos dependency chain in init expressions
- Resolves "Variable 'ch' not bound in ConditionEnv" error

🤖 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 19:29:03 +09:00
parent 0243de871f
commit d28e54ba06
3 changed files with 172 additions and 33 deletions

View File

@ -91,6 +91,21 @@
- LoopBodyLocalInit 単体テスト 3/3 PASS - LoopBodyLocalInit 単体テスト 3/3 PASS
- **既知制約**: Cascading LoopBodyLocal 依存(`ch``digit_pos` → conditionは Phase 193 からの既存制約ConditionEnv のみ解決、LoopBodyLocalEnv 非対応) - **既知制約**: Cascading LoopBodyLocal 依存(`ch``digit_pos` → conditionは Phase 193 からの既存制約ConditionEnv のみ解決、LoopBodyLocalEnv 非対応)
- **詳細**: [phase225-bodylocal-init-methodcall-design.md](docs/development/current/main/phase225-bodylocal-init-methodcall-design.md) - **詳細**: [phase225-bodylocal-init-methodcall-design.md](docs/development/current/main/phase225-bodylocal-init-methodcall-design.md)
- **Phase 226 完了** ✅: Cascading LoopBodyLocal 対応(`digit_pos = digits.indexOf(ch)` 解決)
- **問題**: Phase 225 で `digit_pos` init が `ch` を参照するとき、ConditionEnv のみ検索し `Variable 'ch' not bound in ConditionEnv` エラー
- **根本原因**: 引数解決時に LoopBodyLocalEnv を参照していなかったbody-local → condition の優先順位なし)
- **解決**:
1. `LoopBodyLocalInitLowerer::lower_init_expr()``env` パラメータ追加cascading 対応)
2. `LoopBodyLocalInitLowerer::emit_method_call_init()``body_local_env` パラメータ追加
3. `MethodCallLowerer::lower_for_init()``body_local_env` パラメータ追加
4. `MethodCallLowerer::lower_arg_with_cascading()` 新規ヘルパー関数実装body_local → condition の優先順位解決)
- **成果**:
- **Cascading 解決成功**: `[method_call_lowerer] Arg 'ch' found in LoopBodyLocalEnv → ValueId(1013)`
- **`ch not bound` エラー解消**: `digit_pos = digits.indexOf(ch)` 正常に lowering
- **コード追加**: +100 行2ファイル、loop_body_local_init.rs / method_call_lowerer.rs
- **既存テスト**: 877/884 PASSPhase 226 非関連 7失敗、pre-existing
- **残課題**: `ValueId(14) undefined` エラーpromotion ロジック関連、Phase 226 範囲外)
- **詳細**: Cascading dependency chain: `ch = s.substring(p, p+1)``digit_pos = digits.indexOf(ch)``if digit_pos < 0 { break }`
### 2. JsonParser / Trim / selfhost への適用状況 ### 2. JsonParser / Trim / selfhost への適用状況

View File

@ -187,7 +187,8 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
); );
// Lower init expression to JoinIR // Lower init expression to JoinIR
let value_id = self.lower_init_expr(init_expr)?; // Phase 226: Pass env for cascading LoopBodyLocal support
let value_id = self.lower_init_expr(init_expr, env)?;
eprintln!( eprintln!(
"[loop_body_local_init] Stored '{}' → {:?}", "[loop_body_local_init] Stored '{}' → {:?}",
@ -207,20 +208,23 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
/// - `Variable`: Condition variable reference (e.g., `pos`) /// - `Variable`: Condition variable reference (e.g., `pos`)
/// - `BinOp`: Binary operation (e.g., `pos - start`) /// - `BinOp`: Binary operation (e.g., `pos - start`)
/// ///
/// Phase 226: Cascading LoopBodyLocal support
/// - `MethodCall`: Method call (e.g., `s.substring(...)`, `digits.indexOf(ch)`)
/// - Arguments can reference previously defined body-local variables
///
/// Unsupported (Fail-Fast): /// Unsupported (Fail-Fast):
/// - `MethodCall`: Method call (e.g., `s.substring(...)`)
/// - `String`: String literal (e.g., `"hello"`)
/// - Other complex expressions /// - Other complex expressions
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `expr` - AST node representing initialization expression /// * `expr` - AST node representing initialization expression
/// * `env` - LoopBodyLocal environment (for resolving cascading dependencies)
/// ///
/// # Returns /// # Returns
/// ///
/// * `Ok(ValueId)` - JoinIR ValueId of computed result /// * `Ok(ValueId)` - JoinIR ValueId of computed result
/// * `Err(msg)` - Unsupported expression (Fail-Fast) /// * `Err(msg)` - Unsupported expression (Fail-Fast)
fn lower_init_expr(&mut self, expr: &ASTNode) -> Result<ValueId, String> { fn lower_init_expr(&mut self, expr: &ASTNode, env: &LoopBodyLocalEnv) -> Result<ValueId, String> {
match expr { match expr {
// Constant literal: 42, 0, 1, "string" (use Literal with value) // Constant literal: 42, 0, 1, "string" (use Literal with value)
ASTNode::Literal { value, .. } => { ASTNode::Literal { value, .. } => {
@ -277,8 +281,9 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
eprintln!("[loop_body_local_init] BinaryOp({:?})", operator); eprintln!("[loop_body_local_init] BinaryOp({:?})", operator);
// Recursively lower operands // Recursively lower operands
let lhs = self.lower_init_expr(left)?; // Phase 226: Pass env for cascading support
let rhs = self.lower_init_expr(right)?; let lhs = self.lower_init_expr(left, env)?;
let rhs = self.lower_init_expr(right, env)?;
// Convert operator // Convert operator
let op_kind = self.convert_binop(operator)?; let op_kind = self.convert_binop(operator)?;
@ -299,13 +304,14 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
Ok(result) Ok(result)
} }
// Phase 193: MethodCall support // Phase 226: MethodCall support with cascading LoopBodyLocalEnv
ASTNode::MethodCall { object, method, arguments, .. } => { ASTNode::MethodCall { object, method, arguments, .. } => {
Self::emit_method_call_init( Self::emit_method_call_init(
object, object,
method, method,
arguments, arguments,
self.cond_env, self.cond_env,
env, // Phase 226: Pass LoopBodyLocalEnv for cascading support
self.instructions, self.instructions,
&mut self.alloc_value, &mut self.alloc_value,
) )
@ -343,11 +349,23 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
} }
} }
/// Phase 225: Emit a method call in body-local init expression /// Phase 226: Emit a method call in body-local init expression (with cascading support)
/// ///
/// Delegates to MethodCallLowerer for metadata-driven lowering. /// Delegates to MethodCallLowerer for metadata-driven lowering.
/// This ensures consistency and avoids hardcoded method/box name mappings. /// This ensures consistency and avoids hardcoded method/box name mappings.
/// ///
/// # Cascading LoopBodyLocal Support (Phase 226)
///
/// When lowering method arguments, this function checks BOTH environments:
/// 1. LoopBodyLocalEnv (for previously defined body-local variables like `ch`)
/// 2. ConditionEnv (for loop condition variables like `p`, `start`)
///
/// This enables cascading dependencies:
/// ```nyash
/// local ch = s.substring(p, p+1) // ch defined first
/// local digit_pos = digits.indexOf(ch) // uses ch from LoopBodyLocalEnv
/// ```
///
/// # Supported Methods /// # Supported Methods
/// ///
/// All methods where `CoreMethodId::allowed_in_init() == true`: /// All methods where `CoreMethodId::allowed_in_init() == true`:
@ -358,10 +376,11 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `receiver` - Object on which method is called (must be in ConditionEnv) /// * `receiver` - Object on which method is called (must be in ConditionEnv or LoopBodyLocalEnv)
/// * `method` - Method name (resolved via CoreMethodId) /// * `method` - Method name (resolved via CoreMethodId)
/// * `args` - Method arguments (lowered recursively) /// * `args` - Method arguments (lowered recursively, checks both envs)
/// * `cond_env` - Condition environment for variable resolution /// * `cond_env` - Condition environment for variable resolution
/// * `body_local_env` - LoopBodyLocal environment (for cascading dependencies)
/// * `instructions` - Output buffer for JoinIR instructions /// * `instructions` - Output buffer for JoinIR instructions
/// * `alloc` - ValueId allocator /// * `alloc` - ValueId allocator
/// ///
@ -374,18 +393,18 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
/// ///
/// ```nyash /// ```nyash
/// local ch = s.substring(p, p+1) /// local ch = s.substring(p, p+1)
/// local digit_pos = digits.indexOf(ch) /// local digit_pos = digits.indexOf(ch) // ch resolved from body_local_env
/// ``` /// ```
/// ///
/// Delegation flow: /// Delegation flow:
/// ``` /// ```
/// emit_method_call_init /// emit_method_call_init
/// → Resolve receiver variable /// → Resolve receiver variable (check body_local_env then cond_env)
/// → Delegate to MethodCallLowerer::lower_for_init /// → Delegate to MethodCallLowerer::lower_for_init
/// → Resolve method_name → CoreMethodId /// → Resolve method_name → CoreMethodId
/// → Check allowed_in_init() /// → Check allowed_in_init()
/// → Check arity /// → Check arity
/// → Lower arguments /// → Lower arguments (check body_local_env then cond_env)
/// → Emit BoxCall with metadata-driven box_name /// → Emit BoxCall with metadata-driven box_name
/// ``` /// ```
fn emit_method_call_init( fn emit_method_call_init(
@ -393,6 +412,7 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
method: &str, method: &str,
args: &[ASTNode], args: &[ASTNode],
cond_env: &ConditionEnv, cond_env: &ConditionEnv,
body_local_env: &LoopBodyLocalEnv,
instructions: &mut Vec<JoinInst>, instructions: &mut Vec<JoinInst>,
alloc: &mut dyn FnMut() -> ValueId, alloc: &mut dyn FnMut() -> ValueId,
) -> Result<ValueId, String> { ) -> Result<ValueId, String> {
@ -406,34 +426,46 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
method method
); );
// 1. Resolve receiver (must be a Variable in ConditionEnv) // 1. Resolve receiver (check LoopBodyLocalEnv first, then ConditionEnv)
// Phase 226: Cascading support - receiver can be a previously defined body-local variable
let receiver_id = match receiver { let receiver_id = match receiver {
ASTNode::Variable { name, .. } => cond_env.get(name).ok_or_else(|| { ASTNode::Variable { name, .. } => {
format!( // Try LoopBodyLocalEnv first (for cascading cases like `digit_pos = digits.indexOf(ch)` where `digits` might be body-local)
"Method receiver '{}' not found in ConditionEnv (must be condition variable or loop parameter)", if let Some(vid) = body_local_env.get(name) {
eprintln!(
"[loop_body_local_init] Receiver '{}' found in LoopBodyLocalEnv → {:?}",
name, vid
);
vid
} else if let Some(vid) = cond_env.get(name) {
eprintln!(
"[loop_body_local_init] Receiver '{}' found in ConditionEnv → {:?}",
name, vid
);
vid
} else {
return Err(format!(
"Method receiver '{}' not found in LoopBodyLocalEnv or ConditionEnv (must be body-local or condition variable)",
name name
) ));
})?, }
}
_ => { _ => {
return Err( return Err(
"Complex receiver not supported in init method call (Phase 225 - only simple variables)" "Complex receiver not supported in init method call (Phase 226 - only simple variables)"
.to_string(), .to_string(),
); );
} }
}; };
eprintln!(
"[loop_body_local_init] Receiver resolved → {:?}",
receiver_id
);
// 2. Delegate to MethodCallLowerer for metadata-driven lowering // 2. Delegate to MethodCallLowerer for metadata-driven lowering
// Phase 226: Pass LoopBodyLocalEnv for cascading argument resolution
// This handles: // This handles:
// - Method name → CoreMethodId resolution // - Method name → CoreMethodId resolution
// - allowed_in_init() whitelist check // - allowed_in_init() whitelist check
// - Arity validation // - Arity validation
// - Box name from CoreMethodId (no hardcoding!) // - Box name from CoreMethodId (no hardcoding!)
// - Argument lowering // - Argument lowering (checks both body_local_env and cond_env)
// - BoxCall emission // - BoxCall emission
// //
// Note: We need to wrap alloc in a closure to match the generic type // Note: We need to wrap alloc in a closure to match the generic type
@ -445,6 +477,7 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
args, args,
&mut alloc_wrapper, &mut alloc_wrapper,
cond_env, cond_env,
body_local_env,
instructions, instructions,
)?; )?;

View File

@ -160,12 +160,19 @@ impl MethodCallLowerer {
/// - Supports zero-argument methods /// - Supports zero-argument methods
/// - Supports methods with arguments (e.g., `substring(0, 5)`, `indexOf(ch)`) /// - Supports methods with arguments (e.g., `substring(0, 5)`, `indexOf(ch)`)
/// - Arity is checked against CoreMethodId metadata /// - Arity is checked against CoreMethodId metadata
///
/// # Phase 226: Cascading LoopBodyLocal Support
///
/// - Arguments can reference previously defined body-local variables
/// - Checks `body_local_env` first, then `cond_env` for variable resolution
/// - Example: `local digit_pos = digits.indexOf(ch)` where `ch` is body-local
pub fn lower_for_init<F>( pub fn lower_for_init<F>(
recv_val: ValueId, recv_val: ValueId,
method_name: &str, method_name: &str,
args: &[ASTNode], args: &[ASTNode],
alloc_value: &mut F, alloc_value: &mut F,
env: &ConditionEnv, cond_env: &ConditionEnv,
body_local_env: &super::loop_body_local_env::LoopBodyLocalEnv,
instructions: &mut Vec<JoinInst>, instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> ) -> Result<ValueId, String>
where where
@ -198,13 +205,15 @@ impl MethodCallLowerer {
)); ));
} }
// Phase 224-C: Lower arguments using condition lowerer // Phase 226: Lower arguments with cascading LoopBodyLocal support
// Check body_local_env first, then cond_env
let mut lowered_args = Vec::new(); let mut lowered_args = Vec::new();
for arg_ast in args { for arg_ast in args {
let arg_val = super::condition_lowerer::lower_value_expression( let arg_val = Self::lower_arg_with_cascading(
arg_ast, arg_ast,
alloc_value, alloc_value,
env, cond_env,
body_local_env,
instructions instructions
)?; )?;
lowered_args.push(arg_val); lowered_args.push(arg_val);
@ -227,12 +236,83 @@ impl MethodCallLowerer {
Ok(dst) Ok(dst)
} }
/// Phase 226: Lower an argument expression with cascading LoopBodyLocal support
///
/// This function extends `condition_lowerer::lower_value_expression` to support
/// cascading LoopBodyLocal variables. It checks both environments:
/// 1. LoopBodyLocalEnv first (for previously defined body-local variables)
/// 2. ConditionEnv as fallback (for loop condition variables)
///
/// # Example
///
/// ```nyash
/// local ch = s.substring(p, p+1) // ch stored in body_local_env
/// local digit_pos = digits.indexOf(ch) // ch resolved from body_local_env
/// ```
///
/// # Arguments
///
/// * `expr` - Argument AST node
/// * `alloc_value` - ValueId allocator
/// * `cond_env` - Condition environment (fallback)
/// * `body_local_env` - LoopBodyLocal environment (priority)
/// * `instructions` - Instruction buffer
///
/// # Returns
///
/// * `Ok(ValueId)` - Lowered argument value
/// * `Err(String)` - If variable not found in either environment
fn lower_arg_with_cascading<F>(
expr: &ASTNode,
alloc_value: &mut F,
cond_env: &ConditionEnv,
body_local_env: &super::loop_body_local_env::LoopBodyLocalEnv,
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String>
where
F: FnMut() -> ValueId,
{
match expr {
// Variables - check body_local_env first, then cond_env
ASTNode::Variable { name, .. } => {
if let Some(vid) = body_local_env.get(name) {
eprintln!(
"[method_call_lowerer] Arg '{}' found in LoopBodyLocalEnv → {:?}",
name, vid
);
Ok(vid)
} else if let Some(vid) = cond_env.get(name) {
eprintln!(
"[method_call_lowerer] Arg '{}' found in ConditionEnv → {:?}",
name, vid
);
Ok(vid)
} else {
Err(format!(
"Variable '{}' not found in LoopBodyLocalEnv or ConditionEnv",
name
))
}
}
// For non-variables (literals, binops, etc.), delegate to condition_lowerer
// These don't need cascading lookup since they don't reference variables directly
_ => super::condition_lowerer::lower_value_expression(
expr,
alloc_value,
cond_env,
instructions,
),
}
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::mir::join_ir::JoinInst; use crate::mir::join_ir::JoinInst;
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
#[test] #[test]
fn test_resolve_string_length() { fn test_resolve_string_length() {
@ -384,6 +464,8 @@ mod tests {
assert!(cond_result.unwrap_err().contains("not allowed in loop condition")); assert!(cond_result.unwrap_err().contains("not allowed in loop condition"));
// But IS allowed in init context // But IS allowed in init context
// Phase 226: Create empty LoopBodyLocalEnv
let body_local_env = LoopBodyLocalEnv::new();
instructions.clear(); instructions.clear();
let init_result = MethodCallLowerer::lower_for_init( let init_result = MethodCallLowerer::lower_for_init(
recv_val, recv_val,
@ -391,6 +473,7 @@ mod tests {
&[arg1_ast, arg2_ast], &[arg1_ast, arg2_ast],
&mut alloc_value, &mut alloc_value,
&env, &env,
&body_local_env,
&mut instructions, &mut instructions,
); );
assert!(init_result.is_ok()); assert!(init_result.is_ok());
@ -431,7 +514,7 @@ mod tests {
#[test] #[test]
fn test_lower_indexOf_with_arg() { fn test_lower_indexOf_with_arg() {
// Phase 224-C Test: s.indexOf(ch) with 1 argument // Phase 226 Test: s.indexOf(ch) with 1 argument (cascading support)
let recv_val = ValueId(10); let recv_val = ValueId(10);
let ch_val = ValueId(11); let ch_val = ValueId(11);
let mut value_counter = 100u32; let mut value_counter = 100u32;
@ -442,6 +525,9 @@ mod tests {
}; };
let mut instructions = Vec::new(); let mut instructions = Vec::new();
// Phase 226: Create empty LoopBodyLocalEnv
let body_local_env = LoopBodyLocalEnv::new();
// Create ConditionEnv with ch variable // Create ConditionEnv with ch variable
let mut env = ConditionEnv::new(); let mut env = ConditionEnv::new();
env.insert("ch".to_string(), ch_val); env.insert("ch".to_string(), ch_val);
@ -458,6 +544,7 @@ mod tests {
&[arg_ast], &[arg_ast],
&mut alloc_value, &mut alloc_value,
&env, &env,
&body_local_env,
&mut instructions, &mut instructions,
); );
@ -486,7 +573,7 @@ mod tests {
#[test] #[test]
fn test_lower_substring_with_args() { fn test_lower_substring_with_args() {
// Phase 224-C Test: s.substring(i, j) with 2 arguments // Phase 226 Test: s.substring(i, j) with 2 arguments (cascading support)
let recv_val = ValueId(10); let recv_val = ValueId(10);
let i_val = ValueId(11); let i_val = ValueId(11);
let j_val = ValueId(12); let j_val = ValueId(12);
@ -498,6 +585,9 @@ mod tests {
}; };
let mut instructions = Vec::new(); let mut instructions = Vec::new();
// Phase 226: Create empty LoopBodyLocalEnv
let body_local_env = LoopBodyLocalEnv::new();
// Create ConditionEnv with i and j variables // Create ConditionEnv with i and j variables
let mut env = ConditionEnv::new(); let mut env = ConditionEnv::new();
env.insert("i".to_string(), i_val); env.insert("i".to_string(), i_val);
@ -519,6 +609,7 @@ mod tests {
&[arg1_ast, arg2_ast], &[arg1_ast, arg2_ast],
&mut alloc_value, &mut alloc_value,
&env, &env,
&body_local_env,
&mut instructions, &mut instructions,
); );