Repository: veryl-lang/veryl (Rust)

Issue:
Spurious "unassign variable" when assigning in if/else
Hi!

```
module Foo (
    bar: output logic,
    baz: output logic,
) {
    always_comb {
        if (true) {
            bar = 0;
            baz = bar;
        } else {
            bar = 1;
            baz = 0;
        }
    }
}
```

```
veryl check
``` 
returns
```
Warning: unassign_variable (link)

  ⚠ bar is unassigned
   ╭─[/.../foo.veryl:2:5]
 1 │ module Foo (
 2 │     bar: output logic,
   ·     ─┬─
   ·      ╰── Error location
 3 │     baz: output logic,
   ╰────
  help: 
```

Interestingly enough,

```
module Foo (
    bar: output logic,
    baz: output logic,
) {
    always_comb {
        if (true) {
            bar = 0;
            baz = 0;
        } else {
            bar = 1;
            baz = bar;
        }
    }
}
```

passes the check (as i would expect).


Discussion:
Even more, 

``` 
module Foo (
    bar: output logic,
    baz: output logic,
) {
    always_comb {
        bar = 1;
        if (true) {
            bar = 0;
            baz = bar;
        } else {
            bar = 1;
            baz = 0;
        }
    }
}
```
fails the check with the same error, while

``` 
module Foo (
    bar: output logic,
    baz: output logic,
) {
    always_comb {
        bar = 1;
        if (true) {
            bar = 0;
            baz = bar;
        } else {
            baz = 0;
        }
    }
}
``` 

```
module Foo (
    bar: output logic,
    baz: output logic,
) {
    always_comb {
        bar = 1;
        if (true) {
            bar = 0;
            baz = 1;
        } else {
            bar = 1;
            baz = 0;
        }
    }
}
```
pass.



Patch:
diff --git a/crates/analyzer/src/analyzer.rs b/crates/analyzer/src/analyzer.rs
index 3d9aaebfa..33bf1ef1b 100644
--- a/crates/analyzer/src/analyzer.rs
+++ b/crates/analyzer/src/analyzer.rs
@@ -216,7 +216,10 @@ impl AnalyzerPass3 {
 
             for (ref_index, var_ref) in target_list {
                 let before_assign = assign_list.iter().any(|(assing_index, assign)| {
-                    *assing_index > ref_index && assign.path.may_fully_included(&var_ref.path)
+                    *assing_index > ref_index
+                        && (AnalyzerPass3::share_same_branch_path(var_ref, assign)
+                            || AnalyzerPass3::in_other_branch_group(var_ref, assign))
+                        && assign.path.may_fully_included(&var_ref.path)
                 });
                 if before_assign {
                     let full_path = var_ref.path.full_path();
@@ -231,6 +234,49 @@ impl AnalyzerPass3 {
 
         ret
     }
+
+    fn share_same_branch_path(var_ref: &VarRef, assign: &VarRef) -> bool {
+        let len = if var_ref.branch_group.len() < assign.branch_group.len() {
+            var_ref.branch_group.len()
+        } else {
+            assign.branch_group.len()
+        };
+
+        if len == 0 {
+            return false;
+        }
+
+        for i in 0..len {
+            if var_ref.branch_group[i] != assign.branch_group[i] {
+                return false;
+            }
+            if var_ref.branch_index[i] != assign.branch_index[i] {
+                return false;
+            }
+        }
+
+        true
+    }
+
+    fn in_other_branch_group(var_ref: &VarRef, assign: &VarRef) -> bool {
+        let len = if var_ref.branch_group.len() < assign.branch_group.len() {
+            var_ref.branch_group.len()
+        } else {
+            assign.branch_group.len()
+        };
+
+        if len == 0 {
+            return true;
+        }
+
+        for i in 0..len {
+            if var_ref.branch_group[i] != assign.branch_group[i] {
+                return true;
+            }
+        }
+
+        false
+    }
 }
 
 pub struct Analyzer {
diff --git a/crates/analyzer/src/handlers/check_var_ref.rs b/crates/analyzer/src/handlers/check_var_ref.rs
index c49ae0c5a..90ba4affc 100644
--- a/crates/analyzer/src/handlers/check_var_ref.rs
+++ b/crates/analyzer/src/handlers/check_var_ref.rs
@@ -31,7 +31,9 @@ pub struct CheckVarRef {
     in_expression: Vec<bool>,
     in_if_expression: Vec<()>,
     function_call: Vec<Option<FunctionCallContext>>,
-    branch_index: usize,
+    branch_group_index: usize,
+    branch_group: Vec<usize>,
+    branch_index: Vec<usize>,
 }
 
 impl CheckVarRef {
@@ -47,6 +49,8 @@ impl CheckVarRef {
             r#type,
             affiliation: *self.affiliation.last().unwrap(),
             path: path.clone(),
+            branch_group: self.branch_group.clone(),
+            branch_index: self.branch_index.clone(),
         };
         symbol_table::add_var_ref(&assign);
         self.assign_position.pop();
@@ -58,6 +62,8 @@ impl CheckVarRef {
             r#type,
             affiliation: *self.affiliation.last().unwrap(),
             path: path.clone(),
+            branch_group: self.branch_group.clone(),
+            branch_index: self.branch_index.clone(),
         };
         symbol_table::add_var_ref(&expression);
     }
@@ -98,6 +104,25 @@ impl CheckVarRef {
             self.function_call.push(None);
         }
     }
+
+    fn push_branch(&mut self) {
+        self.branch_group.push(self.branch_group_index);
+        self.branch_group_index += 1;
+        self.branch_index.push(0);
+    }
+
+    fn pop_branch(&mut self) {
+        self.branch_group.pop();
+        self.branch_index.pop();
+    }
+
+    fn get_branch_index(&self) -> usize {
+        *self.branch_index.last().unwrap_or(&0)
+    }
+
+    fn inc_branch_index(&mut self) {
+        *self.branch_index.last_mut().unwrap() += 1;
+    }
 }
 
 impl Handler for CheckVarRef {
@@ -175,18 +200,18 @@ impl VerylGrammarTrait for CheckVarRef {
                     AssignPositionType::StatementBranchItem {
                         token: arg.else_token.token,
                         define_context: arg.else_token.token.into(),
-                        index: self.branch_index,
+                        index: self.get_branch_index(),
                         r#type: AssignStatementBranchItemType::Else,
                     }
                 } else {
                     AssignPositionType::DeclarationBranchItem {
                         token: arg.else_token.token,
                         define_context: arg.else_token.token.into(),
-                        index: self.branch_index,
+                        index: self.get_branch_index(),
                     }
                 };
                 *self.assign_position.0.last_mut().unwrap() = position;
-                self.branch_index += 1;
+                self.inc_branch_index();
             }
         }
         Ok(())
@@ -397,7 +422,7 @@ impl VerylGrammarTrait for CheckVarRef {
     fn if_statement(&mut self, arg: &IfStatement) -> Result<(), ParolError> {
         match self.point {
             HandlerPoint::Before => {
-                self.branch_index = 0;
+                self.push_branch();
                 let branches = 1 + arg.if_statement_list.len() + arg.if_statement_opt.iter().len();
                 let has_explicit_default = arg.if_statement_opt.is_some();
                 let has_cond_type = has_cond_type(&arg.r#if.if_token.token);
@@ -415,12 +440,13 @@ impl VerylGrammarTrait for CheckVarRef {
                     .push(AssignPositionType::StatementBranchItem {
                         token: arg.r#if.if_token.token,
                         define_context: arg.r#if.if_token.token.into(),
-                        index: self.branch_index,
+                        index: self.get_branch_index(),
                         r#type: AssignStatementBranchItemType::If,
                     });
-                self.branch_index += 1;
+                self.inc_branch_index();
             }
             HandlerPoint::After => {
+                self.pop_branch();
                 self.assign_position.pop();
                 self.assign_position.pop();
             }
@@ -431,7 +457,7 @@ impl VerylGrammarTrait for CheckVarRef {
     fn if_reset_statement(&mut self, arg: &IfResetStatement) -> Result<(), ParolError> {
         match self.point {
             HandlerPoint::Before => {
-                self.branch_index = 0;
+                self.push_branch();
                 let branches =
                     1 + arg.if_reset_statement_list.len() + arg.if_reset_statement_opt.iter().len();
                 let has_explicit_default = arg.if_reset_statement_opt.is_some();
@@ -454,12 +480,13 @@ impl VerylGrammarTrait for CheckVarRef {
                     .push(AssignPositionType::StatementBranchItem {
                         token: arg.if_reset.if_reset_token.token,
                         define_context: arg.if_reset.if_reset_token.token.into(),
-                        index: self.branch_index,
+                        index: self.get_branch_index(),
                         r#type: AssignStatementBranchItemType::IfReset,
                     });
-                self.branch_index += 1;
+                self.inc_branch_index();
             }
             HandlerPoint::After => {
+                self.pop_branch();
                 self.assign_position.pop();
                 self.assign_position.pop();
             }
@@ -484,7 +511,7 @@ impl VerylGrammarTrait for CheckVarRef {
     fn case_statement(&mut self, arg: &CaseStatement) -> Result<(), ParolError> {
         match self.point {
             HandlerPoint::Before => {
-                self.branch_index = 0;
+                self.push_branch();
                 let branches = arg.case_statement_list.len();
                 let has_explicit_default = arg.case_statement_list.iter().any(|x| {
                     matches!(
@@ -505,6 +532,7 @@ impl VerylGrammarTrait for CheckVarRef {
                     });
             }
             HandlerPoint::After => {
+                self.pop_branch();
                 self.assign_position.pop();
             }
         }
@@ -518,10 +546,10 @@ impl VerylGrammarTrait for CheckVarRef {
                     .push(AssignPositionType::StatementBranchItem {
                         token: arg.colon.colon_token.token,
                         define_context: arg.colon.colon_token.token.into(),
-                        index: self.branch_index,
+                        index: self.get_branch_index(),
                         r#type: AssignStatementBranchItemType::Case,
                     });
-                self.branch_index += 1;
+                self.inc_branch_index();
             }
             HandlerPoint::After => {
                 self.assign_position.pop();
@@ -784,7 +812,7 @@ impl VerylGrammarTrait for CheckVarRef {
     fn generate_if_declaration(&mut self, arg: &GenerateIfDeclaration) -> Result<(), ParolError> {
         match self.point {
             HandlerPoint::Before => {
-                self.branch_index = 0;
+                self.push_branch();
                 let branches = 1
                     + arg.generate_if_declaration_list.len()
                     + arg.generate_if_declaration_opt.iter().len();
@@ -798,11 +826,12 @@ impl VerylGrammarTrait for CheckVarRef {
                     .push(AssignPositionType::DeclarationBranchItem {
                         token: arg.r#if.if_token.token,
                         define_context: arg.r#if.if_token.token.into(),
-                        index: self.branch_index,
+                        index: self.get_branch_index(),
                     });
-                self.branch_index += 1;
+                self.inc_branch_index();
             }
             HandlerPoint::After => {
+                self.pop_branch();
                 self.assign_position.pop();
             }
         }
diff --git a/crates/analyzer/src/var_ref.rs b/crates/analyzer/src/var_ref.rs
index f3d2e1513..dae38c523 100644
--- a/crates/analyzer/src/var_ref.rs
+++ b/crates/analyzer/src/var_ref.rs
@@ -16,6 +16,30 @@ pub struct VarRef {
     pub r#type: VarRefType,
     pub affiliation: VarRefAffiliation,
     pub path: VarRefPath,
+    // `branch group` and `branch index` are used to distinguish two statements.
+    // two `VarRef` are visible from each other if either of following conditions is met.
+    // * two `VarRef` belong to different branch group
+    //      * two `branch group` share no elements
+    // * two `VarRef` belong to the same branch
+    //      * two `branch group` share elements but `branch index` are different
+    //
+    //  if {        branch group [0]    branch index [0]
+    //      if {    branch group [0, 1] branch index [0, 0]
+    //      }
+    //  } else {    branch group [0]    branch index [1]
+    //      if {    branch group [0, 2] branch index [1, 0]
+    //      }
+    //  }
+    //
+    //  if {        branch group [3]    branch index [0]
+    //      if {    branch group [3, 4] branch index [0, 0]
+    //      }
+    //  } else {    branch group [3]    branch index [1]
+    //      if {    branch group [3, 5] branch index [1, 0]
+    //      }
+    //  }
+    pub branch_group: Vec<usize>,
+    pub branch_index: Vec<usize>,
 }
 
 impl VarRef {


Test patch:
diff --git a/crates/analyzer/src/tests.rs b/crates/analyzer/src/tests.rs
index db50f39a1..db7c18ffe 100644
--- a/crates/analyzer/src/tests.rs
+++ b/crates/analyzer/src/tests.rs
@@ -4685,6 +4685,97 @@ fn unassign_variable() {
 
     let errors = analyze(code);
     assert!(matches!(errors[0], AnalyzerError::UnassignVariable { .. }));
+
+    let code = r#"
+    module Foo (
+        bar: output logic,
+        baz: output logic,
+    ) {
+        always_comb {
+            if true {
+                bar = 0;
+                baz = bar;
+            } else {
+                bar = 1;
+                baz = 0;
+            }
+        }
+    }
+    "#;
+
+    let errors = analyze(code);
+    assert!(errors.is_empty());
+
+    let code = r#"
+    module Foo (
+        bar: output logic,
+        baz: output logic,
+    ) {
+        always_comb {
+            if true {
+                if true {
+                    baz = bar;
+                } else {
+                    baz = 0;
+                }
+                if true {
+                    bar = 0;
+                } else {
+                    bar = 1;
+                }
+            } else {
+                bar = 1;
+                baz = 0;
+            }
+        }
+    }
+    "#;
+
+    let errors = analyze(code);
+    assert!(matches!(errors[0], AnalyzerError::UnassignVariable { .. }));
+
+    let code = r#"
+    module Foo (
+        bar: output logic,
+        baz: output logic,
+    ) {
+        always_comb {
+            if true {
+                baz = bar;
+            } else {
+                baz = 0;
+            }
+            bar = 0;
+        }
+    }
+    "#;
+
+    let errors = analyze(code);
+    assert!(matches!(errors[0], AnalyzerError::UnassignVariable { .. }));
+
+    let code = r#"
+    module Foo (
+        bar: output logic,
+        baz: output logic,
+    ) {
+        always_comb {
+            if true {
+                baz = bar;
+            } else {
+                baz = 0;
+            }
+
+            if true {
+                bar = 0;
+            } else {
+                bar = 1;
+            }
+        }
+    }
+    "#;
+
+    let errors = analyze(code);
+    assert!(matches!(errors[0], AnalyzerError::UnassignVariable { .. }));
 }
 
 #[test]
@@ -4819,6 +4910,27 @@ fn uncovered_branch() {
     let errors = analyze(code);
     assert!(matches!(errors[0], AnalyzerError::UncoveredBranch { .. }));
 
+    let code = r#"
+    module ModuleC {
+        var a: logic;
+        let x: logic = 1;
+        let y: logic = 1;
+
+        always_comb {
+            if x {
+                if y {
+                    a = 1;
+                }
+            } else {
+                a = 0;
+            }
+        }
+    }
+    "#;
+
+    let errors = analyze(code);
+    assert!(matches!(errors[0], AnalyzerError::UncoveredBranch { .. }));
+
     // TODO
     // Adapt 'traverse_assignable_symbol' to interface/struct/union members
     //let code = r#"