Repository: microsoft/qsharp
Pull request #2224 (2025-03-12 16:55:53)

Enums used with Qiskit can now be deep copied


Patch:
diff --git a/compiler/qsc_frontend/src/resolve.rs b/compiler/qsc_frontend/src/resolve.rs
index 77807a8d..bed43ed7 100644
--- a/compiler/qsc_frontend/src/resolve.rs
+++ b/compiler/qsc_frontend/src/resolve.rs
@@ -1626,9 +1626,11 @@ impl GlobalTable {
             global.visibility == hir::Visibility::Public
                 || matches!(&global.kind, global::Kind::Term(t) if t.intrinsic)
         }) {
-            // If the namespace is `Main`, we treat it as the root of the package, so there's no
-            // namespace prefix.
-            let global_namespace = if global.namespace.len() == 1 && &*global.namespace[0] == "Main"
+            // If the namespace is `Main` and we have an alias, we treat it as the root of the package, so there's no
+            // namespace prefix between the dependency alias and the defined items.
+            let global_namespace = if global.namespace.len() == 1
+                && &*global.namespace[0] == "Main"
+                && alias.is_some()
             {
                 vec![]
             } else {
@@ -2309,7 +2311,7 @@ fn find_symbol_in_namespace<O>(
         return;
     }
 
-    // Attempt to get the symbol from the global scope. If the namespace is None, use the candidate_namespace_id as a fallback
+    // Attempt to get the symbol from the global scope.
     let res = namespace.and_then(|ns_id| globals.get(kind, ns_id, &provided_symbol_name.name));
 
     // If a symbol was found, insert it into the candidates map
diff --git a/katas/content/qubit/index.md b/katas/content/qubit/index.md
index d92beeeb..16629bb5 100644
--- a/katas/content/qubit/index.md
+++ b/katas/content/qubit/index.md
@@ -142,7 +142,7 @@ Complex numbers have a parameter called the phase. If a complex number $z = x +
 > between $-\pi$ and $\pi$ that has $\cos \theta = x$ and $\sin \theta = y$. Unlike using $\tan^{-1}(\frac{y}{x})$, `atan2` computes
 > the correct quadrant for the angle, since it preserves information about the signs of both sine and cosine of the angle.
 
-The probability amplitudes $\alpha$ and $\beta$ are complex numbers, therefore $\alpha$ and $\beta$ have a phase. For example, consider a qubit in state $\frac{1 + i}{2}\ket{0} + \frac{1 - i}{2}\ket{1}$. If you do the math, you see that the phase of $\ket{0}$ is $atan2(\frac12, \frac12) = \frac{\pi}{4}$, and the phase of $\ket{1}$ is $atan2(\frac12, -\frac12) = -\frac{\pi}{4}$. The difference between these two phases is known as **relative phase**.
+The probability amplitudes $\alpha$ and $\beta$ are complex numbers, therefore $\alpha$ and $\beta$ have a phase. For example, consider a qubit in state $\frac{1 + i}{2}\ket{0} + \frac{1 - i}{2}\ket{1}$. If you do the math, you see that the phase of $\ket{0}$ amplitude is $atan2(\frac12, \frac12) = \frac{\pi}{4}$, and the phase of $\ket{1}$ amplitude is $atan2(-\frac12, \frac12) = -\frac{\pi}{4}$. The difference between these two phases is known as **relative phase**.
 
 Multiplying the state of the entire system by $e^{i\theta}$ doesn't affect the relative phase: $\alpha\ket{0} + \beta\ket{1}$ has the same relative phase as $e^{i\theta}\big(\alpha\ket{0} + \beta\ket{1}\big)$. In the second expression, $\theta$ is known as the system's **global phase**.
 
diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs
index 0ff71024..38d5b650 100644
--- a/pip/src/interpreter.rs
+++ b/pip/src/interpreter.rs
@@ -89,8 +89,8 @@ fn _native<'a>(py: Python<'a>, m: &Bound<'a, PyModule>) -> PyResult<()> {
 }
 
 // This ordering must match the _native.pyi file.
-#[derive(Clone, Copy, PartialEq)]
-#[pyclass(eq, eq_int)]
+#[derive(Clone, Copy, Default, PartialEq)]
+#[pyclass(eq, eq_int, module = "qsharp._native")]
 #[allow(non_camel_case_types)]
 /// A Q# target profile.
 ///
@@ -100,6 +100,7 @@ pub(crate) enum TargetProfile {
     /// Target supports the minimal set of capabilities required to run a quantum program.
     ///
     /// This option maps to the Base Profile as defined by the QIR specification.
+    #[default]
     Base,
     /// Target supports the Adaptive profile with the integer computation extension.
     ///
@@ -122,6 +123,32 @@ pub(crate) enum TargetProfile {
 
 #[pymethods]
 impl TargetProfile {
+    #[new]
+    // We need to define `new` so that instances of `TargetProfile` can be created by Python
+    pub(crate) fn new() -> Self {
+        Self::default()
+    }
+
+    // called and the returned object is pickled as the contents for the instance
+    #[allow(clippy::trivially_copy_pass_by_ref)]
+    fn __getstate__(&self) -> PyResult<isize> {
+        Ok(self.__pyo3__int__())
+    }
+
+    // called with the unpickled state and the instance is updated in place
+    // This is what requires `new` to be implemented as we can't hydrate an
+    // unininitialized instance in Python.
+    fn __setstate__(&mut self, state: i32) -> PyResult<()> {
+        (*self) = match state {
+            0 => Self::Base,
+            1 => Self::Adaptive_RI,
+            2 => Self::Adaptive_RIF,
+            3 => Self::Unrestricted,
+            _ => return Err(PyValueError::new_err("invalid state")),
+        };
+        Ok(())
+    }
+
     #[allow(clippy::trivially_copy_pass_by_ref)]
     fn __str__(&self) -> String {
         Into::<Profile>::into(*self).to_str().to_owned()
@@ -162,8 +189,8 @@ impl From<TargetProfile> for Profile {
 }
 
 // This ordering must match the _native.pyi file.
-#[derive(Clone, Copy, PartialEq)]
-#[pyclass(eq, eq_int)]
+#[derive(Clone, Copy, Default, PartialEq)]
+#[pyclass(eq, eq_int, module = "qsharp._native")]
 #[allow(non_camel_case_types)]
 /// Represents the output semantics for OpenQASM 3 compilation.
 /// Each has implications on the output of the compilation
@@ -173,6 +200,7 @@ pub(crate) enum OutputSemantics {
     /// is all of the classical registers, in reverse order
     /// in which they were added to the circuit with each
     /// bit within each register in reverse order.
+    #[default]
     Qiskit,
     /// [OpenQASM 3 has two output modes](https://openqasm.com/language/directives.html#input-output)
     /// - If the programmer provides one or more `output` declarations, then
@@ -184,6 +212,34 @@ pub(crate) enum OutputSemantics {
     ResourceEstimation,
 }
 
+#[pymethods]
+impl OutputSemantics {
+    #[new]
+    // We need to define `new` so that instances of `TargetProfile` can be created by Python
+    pub(crate) fn new() -> Self {
+        Self::default()
+    }
+
+    // called and the returned object is pickled as the contents for the instance
+    #[allow(clippy::trivially_copy_pass_by_ref)]
+    fn __getstate__(&self) -> PyResult<isize> {
+        Ok(self.__pyo3__int__())
+    }
+
+    // called with the unpickled state and the instance is updated in place
+    // This is what requires `new` to be implemented as we can't hydrate an
+    // unininitialized instance in Python.
+    fn __setstate__(&mut self, state: i32) -> PyResult<()> {
+        (*self) = match state {
+            0 => Self::Qiskit,
+            1 => Self::OpenQasm,
+            2 => Self::ResourceEstimation,
+            _ => return Err(PyValueError::new_err("invalid state")),
+        };
+        Ok(())
+    }
+}
+
 impl From<OutputSemantics> for qsc::qasm3::OutputSemantics {
     fn from(output_semantics: OutputSemantics) -> Self {
         match output_semantics {
@@ -195,8 +251,8 @@ impl From<OutputSemantics> for qsc::qasm3::OutputSemantics {
 }
 
 // This ordering must match the _native.pyi file.
-#[derive(Clone, PartialEq)]
-#[pyclass(eq)]
+#[derive(Clone, Copy, Default, PartialEq)]
+#[pyclass(eq, eq_int, module = "qsharp._native")]
 #[allow(non_camel_case_types)]
 /// Represents the type of compilation output to create
 pub enum ProgramType {
@@ -204,6 +260,7 @@ pub enum ProgramType {
     /// file. Inputs are lifted to the operation params. Output are lifted to
     /// the operation return type. The operation is marked as `@EntryPoint`
     /// as long as there are no input parameters.
+    #[default]
     File,
     /// Programs are compiled to a standalone function. Inputs are lifted to
     /// the operation params. Output are lifted to the operation return type.
@@ -215,6 +272,34 @@ pub enum ProgramType {
     Fragments,
 }
 
+#[pymethods]
+impl ProgramType {
+    #[new]
+    // We need to define `new` so that instances of `TargetProfile` can be created by Python
+    pub(crate) fn new() -> Self {
+        Self::default()
+    }
+
+    // called and the returned object is pickled as the contents for the instance
+    #[allow(clippy::trivially_copy_pass_by_ref)]
+    fn __getstate__(&self) -> PyResult<isize> {
+        Ok(self.__pyo3__int__())
+    }
+
+    // called with the unpickled state and the instance is updated in place
+    // This is what requires `new` to be implemented as we can't hydrate an
+    // unininitialized instance in Python.
+    fn __setstate__(&mut self, state: i32) -> PyResult<()> {
+        (*self) = match state {
+            0 => Self::File,
+            1 => Self::Operation,
+            2 => Self::Fragments,
+            _ => return Err(PyValueError::new_err("invalid state")),
+        };
+        Ok(())
+    }
+}
+
 impl From<ProgramType> for qsc::qasm3::ProgramType {
     fn from(output_semantics: ProgramType) -> Self {
         match output_semantics {
@@ -598,7 +683,7 @@ impl Interpreter {
             source,
             &operation_name,
             &resolver,
-            program_type.clone(),
+            program_type,
             output_semantics,
             false,
         )?;


Test patch:
diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs
index 0d2b52bc..86fa3771 100644
--- a/compiler/qsc/src/interpret/tests.rs
+++ b/compiler/qsc/src/interpret/tests.rs
@@ -1447,12 +1447,11 @@ mod given_interpreter {
     }
 
     fn is_unit_with_output(result: &InterpretResult, output: &str, expected_output: &str) {
-        assert_eq!(expected_output, output);
-
         match result {
             Ok(value) => assert_eq!(Value::unit(), *value),
             Err(e) => panic!("Expected unit value, got {e:?}"),
         }
+        assert_eq!(expected_output, output);
     }
 
     fn is_only_error<E>(result: &Result<Value, Vec<E>>, output: &str, expected_errors: &Expect)
@@ -2089,5 +2088,73 @@ mod given_interpreter {
                 entry: None,
             }
         }
+
+        #[test]
+        fn name_resolution_from_source_named_main_should_succeed() {
+            let sources = SourceMap::new(
+                [(
+                    "Main".into(),
+                    r#"function Foo() : Unit { Message("hello there..."); }"#.into(),
+                )],
+                None,
+            );
+            let (std_id, store) =
+                crate::compile::package_store_with_stdlib(TargetCapabilityFlags::all());
+            let mut interpreter = Interpreter::new(
+                sources,
+                PackageType::Lib,
+                TargetCapabilityFlags::all(),
+                LanguageFeatures::default(),
+                store,
+                &[(std_id, None)],
+            )
+            .expect("interpreter should be created");
+
+            // Operations defined in Main.qs should also be visible with Main qualifier.
+            let (result, output) = line(&mut interpreter, "Main.Foo()");
+            is_unit_with_output(&result, &output, "hello there...");
+
+            // Operations defined in Main.qs should be importable with fully qualified name.
+            let (result, output) = line(&mut interpreter, "import Main.Foo;");
+            is_only_value(&result, &output, &Value::unit());
+
+            // After import the operation can be invoked without Main qualifier.
+            let (result, output) = line(&mut interpreter, "Foo()");
+            is_unit_with_output(&result, &output, "hello there...");
+        }
+
+        #[test]
+        fn name_resolution_from_source_named_main_without_full_path_or_import_should_fail() {
+            let sources = SourceMap::new(
+                [(
+                    "Main".into(),
+                    r#"function Foo() : Unit { Message("hello there..."); }"#.into(),
+                )],
+                None,
+            );
+            let (std_id, store) =
+                crate::compile::package_store_with_stdlib(TargetCapabilityFlags::all());
+            let mut interpreter = Interpreter::new(
+                sources,
+                PackageType::Lib,
+                TargetCapabilityFlags::all(),
+                LanguageFeatures::default(),
+                store,
+                &[(std_id, None)],
+            )
+            .expect("interpreter should be created");
+
+            // Operations defined in Main.qs should also be visible with Main qualifier.
+            let (errors, _) = line(&mut interpreter, "Foo()");
+            is_error(
+                &errors.expect_err("line invocation should fail with error"),
+                &expect![[r#"
+                    name error: `Foo` not found
+                       [line_0] [Foo]
+                    type error: insufficient type information to infer type
+                       [line_0] [Foo()]
+                "#]],
+            );
+        }
     }
 }
diff --git a/pip/tests/test_enums.py b/pip/tests/test_enums.py
new file mode 100644
index 00000000..839075b3
--- /dev/null
+++ b/pip/tests/test_enums.py
@@ -0,0 +1,106 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+from textwrap import dedent
+import pytest
+import qsharp
+import qsharp.code
+import qsharp.utils
+from contextlib import redirect_stdout
+import io
+
+from qsharp import TargetProfile
+# pull in from native module for tests so that we don't have to install qiskit
+# using the interop module
+from qsharp._native import OutputSemantics, ProgramType
+
+
+def test_target_profile_int_values_match_enum_values() -> None:
+    assert 0 == TargetProfile.Base
+    assert 1 == TargetProfile.Adaptive_RI
+    assert 2 == TargetProfile.Adaptive_RIF
+    assert 3 == TargetProfile.Unrestricted
+
+
+def test_target_profile_serialization() -> None:
+    input = [
+        TargetProfile.Base,
+        TargetProfile.Adaptive_RI,
+        TargetProfile.Adaptive_RIF,
+        TargetProfile.Unrestricted,
+    ]
+    import pickle
+
+    ser = pickle.dumps(input)
+    de = pickle.loads(ser)
+    assert de == input
+
+
+def test_target_profile_str_values_match_enum_values() -> None:
+    target_profile = TargetProfile.Base
+    str_value = str(target_profile)
+    assert str_value == "Base"
+    target_profile = TargetProfile.Adaptive_RI
+    str_value = str(target_profile)
+    assert str_value == "Adaptive_RI"
+    target_profile = TargetProfile.Adaptive_RIF
+    str_value = str(target_profile)
+    assert str_value == "Adaptive_RIF"
+    target_profile = TargetProfile.Unrestricted
+    str_value = str(target_profile)
+    assert str_value == "Unrestricted"
+
+
+def test_target_profile_from_str_match_enum_values() -> None:
+    target_profile = TargetProfile.Base
+    str_value = str(target_profile)
+    assert TargetProfile.from_str(str_value) == target_profile
+    target_profile = TargetProfile.Adaptive_RI
+    str_value = str(target_profile)
+    assert TargetProfile.from_str(str_value) == target_profile
+    target_profile = TargetProfile.Adaptive_RIF
+    str_value = str(target_profile)
+    assert TargetProfile.from_str(str_value) == target_profile
+    target_profile = TargetProfile.Unrestricted
+    str_value = str(target_profile)
+    assert TargetProfile.from_str(str_value) == target_profile
+    with pytest.raises(ValueError):
+        TargetProfile.from_str("Invalid")
+
+
+def test_output_semantics_int_values_match_enum_values() -> None:
+    assert 0 == OutputSemantics.Qiskit
+    assert 1 == OutputSemantics.OpenQasm
+    assert 2 == OutputSemantics.ResourceEstimation
+
+
+def test_output_semantics_serialization() -> None:
+    input = [
+        OutputSemantics.Qiskit,
+        OutputSemantics.OpenQasm,
+        OutputSemantics.ResourceEstimation,
+    ]
+    import pickle
+
+    ser = pickle.dumps(input)
+    de = pickle.loads(ser)
+    assert de == input
+
+
+def test_program_type_int_values_match_enum_values() -> None:
+    assert 0 == ProgramType.File
+    assert 1 == ProgramType.Operation
+    assert 2 == ProgramType.Fragments
+
+
+def test_program_type_serialization() -> None:
+    input = [
+        ProgramType.File,
+        ProgramType.Operation,
+        ProgramType.Fragments,
+    ]
+    import pickle
+
+    ser = pickle.dumps(input)
+    de = pickle.loads(ser)
+    assert de == input
diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py
index 233caadd..7590c944 100644
--- a/pip/tests/test_qsharp.py
+++ b/pip/tests/test_qsharp.py
@@ -498,38 +498,6 @@ def test_run_with_invalid_shots_produces_error() -> None:
         assert False
 
 
-def test_target_profile_str_values_match_enum_values() -> None:
-    target_profile = qsharp.TargetProfile.Base
-    str_value = str(target_profile)
-    assert str_value == "Base"
-    target_profile = qsharp.TargetProfile.Adaptive_RI
-    str_value = str(target_profile)
-    assert str_value == "Adaptive_RI"
-    target_profile = qsharp.TargetProfile.Adaptive_RIF
-    str_value = str(target_profile)
-    assert str_value == "Adaptive_RIF"
-    target_profile = qsharp.TargetProfile.Unrestricted
-    str_value = str(target_profile)
-    assert str_value == "Unrestricted"
-
-
-def test_target_profile_from_str_match_enum_values() -> None:
-    target_profile = qsharp.TargetProfile.Base
-    str_value = str(target_profile)
-    assert qsharp.TargetProfile.from_str(str_value) == target_profile
-    target_profile = qsharp.TargetProfile.Adaptive_RI
-    str_value = str(target_profile)
-    assert qsharp.TargetProfile.from_str(str_value) == target_profile
-    target_profile = qsharp.TargetProfile.Adaptive_RIF
-    str_value = str(target_profile)
-    assert qsharp.TargetProfile.from_str(str_value) == target_profile
-    target_profile = qsharp.TargetProfile.Unrestricted
-    str_value = str(target_profile)
-    assert qsharp.TargetProfile.from_str(str_value) == target_profile
-    with pytest.raises(ValueError):
-        qsharp.TargetProfile.from_str("Invalid")
-
-
 def test_callables_exposed_into_env() -> None:
     qsharp.init()
     qsharp.eval("function Four() : Int { 4 }")