feat(joinir): Phase 200-A ConditionEnv extension infrastructure

Added type and skeleton infrastructure for function-scoped variable
capture, preparing for Phase 200-B integration with ConditionEnv.

New Types:
- CapturedVar: { name, host_id, is_immutable }
- CapturedEnv: Collection of captured variables
- ParamRole: { LoopParam, Condition, Carrier, ExprResult }

New Functions (Skeletons):
- analyze_captured_vars(): Detects function-scoped "constants"
- build_with_captures(): ConditionEnvBuilder v2 entry point
- add_param_with_role(): Role-based parameter routing

New File:
- src/mir/loop_pattern_detection/function_scope_capture.rs

Design Principles:
- Infra only: Types and skeletons, no behavior changes
- Existing behavior maintained: All current loops work identically
- Box-first: New responsibilities in new file
- Documentation: Future implementation plans in code comments

Test Results:
- 6 new unit tests (function_scope_capture: 3, param_role: 3)
- All 804 existing tests PASS (0 regressions)

Next: Phase 200-B (actual capture detection and integration)

🤖 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 16:19:08 +09:00
parent f47fa9a7a8
commit 3a9b44c4e2
7 changed files with 914 additions and 1 deletions

View File

@ -27,6 +27,8 @@
use crate::ast::ASTNode;
use crate::mir::join_ir::lowering::condition_env::{ConditionBinding, ConditionEnv};
use crate::mir::join_ir::lowering::condition_to_joinir::extract_condition_variables;
use crate::mir::join_ir::lowering::inline_boundary_builder::JoinInlineBoundaryBuilder;
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
use crate::mir::ValueId;
use std::collections::BTreeMap;
@ -122,6 +124,64 @@ impl ConditionEnvBuilder {
env.insert(loop_var_name.to_string(), ValueId(0));
env
}
/// Build ConditionEnv with optional captured variables (Phase 200-A v2 entry point)
///
/// # Phase 200-A Status
///
/// Currently ignores `captured` parameter and delegates to existing implementation.
/// Integration with CapturedEnv will be implemented in Phase 200-B.
///
/// # Future Behavior (Phase 200-B+)
///
/// - Add captured variables to ConditionEnv
/// - 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
///
/// # 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+)
///
/// # Returns
///
/// ConditionEnv with loop parameter mapping (Phase 200-A: same as build_loop_param_only)
///
/// # Example (Future Phase 200-B)
///
/// ```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)
/// &mut boundary,
/// );
/// // Phase 200-B: env will contain "pos" → ValueId(0), "digits" → ValueId(1)
/// // Phase 200-B: boundary.condition_bindings will have entry for "digits"
/// ```
pub fn build_with_captures(
loop_var_name: &str,
_captured: &CapturedEnv,
_boundary: &mut JoinInlineBoundaryBuilder,
) -> 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
Self::build_loop_param_only(loop_var_name)
}
}
#[cfg(test)]

View File

@ -28,6 +28,50 @@ use crate::mir::ValueId;
use super::inline_boundary::{JoinInlineBoundary, LoopExitBinding};
use super::condition_to_joinir::ConditionBinding;
/// Role of a parameter in JoinIR lowering (Phase 200-A)
///
/// This enum explicitly classifies parameters to ensure correct routing
/// during JoinIR → MIR lowering and boundary construction.
///
/// # Invariants
///
/// - **LoopParam**: Participates in header PHI, updated in loop body
/// - Example: `i` in `loop(i < len)` - iteration variable
/// - Routing: join_inputs + host_inputs + header PHI + exit_bindings
///
/// - **Condition**: Used in condition only, NOT in header PHI, NOT in ExitLine
/// - Example: `digits` in `digits.indexOf(ch)` - function-scoped constant
/// - Routing: condition_bindings ONLY (no PHI, no exit_bindings)
/// - Rationale: Condition-only vars are immutable and not updated in loop
///
/// - **Carrier**: Updated in loop body, participates in header PHI and ExitLine
/// - Example: `sum`, `count` in accumulation loops
/// - Routing: join_inputs + host_inputs + header PHI + exit_bindings
///
/// - **ExprResult**: Return value of the loop expression
/// - Example: Loop result in `return loop(...)`
/// - Routing: Handled by exit_phi_builder (set_expr_result)
///
/// # Phase 200-A Status
///
/// Enum is defined but not yet used for routing. Routing implementation
/// will be added in Phase 200-B when CapturedEnv integration is complete.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParamRole {
/// Loop iteration variable (e.g., `i` in `loop(i < len)`)
LoopParam,
/// Condition-only parameter (e.g., `digits` in `digits.indexOf(ch)`)
/// NOT included in header PHI or ExitLine
Condition,
/// State carried across iterations (e.g., `sum`, `count`)
Carrier,
/// Expression result returned by the loop
ExprResult,
}
/// Builder for constructing JoinInlineBoundary objects
///
/// Provides a fluent API to set boundary fields without direct field manipulation.
@ -128,6 +172,70 @@ impl JoinInlineBoundaryBuilder {
pub fn build(self) -> JoinInlineBoundary {
self.boundary
}
/// Add a parameter with explicit role (Phase 200-A)
///
/// This method allows adding parameters with explicit role classification,
/// ensuring correct routing during JoinIR → MIR lowering.
///
/// # Phase 200-A Status
///
/// Currently stores parameters based on role but does not use role for advanced routing.
/// Full role-based routing will be implemented in Phase 200-B.
///
/// # Arguments
///
/// * `name` - Variable name (e.g., "i", "digits", "sum")
/// * `host_id` - Host function's ValueId for this variable
/// * `role` - Parameter role (LoopParam / Condition / Carrier / ExprResult)
///
/// # Routing Rules (Phase 200-B+)
///
/// - **LoopParam**: add_input (join_inputs + host_inputs)
/// - **Condition**: add to condition_bindings (no PHI, no exit_bindings)
/// - **Carrier**: add_input + exit_bindings
/// - **ExprResult**: set_expr_result (handled separately)
///
/// # Example (Future Phase 200-B)
///
/// ```ignore
/// builder.add_param_with_role("i", ValueId(100), ParamRole::LoopParam);
/// 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
//
// Routing implementation:
// - LoopParam: join_inputs + host_inputs
// - Condition: condition_bindings (with JoinIR-local ValueId allocation)
// - Carrier: join_inputs + host_inputs + exit_bindings
// - 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.
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
// 1. Allocate JoinIR-local ValueId
// 2. Create ConditionBinding { name, host_id, join_id }
// 3. Add to self.boundary.condition_bindings
}
ParamRole::ExprResult => {
// Handled separately by set_expr_result
// No action needed here
}
}
}
}
impl Default for JoinInlineBoundaryBuilder {
@ -255,4 +363,40 @@ mod tests {
assert_eq!(boundary.join_inputs.len(), 3);
assert_eq!(boundary.host_inputs.len(), 3);
}
// Phase 200-A: ParamRole tests
#[test]
fn test_param_role_loop_param() {
let mut builder = JoinInlineBoundaryBuilder::new();
builder.add_param_with_role("i", ValueId(100), ParamRole::LoopParam);
let boundary = builder.build();
assert_eq!(boundary.join_inputs.len(), 1);
assert_eq!(boundary.host_inputs.len(), 1);
assert_eq!(boundary.host_inputs[0], ValueId(100));
}
#[test]
fn test_param_role_condition() {
let mut builder = JoinInlineBoundaryBuilder::new();
// Phase 200-A: Condition role is logged but not yet routed
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
assert_eq!(boundary.join_inputs.len(), 0);
assert_eq!(boundary.condition_bindings.len(), 0);
}
#[test]
fn test_param_role_carrier() {
let mut builder = JoinInlineBoundaryBuilder::new();
builder.add_param_with_role("sum", ValueId(101), ParamRole::Carrier);
let boundary = builder.build();
assert_eq!(boundary.join_inputs.len(), 1);
assert_eq!(boundary.host_inputs.len(), 1);
assert_eq!(boundary.host_inputs[0], ValueId(101));
}
}

View File

@ -0,0 +1,203 @@
//! Phase 200-A: Function scope capture infrastructure
//!
//! This module provides types for capturing function-scoped variables
//! that are effectively immutable within a loop context.
//!
//! # Example
//!
//! For a function like JsonParser._atoi():
//!
//! ```nyash
//! method _atoi(s, pos, len) {
//! local digits = "0123456789" // <-- Captured variable
//! local value = 0
//! loop(pos < len) {
//! local ch = s.charAt(pos)
//! local digit = digits.indexOf(ch) // Uses captured 'digits'
//! if (digit < 0) { break }
//! value = value * 10 + digit
//! pos = pos + 1
//! }
//! return value
//! }
//! ```
//!
//! Here, `digits` is:
//! - Declared in function scope (before the loop)
//! - Never reassigned (effectively immutable)
//! - Referenced in loop body (digits.indexOf(ch))
//!
//! Phase 200-A creates the infrastructure to capture such variables.
//! Phase 200-B will implement the actual detection logic.
use crate::mir::ValueId;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::ast::ASTNode;
/// A variable captured from function scope for use in loop conditions/body.
///
/// Example: `local digits = "0123456789"` in JsonParser._atoi()
///
/// # Invariants
///
/// - `name`: Variable name as it appears in the source code
/// - `host_id`: MIR ValueId of the original definition in the host function
/// - `is_immutable`: True if the variable is never reassigned in the function
#[derive(Debug, Clone)]
pub struct CapturedVar {
/// Variable name (e.g., "digits", "table")
pub name: String,
/// MIR ValueId of the original definition in the host function
pub host_id: ValueId,
/// Whether this variable is never reassigned in the function
///
/// Phase 200-B will implement assignment analysis to determine this.
/// For now, this is always set to true as a conservative default.
pub is_immutable: bool,
}
/// Environment containing function-scoped captured variables.
///
/// Phase 200-A: Type definition only, not yet integrated with ConditionEnv.
/// Phase 200-B: Will be populated by FunctionScopeCaptureAnalyzer and
/// integrated into ConditionEnv via ConditionEnvBuilder v2.
#[derive(Debug, Clone, Default)]
pub struct CapturedEnv {
/// List of captured variables
pub vars: Vec<CapturedVar>,
}
impl CapturedEnv {
/// Create a new empty environment
pub fn new() -> Self {
Self { vars: Vec::new() }
}
/// Check if the environment is empty
pub fn is_empty(&self) -> bool {
self.vars.is_empty()
}
/// Add a captured variable to the environment
pub fn add_var(&mut self, var: CapturedVar) {
self.vars.push(var);
}
/// Look up a captured variable by name
///
/// Returns `Some(&CapturedVar)` if found, `None` otherwise.
pub fn get(&self, name: &str) -> Option<&CapturedVar> {
self.vars.iter().find(|v| v.name == name)
}
}
/// Analyzes function-scoped variables that can be safely captured for loop conditions/body.
///
/// # Phase 200-A Status
///
/// Currently returns empty CapturedEnv (skeleton implementation).
/// Actual capture detection will be implemented in Phase 200-B.
///
/// # Future Detection Criteria (Phase 200-B+)
///
/// A variable is captured if ALL of the following conditions are met:
///
/// 1. **Declared before the loop**: Variable must be declared in function scope before the loop
/// 2. **Never reassigned**: Variable is never reassigned within the function (is_immutable = true)
/// 3. **Referenced in loop**: Variable is referenced in loop condition or body
/// 4. **Not a loop parameter**: Variable is not the loop iteration variable
/// 5. **Not a body-local**: Variable is not declared inside the loop body
///
/// # Example
///
/// ```nyash
/// method _atoi(s, pos, len) {
/// local digits = "0123456789" // ✅ Captured (declared before loop, never reassigned)
/// local value = 0 // ❌ Not captured (reassigned in loop body)
/// loop(pos < len) {
/// local ch = s.charAt(pos) // ❌ Not captured (body-local)
/// local digit = digits.indexOf(ch)
/// value = value * 10 + digit
/// pos = pos + 1
/// }
/// }
/// ```
///
/// # Arguments
///
/// * `_fn_body` - AST nodes of the function body (for analysis)
/// * `_loop_ast` - AST node of the loop statement
/// * `_scope` - LoopScopeShape (for excluding loop params and body-locals)
///
/// # Returns
///
/// `CapturedEnv` containing all captured variables (empty in Phase 200-A)
pub fn analyze_captured_vars(
_fn_body: &[ASTNode],
_loop_ast: &ASTNode,
_scope: &LoopScopeShape,
) -> CapturedEnv {
// Phase 200-A: Skeleton implementation
// TODO(Phase 200-B): Implement actual capture detection
//
// Detection algorithm:
// 1. Find all `local` declarations before the loop in fn_body
// 2. For each declaration:
// a. Check if it's never reassigned in the function (is_immutable = true)
// b. Check if it's referenced in loop condition or body
// c. Exclude if it's in scope.pinned, scope.carriers, or scope.body_locals
// 3. Collect matching variables into CapturedEnv
// 4. Return the populated environment
CapturedEnv::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_captured_env_empty() {
let env = CapturedEnv::new();
assert!(env.is_empty());
assert!(env.get("digits").is_none());
}
#[test]
fn test_captured_env_add_and_get() {
let mut env = CapturedEnv::new();
env.add_var(CapturedVar {
name: "digits".to_string(),
host_id: ValueId(42),
is_immutable: true,
});
assert!(!env.is_empty());
let var = env.get("digits").unwrap();
assert_eq!(var.name, "digits");
assert_eq!(var.host_id, ValueId(42));
assert!(var.is_immutable);
}
#[test]
fn test_captured_env_multiple_vars() {
let mut env = CapturedEnv::new();
env.add_var(CapturedVar {
name: "digits".to_string(),
host_id: ValueId(42),
is_immutable: true,
});
env.add_var(CapturedVar {
name: "table".to_string(),
host_id: ValueId(100),
is_immutable: true,
});
assert_eq!(env.vars.len(), 2);
assert!(env.get("digits").is_some());
assert!(env.get("table").is_some());
assert!(env.get("nonexistent").is_none());
}
}

View File

@ -780,3 +780,6 @@ pub use trim_loop_helper::TrimLoopHelper;
// Phase 33-23: Break Condition Analysis (Stage 2, Issue 6)
pub mod break_condition_analyzer;
// Phase 200-A: Function Scope Capture Infrastructure
pub mod function_scope_capture;