Repository: microsoft/qdk (Rust)

Issue:
RCA panics when assigning call result to tuple
RCA fails with this specific code snippet:

```qsharp
operation Foo() : (Int, Int) {
    return (1,2);
}

mutable a = 1;
mutable b = 2;

set (a, b) = Foo();
```

**To Reproduce**

Try running this unit test alongside the RCA unit tests:

```rs
#[test]
fn assign_call_result_to_tuple() {
    let mut compilation_context = CompilationContext::default();
    compilation_context.update(
        r#"
        operation Foo() : (Int, Int) {
            return (1,2);
        }

        mutable a = 1;
        mutable b = 2;

        set (a, b) = Foo();

        "#,
    );
    let package_store_compute_properties = compilation_context.get_compute_properties();
    check_last_statement_compute_properties(
        package_store_compute_properties,
        &expect![
            r#"
            ApplicationsGeneratorSet:
                inherent: Quantum: QuantumProperties:
                    runtime_features: RuntimeFeatureFlags(0x0)
                    value_kind: Element(Static)
                dynamic_param_applications: <empty>"#
        ],
    );
}

```

Actual:

```
thread 'assign_call_result_to_tuple' panicked at compiler/qsc_rca/src/core.rs:1394:21:
expected a tuple
stack backtrace:
   0: rust_begin_unwind
             at /rustc/7cf61ebde7b22796c69757901dd346d0fe70bd97/library/std/src/panicking.rs:647:5
   1: core::panicking::panic_fmt
             at /rustc/7cf61ebde7b22796c69757901dd346d0fe70bd97/library/core/src/panicking.rs:72:14
   2: qsc_rca::core::Analyzer::update_locals_compute_kind
             at ./src/core.rs:1394:21
   3: qsc_rca::core::Analyzer::analyze_expr_assign
             at ./src/core.rs:148:36
   4: <qsc_rca::core::Analyzer as qsc_fir::visit::Visitor>::visit_expr
             at ./src/core.rs:1516:17
   5: <qsc_rca::core::Analyzer as qsc_fir::visit::Visitor>::visit_stmt
             at ./src/core.rs:1671:17
   6: qsc_rca::core::Analyzer::analyze_package_internal
             at ./src/core.rs:998:13
   7: qsc_rca::core::Analyzer::analyze_package
             at ./src/core.rs:54:9
   8: qsc_rca::analyzer::Analyzer::analyze_package
             at ./src/analyzer.rs:61:9
   9: qubits::test_utils::CompilationContext::update
             at ./tests/test_utils.rs:65:35
  10: qubits::assign_call_result_to_tuple
             at ./tests/qubits.rs:16:5
  11: qubits::assign_call_result_to_tuple::{{closure}}
             at ./tests/qubits.rs:14:33
  12: core::ops::function::FnOnce::call_once
             at /rustc/7cf61ebde7b22796c69757901dd346d0fe70bd97/library/core/src/ops/function.rs:250:5
  13: core::ops::function::FnOnce::call_once
             at /rustc/7cf61ebde7b22796c69757901dd346d0fe70bd97/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
test assign_call_result_to_tuple ... FAILED

failures:

failures:
    assign_call_result_to_tuple

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.86s

```


Discussion:

Patch:
diff --git a/compiler/qsc_rca/src/core.rs b/compiler/qsc_rca/src/core.rs
index 76dde230f9..c3b2d63e90 100644
--- a/compiler/qsc_rca/src/core.rs
+++ b/compiler/qsc_rca/src/core.rs
@@ -1390,27 +1390,39 @@ impl<'a> Analyzer<'a> {
                 updated_compute_kind
             }
             ExprKind::Tuple(assignee_exprs) => {
-                let ExprKind::Tuple(value_exprs) = &value_expr.kind else {
-                    panic!("expected a tuple");
-                };
-                assert!(assignee_exprs.len() == value_exprs.len());
-
-                // To determine the update compute kind, we aggregate the runtime features of each element.
-                let default_value_kind = ValueKind::new_static_from_type(&value_expr.ty);
-                let mut updated_compute_kind = ComputeKind::Classical;
-                for (element_assignee_expr_id, element_value_expr_id) in
-                    assignee_exprs.iter().zip(value_exprs.iter())
-                {
-                    let element_update_compute_kind = self.update_locals_compute_kind(
-                        *element_assignee_expr_id,
-                        *element_value_expr_id,
-                    );
-                    updated_compute_kind = updated_compute_kind.aggregate_runtime_features(
-                        element_update_compute_kind,
-                        default_value_kind,
-                    );
+                if let ExprKind::Tuple(value_exprs) = &value_expr.kind {
+                    assert!(assignee_exprs.len() == value_exprs.len());
+
+                    // To determine the update compute kind, we aggregate the runtime features of each element.
+                    let default_value_kind = ValueKind::new_static_from_type(&value_expr.ty);
+                    let mut updated_compute_kind = ComputeKind::Classical;
+                    for (element_assignee_expr_id, element_value_expr_id) in
+                        assignee_exprs.iter().zip(value_exprs.iter())
+                    {
+                        let element_update_compute_kind = self.update_locals_compute_kind(
+                            *element_assignee_expr_id,
+                            *element_value_expr_id,
+                        );
+                        updated_compute_kind = updated_compute_kind.aggregate_runtime_features(
+                            element_update_compute_kind,
+                            default_value_kind,
+                        );
+                    }
+                    updated_compute_kind
+                } else {
+                    // To determine the update compute kind, we aggregate the runtime features of each update.
+                    let default_value_kind = ValueKind::new_static_from_type(&value_expr.ty);
+                    let mut updated_compute_kind = ComputeKind::Classical;
+                    for element_assignee_expr_id in assignee_exprs {
+                        let element_update_compute_kind = self
+                            .update_locals_compute_kind(*element_assignee_expr_id, value_expr_id);
+                        updated_compute_kind = updated_compute_kind.aggregate_runtime_features(
+                            element_update_compute_kind,
+                            default_value_kind,
+                        );
+                    }
+                    updated_compute_kind
                 }
-                updated_compute_kind
             }
             _ => panic!("expected a local variable or a tuple"),
         }


Test patch:
diff --git a/compiler/qsc_rca/tests/assigns.rs b/compiler/qsc_rca/tests/assigns.rs
index 76d30555a6..98f4bdf34d 100644
--- a/compiler/qsc_rca/tests/assigns.rs
+++ b/compiler/qsc_rca/tests/assigns.rs
@@ -132,3 +132,147 @@ fn check_rca_for_dynamic_double_assign_to_local() {
         ],
     );
 }
+
+#[test]
+fn chec_rca_for_assign_call_result_to_tuple_of_vars() {
+    let mut compilation_context = CompilationContext::default();
+    compilation_context.update(
+        r#"
+        function Foo() : (Int, Int) {
+            return (1,2);
+        }
+        mutable a = 1;
+        mutable b = 2;
+        set (a, b) = Foo();
+        "#,
+    );
+    let package_store_compute_properties = compilation_context.get_compute_properties();
+    check_last_statement_compute_properties(
+        package_store_compute_properties,
+        &expect![[r#"
+            ApplicationsGeneratorSet:
+                inherent: Classical
+                dynamic_param_applications: <empty>"#]],
+    );
+}
+
+#[test]
+fn chec_rca_for_assign_var_binded_to_call_result_to_tuple_of_vars() {
+    let mut compilation_context = CompilationContext::default();
+    compilation_context.update(
+        r#"
+        function Foo() : (Int, Int) {
+            return (1,2);
+        }
+        let x = Foo();
+        mutable a = 1;
+        mutable b = 2;
+        set (a, b) = x;
+        "#,
+    );
+    let package_store_compute_properties = compilation_context.get_compute_properties();
+    check_last_statement_compute_properties(
+        package_store_compute_properties,
+        &expect![[r#"
+            ApplicationsGeneratorSet:
+                inherent: Classical
+                dynamic_param_applications: <empty>"#]],
+    );
+}
+
+#[test]
+fn chec_rca_for_assign_tuple_var_to_tuple_of_vars() {
+    let mut compilation_context = CompilationContext::default();
+    compilation_context.update(
+        r#"
+        let x = (1, (2, 3));
+        mutable a = 4;
+        mutable b = (5, 6);
+        set (a, b) = x;
+        "#,
+    );
+    let package_store_compute_properties = compilation_context.get_compute_properties();
+    check_last_statement_compute_properties(
+        package_store_compute_properties,
+        &expect![[r#"
+            ApplicationsGeneratorSet:
+                inherent: Classical
+                dynamic_param_applications: <empty>"#]],
+    );
+}
+
+#[test]
+fn check_rca_for_assign_classical_call_result_to_tuple_of_vars() {
+    let mut compilation_context = CompilationContext::default();
+    compilation_context.update(
+        r#"
+        function Foo(a : Int, b : Int) : (Int, Int) {
+            return (b, a);
+        }
+        mutable a = 1;
+        mutable b = 2;
+        set (a, b) = Foo(a, b);
+        a
+        "#,
+    );
+    check_last_statement_compute_properties(
+        compilation_context.get_compute_properties(),
+        &expect![[r#"
+            ApplicationsGeneratorSet:
+                inherent: Classical
+                dynamic_param_applications: <empty>"#]],
+    );
+    compilation_context.update(
+        r#"
+        b
+        "#,
+    );
+    check_last_statement_compute_properties(
+        compilation_context.get_compute_properties(),
+        &expect![[r#"
+            ApplicationsGeneratorSet:
+                inherent: Classical
+                dynamic_param_applications: <empty>"#]],
+    );
+}
+
+#[test]
+fn check_rca_for_assign_dynamic_call_result_to_tuple_of_vars() {
+    let mut compilation_context = CompilationContext::default();
+    compilation_context.update(
+        r#"
+        function Foo(a : Int, b : Int) : (Int, Int) {
+            return (b, a);
+        }
+        use q = Qubit();
+        let r = MResetZ(q);
+        mutable a = r == Zero ? 0 | 1;
+        mutable b = 2;
+        set (a, b) = Foo(a, b);
+        a
+        "#,
+    );
+    check_last_statement_compute_properties(
+        compilation_context.get_compute_properties(),
+        &expect![[r#"
+            ApplicationsGeneratorSet:
+                inherent: Quantum: QuantumProperties:
+                    runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | UseOfDynamicInt)
+                    value_kind: Element(Dynamic)
+                dynamic_param_applications: <empty>"#]],
+    );
+    compilation_context.update(
+        r#"
+        b
+        "#,
+    );
+    check_last_statement_compute_properties(
+        compilation_context.get_compute_properties(),
+        &expect![[r#"
+            ApplicationsGeneratorSet:
+                inherent: Quantum: QuantumProperties:
+                    runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | UseOfDynamicInt)
+                    value_kind: Element(Dynamic)
+                dynamic_param_applications: <empty>"#]],
+    );
+}