feat(joinir): Phase 224 - DigitPosPromoter for A-4 pattern (core complete)

- New DigitPosPromoter Box for cascading indexOf pattern detection
- Two-tier promotion strategy: A-3 Trim → A-4 DigitPos fallback
- Unit tests 6/6 PASS (comparison operators, cascading dependency)
- Promotion verified: digit_pos → is_digit_pos carrier

⚠️ Lowerer integration gap: lower_loop_with_break_minimal doesn't
recognize promoted variables yet (Phase 224-continuation)

🤖 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 16:21:01 +09:00
parent b5661c1915
commit 00d1ec7cc5
8 changed files with 1704 additions and 33 deletions

View File

@ -0,0 +1,358 @@
# Phase 224: A-4 DigitPos Promoter - Implementation Summary
**Date**: 2025-12-10
**Status**: Core Implementation Complete, Integration Requires Additional Work
**Branch**: main
**Commits**: TBD
---
## Executive Summary
Phase 224 successfully implemented the **DigitPosPromoter** Box for A-4 pattern (cascading indexOf) promotion, achieving:
**Complete**: DigitPosPromoter implementation with full unit test coverage (6/6 tests passing)
**Complete**: Integration into LoopBodyCondPromoter orchestrator
**Complete**: Two-tier promotion strategy (A-3 Trim → A-4 DigitPos fallback)
**Verified**: Promotion detection working correctly in Pattern2 pipeline
⚠️ **Partial**: Full E2E flow blocked by lowerer integration issue
---
## Accomplishments
### 1. Design Document (224-2) ✅
**File**: `docs/development/current/main/phase224-digitpos-promoter-design.md`
**Key Design Decisions**:
- **One Box, One Question**: DigitPosPromoter handles ONLY A-4 pattern (indexOf-based)
- **Separation of Concerns**: Trim patterns remain in LoopBodyCarrierPromoter
- **Orchestrator Pattern**: LoopBodyCondPromoter delegates to specialized promoters
- **Bool Carrier**: Promote to `is_digit_pos` (bool) for consistency with A-3 Trim
### 2. DigitPosPromoter Implementation (224-3) ✅
**File**: `src/mir/loop_pattern_detection/loop_body_digitpos_promoter.rs` (467 lines)
**Features**:
- **Pattern Detection**: Identifies cascading substring() → indexOf() → comparison
- **Comparison Operators**: Supports `<`, `>`, `<=`, `>=`, `!=` (not equality)
- **Dependency Validation**: Verifies indexOf() depends on another LoopBodyLocal
- **Comprehensive Tests**: 6 unit tests covering normal/edge cases
**Test Results**:
```bash
cargo test --release --lib digitpos
# running 6 tests
# test result: ok. 6 passed; 0 failed; 0 ignored
```
### 3. LoopBodyCondPromoter Integration (224-4) ✅
**File**: `src/mir/loop_pattern_detection/loop_body_cond_promoter.rs`
**Two-Tier Strategy**:
```
Step 1: Try A-3 Trim promotion (LoopBodyCarrierPromoter)
↓ (if fails)
Step 2: Try A-4 DigitPos promotion (DigitPosPromoter)
↓ (if fails)
Step 3: Fail-Fast with clear error message
```
**Logs Verify Success**:
```
[cond_promoter] A-3 Trim promotion failed: No promotable Trim pattern detected
[cond_promoter] Trying A-4 DigitPos promotion...
[digitpos_promoter] Phase 224: Found 1 LoopBodyLocal variables: ["digit_pos"]
[digitpos_promoter] A-4 DigitPos pattern promoted: digit_pos → is_digit_pos
[cond_promoter] A-4 DigitPos pattern promoted: 'digit_pos' → carrier 'is_digit_pos'
```
---
## Current Limitation: Lowerer Integration Gap
### Problem Statement
**Symptom**: E2E test fails despite successful promotion
**Root Cause**: `lower_loop_with_break_minimal` performs independent LoopBodyLocal check
**Result**: Promoted variables are detected as "unsupported" by the lowerer
### Error Flow
```
Phase 223.5 (Pattern2) → LoopBodyCondPromoter.try_promote() → SUCCESS ✅
Phase 180-3 (Pattern2) → TrimLoopLowerer.try_lower() → SKIP (not Trim)
Pattern2 Lowerer → lower_loop_with_break_minimal() → Analyze break condition
LoopConditionScopeBox.analyze() → Detects "digit_pos" as LoopBodyLocal
ERROR ❌: "Unsupported condition: uses loop-body-local variables: [\"digit_pos\"]"
```
### Why A-3 Trim Patterns Work
For A-3 Trim patterns, TrimLoopLowerer **rewrites the break condition** to remove LoopBodyLocal references before passing to lower_loop_with_break_minimal:
```rust
// TrimLoopLowerer returns trim_result.condition (rewritten)
let effective_break_condition = trim_result.condition; // No LoopBodyLocal!
```
But for A-4 DigitPos (Phase 223.5), we:
- Successfully promote to carrier: `digit_pos``is_digit_pos`
- Merge carrier into CarrierInfo ✅
- **BUT**: Break condition AST still contains `digit_pos`
### Root Cause Analysis
The break condition is an **AST node** containing:
```nyash
if digit_pos < 0 { break }
```
After promotion:
- CarrierInfo knows about `is_digit_pos` carrier ✅
- LoopBodyCondPromoter recorded the promotion ✅
- **But**: AST node still says `digit_pos`, not `is_digit_pos`
When lower_loop_with_break_minimal analyzes the condition:
```rust
let cond_scope = LoopConditionScopeBox::analyze(&conditions);
if cond_scope.has_loop_body_local() {
// ERROR: Still sees "digit_pos" in AST!
}
```
---
## Solution Options (Phase 224-continuation)
### Option A: AST Rewriting (Comprehensive)
**Approach**: Rewrite break condition AST to replace promoted variables with carrier references
**Implementation**:
1. After LoopBodyCondPromoter.try_promote() succeeds
2. Create ASTRewriter to traverse break_condition_node
3. Replace Variable("digit_pos") → Variable("is_digit_pos")
4. Pass rewritten condition to lower_loop_with_break_minimal
**Pros**: Clean, consistent with Trim pattern flow
**Cons**: AST rewriting is complex, error-prone
**Effort**: ~2-3 hours
### Option B: Promoted Variable Tracking (Surgical)
**Approach**: Add metadata to track promoted variables, exclude from LoopBodyLocal check
**Implementation**:
1. Add `promoted_loopbodylocals: Vec<String>` to CarrierInfo
2. In Phase 223.5, record `promoted_var` in CarrierInfo
3. Modify lower_loop_with_break_minimal signature:
```rust
fn lower_loop_with_break_minimal(
...,
promoted_vars: &[String], // NEW
) -> Result<...>
```
4. In LoopConditionScopeBox::analyze(), filter out promoted_vars:
```rust
if cond_scope.has_loop_body_local_except(promoted_vars) {
// Only error if non-promoted LoopBodyLocal exists
}
```
**Pros**: Minimal changes, surgical fix
**Cons**: Adds parameter to lowerer API
**Effort**: ~1-2 hours
### Option C: DigitPosLoopHelper Metadata (Consistent)
**Approach**: Create DigitPosLoopHelper similar to TrimLoopHelper
**Implementation**:
1. Create `loop_body_digitpos_helper.rs` (similar to trim_loop_helper.rs)
2. Attach DigitPosLoopHelper to CarrierInfo in DigitPosPromoter
3. In Phase 223.5, check for digitpos_helper (like trim_helper check at line 321)
4. In lower_loop_with_break_minimal, check CarrierInfo for helpers before error
**Pros**: Consistent with Trim pattern architecture
**Cons**: More boilerplate
**Effort**: ~2-3 hours
---
## Recommended Next Steps
### Immediate (P0 - Phase 224 Completion)
**Goal**: Unblock `phase2235_p2_digit_pos_min.hako` test
**Approach**: Implement **Option B** (Promoted Variable Tracking)
**Tasks**:
1. Add `promoted_loopbodylocals` field to CarrierInfo
2. Record promoted_var in Phase 223.5 (pattern2_with_break.rs:303)
3. Pass promoted list to lower_loop_with_break_minimal
4. Modify LoopConditionScopeBox to exclude promoted vars
5. Verify E2E test passes
**Estimated Time**: 1-2 hours
**Risk**: Low (surgical change)
### Short-term (P1 - Phase 225)
**Goal**: Extend to Pattern4 (with continue)
**Tasks**:
1. Apply same promoted variable tracking to Pattern4
2. Test with `parser_box_minimal.hako` (skip_ws pattern)
3. Verify no regression in existing Trim patterns
**Estimated Time**: 1 hour
**Risk**: Low (reuse Option B infrastructure)
### Medium-term (P2 - Phase 226)
**Goal**: A-5/A-6 patterns (multi-variable, cascading)
**Tasks**:
1. Extend DigitPosPromoter to handle multiple indexOf() calls
2. Support range check patterns (A-6: `ch < "0" || ch > "9"`)
3. Add multi-variable promotion support
**Estimated Time**: 3-4 hours
**Risk**: Medium (more complex patterns)
---
## Files Modified
### New Files (3)
- `docs/development/current/main/phase224-digitpos-promoter-design.md` (design doc)
- `src/mir/loop_pattern_detection/loop_body_digitpos_promoter.rs` (promoter implementation)
- `docs/development/current/main/PHASE_224_SUMMARY.md` (this file)
### Modified Files (2)
- `src/mir/loop_pattern_detection/mod.rs` (add digitpos_promoter module)
- `src/mir/loop_pattern_detection/loop_body_cond_promoter.rs` (two-tier integration)
### Test Coverage
- Unit tests: 6/6 passing (100%)
- E2E test: 0/1 passing (blocked by lowerer integration)
---
## Build & Test Status
### Build
```bash
cargo build --release
# Finished `release` profile [optimized] target(s) in 1m 11s
# 7 warnings (snake_case naming, visibility)
```
### Unit Tests
```bash
cargo test --release --lib digitpos
# running 6 tests
# test result: ok. 6 passed; 0 failed; 0 ignored
```
### E2E Test (Current State)
```bash
./target/release/hakorune apps/tests/phase2235_p2_digit_pos_min.hako
# [digitpos_promoter] A-4 DigitPos pattern promoted: digit_pos → is_digit_pos ✅
# [cond_promoter] A-4 DigitPos pattern promoted ✅
# ERROR: Unsupported condition: uses loop-body-local variables: ["digit_pos"] ❌
```
---
## Technical Debt & Future Work
### Code Quality Improvements
1. Fix snake_case warnings in digitpos_promoter.rs (is_indexOf → is_index_of)
2. Add LoopScopeShape visibility annotation (pub(crate) or pub)
3. Extract common AST traversal logic (find_definition_in_body) to shared module
### Documentation
1. Add DigitPosPromoter to `joinir-architecture-overview.md` box catalog
2. Update `phase223-loopbodylocal-condition-inventory.md` with Phase 224 status
3. Create integration guide for future pattern promoters
### Testing
1. Add integration tests for Pattern2+DigitPos combination
2. Add regression tests for Trim patterns (ensure no impact)
3. Add performance benchmarks for promotion detection
---
## Appendix: Key Design Insights
### Why Two Promoters Instead of One?
**Question**: Why not extend LoopBodyCarrierPromoter to handle A-4 patterns?
**Answer**: **Separation of Concerns**
- LoopBodyCarrierPromoter: Equality-based patterns (A-3 Trim)
- `ch == " " || ch == "\t"` (OR chain)
- Single LoopBodyLocal
- Well-tested, stable
- DigitPosPromoter: Comparison-based patterns (A-4 DigitPos)
- `digit_pos < 0` (comparison)
- Cascading dependencies (ch → digit_pos)
- New, experimental
**Box-First Principle**: "One Box = One Question"
### Why Bool Carrier Instead of Int?
**Question**: Why not preserve `digit_pos` as int carrier?
**Answer**: **Consistency with Existing Architecture**
- A-3 Trim patterns use bool carriers (`is_whitespace`)
- Pattern2/Pattern4 lowerers expect bool carriers in conditions
- Bool carrier simplifies condition rewriting (just a flag)
**Future**: Int carrier variant can be added for downstream use (Phase 226+)
### Why Not Rewrite AST Immediately?
**Question**: Why defer AST rewriting to Phase 224-continuation?
**Answer**: **Incremental Development**
- AST rewriting is complex and error-prone
- Surgical fix (Option B) unblocks test faster
- Learn from A-3 Trim pattern experience first
- Can refactor to AST rewriting later if needed
---
## Conclusion
Phase 224 successfully implemented the **core promotion logic** for A-4 DigitPos patterns, achieving:
- ✅ Comprehensive design document
- ✅ Robust promoter implementation with full test coverage
- ✅ Clean integration into orchestrator pattern
- ✅ Verified promotion detection in Pattern2 pipeline
**Remaining Work**: Lowerer integration (1-2 hours, Option B approach)
**Next Session**: Implement Option B (promoted variable tracking) to complete Phase 224 and unblock `phase2235_p2_digit_pos_min.hako` test.
---
## References
- [Phase 223 Inventory](phase223-loopbodylocal-condition-inventory.md) - A-4 pattern specification
- [Phase 224 Design](phase224-digitpos-promoter-design.md) - Detailed design document
- [JoinIR Architecture](joinir-architecture-overview.md) - Overall system architecture
- [Test File](../../../apps/tests/phase2235_p2_digit_pos_min.hako) - Minimal A-4 test case

View File

@ -308,26 +308,62 @@ Local Region (1000+):
- Phase 174 で `_parse_string` 最小化版(終端クォート検出)でも動作確認済み。
- → 空白文字以外の文字比較ループにも対応可能TrimLoopHelper の汎用性実証)。
- **LoopBodyCondPromoterPhase 223-3 実装完了)**
- **LoopBodyCondPromoterPhase 223-3 + 223.5 + 224 実装完了)**
- ファイル: `src/mir/loop_pattern_detection/loop_body_cond_promoter.rs`
- 責務:
- ループ条件header/break/continueに出てくる LoopBodyLocal を carrier に昇格する統一 API。
- P0Category A-3: _skip_whitespace: 単一変数の Trim パターンのみ対応
- Pattern 2/Pattern 4 両対応の薄いコーディネーター箱detection は LoopBodyCarrierPromoter に委譲)
- Pattern 2/Pattern 4 両対応の薄いコーディネーター箱detection は専門 Promoter に委譲)
- **Phase 224: Two-tier strategy** - A-3 Trim → A-4 DigitPos フォールバック型オーケストレーション
- Phase 223-3 実装内容:
- `extract_continue_condition()`: body 内の if 文から continue 条件を抽出。
- `try_promote_for_condition()`: LoopBodyCarrierPromoter を使った昇格処理。
- Pattern4 への統合完了: LoopBodyLocal 条件の昇格成功時に lowering を続行(以前は Fail-Fast
- Phase 223.5 実装内容:
- Pattern2 への統合完了: header/break 条件を分析し昇格を試みる。
- A-4digit_posテスト追加: cascading LoopBodyLocal パターンで Fail-Fast 動作を確認。
- error_messages.rs に Pattern2 用エラー関数追加: `format_error_pattern2_promotion_failed()` など。
- Phase 224 実装内容Core Implementation Complete ⚠️):
- **Two-tier promotion**: Step1 で A-3 Trim 試行 → 失敗なら Step2 で A-4 DigitPos 試行 → 両方失敗で Fail-Fast。
- **DigitPosPromoter 統合**: cascading indexOf パターンsubstring → indexOf → comparisonの昇格をサポート。
- **Unit test 完全成功**: 6/6 PASSpromoter 自体は完璧動作)。
- **Lowerer Integration Gap**: lower_loop_with_break_minimal が昇格済み変数を認識せず、独立チェックでエラー検出Phase 224-continuation で対応予定)。
- 設計原則:
- **Thin coordinator**: LoopBodyCarrierPromoter に promotion ロジックを委譲し、metadata のみ返す
- **Thin coordinator**: 専門 PromoterLoopBodyCarrierPromoter / DigitPosPromoterに昇格ロジックを委譲
- **Pattern-agnostic**: Pattern2 (break) / Pattern4 (continue) の統一入口として機能。
- **Fail-Fast**: 複雑パターンCategory B: 多段 if / method chainは引き続き reject
- **Fail-Fast with clear routing**: A-3 → A-4 順で試行し、両方失敗なら明示的エラー
- 入出力:
- 入力: `ConditionPromotionRequest`loop_param_name, cond_scope, break_cond/continue_cond, loop_body
- 出力: `ConditionPromotionResult::Promoted { carrier_info, promoted_var, carrier_name }` または `CannotPromote { reason, vars }`
- 使用元Phase 223-3 実装:
- 使用元Phase 223.5 実装完了、Phase 224 拡張済み:
- Pattern4: promotion-first昇格試行 → 成功なら CarrierInfo merge → 失敗なら Fail-Fast
- Pattern2: 既存 TrimLoopLowerer 経由で間接利用(将来的に統一可能
- Pattern2: promotion-first同上、break条件を分析対象とする
- **DigitPosPromoterPhase 224 実装完了 ⚠️ Core Complete**
- ファイル: `src/mir/loop_pattern_detection/loop_body_digitpos_promoter.rs`467 lines
- 責務:
- **A-4 pattern**: Cascading LoopBodyLocal with indexOfsubstring → indexOf → comparisonの昇格。
- **Pattern detection**: `local ch = s.substring(...); local digit_pos = digits.indexOf(ch); if digit_pos < 0 { break }`
- **Comparison operators**: `<`, `>`, `<=`, `>=`, `!=` をサポートequality `==` は A-3 Trim 領域)。
- **Dependency validation**: indexOf() が別の LoopBodyLocal に依存していることを検証cascading pattern
- Phase 224 実装内容:
- `try_promote()`: A-4 パターン検出 & bool carrier 昇格(`digit_pos` → `is_digit_pos`)。
- **Unit tests**: 6/6 PASSbasic pattern, non-indexOf rejection, no dependency rejection, comparison operators, equality rejection
- **Integration**: LoopBodyCondPromoter から A-3 Trim フォールバック後に呼ばれる。
- 設計原則:
- **One Box, One Question**: A-4 DigitPos パターン専用A-3 Trim は LoopBodyCarrierPromoter に残す)。
- **Separation of Concerns**: Trimequality-basedと DigitPoscomparison-basedを分離。
- **Bool carrier consistency**: A-3 Trim と同じく bool carrier に昇格(`is_digit_pos`)。
- 入出力:
- 入力: `DigitPosPromotionRequest`cond_scope, break_cond/continue_cond, loop_body
- 出力: `DigitPosPromotionResult::Promoted { carrier_info, promoted_var, carrier_name }` または `CannotPromote { reason, vars }`
- 現状の制約Phase 224-continuation で対応予定):
- **Promotion は成功**: LoopBodyCondPromoter → DigitPosPromoter → Promoted ✅
- **Lowerer integration gap**: lower_loop_with_break_minimal が昇格済み変数を認識せず、break condition AST に元の変数名が残っているため独立チェックでエラー。
- **Solution**: Option Bpromoted variable trackingで昇格済み変数リストを lowerer に渡す1-2h 実装予定)。
- 参考:
- 設計ドキュメント: `docs/development/current/main/phase224-digitpos-promoter-design.md`
- 完全サマリ: `docs/development/current/main/PHASE_224_SUMMARY.md`
- テストケース: `apps/tests/phase2235_p2_digit_pos_min.hako`
- **ContinueBranchNormalizer / LoopUpdateAnalyzer**
- ファイル:

View File

@ -0,0 +1,530 @@
# Phase 224: A-4 DigitPos Promoter Design
## Purpose
Implement carrier promotion support for the **A-4 Digit Position** pattern, enabling cascading LoopBodyLocal conditions like:
```nyash
loop(p < s.length()) {
local ch = s.substring(p, p+1) // First LoopBodyLocal
local digit_pos = digits.indexOf(ch) // Second LoopBodyLocal (depends on ch)
if digit_pos < 0 { // Break condition uses digit_pos
break
}
// Continue processing...
p = p + 1
}
```
This pattern is blocked by Phase 223's Fail-Fast mechanism because `digit_pos` is a LoopBodyLocal variable appearing in the loop condition.
---
## Pattern Characteristics
### A-4 Pattern Structure
**Category**: A-4 Cascading LoopBodyLocal (from phase223-loopbodylocal-condition-inventory.md)
**Key Elements**:
1. **First LoopBodyLocal**: `ch = s.substring(...)` (substring extraction)
2. **Second LoopBodyLocal**: `digit_pos = digits.indexOf(ch)` (depends on first)
3. **Condition**: `if digit_pos < 0 { break }` (comparison, NOT equality)
4. **Dependency Chain**: `s``ch``digit_pos` → condition
**AST Structure Requirements**:
- First variable must be defined by `substring()` method call
- Second variable must be defined by `indexOf()` method call
- Second variable depends on first variable (cascading)
- Break condition uses comparison operator (`<`, `>`, `<=`, `>=`, `!=`)
- NOT equality operator (`==`) like A-3 Trim pattern
---
## Design Principles
### 1. Box-First Architecture
Following Nyash's "Everything is Box" philosophy:
- **DigitPosPromoter**: Single responsibility - detect and promote A-4 pattern
- **LoopBodyCondPromoter**: Orchestrator - delegates to specialized promoters
- **CarrierInfo**: Carrier metadata container
### 2. Separation of Concerns
**LoopBodyCarrierPromoter** (existing):
- Handles A-3 Trim pattern (equality-based)
- substring() + equality chain (`ch == " " || ch == "\t"`)
- Remains specialized for Trim/whitespace patterns
**DigitPosPromoter** (new):
- Handles A-4 Digit Position pattern (comparison-based)
- substring() → indexOf() → comparison
- Specialized for cascading indexOf patterns
**LoopBodyCondPromoter** (orchestrator):
- Tries Trim promotion first (A-3)
- Falls back to DigitPos promotion (A-4)
- Returns first successful promotion or CannotPromote
### 3. Fail-Fast Philosophy
- Explicit error messages for unsupported patterns
- Early return on detection failure
- Clear distinction between "safe" and "complex" patterns
---
## API Design
### DigitPosPromotionRequest
```rust
pub struct DigitPosPromotionRequest<'a> {
/// Loop parameter name (e.g., "p")
pub loop_param_name: &'a str,
/// Condition scope analysis result
pub cond_scope: &'a LoopConditionScope,
/// Loop structure metadata (for future use)
pub scope_shape: Option<&'a LoopScopeShape>,
/// Break condition AST (Pattern2: Some, Pattern4: None)
pub break_cond: Option<&'a ASTNode>,
/// Continue condition AST (Pattern4: Some, Pattern2: None)
pub continue_cond: Option<&'a ASTNode>,
/// Loop body statements
pub loop_body: &'a [ASTNode],
}
```
### DigitPosPromotionResult
```rust
pub enum DigitPosPromotionResult {
/// Promotion successful
Promoted {
/// Carrier metadata (for Pattern2/Pattern4 integration)
carrier_info: CarrierInfo,
/// Variable name that was promoted (e.g., "digit_pos")
promoted_var: String,
/// Promoted carrier name (e.g., "is_digit")
carrier_name: String,
},
/// Cannot promote (Fail-Fast)
CannotPromote {
/// Human-readable reason
reason: String,
/// List of problematic LoopBodyLocal variables
vars: Vec<String>,
},
}
```
### DigitPosPromoter
```rust
pub struct DigitPosPromoter;
impl DigitPosPromoter {
/// Try to promote A-4 pattern (cascading indexOf)
pub fn try_promote(req: DigitPosPromotionRequest) -> DigitPosPromotionResult;
// Private helpers
fn find_indexOf_definition<'a>(body: &'a [ASTNode], var_name: &str) -> Option<&'a ASTNode>;
fn is_indexOf_method_call(node: &ASTNode) -> bool;
fn extract_comparison_var(cond: &ASTNode) -> Option<String>;
fn find_first_loopbodylocal_dependency<'a>(
body: &'a [ASTNode],
indexOf_call: &'a ASTNode
) -> Option<&'a str>;
}
```
---
## Detection Algorithm
### Step 1: Extract LoopBodyLocal Variables
From `LoopConditionScope`, extract all variables with `CondVarScope::LoopBodyLocal`.
**Expected for A-4 pattern**: `["ch", "digit_pos"]` (2 variables)
### Step 2: Find indexOf() Definition
For each LoopBodyLocal variable, find its definition in loop body:
```rust
// Search for: local digit_pos = digits.indexOf(ch)
let def_node = find_indexOf_definition(loop_body, "digit_pos");
if !is_indexOf_method_call(def_node) {
return CannotPromote("Not an indexOf() pattern");
}
```
### Step 3: Extract Comparison Variable
Extract the variable used in break/continue condition:
```rust
// Pattern: if digit_pos < 0 { break }
let cond = break_cond.or(continue_cond);
let var_in_cond = extract_comparison_var(cond);
```
### Step 4: Verify Cascading Dependency
Check that indexOf() call depends on another LoopBodyLocal (e.g., `ch`):
```rust
// digits.indexOf(ch) - extract "ch"
let dependency = find_first_loopbodylocal_dependency(loop_body, indexOf_call);
if dependency.is_none() {
return CannotPromote("indexOf() does not depend on LoopBodyLocal");
}
```
### Step 5: Build CarrierInfo
```rust
// Promote to bool carrier: is_digit = (digit_pos >= 0)
let carrier_name = format!("is_{}", var_in_cond);
let carrier_info = CarrierInfo::with_carriers(
carrier_name.clone(),
ValueId(0), // Placeholder (will be remapped)
vec![],
);
return Promoted {
carrier_info,
promoted_var: var_in_cond,
carrier_name,
};
```
---
## CarrierInfo Construction
### Carrier Type Decision
**Option A: bool carrier** (Recommended)
```rust
carrier_name: "is_digit"
carrier_type: bool
initialization: is_digit = (digit_pos >= 0)
```
**Rationale**:
- Consistent with A-3 Trim pattern (also bool)
- Simplifies condition rewriting
- Pattern2/Pattern4 integration expects bool carriers
**Option B: int carrier**
```rust
carrier_name: "digit_pos_carrier"
carrier_type: int
initialization: digit_pos_carrier = digits.indexOf(ch)
```
**Rationale**:
- Preserves original int value
- Could be useful for downstream computations
**Decision**: Use **Option A (bool carrier)** for P0 implementation. Option B can be added later if needed.
### Carrier Initialization Logic
```rust
// At loop entry (before first iteration):
local ch = s.substring(p, p+1)
local digit_pos = digits.indexOf(ch)
local is_digit = (digit_pos >= 0) // Bool carrier
// Loop condition uses carrier:
loop(p < n && is_digit) {
// Body uses original digit_pos if needed
num_str = num_str + ch
p = p + 1
// Update carrier at end of body:
ch = s.substring(p, p+1)
digit_pos = digits.indexOf(ch)
is_digit = (digit_pos >= 0)
}
```
---
## LoopBodyCondPromoter Integration
### Current Implementation (A-3 Only)
```rust
pub fn try_promote_for_condition(req: ConditionPromotionRequest) -> ConditionPromotionResult {
// Build request for LoopBodyCarrierPromoter (A-3 Trim)
let promotion_request = PromotionRequest { ... };
match LoopBodyCarrierPromoter::try_promote(&promotion_request) {
PromotionResult::Promoted { trim_info } => {
// Return Promoted with CarrierInfo
}
PromotionResult::CannotPromote { reason, vars } => {
// Fail-Fast
}
}
}
```
### Extended Implementation (A-3 → A-4 Cascade)
```rust
pub fn try_promote_for_condition(req: ConditionPromotionRequest) -> ConditionPromotionResult {
// Step 1: Try Trim promotion (A-3 pattern)
let trim_request = PromotionRequest { ... };
match LoopBodyCarrierPromoter::try_promote(&trim_request) {
PromotionResult::Promoted { trim_info } => {
eprintln!("[cond_promoter] A-3 Trim pattern promoted");
return ConditionPromotionResult::Promoted {
carrier_info: trim_info.to_carrier_info(),
promoted_var: trim_info.var_name,
carrier_name: trim_info.carrier_name,
};
}
PromotionResult::CannotPromote { .. } => {
eprintln!("[cond_promoter] A-3 Trim promotion failed, trying A-4 DigitPos");
}
}
// Step 2: Try DigitPos promotion (A-4 pattern)
let digitpos_request = DigitPosPromotionRequest {
loop_param_name: req.loop_param_name,
cond_scope: req.cond_scope,
scope_shape: req.scope_shape,
break_cond: req.break_cond,
continue_cond: req.continue_cond,
loop_body: req.loop_body,
};
match DigitPosPromoter::try_promote(digitpos_request) {
DigitPosPromotionResult::Promoted { carrier_info, promoted_var, carrier_name } => {
eprintln!("[cond_promoter] A-4 DigitPos pattern promoted");
return ConditionPromotionResult::Promoted {
carrier_info,
promoted_var,
carrier_name,
};
}
DigitPosPromotionResult::CannotPromote { reason, vars } => {
eprintln!("[cond_promoter] A-4 DigitPos promotion failed: {}", reason);
}
}
// Step 3: Fail-Fast (no pattern matched)
ConditionPromotionResult::CannotPromote {
reason: "No promotable pattern detected (tried A-3 Trim, A-4 DigitPos)".to_string(),
vars: extract_body_local_names(&req.cond_scope.vars),
}
}
```
---
## Pattern Detection Edge Cases
### Edge Case 1: Multiple indexOf() Calls
```nyash
local digit_pos1 = digits.indexOf(ch1)
local digit_pos2 = digits.indexOf(ch2)
```
**Handling**: Promote the variable that appears in the condition. Use `extract_comparison_var()` to identify it.
### Edge Case 2: indexOf() on Non-LoopBodyLocal
```nyash
local pos = fixed_string.indexOf(ch) // fixed_string is outer variable
```
**Handling**: This is NOT a cascading pattern. Return `CannotPromote("indexOf() does not depend on LoopBodyLocal")`.
### Edge Case 3: Comparison with Non-Zero
```nyash
if digit_pos < 5 { break } // Not the standard "< 0" pattern
```
**Handling**: Still promote - comparison operator is what matters, not the literal value. The carrier becomes `is_digit = (digit_pos < 5)`.
### Edge Case 4: indexOf() with Multiple Arguments
```nyash
local pos = s.indexOf(ch, start_index) // indexOf with start position
```
**Handling**: Still promote - as long as one argument is a LoopBodyLocal, it's a valid cascading pattern.
---
## Testing Strategy
### Unit Tests (in loop_body_digitpos_promoter.rs)
```rust
#[test]
fn test_digitpos_promoter_basic_pattern() {
// ch = s.substring(...) → digit_pos = digits.indexOf(ch) → if digit_pos < 0
// Expected: Promoted
}
#[test]
fn test_digitpos_promoter_non_indexOf_method() {
// ch = s.substring(...) → pos = s.length() → if pos < 0
// Expected: CannotPromote
}
#[test]
fn test_digitpos_promoter_no_loopbodylocal_dependency() {
// digit_pos = fixed_string.indexOf("x") // No LoopBodyLocal dependency
// Expected: CannotPromote
}
#[test]
fn test_digitpos_promoter_comparison_operators() {
// Test <, >, <=, >=, != operators
// Expected: All should be Promoted
}
#[test]
fn test_digitpos_promoter_equality_operator() {
// if digit_pos == -1 { break } // Equality, not comparison
// Expected: CannotPromote (this is A-3 Trim territory)
}
```
### Integration Test (Pattern2/Pattern4)
**Test File**: `apps/tests/phase2235_p2_digit_pos_min.hako`
**Before Phase 224**:
```
[joinir/freeze] LoopBodyLocal in condition: ["digit_pos"]
Cannot promote LoopBodyLocal variables ["digit_pos"]: No promotable Trim pattern detected
```
**After Phase 224**:
```
[cond_promoter] A-3 Trim promotion failed, trying A-4 DigitPos
[cond_promoter] A-4 DigitPos pattern promoted: digit_pos → is_digit
[pattern2/lowering] Using promoted carrier: is_digit
```
### E2E Test
```bash
# Compile and run
NYASH_JOINIR_DEBUG=1 ./target/release/hakorune apps/tests/phase2235_p2_digit_pos_min.hako
# Expected output:
# p = 3
# num_str = 123
# (No [joinir/freeze] error)
```
---
## Comparison: A-3 Trim vs A-4 DigitPos
| Feature | A-3 Trim (LoopBodyCarrierPromoter) | A-4 DigitPos (DigitPosPromoter) |
|---------|-----------------------------------|--------------------------------|
| **Method Call** | `substring()` | `substring()``indexOf()` |
| **Dependency** | Single LoopBodyLocal (`ch`) | Cascading (`ch``digit_pos`) |
| **Condition Type** | Equality (`==`, `!=`) | Comparison (`<`, `>`, `<=`, `>=`) |
| **Condition Structure** | OR chain: `ch == " "` \|\| `ch == "\t"` | Single comparison: `digit_pos < 0` |
| **Carrier Type** | Bool (`is_whitespace`) | Bool (`is_digit`) |
| **Pattern Count** | 1 variable | 2 variables (cascading) |
---
## Future Extensions (Post-P0)
### A-5: Multi-Variable Patterns (P2)
```nyash
local ch_s = s.substring(...)
local ch_lit = literal.substring(...)
if ch_s != ch_lit { break }
```
**Challenge**: Two independent LoopBodyLocal variables, not cascading.
### A-6: Multiple Break Conditions (P2)
```nyash
if ch < "0" || ch > "9" { break } // Range check
if pos < 0 { break } // indexOf check
```
**Challenge**: Two separate break conditions using different variables.
### Option B: Int Carrier Support
If downstream code needs the actual `digit_pos` value (not just bool), implement int carrier variant:
```rust
carrier_name: "digit_pos_carrier"
carrier_type: int
initialization: digit_pos_carrier = digits.indexOf(ch)
condition: loop(p < n && digit_pos_carrier >= 0)
```
---
## File Structure
```
src/mir/loop_pattern_detection/
├── loop_body_carrier_promoter.rs (existing - A-3 Trim)
├── loop_body_digitpos_promoter.rs (NEW - A-4 DigitPos)
├── loop_body_cond_promoter.rs (modified - orchestrator)
└── mod.rs (add pub mod)
```
---
## Success Criteria
1. **Build Success**: `cargo build --release` with 0 errors, 0 warnings
2. **Unit Tests Pass**: All tests in `loop_body_digitpos_promoter.rs` pass
3. **Integration Test**: `apps/tests/phase2235_p2_digit_pos_min.hako` no longer Fail-Fasts
4. **E2E Test**: Program executes and outputs correct result (`p = 3`, `num_str = 123`)
5. **No Regression**: Existing A-3 Trim tests still pass (e.g., `skip_whitespace` tests)
6. **Documentation**: This design doc + updated architecture overview
---
## Implementation Order
1. **224-2**: This design document ✅
2. **224-3**: Implement `DigitPosPromoter` with unit tests
3. **224-4**: Integrate into `LoopBodyCondPromoter`
4. **224-5**: E2E test with `phase2235_p2_digit_pos_min.hako`
5. **224-6**: Update docs and CURRENT_TASK.md
---
## Revision History
- **2025-12-10**: Phase 224 design document created