feat(joinir): Phase 225 - MethodCall init meta-driven lowering

- Remove hardcoded method whitelist from loop_body_local_init.rs
- Remove hardcoded box name matching
- Delegate to MethodCallLowerer for all MethodCall handling
- CoreMethodId metadata is now SSOT for init method validation
- substring now works in body-local init (-82 net lines)

🤖 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:08:18 +09:00
parent 4e00edcea5
commit 0243de871f
7 changed files with 765 additions and 172 deletions

View File

@ -60,14 +60,13 @@
- **A-4 テスト追加**: `apps/tests/phase2235_p2_digit_pos_min.hako`cascading LoopBodyLocal パターン → Fail-Fast 確認)
- **error_messages.rs 拡張**: `format_error_pattern2_promotion_failed()` 等追加
- **成果**: Pattern2/4 両方から LoopBodyCondPromoter を使う統一構造が完成
- **Phase 224 完了** ⚠️: A-4 DigitPos PromoterCore Implementation Complete
- **Phase 224 完了** : A-4 DigitPos PromoterCore Implementation + ConditionAlias Bridge
- **DigitPosPromoter 実装**: cascading indexOf パターンsubstring → indexOf → comparisonの昇格ロジック完成
- **Two-tier 戦略**: A-3 Trim → A-4 DigitPos フォールバック型オーケストレーション
- **Unit Tests**: 6/6 PASScomparison operators, cascading dependency, edge cases
- **Promotion 検証**: Pattern2 パイプラインで digit_pos → is_digit_pos 昇格成功確認
- **Lowerer Integration Gap**: lower_loop_with_break_minimal が独立した LoopBodyLocal チェックを実行し、昇格済み変数をエラー検出
- **Root Cause**: Break condition AST が元の変数名digit_posを保持したまま lowerer に渡される
- **Next Steps**: Option Bpromoted variable trackingで lowerer に昇格済み変数を通知する仕組みを追加1-2h
- **Promotion 検証**: Pattern2/Pattern4 パイプラインで digit_pos → is_digit_pos 昇格成功確認
- **Phase 224-D**: ConditionAlias を CarrierInfo/ConditionEnv に導入し、`digit_pos``is_digit_pos` の条件解決ブリッジを追加LoopBodyLocal 名で書かれた break 条件でも carrier が参照されるようになった)
- **残課題**: substring/indexOf を含む body-local init の MethodCall lowering は Phase 193/224-B/C/225 のラインで段階的に対応中(完全対応は後続 Phase
- **詳細**: [PHASE_224_SUMMARY.md](docs/development/current/main/PHASE_224_SUMMARY.md)
- **Phase 224-D 完了** ✅: ConditionAlias 導入(昇格変数の条件参照解決)
- **ConditionAlias 型追加**: `CarrierInfo``condition_aliases: Vec<ConditionAlias>` フィールド追加
@ -75,7 +74,23 @@
- **Pattern2 統合**: 昇格・merge 後に join_id 割り当て、ConditionEnv に alias を追加(`digit_pos` → ValueId(104)
- **CarrierInfo 構造修正**: DigitPosPromoter が carriers list に追加する形に変更loop_var_name 置換ではなく)
- **検証**: `phase2235_p2_digit_pos_min.hako` で alias 解決成功、エラーが次段階substring initに進展
- **残課題**: substring method in body-local initPhase 193 limitation
- **残課題**: substring method in body-local initPhase 193 limitation → Phase 225 で解決
- **Phase 225 完了** ✅: LoopBodyLocalInit MethodCall メタ駆動化(ハードコード完全削除)
- **問題**: Phase 193 の `emit_method_call_init` にハードコードされた whitelist (`SUPPORTED_INIT_METHODS`) と Box 名 match 文
- **解決**: MethodCallLowerer への委譲により単一責任原則達成
- `SUPPORTED_INIT_METHODS` 定数削除indexOf, get, toString の硬直した whitelist
- Box 名 match 文削除(`indexOf → StringBox` 等のハードコード)
- CoreMethodId メタデータで `allowed_in_init()` 管理substring, indexOf 等を動的許可)
- **成果**:
- **substring メソッド追加**: Phase 225 で substring が body-local init で使用可能に(デジタル解析等で必須)
- **コード削減**: -82 行158 削除 - 76 追加)
- **メタ駆動**: すべてのメソッド判定が CoreMethodId 経由(拡張性向上)
- **テスト**:
- 877/884 テスト PASS7 失敗は pre-existing P3 accumulator issues
- MethodCallLowerer 単体テスト 8/8 PASS
- 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)
### 2. JsonParser / Trim / selfhost への適用状況
@ -161,4 +176,3 @@
- 新しい大フェーズを始めたら:
1. まず docs 配下に `phase-XXX-*.md` を書く。
2. CURRENT_TASK には「そのフェーズの一行要約」と「今のフォーカスかどうか」だけを書く。

View File

@ -0,0 +1,273 @@
# Phase 225: LoopBodyLocalInit MethodCall Meta-Driven Lowering - Complete Summary
## Overview
Phase 225 successfully eliminated ALL hardcoding from `loop_body_local_init.rs` by delegating MethodCall lowering to `MethodCallLowerer` and using `CoreMethodId` metadata exclusively.
## Problem Statement
Phase 193 introduced MethodCall support in body-local init expressions (e.g., `local digit_pos = digits.indexOf(ch)`), but the implementation contained hardcoded method names and box name mappings:
```rust
// Line 387: Hardcoded whitelist
const SUPPORTED_INIT_METHODS: &[&str] = &["indexOf", "get", "toString"];
// Lines 433-438: Hardcoded box name mapping
let box_name = match method {
"indexOf" => "StringBox".to_string(),
"get" => "ArrayBox".to_string(),
"toString" => "IntegerBox".to_string(),
_ => unreachable!("Whitelist check should have caught this"),
};
```
This caused errors like:
```
Method 'substring' not supported in body-local init (Phase 193 limitation - only indexOf, get, toString supported)
```
## Solution: Meta-Driven Architecture
### Architecture Change
**Before (Phase 193 - Hardcoded)**:
```
LoopBodyLocalInitLowerer
└─ emit_method_call_init (static method)
├─ SUPPORTED_INIT_METHODS whitelist ❌
├─ match method { "indexOf" => "StringBox" } ❌
└─ Emit BoxCall instruction
```
**After (Phase 225 - Meta-Driven)**:
```
LoopBodyLocalInitLowerer
└─ emit_method_call_init (static method)
└─ Delegates to MethodCallLowerer::lower_for_init
├─ Resolve method_name → CoreMethodId ✅
├─ Check allowed_in_init() ✅
├─ Get box_name from CoreMethodId ✅
├─ Check arity ✅
└─ Emit BoxCall instruction
```
### Key Changes
1. **Deleted hardcoded whitelist** (`SUPPORTED_INIT_METHODS` constant)
2. **Deleted hardcoded box name match** (`indexOf → StringBox` mapping)
3. **Delegated to MethodCallLowerer** (single responsibility principle)
4. **All decisions driven by `CoreMethodId` metadata** (SSOT)
## Implementation Details
### Files Modified
1. **`src/mir/join_ir/lowering/loop_body_local_init.rs`** (main refactoring)
- Import `MethodCallLowerer`
- Refactor `emit_method_call_init` to delegate
- Delete `lower_init_arg` helper (no longer needed)
- Update module documentation
- **Net change**: -82 lines (158 deleted - 76 added)
2. **`src/mir/builder/control_flow/joinir/patterns/pattern_pipeline.rs`** (test fixes)
- Add `condition_aliases: Vec::new()` to CarrierInfo test structs (2 occurrences)
3. **`src/mir/builder/control_flow/joinir/patterns/pattern4_carrier_analyzer.rs`** (test fixes)
- Add `condition_aliases: Vec::new()` to CarrierInfo test struct (1 occurrence)
4. **`docs/development/current/main/joinir-architecture-overview.md`** (documentation)
- Added Phase 225 section to LoopBodyLocal init history
5. **`CURRENT_TASK.md`** (status update)
- Added Phase 225 completion summary
### CoreMethodId Metadata
The `allowed_in_init()` method already included the methods we needed:
```rust
pub fn allowed_in_init(&self) -> bool {
use CoreMethodId::*;
match self {
// String operations - allowed
StringLength | StringSubstring | StringIndexOf => true, // ✅
// String transformations - allowed for init
StringUpper | StringLower | StringTrim => true,
// Array operations - allowed
ArrayLength | ArrayGet => true,
// Map operations - allowed
MapGet | MapHas | MapKeys => true,
// ...
}
}
```
No changes were needed to metadata - it was already correct!
## Results
### Code Quality
- **-82 net lines** (158 deletions - 76 additions)
- **Zero hardcoded method names** (all resolved via `CoreMethodId::iter()`)
- **Zero hardcoded box names** (all from `method_id.box_id().name()`)
- **Single Responsibility**: MethodCallLowerer is the only place that handles MethodCall → JoinIR
### Test Results
#### Unit Tests
- **MethodCallLowerer**: 8/8 tests PASS
- **loop_body_local_init**: 3/3 tests PASS
#### Integration Tests
- **877/884 tests PASS** (99.2% pass rate)
- **7 failures**: Pre-existing Pattern 3 accumulator variable issues (not related to Phase 225)
#### E2E Verification
- **substring now works in body-local init** ✅
- Simple test case: `local ch = s.substring(p, p+1)` compiles without error
- Complex test case: `phase2235_p2_digit_pos_min.hako` progresses past substring error to cascading dependency issue (which is a pre-existing limitation from Phase 193)
### New Capabilities
- **substring** method now usable in loop body-local init
- **Any method with `allowed_in_init() == true`** automatically supported
- **Easy to extend**: Add method to `CoreMethodId`, set `allowed_in_init()`, done!
## Known Limitations
### Cascading LoopBodyLocal Dependencies
The test `apps/tests/phase2235_p2_digit_pos_min.hako` reveals a **pre-existing limitation** (not introduced by Phase 225):
```nyash
loop(p < s.length()) {
local ch = s.substring(p, p+1) // ✅ Now works (Phase 225)
local digit_pos = digits.indexOf(ch) // ❌ Error: 'ch' not found in ConditionEnv
...
}
```
**Root cause**: When lowering `digit_pos = digits.indexOf(ch)`, the argument `ch` is looked up in `ConditionEnv` only. However, `ch` is a LoopBodyLocal variable, so it should be looked up in `LoopBodyLocalEnv`.
**Status**: This limitation existed in Phase 193 - the original `lower_init_arg` also only checked `ConditionEnv`.
**Future work**: Phase 226+ could extend argument lowering to check `LoopBodyLocalEnv` for cascading dependencies.
## Architecture Benefits
### 1. Metadata-Driven
- **Single Source of Truth**: `CoreMethodId` defines all method metadata
- **No duplication**: Method name, box name, arity, whitelist all in one place
- **Easy to extend**: Add new methods by updating `CoreMethodId` only
### 2. Single Responsibility
- **MethodCallLowerer**: "MethodCall → JoinIR" conversion (Phase 224-B)
- **LoopBodyLocalInitLowerer**: Loop body-local init coordination (Phase 186)
- **Clear boundary**: Init lowerer delegates, doesn't duplicate logic
### 3. Fail-Fast
- **Unknown methods** → immediate error (not silent fallback)
- **Arity mismatch** → immediate error
- **Not whitelisted** → immediate error with clear message
### 4. Type Safety
- **No string matching** → use enum (`CoreMethodId`)
- **Compile-time checks** → catch errors early
- **Refactoring-safe** → rename detection
### 5. Maintainability
- **Add new method**: Update `CoreMethodId` only (one place)
- **Change whitelist**: Update `allowed_in_init()` only
- **No scattered hardcoding** across files
## Comparison: Phase 193 vs Phase 225
| Aspect | Phase 193 | Phase 225 |
|--------|-----------|-----------|
| Method whitelist | Hardcoded constant | CoreMethodId metadata |
| Box name mapping | Match statement | `method_id.box_id().name()` |
| Supported methods | 3 (indexOf, get, toString) | 15+ (all with `allowed_in_init() == true`) |
| Code lines | 158 | 76 (-82 lines) |
| Extensibility | Add to 2 places | Add to `CoreMethodId` only |
| Type safety | String matching | Enum-based |
| Single responsibility | Violated | Achieved |
## Future Work (Not in Phase 225)
### Phase 226+: Potential Improvements
1. **Cascading LoopBodyLocal support**
- Extend argument lowering to check `LoopBodyLocalEnv`
- Enable `ch``digit_pos` → condition chains
2. **Type inference**
- Use actual receiver type instead of heuristics
- More accurate box name resolution
3. **Custom method support**
- User-defined box methods in init expressions
4. **Optimization**
- Dead code elimination for unused method calls
- Common subexpression elimination
5. **Error messages**
- Better diagnostics with suggestions
- "Did you mean...?" for typos
## Commit Message
```
refactor(joinir): Phase 225 - LoopBodyLocalInit MethodCall meta-driven
Eliminate ALL hardcoding from loop_body_local_init.rs by delegating
to MethodCallLowerer and using CoreMethodId metadata exclusively.
Changes:
- Delete SUPPORTED_INIT_METHODS whitelist constant
- Delete hardcoded box name match (indexOf→StringBox, etc.)
- Delegate emit_method_call_init to MethodCallLowerer::lower_for_init
- Use CoreMethodId metadata for allowed_in_init() whitelist
- Delete lower_init_arg helper (no longer needed)
- Fix test structs to include condition_aliases field
Results:
- substring method now works in body-local init
- Net change: -82 lines (158 deleted - 76 added)
- 877/884 tests PASS (7 pre-existing P3 failures)
- Zero hardcoded method/box names remaining
Architecture:
- Single Source of Truth: CoreMethodId metadata
- Single Responsibility: MethodCallLowerer handles all MethodCall lowering
- Fail-Fast: Unknown methods → immediate error
- Type Safety: Enum-based instead of string matching
Phase 225 complete - meta-driven architecture achieved ✅
```
## Success Criteria ✅
All success criteria met:
1.`cargo build --release` succeeds
2. ✅ All unit tests in `method_call_lowerer.rs` pass (8/8)
3. ✅ All unit tests in `loop_body_local_init.rs` pass (3/3)
4.`substring` method now works in body-local init
5.**Zero hardcoded method names or box names** in `emit_method_call_init`
6. ✅ No regressions in existing tests (877/884 pass, 7 pre-existing failures)
## References
- **Design Document**: [phase225-bodylocal-init-methodcall-design.md](phase225-bodylocal-init-methodcall-design.md)
- **Phase 186**: Loop Body-Local Variable Initialization (initial implementation)
- **Phase 193**: MethodCall support in body-local init (hardcoded version)
- **Phase 224-B**: MethodCallLowerer Box creation (metadata-driven)
- **Phase 224-C**: MethodCallLowerer argument support
- **Phase 224-D**: ConditionAlias variable resolution
- **Architecture Overview**: [joinir-architecture-overview.md](joinir-architecture-overview.md)

View File

@ -296,20 +296,22 @@ Local Region (1000+):
- PatternPipelineContext.is_if_sum_pattern() で条件複雑度をチェック。
- P3 if-sum mode は単純比較のみ受理、複雑条件は PoC lowerer へフォールバック。
- **MethodCallLowererPhase 224-B 実装完了)**
- **MethodCallLowererPhase 224-B / 224-C / 225 実装完了)**
- ファイル: `src/mir/join_ir/lowering/method_call_lowerer.rs`
- 責務:
- AST MethodCall ノードを JoinIR BoxCall に loweringメタデータ駆動
- CoreMethodId の `is_pure()`, `allowed_in_condition()`, `allowed_in_init()` でホワイトリスト判定。
- Phase 224-B P0: 引数なしメソッドのみ対応(`s.length()`, `arr.length()` 等)。
- CoreMethodId の `is_pure()`, `allowed_in_condition()`, `allowed_in_init()` などのメタでホワイトリスト判定。
- Phase 224-B: 引数なしメソッド`length()` 等)対応
- Phase 224-C: 引数付きメソッド(`substring(i,j)`, `indexOf(ch)` 等)対応。
- Phase 225: body-local init 用の MethodCall lowering も完全に CoreMethodId メタ駆動に統一(メソッド名ハードコード/Box 名ハードコードを削除)。
- 設計原則:
- **メソッド名ハードコード禁止**: CoreMethodId メタデータのみ参照。
- **Fail-Fast**: ホワイトリストにないメソッドは即座にエラー。
- **Box-First**: 単一責任("このMethodCallをJoinIRにできるか")だけを担当。
- 使用箇所:
- `condition_lowerer.rs` の `lower_value_expression()` から呼び出し。
- Pattern 2/3/4 のループ条件式で `s.length()` 等をサポート可能
- 次ステップ: Phase 224-C で引数付きメソッド(`substring(i, i+1)`, `indexOf(ch)`対応予定
- `loop_body_local_init.rs` の init lowering からも呼び出され、bodylocal init での substring/indexOf などを lowering
- Pattern 2/3/4 のループ条件式や bodylocal init で `s.length()`, `s.substring(...)`, `digits.indexOf(ch)` 等をサポート可能(メタ条件を満たす範囲で)。
- **LoopBodyCarrierPromoterPhase 171-C-2 実装済み)**
- ファイル: `src/mir/loop_pattern_detection/loop_body_carrier_promoter.rs`
@ -855,10 +857,17 @@ JoinIR は Rust 側だけでなく、将来的に .hako selfhost コンパイラ
- テスト: phase192_normalization_demo.hako → 123 ✅
- 制約: MethodCall を含む init 式は Phase 193 で対応予定
3. **✅ MethodCall を含む init 式の対応** → Phase 193 完了
3. **✅ MethodCall を含む init 式の対応** → Phase 193 完了、Phase 225 でメタ駆動化
- `local digit = digits.indexOf(ch)` のような MethodCall init の lowering 完了
- LoopBodyLocalInitLowerer 拡張BoxCall emission
- メソッド whitelist: indexOf, get, toString 対応
- **Phase 193**: ハードコードされた whitelist: indexOf, get, toString 対応
- **Phase 225**: CoreMethodId メタ駆動化によりハードコード完全削除 ✅
- `SUPPORTED_INIT_METHODS` 定数削除(メソッド名 whitelist
- Box 名の match 文削除(`indexOf → StringBox` 等のハードコード)
- MethodCallLowerer への委譲により単一責任原則達成
- `allowed_in_init()` メタデータで whitelist 管理
- **substring メソッド追加**: Phase 225 で substring が body-local init で使用可能に
- **コード削減**: -82 行158削除 - 76追加
- 制約: body-local init のみ対応、condition 内の MethodCall は Phase 200+
4. **✅ JsonParser 実戦投入P1/P2/P5 検証)** → Phase 194 完了

View File

@ -0,0 +1,376 @@
# Phase 225: LoopBodyLocalInit MethodCall Lowering - Meta-Driven Design
## Background
Phase 224-D completed `ConditionAlias` variable resolution, but `loop_body_local_init.rs` still has hardcoded method name whitelists and box name mappings in `emit_method_call_init`:
```rust
// Line 387: Method name whitelist (substring is missing!)
const SUPPORTED_INIT_METHODS: &[&str] = &["indexOf", "get", "toString"];
// Line 433-438: Box name hardcoding
let box_name = match method {
"indexOf" => "StringBox".to_string(),
"get" => "ArrayBox".to_string(),
"toString" => "IntegerBox".to_string(),
_ => unreachable!("Whitelist check should have caught this"),
};
```
This causes errors like:
```
Method 'substring' not supported in body-local init (Phase 193 limitation - only indexOf, get, toString supported)
```
## Problem Statement
Test case `apps/tests/phase2235_p2_digit_pos_min.hako` fails at:
```nyash
local ch = s.substring(p, p+1) // ❌ substring not in whitelist
local digit_pos = digits.indexOf(ch) // ✅ indexOf is in whitelist
```
The hardcoded whitelist prevents legitimate pure methods from being used in loop body-local initialization.
## Goal
**Eliminate ALL hardcoding** and make method call lowering **metadata-driven** using `CoreMethodId`:
1. **No method name hardcoding** - Use `CoreMethodId::iter()` to resolve methods
2. **No box name hardcoding** - Use `method_id.box_id().name()` to get box name
3. **Metadata-driven whitelist** - Use `method_id.allowed_in_init()` for permission check
4. **Delegation to MethodCallLowerer** - Single responsibility, reuse existing logic
5. **Fail-Fast** - Methods not in `CoreMethodId` immediately error
## Target Pattern
```nyash
loop(p < s.length()) {
local ch = s.substring(p, p+1) // ✅ Phase 225: substring allowed
local digit_pos = digits.indexOf(ch) // ✅ Already works
if digit_pos < 0 {
break
}
num_str = num_str + ch
p = p + 1
}
```
## Architecture
### Before (Phase 193 - Hardcoded)
```
LoopBodyLocalInitLowerer
└─ emit_method_call_init (static method)
├─ SUPPORTED_INIT_METHODS whitelist ❌
├─ match method { "indexOf" => "StringBox" } ❌
└─ Emit BoxCall instruction
```
### After (Phase 225 - Meta-Driven)
```
LoopBodyLocalInitLowerer
└─ emit_method_call_init (static method)
└─ Delegates to MethodCallLowerer::lower_for_init
├─ Resolve method_name → CoreMethodId ✅
├─ Check allowed_in_init() ✅
├─ Get box_name from CoreMethodId ✅
├─ Check arity ✅
└─ Emit BoxCall instruction
```
**Key Principle**: `MethodCallLowerer` is the **single source of truth** for all MethodCall → JoinIR lowering.
## Implementation Plan
### 225-2: Add `MethodCallLowerer::lower_for_init`
**Location**: `src/mir/join_ir/lowering/method_call_lowerer.rs`
**Note**: This method **already exists** (added in Phase 224-C). We just need to verify it works correctly:
```rust
/// 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.
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,
{
// 1. 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: {}", method_name))?;
// 2. 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));
}
// 3. 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()));
}
// 4. Lower arguments
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);
}
// 5. Emit BoxCall instruction
let dst = alloc_value();
let box_name = method_id.box_id().name().to_string(); // ✅ No hardcoding!
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: full_args,
}));
Ok(dst)
}
```
**Verification needed**: Check that `allowed_in_init()` returns `true` for `substring` and `indexOf`.
### 225-3: Refactor `emit_method_call_init` to Delegate
**Location**: `src/mir/join_ir/lowering/loop_body_local_init.rs`
**Changes**:
1. **Delete** `SUPPORTED_INIT_METHODS` whitelist (line 387)
2. **Delete** hardcoded box name match (lines 433-438)
3. **Delegate** to `MethodCallLowerer::lower_for_init`
```rust
// Before: 80 lines of hardcoded logic
fn emit_method_call_init(...) -> Result<ValueId, String> {
const SUPPORTED_INIT_METHODS: &[&str] = &["indexOf", "get", "toString"]; // ❌ DELETE
if !SUPPORTED_INIT_METHODS.contains(&method) { ... } // ❌ DELETE
let receiver_id = ...; // ✅ Keep (resolve receiver)
let arg_ids = ...; // ✅ Keep (lower arguments)
let box_name = match method { ... }; // ❌ DELETE
instructions.push(JoinInst::Compute(MirLikeInst::BoxCall { ... })); // ❌ DELETE
}
// After: 20 lines of delegation
fn emit_method_call_init(
receiver: &ASTNode,
method: &str,
args: &[ASTNode],
cond_env: &ConditionEnv,
instructions: &mut Vec<JoinInst>,
alloc: &mut dyn FnMut() -> ValueId,
) -> Result<ValueId, String> {
// 1. Resolve receiver (existing logic)
let receiver_id = match receiver {
ASTNode::Variable { name, .. } => {
cond_env.get(name).ok_or_else(|| {
format!("Method receiver '{}' not found in ConditionEnv", name)
})?
}
_ => {
return Err("Complex receiver not supported in init method call".to_string());
}
};
// 2. Delegate to MethodCallLowerer! ✅
MethodCallLowerer::lower_for_init(
receiver_id,
method,
args,
alloc,
cond_env,
instructions,
)
}
```
**Key Change**: Argument lowering is now handled by `MethodCallLowerer::lower_for_init` (via `condition_lowerer::lower_value_expression`), so we don't need to duplicate that logic.
### 225-4: Verify CoreMethodId Metadata
**Location**: `src/runtime/core_box_ids.rs`
Check `allowed_in_init()` implementation (lines 432-464):
```rust
pub fn allowed_in_init(&self) -> bool {
use CoreMethodId::*;
match self {
// String operations - allowed
StringLength | StringSubstring | StringIndexOf => true, // ✅ substring and indexOf!
// String transformations - allowed for init
StringUpper | StringLower | StringTrim => true,
// Array operations - allowed
ArrayLength | ArrayGet => true,
// ...
}
}
```
**Verification**: Confirm that:
- `StringSubstring.allowed_in_init() == true` ✅ (line 436)
- `StringIndexOf.allowed_in_init() == true` ✅ (line 436)
No changes needed - metadata is already correct!
## Testing Strategy
### Unit Tests
**Location**: `src/mir/join_ir/lowering/method_call_lowerer.rs`
Existing tests to verify:
- `test_lower_substring_for_init` - substring in init context (line 346)
- `test_lower_indexOf_with_arg` - indexOf with 1 argument (line 433)
- `test_phase224c_arity_mismatch` - arity checking (line 401)
### E2E Test
**Location**: `apps/tests/phase2235_p2_digit_pos_min.hako`
Expected behavior after Phase 225:
```bash
$ ./target/release/hakorune --backend vm apps/tests/phase2235_p2_digit_pos_min.hako
# Before Phase 225:
❌ Error: Method 'substring' not supported in body-local init
# After Phase 225:
p = 3
num_str = 123
```
### Regression Tests
Run existing tests to ensure no breakage:
```bash
cargo test --release --lib method_call_lowerer
cargo test --release --lib loop_body_local_init
```
## Success Criteria
1.`cargo build --release` succeeds
2. ✅ All unit tests in `method_call_lowerer.rs` pass
3. ✅ All unit tests in `loop_body_local_init.rs` pass
4.`phase2235_p2_digit_pos_min.hako` runs successfully (substring error disappears)
5.**Zero hardcoded method names or box names** in `emit_method_call_init`
6. ✅ No regressions in existing tests
## Hardcoding Inventory (To Be Deleted)
### In `loop_body_local_init.rs`:
1. **Line 387**: `const SUPPORTED_INIT_METHODS: &[&str] = &["indexOf", "get", "toString"];`
2. **Lines 389-394**: Method whitelist check
3. **Lines 433-438**: Box name match statement
**Total lines to delete**: ~20 lines
**Total lines to add**: ~5 lines (delegation call)
**Net change**: -15 lines (cleaner, simpler, more maintainable)
## Benefits
### 1. **Metadata-Driven Architecture**
- Single Source of Truth: `CoreMethodId` defines all method metadata
- No duplication: Method name, box name, arity, whitelist all in one place
- Easy to extend: Add new methods by updating `CoreMethodId` only
### 2. **Single Responsibility**
- `MethodCallLowerer`: "MethodCall → JoinIR" conversion (Phase 224-B)
- `LoopBodyLocalInitLowerer`: Loop body-local init coordination (Phase 186)
- Clear boundary: Init lowerer delegates, doesn't duplicate logic
### 3. **Fail-Fast**
- Unknown methods → immediate error (not silent fallback)
- Arity mismatch → immediate error
- Not whitelisted → immediate error with clear message
### 4. **Type Safety**
- No string matching → use enum (`CoreMethodId`)
- Compile-time checks → catch errors early
- Refactoring-safe → rename detection
### 5. **Maintainability**
- Add new method: Update `CoreMethodId` only (one place)
- Change whitelist: Update `allowed_in_init()` only
- No scattered hardcoding across files
## Future Work (Not in Phase 225)
### Phase 226+: Additional Improvements
1. **Type inference**: Use actual receiver type instead of heuristics
2. **Custom method support**: User-defined box methods
3. **Optimization**: Dead code elimination for unused method calls
4. **Error messages**: Better diagnostics with suggestions
## References
- **Phase 186**: Loop Body-Local Variable Initialization (initial implementation)
- **Phase 193**: MethodCall support in body-local init (hardcoded version)
- **Phase 224-B**: MethodCallLowerer Box creation (metadata-driven)
- **Phase 224-C**: MethodCallLowerer argument support
- **Phase 224-D**: ConditionAlias variable resolution
## Commit Message Template
```
refactor(joinir): Phase 225 - LoopBodyLocalInit MethodCall meta-driven
- Delete SUPPORTED_INIT_METHODS whitelist in loop_body_local_init.rs
- Delete hardcoded box name match (indexOf→StringBox, etc.)
- Delegate emit_method_call_init to MethodCallLowerer::lower_for_init
- Use CoreMethodId metadata for allowed_in_init() whitelist
- Fix: substring now works in body-local init (digit_pos test)
Hardcoding removed:
- SUPPORTED_INIT_METHODS constant (line 387)
- Box name match statement (lines 433-438)
- Whitelist check (lines 389-394)
Net change: -15 lines (cleaner, simpler, more maintainable)
Single Source of Truth: CoreMethodId metadata drives all decisions
Single Responsibility: MethodCallLowerer handles all MethodCall lowering
✅ All tests passing
✅ phase2235_p2_digit_pos_min.hako now works
✅ Zero hardcoded method/box names remaining
Phase 225 complete - meta-driven architecture achieved
```

View File

@ -290,6 +290,7 @@ mod tests {
],
trim_helper: None,
promoted_loopbodylocals: Vec::new(), // Phase 224
condition_aliases: Vec::new(), // Phase 224-D
};
// Analyze carriers

View File

@ -405,6 +405,7 @@ mod tests {
],
trim_helper: None,
promoted_loopbodylocals: Vec::new(), // Phase 224
condition_aliases: Vec::new(), // Phase 224-D
},
loop_scope: LoopScopeShapeBuilder::empty_body_locals(
BasicBlockId(0),
@ -444,6 +445,7 @@ mod tests {
whitespace_chars: vec![" ".to_string(), "\t".to_string()],
}),
promoted_loopbodylocals: Vec::new(), // Phase 224
condition_aliases: Vec::new(), // Phase 224-D
},
loop_scope: LoopScopeShapeBuilder::empty_body_locals(
BasicBlockId(0),

View File

@ -20,21 +20,24 @@
//! - **Fail-Fast**: Unsupported expressions → explicit error
//! - **Deterministic**: Processes variables in declaration order
//!
//! ## Scope (Phase 186)
//! ## Scope
//!
//! **Supported init expressions** (int/arithmetic only):
//! - Binary operations: `+`, `-`, `*`, `/`
//! - Constant literals: `42`, `0`, `1`
//! - Variable references: `pos`, `start`, `i`
//! **Supported init expressions**:
//! - Binary operations: `+`, `-`, `*`, `/` (Phase 186)
//! - Constant literals: `42`, `"hello"` (Phase 186, 193)
//! - Variable references: `pos`, `start`, `i` (Phase 186)
//! - Method calls: `s.substring(...)`, `digits.indexOf(ch)` (Phase 193, 225)
//! - Uses metadata-driven whitelist via `CoreMethodId::allowed_in_init()`
//! - Delegates to `MethodCallLowerer` for consistent lowering
//!
//! **NOT supported** (Fail-Fast):
//! - String operations: `s.substring(...)`, `s + "abc"`
//! - Method calls: `box.method(...)`
//! - Complex expressions: nested calls, non-arithmetic operations
//! - Complex expressions: nested calls, function calls
//! - User-defined box methods (only CoreMethodId supported)
use crate::ast::ASTNode;
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
use crate::mir::join_ir::lowering::method_call_lowerer::MethodCallLowerer;
use crate::mir::join_ir::{BinOpKind, ConstValue, JoinInst, MirLikeInst};
use crate::mir::ValueId;
@ -340,20 +343,24 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
}
}
/// Phase 193: Emit a method call in body-local init expression
/// Phase 225: Emit a method call in body-local init expression
///
/// Lowers method calls like `digits.indexOf(ch)` to JoinIR BoxCall instruction.
/// Delegates to MethodCallLowerer for metadata-driven lowering.
/// This ensures consistency and avoids hardcoded method/box name mappings.
///
/// # Supported Methods (Whitelist - Fail-Fast)
/// # Supported Methods
///
/// - `indexOf` (StringBox): Returns integer index
/// - `get` (ArrayBox): Returns element at index
/// All methods where `CoreMethodId::allowed_in_init() == true`:
/// - `substring`, `indexOf`, `upper`, `lower`, `trim` (StringBox)
/// - `get` (ArrayBox)
/// - `get`, `has`, `keys` (MapBox)
/// - And more - see CoreMethodId metadata
///
/// # Arguments
///
/// * `receiver` - Object on which method is called (must be in ConditionEnv)
/// * `method` - Method name (must be in whitelist)
/// * `args` - Method arguments (only IntLiteral and Variable supported)
/// * `method` - Method name (resolved via CoreMethodId)
/// * `args` - Method arguments (lowered recursively)
/// * `cond_env` - Condition environment for variable resolution
/// * `instructions` - Output buffer for JoinIR instructions
/// * `alloc` - ValueId allocator
@ -361,19 +368,25 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
/// # Returns
///
/// * `Ok(ValueId)` - JoinIR ValueId of method call result
/// * `Err(msg)` - Unsupported method or argument pattern
/// * `Err(msg)` - Unknown method or not allowed in init context
///
/// # Example
///
/// ```nyash
/// local digit = digits.indexOf(ch)
/// local ch = s.substring(p, p+1)
/// local digit_pos = digits.indexOf(ch)
/// ```
///
/// Emits:
/// Delegation flow:
/// ```
/// %receiver = <resolve digits from ConditionEnv>
/// %arg0 = <resolve ch from ConditionEnv>
/// %result = BoxCall("StringBox", "indexOf", [%receiver, %arg0])
/// emit_method_call_init
/// → Resolve receiver variable
/// → Delegate to MethodCallLowerer::lower_for_init
/// → Resolve method_name → CoreMethodId
/// → Check allowed_in_init()
/// → Check arity
/// → Lower arguments
/// → Emit BoxCall with metadata-driven box_name
/// ```
fn emit_method_call_init(
receiver: &ASTNode,
@ -383,161 +396,66 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
instructions: &mut Vec<JoinInst>,
alloc: &mut dyn FnMut() -> ValueId,
) -> Result<ValueId, String> {
// Method whitelist - Fail-Fast for unsupported methods
const SUPPORTED_INIT_METHODS: &[&str] = &["indexOf", "get", "toString"];
if !SUPPORTED_INIT_METHODS.contains(&method) {
return Err(format!(
"Method '{}' not supported in body-local init (Phase 193 limitation - only indexOf, get, toString supported)",
method
));
}
eprintln!("[loop_body_local_init] MethodCall: {}.{}(...)",
if let ASTNode::Variable { name, .. } = receiver { name } else { "?" },
method);
eprintln!(
"[loop_body_local_init] MethodCall: {}.{}(...)",
if let ASTNode::Variable { name, .. } = receiver {
name
} else {
"?"
},
method
);
// 1. Resolve receiver (must be a Variable in ConditionEnv)
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, .. } => cond_env.get(name).ok_or_else(|| {
format!(
"Method receiver '{}' not found in ConditionEnv (must be condition variable or loop parameter)",
name
)
})?,
_ => {
return Err(
"Complex receiver not supported in init method call (Phase 193 - only simple variables)".to_string()
"Complex receiver not supported in init method call (Phase 225 - only simple variables)"
.to_string(),
);
}
};
eprintln!("[loop_body_local_init] Receiver resolved → {:?}", receiver_id);
eprintln!(
"[loop_body_local_init] Receiver resolved → {:?}",
receiver_id
);
// 2. Lower arguments (recursively)
let arg_ids: Result<Vec<ValueId>, String> = args.iter()
.map(|arg| Self::lower_init_arg(arg, cond_env, instructions, alloc))
.collect();
let arg_ids = arg_ids?;
eprintln!("[loop_body_local_init] Args resolved → {:?}", arg_ids);
// 3. Determine box_name heuristically (Phase 193 simple heuristic)
// In real usage, type inference would provide this, but for init expressions
// we can infer from method name:
// - indexOf → StringBox
// - get → ArrayBox (generic, but we'll use "ArrayBox" as placeholder)
// - toString → IntegerBox (for loop counters)
let box_name = match method {
"indexOf" => "StringBox".to_string(),
"get" => "ArrayBox".to_string(),
"toString" => "IntegerBox".to_string(),
_ => unreachable!("Whitelist check should have caught this"),
};
// 4. Emit BoxCall instruction
let result_id = alloc();
// Build complete args: receiver + method args
let mut full_args = vec![receiver_id];
full_args.extend(arg_ids);
instructions.push(JoinInst::Compute(MirLikeInst::BoxCall {
dst: Some(result_id),
box_name,
method: method.to_string(),
args: full_args,
}));
// 2. Delegate to MethodCallLowerer for metadata-driven lowering
// This handles:
// - Method name → CoreMethodId resolution
// - allowed_in_init() whitelist check
// - Arity validation
// - Box name from CoreMethodId (no hardcoding!)
// - Argument lowering
// - BoxCall emission
//
// Note: We need to wrap alloc in a closure to match the generic type
// parameter expected by lower_for_init (F: FnMut() -> ValueId)
let mut alloc_wrapper = || alloc();
let result_id = MethodCallLowerer::lower_for_init(
receiver_id,
method,
args,
&mut alloc_wrapper,
cond_env,
instructions,
)?;
eprintln!(
"[loop_body_local_init] Emitted BoxCall{:?}",
"[loop_body_local_init] MethodCallLowerer completed{:?}",
result_id
);
Ok(result_id)
}
/// Phase 193: Lower a method call argument
///
/// Supported argument types:
/// - IntLiteral: Constant integer
/// - Variable: Variable reference (resolved from ConditionEnv)
///
/// # Arguments
///
/// * `arg` - Argument AST node
/// * `cond_env` - Condition environment for variable resolution
/// * `instructions` - Output buffer for JoinIR instructions
/// * `alloc` - ValueId allocator
///
/// # Returns
///
/// * `Ok(ValueId)` - JoinIR ValueId of argument value
/// * `Err(msg)` - Unsupported argument type
fn lower_init_arg(
arg: &ASTNode,
cond_env: &ConditionEnv,
instructions: &mut Vec<JoinInst>,
alloc: &mut dyn FnMut() -> ValueId,
) -> Result<ValueId, String> {
match arg {
// Integer literal: emit Const instruction
ASTNode::Literal { value, .. } => {
match value {
crate::ast::LiteralValue::Integer(i) => {
let vid = alloc();
instructions.push(JoinInst::Compute(MirLikeInst::Const {
dst: vid,
value: ConstValue::Integer(*i),
}));
eprintln!(
"[loop_body_local_init] Arg Const({}) → {:?}",
i, vid
);
Ok(vid)
}
crate::ast::LiteralValue::String(s) => {
let vid = alloc();
instructions.push(JoinInst::Compute(MirLikeInst::Const {
dst: vid,
value: ConstValue::String(s.clone()),
}));
eprintln!(
"[loop_body_local_init] Arg Const(\"{}\") → {:?}",
s, vid
);
Ok(vid)
}
_ => Err(format!(
"Unsupported literal type in method arg: {:?} (Phase 193 - only Integer/String supported)",
value
)),
}
}
// Variable reference: resolve from ConditionEnv
ASTNode::Variable { name, .. } => {
let vid = cond_env.get(name).ok_or_else(|| {
format!(
"Method arg variable '{}' not found in ConditionEnv",
name
)
})?;
eprintln!(
"[loop_body_local_init] Arg Variable({}) → {:?}",
name, vid
);
Ok(vid)
}
// Fail-Fast for complex expressions
_ => Err(format!(
"Complex method arguments not supported in init (Phase 193 - only int literals and variables)"
)),
}
}
}
#[cfg(test)]