Files
hakorune/docs/development/current/main/phase201-join-value-space-design.md
nyash-codex 32a91e31ac feat(joinir): Phase 200-B/C/D capture analysis + Phase 201-A reserved_value_ids infra
Phase 200-B: FunctionScopeCaptureAnalyzer implementation
- analyze_captured_vars_v2() with structural loop matching
- CapturedEnv for immutable function-scope variables
- ParamRole::Condition for condition-only variables

Phase 200-C: ConditionEnvBuilder extension
- build_with_captures() integrates CapturedEnv into ConditionEnv
- fn_body propagation through LoopPatternContext to Pattern 2

Phase 200-D: E2E verification
- capture detection working for base, limit, n etc.
- Test files: phase200d_capture_minimal.hako, phase200d_capture_in_condition.hako

Phase 201-A: MirBuilder reserved_value_ids infrastructure
- reserved_value_ids: HashSet<ValueId> field in MirBuilder
- next_value_id() skips reserved IDs
- merge/mod.rs sets/clears reserved IDs around JoinIR merge

Phase 201: JoinValueSpace design document
- Param/Local/PHI disjoint regions design
- API: alloc_param(), alloc_local(), reserve_phi()
- Migration plan for Pattern 1-4 lowerers

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 18:32:03 +09:00

8.6 KiB

Phase 201: JoinValueSpace Design

1. Problem Statement

1.1 Root Cause (Phase 201-A Analysis)

Pattern 2 frontend と JoinIR lowering の間で ValueId 空間が分離されていないため、衝突が発生している。

Pattern 2 Frontend:                    JoinIR Lowering:
┌────────────────────────┐             ┌────────────────────────┐
│ alloc_join_value()     │             │ alloc_value()          │
│ → env['v'] = ValueId(7)│             │ → const 100 dst=ValueId(7)│
└────────────────────────┘             └────────────────────────┘
                    │                             │
                    └─────────── Collision! ──────┘
                                │
                                ▼
                    remapper → Both → ValueId(12)
                                │
                                ▼
                    PHI corruption: %12 = phi [...], %12 = const 100

1.2 Affected Components

Component Current ValueId Source Issue
ConditionEnv alloc_join_value() Param IDs may collide with local IDs
CarrierInfo.join_id alloc_join_value() Same allocator as ConditionEnv
CapturedEnv alloc_join_value() Same allocator
Pattern lowerers alloc_value() (starts from 0) Collides with param IDs
LoopHeaderPhiBuilder Uses remapped IDs PHI dst may be overwritten

2. Solution: JoinValueSpace

2.1 Design Goals

  1. Single Source of Truth: All JoinIR ValueId allocation goes through one box
  2. Disjoint Regions: Param IDs, Local IDs, and PHI dst never overlap
  3. Contract Enforcement: Debug-mode assertions catch violations
  4. Backward Compatible: Existing APIs continue to work

2.2 ValueId Space Layout

JoinValueSpace Memory Layout:

 0          100        1000                     u32::MAX
 ├──────────┼──────────┼──────────────────────────┤
 │  PHI     │  Param   │       Local             │
 │  Reserved│  Region  │       Region            │
 └──────────┴──────────┴──────────────────────────┘

PHI Reserved (0-99):
  - Pre-reserved for LoopHeader PHI dst
  - reserve_phi(id) marks specific IDs

Param Region (100-999):
  - alloc_param() allocates here
  - Used by: ConditionEnv, CarrierInfo.join_id, CapturedEnv

Local Region (1000+):
  - alloc_local() allocates here
  - Used by: Pattern lowerers (Const, BinOp, etc.)

2.3 API Design

/// Single source of truth for JoinIR ValueId allocation
pub struct JoinValueSpace {
    /// Next available param ID (starts at PARAM_BASE)
    next_param: u32,
    /// Next available local ID (starts at LOCAL_BASE)
    next_local: u32,
    /// Reserved PHI dst IDs (debug verification only)
    reserved_phi: HashSet<u32>,
}

impl JoinValueSpace {
    /// Create a new JoinValueSpace with default regions
    pub fn new() -> Self;

    /// Allocate a parameter ValueId (for ConditionEnv, CarrierInfo, etc.)
    /// Returns ValueId in Param Region (100-999)
    pub fn alloc_param(&mut self) -> ValueId;

    /// Allocate a local ValueId (for Const, BinOp, etc. in lowerers)
    /// Returns ValueId in Local Region (1000+)
    pub fn alloc_local(&mut self) -> ValueId;

    /// Reserve a PHI dst ValueId (called by PHI builder before allocation)
    /// No allocation - just marks the ID as reserved for PHI use
    pub fn reserve_phi(&mut self, id: ValueId);

    /// Check if a ValueId is in a specific region (debug use)
    pub fn region_of(&self, id: ValueId) -> Region;

    /// Verify no overlap between regions (debug assertion)
    #[cfg(debug_assertions)]
    pub fn verify_no_overlap(&self) -> Result<(), String>;
}

pub enum Region {
    PhiReserved,
    Param,
    Local,
    Unknown,
}

2.4 Constants

// Region boundaries (can be tuned based on actual usage)
const PHI_MAX: u32 = 99;      // PHI dst range: 0-99
const PARAM_BASE: u32 = 100;  // Param range: 100-999
const LOCAL_BASE: u32 = 1000; // Local range: 1000+

3. Integration Points

3.1 ConditionEnv / CapturedEnv

// Before (collision-prone):
let mut env = ConditionEnv::new();
let join_id = alloc_join_value(); // Could be 0, 1, 2...
env.insert("i".to_string(), join_id);

// After (JoinValueSpace-based):
let mut space = JoinValueSpace::new();
let mut env = ConditionEnv::new();
let join_id = space.alloc_param(); // Always 100+
env.insert("i".to_string(), join_id);

3.2 CarrierInfo.join_id

// Before:
carrier.join_id = Some(alloc_join_value()); // Could collide

// After:
carrier.join_id = Some(space.alloc_param()); // Safe in Param region

3.3 Pattern Lowerers

// Before (loop_with_break_minimal.rs):
let mut value_counter = 0u32;
let mut alloc_value = || {
    let id = ValueId(value_counter);
    value_counter += 1;
    id
}; // Starts from 0 - collides with env!

// After:
let mut alloc_value = || space.alloc_local(); // Starts from 1000

3.4 LoopHeaderPhiBuilder

// Before merge:
space.reserve_phi(phi_dst); // Mark PHI dst as reserved

// After finalization:
// verify_no_overlap() checks no local overwrote PHI dst

4. Migration Plan

Phase 201-2: JoinValueSpace Box

  1. Create join_value_space.rs in src/mir/join_ir/lowering/
  2. Implement struct and core methods
  3. Add unit tests for region separation
  4. No integration yet - box only

Phase 201-3: Param Region Migration

  1. Modify pattern2_with_break.rs to pass JoinValueSpace
  2. Update ConditionEnvBuilder to use alloc_param()
  3. Update CarrierInfo initialization to use alloc_param()
  4. Verify: Param IDs are now 100+

Phase 201-4: PHI Reservation

  1. Modify LoopHeaderPhiBuilder to call reserve_phi()
  2. Add verification in merge/mod.rs
  3. Verify: PHI dst is protected from overwrite

Phase 201-5: Local Region Migration

  1. Modify all pattern lowerers to use alloc_local()
  2. Files: loop_with_break_minimal.rs, loop_with_continue_minimal.rs, etc.
  3. Verify: Local IDs are now 1000+

Phase 201-6: Testing

  1. Run all existing tests (no regression)
  2. Add phase201_valueid_collision.hako test
  3. Verify phase200d_capture_minimal.hako outputs 30 (not 110)

5. Design Decisions

5.1 Why Fixed Regions?

Alternative: Dynamic start offset based on env.max_value_id()

  • Pro: No wasted ID space
  • Con: Complex, error-prone, requires coordination

Fixed regions are simpler:

  • Clear boundaries (100, 1000)
  • Easy to debug (看ID值就知道是Param还是Local)
  • No coordination needed between allocators

5.2 Why reserve_phi() Instead of alloc_phi()?

PHI dst IDs come from MirBuilder (host side), not JoinValueSpace. JoinValueSpace only needs to know "don't overwrite these IDs". Hence reserve_phi() is a marker, not an allocator.

5.3 Relation to value_id_ranges.rs

value_id_ranges.rs is for module-level isolation (min_loop, skip_ws, etc.) Each module gets a large fixed range (2000 IDs).

JoinValueSpace is for intra-lowering isolation (param vs local vs PHI). It operates within a single lowering call.

They are complementary:

  • Module-level: value_id_ranges.rs
  • Intra-lowering: JoinValueSpace

6. Success Criteria

  1. phase200d_capture_minimal.hako outputs 30 (not 110)
  2. All existing tests pass (no regression)
  3. Debug build asserts on ValueId collision
  4. Architecture doc updated with JoinValueSpace section

7. File Changes Summary

File Change
join_value_space.rs (NEW) JoinValueSpace struct + methods
condition_env.rs No change (env is storage, not allocator)
condition_env_builder.rs Use JoinValueSpace.alloc_param()
carrier_info.rs No change (storage only)
pattern2_with_break.rs Pass JoinValueSpace, use alloc_param()
loop_with_break_minimal.rs Use JoinValueSpace.alloc_local()
loop_with_continue_minimal.rs Use JoinValueSpace.alloc_local()
loop_with_if_phi_minimal.rs Use JoinValueSpace.alloc_local()
loop_header_phi_builder.rs Call reserve_phi()
merge/mod.rs Create JoinValueSpace, pass down

8. References

  • Phase 201-A analysis: carrier PHI dst overwrite bug
  • joinir-architecture-overview.md: JoinIR invariants
  • value_id_ranges.rs: Module-level ValueId isolation