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>
This commit is contained in:
nyash-codex
2025-12-09 18:32:03 +09:00
parent 3a9b44c4e2
commit 32a91e31ac
24 changed files with 2815 additions and 193 deletions

View File

@ -20,6 +20,12 @@ use std::collections::HashMap;
/// Maps variable names to JoinIR-local ValueIds. Used when lowering
/// condition AST nodes to JoinIR instructions.
///
/// # Phase 200-B Extension
///
/// Added `captured` field to track function-scoped captured variables
/// separately from loop parameters. Captured variables have ParamRole::Condition
/// and do NOT participate in header PHI or ExitLine.
///
/// # Example
///
/// ```ignore
@ -27,6 +33,9 @@ use std::collections::HashMap;
/// env.insert("i".to_string(), ValueId(0)); // Loop parameter
/// env.insert("end".to_string(), ValueId(1)); // Condition-only var
///
/// // Phase 200-B: Add captured variable
/// env.captured.insert("digits".to_string(), ValueId(2));
///
/// // Later during lowering:
/// if let Some(value_id) = env.get("i") {
/// // Use value_id in JoinIR instruction
@ -34,7 +43,17 @@ use std::collections::HashMap;
/// ```
#[derive(Debug, Clone, Default)]
pub struct ConditionEnv {
/// Loop parameters and condition-only variables (legacy)
name_to_join: HashMap<String, ValueId>,
/// Phase 200-B: Captured function-scoped variables (ParamRole::Condition)
///
/// These variables are:
/// - Declared in function scope before the loop
/// - Never reassigned (effectively immutable)
/// - Used in loop condition or body
/// - NOT included in header PHI or ExitLine (condition-only)
pub captured: HashMap<String, ValueId>,
}
impl ConditionEnv {
@ -42,6 +61,7 @@ impl ConditionEnv {
pub fn new() -> Self {
Self {
name_to_join: HashMap::new(),
captured: HashMap::new(),
}
}
@ -57,38 +77,82 @@ impl ConditionEnv {
/// Look up a variable by name
///
/// Phase 200-B: Searches both name_to_join (loop params) and captured fields.
///
/// Returns `Some(ValueId)` if the variable exists in the environment,
/// `None` otherwise.
pub fn get(&self, name: &str) -> Option<ValueId> {
self.name_to_join.get(name).copied()
.or_else(|| self.captured.get(name).copied())
}
/// Check if a variable exists in the environment
///
/// Phase 200-B: Checks both name_to_join and captured fields.
pub fn contains(&self, name: &str) -> bool {
self.name_to_join.contains_key(name)
self.name_to_join.contains_key(name) || self.captured.contains_key(name)
}
/// Check if a variable is a captured (Condition role) variable
///
/// Phase 200-B: New method to distinguish captured vars from loop params.
pub fn is_captured(&self, name: &str) -> bool {
self.captured.contains_key(name)
}
/// Get the number of variables in the environment
///
/// Phase 200-B: Counts both name_to_join and captured fields.
pub fn len(&self) -> usize {
self.name_to_join.len()
self.name_to_join.len() + self.captured.len()
}
/// Check if the environment is empty
///
/// Phase 200-B: Checks both name_to_join and captured fields.
pub fn is_empty(&self) -> bool {
self.name_to_join.is_empty()
self.name_to_join.is_empty() && self.captured.is_empty()
}
/// Get an iterator over all (name, ValueId) pairs
///
/// Phase 200-B: Note - this only iterates over name_to_join (loop params).
/// For captured variables, access the `captured` field directly.
pub fn iter(&self) -> impl Iterator<Item = (&String, &ValueId)> {
self.name_to_join.iter()
}
/// Get all variable names (sorted)
///
/// Phase 200-B: Includes both name_to_join and captured variables.
pub fn names(&self) -> Vec<String> {
let mut names: Vec<_> = self.name_to_join.keys().cloned().collect();
let mut names: Vec<_> = self.name_to_join.keys()
.chain(self.captured.keys())
.cloned()
.collect();
names.sort();
names.dedup(); // Remove duplicates (shouldn't happen, but be safe)
names
}
/// Phase 201-A: Get the maximum ValueId used in this environment
///
/// Returns the highest ValueId.0 value from both name_to_join and captured,
/// or None if the environment is empty.
///
/// This is used by JoinIR lowering to determine the starting point for
/// alloc_value() to avoid ValueId collisions.
pub fn max_value_id(&self) -> Option<u32> {
let name_max = self.name_to_join.values().map(|v| v.0).max();
let captured_max = self.captured.values().map(|v| v.0).max();
match (name_max, captured_max) {
(Some(a), Some(b)) => Some(a.max(b)),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
}
/// Binding between HOST and JoinIR ValueIds for condition variables

View File

@ -203,39 +203,63 @@ impl JoinInlineBoundaryBuilder {
/// builder.add_param_with_role("digits", ValueId(42), ParamRole::Condition);
/// builder.add_param_with_role("sum", ValueId(101), ParamRole::Carrier);
/// ```
pub fn add_param_with_role(&mut self, _name: &str, host_id: ValueId, role: ParamRole) {
// Phase 200-A: Basic routing only
// TODO(Phase 200-B): Implement full role-based routing
pub fn add_param_with_role(&mut self, name: &str, host_id: ValueId, role: ParamRole) {
// Phase 200-B: Full role-based routing implementation
//
// Routing implementation:
// - LoopParam: join_inputs + host_inputs
// - Condition: condition_bindings (with JoinIR-local ValueId allocation)
// - Carrier: join_inputs + host_inputs + exit_bindings
// Routing rules:
// - LoopParam: join_inputs + host_inputs (participates in PHI)
// - Condition: condition_bindings ONLY (no PHI, no ExitLine)
// - Carrier: join_inputs + host_inputs (participates in PHI + ExitLine)
// - ExprResult: Handled by set_expr_result
match role {
ParamRole::LoopParam | ParamRole::Carrier => {
// Existing behavior: add to join_inputs
// Note: In Phase 200-A, we don't have a simple add_input method
// that takes a name. This is a skeleton implementation.
// In Phase 200-B, we'll need to allocate JoinIR-local ValueIds.
// Add to join_inputs + host_inputs
let join_id = ValueId(self.boundary.join_inputs.len() as u32);
self.boundary.join_inputs.push(join_id);
self.boundary.host_inputs.push(host_id);
}
ParamRole::Condition => {
// Phase 200-A: Log only
// TODO(Phase 200-B): Add to condition_bindings without PHI
// Phase 200-B: Add to condition_bindings without PHI
// 1. Allocate JoinIR-local ValueId
// 2. Create ConditionBinding { name, host_id, join_id }
// 3. Add to self.boundary.condition_bindings
let join_id = ValueId(
(self.boundary.join_inputs.len() + self.boundary.condition_bindings.len()) as u32
);
// 2. Create ConditionBinding
let binding = ConditionBinding {
name: name.to_string(),
host_value: host_id,
join_value: join_id,
};
// 3. Add to condition_bindings
self.boundary.condition_bindings.push(binding);
}
ParamRole::ExprResult => {
// Handled separately by set_expr_result
// Handled separately by with_expr_result
// No action needed here
}
}
}
/// Get JoinIR ValueId for a condition-only binding (Phase 200-B)
///
/// Returns the JoinIR-local ValueId for a captured variable that was added
/// with ParamRole::Condition.
///
/// # Arguments
///
/// * `name` - Variable name to look up
///
/// # Returns
///
/// `Some(ValueId)` if the variable exists in condition_bindings, `None` otherwise.
pub fn get_condition_binding(&self, name: &str) -> Option<ValueId> {
self.boundary.condition_bindings.iter()
.find(|b| b.name == name)
.map(|b| b.join_value)
}
}
impl Default for JoinInlineBoundaryBuilder {
@ -379,14 +403,15 @@ mod tests {
#[test]
fn test_param_role_condition() {
let mut builder = JoinInlineBoundaryBuilder::new();
// Phase 200-A: Condition role is logged but not yet routed
// Phase 200-B: Condition role is added to condition_bindings
builder.add_param_with_role("digits", ValueId(42), ParamRole::Condition);
let boundary = builder.build();
// Phase 200-A: No action for Condition role yet
// Phase 200-B: This will add to condition_bindings
// Phase 200-B: Condition params go to condition_bindings, not join_inputs
assert_eq!(boundary.join_inputs.len(), 0);
assert_eq!(boundary.condition_bindings.len(), 0);
assert_eq!(boundary.condition_bindings.len(), 1);
assert_eq!(boundary.condition_bindings[0].name, "digits");
assert_eq!(boundary.condition_bindings[0].host_value, ValueId(42));
}
#[test]