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
- **既知制約**: 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)
- **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 への適用状況

View File

@ -187,7 +187,8 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
);
// 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!(
"[loop_body_local_init] Stored '{}' → {:?}",
@ -207,20 +208,23 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
/// - `Variable`: Condition variable reference (e.g., `pos`)
/// - `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):
/// - `MethodCall`: Method call (e.g., `s.substring(...)`)
/// - `String`: String literal (e.g., `"hello"`)
/// - Other complex expressions
///
/// # Arguments
///
/// * `expr` - AST node representing initialization expression
/// * `env` - LoopBodyLocal environment (for resolving cascading dependencies)
///
/// # Returns
///
/// * `Ok(ValueId)` - JoinIR ValueId of computed result
/// * `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 {
// Constant literal: 42, 0, 1, "string" (use Literal with value)
ASTNode::Literal { value, .. } => {
@ -277,8 +281,9 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
eprintln!("[loop_body_local_init] BinaryOp({:?})", operator);
// Recursively lower operands
let lhs = self.lower_init_expr(left)?;
let rhs = self.lower_init_expr(right)?;
// Phase 226: Pass env for cascading support
let lhs = self.lower_init_expr(left, env)?;
let rhs = self.lower_init_expr(right, env)?;
// Convert operator
let op_kind = self.convert_binop(operator)?;
@ -299,13 +304,14 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
Ok(result)
}
// Phase 193: MethodCall support
// Phase 226: MethodCall support with cascading LoopBodyLocalEnv
ASTNode::MethodCall { object, method, arguments, .. } => {
Self::emit_method_call_init(
object,
method,
arguments,
self.cond_env,
env, // Phase 226: Pass LoopBodyLocalEnv for cascading support
self.instructions,
&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.
/// 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
///
/// All methods where `CoreMethodId::allowed_in_init() == true`:
@ -358,10 +376,11 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
///
/// # 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)
/// * `args` - Method arguments (lowered recursively)
/// * `args` - Method arguments (lowered recursively, checks both envs)
/// * `cond_env` - Condition environment for variable resolution
/// * `body_local_env` - LoopBodyLocal environment (for cascading dependencies)
/// * `instructions` - Output buffer for JoinIR instructions
/// * `alloc` - ValueId allocator
///
@ -374,18 +393,18 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
///
/// ```nyash
/// 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:
/// ```
/// emit_method_call_init
/// → Resolve receiver variable
/// → Resolve receiver variable (check body_local_env then cond_env)
/// → Delegate to MethodCallLowerer::lower_for_init
/// → Resolve method_name → CoreMethodId
/// → Check allowed_in_init()
/// → Check arity
/// → Lower arguments
/// → Lower arguments (check body_local_env then cond_env)
/// → Emit BoxCall with metadata-driven box_name
/// ```
fn emit_method_call_init(
@ -393,6 +412,7 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
method: &str,
args: &[ASTNode],
cond_env: &ConditionEnv,
body_local_env: &LoopBodyLocalEnv,
instructions: &mut Vec<JoinInst>,
alloc: &mut dyn FnMut() -> ValueId,
) -> Result<ValueId, String> {
@ -406,34 +426,46 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
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 {
ASTNode::Variable { name, .. } => cond_env.get(name).ok_or_else(|| {
format!(
"Method receiver '{}' not found in ConditionEnv (must be condition variable or loop parameter)",
name
)
})?,
ASTNode::Variable { name, .. } => {
// Try LoopBodyLocalEnv first (for cascading cases like `digit_pos = digits.indexOf(ch)` where `digits` might be body-local)
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
));
}
}
_ => {
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(),
);
}
};
eprintln!(
"[loop_body_local_init] Receiver resolved → {:?}",
receiver_id
);
// 2. Delegate to MethodCallLowerer for metadata-driven lowering
// Phase 226: Pass LoopBodyLocalEnv for cascading argument resolution
// This handles:
// - Method name → CoreMethodId resolution
// - allowed_in_init() whitelist check
// - Arity validation
// - Box name from CoreMethodId (no hardcoding!)
// - Argument lowering
// - Argument lowering (checks both body_local_env and cond_env)
// - BoxCall emission
//
// Note: We need to wrap alloc in a closure to match the generic type
@ -445,6 +477,7 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
args,
&mut alloc_wrapper,
cond_env,
body_local_env,
instructions,
)?;

View File

@ -160,12 +160,19 @@ impl MethodCallLowerer {
/// - Supports zero-argument methods
/// - Supports methods with arguments (e.g., `substring(0, 5)`, `indexOf(ch)`)
/// - 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>(
recv_val: ValueId,
method_name: &str,
args: &[ASTNode],
alloc_value: &mut F,
env: &ConditionEnv,
cond_env: &ConditionEnv,
body_local_env: &super::loop_body_local_env::LoopBodyLocalEnv,
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String>
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();
for arg_ast in args {
let arg_val = super::condition_lowerer::lower_value_expression(
let arg_val = Self::lower_arg_with_cascading(
arg_ast,
alloc_value,
env,
cond_env,
body_local_env,
instructions
)?;
lowered_args.push(arg_val);
@ -227,12 +236,83 @@ impl MethodCallLowerer {
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)]
mod tests {
use super::*;
use crate::mir::join_ir::JoinInst;
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
#[test]
fn test_resolve_string_length() {
@ -384,6 +464,8 @@ mod tests {
assert!(cond_result.unwrap_err().contains("not allowed in loop condition"));
// But IS allowed in init context
// Phase 226: Create empty LoopBodyLocalEnv
let body_local_env = LoopBodyLocalEnv::new();
instructions.clear();
let init_result = MethodCallLowerer::lower_for_init(
recv_val,
@ -391,6 +473,7 @@ mod tests {
&[arg1_ast, arg2_ast],
&mut alloc_value,
&env,
&body_local_env,
&mut instructions,
);
assert!(init_result.is_ok());
@ -431,7 +514,7 @@ mod tests {
#[test]
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 ch_val = ValueId(11);
let mut value_counter = 100u32;
@ -442,6 +525,9 @@ mod tests {
};
let mut instructions = Vec::new();
// Phase 226: Create empty LoopBodyLocalEnv
let body_local_env = LoopBodyLocalEnv::new();
// Create ConditionEnv with ch variable
let mut env = ConditionEnv::new();
env.insert("ch".to_string(), ch_val);
@ -458,6 +544,7 @@ mod tests {
&[arg_ast],
&mut alloc_value,
&env,
&body_local_env,
&mut instructions,
);
@ -486,7 +573,7 @@ mod tests {
#[test]
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 i_val = ValueId(11);
let j_val = ValueId(12);
@ -498,6 +585,9 @@ mod tests {
};
let mut instructions = Vec::new();
// Phase 226: Create empty LoopBodyLocalEnv
let body_local_env = LoopBodyLocalEnv::new();
// Create ConditionEnv with i and j variables
let mut env = ConditionEnv::new();
env.insert("i".to_string(), i_val);
@ -519,6 +609,7 @@ mod tests {
&[arg1_ast, arg2_ast],
&mut alloc_value,
&env,
&body_local_env,
&mut instructions,
);