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:
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
Reference in New Issue
Block a user