Files
hakorune/docs/development/current/main/phase224-digitpos-promoter-design.md
nyash-codex d4f90976da refactor(joinir): Phase 244 - ConditionLoweringBox trait unification
Unify condition lowering logic across Pattern 2/4 with trait-based API.

New infrastructure:
- condition_lowering_box.rs: ConditionLoweringBox trait + ConditionContext (293 lines)
- ExprLowerer implements ConditionLoweringBox trait (+51 lines)

Pattern migrations:
- Pattern 2 (loop_with_break_minimal.rs): Use trait API
- Pattern 4 (loop_with_continue_minimal.rs): Use trait API

Benefits:
- Unified condition lowering interface
- Extensible for future lowering strategies
- Clean API boundary between patterns and lowering logic
- Zero code duplication

Test results: 911/911 PASS (+2 new tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-11 02:35:31 +09:00

533 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
Status: Active
Scope: digitpos promoter 設計ExprLowerer ライン)