feat(joinir): Phase 190-impl NumberAccumulation pattern implementation

Phase 190 implementation: Detect and emit number accumulation patterns
like `result = result * 10 + digit` in Pattern 2 loops.

## Changes

### Task 190-impl-1: UpdateRhs enum extension
- Added `NumberAccumulation { base, digit_var }` variant to UpdateRhs
- Implemented detection logic in `analyze_update_value()`:
  - Detects pattern: `(carrier * base) + digit`
  - Supports both Add and Subtract operations
  - Base must be integer constant, digit must be variable
- Added 3 unit tests (base10, base2, wrong_lhs cases)

### Task 190-impl-2: Pattern2/4 whitelist update
- Updated `check_carrier_updates_allowed()` in common_init.rs
- NumberAccumulation now allowed in can_lower()
- Pattern 4 (continue) rejects with passthrough (not yet implemented)

### Task 190-impl-3: Carrier update emission
- Implemented NumberAccumulation emission in carrier_update_emitter.rs
- Emits 3 instructions:
  1. Const(base)
  2. BinOp(Mul, carrier, base) → tmp
  3. BinOp(Add/Sub, tmp, digit) → result
- Added 2 unit tests (base10 emission, digit_not_found error)
- Both UpdateEnv and ConditionEnv versions supported

### Task 190-impl-4: E2E tests (in progress)
- Created phase190_atoi_impl.hako (Pattern 2 with break)
- Created phase190_parse_number_impl.hako (Pattern 2 with break)
- Tests compile and use Pattern 2 correctly
- Runtime execution validation pending

## Files Modified
- loop_update_analyzer.rs (+180 lines: enum, detection, 3 tests)
- carrier_update_emitter.rs (+182 lines: emission, 2 tests)
- common_init.rs (+4 lines: whitelist update)
- loop_with_continue_minimal.rs (+16 lines: Pattern 4 passthrough)

## Test Results
-  All analyzer unit tests pass (4/4)
-  All emitter unit tests pass (12/12)
- 🔄 E2E runtime validation in progress

## Architecture Notes
- **Box-first modular design**: Single responsibility per function
- **Fail-fast**: Complex patterns rejected early in can_lower()
- **Pattern 2 only**: Pattern 1/3 don't support carriers yet
- **Pattern 4 future**: Passthrough stub for continue support

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-09 02:14:57 +09:00
parent 0b705f9ca0
commit 4bff4ecf43
6 changed files with 485 additions and 2 deletions

View File

@ -0,0 +1,22 @@
// Phase 190: Number accumulation test - atoi implementation
// Tests: result = result * 10 + digit pattern
// Uses Pattern 2 (break) to enable carrier support
static box AtoiImpl {
method main() {
local result
result = 0
local i
i = 0
loop(i < 10) {
if i >= 3 {
break
}
local digit
digit = i
result = result * 10 + digit
i = i + 1
}
return result
}
}

View File

@ -0,0 +1,20 @@
// Phase 190: Number accumulation test - parse number implementation
// Tests: num = num * 10 + i pattern with different initial values
// Uses Pattern 2 (break) to enable carrier support
static box ParseNumberImpl {
method main() {
local num
num = 0
local i
i = 1
loop(i < 10) {
if i > 3 {
break
}
num = num * 10 + i
i = i + 1
}
print(num)
}
}

View File

@ -178,7 +178,8 @@ impl CommonPatternInitializer {
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(body, &dummy_carriers);
// Phase 188: Check if any update is complex (reject only UpdateRhs::Other)
// Allow: Const (numeric), Variable (numeric/string), StringLiteral
// Phase 190: Allow NumberAccumulation pattern
// Allow: Const (numeric), Variable (numeric/string), StringLiteral, NumberAccumulation
// Reject: Other (method calls, nested BinOp)
for update in updates.values() {
if let UpdateExpr::BinOp { rhs, .. } = update {
@ -193,6 +194,9 @@ impl CommonPatternInitializer {
UpdateRhs::StringLiteral(_) => {
// Phase 188: StringAppendLiteral: s = s + "x" (allowed)
}
UpdateRhs::NumberAccumulation { .. } => {
// Phase 190: Number accumulation: result = result * 10 + digit (allowed)
}
UpdateRhs::Other => {
// Phase 188: Complex update (method call, nested BinOp) - reject
eprintln!("[common_init/check_carriers] Phase 188: Complex update detected (UpdateRhs::Other), rejecting pattern");

View File

@ -134,6 +134,52 @@ pub fn emit_carrier_update_with_env(
}));
const_id
}
// Phase 190: Number accumulation pattern: result = result * base + digit
// Emit as: tmp = carrier * base; result = tmp + digit
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(|| {
format!(
"Number accumulation digit variable '{}' not found in UpdateEnv",
digit_var
)
})?;
// Step 4: Emit addition: result = tmp + digit
// This will be handled by the outer BinOp emission
// For now, return digit_id to be used as RHS
// We need to handle this specially - return tmp_id instead
// and adjust the outer BinOp to use correct values
// Actually, we need to emit both operations here
// Final result = tmp + digit
let result = alloc_value();
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: result,
op: *op, // Use the operation from outer UpdateExpr
lhs: tmp_id,
rhs: digit_id,
}));
// Return result directly - we've already emitted everything
return Ok(result);
}
// Phase 178/188: Complex updates (method calls) still rejected
UpdateRhs::Other => {
return Err(format!(
@ -276,6 +322,45 @@ pub fn emit_carrier_update(
}));
const_id
}
// Phase 190: Number accumulation pattern: result = result * base + digit
// Emit as: tmp = carrier * base; result = tmp + digit
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.get(digit_var).ok_or_else(|| {
format!(
"Number accumulation digit variable '{}' not found in ConditionEnv",
digit_var
)
})?;
// Step 4: Emit addition: result = tmp + digit
let result = alloc_value();
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: result,
op: *op, // Use the operation from outer UpdateExpr
lhs: tmp_id,
rhs: digit_id,
}));
// Return result directly - we've already emitted everything
return Ok(result);
}
// Phase 178/188: Complex updates (method calls) still rejected
UpdateRhs::Other => {
return Err(format!(
@ -743,4 +828,124 @@ mod tests {
assert!(result.is_ok());
assert_eq!(instructions.len(), 2); // Const + BinOp
}
#[test]
fn test_emit_number_accumulation_base10() {
// Phase 190: Test number accumulation pattern: result = result * 10 + digit
let mut cond_env = ConditionEnv::new();
cond_env.insert("result".to_string(), ValueId(20)); // Carrier parameter
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 carrier = test_carrier("result", 200);
let update = UpdateExpr::BinOp {
lhs: "result".to_string(),
op: BinOpKind::Add,
rhs: UpdateRhs::NumberAccumulation {
base: 10,
digit_var: "digit".to_string(),
},
};
let mut value_counter = 150u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
let mut instructions = Vec::new();
let result = emit_carrier_update_with_env(
&carrier,
&update,
&mut alloc_value,
&update_env,
&mut instructions,
);
assert!(result.is_ok());
let result_id = result.unwrap();
// Should generate 3 instructions:
// 1. Const(10) for base
// 2. BinOp(Mul, result, base) for tmp
// 3. BinOp(Add, tmp, digit) for final result
assert_eq!(instructions.len(), 3);
// Instruction 1: Const(10)
match &instructions[0] {
JoinInst::Compute(MirLikeInst::Const { dst, value }) => {
assert_eq!(*dst, ValueId(150)); // First allocated
assert!(matches!(value, ConstValue::Integer(10)));
}
_ => panic!("Expected Const instruction"),
}
// Instruction 2: BinOp(Mul, result, base)
match &instructions[1] {
JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs }) => {
assert_eq!(*dst, ValueId(151)); // Second allocated (tmp)
assert_eq!(*op, BinOpKind::Mul);
assert_eq!(*lhs, ValueId(20)); // result from env
assert_eq!(*rhs, ValueId(150)); // base const
}
_ => panic!("Expected BinOp(Mul) instruction"),
}
// Instruction 3: BinOp(Add, tmp, digit)
match &instructions[2] {
JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs }) => {
assert_eq!(*dst, ValueId(152)); // Third allocated (final result)
assert_eq!(*op, BinOpKind::Add);
assert_eq!(*lhs, ValueId(151)); // tmp from previous mul
assert_eq!(*rhs, ValueId(30)); // digit from env
}
_ => panic!("Expected BinOp(Add) instruction"),
}
assert_eq!(result_id, ValueId(152));
}
#[test]
fn test_emit_number_accumulation_digit_not_found() {
// Phase 190: Test error when digit variable not in env
let mut cond_env = ConditionEnv::new();
cond_env.insert("result".to_string(), ValueId(20));
// Note: digit NOT in env
let body_env = LoopBodyLocalEnv::new();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let carrier = test_carrier("result", 200);
let update = UpdateExpr::BinOp {
lhs: "result".to_string(),
op: BinOpKind::Add,
rhs: UpdateRhs::NumberAccumulation {
base: 10,
digit_var: "digit".to_string(),
},
};
let mut value_counter = 160u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
let mut instructions = Vec::new();
let result = emit_carrier_update_with_env(
&carrier,
&update,
&mut alloc_value,
&update_env,
&mut instructions,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("Number accumulation digit variable 'digit' not found"));
}
}

View File

@ -40,6 +40,7 @@ pub enum UpdateExpr {
/// Right-hand side of update expression
///
/// Phase 178: Extended to detect string updates for multi-carrier loops.
/// Phase 190: Extended to detect number accumulation pattern (result = result * base + digit).
/// The goal is "carrier detection", not full semantic understanding.
#[derive(Debug, Clone)]
pub enum UpdateRhs {
@ -49,6 +50,14 @@ pub enum UpdateRhs {
Variable(String),
/// Phase 178: String literal: result + "x"
StringLiteral(String),
/// Phase 190: Number accumulation pattern: result = result * base + digit
/// Represents expressions like: result * 10 + digit
NumberAccumulation {
/// Multiplication base (e.g., 10 for decimal, 2 for binary)
base: i64,
/// Variable name containing the digit to add
digit_var: String,
},
/// Phase 178: Other expression (method call, complex expr)
/// Used to detect "carrier has an update" without understanding semantics
Other,
@ -136,6 +145,7 @@ impl LoopUpdateAnalyzer {
/// Recognizes patterns like:
/// - `sum + i` → BinOp { lhs: "sum", op: Add, rhs: Variable("i") }
/// - `count + 1` → BinOp { lhs: "count", op: Add, rhs: Const(1) }
/// - Phase 190: `result * 10 + digit` → BinOp { lhs: "result", op: Add, rhs: NumberAccumulation {...} }
fn analyze_update_value(carrier_name: &str, value: &ASTNode) -> Option<UpdateExpr> {
match value {
ASTNode::BinaryOp {
@ -144,6 +154,43 @@ impl LoopUpdateAnalyzer {
right,
..
} => {
// Phase 190: Check for number accumulation pattern first
// Pattern: (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 let Some(mul_lhs_name) = Self::extract_variable_name(mul_left) {
if mul_lhs_name == carrier_name {
if let ASTNode::Literal {
value: LiteralValue::Integer(base),
..
} = mul_right.as_ref()
{
// Check if RHS is a variable (digit)
if let Some(digit_var) = Self::extract_variable_name(right) {
// This is number accumulation pattern!
let op = Self::convert_operator(operator)?;
return Some(UpdateExpr::BinOp {
lhs: carrier_name.to_string(),
op,
rhs: UpdateRhs::NumberAccumulation {
base: *base,
digit_var,
},
});
}
}
}
}
}
}
// Check if LHS is the carrier itself (e.g., sum in "sum + i")
if let Some(lhs_name) = Self::extract_variable_name(left) {
if lhs_name == carrier_name {
@ -169,6 +216,7 @@ impl LoopUpdateAnalyzer {
/// Analyze right-hand side of update expression
///
/// Phase 178: Extended to detect string updates.
/// Phase 190: Extended to detect number accumulation pattern.
/// Goal: "carrier has update" detection, not semantic understanding.
fn analyze_rhs(node: &ASTNode) -> Option<UpdateRhs> {
match node {
@ -189,11 +237,16 @@ impl LoopUpdateAnalyzer {
Some(UpdateRhs::Variable(name.clone()))
}
// Phase 190: Number accumulation pattern detection
// This is called from analyze_update_value, so we're analyzing the full RHS of an assignment
// Pattern not detected at this level - handled in analyze_update_value
// BinaryOp nodes that aren't simple Add/Sub are treated as complex
ASTNode::BinaryOp { .. } => Some(UpdateRhs::Other),
// Phase 178: Method call or other complex expression
// e.g., result + s.substring(pos, end)
ASTNode::Call { .. }
| ASTNode::MethodCall { .. }
| ASTNode::BinaryOp { .. }
| ASTNode::UnaryOp { .. } => Some(UpdateRhs::Other),
_ => None,
@ -264,4 +317,165 @@ mod tests {
panic!("Expected BinOp, got {:?}", updates.get("count"));
}
}
#[test]
fn test_analyze_number_accumulation_base10() {
// Test case: result = result * 10 + digit
use crate::ast::Span;
let body = vec![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".to_string(),
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,
}];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
assert_eq!(updates.len(), 1);
assert!(updates.contains_key("result"));
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);
assert_eq!(digit_var, "digit");
} else {
panic!("Expected NumberAccumulation, got {:?}", rhs);
}
} else {
panic!("Expected BinOp, got {:?}", updates.get("result"));
}
}
#[test]
fn test_analyze_number_accumulation_base2() {
// Test case: result = result * 2 + bit
use crate::ast::Span;
let body = vec![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(2),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "bit".to_string(),
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,
}];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
assert_eq!(updates.len(), 1);
assert!(updates.contains_key("result"));
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, 2);
assert_eq!(digit_var, "bit");
} else {
panic!("Expected NumberAccumulation, got {:?}", rhs);
}
} else {
panic!("Expected BinOp, got {:?}", updates.get("result"));
}
}
#[test]
fn test_analyze_number_accumulation_wrong_lhs() {
// Test case: result = other * 10 + digit (should be Complex/Other)
use crate::ast::Span;
let body = vec![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: "other".to_string(), // Wrong variable!
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".to_string(),
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,
}];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
// Should detect assignment but with Other (complex) RHS
assert_eq!(updates.len(), 0); // Won't match because lhs != carrier
}
}

View File

@ -387,6 +387,24 @@ pub(crate) fn lower_loop_with_continue_minimal(
const_1
}
}
// Phase 190: Number accumulation not supported in Pattern 4 yet
// Skip JoinIR update - use Select passthrough to keep carrier_merged defined
UpdateRhs::NumberAccumulation { .. } => {
eprintln!(
"[loop_with_continue_minimal] Phase 190: Carrier '{}' has number accumulation - not supported in Pattern 4, using Select passthrough",
carrier_name
);
// Emit Select with same values: carrier_merged = Select(_, carrier_param, carrier_param)
// This is effectively a passthrough (no JoinIR update)
loop_step_func.body.push(JoinInst::Select {
dst: carrier_merged,
cond: continue_cond, // Condition doesn't matter when both values are same
then_val: carrier_param,
else_val: carrier_param,
type_hint: None,
});
continue; // Skip the BinOp and normal Select below
}
// Phase 178: String updates detected but not lowered to JoinIR yet
// Skip JoinIR update - use Select passthrough to keep carrier_merged defined
UpdateRhs::StringLiteral(_) | UpdateRhs::Other => {