feat(joinir): Phase 246-EX Part 1 - FromHost carrier infrastructure

Extends Phase 247-EX dual-value architecture for _atoi NumberAccumulation
support. Implements FromHost carrier handling throughout JoinIR pipeline.

## Problem Analysis

_atoi requires `result = result * 10 + digit_pos` where:
- digit_pos is promoted to dual carriers: is_digit_pos (bool) + digit_value (int)
- digit_value is used in NumberAccumulation but NOT updated itself
- Existing infrastructure filtered out carriers without updates

## Implementation

### 1. Carrier Filtering (pattern2_with_break.rs:507-514)
**Added FromHost retention**:
```rust
carrier_updates.contains_key(&carrier.name)
    || carrier.role == CarrierRole::ConditionOnly
    || carrier.init == CarrierInit::FromHost  // Phase 247-EX
```

**Effect**: Keeps digit_value carrier despite no update expression

### 2. Carrier Update Passthrough (loop_with_break_minimal.rs:411-426)
**Added FromHost passthrough**:
- FromHost carriers without updates pass through from env
- Similar to Phase 227 ConditionOnly handling
- Logged as `[loop/carrier_update] Phase 247-EX: FromHost carrier passthrough`

### 3. Exit Bindings Collection (meta_collector.rs:156-172)
**Added FromHost exit_bindings inclusion**:
```rust
Some((CarrierRole::LoopState, CarrierInit::FromHost)) => {
    // Include in exit_bindings for latch incoming
    // Not for exit PHI or variable_map
}
```

**Effect**: digit_value gets latch incoming for header PHI

## Test Results

- **Before**: 931 tests PASS
- **After**: 931 tests PASS (0 regressions)

## Verification

**Phase 247-EX UpdateEnv working**:
```
[update_env/phase247ex] Resolved promoted 'digit_pos' → 'digit_value' (integer carrier): ValueId(111)
```

**NumberAccumulation MIR generated**:
```
%39 = %14 Mul %38    ← result * 10
%40 = %39 Add %9     ← tmp + digit_value
```

## Status

-  Pattern2 classification
-  NumberAccumulation detection
-  dual-value carrier resolution
-  FromHost carrier handling
- ⚠️ RC:0 issue (runtime value problem, Part 2)

## Related

- Phase 247-EX: DigitPos dual-value architecture (commit 8900a3cc)
- Phase 227: ConditionOnly carrier handling
- Phase 228-8: ConditionOnly exit_bindings

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-11 15:28:36 +09:00
parent 8900a3cc44
commit e356524b0a
4 changed files with 104 additions and 26 deletions

View File

@ -0,0 +1,33 @@
// Phase 246-EX: Minimal _atoi test
// Tests NumberAccumulation pattern: result = result * 10 + digit_pos
static box Phase246ExAtoiMini {
main() {
local s = "42"
local len = 2
local result = me.atoi(s, len)
// Expected: 42
if result == 42 {
return "PASS"
} else {
return "FAIL"
}
}
method atoi(s, len) {
local result = 0
local digits = "0123456789"
local i = 0
loop(i < len) {
local ch = s.substring(i, i + 1)
local digit_pos = digits.indexOf(ch)
if digit_pos < 0 { break }
result = result * 10 + digit_pos
i = i + 1
}
return result
}
}

View File

@ -125,15 +125,18 @@ impl ExitMetaCollector {
bindings.push(binding);
} else {
// Phase 228-8: Check if this is a ConditionOnly carrier
use crate::mir::join_ir::lowering::carrier_info::CarrierRole;
let is_condition_only = if let Some(ci) = carrier_info {
// Phase 247-EX: Also check if this is a FromHost carrier (e.g., digit_value)
use crate::mir::join_ir::lowering::carrier_info::{CarrierRole, CarrierInit};
let carrier_meta = if let Some(ci) = carrier_info {
ci.carriers.iter()
.any(|c| c.name == *carrier_name && c.role == CarrierRole::ConditionOnly)
.find(|c| c.name == *carrier_name)
.map(|c| (c.role, c.init))
} else {
false
None
};
if is_condition_only {
match carrier_meta {
Some((CarrierRole::ConditionOnly, _)) => {
// Phase 228-8: Include ConditionOnly carrier in exit_bindings
// (needed for latch incoming, not for exit PHI)
let binding = LoopExitBinding {
@ -149,14 +152,33 @@ impl ExitMetaCollector {
);
bindings.push(binding);
} else {
}
Some((CarrierRole::LoopState, CarrierInit::FromHost)) => {
// Phase 247-EX: Include FromHost carrier in exit_bindings
// (needed for latch incoming, not for exit PHI or variable_map)
let binding = LoopExitBinding {
carrier_name: carrier_name.clone(),
join_exit_value: *join_exit_value,
host_slot: ValueId(0), // Placeholder - not used for FromHost
role: CarrierRole::LoopState,
};
eprintln!(
"[cf_loop/exit_line] ExitMetaCollector DEBUG: Carrier '{}' not in variable_map and not ConditionOnly (skip)",
"[cf_loop/exit_line] Phase 247-EX: Collected FromHost carrier '{}' JoinIR {:?} (not in variable_map)",
carrier_name, join_exit_value
);
bindings.push(binding);
}
_ => {
eprintln!(
"[cf_loop/exit_line] ExitMetaCollector DEBUG: Carrier '{}' not in variable_map and not ConditionOnly/FromHost (skip)",
carrier_name
);
}
}
}
}
if debug {
eprintln!(

View File

@ -503,10 +503,14 @@ impl MirBuilder {
//
// Phase 227: Keep ConditionOnly carriers even if they don't have updates
// (they're used in loop/break conditions, not updated)
// Phase 247-EX: Keep FromHost carriers (e.g., digit_value) even if they don't have updates
// (they're initialized from loop body and used in update expressions)
let original_carrier_count = carrier_info.carriers.len();
carrier_info.carriers.retain(|carrier| {
use crate::mir::join_ir::lowering::carrier_info::CarrierRole;
carrier_updates.contains_key(&carrier.name) || carrier.role == CarrierRole::ConditionOnly
use crate::mir::join_ir::lowering::carrier_info::{CarrierRole, CarrierInit};
carrier_updates.contains_key(&carrier.name)
|| carrier.role == CarrierRole::ConditionOnly
|| carrier.init == CarrierInit::FromHost // Phase 247-EX
});
eprintln!(

View File

@ -391,7 +391,9 @@ pub(crate) fn lower_loop_with_break_minimal(
// Phase 227: ConditionOnly carriers don't have update expressions
// They just pass through their current value unchanged
use crate::mir::join_ir::lowering::carrier_info::CarrierRole;
// Phase 247-EX: FromHost carriers (e.g., digit_value) also passthrough
// They're initialized from loop body and used in update expressions but not updated themselves
use crate::mir::join_ir::lowering::carrier_info::{CarrierRole, CarrierInit};
if carrier.role == CarrierRole::ConditionOnly {
// ConditionOnly carrier: just pass through the current value
// The carrier's ValueId from env is passed unchanged
@ -406,6 +408,23 @@ pub(crate) fn lower_loop_with_break_minimal(
continue;
}
// Phase 247-EX: FromHost carriers passthrough (no update expressions)
// FromHost carriers (e.g., digit_value) are initialized from loop body (indexOf result)
// and used in update expressions, but not updated themselves.
// They're already in env (added by Phase 176-5), so pass through from there.
if carrier.init == CarrierInit::FromHost && !carrier_updates.contains_key(carrier_name) {
// FromHost carrier without update: pass through current value from env
let current_value = env.get(carrier_name).ok_or_else(|| {
format!("FromHost carrier '{}' not found in env", carrier_name)
})?;
updated_carrier_values.push(current_value);
eprintln!(
"[loop/carrier_update] Phase 247-EX: FromHost carrier '{}' passthrough: {:?}",
carrier_name, current_value
);
continue;
}
// Get the update expression for this carrier
let update_expr = carrier_updates.get(carrier_name).ok_or_else(|| {
format!(