Added verify_exit_phi_no_collision() to contract_checks.rs for early detection
of ValueId collisions between exit PHIs and other instructions.
Problem detected:
- If exit_phi_builder uses builder.value_gen.next() (module-level) instead of
func.next_value_id() (function-level), ValueIds can collide:
Example:
- bb0: %1 = const 0 (counter init)
- bb3: %1 = phi ... (exit PHI - collision!)
Previous behavior:
- Error only detected at LLVM backend runtime
- Cryptic error: "Cannot overwrite PHI dst=1"
New behavior:
- Panic at Rust compile time (debug build)
- Clear error message with fix suggestion:
"Exit PHI dst %1 collides with instruction in block 0
Fix: Use func.next_value_id() in exit_phi_builder.rs"
Benefits:
- 🔥 Fail-Fast: Catch errors during Rust compilation, not LLVM execution
- 📋 Clear messages: Exact collision point + fix suggestion
- 🧪 Testable: verify_exit_phi_no_collision() can be unit tested
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: exit_phi_builder.rs used builder.value_gen.next() (module-level counter)
instead of func.next_value_id() (function-level counter) for allocating exit PHI dst.
This caused ValueId collisions in functions like:
- bb0: %1 = const 0 (initial counter value)
- bb3: %1 = phi ... (exit PHI - collision!)
The LLVM backend then failed with:
"Cannot overwrite PHI dst=1. ValueId namespace collision detected."
Fix: Use func.next_value_id() to allocate from the function's local counter,
ensuring unique ValueIds within each function.
Verified:
- Pattern 1 (simple while): VM RC:3, LLVM RC:3 ✅
- Case C (Pattern 5): VM returns 3, LLVM Result: 3 ✅🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
## Summary
Observed PHI dst ValueId distribution to determine if verifier can enforce
reserved region (0-99). **Conclusion: Verifier strengthening NOT recommended.**
## Key Finding
PHI dst allocation does NOT architecturally respect reserved region:
- PHI dst comes from `builder.next_value_id()` (host MirBuilder)
- Reserved region (0-99) is a JoinValueSpace contract for JoinIR lowering
- These are separate allocation pools with no enforcement mechanism
- Current stability is accidental (ValueId allocation ordering)
## Evidence
Manual verification (`apps/tests/loop_min_while.hako`):
- PHI dst = %3 (ValueId(3)) ✅ in reserved region
- Works because PHI allocated early in function (0-20 typical)
- JoinValueSpace allocates high (100+, 1000+)
- Accidental separation, not enforced
## Implementation
Added observation infrastructure (debug-only):
- `src/mir/join_ir/verify_phi_reserved.rs` (266 lines)
- PHI dst observer with distribution analyzer
- Region classifier (Reserved/Param/Local)
- Human-readable report generator
- Instrumentation at PHI allocation points:
- loop_header_phi_builder.rs:94, 151
- Debug-only `observe_phi_dst()` calls
## Test Results
- Unit tests: ✅ 4/4 PASS (verify_phi_reserved module)
- Lib tests: ✅ 950/950 PASS, 56 ignored
- Normalized tests: ✅ 54/54 PASS
- Manual verification: ✅ PHI dst in 0-99 observed
## Decision: Document, Don't Enforce
**Rationale**:
1. No architectural mechanism to enforce PHI dst ∈ [0, 99]
2. Adding verifier creates false assumptions about allocation order
3. Current system stable through separate pools (950/950 tests)
4. Future architectural fix possible (Phase 73+) but not urgent
## Documentation
- PHASE_72_SUMMARY.md: Executive summary and implementation record
- phase72-phi-reserved-observation.md: Detailed findings and analysis
- CURRENT_TASK.md: Phase 72 completion entry
## Next Steps
- Phase 73: Update architecture overview with Phase 72 findings
- Optional: Explicit PHI reserved pool (future enhancement)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extend CapturedEnv to include function parameters used in loop conditions,
enabling ExprLowerer to resolve variables like `s` in `loop(p < s.length())`.
Phase 245C changes:
- function_scope_capture.rs: Add collect_names_in_loop_parts() helper
- function_scope_capture.rs: Extend analyze_captured_vars_v2() with param capture logic
- function_scope_capture.rs: Add 4 new comprehensive tests
Test fix:
- expr_lowerer/ast_support.rs: Accept all MethodCall nodes for syntax support
(validation happens during lowering in MethodCallLowerer)
Problem solved: "Variable not found: s" errors in loop conditions
Test results: 924/924 PASS (+13 from baseline 911)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- ExitMetaCollector includes ConditionOnly carriers in exit_bindings (for latch)
- ExitLineReconnector skips ConditionOnly in variable_map updates
- Pattern 2/3/4 pass carrier_info to ExitMetaCollector
- Resolves "has no latch incoming set" error
Role separation:
- Header PHI: entry + latch for both LoopState and ConditionOnly
- Exit PHI: LoopState only (ConditionOnly excluded)
- variable_map: LoopState only (ConditionOnly skipped)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CarrierInit enum (FromHost/BoolConst) for explicit initialization policy
- LoopHeaderPhiBuilder generates Const instruction for BoolConst carriers
- merge/mod.rs uses carrier_info to include ALL carriers in header PHI
- Pattern 2/4 pass carrier_info to boundary builder
- ConditionOnly carriers (is_digit_pos) now included in header PHI
Remaining: latch incoming for ConditionOnly carriers (Phase 228-8)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CarrierRole enum to distinguish state carriers from condition-only carriers
- ConditionOnly carriers (is_digit_pos) skip exit PHI but keep header PHI
- Update all test struct literals with role field
- 877/884 tests PASS (7 pre-existing failures unrelated)
Remaining: ValueId(0) undefined - header PHI initialization for ConditionOnly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add expr_result handling in merge_joinir_mir_blocks
- When expr_result matches a carrier, return carrier PHI dst
- Enables expr-position loops to properly return accumulator values
Note: Phase 219 regression (loop_if_phi.hako) to be fixed in next commit
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fixed ValueId collision between body-local and carrier params
- Added ExitLine contract verifier (debug assertions)
- Updated test files to use Main box
- E2E verified: atoi→12, parse_number→123
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 179-A Step 3: Improve code maintainability by replacing hardcoded
magic values with descriptive named constants.
Changes:
- instruction_rewriter.rs: K_EXIT_FUNC_NAME constant for "join_func_2"
- pattern3_with_if_phi.rs: PATTERN3_K_EXIT_SUM_FINAL_ID for ValueId(18)
Benefits:
- Self-documenting code (names explain the meaning)
- Easier to maintain (change in one place)
- Prevents typos and inconsistencies
Task 176-1: Pattern2 limitation investigation
- Identified 10 limitation points where only position carrier was handled
- Added TODO markers for Phase 176-2/3 implementation
- Created phase176-pattern2-limitations.md documentation
Task 176-2: CarrierUpdateLowerer helper implementation
- Implemented emit_carrier_update() helper function
- Supports CounterLike and AccumulationLike UpdateExpr patterns
- Added 6 unit tests (all passing)
- Fail-Fast error handling for carrier/variable not found
Task 176-3: Pattern2 lowerer multi-carrier extension
- Extended header PHI generation for all carriers
- Implemented loop update for all carriers using emit_carrier_update()
- Extended ExitLine/ExitMeta construction for all carriers
- Updated function call/jump args to include all carriers
- 9/10 tests passing (1 pre-existing test issue)
Task 176-4: E2E testing and bug fixes
- Fixed Trim pattern loop_var_name overwrite bug (pattern2_with_break.rs)
- Fixed InstructionRewriter latch_incoming mapping bug
- All E2E tests passing (RC=0): pos + result dual-carrier loops work
- test_jsonparser_parse_string_min2.hako verified
Task 176-5: Documentation updates
- Created phase176-completion-report.md
- Updated phase175-multicarrier-design.md with completion status
- Updated joinir-architecture-overview.md roadmap
- Updated CURRENT_TASK.md with Phase 176 completion + Phase 177 TODO
- Updated loop_pattern_space.md F-axis (multi-carrier support complete)
Technical achievements:
- Pattern2 now handles single/multiple carriers uniformly
- CarrierInfo architecture proven to work end-to-end
- Two critical bugs fixed (loop_var overwrite, latch_incoming mapping)
- No regressions in existing tests
Next: Phase 177 - Apply to JsonParser _parse_string full implementation
Debug-only verification module to catch JoinIR contract violations early:
- verify_loop_header_phis: Checks loop_var_name → PHI exists in header block
- verify_exit_line: Checks exit_bindings → values exist, exit block in range
- verify_joinir_contracts: Main entry point, runs all checks
Implementation:
- Added verification functions to merge/mod.rs (private module has type access)
- Called from merge_joinir_mir_blocks after exit block setup
- Only active in debug builds (#[cfg(debug_assertions)])
Benefits:
- Catches "动くけど header PHI 無い" bugs immediately
- Validates exit_bindings before variable_map reconnection
- Prevents silent contract violations during development
## Problem
Pattern 4 (loop with continue) was producing SSA-undef errors due to function_params
key mismatch. function_params uses 'join_func_0'/'join_func_1' format (from join_func_name()),
but code was looking for 'main'/'loop_step'.
## Solution
- Fix mod.rs: Use correct function keys 'join_func_0' and 'join_func_1'
- Add warning logs to detect future key mismatches immediately
- Update pattern4_with_continue.rs: Mark as fully implemented (not stub)
- Remove unused imports: MergeResult, TailCallKind, classify_tail_call, etc.
## Testing
- Pattern 3 (If PHI): sum=9 ✅
- Pattern 4 (Continue): 25 ✅ (1+3+5+7+9)
- No SSA-undef errors
- No new warnings on successful execution
## Debug Improvements
Added pre-flight checks in merge/mod.rs Phase 33-21:
```
[cf_loop/joinir] WARNING: function_params.get('join_func_0') returned None.
Available keys: [...]
```
This will save significant debugging time for future parameter mapping issues.
🤖 Generated with Claude Code
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Box theory refactoring: Extract exit_bindings construction logic from
pattern lowerers into focused, reusable ExitMetaCollector Box.
**Changes**:
1. Created meta_collector.rs (+102 lines):
- ExitMetaCollector::collect() builds exit_bindings from ExitMeta
- Pure function (no side effects)
- Reusable by all pattern lowerers
2. Updated pattern2_with_break.rs (-20 lines):
- Use ExitMetaCollector::collect() instead of inline filter_map
- Removed manual binding construction loop
- Cleaner caller code
3. Made exit_line module public:
- Allows pattern lowerers to use ExitMetaCollector
- Clear module visibility boundaries
**Box Design**:
- Single responsibility: Convert ExitMeta + variable_map → exit_bindings
- Pure function: No side effects, testable independently
- Reusable: Can be used by Pattern 3, Pattern 4, etc.
**Testing**:
- Build: ✅ Success (1m 04s)
- Execution: ✅ RC: 0 (Pattern 2 verified)
- Regression: ✅ No issues
**Metrics**:
- New lines: +102 (meta_collector.rs)
- Removed lines: -20 (pattern2_with_break.rs)
- Net change: +82 lines
- Code clarity: Significantly improved
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
Previously, PHI incoming values were not being remapped from JoinIR-local
ValueIds to Host ValueIds. Only the block IDs were remapped.
This fix ensures both block ID and value ID are remapped for PHI
instructions during JoinIR merge.
Note: trim still fails because condition variable exit values are not
being reflected in variable_map (Phase 172-4/5 work needed).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Proper HOST↔JoinIR ValueId separation for condition variables:
- Add ConditionEnv struct (name → JoinIR-local ValueId mapping)
- Add ConditionBinding struct (HOST/JoinIR ValueId pairs)
- Modify condition_to_joinir to use ConditionEnv instead of builder.variable_map
- Update Pattern2 lowerer to build ConditionEnv and ConditionBindings
- Extend JoinInlineBoundary with condition_bindings field
- Update BoundaryInjector to inject Copy instructions for condition variables
This fixes the undefined ValueId errors where HOST ValueIds were being
used directly in JoinIR instructions. Programs now execute (RC: 0),
though loop variable exit values still need Phase 172 work.
Key invariants established:
1. JoinIR uses ONLY JoinIR-local ValueIds
2. HOST↔JoinIR bridging is ONLY through JoinInlineBoundary
3. condition_to_joinir NEVER accesses builder.variable_map
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Two root causes for Pattern 4 outputting 0 instead of 25:
1. instruction_rewriter.rs:92 - BasicBlock::new() was overwriting
existing blocks that already contained PHI instructions from
JoinIR Select lowering. Fixed by removing and reusing existing
blocks instead of creating new ones.
2. pattern4_with_continue.rs - JoinIR exit PHI (ValueId 15) was not
connected to host's sum variable, causing DCE to eliminate the PHI
as "unused". Fixed by using new_with_exit_bindings() with explicit
LoopExitBinding to connect k_exit's sum_exit to host's sum slot.
Test result: loop_continue_pattern4.hako now correctly outputs 25.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>