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>
15 KiB
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:
- First LoopBodyLocal:
ch = s.substring(...)(substring extraction) - Second LoopBodyLocal:
digit_pos = digits.indexOf(ch)(depends on first) - Condition:
if digit_pos < 0 { break }(comparison, NOT equality) - 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
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 (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)
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
- Build Success:
cargo build --releasewith 0 errors, 0 warnings - Unit Tests Pass: All tests in
loop_body_digitpos_promoter.rspass - Integration Test:
apps/tests/phase2235_p2_digit_pos_min.hakono longer Fail-Fasts - E2E Test: Program executes and outputs correct result (
p = 3,num_str = 123) - No Regression: Existing A-3 Trim tests still pass (e.g.,
skip_whitespacetests) - Documentation: This design doc + updated architecture overview
Implementation Order
- 224-2: This design document ✅
- 224-3: Implement
DigitPosPromoterwith unit tests - 224-4: Integrate into
LoopBodyCondPromoter - 224-5: E2E test with
phase2235_p2_digit_pos_min.hako - 224-6: Update docs and CURRENT_TASK.md
Revision History
- 2025-12-10: Phase 224 design document created
Status: Active
Scope: digitpos promoter 設計(ExprLowerer ライン)