Files
hakorune/docs/development/current/main/phase193_exit_binding_builder.md
nyash-codex 350dba92b4 feat(joinir): Phase 193-4 - Exit Binding Builder implementation
Implemented fully boxified exit binding generation for Pattern 3 & 4.
Eliminates hardcoded variable names and ValueId assumptions.

**New files**:
- docs/development/current/main/phase193_exit_binding_builder.md: Design document
- src/mir/builder/control_flow/joinir/patterns/exit_binding.rs: ExitBindingBuilder implementation (400+ lines)

**Key components**:
- LoopExitBinding: Maps JoinIR exit values to host function variables
- ExitBindingBuilder: Generates bindings from CarrierInfo + ExitMeta
- Comprehensive validation:
  - Carrier name mismatch detection
  - Missing carrier detection
  - Loop variable incorrectly in exit_values
- Builder methods:
  - new(): Create builder with metadata validation
  - build_loop_exit_bindings(): Generate bindings, update variable_map
  - apply_to_boundary(): Set JoinInlineBoundary host/join_outputs
  - loop_var_exit_binding(): Get loop variable exit binding
- Unit tests: 6 test cases covering single/multi-carrier and error scenarios

**Features**:
- Supports both single and multi-carrier loop patterns
- Automatic post-loop ValueId allocation for carriers
- Sorted carrier processing for determinism
- Full integration with CarrierInfo and ExitMeta from Phase 193-2

**Status**: Phase 193-4 implementation complete. Ready for Phase 193-5 integration.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 11:09:03 +09:00

8.7 KiB

Phase 193-4: ExitBindingBuilder Design & Implementation

Phase: 193-4 Status: Design Phase Date: 2025-12-06 Goal: Fully boxify loop exit binding generation for Pattern 3 & 4, eliminating hardcoded variable names and ValueId assumptions


Overview

The ExitBindingBuilder box connects JoinIR exit values (from loop lowering) back to the host function's variable_map. This eliminates:

  • Hardcoded variable names like "sum", "printed"
  • Assumptions about single-carrier patterns
  • Complex ValueId plumbing scattered across Pattern 3/4 lowerers

Architecture Diagram

Pattern Lowerer
    ↓
    ├─ CarrierInfo (loop_var, carriers[], host_ids)
    ├─ ExitMeta (join_exit_values[])
    └─ variable_map
    ↓
ExitBindingBuilder
    ↓
    ├─ LoopExitBinding[] (carrier → host mapping)
    └─ JoinInlineBoundary update (host_outputs, join_outputs)
    ↓
Host function variable_map (updated with new ValueIds)

Data Structures

Input: CarrierInfo

pub struct CarrierInfo {
    pub loop_var_name: String,        // e.g., "i"
    pub loop_var_id: ValueId,         // Host-side ValueId for loop var
    pub carriers: Vec<CarrierVar>,    // [{ name: "sum", host_id: ValueId(10) }, ...]
}

pub struct CarrierVar {
    pub name: String,                 // Variable name (e.g., "sum")
    pub host_id: ValueId,             // Host-side ValueId (initial value)
}

Input: ExitMeta

pub struct ExitMeta {
    pub exit_values: Vec<(String, ValueId)>,
    // Example: [("sum", ValueId(15)), ("printed", ValueId(16))]
    // where ValueId(15/16) are in JoinIR-local space (parameters/results)
}

Output: LoopExitBinding (New)

pub struct LoopExitBinding {
    /// Carrier variable name (e.g., "sum", "printed")
    pub carrier_name: String,

    /// Host-side ValueId for this carrier
    pub host_id: ValueId,

    /// Join-side exit ValueId (from ExitMeta)
    pub join_exit_id: ValueId,
}

JoinInlineBoundary Updates

pub struct JoinInlineBoundary {
    // ... existing fields ...

    /// Host-side output ValueIds (one per carrier + loop_var)
    pub host_outputs: Vec<ValueId>,

    /// Join-side output ValueIds (one per carrier + loop_var, in JoinIR space)
    pub join_outputs: Vec<ValueId>,
}

API Design

ExitBindingBuilder

pub struct ExitBindingBuilder<'a> {
    carrier_info: &'a CarrierInfo,
    exit_meta: &'a ExitMeta,
    variable_map: &'a mut HashMap<String, ValueId>,
}

impl<'a> ExitBindingBuilder<'a> {
    /// Create a new builder from metadata
    pub fn new(
        carrier_info: &'a CarrierInfo,
        exit_meta: &'a ExitMeta,
        variable_map: &'a mut HashMap<String, ValueId>,
    ) -> Result<Self, String>;

    /// Generate loop exit bindings
    ///
    /// Returns one LoopExitBinding per carrier, in sorted order.
    /// Updates variable_map with new post-loop ValueIds.
    pub fn build_loop_exit_bindings(&mut self) -> Result<Vec<LoopExitBinding>, String>;

    /// Apply bindings to JoinInlineBoundary
    ///
    /// Sets host_outputs and join_outputs based on loop_var + carriers.
    /// Must be called after build_loop_exit_bindings().
    pub fn apply_to_boundary(&self, boundary: &mut JoinInlineBoundary) -> Result<(), String>;

    /// Get the updated loop_var exit binding (always first)
    pub fn loop_var_exit_binding(&self) -> LoopExitBinding;
}

Validation Rules

Single Carrier Case

Input Example:

CarrierInfo {
    loop_var_name: "i",
    loop_var_id: ValueId(5),
    carriers: [{ name: "sum", host_id: ValueId(10) }]
}

ExitMeta {
    exit_values: [("sum", ValueId(15))]
}

variable_map: {"i": ValueId(5), "sum": ValueId(10)}

Output:

LoopExitBinding {
    carrier_name: "sum",
    host_id: ValueId(10),
    join_exit_id: ValueId(15)
}

variable_map (updated): {"i": ValueId(5), "sum": ValueId(???)}  // NEW ValueId for post-loop sum

Multiple Carrier Case

Input Example:

CarrierInfo {
    loop_var_name: "i",
    loop_var_id: ValueId(5),
    carriers: [
        { name: "printed", host_id: ValueId(11) },
        { name: "sum", host_id: ValueId(10) }
    ]
}

ExitMeta {
    exit_values: [
        ("printed", ValueId(14)),
        ("sum", ValueId(15))
    ]
}

variable_map: {"i": ValueId(5), "sum": ValueId(10), "printed": ValueId(11)}

Output:

LoopExitBinding[
    { carrier_name: "printed", host_id: ValueId(11), join_exit_id: ValueId(14) },
    { carrier_name: "sum", host_id: ValueId(10), join_exit_id: ValueId(15) }
]

variable_map (updated):
{
    "i": ValueId(5),
    "sum": ValueId(???),      // NEW post-loop ValueId
    "printed": ValueId(???)   // NEW post-loop ValueId
}

Error Cases

  1. Carrier name mismatch: ExitMeta contains carrier name not in CarrierInfo

    • Error: "Exit carrier 'foo' not found in CarrierInfo"
  2. Missing carrier in ExitMeta: CarrierInfo has carrier not in ExitMeta

    • Error: "Carrier 'sum' missing in ExitMeta"
  3. Loop variable in ExitMeta: ExitMeta erroneously maps loop_var

    • Error: "Loop variable 'i' should not be in exit_values"

Implementation Strategy

File Structure

New file: src/mir/builder/control_flow/joinir/exit_binding.rs

use crate::mir::ValueId;
use crate::mir::join_ir::JoinInlineBoundary;
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, ExitMeta};
use std::collections::HashMap;

pub struct LoopExitBinding { ... }

pub struct ExitBindingBuilder<'a> { ... }

impl<'a> ExitBindingBuilder<'a> {
    pub fn new(...) -> Result<Self, String> { ... }
    pub fn build_loop_exit_bindings(&mut self) -> Result<Vec<LoopExitBinding>, String> { ... }
    pub fn apply_to_boundary(&self, boundary: &mut JoinInlineBoundary) -> Result<(), String> { ... }
}

Module Declaration

Update src/mir/builder/control_flow/joinir/mod.rs:

pub mod exit_binding;

Integration Points

Pattern 3 & 4 Lowerers:

// OLD: Direct boundary manipulation
boundary.host_outputs.push(sum_value_id);
boundary.join_outputs.push(join_sum_exit);
variable_map.insert("sum".to_string(), new_sum_id);

// NEW: Via ExitBindingBuilder
let mut builder = ExitBindingBuilder::new(&carrier_info, &exit_meta, variable_map)?;
let _bindings = builder.build_loop_exit_bindings()?;
builder.apply_to_boundary(&mut boundary)?;

Testing Strategy

Unit Tests (exit_binding.rs)

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_single_carrier_binding() { ... }

    #[test]
    fn test_multi_carrier_binding() { ... }

    #[test]
    fn test_carrier_name_mismatch_error() { ... }

    #[test]
    fn test_variable_map_update() { ... }
}

Integration Tests

File: apps/tests/loop_continue_multi_carrier.hako

static box Main {
    main() {
        local sum = 0
        local printed = 0

        loop(i = 0; i < 5; i = i + 1) {
            if (i > 2) {
                printed = printed + 1
                continue
            }
            sum = sum + i
        }

        // Expected: sum = 0+1+2 = 3, printed = 2 (i=3,4)
        print(sum)
        print(printed)
    }
}

Test command:

NYASH_JOINIR_CORE=1 ./target/release/hakorune apps/tests/loop_continue_multi_carrier.hako
# Expected output:
# 3
# 2

Tracking Variable Updates

variable_map lifecycle

  1. Before loop lowering: {"i": ValueId(5), "sum": ValueId(10), "printed": ValueId(11)}

  2. After JoinModule creation: (unchanged)

  3. ExitBindingBuilder::build_loop_exit_bindings():

    • Allocates new ValueIds for post-loop carrier values
    • Updates variable_map: {"i": ValueId(5), "sum": ValueId(??), "printed": ValueId(??)}
  4. After loop lowering: variable_map reflects post-loop state

Debugging support

Optional environment variable: NYASH_TRACE_EXIT_BINDING=1

Output example:

[exit_binding] Carrier "sum": host_id=ValueId(10) → join_exit=ValueId(15) → post_loop=ValueId(23)
[exit_binding] Carrier "printed": host_id=ValueId(11) → join_exit=ValueId(14) → post_loop=ValueId(24)
[exit_binding] JoinInlineBoundary: host_outputs=[ValueId(5), ValueId(23), ValueId(24)]

  • Phase 188: JoinInlineBoundary initial design
  • Phase 190: CarrierInfo (Phase 193-2 enhancement)
  • Phase 193-3: Pattern classification helpers
  • Phase 193-4: ExitBindingBuilder (THIS PHASE)
  • Phase 193-5: Multi-carrier testing and validation

Success Criteria

  • ExitBindingBuilder compiles and passes unit tests
  • Pattern 3 & 4 lowerers refactored to use ExitBindingBuilder
  • No hardcoded variable names or ValueId assumptions remain in lowering
  • loop_continue_multi_carrier.hako test passes with correct output
  • Variable map correctly reflects post-loop carrier state
  • Debugging environment variable works as expected