diff --git a/docs/development/current/main/joinir-architecture-overview.md b/docs/development/current/main/joinir-architecture-overview.md index 5029307f..926df96e 100644 --- a/docs/development/current/main/joinir-architecture-overview.md +++ b/docs/development/current/main/joinir-architecture-overview.md @@ -91,6 +91,13 @@ JoinIR ラインで守るべきルールを先に書いておくよ: - `ConditionEnv` 経由で「変数名 → JoinIR ValueId」のみを見る。 - host 側の ValueId は `ConditionBinding { name, host_value, join_value }` として JoinInlineBoundary に記録する。 +- **LoopConditionScopeBox(設計中)** + - 予定ファイル: `src/mir/loop_pattern_detection/loop_condition_scope.rs` 等 + - 責務: + - 条件式に登場する変数が、ループパラメータ(LoopParam)/ループ外ローカル(OuterLocal)/ループ本体ローカル(LoopBodyLocal)のどれかを分類する。 + - Pattern2/4 が「対応してよい条件のスコープ」を判定するための箱。 + - ループ本体ローカルを条件に含む高度なパターンは、将来の Pattern5+ で扱う設計とし、現状は Fail‑Fast で明示的に弾く。 + ### 2.3 キャリア / Exit / Boundary ライン - **CarrierInfo / LoopUpdateAnalyzer** diff --git a/docs/development/current/main/phase170-loop-condition-scope.md b/docs/development/current/main/phase170-loop-condition-scope.md new file mode 100644 index 00000000..3479255f --- /dev/null +++ b/docs/development/current/main/phase170-loop-condition-scope.md @@ -0,0 +1,110 @@ +# Phase 170‑D: LoopConditionScopeBox 設計メモ + +日付: 2025‑12‑07 +状態: 設計完了(実装は今後の Phase で) + +## 背景 + +Pattern2/Pattern4(Loop with Break / Loop with Continue)は、もともと + +- ループパラメータ(例: `i`) +- ループ外のローカル・引数(例: `start`, `end`, `len`) + +のみを条件式に使う前提で設計されていた。 + +しかし JsonParserBox / trim などでは、 + +```hako +local ch = … +if (ch != " ") { break } +``` + +のように「ループ本体ローカル」を条件に使うパターンが現れ、この範囲を超えると + +- バグが出たり(ValueId 伝播ミス) +- たまたま動いたり + +という **曖昧な状態** になっていた。 + +これを箱理論的に整理するために、条件で使われる変数の「スコープ」を明示的に分類する +LoopConditionScopeBox を導入する。 + +## LoopConditionScopeBox の責務 + +責務は 1 つだけ: + +> 「条件式に登場する変数が、どのスコープで定義されたか」を教える + +### 型イメージ + +```rust +pub enum CondVarScope { + LoopParam, // ループパラメータ (i) + OuterLocal, // ループ外のローカル/引数 (start, end, len) + LoopBodyLocal, // ループ本体で定義された変数 (ch) +} + +pub struct CondVarInfo { + pub name: String, + pub scope: CondVarScope, +} + +pub struct LoopConditionScope { + pub vars: Vec, +} +``` + +主なメソッド例: + +```rust +impl LoopConditionScope { + pub fn has_loop_body_local(&self) -> bool { … } + + pub fn all_in(&self, allowed: &[CondVarScope]) -> bool { … } + + pub fn vars_in(&self, scope: CondVarScope) -> impl Iterator { … } +} +``` + +### 入力 / 出力 + +入力: + +- ループヘッダ条件 AST +- break/continue 条件 AST(Pattern2/4) +- LoopScopeShape(どの変数がどのスコープで定義されたか) + +出力: + +- `LoopConditionScope`(CondVarInfo の集合) + +## Pattern2/Pattern4 との関係 + +Pattern2/4 は LoopConditionScopeBox の結果だけを見て「対応可否」を決める: + +```rust +let cond_scope = LoopConditionScopeBox::analyze(&loop_ast, &loop_scope); + +// 対応範囲:LoopParam + OuterLocal のみ +if !cond_scope.all_in(&[CondVarScope::LoopParam, CondVarScope::OuterLocal]) { + return Err(JoinIrError::UnsupportedPattern { … }); +} +``` + +これにより、 + +- いままで暗黙だった「対応範囲」が **設計として明示**される +- LoopBodyLocal を条件に含む trim/JsonParser 系ループは + - 現状は `[joinir/freeze] UnsupportedPattern` にする + - 将来 Pattern5+ で扱いたくなったときに、LoopConditionScopeBox の結果を使って設計できる + +## 将来の拡張 + +LoopBodyLocal を含む条件式を扱いたくなった場合は: + +- LoopConditionScopeBox の結果から `vars_in(LoopBodyLocal)` を取り出し、 + - その変数を carrier に昇格させる + - もしくは LoopHeader に「状態保持用」の追加パラメータを生やす +- それを新しい Pattern5 として設計すれば、既存 Pattern2/4 の仕様を崩さずに拡張できる。 + +このドキュメントは設計メモのみであり、実装は別フェーズ(Phase 170‑D‑impl など)で行う。 diff --git a/src/mir/loop_pattern_detection/loop_condition_scope.rs b/src/mir/loop_pattern_detection/loop_condition_scope.rs new file mode 100644 index 00000000..7514100c --- /dev/null +++ b/src/mir/loop_pattern_detection/loop_condition_scope.rs @@ -0,0 +1,238 @@ +//! Phase 170-D: Loop Condition Scope Analysis Box +//! +//! Analyzes which variables appear in loop conditions (header, break, continue) +//! and classifies them by their scope: +//! - LoopParam: The loop parameter itself (e.g., 'i' in loop(i < 10)) +//! - OuterLocal: Variables from outer scope (pre-existing before loop) +//! - LoopBodyLocal: Variables defined inside the loop body +//! +//! This Box enables Pattern2/4 to determine if they can handle a given loop's +//! condition expressions, providing clear Fail-Fast when conditions reference +//! unsupported loop-body variables. + +use crate::ast::ASTNode; +use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; +use std::collections::HashSet; + +/// Scope classification for a condition variable +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CondVarScope { + /// The loop parameter itself (e.g., 'i' in loop(i < 10)) + LoopParam, + /// A variable from outer scope, defined before the loop + OuterLocal, + /// A variable defined inside the loop body + LoopBodyLocal, +} + +/// Information about a single condition variable +#[derive(Debug, Clone)] +pub struct CondVarInfo { + pub name: String, + pub scope: CondVarScope, +} + +/// Analysis result for all variables in loop conditions +#[derive(Debug, Clone)] +pub struct LoopConditionScope { + pub vars: Vec, +} + +impl LoopConditionScope { + /// Create a new empty condition scope + pub fn new() -> Self { + LoopConditionScope { vars: Vec::new() } + } + + /// Check if this scope contains any loop-body-local variables + pub fn has_loop_body_local(&self) -> bool { + self.vars.iter().any(|v| v.scope == CondVarScope::LoopBodyLocal) + } + + /// Check if all variables in this scope are in the allowed set + pub fn all_in(&self, allowed: &[CondVarScope]) -> bool { + self.vars.iter().all(|v| allowed.contains(&v.scope)) + } + + /// Get all variable names as a set + pub fn var_names(&self) -> HashSet { + self.vars.iter().map(|v| v.name.clone()).collect() + } + + /// Add a variable to this scope (avoiding duplicates by name) + pub fn add_var(&mut self, name: String, scope: CondVarScope) { + // Check if variable already exists + if !self.vars.iter().any(|v| v.name == name) { + self.vars.push(CondVarInfo { name, scope }); + } + } +} + +impl Default for LoopConditionScope { + fn default() -> Self { + Self::new() + } +} + +/// Phase 170-D: Loop Condition Scope Analysis Box +/// +/// This is the main analyzer that determines variable scopes for condition expressions. +pub struct LoopConditionScopeBox; + +impl LoopConditionScopeBox { + /// Analyze condition variable scopes for a loop + /// + /// # Arguments + /// + /// * `loop_param_name` - Name of the loop parameter (e.g., "i" in loop(i < 10)) + /// * `condition_nodes` - Array of AST nodes containing conditions to analyze + /// * `scope` - LoopScopeShape with information about variable definitions + /// + /// # Returns + /// + /// LoopConditionScope with classified variables + pub fn analyze( + loop_param_name: &str, + condition_nodes: &[&ASTNode], + scope: Option<&LoopScopeShape>, + ) -> LoopConditionScope { + let mut result = LoopConditionScope::new(); + let mut found_vars = HashSet::new(); + + // Extract variable names from all condition nodes + for node in condition_nodes { + Self::extract_vars(node, &mut found_vars); + } + + // Classify each variable + for var_name in found_vars { + let var_scope = if var_name == loop_param_name { + CondVarScope::LoopParam + } else if Self::is_outer_local(&var_name, scope) { + CondVarScope::OuterLocal + } else { + // Default: assume it's loop-body-local if not identified as outer + CondVarScope::LoopBodyLocal + }; + + result.add_var(var_name, var_scope); + } + + result + } + + /// Recursively extract variable names from an AST node + fn extract_vars(node: &ASTNode, vars: &mut HashSet) { + match node { + ASTNode::Variable { name, .. } => { + vars.insert(name.clone()); + } + ASTNode::UnaryOp { operand, .. } => { + Self::extract_vars(operand, vars); + } + ASTNode::BinaryOp { + left, right, .. + } => { + Self::extract_vars(left, vars); + Self::extract_vars(right, vars); + } + ASTNode::MethodCall { + object, arguments, .. + } => { + Self::extract_vars(object, vars); + for arg in arguments { + Self::extract_vars(arg, vars); + } + } + ASTNode::FieldAccess { object, .. } => { + Self::extract_vars(object, vars); + } + ASTNode::Index { target, index, .. } => { + Self::extract_vars(target, vars); + Self::extract_vars(index, vars); + } + ASTNode::If { + condition, + then_body, + else_body, + .. + } => { + Self::extract_vars(condition, vars); + for stmt in then_body { + Self::extract_vars(stmt, vars); + } + if let Some(else_body) = else_body { + for stmt in else_body { + Self::extract_vars(stmt, vars); + } + } + } + _ => {} // Skip other node types for now + } + } + + /// Check if a variable is from outer scope + /// + /// Phase 170-D: Simplified heuristic - just check if LoopScopeShape exists + /// and has external variable definitions. Full implementation would track + /// variable_definitions in the LoopScopeShape. + fn is_outer_local(_var_name: &str, _scope: Option<&LoopScopeShape>) -> bool { + // Phase 170-D: Simplified implementation + // Real implementation would check variable_definitions from LoopScopeShape + // For now, we rely on the default fallback to LoopBodyLocal + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_condition_scope_new() { + let scope = LoopConditionScope::new(); + assert!(scope.vars.is_empty()); + assert!(!scope.has_loop_body_local()); + } + + #[test] + fn test_add_var() { + let mut scope = LoopConditionScope::new(); + scope.add_var("i".to_string(), CondVarScope::LoopParam); + scope.add_var("end".to_string(), CondVarScope::OuterLocal); + + assert_eq!(scope.vars.len(), 2); + assert!(!scope.has_loop_body_local()); + } + + #[test] + fn test_has_loop_body_local() { + let mut scope = LoopConditionScope::new(); + scope.add_var("i".to_string(), CondVarScope::LoopParam); + scope.add_var("ch".to_string(), CondVarScope::LoopBodyLocal); + + assert!(scope.has_loop_body_local()); + } + + #[test] + fn test_all_in() { + let mut scope = LoopConditionScope::new(); + scope.add_var("i".to_string(), CondVarScope::LoopParam); + scope.add_var("end".to_string(), CondVarScope::OuterLocal); + + assert!(scope.all_in(&[CondVarScope::LoopParam, CondVarScope::OuterLocal])); + assert!(!scope.all_in(&[CondVarScope::LoopParam])); + } + + #[test] + fn test_var_names() { + let mut scope = LoopConditionScope::new(); + scope.add_var("i".to_string(), CondVarScope::LoopParam); + scope.add_var("end".to_string(), CondVarScope::OuterLocal); + + let names = scope.var_names(); + assert!(names.contains("i")); + assert!(names.contains("end")); + assert_eq!(names.len(), 2); + } +} diff --git a/src/mir/loop_pattern_detection.rs b/src/mir/loop_pattern_detection/mod.rs similarity index 99% rename from src/mir/loop_pattern_detection.rs rename to src/mir/loop_pattern_detection/mod.rs index 6873ba47..22948532 100644 --- a/src/mir/loop_pattern_detection.rs +++ b/src/mir/loop_pattern_detection/mod.rs @@ -751,3 +751,6 @@ mod tests { // Step 3: Assert returns false } } + +// Phase 170-D: Loop Condition Scope Analysis Box +pub mod loop_condition_scope;