From fcf49dbf64ca93a3ca3ecb0736c7cd216a2610bb Mon Sep 17 00:00:00 2001 From: Arnav Nagzirkar <113314200+arnavnagzirkar@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:05:19 -0700 Subject: [PATCH 1/2] fix: Add more tests for control vs. broadcast Fixes NVIDIA/cuda-quantum#913 Signed-off-by: Arnav Nagzirkar <113314200+arnavnagzirkar@users.noreply.github.com> --- cudaq/test/AST-Quake/ctrl_broadcast.cpp | 142 ++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 cudaq/test/AST-Quake/ctrl_broadcast.cpp diff --git a/cudaq/test/AST-Quake/ctrl_broadcast.cpp b/cudaq/test/AST-Quake/ctrl_broadcast.cpp new file mode 100644 index 00000000000..de9a4479eb3 --- /dev/null +++ b/cudaq/test/AST-Quake/ctrl_broadcast.cpp @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// Tests documenting the difference between broadcast and control semantics +// for single-qubit gates and swap, with and without the modifier. + +// RUN: cudaq-quake %s | cudaq-opt | FileCheck %s + +#include + +// Broadcast: x applied to three individually addressed qubits. +// All three are targets; no controls are generated. +struct broadcast_individual { + void operator()() __qpu__ { + cudaq::qvector q(4); + x(q[0], q[1], q[2]); + } +}; + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__broadcast_individual +// CHECK: %[[Q:.*]] = quake.alloca !quake.veq<4> +// CHECK: %[[R0:.*]] = quake.extract_ref %[[Q]][0] : (!quake.veq<4>) -> !quake.ref +// CHECK: %[[R1:.*]] = quake.extract_ref %[[Q]][1] : (!quake.veq<4>) -> !quake.ref +// CHECK: %[[R2:.*]] = quake.extract_ref %[[Q]][2] : (!quake.veq<4>) -> !quake.ref +// CHECK: quake.x %[[R0]] : (!quake.ref) -> () +// CHECK: quake.x %[[R1]] : (!quake.ref) -> () +// CHECK: quake.x %[[R2]] : (!quake.ref) -> () +// CHECK: return + +// Broadcast: x applied to every qubit in a register via an invariant loop. +struct broadcast_register { + void operator()() __qpu__ { + cudaq::qvector q(4); + x(q); + } +}; + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__broadcast_register +// CHECK: quake.alloca !quake.veq<4> +// CHECK: cc.loop while +// CHECK: quake.x %{{.*}} : (!quake.ref) -> () + +// Broadcast: even with , passing a single register still +// broadcasts (the ctrl modifier is ignored for a single-veq operand). +struct ctrl_broadcast_register { + void operator()() __qpu__ { + cudaq::qvector q(4); + x(q); + } +}; + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__ctrl_broadcast_register +// CHECK: quake.alloca !quake.veq<4> +// CHECK: cc.loop while +// CHECK: quake.x %{{.*}} : (!quake.ref) -> () +// CHECK-NOT: quake.x [% + +// Control: with multiple refs — first N-1 are controls, last +// is the target. Here q[0] and q[1] are controls; q[2] is the target. +struct ctrl_individual { + void operator()() __qpu__ { + cudaq::qvector q(4); + x(q[0], q[1], q[2]); + } +}; + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__ctrl_individual +// CHECK: quake.alloca !quake.veq<4> +// CHECK: quake.x [%{{.*}}, %{{.*}}] %{{.*}} : (!quake.ref, !quake.ref, !quake.ref) -> () +// CHECK: return + +// Implicit control: passing a qvector followed by a qubit selects the +// two-argument overload whose default modifier is ctrl, so no explicit +// is required. +struct veq_implicit_ctrl { + void operator()() __qpu__ { + cudaq::qvector ctrls(4); + cudaq::qubit target; + x(ctrls, target); + } +}; + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__veq_implicit_ctrl +// CHECK: %[[CTRLS:.*]] = quake.alloca !quake.veq<4> +// CHECK: %[[TGT:.*]] = quake.alloca !quake.ref +// CHECK: quake.x [%[[CTRLS]]] %[[TGT]] : (!quake.veq<4>, !quake.ref) -> () +// CHECK: return + +// Simple swap: two qubits, no controls. +struct simple_swap { + void operator()() __qpu__ { + cudaq::qvector q(4); + swap(q[0], q[1]); + } +}; + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__simple_swap +// CHECK: %[[Q:.*]] = quake.alloca !quake.veq<4> +// CHECK: %[[R0:.*]] = quake.extract_ref %[[Q]][0] : (!quake.veq<4>) -> !quake.ref +// CHECK: %[[R1:.*]] = quake.extract_ref %[[Q]][1] : (!quake.veq<4>) -> !quake.ref +// CHECK: quake.swap %[[R0]], %[[R1]] : (!quake.ref, !quake.ref) -> () +// CHECK: return + +// Controlled swap: with three individual qubits — the first +// qubit becomes the control, the remaining two are swapped. +struct ctrl_swap_individual { + void operator()() __qpu__ { + cudaq::qvector q(4); + swap(q[0], q[1], q[2]); + } +}; + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__ctrl_swap_individual +// CHECK: %[[Q:.*]] = quake.alloca !quake.veq<4> +// CHECK: %[[R0:.*]] = quake.extract_ref %[[Q]][0] : (!quake.veq<4>) -> !quake.ref +// CHECK: %[[R1:.*]] = quake.extract_ref %[[Q]][1] : (!quake.veq<4>) -> !quake.ref +// CHECK: %[[R2:.*]] = quake.extract_ref %[[Q]][2] : (!quake.veq<4>) -> !quake.ref +// CHECK: quake.swap [%[[R0]]] %[[R1]], %[[R2]] : (!quake.ref, !quake.ref, !quake.ref) -> () +// CHECK: return + +// Controlled swap: a register of controls with two individual qubit targets. +// The dedicated three-argument overload swap(QuantumRegister, qubit, qubit) +// always treats the first argument as controls — no needed. +struct ctrl_swap_veq { + void operator()() __qpu__ { + cudaq::qvector ctrl_reg(4); + cudaq::qubit src, tgt; + swap(ctrl_reg, src, tgt); + } +}; + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__ctrl_swap_veq +// CHECK: %[[CTRLS:.*]] = quake.alloca !quake.veq<4> +// CHECK: %[[SRC:.*]] = quake.alloca !quake.ref +// CHECK: %[[TGT:.*]] = quake.alloca !quake.ref +// CHECK: quake.swap [%[[CTRLS]]] %[[SRC]], %[[TGT]] : (!quake.veq<4>, !quake.ref, !quake.ref) -> () +// CHECK: return From 190e52b829eea79d2979e5ad246c237fb760d8e3 Mon Sep 17 00:00:00 2001 From: Arnav Nagzirkar <113314200+arnavnagzirkar@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:37:19 -0700 Subject: [PATCH 2/2] test: expand pruned FileCheck lines in ctrl_broadcast Address review feedback: bind SSA values and capture the full broadcast loop body (veq_size, cc.loop while/do/step, extract_ref, gate, invariant) and the individual control operands so the tests catch regressions instead of matching any string. Signed-off-by: Arnav Nagzirkar <113314200+arnavnagzirkar@users.noreply.github.com> --- cudaq/test/AST-Quake/ctrl_broadcast.cpp | 45 ++++++++++++++++++++----- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/cudaq/test/AST-Quake/ctrl_broadcast.cpp b/cudaq/test/AST-Quake/ctrl_broadcast.cpp index de9a4479eb3..32cb8ef5a7f 100644 --- a/cudaq/test/AST-Quake/ctrl_broadcast.cpp +++ b/cudaq/test/AST-Quake/ctrl_broadcast.cpp @@ -41,9 +41,21 @@ struct broadcast_register { }; // CHECK-LABEL: func.func @__nvqpp__mlirgen__broadcast_register -// CHECK: quake.alloca !quake.veq<4> -// CHECK: cc.loop while -// CHECK: quake.x %{{.*}} : (!quake.ref) -> () +// CHECK: %[[Q:.*]] = quake.alloca !quake.veq<4> +// CHECK: %[[SIZE:.*]] = quake.veq_size %[[Q]] : (!quake.veq<4>) -> i64 +// CHECK: %{{.*}} = cc.loop while ((%[[I:.*]] = %{{.*}}) -> (i64)) { +// CHECK: %[[CMP:.*]] = arith.cmpi slt, %[[I]], %{{.*}} : i64 +// CHECK: cc.condition %[[CMP]](%[[I]] : i64) +// CHECK: } do { +// CHECK: ^bb0(%[[IARG:.*]]: i64): +// CHECK: %[[REF:.*]] = quake.extract_ref %[[Q]][%[[IARG]]] : (!quake.veq<4>, i64) -> !quake.ref +// CHECK: quake.x %[[REF]] : (!quake.ref) -> () +// CHECK: cc.continue %[[IARG]] : i64 +// CHECK: } step { +// CHECK: ^bb0(%[[ISTEP:.*]]: i64): +// CHECK: %{{.*}} = arith.addi %[[ISTEP]], %{{.*}} : i64 +// CHECK: } {invariant} +// CHECK: return // Broadcast: even with , passing a single register still // broadcasts (the ctrl modifier is ignored for a single-veq operand). @@ -55,9 +67,23 @@ struct ctrl_broadcast_register { }; // CHECK-LABEL: func.func @__nvqpp__mlirgen__ctrl_broadcast_register -// CHECK: quake.alloca !quake.veq<4> -// CHECK: cc.loop while -// CHECK: quake.x %{{.*}} : (!quake.ref) -> () +// CHECK: %[[Q:.*]] = quake.alloca !quake.veq<4> +// CHECK: %[[SIZE:.*]] = quake.veq_size %[[Q]] : (!quake.veq<4>) -> i64 +// CHECK: %{{.*}} = cc.loop while ((%[[I:.*]] = %{{.*}}) -> (i64)) { +// CHECK: %[[CMP:.*]] = arith.cmpi slt, %[[I]], %{{.*}} : i64 +// CHECK: cc.condition %[[CMP]](%[[I]] : i64) +// CHECK: } do { +// CHECK: ^bb0(%[[IARG:.*]]: i64): +// CHECK: %[[REF:.*]] = quake.extract_ref %[[Q]][%[[IARG]]] : (!quake.veq<4>, i64) -> !quake.ref +// CHECK: quake.x %[[REF]] : (!quake.ref) -> () +// CHECK: cc.continue %[[IARG]] : i64 +// CHECK: } step { +// CHECK: ^bb0(%[[ISTEP:.*]]: i64): +// CHECK: %{{.*}} = arith.addi %[[ISTEP]], %{{.*}} : i64 +// CHECK: } {invariant} +// CHECK: return +// The ctrl modifier is ignored for a single veq, so no controlled form +// (quake.x [ ... ]) is ever emitted. // CHECK-NOT: quake.x [% // Control: with multiple refs — first N-1 are controls, last @@ -70,8 +96,11 @@ struct ctrl_individual { }; // CHECK-LABEL: func.func @__nvqpp__mlirgen__ctrl_individual -// CHECK: quake.alloca !quake.veq<4> -// CHECK: quake.x [%{{.*}}, %{{.*}}] %{{.*}} : (!quake.ref, !quake.ref, !quake.ref) -> () +// CHECK: %[[Q:.*]] = quake.alloca !quake.veq<4> +// CHECK: %[[R0:.*]] = quake.extract_ref %[[Q]][0] : (!quake.veq<4>) -> !quake.ref +// CHECK: %[[R1:.*]] = quake.extract_ref %[[Q]][1] : (!quake.veq<4>) -> !quake.ref +// CHECK: %[[R2:.*]] = quake.extract_ref %[[Q]][2] : (!quake.veq<4>) -> !quake.ref +// CHECK: quake.x [%[[R0]], %[[R1]]] %[[R2]] : (!quake.ref, !quake.ref, !quake.ref) -> () // CHECK: return // Implicit control: passing a qvector followed by a qubit selects the