feat(joinir): Phase 78 - BindingId infrastructure for promoted carriers (dev-only)

Phase 78 adds infrastructure to assign BindingIds to synthetic promoted
carriers (e.g., is_digit_pos, is_ch_match), enabling type-safe promoted
variable lookup without string-based naming conventions.

Key Changes:
1. CarrierVar.binding_id field (dev-only):
   - Added Option<BindingId> to track BindingId for each carrier
   - Updated all constructors and struct instantiations

2. CarrierBindingAssigner Box (new file, 273 lines):
   - Allocates BindingIds for promoted carriers via builder.allocate_binding_id()
   - Records original → promoted mapping in promoted_bindings
   - Sets binding_id field on promoted CarrierVar
   - Includes 3 comprehensive unit tests

3. ConditionEnv.register_carrier_binding() (new method):
   - Registers carrier BindingId → ValueId mappings
   - Enables type-safe lookup via binding_id_map

4. Logging cleanup:
   - Gated 6 eprintln! statements with NYASH_JOINIR_DEBUG
   - Unified logging tags to [binding_pilot/*]

Design Decisions:
- Promoters create CarrierInfo, lowering code assigns BindingIds
- CarrierBindingAssigner called from Pattern2/4 lowering (has builder access)
- Clear documentation prevents misuse (promoters lack builder access)

Files modified (18):
- carrier_info.rs: binding_id field added to CarrierVar
- carrier_binding_assigner.rs: New Box for BindingId allocation
- condition_env.rs: register_carrier_binding() method
- mod.rs: Module exports
- pattern2_with_break.rs, pattern4_with_continue.rs: Updated for binding_id
- loop_body_*_promoter.rs: Logging cleanup + binding_id in structs
- phase78-bindingid-promoted-carriers.md: Architecture documentation

Tests: 970/970 PASS (zero regressions)
Status: Infrastructure complete, integration deferred to Phase 79

Next Phase: Wire CarrierBindingAssigner in Pattern2/4 lowering + E2E tests

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-13 16:20:33 +09:00
parent 48bdf2fb98
commit 8b48bec962
21 changed files with 815 additions and 107 deletions

View File

@ -0,0 +1,61 @@
## Phase 78: BindingId for Promoted Carriers (dev-only)
### Goal
Make LoopBodyLocal promotion (DigitPos/Trim) **BindingId-aware** without relying on fragile name-based hacks like `format!("is_{}", name)`.
This phase introduces a dev-only identity link:
- `BindingId(original LoopBodyLocal)``BindingId(promoted carrier)``ValueId(join carrier param)`
### Problem
Promotion creates synthetic carrier names (例: `is_digit_pos`, `is_ch_match`) that do not exist as source-level bindings.
- `PromotedBindingRecorder` expects both original/promoted names in `MirBuilder.binding_map`.
- For promoted carriers this is not true by construction.
- For LoopBodyLocal variables, promotion can happen before their `local` is lowered, so the original name may also be absent.
### Solution (current state)
#### 1) CarrierVar gets optional BindingId (dev-only)
- `src/mir/join_ir/lowering/carrier_info.rs`
- `CarrierVar.binding_id: Option<BindingId>` (feature `normalized_dev`)
#### 2) CarrierBindingAssigner box
- `src/mir/join_ir/lowering/carrier_binding_assigner.rs`
- Ensures both sides have BindingIds:
- If `original_name` is missing in `builder.binding_map`, allocate a temporary BindingId.
- If `promoted_carrier_name` is missing, allocate a temporary BindingId.
- Records:
- `carrier_info.promoted_bindings[original_bid] = promoted_bid`
- `CarrierVar.binding_id = Some(promoted_bid)` for the promoted carrier
- Restores `builder.binding_map` after recording (synthetic names do not leak into the source map).
#### 3) Pattern2/Pattern4 wiring
- Pattern2 (break): `src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs`
- Calls `CarrierBindingAssigner::assign_promoted_binding()` immediately after promotion.
- Registers `BindingId → ValueId` via `ConditionEnv.register_carrier_binding()` after carrier join_id allocation.
- Keeps legacy name-based aliasing (`digit_pos``is_digit_pos`) for now, but removes local “guess naming” by using the promoted carrier name returned by the promoter.
- Pattern4 (continue): `src/mir/builder/control_flow/joinir/patterns/pattern4_with_continue.rs`
- Calls `CarrierBindingAssigner::assign_promoted_binding()` after promotion (analysis metadata only, since Pattern4s current lowering path does not use JoinValueSpace params).
### Tests
- Unit test (dev-only): `src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs`
- `phase78_promoted_binding_is_recorded_for_digitpos`
- Asserts:
- `promoted_bindings` has one mapping
- promoted carrier has `binding_id`
- `ConditionEnv.binding_id_map[promoted_bid] == promoted_carrier.join_id`
### Open Follow-ups (Phase 79+)
1. Wire `ScopeManager::lookup_with_binding()` into ExprLowerer/ConditionLoweringBox call-sites (so BindingId actually drives resolution).
2. Extend coverage to Trim and other promotion shapes (additional unit/E2E tests).
3. Shrink/remove legacy name-based promoted lookup (`resolve_promoted_join_id`) after call-sites consistently provide BindingId.