204 lines
6.4 KiB
Rust
204 lines
6.4 KiB
Rust
|
|
//! 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());
|
||
|
|
}
|
||
|
|
}
|