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

@ -125,16 +125,16 @@ impl ConditionEnvBuilder {
env
}
/// Build ConditionEnv with optional captured variables (Phase 200-A v2 entry point)
/// Build ConditionEnv with optional captured variables (Phase 200-B implementation)
///
/// # Phase 200-A Status
/// # Phase 200-B Implementation
///
/// Currently ignores `captured` parameter and delegates to existing implementation.
/// Integration with CapturedEnv will be implemented in Phase 200-B.
/// Adds captured function-scoped variables to ConditionEnv and generates
/// condition_bindings for the boundary builder.
///
/// # Future Behavior (Phase 200-B+)
/// # Behavior
///
/// - Add captured variables to ConditionEnv
/// - Add captured variables to ConditionEnv.captured field
/// - Generate condition_bindings for captured vars in boundary
/// - Track captured vars separately from loop params
/// - Ensure captured vars do NOT participate in header PHI or exit_bindings
@ -142,45 +142,91 @@ impl ConditionEnvBuilder {
/// # Arguments
///
/// * `loop_var_name` - Loop parameter name (e.g., "i", "pos")
/// * `_captured` - Function-scoped captured variables (Phase 200-B+)
/// * `_boundary` - Boundary builder for adding condition_bindings (Phase 200-B+)
/// * `captured` - Function-scoped captured variables with host ValueIds
/// * `boundary` - Boundary builder for adding condition_bindings
/// * `variable_map` - Host function's variable_map to resolve host ValueIds
///
/// # Returns
///
/// ConditionEnv with loop parameter mapping (Phase 200-A: same as build_loop_param_only)
/// ConditionEnv with loop parameter and captured variables
///
/// # Example (Future Phase 200-B)
/// # Example
///
/// ```ignore
/// let captured = analyze_captured_vars(fn_body, loop_ast, scope);
/// let mut boundary = JoinInlineBoundaryBuilder::new();
/// let env = ConditionEnvBuilder::build_with_captures(
/// "pos",
/// &captured, // Contains "digits" with host ValueId(42)
/// &captured, // Contains "digits"
/// &mut boundary,
/// &variable_map, // To resolve "digits" → ValueId(42)
/// );
/// // Phase 200-B: env will contain "pos" → ValueId(0), "digits" → ValueId(1)
/// // Phase 200-B: boundary.condition_bindings will have entry for "digits"
/// // env.params: "pos" → ValueId(0)
/// // env.captured: "digits" → ValueId(1)
/// // boundary.condition_bindings: [ConditionBinding { name: "digits", host_value: ValueId(42), join_value: ValueId(1) }]
/// ```
pub fn build_with_captures(
loop_var_name: &str,
_captured: &CapturedEnv,
_boundary: &mut JoinInlineBoundaryBuilder,
captured: &CapturedEnv,
boundary: &mut JoinInlineBoundaryBuilder,
variable_map: &BTreeMap<String, ValueId>,
) -> ConditionEnv {
// Phase 200-A: Delegate to existing implementation
// TODO(Phase 200-B): Integrate captured vars into ConditionEnv
//
// Integration steps:
// 1. Start with loop parameter in env (ValueId(0))
// 2. For each captured var:
// a. Allocate JoinIR-local ValueId (starting from 1)
// b. Add to ConditionEnv (var.name → join_id)
// c. Add to boundary.condition_bindings (host_id ↔ join_id)
// d. Mark as ParamRole::Condition (not Carrier or LoopParam)
// 3. Ensure captured vars are NOT in exit_bindings (condition-only)
// 4. Return populated ConditionEnv
use std::env;
Self::build_loop_param_only(loop_var_name)
let debug = env::var("NYASH_CAPTURE_DEBUG").is_ok();
if debug {
eprintln!("[capture/env_builder] Building ConditionEnv with {} captured vars", captured.vars.len());
}
// Step 1: Build base ConditionEnv with loop params (existing logic)
let mut env = Self::build_loop_param_only(loop_var_name);
// Step 2: Add captured vars as ParamRole::Condition
for var in &captured.vars {
// 2a: Resolve host_id from variable_map
let host_id = match variable_map.get(&var.name) {
Some(&id) => id,
None => {
if debug {
eprintln!("[capture/env_builder] WARNING: Captured var '{}' not found in variable_map, skipping", var.name);
}
continue;
}
};
// 2b: Add to boundary with Condition role
boundary.add_param_with_role(&var.name, host_id, crate::mir::join_ir::lowering::inline_boundary_builder::ParamRole::Condition);
// 2c: Get JoinIR ValueId from boundary
let join_id = boundary.get_condition_binding(&var.name)
.expect("captured var should be in boundary after add_param_with_role");
// 2d: Add to ConditionEnv.captured map
env.captured.insert(var.name.clone(), join_id);
if debug {
eprintln!("[capture/env_builder] Added captured var '{}': host={:?}, join={:?}", var.name, host_id, join_id);
}
}
// Step 3: Debug guard - Condition params must NOT be in PHI candidates
#[cfg(debug_assertions)]
for var in &captured.vars {
if env.contains(&var.name) && !env.is_captured(&var.name) {
panic!(
"Captured var '{}' must not be in loop params (ParamRole conflict)",
var.name
);
}
}
if debug {
let param_count = env.iter().count();
eprintln!("[capture/env_builder] Final ConditionEnv: {} params, {} captured", param_count, env.captured.len());
}
env
}
}

View File

@ -72,17 +72,19 @@ pub fn can_lower(builder: &MirBuilder, ctx: &super::router::LoopPatternContext)
/// Phase 194: Lowering function for Pattern 2
///
/// Wrapper around cf_loop_pattern2_with_break to match router signature
/// Phase 200-C: Pass fn_body to cf_loop_pattern2_with_break
pub fn lower(
builder: &mut MirBuilder,
ctx: &super::router::LoopPatternContext,
) -> Result<Option<ValueId>, String> {
builder.cf_loop_pattern2_with_break(ctx.condition, ctx.body, ctx.func_name, ctx.debug)
builder.cf_loop_pattern2_with_break_impl(ctx.condition, ctx.body, ctx.func_name, ctx.debug, ctx.fn_body)
}
impl MirBuilder {
/// Phase 179-B: Pattern 2 (Loop with Conditional Break) minimal lowerer
///
/// **Refactored**: Now uses PatternPipelineContext for unified preprocessing
/// **Phase 200-C**: Added fn_body parameter for capture analysis
///
/// # Pipeline (Phase 179-B)
/// 1. Build preprocessing context → PatternPipelineContext
@ -99,6 +101,19 @@ impl MirBuilder {
_body: &[ASTNode],
_func_name: &str,
debug: bool,
) -> Result<Option<ValueId>, String> {
// Phase 200-C: Delegate to impl function with fn_body=None for backward compatibility
self.cf_loop_pattern2_with_break_impl(condition, _body, _func_name, debug, None)
}
/// Phase 200-C: Pattern 2 implementation with optional fn_body for capture analysis
fn cf_loop_pattern2_with_break_impl(
&mut self,
condition: &ASTNode,
_body: &[ASTNode],
_func_name: &str,
debug: bool,
fn_body: Option<&[ASTNode]>,
) -> Result<Option<ValueId>, String> {
use crate::mir::join_ir::lowering::loop_with_break_minimal::lower_loop_with_break_minimal;
@ -129,9 +144,34 @@ impl MirBuilder {
// Phase 195: Use unified trace
trace::trace().varmap("pattern2_start", &self.variable_map);
// Phase 171-172: Use ConditionEnvBuilder for unified construction (Issue 5)
// Phase 200-C: Integrate capture analysis
use crate::mir::loop_pattern_detection::function_scope_capture::{analyze_captured_vars_v2, CapturedEnv};
use super::condition_env_builder::ConditionEnvBuilder;
use crate::mir::join_ir::lowering::condition_env::ConditionBinding;
eprintln!("[pattern2/phase200c] fn_body is {}", if fn_body.is_some() { "SOME" } else { "NONE" });
let captured_env = if let Some(fn_body_ref) = fn_body {
eprintln!("[pattern2/phase200c] fn_body has {} nodes", fn_body_ref.len());
// Phase 200-C: Use v2 API with structural matching
// Pass condition and body directly instead of constructing loop AST
analyze_captured_vars_v2(fn_body_ref, condition, _body, &scope)
} else {
eprintln!("[pattern2/phase200c] fn_body is None, using empty CapturedEnv");
// fn_body not available - use empty CapturedEnv
CapturedEnv::new()
};
eprintln!("[pattern2/capture] Phase 200-C: Captured {} variables",
captured_env.vars.len());
for var in &captured_env.vars {
eprintln!("[pattern2/capture] '{}': host_id={:?}, immutable={}",
var.name, var.host_id, var.is_immutable);
}
// Phase 200-C: Use existing path and manually add captured variables
// TODO Phase 200-D: Refactor to use build_with_captures with boundary builder
let (mut env, mut condition_bindings) = ConditionEnvBuilder::build_for_break_condition(
condition,
&loop_var_name,
@ -139,6 +179,26 @@ impl MirBuilder {
loop_var_id,
)?;
// Phase 200-C: Manually add captured variables to env for E2E testing
// This is a temporary approach until Phase 200-D refactors the boundary creation
for var in &captured_env.vars {
if let Some(&host_id) = self.variable_map.get(&var.name) {
// Allocate a JoinIR ValueId for this captured variable
let join_id = crate::mir::ValueId(env.len() as u32);
env.insert(var.name.clone(), join_id);
// Add to condition_bindings for boundary processing
condition_bindings.push(ConditionBinding {
name: var.name.clone(),
host_value: host_id,
join_value: join_id,
});
eprintln!("[pattern2/capture] Manually added captured '{}' to env: host={:?}, join={:?}",
var.name, host_id, join_id);
}
}
// Phase 190-impl-D: Calculate ValueId offset for body-local variables
// JoinIR main() params are: [ValueId(0), ValueId(1), ...] for (loop_var, carrier1, carrier2, ...)
// Body-local variables must start AFTER all carrier params to avoid collision.

View File

@ -54,6 +54,10 @@ pub struct LoopPatternContext<'a> {
/// Phase 192: Pattern classification based on features
pub pattern_kind: LoopPatternKind,
/// Phase 200-C: Optional function body AST for capture analysis
/// None if not available, Some(&[ASTNode]) if function body is accessible
pub fn_body: Option<&'a [ASTNode]>,
}
impl<'a> LoopPatternContext<'a> {
@ -87,8 +91,22 @@ impl<'a> LoopPatternContext<'a> {
has_break,
features,
pattern_kind,
fn_body: None, // Phase 200-C: Default to None
}
}
/// Phase 200-C: Create context with fn_body for capture analysis
pub fn with_fn_body(
condition: &'a ASTNode,
body: &'a [ASTNode],
func_name: &'a str,
debug: bool,
fn_body: &'a [ASTNode],
) -> Self {
let mut ctx = Self::new(condition, body, func_name, debug);
ctx.fn_body = Some(fn_body);
ctx
}
}
/// Phase 193: Feature extraction moved to ast_feature_extractor module