feat(joinir): Phase 247-EX - DigitPos dual-value architecture

Extends DigitPos promotion to generate TWO carriers for Pattern A/B support:
- Boolean carrier (is_digit_pos) for break conditions
- Integer carrier (digit_value) for NumberAccumulation

## Implementation

1. **DigitPosPromoter** (loop_body_digitpos_promoter.rs)
   - Generates dual carriers: is_<var> (bool) + <base>_value (int)
   - Smart naming: "digit_pos" → "digit" (removes "_pos" suffix)

2. **UpdateEnv** (update_env.rs)
   - Context-aware promoted variable resolution
   - Priority: <base>_value (int) → is_<var> (bool) → standard
   - Pass promoted_loopbodylocals from CarrierInfo

3. **Integration** (loop_with_break_minimal.rs)
   - UpdateEnv constructor updated to pass promoted list

## Test Results

- **Before**: 925 tests PASS
- **After**: 931 tests PASS (+6 new tests, 0 failures)

## New Tests

- test_promoted_variable_resolution_digit_pos - Full dual-value
- test_promoted_variable_resolution_fallback_to_bool - Fallback
- test_promoted_variable_not_a_carrier - Error handling

## Impact

| Pattern | Before | After |
|---------|--------|-------|
| _parse_number |  Works (bool only) |  Works (bool used, int unused) |
| _atoi |  Failed (missing int) |  READY (int carrier available!) |

🤖 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:08:14 +09:00
parent d4597dacfa
commit 8900a3cc44
12 changed files with 1868 additions and 40 deletions

View File

@ -8,7 +8,7 @@
## 🎯 今フォーカスしているテーマ2025-12-10 時点のスナップショット)
### 0. JoinIR / ExprLowerer / Pattern24 ラインPhase 230244 完了)✅
### 0. JoinIR / ExprLowerer / Pattern24 + JsonParser `_parse_number` / DigitPos ラインPhase 230247-EX 完了)✅
- Pattern14while / break / ifPHI / continue P5(Trim) でループ lowering を JoinIR 経路に一本化。
- Phase 231/236/240-EX:
@ -23,18 +23,26 @@
- Phase 243244-EX:
- Pattern3/4 の公開 API を `can_lower + lower` の最小セットに整理し、内部 helper を箱の中に閉じた。
- `loop_pattern_detection` の classify() が代表ループを P1〜P4 に分類することをユニットテストで固定。
- 現在: `cargo test --release --lib` で 909/909 テスト PASS既知 FAIL なし)。
- Phase 245-EX / 245C:
- `_parse_number` のループについて、ループ変数 `p` の header/break 条件と `p = p + 1` 更新を Pattern2 + ExprLowerer 経路に載せて本番化num_str は未導入)。
- FunctionScopeCapture / CapturedEnv を拡張し、関数パラメータ(例: `s`, `len`)をループ条件/本体で読み取り専用なら CapturedEnv 経由で ConditionEnv に載せるようにした。
- これにより、`p < s.length()` のような header 条件や JsonParser 系ループでのパラメータ参照が、ExprLowerer/ScopeManager から安全に解決される。
- Phase 247-EX:
- DigitPos promotion を二重値化し、`digit_pos` から boolean carrier `is_digit_pos`ConditionOnlyと integer carrier `digit_value`LoopStateを生成。
- UpdateEnv で `digit_pos` 解決時に `digit_value` を優先し、NumberAccumulation`result = result * 10 + digit_pos`)と break 条件の両方で DigitPos パターンが安全に利用可能に。
- 現在: `cargo test --release --lib` で 931/931 テスト PASS既知 FAIL なし)。
### 1. いまコード側で意識しておきたいフォーカス
- JoinIR ループ基盤Pattern14 + ExprLowerer + ScopeManagerは一応の完成状態に入ったので、当面は:
- JoinIR ループ基盤Pattern14 + ExprLowerer + ScopeManager + CapturedEnv)は一応の完成状態に入ったので、当面は:
- 既存パターン/箱の範囲内での **バグ修正・FailFast/invariant 追加・テスト強化** を優先する。
- JsonParser/selfhost への新しい適用や大きな仕様拡張は、docs 側で Phase 設計が固まってからコード側に持ち込む。
- コード側の短期フォーカス:
- 直近のコード側フォーカス候補:
- Phase 246-EXコード: JsonParser `_atoi` のループを Pattern2 + NumberAccumulation + DigitPos 二重値インフラ上に載せる。
- header/break 条件を ExprLowerer/ConditionEnv/FunctionScopeCapture 経由で JoinIR に lower。
- `result = result * 10 + digit_pos` を NumberAccumulation + `digit_value` carrier で扱い、従来の意味論を E2E テストで固定する。
- Pattern14 / ExprLowerer / ScopeManager まわりで、by-name ハードコードやサイレントフォールバックが見つかった場合は、
CarrierInfo / ConditionEnv / Scope 情報を使って「構造で」直す。
- Router / lowerer の要所(あり得ない PatternKind の組み合わせなど)に `debug_assert!` を追加し、
**挙動を変えない安全側の強化**を段階的に進める。
### 2. 直近で意識しておきたい設計の芯
@ -87,13 +95,19 @@
ここは「やることリスト」ではなく「今後やるとしたらこの辺」というメモだよ。
実際に着手するタイミングで、別途 Phase/タスクを切る想定。
1. JsonParser 残りループへの JoinIR 展開(設計フェーズ優先)
1. Phase 245Bコード: JsonParser `_parse_number``num_str` を LoopState carrier として Pattern2/P5 ラインに統合
- UpdateExpr で `num_str = num_str + ch` 1 形だけを許可し、StringAppend 経路で JoinIR に落とす。
- ExitMeta/JoinIR/E2E テストで num_str/p/戻り値の意味論を従来どおり固定する。
2. Phase 246-EXコード: JsonParser `_atoi` を Pattern2 + NumberAccumulation ラインに統合
- `_atoi` のループを Pattern2 detect → Pattern2 lowerer に接続し、DigitPos 二重値 + NumberAccumulation で数値更新を扱う。
- JsonParser の `_atoi` 呼び出しテスト(既存 or 追加で、JoinIR 経路でも従来どおりのパース結果になることを固定する。
3. JsonParser 残りループへの JoinIR 展開
- `_parse_array` / `_parse_object` / `_unescape_string` / 本体 `_parse_string` など。
- 既存の P2/P3/P4P5 パイプラインをどこまで延ばせるかを docs 側で設計 → コード側はその設計に沿って小さく実装。
2. selfhost depth2 ラインの再開
4. selfhost depth2 ラインの再開
- `.hako` 側で Program/MIR JSON を読んで JoinIR/MIR/VM/LLVM に流すライン。
- JsonParser 側のカバレッジが上がったあとに、小さいループから順に移植する。
3. JoinIR Verify / 最適化まわり
5. JoinIR Verify / 最適化まわり
- すでに PHI/ValueId 契約は debug ビルドで検証しているので、
必要なら SSADFA や軽い最適化Loop invariant / Strength reductionを検討。
@ -108,4 +122,3 @@
- 新しい大フェーズを始めたら:
1. まず docs 配下に `phase-XXX-*.md` を書く。
2. CURRENT_TASK には「そのフェーズの一行要約」と「今のフォーカスかどうか」だけを書く。

View File

@ -0,0 +1,276 @@
# Phase 247-EX: DigitPos Dual-Value Architecture - IMPLEMENTATION COMPLETE
**Date**: 2025-12-11
**Status**: ✅ **IMPLEMENTATION COMPLETE** - All tests passing (931/931)
**Scope**: Dual-value carrier generation for DigitPos pattern - resolves Phase 246-EX _atoi NumberAccumulation failure
---
## 🎯 Problem Statement
**Phase 246-EX Discovery**: DigitPos promotion loses integer digit value needed for NumberAccumulation.
### Two Different Patterns
**Pattern A (_parse_number)**: ✅ Works with Phase 224
```nyash
loop(p < s.length()) {
local ch = s.substring(p, p+1)
local digit_pos = digits.indexOf(ch)
if digit_pos < 0 { break } // Only needs boolean (found/not found)
num_str = num_str + ch // Uses ch, NOT digit_pos
p = p + 1
}
```
**Pattern B (_atoi)**: ❌ Failed with Phase 224 (bool only)
```nyash
loop(i < n) {
local ch = s.substring(i, i+1)
local pos = digits.indexOf(ch)
if pos < 0 { break } // Needs boolean (break condition)
v = v * 10 + pos // Needs INTEGER value for NumberAccumulation!
i = i + 1
}
```
**Root Cause**: Phase 224 DigitPos promotion only generated boolean carrier (`is_digit_pos`), losing the integer digit value needed for accumulation.
---
## ✅ Solution: Dual-Value Model
**One Question → Two Outputs**:
```
indexOf(ch) → -1 or 0-9
↓ (DigitPosPromoter Phase 247-EX)
Output A: is_digit_pos (boolean) ← Break condition: "Is ch in digits?"
Output B: digit_value (integer) ← Accumulation: "What digit value?"
```
### Naming Convention
**Phase 247-EX naming** (Design Option A - Separate naming):
```
"digit_pos" → "is_digit_pos" (boolean) + "digit_value" (integer)
"pos" → "is_pos" (boolean) + "pos_value" (integer)
```
**Base name extraction**: `"digit_pos"``"digit"` (remove `"_pos"` suffix) → `"digit_value"`
---
## 📦 Implementation Details
### 1. DigitPosPromoter Extension
**File**: `src/mir/loop_pattern_detection/loop_body_digitpos_promoter.rs`
**Changes**:
- Generate **two carriers** instead of one
- Carrier 1: `is_<var>` (boolean, ConditionOnly role)
- Carrier 2: `<base>_value` (integer, LoopState role)
- Base name extraction: remove `_pos` suffix
**Code**:
```rust
// Boolean carrier (condition-only, for break)
let promoted_carrier_bool = CarrierVar {
name: format!("is_{}", var_in_cond),
role: CarrierRole::ConditionOnly,
init: CarrierInit::BoolConst(false),
// ...
};
// Integer carrier (loop-state, for NumberAccumulation)
let base_name = if var_in_cond.ends_with("_pos") {
&var_in_cond[..var_in_cond.len() - 4]
} else {
var_in_cond.as_str()
};
let promoted_carrier_int = CarrierVar {
name: format!("{}_value", base_name),
role: CarrierRole::LoopState,
init: CarrierInit::FromHost,
// ...
};
carrier_info.carriers = vec![promoted_carrier_bool, promoted_carrier_int];
```
### 2. UpdateEnv Resolution Logic
**File**: `src/mir/join_ir/lowering/update_env.rs`
**Changes**:
- Added `promoted_loopbodylocals: &'a [String]` field
- Enhanced `resolve()` method with promoted variable logic
- Resolution priority:
1. Try `<base>_value` (integer carrier)
2. Fall back to `is_<var>` (boolean carrier, rare in updates)
3. Standard resolution
**Code**:
```rust
pub fn resolve(&self, name: &str) -> Option<ValueId> {
if self.promoted_loopbodylocals.iter().any(|v| v == name) {
// Extract base name: "digit_pos" → "digit"
let base_name = if name.ends_with("_pos") {
&name[..name.len() - 4]
} else {
name
};
// Try <base>_value (integer carrier for NumberAccumulation)
let int_carrier_name = format!("{}_value", base_name);
if let Some(value_id) = self.condition_env.get(&int_carrier_name) {
return Some(value_id);
}
// Fall back to is_<name> (boolean carrier)
let bool_carrier_name = format!("is_{}", name);
if let Some(value_id) = self.condition_env.get(&bool_carrier_name) {
return Some(value_id);
}
}
// Standard resolution
self.condition_env.get(name).or_else(|| self.body_local_env.get(name))
}
```
### 3. Call Site Updates
**File**: `src/mir/join_ir/lowering/loop_with_break_minimal.rs`
**Change**: Pass `promoted_loopbodylocals` to UpdateEnv constructor:
```rust
let update_env = UpdateEnv::new(env, body_env, &carrier_info.promoted_loopbodylocals);
```
---
## 🧪 Testing
### Unit Tests (3 new tests)
**File**: `src/mir/join_ir/lowering/update_env.rs` (tests module)
1. **`test_promoted_variable_resolution_digit_pos`** - Full dual-value resolution
2. **`test_promoted_variable_resolution_fallback_to_bool`** - Boolean-only fallback
3. **`test_promoted_variable_not_a_carrier`** - Error handling (variable not found)
**All tests pass**: ✅
### Regression Tests
**Before Phase 247-EX**: 925 tests PASS
**After Phase 247-EX**: **931 tests PASS** (+6 new tests)
**Result**: ✅ **0 FAILURES, 0 REGRESSIONS**
---
## 📊 Impact Summary
| Aspect | Before Phase 247-EX | After Phase 247-EX |
|--------|---------------------|-------------------|
| **DigitPos carriers** | 1 (boolean only) | 2 (boolean + integer) |
| **_parse_number** | ✅ Works (bool sufficient) | ✅ Works (bool used, int unused) |
| **_atoi** | ❌ Fails (missing int value) | ✅ **READY** (int carrier available) |
| **Test count** | 925 PASS | 931 PASS (+6) |
| **Lines changed** | - | +130 net (implementation + tests) |
---
## 🎯 Next Steps
### Phase 247-EX Completion Tasks
- [x] DigitPosPromoter dual-carrier generation
- [x] UpdateEnv promoted variable resolution
- [x] Unit tests for dual-value logic
- [x] Regression tests (931/931 PASS)
- [ ] **E2E Tests**: Test actual _atoi and _parse_number loops
- [ ] **Commit**: Phase 247-EX implementation
### Future E2E Verification
**Test _parse_number** (Pattern A - bool only):
```bash
./target/release/hakorune apps/tests/phase189_parse_number_mini.hako
# Expected: Works with is_digit_pos (boolean), digit_value unused
```
**Test _atoi** (Pattern B - bool + int):
```bash
./target/release/hakorune apps/tests/phase246ex_atoi_e2e_42.hako
# Expected: Works with is_pos (bool for break) + pos_value (int for accumulation)
```
---
## 🏗️ Architecture Principles
### Box-First Design
**Single Responsibility**:
- **DigitPosPromoter**: Detects indexOf patterns, generates dual carriers
- **UpdateEnv**: Resolves variables with context-aware promoted logic
- **ConditionEnv**: Stores carrier ValueIds
- **CarrierUpdateEmitter**: Emits JoinIR using resolved ValueIds
**Fail-Safe**:
- Unused carriers (e.g., `digit_value` in _parse_number) are harmless
- Resolution failures logged with warnings
- Falls back to boolean carrier if integer carrier missing
**Boundary Clarity**:
```
Input: digit_pos = indexOf(ch) (AST)
↓ DigitPosPromoter
Output: is_digit_pos (bool) + digit_value (int) (CarrierInfo)
↓ Pattern2 lowerer → ConditionEnv stores both carriers
↓ UpdateEnv resolves
Break: digit_pos → is_digit_pos (bool, via DigitPosConditionNormalizer)
Update: digit_pos → digit_value (int, via UpdateEnv promoted logic)
```
---
## 📚 References
**Design Documents**:
- [phase247-digitpos-dual-value-design.md](phase247-digitpos-dual-value-design.md) - Complete design spec
- [phase246-jsonparser-atoi-joinir-integration.md](phase246-jsonparser-atoi-joinir-integration.md) - _atoi integration plan
- [phase245-jsonparser-parse-number-joinir-integration.md](phase245-jsonparser-parse-number-joinir-integration.md) - _parse_number context
**Related Phases**:
- **Phase 224**: DigitPos promotion (boolean-only, foundation)
- **Phase 224-E**: DigitPosConditionNormalizer (AST transformation)
- **Phase 227**: CarrierRole enum (LoopState vs ConditionOnly)
- **Phase 228**: CarrierInit enum (FromHost vs BoolConst)
- **Phase 246-EX**: _atoi pattern discovery (triggered Phase 247-EX)
**Implementation Files**:
- `src/mir/loop_pattern_detection/loop_body_digitpos_promoter.rs` - Dual-carrier generation
- `src/mir/join_ir/lowering/update_env.rs` - Promoted variable resolution
- `src/mir/join_ir/lowering/loop_with_break_minimal.rs` - Pattern2 integration
---
## 🎉 Success Metrics
**All objectives achieved**:
1. Dual-carrier generation implemented
2. Context-aware resolution working
3. Zero regressions (931/931 tests PASS)
4. Clean architecture (Box-First principles)
5. Comprehensive unit tests (+3 new tests)
**Phase 247-EX Status**: ✅ **IMPLEMENTATION COMPLETE** - Ready for E2E validation and commit

View File

@ -0,0 +1,435 @@
# Phase 246-EX: JsonParser _atoi JoinIR Integration
## 0. Executive Summary
**Goal**: Integrate JsonParser's `_atoi` function into the JoinIR loop lowering system using the existing NumberAccumulation infrastructure.
**Status**: Step 0 - Infrastructure confirmed, design document created
**Key Finding**: ✅ `UpdateRhs::NumberAccumulation` already exists (Phase 190) and fully supports the `result = result * 10 + digit_pos` pattern!
---
## 1. _atoi Loop Structure Analysis
### Source Code (from `apps/json/jsonparser.hako`)
```nyash
box JsonParser {
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
}
}
```
### Loop Components
#### Loop Header
- **Condition**: `i < len`
- **Type**: Simple comparison (supported by ExprLowerer)
#### Loop Variables
| Variable | Type | Role | Initial Value |
|----------|------|------|---------------|
| `i` | position | LoopState (counter) | 0 |
| `result` | accumulator | LoopState (number accumulation) | 0 |
#### Loop Body
1. **Local declarations**:
- `ch = s.substring(i, i + 1)` - current character
- `digit_pos = digits.indexOf(ch)` - digit value (-1 if not digit)
2. **Break condition**:
- `if digit_pos < 0 { break }` - exit on non-digit character
3. **Updates**:
- `result = result * 10 + digit_pos` - number accumulation (NumberAccumulation pattern)
- `i = i + 1` - position increment (Const increment)
#### Captured Variables (Function Parameters)
- `s` - input string to parse
- `len` - length of string to process
- `digits` - pre-loop local (digit lookup string "0123456789")
---
## 2. Pattern Classification
### Pattern Type: **Pattern 2 (Break)**
**Rationale**:
- Single if-break structure
- No continue statements
- Early exit condition (non-digit character)
- Two carriers with different update patterns
### Carriers
| Carrier | Role | Update Pattern | UpdateExpr Variant |
|---------|------|----------------|-------------------|
| `i` | Loop counter | `i = i + 1` | `UpdateExpr::Const(1)` |
| `result` | Number accumulator | `result = result * 10 + digit_pos` | `UpdateExpr::BinOp { rhs: NumberAccumulation { base: 10, digit_var: "digit_pos" } }` |
---
## 3. UpdateExpr Infrastructure Confirmation (Phase 190)
### 3.1 Existing NumberAccumulation Support
**Location**: `/home/tomoaki/git/hakorune-selfhost/src/mir/join_ir/lowering/loop_update_analyzer.rs`
#### UpdateRhs Enum (lines 46-64)
```rust
pub enum UpdateRhs {
Const(i64),
Variable(String),
StringLiteral(String),
/// Phase 190: Number accumulation pattern: result = result * base + digit
NumberAccumulation {
base: i64,
digit_var: String,
},
Other,
}
```
**NumberAccumulation variant exists!**
### 3.2 Detection Logic (lines 157-192)
**Pattern Recognition**:
```rust
// Detects: (carrier * base) + digit
if matches!(operator, BinaryOperator::Add | BinaryOperator::Subtract) {
if let ASTNode::BinaryOp {
operator: BinaryOperator::Multiply,
left: mul_left,
right: mul_right,
..
} = left.as_ref() {
// Check if multiplication is: carrier * base
if mul_lhs_name == carrier_name {
if let ASTNode::Literal { value: LiteralValue::Integer(base), .. } = mul_right.as_ref() {
if let Some(digit_var) = Self::extract_variable_name(right) {
// NumberAccumulation pattern detected!
return Some(UpdateExpr::BinOp {
lhs: carrier_name.to_string(),
op,
rhs: UpdateRhs::NumberAccumulation { base: *base, digit_var },
});
}
}
}
}
}
```
**Exactly matches our pattern: `result = result * 10 + digit_pos`**
### 3.3 Emission Logic (carrier_update_emitter.rs)
**Location**: `/home/tomoaki/git/hakorune-selfhost/src/mir/join_ir/lowering/carrier_update_emitter.rs` (lines 139-170)
```rust
UpdateRhs::NumberAccumulation { base, digit_var } => {
// Step 1: Emit const for base
let base_id = alloc_value();
instructions.push(JoinInst::Compute(MirLikeInst::Const {
dst: base_id,
value: ConstValue::Integer(*base),
}));
// Step 2: Emit multiplication: tmp = carrier * base
let tmp_id = alloc_value();
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: tmp_id,
op: BinOpKind::Mul,
lhs: carrier_param,
rhs: base_id,
}));
// Step 3: Resolve digit variable
let digit_id = env.resolve(digit_var).ok_or_else(...)?;
// Step 4: Emit addition: result = tmp + digit
let result = alloc_value();
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: result,
op: *op, // Add or Subtract
lhs: tmp_id,
rhs: digit_id,
}));
}
```
**Complete emission infrastructure ready!**
### 3.4 Test Coverage
**Existing tests** (loop_update_analyzer.rs):
-`test_analyze_number_accumulation_base10()` - decimal pattern
-`test_analyze_number_accumulation_base2()` - binary pattern
-`test_analyze_number_accumulation_wrong_lhs()` - validation
**Existing tests** (carrier_update_emitter.rs):
-`test_emit_number_accumulation_base10()` - JoinIR emission
-`test_emit_number_accumulation_digit_not_found()` - error handling
---
## 4. Infrastructure Confirmation Results
### ✅ Checklist
- [x] `UpdateRhs::NumberAccumulation` variant exists
- [x] `result = result * 10 + digit_pos` pattern fully supported
- [x] Detection logic handles nested BinaryOp (Mul inside Add)
- [x] Emission logic generates correct JoinIR sequence
- [x] Unit tests cover base10 and base2 patterns
- [x] Error handling for missing digit variable
### 🎯 No Extensions Needed!
The existing Phase 190 infrastructure is **complete and ready** for _atoi integration. No modifications to UpdateExpr, detection, or emission logic required.
---
## 5. Integration Requirements
### 5.1 Condition Expression Support
#### Header Condition: `i < len`
- **Type**: Simple comparison
- **Lowering**: ExprLowerer with ConditionEnv
- **Required vars in env**: `i`, `len`
#### Break Condition: `digit_pos < 0`
- **Type**: Simple comparison
- **Lowering**: ExprLowerer or digitpos_condition_normalizer
- **Required vars in env**: `digit_pos`
### 5.2 Variable Capture Requirements
#### Function Parameters (must be in ConditionEnv)
- `s` - input string
- `len` - string length
#### Loop-Local Variables (LoopBodyLocal detection required)
- `digits` - digit lookup string (pre-loop local)
- `ch` - current character (body-local)
- `digit_pos` - digit position (body-local, used in break condition)
### 5.3 UpdateEnv Resolution
The UpdateEnv must resolve:
1. **Carriers**: `i`, `result` (from carrier params)
2. **Condition vars**: `len` (from function params)
3. **Body-locals**: `digit_pos` (from loop body)
---
## 6. Test Plan
### 6.1 E2E Test Cases
**File**: `apps/tests/json_atoi_smoke.hako`
| Input | Expected Output | Test Case |
|-------|----------------|-----------|
| `"0"` | `0` | Single digit zero |
| `"42"` | `42` | Two digits |
| `"123"` | `123` | Multiple digits |
| `"007"` | `7` | Leading zeros |
| `"123abc"` | `123` | Break at non-digit |
| `"abc"` | `0` | Immediate break (no digits) |
| `""` | `0` | Empty string |
### 6.2 JoinIR Structure Tests
**Verify**:
1. **UpdateExpr detection**:
- `i = i + 1``UpdateExpr::Const(1)`
- `result = result * 10 + digit_pos``UpdateExpr::BinOp { rhs: NumberAccumulation { base: 10, digit_var: "digit_pos" } }`
2. **CarrierInfo**:
- Both `i` and `result` marked as LoopState
- Correct initial values (both 0)
3. **ExitMeta**:
- Contains `("i", ...)` and `("result", ...)`
- Exit ValueIds available for function return
4. **JoinIR instructions**:
- Mul+Add sequence emitted for `result` update
- Correct base const (10)
- Correct digit_pos variable resolution
### 6.3 MIR Dump Verification
**Commands**:
```bash
# Basic MIR structure
./target/release/hakorune --dump-mir apps/tests/json_atoi_smoke.hako
# Detailed MIR with effects
./target/release/hakorune --dump-mir --mir-verbose --mir-verbose-effects apps/tests/json_atoi_smoke.hako
# JSON format for detailed analysis
./target/release/hakorune --emit-mir-json mir.json apps/tests/json_atoi_smoke.hako
jq '.functions[] | select(.name == "_atoi") | .blocks' mir.json
```
**Expected MIR patterns**:
- Const instruction for base 10
- Mul instruction: `%tmp = %result * %base`
- Add instruction: `%result_next = %tmp + %digit_pos`
- PHI nodes for both carriers at loop header
---
## 7. Implementation Strategy (Recommended Steps)
### Step 1: Minimal Smoke Test
- Create simple test case with hardcoded digit loop
- Verify NumberAccumulation detection works
- Confirm JoinIR emission is correct
### Step 2: LoopBodyLocal Detection
- Ensure `digit_pos` is detected as loop body local
- Verify it's available in UpdateEnv during emission
- Test break condition lowering with `digit_pos < 0`
### Step 3: Function Parameter Capture
- Verify `s` and `len` are captured correctly
- Test ConditionEnv resolution for header condition `i < len`
- Ensure ExprLowerer can access function params
### Step 4: Full _atoi Integration
- Test complete `_atoi` function from jsonparser.hako
- Verify all E2E test cases pass
- Check MIR dump for correct structure
### Step 5: Edge Cases
- Empty string handling
- Single character strings
- Non-digit immediate break
- Large numbers (overflow consideration)
---
## 8. Potential Challenges
### 8.1 LoopBodyLocal Detection
**Issue**: `digit_pos` must be recognized as a loop body local variable that is:
1. Declared inside loop body (`local digit_pos = ...`)
2. Used in break condition (`if digit_pos < 0`)
3. Available in UpdateEnv during result update emission
**Solution**: Existing LoopBodyLocalDetector should handle this (Phase 184).
### 8.2 Method Call in Loop Body
**Issue**: `s.substring(i, i + 1)` and `digits.indexOf(ch)` are method calls with multiple arguments.
**Solution**: LoopBodyLocal lowering should handle method calls as "complex expressions" (Phase 184 already supports this via `ExprLowerer`).
### 8.3 UpdateEnv Variable Resolution
**Issue**: UpdateEnv must resolve variables from three sources:
- Carriers: `i`, `result`
- Function params: `s`, `len`
- Body-locals: `ch`, `digit_pos`
**Solution**: Phase 184's `UpdateEnv::new(&cond_env, &body_env)` should handle this if:
- ConditionEnv contains function params + carriers
- LoopBodyLocalEnv contains body-local ValueIds
---
## 9. Success Criteria
### ✅ Phase 246-EX Complete When:
1. **Detection**: `result = result * 10 + digit_pos` correctly identified as NumberAccumulation
2. **Emission**: Mul+Add JoinIR sequence generated with correct ValueIds
3. **Execution**: All E2E test cases produce correct numeric outputs
4. **Validation**: MIR dump shows expected loop structure with PHI nodes
5. **Integration**: Works within full JsonParser context (not just isolated test)
---
## 10. References
### Phase 190: NumberAccumulation Infrastructure
- Detection: `/src/mir/join_ir/lowering/loop_update_analyzer.rs` (lines 157-192)
- Emission: `/src/mir/join_ir/lowering/carrier_update_emitter.rs` (lines 139-170)
- Tests: Both files contain comprehensive unit tests
### Phase 184: UpdateEnv and LoopBodyLocal
- UpdateEnv: `/src/mir/join_ir/lowering/update_env.rs`
- LoopBodyLocal detection: (search for LoopBodyLocalDetector)
### Related Phases
- Phase 176-2: Carrier update emission basics
- Phase 178: String literal updates (similar multi-carrier pattern)
- Phase 197: Loop update analyzer extraction
---
## Appendix A: Quick Reference
### Pattern Match: _atoi Loop
```
Loop Type: Pattern 2 (Break)
Header: i < len
Break: digit_pos < 0
Carriers: i (counter), result (accumulator)
Body-locals: ch, digit_pos
Captured: s, len, digits (pre-loop)
UpdateExpr:
i -> Const(1)
result -> BinOp { op: Add, rhs: NumberAccumulation { base: 10, digit_var: "digit_pos" } }
```
### JoinIR Emission Sequence (NumberAccumulation)
```
%base_10 = Const(10)
%tmp = BinOp(Mul, %result_param, %base_10)
%digit_id = <resolved from UpdateEnv>
%result_next = BinOp(Add, %tmp, %digit_id)
```
### Debug Commands
```bash
# Trace variable mapping
NYASH_TRACE_VARMAP=1 cargo test --release test_json_atoi -- --nocapture
# JoinIR debug
NYASH_JOINIR_DEBUG=1 ./target/release/hakorune apps/tests/json_atoi_smoke.hako 2>&1 | grep "\[trace:"
# UpdateExpr detection (if logging added)
NYASH_UPDATE_ANALYZER_DEBUG=1 ./target/release/hakorune apps/tests/json_atoi_smoke.hako
```
---
**Document Status**: ✅ Infrastructure confirmed, ready for implementation
**Next Step**: Phase 246-EX Step 1 - Minimal smoke test creation
**Last Updated**: 2025-12-11 (Phase 246-EX Step 0 completion)

View File

@ -0,0 +1,553 @@
# Phase 247-EX: DigitPos Dual-Value Design
Status: Active
Scope: DigitPos パターンを「1つの質問→2つの値」に整理し、break 条件と NumberAccumulation 両方に対応。
## 1. DigitPos の二重値化モデル
### 1.1 問題の本質
**Phase 224 の現状**:
```rust
// Phase 224: digit_pos (integer) → is_digit_pos (boolean) に変換
digit_pos = digits.indexOf(ch) // -1 or 0-9
(DigitPosPromoter)
is_digit_pos (boolean carrier) // indexOf() の成否のみ
```
**何が失われるか**:
- indexOf() の戻り値0-9 の digit 値)が消える
- break 条件 `digit_pos < 0``!is_digit_pos` で対応できるPhase 224-E で実装済み)
- しかし NumberAccumulation `result = result * 10 + digit_pos` で digit 値が必要!
**具体例**:
```nyash
// _atoi ループ (_parse_number も同様)
loop(i < n) {
local ch = s.substring(i, i+1)
local digit_pos = digits.indexOf(ch) // -1 (not found) or 0-9 (digit value)
if digit_pos < 0 { break } // Break 用途: 成否判定boolean
v = v * 10 + digit_pos // Accumulation 用途: digit 値integer
i = i + 1
}
```
### 1.2 解決: 二重値化アーキテクチャ
```
digit_pos = digits.indexOf(ch)
↓ (DigitPosPromoter 拡張)
Output A: is_digit_pos (boolean carrier) ← break 条件用
Output B: digit_value (integer carrier) ← accumulation 用
```
**質問と出力の対応**:
- **質問**: この ch は digits に含まれているか? index はいくつか?
- **出力A**: `is_digit_pos: bool` - 含まれているかcondition 側)
- **出力B**: `digit_value: i64` - index 値accumulation 側)
**両方を CarrierInfo に含める**:
```rust
// Phase 247-EX: DigitPos dual-value promotion
CarrierInfo {
carriers: vec![
CarrierVar {
name: "is_digit_pos",
role: CarrierRole::ConditionOnly, // Exit PHI 不要
init: CarrierInit::BoolConst(false),
},
CarrierVar {
name: "digit_value",
role: CarrierRole::LoopState, // Exit PHI 必要
init: CarrierInit::FromHost,
},
],
promoted_loopbodylocals: vec!["digit_pos".to_string()],
}
```
---
## 2. 箱の責務分割
### 2.1 DigitPosPromoter (拡張)
**Phase 224 の責務**:
- Input: `digit_pos = digits.indexOf(ch)` (LoopBodyLocal)
- Output: `is_digit_pos` (boolean carrier)
**Phase 247-EX の拡張**:
- Input: 同上
- Output:
- `is_digit_pos` (boolean carrier) - 既存
- `digit_value` (integer carrier) - **新規追加**
**実装方針**:
```rust
// Phase 247-EX: Dual-value carrier creation
let promoted_carrier_bool = CarrierVar {
name: format!("is_{}", var_in_cond), // "is_digit_pos"
host_id: ValueId(0),
join_id: None,
role: CarrierRole::ConditionOnly,
init: CarrierInit::BoolConst(false),
};
let promoted_carrier_int = CarrierVar {
name: format!("{}_value", var_in_cond), // "digit_pos_value" or "digit_value"
host_id: ValueId(0),
join_id: None,
role: CarrierRole::LoopState,
init: CarrierInit::FromHost,
};
let mut carrier_info = CarrierInfo::with_carriers(
"__dummy_loop_var__".to_string(),
ValueId(0),
vec![promoted_carrier_bool, promoted_carrier_int],
);
```
### 2.2 CarrierInfo
**Phase 247-EX で含まれるもの**:
```rust
CarrierInfo {
carriers: vec![
// Boolean carrier (condition 用)
CarrierVar {
name: "is_digit_pos",
role: ConditionOnly,
init: BoolConst(false),
},
// Integer carrier (accumulation 用)
CarrierVar {
name: "digit_value",
role: LoopState,
init: FromHost,
},
],
promoted_loopbodylocals: vec!["digit_pos"],
}
```
**ScopeManager による解決**:
- Break 条件: `digit_pos``is_digit_pos` (ConditionEnv)
- Update 式: `digit_pos``digit_value` (UpdateEnv)
### 2.3 ConditionEnv / ExprLowerer
**Phase 224-E の既存動作**:
```rust
// Break condition: digit_pos < 0
// ↓ (DigitPosConditionNormalizer)
// !is_digit_pos
```
**Phase 247-EX で不変**:
- Break 条件は引き続き `is_digit_pos` を参照
- ConditionEnv は `digit_pos``is_digit_pos` を解決
### 2.4 UpdateEnv / NumberAccumulation
**Phase 247-EX の新規動作**:
```rust
// Update expression: v = v * 10 + digit_pos
// ↓ (UpdateEnv resolution)
// v = v * 10 + digit_value
```
**UpdateEnv の変更**:
- `digit_pos` 参照時に `digit_value` を解決
- NumberAccumulation 検出時に `digit_var: "digit_pos"``"digit_value"` に変換
**CarrierUpdateEmitter の変更**:
- `UpdateRhs::NumberAccumulation { digit_var }``digit_var` を UpdateEnv から解決
- UpdateEnv が既に `digit_pos``digit_value` を解決済みと仮定
---
## 3. 影響範囲(パターン別)
### 3.1 Pattern2: _parse_number
**ループ構造**:
```nyash
loop(p < s.length()) {
local ch = s.substring(p, p+1)
local digit_pos = digits.indexOf(ch)
if digit_pos < 0 { break } // Break 条件
num_str = num_str + ch // StringAppend (digit_pos 不使用)
p = p + 1
}
```
**変数の役割**:
| Variable | Type | Usage | Carrier Type |
|----------|------|-------|--------------|
| `p` | position | loop counter | LoopState |
| `num_str` | string | accumulator | LoopState |
| `ch` | string | LoopBodyLocal | - |
| `digit_pos` | integer | LoopBodyLocal | → `is_digit_pos` (ConditionOnly) + `digit_value` (unused) |
**Phase 247-EX での扱い**:
- Header: `p < s.length()` (ExprLowerer)
- Break: `digit_pos < 0``!is_digit_pos` (ConditionEnv + Normalizer)
- Update:
- `p = p + 1` (Increment)
- `num_str = num_str + ch` (StringAppend)
- **digit_value は生成されるが使用されない**(無駄だが害なし)
### 3.2 Pattern2: _atoi
**ループ構造**:
```nyash
loop(i < n) {
local ch = s.substring(i, i+1)
local pos = digits.indexOf(ch)
if pos < 0 { break } // Break 条件
v = v * 10 + pos // NumberAccumulation (pos 使用!)
i = i + 1
}
```
**変数の役割**:
| Variable | Type | Usage | Carrier Type |
|----------|------|-------|--------------|
| `i` | position | loop counter | LoopState |
| `v` | integer | number accumulator | LoopState |
| `ch` | string | LoopBodyLocal | - |
| `pos` | integer | LoopBodyLocal | → `is_pos` (ConditionOnly) + `pos_value` (LoopState) |
**Phase 247-EX での扱い**:
- Header: `i < n` (ExprLowerer)
- Break: `pos < 0``!is_pos` (ConditionEnv + Normalizer)
- Update:
- `i = i + 1` (Increment)
- `v = v * 10 + pos``v = v * 10 + pos_value` (NumberAccumulation)
- **両方の carrier が必要!**
### 3.3 Pattern2: _atof_loop [Future]
**ループ構造** (想定):
```nyash
loop(i < n) {
local ch = s.substring(i, i+1)
local digit_pos = digits.indexOf(ch)
if digit_pos < 0 { break }
result = result + digit_pos * place_value
place_value = place_value / 10
i = i + 1
}
```
**Phase 247-EX での扱い**:
- 同じ二重値化モデルが適用可能
- `digit_value` を乗算・累積に使用
---
## 4. 実装アーキテクチャ
### 4.1 データフロー図
```
DigitPosPromoter
├→ is_digit_pos (boolean)
│ ↓
│ ConditionEnv (Phase 245C capture)
│ ├→ ExprLowerer (break condition)
│ └→ is_digit_pos → boolean in scope
└→ digit_value (integer)
UpdateEnv
├→ digit_pos reference → digit_value (integer)
└→ NumberAccumulation pattern resolution
```
### 4.2 命名規則
**Phase 247-EX の命名戦略**:
```rust
// Option A: Separate naming (推奨)
is_digit_pos // boolean carrier (condition 用)
digit_value // integer carrier (accumulation 用)
// Option B: Prefix naming
is_digit_pos // boolean
digit_pos_value // integer (冗長だが明示的)
// Option C: Short naming
is_digit_pos // boolean
digit_pos // integer (元の名前を保持、混乱の恐れ)
```
**採用**: Option ASeparate naming
- 理由: 用途が明確、混乱が少ない、既存の `is_*` 命名規則と整合
### 4.3 ScopeManager の解決ルール
**ConditionEnv (break 条件)**:
```rust
// digit_pos 参照 → is_digit_pos (boolean carrier)
env.get("digit_pos") Some(is_digit_pos_value_id)
```
**UpdateEnv (update 式)**:
```rust
// digit_pos 参照 → digit_value (integer carrier)
env.resolve("digit_pos") Some(digit_value_value_id)
```
**実装方針**:
1. CarrierInfo に両方の carrier を登録
2. ScopeManager が context 依存で正しい carrier を返す
- ConditionEnv: `promoted_loopbodylocals` に含まれる名前 → `is_*` carrier
- UpdateEnv: `promoted_loopbodylocals` に含まれる名前 → `*_value` carrier
---
## 5. 責務の明確化
| Component | Input | Output | 用途 |
|-----------|-------|--------|------|
| **DigitPosPromoter** | indexOf() result | `is_digit_pos` + `digit_value` | 二重値化 |
| **CarrierInfo** | DigitPos values | Both as LoopState | キャリア登録 |
| **ConditionEnv** | Promoted carriers | `is_digit_pos` (bool) | Break 条件 |
| **UpdateEnv** | Promoted carriers | `digit_value` (i64) | Accumulation |
| **ExprLowerer** | Condition AST | boolean ValueId | Lowering |
| **DigitPosConditionNormalizer** | `digit_pos < 0` | `!is_digit_pos` | AST 変換 |
| **CarrierUpdateEmitter** | UpdateExpr | JoinIR instructions | Update 生成 |
**Phase 247-EX の変更箇所**:
-**DigitPosPromoter**: 2つの carrier を生成
-**CarrierInfo**: 両方を含める
-**ConditionEnv**: `digit_pos``is_digit_pos` 解決(既存)
- 🆕 **UpdateEnv**: `digit_pos``digit_value` 解決(新規)
-**DigitPosConditionNormalizer**: `digit_pos < 0``!is_digit_pos`(既存)
- 🆕 **CarrierUpdateEmitter**: `digit_value` を UpdateEnv から解決(新規)
---
## 6. テスト戦略
### 6.1 DigitPos Promoter 単体テスト
**既存テスト** (loop_body_digitpos_promoter.rs):
- ✅ Phase 224: `is_digit_pos` 側の生成を確認
- 🆕 Phase 247-EX: `digit_value` 側も生成されることを確認
**新規テスト**:
```rust
#[test]
fn test_digitpos_dual_value_carriers() {
// digit_pos = indexOf(ch) → is_digit_pos + digit_value
let result = DigitPosPromoter::try_promote(req);
match result {
Promoted { carrier_info, .. } => {
assert_eq!(carrier_info.carriers.len(), 2);
// Boolean carrier
let bool_carrier = &carrier_info.carriers[0];
assert_eq!(bool_carrier.name, "is_digit_pos");
assert_eq!(bool_carrier.role, CarrierRole::ConditionOnly);
// Integer carrier
let int_carrier = &carrier_info.carriers[1];
assert_eq!(int_carrier.name, "digit_value");
assert_eq!(int_carrier.role, CarrierRole::LoopState);
}
_ => panic!("Expected Promoted"),
}
}
```
### 6.2 _parse_number (Pattern2) E2E テスト
**テストファイル**: `apps/tests/phase189_parse_number_mini.hako`
**期待される動作**:
- Break 条件: `digit_pos < 0``is_digit_pos` で動作
- `digit_value` carrier は生成されるが未使用(害なし)
- String 累積(`num_str`)は別の carrier で処理
**検証コマンド**:
```bash
NYASH_JOINIR_DEBUG=1 ./target/release/hakorune apps/tests/phase189_parse_number_mini.hako
```
**期待出力**:
```
[digitpos_promoter] A-4 DigitPos pattern promoted: digit_pos → is_digit_pos + digit_value
[pattern2/lowering] Using promoted carrier: is_digit_pos (condition)
[pattern2/lowering] Carrier 'digit_value' registered but unused
```
### 6.3 _atoi (Pattern2) E2E テスト
**テストファイル**: `apps/tests/phase189_atoi_mini.hako` または `tests/phase246_json_atoi.rs`
**期待される動作**:
- Break 条件: `pos < 0``is_pos` で動作
- NumberAccumulation: `v = v * 10 + pos``v = v * 10 + pos_value` で動作
- 両方の carrier が正しく使用される
**検証コマンド**:
```bash
NYASH_JOINIR_DEBUG=1 cargo test --release phase246_json_atoi -- --nocapture
```
**期待出力**:
```
[digitpos_promoter] A-4 DigitPos pattern promoted: pos → is_pos + pos_value
[pattern2/lowering] Using promoted carrier: is_pos (condition)
[pattern2/lowering] Using promoted carrier: pos_value (accumulation)
[carrier_update] NumberAccumulation: digit_var='pos' resolved to pos_value
```
### 6.4 Regression テスト
**対象**:
- Phase 224 系テストDigitPos promotion 基本動作)
- Phase 245 系テスト_parse_number 系)
- Phase 246 系テスト_atoi 系)
**確認項目**:
- 既存の `is_digit_pos` carrier が引き続き動作
- 新規の `digit_value` carrier が追加されても退行なし
---
## 7. 成功条件
### 7.1 ビルド
- [ ] `cargo build --release` 成功0 errors, 0 warnings
### 7.2 単体テスト
- [ ] DigitPosPromoter テスト: 2つの carrier 生成を確認
- [ ] CarrierInfo テスト: 両方の carrier が含まれることを確認
- [ ] UpdateEnv テスト: `digit_pos``digit_value` 解決を確認
### 7.3 E2E テスト
- [ ] _parse_number: `digit_value` 未使用でも正常動作
- [ ] _atoi: `digit_value` を NumberAccumulation で使用
- [ ] Phase 224/245/246 既存テスト: 退行なし
### 7.4 ドキュメント
- [ ] このドキュメント完成
- [ ] 箱の責務が明確に書かれている
- [ ] テスト戦略が具体的
---
## 8. 将来への展開
### 8.1 他のパターンへの適用
同じ二重値化モデルを以下に適用可能:
**_atof_loop** (小数パース):
- `digit_pos``is_digit_pos` + `digit_value`
- 小数点以下の桁数計算にも使用可能
**汎用的な indexOf パターン**:
- 任意の indexOf() 呼び出しで同様の二重値化を適用
- condition-only 用途と value 用途を自動判定
### 8.2 Pattern3/4 への拡張
**Pattern3 (if-sum)**:
- 複数の条件分岐でも同じ二重値化モデルを適用
- Exit PHI で両方の carrier を適切に扱う
**Pattern4 (continue)**:
- continue 条件でも boolean carrier を使用
- update 式で integer carrier を使用
### 8.3 最適化の可能性
**未使用 carrier の削除**:
- `_parse_number` のように `digit_value` が未使用の場合、最適化で削除可能
- CarrierInfo に "used" フラグを追加して不要な carrier を省略
**命名の統一化**:
- `digit_pos``digit_pos` (integer) + `is_digit_pos` (boolean)
- UpdateEnv/ConditionEnv が型情報から自動判定
---
## 9. Box-First 原則の適用
### 9.1 Single Responsibility
- **DigitPosPromoter**: indexOf パターン検出と二重値化のみ
- **CarrierInfo**: Carrier メタデータ保持のみ
- **ConditionEnv**: Condition 側の解決のみ
- **UpdateEnv**: Update 側の解決のみ
### 9.2 Fail-Safe Design
- 未使用の carrier も生成(害なし)
- 解決失敗時は明示的エラーpanic なし)
- 退行テストで既存動作を保証
### 9.3 Boundary Clarity
```
Input: digit_pos = indexOf(ch) (AST)
Output: is_digit_pos (bool) + digit_value (i64) (CarrierInfo)
Usage: ConditionEnv → is_digit_pos
UpdateEnv → digit_value
```
---
## 10. 参考資料
### 10.1 Phase ドキュメント
- **Phase 224**: `phase224-digitpos-promoter-design.md` - DigitPos promotion 基本設計
- **Phase 224-E**: `phase224-digitpos-condition-normalizer.md` - AST 変換設計
- **Phase 245**: `phase245-jsonparser-parse-number-joinir-integration.md` - _parse_number 統合
- **Phase 246**: `phase246-jsonparser-atoi-joinir-integration.md` - _atoi 統合
- **Phase 227**: CarrierRole 導入LoopState vs ConditionOnly
- **Phase 228**: CarrierInit 導入FromHost vs BoolConst
### 10.2 コード参照
- **DigitPosPromoter**: `src/mir/loop_pattern_detection/loop_body_digitpos_promoter.rs`
- **CarrierInfo**: `src/mir/join_ir/lowering/carrier_info.rs`
- **ConditionEnv**: `src/mir/join_ir/lowering/condition_env.rs`
- **UpdateEnv**: `src/mir/join_ir/lowering/update_env.rs`
- **CarrierUpdateEmitter**: `src/mir/join_ir/lowering/carrier_update_emitter.rs`
- **LoopUpdateAnalyzer**: `src/mir/join_ir/lowering/loop_update_analyzer.rs`
### 10.3 テストファイル
- **_parse_number**: `apps/tests/phase189_parse_number_mini.hako`, `tests/phase245_json_parse_number.rs`
- **_atoi**: `apps/tests/phase189_atoi_mini.hako`, `tests/phase246_json_atoi.rs`
- **DigitPos unit**: `src/mir/loop_pattern_detection/loop_body_digitpos_promoter.rs` (tests module)
---
## 11. Revision History
- **2025-12-11**: Phase 247-EX 設計ドキュメント作成
- **Status**: Active - 設計確定、実装準備完了
- **Scope**: DigitPos 二重値化設計ExprLowerer + NumberAccumulation ライン)

View File

@ -739,4 +739,53 @@ mod tests {
"Pattern2 lowerer should accept JsonParser-like break loop"
);
}
#[test]
fn test_atoi_loop_routed_to_pattern2() {
// Phase 246-EX Step 2: _atoi loop router integration test
// loop(i < len) {
// if digit_pos < 0 { break }
// result = result * 10 + digit_pos // NumberAccumulation
// i = i + 1
// }
let condition = bin(BinaryOperator::Less, var("i"), var("len"));
let break_cond = bin(BinaryOperator::Less, var("digit_pos"), lit_i(0));
// result = result * 10 + digit_pos (NumberAccumulation pattern)
let mul_expr = bin(BinaryOperator::Multiply, var("result"), lit_i(10));
let result_update_value = bin(BinaryOperator::Add, mul_expr, var("digit_pos"));
let body = vec![
ASTNode::If {
condition: Box::new(break_cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
ASTNode::Assignment {
target: Box::new(var("result")),
value: Box::new(result_update_value),
span: span(),
},
ASTNode::Assignment {
target: Box::new(var("i")),
value: Box::new(bin(BinaryOperator::Add, var("i"), lit_i(1))),
span: span(),
},
];
let ctx = LoopPatternContext::new(&condition, &body, "_atoi", true);
let builder = MirBuilder::new();
// Verify pattern classification
assert_eq!(ctx.pattern_kind, LoopPatternKind::Pattern2Break,
"_atoi loop should be classified as Pattern2Break");
// Verify Pattern2 lowerer accepts it
assert!(
can_lower(&builder, &ctx),
"Pattern2 lowerer should accept _atoi loop with NumberAccumulation"
);
}
}

View File

@ -686,7 +686,8 @@ mod tests {
};
let (cond_env, body_env) = test_update_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
let mut value_counter = 110u32;
let mut alloc_value = || {
@ -734,7 +735,8 @@ mod tests {
let mut body_env = LoopBodyLocalEnv::new();
body_env.insert("x".to_string(), ValueId(200)); // Body-local: x=200 (should be ignored)
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
let carrier = test_carrier("sum", 200);
let update = UpdateExpr::BinOp {
@ -774,7 +776,8 @@ mod tests {
fn test_emit_update_with_env_variable_not_found() {
// Phase 184: Test error when variable not in either env
let (cond_env, body_env) = test_update_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
let carrier = test_carrier("sum", 200);
let update = UpdateExpr::BinOp {
@ -809,7 +812,8 @@ mod tests {
fn test_emit_update_with_env_const_update() {
// Phase 184: Test UpdateEnv with simple const update (baseline)
let (cond_env, body_env) = test_update_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
let carrier = test_carrier("count", 100);
let update = UpdateExpr::Const(1);
@ -842,7 +846,8 @@ mod tests {
cond_env.insert("digit".to_string(), ValueId(30)); // Digit variable
let body_env = LoopBodyLocalEnv::new();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
let carrier = test_carrier("result", 200);
let update = UpdateExpr::BinOp {
@ -921,7 +926,8 @@ mod tests {
// Note: digit NOT in env
let body_env = LoopBodyLocalEnv::new();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
let carrier = test_carrier("result", 200);
let update = UpdateExpr::BinOp {

View File

@ -486,4 +486,164 @@ mod tests {
// Should detect assignment but with Other (complex) RHS
assert_eq!(updates.len(), 0); // Won't match because lhs != carrier
}
#[test]
fn test_analyze_num_str_string_append() {
// Phase 245B: Test case: num_str = num_str + ch (string append pattern)
use crate::ast::Span;
let body = vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "num_str".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "num_str".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "ch".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}];
let carriers = vec![CarrierVar {
name: "num_str".to_string(),
host_id: crate::mir::ValueId(0),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost,
}];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
assert_eq!(updates.len(), 1);
assert!(updates.contains_key("num_str"));
if let Some(UpdateExpr::BinOp { lhs, op, rhs }) = updates.get("num_str") {
assert_eq!(lhs, "num_str");
assert_eq!(*op, BinOpKind::Add);
if let UpdateRhs::Variable(var_name) = rhs {
assert_eq!(var_name, "ch");
} else {
panic!("Expected Variable('ch'), got {:?}", rhs);
}
} else {
panic!("Expected BinOp, got {:?}", updates.get("num_str"));
}
}
#[test]
fn test_atoi_update_expr_detection() {
// Phase 246-EX Step 3: _atoi loop multi-carrier update detection
// Tests two carriers with different update patterns:
// - i = i + 1 (Const increment)
// - result = result * 10 + digit_pos (NumberAccumulation)
use crate::ast::Span;
let body = vec![
// result = result * 10 + digit_pos
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "result".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Multiply,
left: Box::new(ASTNode::Variable {
name: "result".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "digit_pos".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
// i = i + 1
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
let carriers = vec![
CarrierVar {
name: "result".to_string(),
host_id: crate::mir::ValueId(0),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost,
},
CarrierVar {
name: "i".to_string(),
host_id: crate::mir::ValueId(1),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost,
},
];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
// Verify both carriers have updates
assert_eq!(updates.len(), 2, "Should detect updates for both i and result");
// Verify i = i + 1 (Const increment)
if let Some(UpdateExpr::BinOp { lhs, op, rhs }) = updates.get("i") {
assert_eq!(lhs, "i");
assert_eq!(*op, BinOpKind::Add);
if let UpdateRhs::Const(n) = rhs {
assert_eq!(*n, 1, "i should increment by 1");
} else {
panic!("Expected Const(1) for i update, got {:?}", rhs);
}
} else {
panic!("Expected BinOp for i update, got {:?}", updates.get("i"));
}
// Verify result = result * 10 + digit_pos (NumberAccumulation)
if let Some(UpdateExpr::BinOp { lhs, op, rhs }) = updates.get("result") {
assert_eq!(lhs, "result");
assert_eq!(*op, BinOpKind::Add);
if let UpdateRhs::NumberAccumulation { base, digit_var } = rhs {
assert_eq!(*base, 10, "NumberAccumulation should use base 10");
assert_eq!(digit_var, "digit_pos", "Should use digit_pos variable");
} else {
panic!("Expected NumberAccumulation for result update, got {:?}", rhs);
}
} else {
panic!("Expected BinOp for result update, got {:?}", updates.get("result"));
}
}
}

View File

@ -415,9 +415,10 @@ pub(crate) fn lower_loop_with_break_minimal(
})?;
// Phase 185-2: Emit carrier update with body-local support
// Phase 247-EX: Pass promoted_loopbodylocals for dual-value carrier resolution
let updated_value = if let Some(ref body_env) = body_local_env {
// Use UpdateEnv for body-local variable resolution
let update_env = UpdateEnv::new(env, body_env);
let update_env = UpdateEnv::new(env, body_env, &carrier_info.promoted_loopbodylocals);
emit_carrier_update_with_env(
carrier,
update_expr,

View File

@ -1,9 +1,18 @@
//! Phase 184: Update Expression Environment
//! Phase 247-EX: Extended with promoted variable resolution for dual-value carriers
//!
//! This module provides a unified variable resolution layer for carrier update expressions.
//! It combines ConditionEnv (condition variables) and LoopBodyLocalEnv (body-local variables)
//! with clear priority order.
//!
//! ## Phase 247-EX: Promoted Variable Resolution
//!
//! For promoted LoopBodyLocal variables (e.g., `digit_pos` → `is_digit_pos` + `digit_value`):
//! - When resolving `digit_pos` in update expressions (e.g., `result = result * 10 + digit_pos`)
//! - Try `<var>_value` first (e.g., `digit_value`)
//! - Then fall back to `is_<var>` (boolean carrier, less common in updates)
//! - Finally fall back to standard resolution
//!
//! ## Design Philosophy
//!
//! **Single Responsibility**: This module ONLY handles variable resolution priority logic.
@ -49,7 +58,7 @@ use crate::mir::ValueId;
/// ```ignore
/// let condition_env = /* ... i, sum ... */;
/// let body_local_env = /* ... temp ... */;
/// let update_env = UpdateEnv::new(&condition_env, &body_local_env);
/// let update_env = UpdateEnv::new(&condition_env, &body_local_env, &[]);
///
/// // Resolve "sum" → ConditionEnv (priority 1)
/// assert_eq!(update_env.resolve("sum"), Some(ValueId(X)));
@ -60,6 +69,17 @@ use crate::mir::ValueId;
/// // Resolve "unknown" → None
/// assert_eq!(update_env.resolve("unknown"), None);
/// ```
///
/// # Phase 247-EX Example
///
/// ```ignore
/// // digit_pos promoted → is_digit_pos (bool) + digit_value (i64)
/// let promoted = vec!["digit_pos".to_string()];
/// let update_env = UpdateEnv::new(&condition_env, &body_local_env, &promoted);
///
/// // Resolve "digit_pos" in NumberAccumulation → digit_value (integer carrier)
/// assert_eq!(update_env.resolve("digit_pos"), Some(ValueId(X))); // digit_value
/// ```
#[derive(Debug)]
pub struct UpdateEnv<'a> {
/// Condition variable environment (priority 1)
@ -67,6 +87,10 @@ pub struct UpdateEnv<'a> {
/// Body-local variable environment (priority 2)
body_local_env: &'a LoopBodyLocalEnv,
/// Phase 247-EX: List of promoted LoopBodyLocal variable names
/// For these variables, resolve to <var>_value carrier instead of is_<var>
promoted_loopbodylocals: &'a [String],
}
impl<'a> UpdateEnv<'a> {
@ -76,22 +100,28 @@ impl<'a> UpdateEnv<'a> {
///
/// * `condition_env` - Condition variable environment (highest priority)
/// * `body_local_env` - Body-local variable environment (fallback)
/// * `promoted_loopbodylocals` - Phase 247-EX: List of promoted variable names
pub fn new(
condition_env: &'a ConditionEnv,
body_local_env: &'a LoopBodyLocalEnv,
promoted_loopbodylocals: &'a [String],
) -> Self {
Self {
condition_env,
body_local_env,
promoted_loopbodylocals,
}
}
/// Resolve a variable name to JoinIR ValueId
///
/// Resolution order:
/// 1. Try condition_env.get(name)
/// 2. If not found, try body_local_env.get(name)
/// 3. If still not found, return None
/// Resolution order (Phase 247-EX extended):
/// 1. If name is in promoted_loopbodylocals:
/// a. Try condition_env.get("<name>_value") // Integer carrier for accumulation
/// b. If not found, try condition_env.get("is_<name>") // Boolean carrier (rare in updates)
/// 2. Try condition_env.get(name)
/// 3. If not found, try body_local_env.get(name)
/// 4. If still not found, return None
///
/// # Arguments
///
@ -101,7 +131,52 @@ impl<'a> UpdateEnv<'a> {
///
/// * `Some(ValueId)` - Variable found in one of the environments
/// * `None` - Variable not found in either environment
///
/// # Phase 247-EX Example
///
/// ```ignore
/// // digit_pos promoted → is_digit_pos + digit_value
/// // When resolving "digit_pos" in update expr:
/// env.resolve("digit_pos") → env.get("digit_value") → Some(ValueId(X))
/// ```
pub fn resolve(&self, name: &str) -> Option<ValueId> {
// Phase 247-EX: Check if this is a promoted variable
if self.promoted_loopbodylocals.iter().any(|v| v == name) {
// Phase 247-EX: Naming convention - "digit_pos" → "digit_value" (not "digit_pos_value")
// Extract base name: "digit_pos" → "digit", "pos" → "pos"
let base_name = if name.ends_with("_pos") {
&name[..name.len() - 4] // Remove "_pos" suffix
} else {
name
};
// Priority 1a: Try <base>_value (integer carrier for NumberAccumulation)
let int_carrier_name = format!("{}_value", base_name);
if let Some(value_id) = self.condition_env.get(&int_carrier_name) {
eprintln!(
"[update_env/phase247ex] Resolved promoted '{}' → '{}' (integer carrier): {:?}",
name, int_carrier_name, value_id
);
return Some(value_id);
}
// Priority 1b: Try is_<name> (boolean carrier, less common in updates)
let bool_carrier_name = format!("is_{}", name);
if let Some(value_id) = self.condition_env.get(&bool_carrier_name) {
eprintln!(
"[update_env/phase247ex] Resolved promoted '{}' → '{}' (boolean carrier): {:?}",
name, bool_carrier_name, value_id
);
return Some(value_id);
}
eprintln!(
"[update_env/phase247ex] WARNING: Promoted variable '{}' not found as carrier ({} or {})",
name, int_carrier_name, bool_carrier_name
);
}
// Standard resolution (Phase 184)
self.condition_env
.get(name)
.or_else(|| self.body_local_env.get(name))
@ -149,7 +224,8 @@ mod tests {
// Condition variables should be found first
let cond_env = test_condition_env();
let body_env = LoopBodyLocalEnv::new(); // Empty
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![]; // Phase 247-EX: No promoted variables
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
assert_eq!(update_env.resolve("i"), Some(ValueId(10)));
assert_eq!(update_env.resolve("sum"), Some(ValueId(20)));
@ -161,7 +237,8 @@ mod tests {
// Body-local variables should be found when not in condition env
let cond_env = ConditionEnv::new(); // Empty
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
assert_eq!(update_env.resolve("temp"), Some(ValueId(50)));
assert_eq!(update_env.resolve("digit"), Some(ValueId(60)));
@ -176,7 +253,8 @@ mod tests {
let mut body_env = LoopBodyLocalEnv::new();
body_env.insert("x".to_string(), ValueId(200)); // Body-local: x=200
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
// Should resolve to condition env value (100), not body-local (200)
assert_eq!(update_env.resolve("x"), Some(ValueId(100)));
@ -187,7 +265,8 @@ mod tests {
// Variable not in either environment → None
let cond_env = test_condition_env();
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
assert_eq!(update_env.resolve("unknown"), None);
assert_eq!(update_env.resolve("nonexistent"), None);
@ -198,7 +277,8 @@ mod tests {
// Mixed lookup: some in condition, some in body-local
let cond_env = test_condition_env();
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
// Condition variables
assert_eq!(update_env.resolve("i"), Some(ValueId(10)));
@ -216,7 +296,8 @@ mod tests {
fn test_contains() {
let cond_env = test_condition_env();
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
assert!(update_env.contains("i"));
assert!(update_env.contains("temp"));
@ -228,7 +309,8 @@ mod tests {
// Both environments empty
let cond_env = ConditionEnv::new();
let body_env = LoopBodyLocalEnv::new();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
assert_eq!(update_env.resolve("anything"), None);
assert!(!update_env.contains("anything"));
@ -239,10 +321,60 @@ mod tests {
// Test diagnostic accessor methods
let cond_env = test_condition_env();
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let promoted: Vec<String> = vec![];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
// Should return references to underlying environments
assert_eq!(update_env.condition_env().len(), 3);
assert_eq!(update_env.body_local_env().len(), 2);
}
// Phase 247-EX: Test promoted variable resolution (dual-value carriers)
#[test]
fn test_promoted_variable_resolution_digit_pos() {
// Test case: digit_pos promoted → is_digit_pos (bool) + digit_value (i64)
// Naming: "digit_pos" → "is_digit_pos" + "digit_value" (base_name="_pos" removed)
let mut cond_env = ConditionEnv::new();
// Register both carriers in ConditionEnv
cond_env.insert("is_digit_pos".to_string(), ValueId(100)); // Boolean carrier
cond_env.insert("digit_value".to_string(), ValueId(200)); // Integer carrier (digit_pos → digit)
let body_env = LoopBodyLocalEnv::new();
let promoted: Vec<String> = vec!["digit_pos".to_string()];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
// When resolving "digit_pos" in update expr → should get digit_value (integer carrier)
assert_eq!(update_env.resolve("digit_pos"), Some(ValueId(200)));
// Direct carrier access still works
assert_eq!(update_env.resolve("is_digit_pos"), Some(ValueId(100)));
assert_eq!(update_env.resolve("digit_value"), Some(ValueId(200)));
}
#[test]
fn test_promoted_variable_resolution_fallback_to_bool() {
// Test case: Only boolean carrier exists (integer carrier missing)
let mut cond_env = ConditionEnv::new();
cond_env.insert("is_pos".to_string(), ValueId(150)); // Only boolean carrier
let body_env = LoopBodyLocalEnv::new();
let promoted: Vec<String> = vec!["pos".to_string()];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
// Should fall back to is_pos (boolean carrier)
assert_eq!(update_env.resolve("pos"), Some(ValueId(150)));
}
#[test]
fn test_promoted_variable_not_a_carrier() {
// Test case: Variable in promoted list but no carrier exists
let cond_env = ConditionEnv::new(); // Empty
let body_env = LoopBodyLocalEnv::new();
let promoted: Vec<String> = vec!["missing_var".to_string()];
let update_env = UpdateEnv::new(&cond_env, &body_env, &promoted);
// Should return None (with warning logged)
assert_eq!(update_env.resolve("missing_var"), None);
}
}

View File

@ -180,23 +180,44 @@ impl DigitPosPromoter {
);
// Step 6: Build CarrierInfo
// For DigitPos pattern, we add a NEW carrier (not replace loop_var)
let carrier_name = format!("is_{}", var_in_cond);
// Phase 247-EX: DigitPos generates TWO carriers (dual-value model)
// - is_<var> (boolean): for break condition
// - <prefix>_value (integer): for NumberAccumulation
// Naming: "digit_pos" → "is_digit_pos" + "digit_value" (not "digit_pos_value")
let bool_carrier_name = format!("is_{}", var_in_cond);
// Extract the base name for integer carrier (e.g., "digit_pos" → "digit")
let base_name = if var_in_cond.ends_with("_pos") {
&var_in_cond[..var_in_cond.len() - 4] // Remove "_pos" suffix
} else {
var_in_cond.as_str()
};
let int_carrier_name = format!("{}_value", base_name);
use crate::mir::join_ir::lowering::carrier_info::{CarrierVar, CarrierRole, CarrierInit};
let promoted_carrier = CarrierVar {
name: carrier_name.clone(),
// Boolean carrier (condition-only, for break)
let promoted_carrier_bool = CarrierVar {
name: bool_carrier_name.clone(),
host_id: ValueId(0), // Placeholder (will be remapped)
join_id: None, // Will be allocated later
role: CarrierRole::ConditionOnly, // Phase 227: DigitPos is condition-only
init: CarrierInit::BoolConst(false), // Phase 228: Initialize with false
};
// Integer carrier (loop-state, for NumberAccumulation)
let promoted_carrier_int = CarrierVar {
name: int_carrier_name.clone(),
host_id: ValueId(0), // Placeholder (will be remapped)
join_id: None, // Will be allocated later
role: CarrierRole::LoopState, // Phase 247-EX: LoopState for accumulation
init: CarrierInit::FromHost, // Phase 228: Initialize from indexOf() result
};
// Create CarrierInfo with a dummy loop_var_name (will be ignored during merge)
let mut carrier_info = CarrierInfo::with_carriers(
"__dummy_loop_var__".to_string(), // Placeholder, not used
ValueId(0), // Placeholder
vec![promoted_carrier],
vec![promoted_carrier_bool, promoted_carrier_int],
);
// Phase 229: Record promoted variable (no need for condition_aliases)
@ -204,18 +225,18 @@ impl DigitPosPromoter {
carrier_info.promoted_loopbodylocals.push(var_in_cond.clone());
eprintln!(
"[digitpos_promoter] A-4 DigitPos pattern promoted: {}{}",
var_in_cond, carrier_name
"[digitpos_promoter] Phase 247-EX: A-4 DigitPos pattern promoted: {}{} (bool) + {} (i64)",
var_in_cond, bool_carrier_name, int_carrier_name
);
eprintln!(
"[digitpos_promoter] Phase 229: Recorded promoted variable '{}' (carrier: '{}')",
var_in_cond, carrier_name
"[digitpos_promoter] Phase 229: Recorded promoted variable '{}' (carriers: '{}', '{}')",
var_in_cond, bool_carrier_name, int_carrier_name
);
return DigitPosPromotionResult::Promoted {
carrier_info,
promoted_var: var_in_cond,
carrier_name,
carrier_name: bool_carrier_name, // Return bool carrier name for compatibility
};
} else {
eprintln!(

View File

@ -192,3 +192,43 @@ fn pattern4_continue_loop_is_detected() {
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern4Continue);
}
#[test]
fn test_atoi_loop_classified_as_pattern2() {
// Phase 246-EX Step 1: _atoi loop pattern classification
// 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
// }
// Simplified: loop with break + two carrier updates
let break_cond = bin(BinaryOperator::Less, var("digit_pos"), lit_i(0));
// result = result * 10 + digit_pos (NumberAccumulation pattern)
let mul_expr = bin(BinaryOperator::Multiply, var("result"), lit_i(10));
let result_update = assignment(
var("result"),
bin(BinaryOperator::Add, mul_expr, var("digit_pos"))
);
// i = i + 1
let i_update = assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1)));
let body = vec![
ASTNode::If {
condition: Box::new(break_cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
result_update,
i_update,
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern2Break,
"_atoi loop should be classified as Pattern2 (Break) due to if-break structure");
}

142
tests/phase246_json_atoi.rs Normal file
View File

@ -0,0 +1,142 @@
//! Phase 246-EX Step 5: E2E tests for JsonParser _atoi JoinIR integration
//!
//! Tests the complete _atoi loop lowering using Pattern 2 (Break) + NumberAccumulation.
//!
//! Test cases cover:
//! - Single digit zero: "0" → 0
//! - Two digits: "42" → 42
//! - Multiple digits: "123" → 123
//! - Leading zeros: "007" → 7
//! - Break at non-digit: "123abc" → 123
//! - Immediate break (no digits): "abc" → 0
use std::fs;
use std::process::Command;
/// Helper function to run _atoi implementation via hakorune binary
fn run_atoi_test(input: &str, expected: i64, test_name: &str) {
let code = format!(
r#"
static box Main {{
main() {{
local result = me._atoi("{}", {})
print(result)
return result
}}
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
}}
}}
"#,
input,
input.len()
);
// Write test file
let test_file = format!("local_tests/phase246_atoi_{}.hako", test_name);
fs::write(&test_file, &code).expect("Failed to write test file");
// Run hakorune
let bin = env!("CARGO_BIN_EXE_hakorune");
let output = Command::new(bin)
.arg("--backend")
.arg("vm")
.arg(&test_file)
.env("NYASH_JOINIR_CORE", "1")
.env("NYASH_DISABLE_PLUGINS", "1")
.output()
.expect("Failed to run hakorune");
// Clean up test file
let _ = fs::remove_file(&test_file);
if !output.status.success() {
panic!(
"[phase246/atoi/{}] Test failed (exit={}):\nstdout: {}\nstderr: {}",
test_name,
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
// Verify output
let stdout = String::from_utf8_lossy(&output.stdout);
let output_value: i64 = stdout
.trim()
.parse()
.unwrap_or_else(|_| panic!("Failed to parse output '{}' as integer", stdout.trim()));
assert_eq!(
output_value, expected,
"Failed for input '{}': expected {}, got {}",
input, expected, output_value
);
}
#[test]
fn test_atoi_single_digit_zero() {
// Phase 246-EX: Test case "0" → 0
run_atoi_test("0", 0, "zero");
}
#[test]
fn test_atoi_two_digits() {
// Phase 246-EX: Test case "42" → 42
run_atoi_test("42", 42, "two_digits");
}
#[test]
fn test_atoi_multiple_digits() {
// Phase 246-EX: Test case "123" → 123
run_atoi_test("123", 123, "multiple_digits");
}
#[test]
fn test_atoi_leading_zeros() {
// Phase 246-EX: Test case "007" → 7
run_atoi_test("007", 7, "leading_zeros");
}
#[test]
fn test_atoi_break_at_non_digit() {
// Phase 246-EX: Test case "123abc" → 123 (break at 'a')
run_atoi_test("123abc", 123, "break_non_digit");
}
#[test]
fn test_atoi_immediate_break_no_digits() {
// Phase 246-EX: Test case "abc" → 0 (immediate break, no digits parsed)
run_atoi_test("abc", 0, "immediate_break");
}
#[test]
fn test_atoi_empty_string() {
// Phase 246-EX: Test case "" → 0 (empty string, no iterations)
run_atoi_test("", 0, "empty");
}
#[test]
fn test_atoi_single_digit_nine() {
// Phase 246-EX: Additional test case "9" → 9 (max single digit)
run_atoi_test("9", 9, "nine");
}
#[test]
fn test_atoi_large_number() {
// Phase 246-EX: Test case "999999" → 999999 (large number)
run_atoi_test("999999", 999999, "large");
}