Phase 185: Body-local Pattern2/4 integration skeleton - Added collect_body_local_variables() helper - Integrated UpdateEnv usage in loop_with_break_minimal - Test files created (blocked by init lowering) Phase 186: Body-local init lowering infrastructure - Created LoopBodyLocalInitLowerer box (378 lines) - Supports BinOp (+/-/*//) + Const + Variable - Fail-Fast for method calls/string operations - 3 unit tests passing Phase 187: String UpdateLowering design (doc-only) - Defined UpdateKind whitelist (6 categories) - StringAppendChar/Literal patterns identified - 3-layer architecture documented - No code changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
21 KiB
Phase 185: Body-local Pattern2/4 Integration (int loops priority)
Date: 2025-12-09 Status: In Progress Phase Goal: Integrate Phase 184 infrastructure into Pattern2/4 for integer loop support
Overview
Phase 184 completed the body-local MIR lowering infrastructure with three boxes:
LoopBodyLocalEnv: Storage for body-local variable mappingsUpdateEnv: Unified resolution (ConditionEnv + LoopBodyLocalEnv)CarrierUpdateEmitter: Extended withemit_carrier_update_with_env()
Phase 185 integrates this infrastructure into Pattern2/4 lowerers to enable integer loops with body-local variables.
Target Loops
JsonParser integer loops:
_parse_number: Parses numeric strings withlocal digit_poscalculations_atoi: Converts string to integer withlocal digittemporary
Test cases:
phase184_body_local_update.hako: Pattern1 test (already works)phase184_body_local_with_break.hako: Pattern2 test (needs integration)
What We Do
Integrate body-local variables into update expressions:
loop(pos < len) {
local digit_pos = pos - start // Body-local variable
sum = sum * 10 // Update using body-local
sum = sum + digit_pos
pos = pos + 1
if (sum > 1000) break
}
Enable: digit_pos in sum = sum + digit_pos update expression
What We DON'T Do
String concatenation (Phase 178 Fail-Fast maintained):
loop(pos < len) {
local ch = s.substring(pos, pos+1)
num_str = num_str + ch // ❌ Still rejected (string concat)
}
Reason: String UpdateKind support is Phase 186+ work.
Architecture Integration
Current Flow (Phase 184 - Infrastructure Only)
┌──────────────────────┐
│ ConditionEnvBuilder │ → ConditionEnv (loop params)
└──────────────────────┘
↓
┌──────────────────────┐
│ LoopBodyLocalEnv │ ← NEW (Phase 184)
│ from_locals() │ Body-local variables
└──────────────────────┘
↓
┌──────────────────────┐
│ UpdateEnv │ ← NEW (Phase 184)
│ resolve(name) │ Unified resolution
└──────────────────────┘
↓
┌──────────────────────┐
│ CarrierUpdateEmitter │ ← EXTENDED (Phase 184)
│ emit_carrier_update_ │ UpdateEnv version
│ with_env() │
└──────────────────────┘
Status: Infrastructure complete, but Pattern2/4 still use old ConditionEnv path.
Phase 185 Flow (Integration)
Pattern2 changes:
// 1. Collect body-local variables
let body_locals = collect_body_local_variables(_body);
let body_local_env = LoopBodyLocalEnv::from_locals(body_locals);
// 2. Create UpdateEnv
let update_env = UpdateEnv::new(&condition_env, &body_local_env);
// 3. Use UpdateEnv in carrier update
let update_value = emit_carrier_update_with_env(
&carrier,
&update_expr,
&mut alloc_value,
&update_env, // ✅ Now has body-local support
&mut instructions,
)?;
Pattern4: Same pattern (minimal changes, copy from Pattern2 approach).
Task Breakdown
Task 185-1: Design Document ✅
This document - Architecture, scope, constraints, validation strategy.
Task 185-2: Pattern2 Integration
File: src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs
Changes:
- Add helper function (before cf_loop_pattern2_with_break):
/// Collect body-local variable declarations from loop body
///
/// Returns Vec<(name, ValueId)> for variables declared with `local` in loop body.
fn collect_body_local_variables(
body: &[ASTNode],
alloc_join_value: &mut dyn FnMut() -> ValueId,
) -> Vec<(String, ValueId)> {
let mut locals = Vec::new();
for node in body {
if let ASTNode::LocalDecl { name, .. } = node {
let value_id = alloc_join_value();
locals.push((name.clone(), value_id));
}
}
locals
}
- Modify cf_loop_pattern2_with_break (after ConditionEnvBuilder):
// Phase 185: Collect body-local variables
let body_locals = collect_body_local_variables(_body, &mut alloc_join_value);
let body_local_env = LoopBodyLocalEnv::from_locals(body_locals);
eprintln!("[pattern2/body-local] Collected {} body-local variables", body_local_env.len());
for (name, vid) in body_local_env.iter() {
eprintln!(" {} → {:?}", name, vid);
}
// Phase 185: Create UpdateEnv for unified resolution
let update_env = UpdateEnv::new(&env, &body_local_env);
- Update carrier update calls (search for
emit_carrier_update):
// OLD (Phase 184):
// let update_value = emit_carrier_update(&carrier, &update_expr, &mut alloc_join_value, &env, &mut instructions)?;
// NEW (Phase 185):
use crate::mir::join_ir::lowering::carrier_update_emitter::emit_carrier_update_with_env;
let update_value = emit_carrier_update_with_env(
&carrier,
&update_expr,
&mut alloc_join_value,
&update_env, // ✅ UpdateEnv instead of ConditionEnv
&mut instructions,
)?;
- Add imports:
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
use crate::mir::join_ir::lowering::update_env::UpdateEnv;
use crate::mir::join_ir::lowering::carrier_update_emitter::emit_carrier_update_with_env;
Estimate: 1 hour (straightforward, follow Phase 184 design)
Task 185-3: Pattern4 Integration (Minimal)
File: src/mir/builder/control_flow/joinir/patterns/pattern4_with_continue.rs
Changes: Same pattern as Pattern2 (copy-paste approach):
- Add
collect_body_local_variables()helper - Create
LoopBodyLocalEnvafter ConditionEnvBuilder - Create
UpdateEnv - Replace
emit_carrier_update()withemit_carrier_update_with_env()
Constraint: Only int carriers (string filter from Phase 178 remains active)
Estimate: 45 minutes (copy from Pattern2, minimal changes)
Task 185-4: Test Cases
Existing Tests (Reuse)
-
phase184_body_local_update.hako (Pattern1)
- Already passing (Pattern1 uses UpdateEnv)
- Verification:
NYASH_JOINIR_CORE=1 ./target/release/hakorune apps/tests/phase184_body_local_update.hako
-
phase184_body_local_with_break.hako (Pattern2)
- Currently blocked (Pattern2 not integrated yet)
- Will pass after Task 185-2
New Test: JsonParser Mini Pattern
File: apps/tests/phase185_p2_body_local_int_min.hako
// Minimal JsonParser-style loop with body-local integer calculation
static box Main {
main() {
local sum = 0
local pos = 0
local start = 0
local end = 5
// Pattern2: break loop with body-local digit_pos
loop(pos < end) {
local digit_pos = pos - start // Body-local calculation
sum = sum * 10
sum = sum + digit_pos // Use body-local in update
pos = pos + 1
if (sum > 50) break // Break condition
}
print(sum) // Expected: 0*10+0 → 0*10+1 → 1*10+2 → 12*10+3 → 123 → break
// Output: 123 (breaks before digit_pos=4)
}
}
Expected behavior:
- Pattern2 detection: ✅ (has break, no continue)
- Body-local collection:
digit_pos → ValueId(X) - UpdateEnv resolution:
digit_posfound in LoopBodyLocalEnv - Update emission:
sum = sum + digit_pos→ BinOp instruction - Execution: Output
123
Validation commands:
# Build
cargo build --release
# Structure trace
NYASH_JOINIR_STRUCTURE_ONLY=1 ./target/release/hakorune apps/tests/phase185_p2_body_local_int_min.hako
# Full execution
NYASH_JOINIR_CORE=1 ./target/release/hakorune apps/tests/phase185_p2_body_local_int_min.hako
String Concatenation Test (Fail-Fast Verification)
File: apps/tests/phase185_p2_string_concat_rejected.hako
// Verify Phase 178 Fail-Fast is maintained (string concat still rejected)
static box Main {
main() {
local result = ""
local i = 0
loop(i < 3) {
local ch = "a"
result = result + ch // ❌ Should be rejected (string concat)
i = i + 1
}
print(result)
}
}
Expected behavior:
- Pattern2 can_lower: ❌ Rejected (string/complex update detected)
- Error message:
[pattern2/can_lower] Phase 178: String/complex update detected, rejecting Pattern 2 (unsupported) - Build: ✅ Succeeds (compilation)
- Runtime: ❌ Falls back to error (no legacy LoopBuilder)
Task 185-5: Documentation Updates
Files to update:
- joinir-architecture-overview.md (Section 2.2):
### 2.2 条件式ライン(式の箱)
...
- **LoopBodyLocalEnv / UpdateEnv / CarrierUpdateEmitter(Phase 184-185)**
- **Phase 184**: Infrastructure implementation
- **Phase 185**: Integration into Pattern2/4
- Pattern2/4 now use UpdateEnv for body-local variable support
- String concat still rejected (Phase 178 Fail-Fast maintained)
- CURRENT_TASK.md (Update Phase 185 entry):
- [x] **Phase 185: Body-local Pattern2/4 Integration** ✅ (2025-12-09)
- Task 185-1: Design document (phase185-body-local-integration.md)
- Task 185-2: Pattern2 integration (body-local collection + UpdateEnv)
- Task 185-3: Pattern4 integration (minimal, copy from Pattern2)
- Task 185-4: Test cases (phase185_p2_body_local_int_min.hako)
- Task 185-5: Documentation updates
- **成果**: Pattern2/4 now support body-local variables in integer update expressions
- **制約**: String concat still rejected (Phase 178 Fail-Fast)
- **次ステップ**: Phase 186 (String UpdateKind support)
- This document (phase185-body-local-integration.md):
- Add "Implementation Complete" section
- Record test results
- Document any issues found
Scope and Constraints
In Scope
-
Integer carrier updates with body-local variables ✅
sum = sum + digit_poswheredigit_posis body-local- Pattern2 (break) and Pattern4 (continue)
-
Phase 184 infrastructure integration ✅
- LoopBodyLocalEnv collection
- UpdateEnv usage
- emit_carrier_update_with_env() calls
-
Backward compatibility ✅
- Existing tests must still pass
- No changes to Pattern1/Pattern3
- No changes to Trim patterns (Pattern5)
Out of Scope
-
String concatenation ❌
- Phase 178 Fail-Fast is maintained
result = result + chstill rejected- Will be Phase 186+ work
-
Complex expressions in body-locals ❌
- Method calls:
local ch = s.substring(pos, pos+1)(limited by JoinIrBuilder) - Will be addressed in Phase 186+
- Method calls:
-
Condition variable usage of body-locals ❌
if (temp > 6) breakwheretempis body-local (already handled by Phase 183 rejection)
Validation Strategy
Success Criteria
- Unit tests pass: All existing carrier_update tests still green ✅
- Pattern2 integration: phase184_body_local_with_break.hako executes correctly ✅
- Pattern4 integration: Pattern4 tests with body-locals work (if exist) ✅
- Representative test: phase185_p2_body_local_int_min.hako outputs
123✅ - Fail-Fast maintained: phase185_p2_string_concat_rejected.hako rejects correctly ✅
- No regression: Trim patterns (phase172_trim_while.hako) still work ✅
Test Commands
# 1. Unit tests
cargo test --release --lib pattern2_with_break
cargo test --release --lib pattern4_with_continue
cargo test --release --lib carrier_update
# 2. Integration tests
NYASH_JOINIR_CORE=1 ./target/release/hakorune apps/tests/phase184_body_local_update.hako
NYASH_JOINIR_CORE=1 ./target/release/hakorune apps/tests/phase184_body_local_with_break.hako
NYASH_JOINIR_CORE=1 ./target/release/hakorune apps/tests/phase185_p2_body_local_int_min.hako
# 3. Fail-Fast verification
NYASH_JOINIR_CORE=1 ./target/release/hakorune apps/tests/phase185_p2_string_concat_rejected.hako 2>&1 | grep "String/complex update detected"
# 4. Regression check
NYASH_JOINIR_CORE=1 ./target/release/hakorune apps/tests/phase172_trim_while.hako
Design Principles
Box Theory Compliance
-
Single Responsibility:
- LoopBodyLocalEnv: Storage only
- UpdateEnv: Resolution only
- CarrierUpdateEmitter: Emission only
-
Clear Boundaries:
- ConditionEnv vs LoopBodyLocalEnv (distinct scopes)
- UpdateEnv composition (no ownership, just references)
-
Deterministic:
- BTreeMap in LoopBodyLocalEnv (consistent ordering)
- Priority order in UpdateEnv (condition → body-local)
-
Conservative:
- No changes to Trim/Pattern5 logic
- String concat still rejected (Phase 178 Fail-Fast)
Fail-Fast Principle
From Phase 178: Reject unsupported patterns explicitly, not silently.
Maintained in Phase 185:
- String concat → Explicit error in can_lower()
- Complex expressions → Error from JoinIrBuilder
- Shadowing → Error from UpdateEnv priority logic
No fallback to LoopBuilder (deleted in Phase 187).
Known Limitations
Not Supported (By Design)
- String concatenation:
result = result + ch // ❌ Still rejected (Phase 178)
- Body-local in conditions:
loop(i < 5) {
local temp = i * 2
if (temp > 6) break // ❌ Already rejected (Phase 183)
}
- Complex init expressions:
local temp = s.substring(pos, pos+1) // ⚠️ Limited by JoinIrBuilder
Will Be Addressed
- Phase 186: String UpdateKind support (careful, gradual)
- Phase 187: Method call support in body-local init
- Phase 188: Full JsonParser loop coverage
Implementation Notes
collect_body_local_variables() Helper
Design decision: Keep it simple, only collect LocalDecl nodes.
Why not more complex?:
- Body-local variables are explicitly declared with
localkeyword - No need to track assignments (that's carrier analysis)
- No need to track scopes (loop body is single scope)
Alternative approaches considered:
- Reuse LoopScopeShapeBuilder logic ❌ (too heavyweight, circular dependency)
- Scan all variable references ❌ (over-complex, not needed)
- Simple LocalDecl scan ✅ (chosen - sufficient, clean)
UpdateEnv vs ConditionEnv
Why not extend ConditionEnv?:
- Separation of concerns (condition variables vs body-locals are conceptually different)
- Composition over inheritance (UpdateEnv composes two environments)
- Backward compatibility (ConditionEnv unchanged, existing code still works)
emit_carrier_update_with_env vs emit_carrier_update
Why two functions?:
- Backward compatibility (old code uses ConditionEnv directly)
- Clear API contract (with_env = supports body-locals, without = condition only)
- Gradual migration (Pattern1/3 can stay with old API, Pattern2/4 migrate)
References
- Phase 184: LoopBodyLocalEnv/UpdateEnv/CarrierUpdateEmitter infrastructure
- Phase 183: LoopBodyLocal role separation (condition vs body-only)
- Phase 178: String carrier rejection (Fail-Fast principle)
- Phase 171-C: LoopBodyCarrierPromoter (Trim pattern handling)
- pattern2_with_break.rs: Current Pattern2 implementation
- pattern4_with_continue.rs: Current Pattern4 implementation
- carrier_update_emitter.rs: Update emission logic
Implementation Status (2025-12-09)
✅ Completed
-
Task 185-1: Design document created ✅
-
Task 185-2: Pattern2 integration skeleton completed ✅
collect_body_local_variables()helper addedbody_local_envparameter added tolower_loop_with_break_minimalemit_carrier_update_with_env()integration- Build succeeds (no compilation errors)
-
Task 185-3: Pattern4 deferred ✅ (different architecture, inline lowering)
❌ Blocked
Task 185-4: Test execution BLOCKED by missing body-local init lowering
Error: use of undefined value ValueId(11) for body-local variable digit_pos
Root cause: Phase 184 implemented storage/resolution infrastructure but left initialization lowering unimplemented.
What's missing:
- Body-local init expression lowering (
local digit_pos = pos - start) - JoinIR instruction generation for init expressions
- Insertion of init instructions in loop body
Current behavior:
- ✅ Variables are collected (name → ValueId mapping)
- ✅ UpdateEnv can resolve body-local variable names
- ❌ Init expressions are NOT lowered to JoinIR
- ❌ ValueIds are allocated but never defined
Evidence:
[pattern2/body-local] Collected local 'digit_pos' → ValueId(2) ✅ Name mapping OK
[pattern2/body-local] Phase 185-2: Collected 1 body-local variables ✅ Collection OK
[ERROR] use of undefined value ValueId(11) ❌ Init not lowered
Scope Clarification
Phase 184 scope:
- LoopBodyLocalEnv (storage) ✅
- UpdateEnv (resolution) ✅
- emit_carrier_update_with_env() (emission) ✅
- Body-local init lowering: ⚠️ NOT IMPLEMENTED
Phase 185 intended scope:
- Pattern2/4 integration ✅ (Pattern2 skeleton done)
- Assumed init lowering was in Phase 184 ❌ (incorrect assumption)
Actual blocker: Init lowering is Phase 186 work, not Phase 185.
Next Phase: Phase 186 - Body-local Init Lowering
Goal
Implement body-local variable initialization lowering to make Phase 185 integration functional.
Required Changes
1. Modify collect_body_local_variables()
Current (Phase 185):
fn collect_body_local_variables(body: &[ASTNode], alloc: &mut dyn FnMut() -> ValueId) -> Vec<(String, ValueId)> {
// Only allocates ValueIds, doesn't lower init expressions
for node in body {
if let ASTNode::Local { variables, .. } = node {
for name in variables {
let value_id = alloc(); // Allocated but never defined!
locals.push((name.clone(), value_id));
}
}
}
}
Needed (Phase 186):
fn collect_and_lower_body_locals(
body: &[ASTNode],
env: &ConditionEnv,
alloc: &mut dyn FnMut() -> ValueId,
instructions: &mut Vec<JoinInst>, // Need to emit init instructions!
) -> Result<Vec<(String, ValueId)>, String> {
for node in body {
if let ASTNode::Local { variables, initial_values, .. } = node {
for (name, init_expr_opt) in variables.iter().zip(initial_values.iter()) {
if let Some(init_expr) = init_expr_opt {
// Lower init expression to JoinIR
let init_value_id = lower_expr_to_joinir(init_expr, env, alloc, instructions)?;
locals.push((name.clone(), init_value_id));
} else {
// No init: allocate but leave undefined (or use Void constant)
let value_id = alloc();
locals.push((name.clone(), value_id));
}
}
}
}
}
2. Add Expression Lowerer
Need a helper function to lower AST expressions to JoinIR:
fn lower_expr_to_joinir(
expr: &ASTNode,
env: &ConditionEnv,
alloc: &mut dyn FnMut() -> ValueId,
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
match expr {
ASTNode::BinOp { op, left, right, .. } => {
let lhs = lower_expr_to_joinir(left, env, alloc, instructions)?;
let rhs = lower_expr_to_joinir(right, env, alloc, instructions)?;
let result = alloc();
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: result,
op: map_binop(op),
lhs,
rhs,
}));
Ok(result)
}
ASTNode::Variable { name, .. } => {
env.get(name).ok_or_else(|| format!("Variable '{}' not in scope", name))
}
// ... handle other expression types
}
}
3. Update lower_loop_with_break_minimal
Insert body-local init instructions at the start of loop_step function:
// After allocating loop_step parameters, before break condition:
if let Some(body_env) = body_local_env {
// Emit body-local init instructions
for (name, value_id) in body_env.iter() {
// Init instructions already emitted by collect_and_lower_body_locals
// Just log for debugging
eprintln!("[loop_step] Body-local '{}' initialized as {:?}", name, value_id);
}
}
Estimate
- Helper function (lower_expr_to_joinir): 2-3 hours (complex, many AST variants)
- collect_and_lower_body_locals refactor: 1 hour
- Integration into lower_loop_with_break_minimal: 1 hour
- Testing and debugging: 2 hours
Total: 6-7 hours for Phase 186
Alternative: Simplified Scope
If full expression lowering is too complex, Phase 186-simple could:
- Only support variable references in body-local init (no binops)
local temp = i✅local temp = i + 1❌ (Phase 187)
- Implement just variable copying
- Get tests passing with simple cases
- Defer complex expressions to Phase 187
Estimate for Phase 186-simple: 2-3 hours
Lessons Learned
- Phase 184 scope was incomplete: Infrastructure without lowering is not functional
- Testing earlier would have caught this: Phase 184 should have had E2E test
- Phase 185 assumption was wrong: Assumed init lowering was done, it wasn't
- Clear scope boundaries needed: "Infrastructure" vs "Full implementation"