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:
@ -8,7 +8,7 @@
|
||||
|
||||
## 🎯 今フォーカスしているテーマ(2025-12-10 時点のスナップショット)
|
||||
|
||||
### 0. JoinIR / ExprLowerer / Pattern2–4 ライン(Phase 230–244 完了)✅
|
||||
### 0. JoinIR / ExprLowerer / Pattern2–4 + JsonParser `_parse_number` / DigitPos ライン(Phase 230–247-EX 完了)✅
|
||||
|
||||
- Pattern1–4(while / break / if‑PHI / continue)+ P5(Trim) でループ lowering を JoinIR 経路に一本化。
|
||||
- Phase 231/236/240-EX:
|
||||
@ -23,18 +23,26 @@
|
||||
- Phase 243–244-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 ループ基盤(Pattern1–4 + ExprLowerer + ScopeManager)は一応の完成状態に入ったので、当面は:
|
||||
- JoinIR ループ基盤(Pattern1–4 + ExprLowerer + ScopeManager + CapturedEnv)は一応の完成状態に入ったので、当面は:
|
||||
- 既存パターン/箱の範囲内での **バグ修正・Fail‑Fast/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 テストで固定する。
|
||||
- Pattern1–4 / 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/P4+P5 パイプラインをどこまで延ばせるかを docs 側で設計 → コード側はその設計に沿って小さく実装。
|
||||
2. selfhost depth‑2 ラインの再開
|
||||
4. selfhost depth‑2 ラインの再開
|
||||
- `.hako` 側で Program/MIR JSON を読んで JoinIR/MIR/VM/LLVM に流すライン。
|
||||
- JsonParser 側のカバレッジが上がったあとに、小さいループから順に移植する。
|
||||
3. JoinIR Verify / 最適化まわり
|
||||
5. JoinIR Verify / 最適化まわり
|
||||
- すでに PHI/ValueId 契約は debug ビルドで検証しているので、
|
||||
必要なら SSA‑DFA や軽い最適化(Loop invariant / Strength reduction)を検討。
|
||||
|
||||
@ -108,4 +122,3 @@
|
||||
- 新しい大フェーズを始めたら:
|
||||
1. まず docs 配下に `phase-XXX-*.md` を書く。
|
||||
2. CURRENT_TASK には「そのフェーズの一行要約」と「今のフォーカスかどうか」だけを書く。
|
||||
|
||||
|
||||
276
docs/development/current/main/PHASE_247EX_SUMMARY.md
Normal file
276
docs/development/current/main/PHASE_247EX_SUMMARY.md
Normal 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
|
||||
@ -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)
|
||||
@ -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 A(Separate 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 ライン)
|
||||
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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!(
|
||||
|
||||
@ -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
142
tests/phase246_json_atoi.rs
Normal 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");
|
||||
}
|
||||
Reference in New Issue
Block a user