Repository: microsoft/qdk (Rust)

Issue:
Callables declared as `SimulatableIntrinsic` not checked for globally unique names
Intrinsic callables must be globally unique so that they are assured to resolve to the same external call when generated as QIR. The `DuplicateIntrinsic` error is used to prevent compilation when two intrinsic callables have the same name. However, this check does not verify the `SimulatableIntrinsic` attribute (introduced after), so will not error when two callables have the same name if one (or both) use the attribute. The `decl_is_intrinsic` utility in compiler/qsc_frontend/src/resolve.rs should be updated to take that attribute into account to close this gap.


Discussion:

Patch:
diff --git a/compiler/qsc_frontend/src/resolve.rs b/compiler/qsc_frontend/src/resolve.rs
index 4612300635..62a8f4c951 100644
--- a/compiler/qsc_frontend/src/resolve.rs
+++ b/compiler/qsc_frontend/src/resolve.rs
@@ -121,7 +121,7 @@ pub(super) enum Error {
 
     #[error("duplicate intrinsic `{0}`")]
     #[diagnostic(help(
-        "each callable declared as `body intrinsic` must have a globally unique name"
+        "each callable declared as `body intrinsic` or `@SimulatableIntrinsic` must have a globally unique name"
     ))]
     #[diagnostic(code("Qsc.Resolve.DuplicateIntrinsic"))]
     DuplicateIntrinsic(String, #[label] Span),
@@ -1733,7 +1733,8 @@ fn bind_callable(
     scope: &mut GlobalScope,
 ) -> Result<(), Vec<Error>> {
     let item_id = next_id();
-    let status = ItemStatus::from_attrs(&ast_attrs_as_hir_attrs(item.attrs.as_ref()));
+    let attrs = ast_attrs_as_hir_attrs(item.attrs.as_ref());
+    let status = ItemStatus::from_attrs(&attrs);
     let res = Res::Item(item_id, status);
     names.insert(decl.name.id, res.clone());
     let mut errors = Vec::new();
@@ -1759,7 +1760,7 @@ fn bind_callable(
         }
     }
 
-    if decl_is_intrinsic(decl) && !scope.intrinsics.insert(Rc::clone(&decl.name.name)) {
+    if decl_is_intrinsic(decl, &attrs) && !scope.intrinsics.insert(Rc::clone(&decl.name.name)) {
         errors.push(Error::DuplicateIntrinsic(
             decl.name.name.to_string(),
             decl.name.span,
@@ -1816,7 +1817,13 @@ fn bind_ty(
     }
 }
 
-fn decl_is_intrinsic(decl: &CallableDecl) -> bool {
+fn decl_is_intrinsic(decl: &CallableDecl, attrs: &[hir::Attr]) -> bool {
+    if attrs
+        .iter()
+        .any(|attr| matches!(attr, hir::Attr::SimulatableIntrinsic))
+    {
+        return true;
+    }
     if let CallableBody::Specs(specs) = decl.body.as_ref() {
         specs
             .iter()


Test patch:
diff --git a/compiler/qsc_frontend/src/resolve/tests.rs b/compiler/qsc_frontend/src/resolve/tests.rs
index ce550558e7..e1771d2636 100644
--- a/compiler/qsc_frontend/src/resolve/tests.rs
+++ b/compiler/qsc_frontend/src/resolve/tests.rs
@@ -2910,6 +2910,36 @@ fn disallow_duplicate_intrinsic_and_non_intrinsic_collision() {
     );
 }
 
+#[test]
+fn disallow_duplicate_intrinsic_and_simulatableintrinsic() {
+    check(
+        indoc! {"
+            namespace A {
+                operation C() : Unit {
+                    body intrinsic;
+                }
+            }
+            namespace B {
+                @SimulatableIntrinsic()
+                operation C() : Unit {}
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {
+                    body intrinsic;
+                }
+            }
+            namespace namespace8 {
+                @SimulatableIntrinsic()
+                operation item3() : Unit {}
+            }
+
+            // DuplicateIntrinsic("C", Span { lo: 129, hi: 130 })
+        "#]],
+    );
+}
+
 #[allow(clippy::cast_possible_truncation)]
 fn check_locals(input: &str, expect: &Expect) {
     let parts = input.split('↘').collect::<Vec<_>>();