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

15 KiB
Raw Blame History

Phase 224: A-4 DigitPos Promoter Design

Purpose

Implement carrier promotion support for the A-4 Digit Position pattern, enabling cascading LoopBodyLocal conditions like:

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: schdigit_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

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

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

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:

// 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:

// 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):

// 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

// 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)

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

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

// 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)

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)

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

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

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

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

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)

#[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

# 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 (chdigit_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)

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)

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:

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 ライン)