974 lines
25 KiB
Markdown
974 lines
25 KiB
Markdown
|
|
# Phase 190: NumberAccumulation Update Design (Doc-Only)
|
||
|
|
|
||
|
|
**Status**: Design Complete (Code TBD)
|
||
|
|
**Date**: 2025-12-09
|
||
|
|
**Goal**: Design JoinIR support for `result = result * 10 + digit` style updates
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Section 1: Target Loops and RHS Patterns
|
||
|
|
|
||
|
|
### 1.1 JsonParserBox._atoi (lines 436-467)
|
||
|
|
|
||
|
|
**Loop Pattern**:
|
||
|
|
```nyash
|
||
|
|
local v = 0
|
||
|
|
local digits = "0123456789"
|
||
|
|
loop(i < n) {
|
||
|
|
local ch = s.substring(i, i+1)
|
||
|
|
if ch < "0" || ch > "9" { break }
|
||
|
|
local pos = digits.indexOf(ch)
|
||
|
|
if pos < 0 { break }
|
||
|
|
v = v * 10 + pos // ← NumberAccumulation pattern
|
||
|
|
i = i + 1
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**AST Form of Update**:
|
||
|
|
```
|
||
|
|
Assign(
|
||
|
|
lhs = "v",
|
||
|
|
rhs = BinaryOp(
|
||
|
|
op = Add,
|
||
|
|
left = BinaryOp(
|
||
|
|
op = Mul,
|
||
|
|
left = Variable("v"),
|
||
|
|
right = Literal(Integer(10))
|
||
|
|
),
|
||
|
|
right = Variable("pos")
|
||
|
|
)
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Characteristics**:
|
||
|
|
- LHS appears exactly once in RHS (in left-most multiplication)
|
||
|
|
- Base: 10 (constant)
|
||
|
|
- Addend: `pos` (loop-local variable)
|
||
|
|
- Type: Integer (MirType::Integer)
|
||
|
|
|
||
|
|
### 1.2 JsonParserBox._parse_number (lines 106-142)
|
||
|
|
|
||
|
|
**Loop Pattern**:
|
||
|
|
```nyash
|
||
|
|
local num_str = ""
|
||
|
|
local digits = "0123456789"
|
||
|
|
loop(p < s.length()) {
|
||
|
|
local ch = s.substring(p, p+1)
|
||
|
|
local digit_pos = digits.indexOf(ch)
|
||
|
|
|
||
|
|
if digit_pos < 0 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
|
||
|
|
num_str = num_str + ch // ← StringAppendChar (already supported)
|
||
|
|
p = p + 1
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Note**: This is string accumulation (`num_str + ch`), not number accumulation.
|
||
|
|
Already handled by existing `StringAppendChar` / `AccumulationLike`.
|
||
|
|
|
||
|
|
### 1.3 Other Instances in Codebase
|
||
|
|
|
||
|
|
From grep results:
|
||
|
|
|
||
|
|
1. **apps/tests/phase189_atoi_mini.hako:10**
|
||
|
|
- `result = result * 10 + i`
|
||
|
|
- Simplified test case
|
||
|
|
|
||
|
|
2. **apps/tests/phase183_p2_atoi.hako:25**
|
||
|
|
- `result = result * 10 + digit`
|
||
|
|
- Similar pattern
|
||
|
|
|
||
|
|
3. **apps/tests/phase183_p2_parse_number.hako:28**
|
||
|
|
- `result = result * 10 + digit_pos`
|
||
|
|
- Same pattern with different variable name
|
||
|
|
|
||
|
|
4. **lang/src/compiler/builder/ssa/exit_phi/break_finder.hako:277**
|
||
|
|
- `result = result * 10 + (BreakFinderBox._char_to_digit(ch))`
|
||
|
|
- Includes method call on RHS (Complex)
|
||
|
|
|
||
|
|
### 1.4 Canonical Form Requirements
|
||
|
|
|
||
|
|
**Safe NumberAccumulation Pattern**:
|
||
|
|
```
|
||
|
|
lhs = lhs * BASE + addend
|
||
|
|
```
|
||
|
|
|
||
|
|
**Where**:
|
||
|
|
- `lhs`: carrier variable (appears exactly 1 time in RHS)
|
||
|
|
- `BASE`: integer constant (typically 10)
|
||
|
|
- `addend`: one of:
|
||
|
|
- Variable (loop-local or carrier)
|
||
|
|
- Constant
|
||
|
|
- **NOT** method call (→ Complex)
|
||
|
|
|
||
|
|
**Variants**:
|
||
|
|
- `lhs = lhs * BASE - addend` (subtraction also allowed)
|
||
|
|
- Base can be any integer constant (10, 2, 16, etc.)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Section 2: Safe Pattern Candidates
|
||
|
|
|
||
|
|
### 2.1 Whitelist Criteria
|
||
|
|
|
||
|
|
A NumberAccumulation update is **safe** if:
|
||
|
|
|
||
|
|
1. **Type Check**: Carrier type is `MirType::Integer`
|
||
|
|
2. **Structure Check**: AST matches `lhs = Add(Mul(lhs, Const(base)), addend)`
|
||
|
|
3. **LHS Occurrence**: `lhs` appears exactly 1 time in RHS (in multiplication)
|
||
|
|
4. **Base Check**: Base is integer constant (not variable)
|
||
|
|
5. **Addend Check**: Addend is one of:
|
||
|
|
- `Variable(name)` - loop-local or carrier
|
||
|
|
- `Const(n)` - integer constant
|
||
|
|
6. **No Method Calls**: Addend does not contain method calls
|
||
|
|
|
||
|
|
### 2.2 Reject as Complex
|
||
|
|
|
||
|
|
Mark as `UpdateKind::Complex` if:
|
||
|
|
|
||
|
|
- LHS appears 2+ times in RHS
|
||
|
|
- Base is variable (not constant)
|
||
|
|
- Addend contains method call (e.g., `_char_to_digit(ch)`)
|
||
|
|
- Addend is complex expression (nested BinaryOp)
|
||
|
|
- Type is not Integer
|
||
|
|
- LoopBodyLocal appears in addend (Phase 5 未サポート)
|
||
|
|
|
||
|
|
### 2.3 Pattern Matrix
|
||
|
|
|
||
|
|
| Pattern | LHS Count | Base | Addend | Decision |
|
||
|
|
|---------|-----------|------|--------|----------|
|
||
|
|
| `v = v * 10 + pos` | 1 | Const(10) | Variable | NumberAccumulation |
|
||
|
|
| `v = v * 10 + 5` | 1 | Const(10) | Const(5) | NumberAccumulation |
|
||
|
|
| `v = v * base + x` | 1 | Variable | Variable | Complex |
|
||
|
|
| `v = v * 10 + f(x)` | 1 | Const(10) | MethodCall | Complex |
|
||
|
|
| `v = v * 10 + v` | 2 | Const(10) | Variable | Complex |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Section 3: UpdateKind::NumberAccumulation Definition
|
||
|
|
|
||
|
|
### 3.1 Enum Extension
|
||
|
|
|
||
|
|
**Current** (Phase 170-C-2):
|
||
|
|
```rust
|
||
|
|
pub enum UpdateKind {
|
||
|
|
CounterLike,
|
||
|
|
AccumulationLike,
|
||
|
|
Other,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Proposed** (Phase 190):
|
||
|
|
```rust
|
||
|
|
pub enum UpdateKind {
|
||
|
|
CounterLike,
|
||
|
|
AccumulationLike,
|
||
|
|
|
||
|
|
/// Phase 190: Number accumulation: result = result * base + addend
|
||
|
|
///
|
||
|
|
/// Typical pattern: digit expansion (atoi, parse_number)
|
||
|
|
/// Example: v = v * 10 + digit
|
||
|
|
NumberAccumulation { base: i64 },
|
||
|
|
|
||
|
|
/// Phase 178+: String append patterns (already implemented)
|
||
|
|
StringAppendChar,
|
||
|
|
StringAppendLiteral,
|
||
|
|
|
||
|
|
/// Complex or unrecognized patterns
|
||
|
|
Complex,
|
||
|
|
|
||
|
|
/// Deprecated
|
||
|
|
Other,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 UpdateKind::name() Extension
|
||
|
|
|
||
|
|
```rust
|
||
|
|
impl UpdateKind {
|
||
|
|
pub fn name(&self) -> &'static str {
|
||
|
|
match self {
|
||
|
|
UpdateKind::CounterLike => "CounterLike",
|
||
|
|
UpdateKind::AccumulationLike => "AccumulationLike",
|
||
|
|
UpdateKind::NumberAccumulation { base } => "NumberAccumulation",
|
||
|
|
UpdateKind::StringAppendChar => "StringAppendChar",
|
||
|
|
UpdateKind::StringAppendLiteral => "StringAppendLiteral",
|
||
|
|
UpdateKind::Complex => "Complex",
|
||
|
|
UpdateKind::Other => "Other",
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.3 Alternative: Subfield on AccumulationLike
|
||
|
|
|
||
|
|
Instead of new enum variant, could extend AccumulationLike:
|
||
|
|
|
||
|
|
```rust
|
||
|
|
pub enum UpdateKind {
|
||
|
|
CounterLike,
|
||
|
|
AccumulationLike {
|
||
|
|
style: AccumulationStyle,
|
||
|
|
},
|
||
|
|
Complex,
|
||
|
|
Other,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub enum AccumulationStyle {
|
||
|
|
Simple, // result = result + x
|
||
|
|
NumberDigit { base: i64 }, // result = result * base + x
|
||
|
|
StringAppend, // result = result + "literal"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Decision**: Use dedicated `NumberAccumulation { base: i64 }` for clarity.
|
||
|
|
Simpler to pattern-match, clearer intent.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Section 4: classify_number_update Pseudocode
|
||
|
|
|
||
|
|
### 4.1 Algorithm Overview
|
||
|
|
|
||
|
|
**Input**:
|
||
|
|
- `lhs: &str` - carrier variable name
|
||
|
|
- `rhs_ast: &ASTNode` - RHS expression AST
|
||
|
|
|
||
|
|
**Output**:
|
||
|
|
- `UpdateKind` - classified update pattern
|
||
|
|
|
||
|
|
**Steps**:
|
||
|
|
1. Check if RHS is `BinaryOp(Add, left, right)`
|
||
|
|
2. Check if `left` is `BinaryOp(Mul, mul_left, mul_right)`
|
||
|
|
3. Verify `mul_left` is `Variable(lhs)` (LHS appears once)
|
||
|
|
4. Verify `mul_right` is `Literal(Integer(base))`
|
||
|
|
5. Classify `right` (addend):
|
||
|
|
- `Const(n)` → NumberAccumulation
|
||
|
|
- `Variable(name)` → NumberAccumulation
|
||
|
|
- `MethodCall/Other` → Complex
|
||
|
|
|
||
|
|
### 4.2 Pseudocode
|
||
|
|
|
||
|
|
```rust
|
||
|
|
fn classify_number_update(lhs: &str, rhs_ast: &ASTNode) -> UpdateKind {
|
||
|
|
// Step 1: Check outer addition
|
||
|
|
let BinaryOp { op: Add, left: mul_expr, right: addend } = rhs_ast else {
|
||
|
|
return UpdateKind::Complex;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Step 2: Check inner multiplication
|
||
|
|
let BinaryOp { op: Mul, left: mul_left, right: mul_right } = mul_expr else {
|
||
|
|
return UpdateKind::Complex;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Step 3: Verify LHS appears in multiplication
|
||
|
|
let Variable { name: lhs_name } = mul_left else {
|
||
|
|
return UpdateKind::Complex;
|
||
|
|
};
|
||
|
|
if lhs_name != lhs {
|
||
|
|
return UpdateKind::Complex;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 4: Extract base constant
|
||
|
|
let Literal { value: Integer(base) } = mul_right else {
|
||
|
|
return UpdateKind::Complex; // base is not constant
|
||
|
|
};
|
||
|
|
|
||
|
|
// Step 5: Classify addend
|
||
|
|
match addend {
|
||
|
|
// Variable or Const: NumberAccumulation
|
||
|
|
Literal { value: Integer(_) } | Variable { .. } => {
|
||
|
|
UpdateKind::NumberAccumulation { base: *base }
|
||
|
|
}
|
||
|
|
|
||
|
|
// Method call or complex expression: Complex
|
||
|
|
MethodCall { .. } | BinaryOp { .. } | Call { .. } => {
|
||
|
|
UpdateKind::Complex
|
||
|
|
}
|
||
|
|
|
||
|
|
_ => UpdateKind::Complex,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.3 LHS Occurrence Check
|
||
|
|
|
||
|
|
**Goal**: Ensure LHS appears exactly 1 time in RHS.
|
||
|
|
|
||
|
|
**Implementation**:
|
||
|
|
```rust
|
||
|
|
fn count_lhs_occurrences(lhs: &str, rhs: &ASTNode) -> usize {
|
||
|
|
match rhs {
|
||
|
|
Variable { name } if name == lhs => 1,
|
||
|
|
BinaryOp { left, right, .. } => {
|
||
|
|
count_lhs_occurrences(lhs, left) + count_lhs_occurrences(lhs, right)
|
||
|
|
}
|
||
|
|
MethodCall { receiver, args, .. } => {
|
||
|
|
let mut count = count_lhs_occurrences(lhs, receiver);
|
||
|
|
for arg in args {
|
||
|
|
count += count_lhs_occurrences(lhs, arg);
|
||
|
|
}
|
||
|
|
count
|
||
|
|
}
|
||
|
|
_ => 0,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Usage**:
|
||
|
|
```rust
|
||
|
|
fn classify_number_update(lhs: &str, rhs_ast: &ASTNode) -> UpdateKind {
|
||
|
|
// FIRST: Check LHS occurrence count
|
||
|
|
let lhs_count = count_lhs_occurrences(lhs, rhs_ast);
|
||
|
|
if lhs_count != 1 {
|
||
|
|
return UpdateKind::Complex;
|
||
|
|
}
|
||
|
|
|
||
|
|
// THEN: Check structure
|
||
|
|
// ... (rest of classify_number_update logic)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Section 5: Pattern2/4 can_lower Specification
|
||
|
|
|
||
|
|
### 5.1 Current Behavior
|
||
|
|
|
||
|
|
**Pattern2 can_lower** (Phase 176+):
|
||
|
|
- Accepts: CounterLike, AccumulationLike, StringAppendChar, StringAppendLiteral
|
||
|
|
- Rejects: Complex → `[joinir/freeze]`
|
||
|
|
|
||
|
|
**Pattern4 can_lower** (Phase 33-19+):
|
||
|
|
- Similar criteria to Pattern2
|
||
|
|
|
||
|
|
### 5.2 Proposed Extension
|
||
|
|
|
||
|
|
**Pattern2 can_lower** (Phase 190+):
|
||
|
|
```rust
|
||
|
|
fn can_lower_carrier_updates(updates: &HashMap<String, UpdateExpr>) -> bool {
|
||
|
|
for (_name, update_expr) in updates {
|
||
|
|
let kind = classify_update_kind(update_expr);
|
||
|
|
|
||
|
|
match kind {
|
||
|
|
// Whitelist: Simple patterns
|
||
|
|
UpdateKind::CounterLike
|
||
|
|
| UpdateKind::AccumulationLike
|
||
|
|
| UpdateKind::StringAppendChar
|
||
|
|
| UpdateKind::StringAppendLiteral
|
||
|
|
| UpdateKind::NumberAccumulation { .. } // ← NEW!
|
||
|
|
=> { /* OK */ }
|
||
|
|
|
||
|
|
// Fail-Fast: Complex patterns
|
||
|
|
UpdateKind::Complex => {
|
||
|
|
eprintln!("[joinir/freeze] Complex carrier update detected");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
UpdateKind::Other => {
|
||
|
|
eprintln!("[joinir/freeze] Unknown update pattern");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
true
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5.3 Type Constraint
|
||
|
|
|
||
|
|
**NumberAccumulation Type Check**:
|
||
|
|
```rust
|
||
|
|
fn verify_number_accumulation_type(carrier: &CarrierVar) -> bool {
|
||
|
|
// Phase 190: NumberAccumulation requires Integer type
|
||
|
|
// String types must use StringAppendChar/StringAppendLiteral
|
||
|
|
match carrier.mir_type {
|
||
|
|
MirType::Integer => true,
|
||
|
|
_ => {
|
||
|
|
eprintln!("[joinir/freeze] NumberAccumulation requires Integer type, got {:?}",
|
||
|
|
carrier.mir_type);
|
||
|
|
false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5.4 LoopBodyLocal Handling
|
||
|
|
|
||
|
|
**Phase 5 未サポート**:
|
||
|
|
```rust
|
||
|
|
fn check_loopbodylocal_constraint(addend: &UpdateRhs, loop_scope: &LoopScopeShape) -> bool {
|
||
|
|
match addend {
|
||
|
|
UpdateRhs::Variable(name) => {
|
||
|
|
// Check if variable is LoopBodyLocal
|
||
|
|
if loop_scope.body_locals.contains(name) {
|
||
|
|
eprintln!("[joinir/freeze] LoopBodyLocal in NumberAccumulation not supported yet");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
true
|
||
|
|
}
|
||
|
|
_ => true,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5.5 Fallback Strategy
|
||
|
|
|
||
|
|
**Fail-Fast Principle**:
|
||
|
|
- NumberAccumulation detection failure → `Complex` → `[joinir/freeze]`
|
||
|
|
- **NO** silent fallback to LoopBuilder (already deleted)
|
||
|
|
- **NO** new fallback paths
|
||
|
|
|
||
|
|
**Error Message Template**:
|
||
|
|
```
|
||
|
|
[joinir/freeze] NumberAccumulation pattern not supported:
|
||
|
|
carrier: v
|
||
|
|
reason: addend contains method call
|
||
|
|
suggestion: extract method call to loop-local variable
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Section 6: CarrierUpdateLowerer Pseudocode
|
||
|
|
|
||
|
|
### 6.1 Responsibility Assignment
|
||
|
|
|
||
|
|
**CarrierUpdateLowerer** (Phase 176+):
|
||
|
|
- Input: `CarrierVar`, `UpdateExpr`, `JoinIRBuilder`
|
||
|
|
- Output: Emits JoinIR instructions for carrier update
|
||
|
|
|
||
|
|
**Current Support**:
|
||
|
|
- CounterLike: `dst = lhs + 1`
|
||
|
|
- AccumulationLike: `dst = lhs + rhs`
|
||
|
|
- StringAppendChar: (BoxCall to StringBox.append)
|
||
|
|
|
||
|
|
**Phase 190 Extension**:
|
||
|
|
- NumberAccumulation: `tmp = lhs * base; dst = tmp + addend`
|
||
|
|
|
||
|
|
### 6.2 JoinIR Emission Design
|
||
|
|
|
||
|
|
**Option A: 2-Instruction Approach** (Recommended)
|
||
|
|
```rust
|
||
|
|
fn emit_number_accumulation(
|
||
|
|
builder: &mut JoinIRBuilder,
|
||
|
|
carrier: &CarrierVar,
|
||
|
|
base: i64,
|
||
|
|
addend: &UpdateRhs,
|
||
|
|
) -> ValueId {
|
||
|
|
// Step 1: Multiply by base
|
||
|
|
// tmp = lhs * base
|
||
|
|
let base_value = builder.alloc_value();
|
||
|
|
builder.emit(JoinInst::Const {
|
||
|
|
dst: base_value,
|
||
|
|
value: ConstValue::Integer(base),
|
||
|
|
});
|
||
|
|
|
||
|
|
let mul_result = builder.alloc_value();
|
||
|
|
builder.emit(JoinInst::BinOp {
|
||
|
|
dst: mul_result,
|
||
|
|
op: BinOpKind::Mul,
|
||
|
|
left: carrier.join_id.unwrap(), // Current value from LoopHeader PHI
|
||
|
|
right: base_value,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Step 2: Add addend
|
||
|
|
// dst = tmp + addend
|
||
|
|
let addend_value = emit_addend_value(builder, addend);
|
||
|
|
let result = builder.alloc_value();
|
||
|
|
builder.emit(JoinInst::BinOp {
|
||
|
|
dst: result,
|
||
|
|
op: BinOpKind::Add,
|
||
|
|
left: mul_result,
|
||
|
|
right: addend_value,
|
||
|
|
});
|
||
|
|
|
||
|
|
result
|
||
|
|
}
|
||
|
|
|
||
|
|
fn emit_addend_value(builder: &mut JoinIRBuilder, addend: &UpdateRhs) -> ValueId {
|
||
|
|
match addend {
|
||
|
|
UpdateRhs::Const(n) => {
|
||
|
|
let val = builder.alloc_value();
|
||
|
|
builder.emit(JoinInst::Const {
|
||
|
|
dst: val,
|
||
|
|
value: ConstValue::Integer(*n),
|
||
|
|
});
|
||
|
|
val
|
||
|
|
}
|
||
|
|
UpdateRhs::Variable(name) => {
|
||
|
|
// Look up variable in boundary.join_inputs or condition_bindings
|
||
|
|
builder.lookup_variable(name)
|
||
|
|
}
|
||
|
|
_ => unreachable!("Complex addend should be rejected in can_lower"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Option B: Single Complex Expression** (Not Recommended)
|
||
|
|
```rust
|
||
|
|
// Would require JoinInst::ComplexExpr or similar
|
||
|
|
// Violates "flat instruction" principle of JoinIR
|
||
|
|
// NOT RECOMMENDED
|
||
|
|
```
|
||
|
|
|
||
|
|
**Decision**: Use Option A (2-instruction approach).
|
||
|
|
- Pros: Clean separation, easy to optimize later, follows JoinIR flat instruction principle
|
||
|
|
- Cons: None
|
||
|
|
|
||
|
|
### 6.3 Type Constraint Enforcement
|
||
|
|
|
||
|
|
```rust
|
||
|
|
impl CarrierUpdateLowerer {
|
||
|
|
pub fn emit_update(
|
||
|
|
&self,
|
||
|
|
builder: &mut JoinIRBuilder,
|
||
|
|
carrier: &CarrierVar,
|
||
|
|
update_expr: &UpdateExpr,
|
||
|
|
) -> Result<ValueId, String> {
|
||
|
|
match classify_update_kind(update_expr) {
|
||
|
|
UpdateKind::NumberAccumulation { base } => {
|
||
|
|
// Type check
|
||
|
|
if carrier.mir_type != MirType::Integer {
|
||
|
|
return Err(format!(
|
||
|
|
"NumberAccumulation requires Integer type, got {:?}",
|
||
|
|
carrier.mir_type
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Emit instructions
|
||
|
|
Ok(self.emit_number_accumulation(builder, carrier, base, &extract_addend(update_expr)))
|
||
|
|
}
|
||
|
|
// ... other cases
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6.4 Name Dependency Prohibition
|
||
|
|
|
||
|
|
**Phase 170-C-1 Principle**: No name-based heuristics in lowering.
|
||
|
|
|
||
|
|
**Enforcement**:
|
||
|
|
```rust
|
||
|
|
// ✅ GOOD: Structural detection
|
||
|
|
fn is_number_accumulation(update_expr: &UpdateExpr) -> bool {
|
||
|
|
matches!(
|
||
|
|
update_expr,
|
||
|
|
UpdateExpr::BinOp {
|
||
|
|
op: BinOpKind::Add,
|
||
|
|
lhs: _,
|
||
|
|
rhs: UpdateRhs::Variable(_) | UpdateRhs::Const(_),
|
||
|
|
}
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ❌ BAD: Name-based detection
|
||
|
|
fn is_number_accumulation(carrier_name: &str) -> bool {
|
||
|
|
carrier_name.contains("result") || carrier_name.contains("v")
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Rationale**:
|
||
|
|
- Variable names are user-controlled and unreliable
|
||
|
|
- Structural AST analysis is robust and refactoring-safe
|
||
|
|
- Follows "箱理論" separation of concerns
|
||
|
|
|
||
|
|
### 6.5 Integration with LoopHeaderPhiBuilder
|
||
|
|
|
||
|
|
**Phase 33-22 Context**:
|
||
|
|
- LoopHeader PHI creates SSOT for carrier current value
|
||
|
|
- `carrier.join_id` points to LoopHeader PHI dst
|
||
|
|
|
||
|
|
**Usage in NumberAccumulation**:
|
||
|
|
```rust
|
||
|
|
fn emit_number_accumulation(
|
||
|
|
builder: &mut JoinIRBuilder,
|
||
|
|
carrier: &CarrierVar,
|
||
|
|
base: i64,
|
||
|
|
addend: &UpdateRhs,
|
||
|
|
) -> ValueId {
|
||
|
|
// Use carrier.join_id (LoopHeader PHI dst) as current value
|
||
|
|
let current_value = carrier.join_id.expect("LoopHeader PHI should set join_id");
|
||
|
|
|
||
|
|
// tmp = current_value * base
|
||
|
|
let base_const = builder.emit_const(ConstValue::Integer(base));
|
||
|
|
let mul_result = builder.emit_binop(BinOpKind::Mul, current_value, base_const);
|
||
|
|
|
||
|
|
// dst = tmp + addend
|
||
|
|
let addend_value = emit_addend_value(builder, addend);
|
||
|
|
builder.emit_binop(BinOpKind::Add, mul_result, addend_value)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Invariant**: Never directly access `carrier.host_id` in JoinIR emission.
|
||
|
|
Only use `carrier.join_id` (JoinIR-local ValueId).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Section 7: Implementation Phases
|
||
|
|
|
||
|
|
### Phase 190-impl-A: Core Detection (2-3 commits)
|
||
|
|
|
||
|
|
**Goal**: Extend LoopUpdateAnalyzer to detect NumberAccumulation patterns.
|
||
|
|
|
||
|
|
**Tasks**:
|
||
|
|
1. Add `UpdateKind::NumberAccumulation { base: i64 }`
|
||
|
|
2. Implement `classify_number_update()` in LoopUpdateAnalyzer
|
||
|
|
3. Add `count_lhs_occurrences()` helper
|
||
|
|
4. Unit tests (5+ cases covering Section 2.3 matrix)
|
||
|
|
|
||
|
|
**Success Criteria**:
|
||
|
|
- `_atoi` loop detected as NumberAccumulation { base: 10 }
|
||
|
|
- Complex patterns (method calls) detected as Complex
|
||
|
|
- Type safety: Only Integer carriers allowed
|
||
|
|
|
||
|
|
### Phase 190-impl-B: CarrierUpdateLowerer Extension (2-3 commits)
|
||
|
|
|
||
|
|
**Goal**: Emit JoinIR for NumberAccumulation updates.
|
||
|
|
|
||
|
|
**Tasks**:
|
||
|
|
1. Extend `CarrierUpdateLowerer::emit_update()` with NumberAccumulation branch
|
||
|
|
2. Implement 2-instruction emission (mul + add)
|
||
|
|
3. Type constraint enforcement
|
||
|
|
4. Unit tests (3+ cases)
|
||
|
|
|
||
|
|
**Success Criteria**:
|
||
|
|
- Correct JoinIR emission: `tmp = v * 10; result = tmp + digit`
|
||
|
|
- Type error on non-Integer carriers
|
||
|
|
- Integration with LoopHeader PHI (carrier.join_id)
|
||
|
|
|
||
|
|
### Phase 190-impl-C: Pattern2/4 Integration (1-2 commits)
|
||
|
|
|
||
|
|
**Goal**: Enable NumberAccumulation in Pattern2/4 can_lower.
|
||
|
|
|
||
|
|
**Tasks**:
|
||
|
|
1. Update `can_lower_carrier_updates()` whitelist
|
||
|
|
2. Add NumberAccumulation test case to Pattern2
|
||
|
|
3. Verify Fail-Fast for Complex patterns
|
||
|
|
|
||
|
|
**Success Criteria**:
|
||
|
|
- `phase189_atoi_mini.hako` passes with JoinIR
|
||
|
|
- Complex patterns (with method calls) rejected at can_lower
|
||
|
|
- `[joinir/freeze]` message for unsupported cases
|
||
|
|
|
||
|
|
### Phase 190-impl-D: E2E Validation (1 commit)
|
||
|
|
|
||
|
|
**Goal**: Real-world JsonParserBox._atoi working via JoinIR.
|
||
|
|
|
||
|
|
**Tasks**:
|
||
|
|
1. Enable JoinIR for `_atoi` method
|
||
|
|
2. Verify MIR output correctness
|
||
|
|
3. Runtime test: parse "12345" → 12345
|
||
|
|
|
||
|
|
**Success Criteria**:
|
||
|
|
- JsonParserBox._atoi compiles via JoinIR Pattern2
|
||
|
|
- Correct MIR: multiply-add sequence
|
||
|
|
- Runtime correctness: atoi tests pass
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Section 8: Future Extensions
|
||
|
|
|
||
|
|
### 8.1 Other Bases
|
||
|
|
|
||
|
|
**Current**: `base` is extracted from AST (any integer constant).
|
||
|
|
|
||
|
|
**Example**:
|
||
|
|
- Binary: `v = v * 2 + bit`
|
||
|
|
- Hex: `v = v * 16 + hex_digit`
|
||
|
|
|
||
|
|
**No change needed**: Design already supports arbitrary bases.
|
||
|
|
|
||
|
|
### 8.2 Subtraction Variant
|
||
|
|
|
||
|
|
**Pattern**: `v = v * 10 - offset`
|
||
|
|
|
||
|
|
**Extension**:
|
||
|
|
```rust
|
||
|
|
fn classify_number_update(lhs: &str, rhs_ast: &ASTNode) -> UpdateKind {
|
||
|
|
match rhs_ast {
|
||
|
|
BinaryOp { op: Add | Sub, left: mul_expr, right: addend } => {
|
||
|
|
// ... (same logic for both Add and Sub)
|
||
|
|
}
|
||
|
|
_ => UpdateKind::Complex,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Decision**: Support both Add and Sub in Phase 190-impl-A.
|
||
|
|
|
||
|
|
### 8.3 Complex Addends
|
||
|
|
|
||
|
|
**Examples**:
|
||
|
|
- `v = v * 10 + (ch - '0')`
|
||
|
|
- `v = v * 10 + digits.indexOf(ch)`
|
||
|
|
|
||
|
|
**Strategy**:
|
||
|
|
- Phase 190: Reject as Complex (Fail-Fast)
|
||
|
|
- Phase 191+: Extend to handle BinaryOp/MethodCall in addend
|
||
|
|
- Emit extra JoinIR instructions for addend computation
|
||
|
|
- Store addend in temporary ValueId
|
||
|
|
|
||
|
|
**Not in scope for Phase 190**.
|
||
|
|
|
||
|
|
### 8.4 Multi-Base Patterns
|
||
|
|
|
||
|
|
**Example** (unlikely):
|
||
|
|
```nyash
|
||
|
|
v1 = v1 * 10 + d1
|
||
|
|
v2 = v2 * 16 + d2
|
||
|
|
```
|
||
|
|
|
||
|
|
**Current Design**: Each carrier gets its own `NumberAccumulation { base }`.
|
||
|
|
Already supports different bases per carrier.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Section 9: Testing Strategy
|
||
|
|
|
||
|
|
### 9.1 Unit Tests (LoopUpdateAnalyzer)
|
||
|
|
|
||
|
|
**Coverage Matrix**:
|
||
|
|
|
||
|
|
| Test Case | Pattern | Expected Kind |
|
||
|
|
|-----------|---------|---------------|
|
||
|
|
| `v = v * 10 + pos` | Basic NumberAccumulation | NumberAccumulation { base: 10 } |
|
||
|
|
| `v = v * 10 + 5` | Const addend | NumberAccumulation { base: 10 } |
|
||
|
|
| `v = v * 2 + bit` | Binary base | NumberAccumulation { base: 2 } |
|
||
|
|
| `v = v * 10 - offset` | Subtraction | NumberAccumulation { base: 10 } |
|
||
|
|
| `v = v * base + x` | Variable base | Complex |
|
||
|
|
| `v = v * 10 + f(x)` | Method call | Complex |
|
||
|
|
| `v = v * 10 + v` | LHS appears 2x | Complex |
|
||
|
|
|
||
|
|
**Implementation**:
|
||
|
|
```rust
|
||
|
|
#[test]
|
||
|
|
fn test_number_accumulation_basic() {
|
||
|
|
let ast = parse("v = v * 10 + pos");
|
||
|
|
let kind = LoopUpdateAnalyzer::classify_update("v", &ast);
|
||
|
|
assert!(matches!(kind, UpdateKind::NumberAccumulation { base: 10 }));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 9.2 Integration Tests (Pattern2)
|
||
|
|
|
||
|
|
**Test Files**:
|
||
|
|
1. `apps/tests/phase190_number_update_basic.hako`
|
||
|
|
- Single carrier: `v = v * 10 + i`
|
||
|
|
- Verify JoinIR emission
|
||
|
|
|
||
|
|
2. `apps/tests/phase190_number_update_multi.hako`
|
||
|
|
- Two carriers: counter + accumulator
|
||
|
|
- Verify multi-carrier lowering
|
||
|
|
|
||
|
|
3. `apps/tests/phase190_number_update_complex.hako`
|
||
|
|
- Complex pattern (method call)
|
||
|
|
- Verify Fail-Fast rejection
|
||
|
|
|
||
|
|
### 9.3 E2E Tests (JsonParserBox)
|
||
|
|
|
||
|
|
**Test Cases**:
|
||
|
|
1. `test_jsonparser_atoi_simple.hako`
|
||
|
|
- Input: "123"
|
||
|
|
- Expected: 123
|
||
|
|
|
||
|
|
2. `test_jsonparser_atoi_negative.hako`
|
||
|
|
- Input: "-456"
|
||
|
|
- Expected: -456
|
||
|
|
|
||
|
|
3. `test_jsonparser_parse_number_min_v2.hako`
|
||
|
|
- Full `_parse_number` method
|
||
|
|
- Verify string + number accumulation
|
||
|
|
|
||
|
|
**Success Criteria**:
|
||
|
|
- All tests pass with JoinIR enabled
|
||
|
|
- Same behavior as LoopBuilder (deleted, but historical behavior)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Section 10: Migration Notes
|
||
|
|
|
||
|
|
### 10.1 From LoopBuilder (Deleted)
|
||
|
|
|
||
|
|
**Legacy**: LoopBuilder handled all loop patterns (deleted in Phase 170).
|
||
|
|
|
||
|
|
**Current**: JoinIR Pattern1-4 handle specific patterns.
|
||
|
|
|
||
|
|
**NumberAccumulation Migration**:
|
||
|
|
- Old: LoopBuilder would blindly emit MIR for any loop
|
||
|
|
- New: Pattern2/4 detect NumberAccumulation, emit specialized JoinIR
|
||
|
|
|
||
|
|
**No fallback needed**: LoopBuilder is deleted.
|
||
|
|
|
||
|
|
### 10.2 From Pattern Detection
|
||
|
|
|
||
|
|
**Phase 170-C-1**: Name-based heuristics (e.g., `is_typical_index_name`).
|
||
|
|
|
||
|
|
**Phase 190**: Structural AST analysis (no name dependency).
|
||
|
|
|
||
|
|
**Principle**: Lowering must be name-agnostic, only detection can use names.
|
||
|
|
|
||
|
|
### 10.3 Backward Compatibility
|
||
|
|
|
||
|
|
**Existing Patterns**:
|
||
|
|
- CounterLike: `i = i + 1` (unchanged)
|
||
|
|
- AccumulationLike: `sum = sum + x` (unchanged)
|
||
|
|
- StringAppendChar: `s = s + ch` (unchanged)
|
||
|
|
|
||
|
|
**New Pattern**:
|
||
|
|
- NumberAccumulation: `v = v * 10 + digit` (additive)
|
||
|
|
|
||
|
|
**No Breaking Changes**: All existing patterns continue to work.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Section 11: Open Questions
|
||
|
|
|
||
|
|
### Q1: Should we support division?
|
||
|
|
|
||
|
|
**Pattern**: `v = v / 10`
|
||
|
|
|
||
|
|
**Current**: Only +, -, *, / in BinOpKind.
|
||
|
|
|
||
|
|
**Decision**: Out of scope for Phase 190 (no real-world use case found).
|
||
|
|
|
||
|
|
### Q2: Should addend be restricted to loop params only?
|
||
|
|
|
||
|
|
**Current Design**: Allow any Variable (loop param, outer local, carrier).
|
||
|
|
|
||
|
|
**Alternative**: Only allow LoopParam variables in addend.
|
||
|
|
|
||
|
|
**Decision**: Current design is more flexible, restrict if problems arise.
|
||
|
|
|
||
|
|
### Q3: How to handle floating-point bases?
|
||
|
|
|
||
|
|
**Example**: `v = v * 1.5 + x`
|
||
|
|
|
||
|
|
**Current**: `base: i64` only supports integers.
|
||
|
|
|
||
|
|
**Decision**: Out of scope (no real-world use case in Nyash codebase).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Section 12: Success Metrics
|
||
|
|
|
||
|
|
### 12.1 Functional Metrics
|
||
|
|
|
||
|
|
- ✅ JsonParserBox._atoi compiles via JoinIR
|
||
|
|
- ✅ phase189_atoi_mini.hako passes
|
||
|
|
- ✅ Complex patterns rejected with `[joinir/freeze]`
|
||
|
|
- ✅ No silent fallbacks (Fail-Fast verified)
|
||
|
|
|
||
|
|
### 12.2 Code Quality Metrics
|
||
|
|
|
||
|
|
- ✅ No name-based heuristics in lowering
|
||
|
|
- ✅ Type safety enforced (Integer only)
|
||
|
|
- ✅ Unit test coverage > 80%
|
||
|
|
- ✅ Documentation updated (this doc + overview)
|
||
|
|
|
||
|
|
### 12.3 Performance Metrics
|
||
|
|
|
||
|
|
**Not a goal for Phase 190**: Focus on correctness, not optimization.
|
||
|
|
|
||
|
|
**Future**: Phase 191+ can optimize mul+add to single instruction if needed.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Appendix A: AST Examples
|
||
|
|
|
||
|
|
### A.1 Basic NumberAccumulation
|
||
|
|
|
||
|
|
**Source**:
|
||
|
|
```nyash
|
||
|
|
v = v * 10 + pos
|
||
|
|
```
|
||
|
|
|
||
|
|
**AST**:
|
||
|
|
```
|
||
|
|
Assignment {
|
||
|
|
target: Variable { name: "v" },
|
||
|
|
value: BinaryOp {
|
||
|
|
operator: Add,
|
||
|
|
left: BinaryOp {
|
||
|
|
operator: Mul,
|
||
|
|
left: Variable { name: "v" },
|
||
|
|
right: Literal { value: Integer(10) },
|
||
|
|
},
|
||
|
|
right: Variable { name: "pos" },
|
||
|
|
},
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### A.2 Complex Pattern (Rejected)
|
||
|
|
|
||
|
|
**Source**:
|
||
|
|
```nyash
|
||
|
|
v = v * 10 + digits.indexOf(ch)
|
||
|
|
```
|
||
|
|
|
||
|
|
**AST**:
|
||
|
|
```
|
||
|
|
Assignment {
|
||
|
|
target: Variable { name: "v" },
|
||
|
|
value: BinaryOp {
|
||
|
|
operator: Add,
|
||
|
|
left: BinaryOp {
|
||
|
|
operator: Mul,
|
||
|
|
left: Variable { name: "v" },
|
||
|
|
right: Literal { value: Integer(10) },
|
||
|
|
},
|
||
|
|
right: MethodCall {
|
||
|
|
receiver: Variable { name: "digits" },
|
||
|
|
method: "indexOf",
|
||
|
|
args: [Variable { name: "ch" }],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Decision**: Rejected as Complex (MethodCall in addend).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Appendix B: References
|
||
|
|
|
||
|
|
### B.1 Related Phases
|
||
|
|
|
||
|
|
- **Phase 170-C-1**: Carrier name heuristics
|
||
|
|
- **Phase 170-C-2**: LoopUpdateSummary skeleton
|
||
|
|
- **Phase 176**: Multi-carrier lowering
|
||
|
|
- **Phase 178**: String update detection
|
||
|
|
|
||
|
|
### B.2 Related Files
|
||
|
|
|
||
|
|
**Core**:
|
||
|
|
- `src/mir/join_ir/lowering/loop_update_summary.rs`
|
||
|
|
- `src/mir/join_ir/lowering/loop_update_analyzer.rs`
|
||
|
|
- `src/mir/join_ir/lowering/carrier_update_lowerer.rs`
|
||
|
|
|
||
|
|
**Patterns**:
|
||
|
|
- `src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs`
|
||
|
|
- `src/mir/builder/control_flow/joinir/patterns/pattern4_with_continue.rs`
|
||
|
|
|
||
|
|
**Tests**:
|
||
|
|
- `tools/hako_shared/json_parser.hako` (line 436: _atoi)
|
||
|
|
- `apps/tests/phase189_atoi_mini.hako`
|
||
|
|
|
||
|
|
### B.3 Design Principles
|
||
|
|
|
||
|
|
1. **Fail-Fast**: Reject Complex patterns explicitly
|
||
|
|
2. **Whitelist Control**: Only safe patterns allowed
|
||
|
|
3. **Type Safety**: Integer-only for NumberAccumulation
|
||
|
|
4. **Name Agnostic**: No name-based lowering
|
||
|
|
5. **SSOT**: LoopHeader PHI as single source of truth
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Revision History
|
||
|
|
|
||
|
|
- **2025-12-09**: Initial design (Section 1-12)
|
||
|
|
- **TBD**: Implementation review updates
|