feat(joinir): Phase 170-D-impl-1 LoopConditionScopeBox skeleton creation
Implement the LoopConditionScope analysis infrastructure for Pattern2/4 validation: New module: - src/mir/loop_pattern_detection/loop_condition_scope.rs (220 lines) Types: - CondVarScope enum: LoopParam, OuterLocal, LoopBodyLocal - CondVarInfo: Variable name + scope classification - LoopConditionScope: Collection of analyzed variables with helper methods Box implementation (LoopConditionScopeBox): - analyze(): Main entry point - extracts and classifies condition variables - extract_vars(): Recursive AST traversal to find all variable references - is_outer_local(): Heuristic for outer scope detection (phase 170-D simplified) Helper methods: - has_loop_body_local(): Check for unsupported loop-body variables - all_in(): Validate scope compatibility - var_names(): Get variable set - add_var(): Add with deduplication Tests: 5 unit tests for core functionality Architecture: Refactored loop_pattern_detection.rs → loop_pattern_detection/mod.rs for modular organization. Build: ✅ Passed with no errors 🤖 Generated with Claude Code Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -91,6 +91,13 @@ JoinIR ラインで守るべきルールを先に書いておくよ:
|
|||||||
- `ConditionEnv` 経由で「変数名 → JoinIR ValueId」のみを見る。
|
- `ConditionEnv` 経由で「変数名 → JoinIR ValueId」のみを見る。
|
||||||
- host 側の ValueId は `ConditionBinding { name, host_value, join_value }` として JoinInlineBoundary に記録する。
|
- 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 ライン
|
### 2.3 キャリア / Exit / Boundary ライン
|
||||||
|
|
||||||
- **CarrierInfo / LoopUpdateAnalyzer**
|
- **CarrierInfo / LoopUpdateAnalyzer**
|
||||||
|
|||||||
110
docs/development/current/main/phase170-loop-condition-scope.md
Normal file
110
docs/development/current/main/phase170-loop-condition-scope.md
Normal file
@ -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<CondVarInfo>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
主なメソッド例:
|
||||||
|
|
||||||
|
```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<Item = &CondVarInfo> { … }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 入力 / 出力
|
||||||
|
|
||||||
|
入力:
|
||||||
|
|
||||||
|
- ループヘッダ条件 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 など)で行う。
|
||||||
238
src/mir/loop_pattern_detection/loop_condition_scope.rs
Normal file
238
src/mir/loop_pattern_detection/loop_condition_scope.rs
Normal file
@ -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<CondVarInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String> {
|
||||||
|
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<String>) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -751,3 +751,6 @@ mod tests {
|
|||||||
// Step 3: Assert returns false
|
// Step 3: Assert returns false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 170-D: Loop Condition Scope Analysis Box
|
||||||
|
pub mod loop_condition_scope;
|
||||||
Reference in New Issue
Block a user