Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions source/compiler/qsc/src/interpret/circuit_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1791,3 +1791,232 @@ mod debugger_stepping {
.assert_eq(&circs);
}
}

// Without parallel, released qubits have their IDs recycled on subsequent allocations.
// The inner block releases q1/q2, so q3/q4 reuse the same wires (q_0, q_1).
#[test]
fn parallel_baseline_qubit_ids_recycled_without_parallel() {
let circ = circuit_without_groups(
r"
namespace Test {
@EntryPoint()
operation Main() : Unit {
{ use q1 = Qubit(); H(q1); use q2 = Qubit(); H(q2); }
use q3 = Qubit();
H(q3);
use q4 = Qubit();
H(q4);
}
}
",
CircuitEntryPoint::EntryPoint,
);

expect![[r#"
q_0@test.qs:4:22, test.qs:5:20 ─ H@test.qs:4:40 ─── H@test.qs:6:20 ──
q_1@test.qs:4:47, test.qs:7:20 ─ H@test.qs:4:65 ─── H@test.qs:8:20 ──
"#]]
.assert_eq(&circ);
}

// Inside a parallel expression all releases are deferred, so q3/q4 get fresh wires
// instead of reusing q_0/q_1. This mirrors the baseline test with `parallel` added.
#[test]
fn parallel_defers_qubit_release() {
let circ = circuit_without_groups(
r"
namespace Test {
@EntryPoint()
operation Main() : Unit {
parallel {
{ use q1 = Qubit(); H(q1); use q2 = Qubit(); H(q2); }
use q3 = Qubit();
H(q3);
use q4 = Qubit();
H(q4);
}
}
}
",
CircuitEntryPoint::EntryPoint,
);

expect![[r#"
q_0@test.qs:5:26 ─ H@test.qs:5:44 ──
q_1@test.qs:5:51 ─ H@test.qs:5:69 ──
q_2@test.qs:6:24 ─ H@test.qs:7:24 ──
q_3@test.qs:8:24 ─ H@test.qs:9:24 ──
"#]]
.assert_eq(&circ);
}

// After a parallel block ends its deferred releases become available, so a second
// parallel block reuses the same qubit wires.
#[test]
fn parallel_releases_available_after_block_ends() {
let circ = circuit_without_groups(
r"
namespace Test {
@EntryPoint()
operation Main() : Unit {
parallel {
use q = Qubit();
H(q);
}
parallel {
use q = Qubit();
X(q);
}
}
}
",
CircuitEntryPoint::EntryPoint,
);

expect![[r#"
q_0@test.qs:5:24, test.qs:9:24 ─ H@test.qs:6:24 ─── X@test.qs:10:24 ─
"#]]
.assert_eq(&circ);
}

// In nested parallel expressions, inner block qubits flow to the outer layer on removal
// so the outer block allocates fresh wires even after the inner block ends.
#[test]
fn parallel_nested_defers_inner_releases_to_outer() {
let circ = circuit_without_groups(
r"
namespace Test {
@EntryPoint()
operation Main() : Unit {
parallel {
use outer = Qubit();
H(outer);
parallel {
use inner1 = Qubit();
H(inner1);
use inner2 = Qubit();
H(inner2);
}
use outer2 = Qubit();
H(outer2);
}
}
}
",
CircuitEntryPoint::EntryPoint,
);

expect![[r#"
q_0@test.qs:5:24 ─ H@test.qs:6:24 ──
q_1@test.qs:8:28 ─ H@test.qs:9:28 ──
q_2@test.qs:10:28 ─ H@test.qs:11:28 ─
q_3@test.qs:13:24 ─ H@test.qs:14:24 ─
"#]]
.assert_eq(&circ);
}

// parallel within N: once N qubits are deferred the pool replenishes, so later
// allocations reuse existing wires rather than creating new ones.
#[test]
fn parallel_within_reuses_wires_after_limit() {
let circ = circuit_without_groups(
r"
namespace Test {
@EntryPoint()
operation Main() : Unit {
parallel within 2 {
{ use q1 = Qubit(); H(q1); }
{ use q2 = Qubit(); H(q2); }
{ use q3 = Qubit(); H(q3); }
{ use q4 = Qubit(); H(q4); }
}
}
}
",
CircuitEntryPoint::EntryPoint,
);

expect![[r#"
q_0@test.qs:5:26 ─ H@test.qs:5:44 ─── H@test.qs:7:44 ──
q_1@test.qs:6:26 ─ H@test.qs:6:44 ─── H@test.qs:8:44 ──
"#]]
.assert_eq(&circ);
}

// Outer `parallel within 6` with inner `parallel within 2`. The inner limit reuses
// wires within each iteration. Once the outer deferred count reaches 6 (iteration 3),
// the outer layer replenishes and reuses its wires too.
#[test]
fn parallel_within_nested_defers_through_outer_limit() {
let circ = circuit_without_groups(
r"
namespace Test {
@EntryPoint()
operation Main() : Unit {
parallel within 6 {
for _ in 0..2 {
{ use q0 = Qubit(); H(q0); }
parallel within 2 {
{ use q1 = Qubit(); H(q1); }
{ use q2 = Qubit(); H(q2); }
{ use q3 = Qubit(); H(q3); }
{ use q4 = Qubit(); H(q4); }
}
}
}
}
}
",
CircuitEntryPoint::EntryPoint,
);

expect![[r#"
q_0@test.qs:6:30 ─ H@test.qs:6:48 ─── H@test.qs:6:48 ────────────────────────────────────────
q_1@test.qs:8:34 ─ H@test.qs:8:52 ─── H@test.qs:10:52 ── H@test.qs:8:52 ─── H@test.qs:10:52 ─
q_2@test.qs:9:34 ─ H@test.qs:9:52 ─── H@test.qs:11:52 ── H@test.qs:9:52 ─── H@test.qs:11:52 ─
q_3@test.qs:6:30 ─ H@test.qs:6:48 ───────────────────────────────────────────────────────────
q_4@test.qs:8:34 ─ H@test.qs:8:52 ─── H@test.qs:10:52 ───────────────────────────────────────
q_5@test.qs:9:34 ─ H@test.qs:9:52 ─── H@test.qs:11:52 ───────────────────────────────────────
"#]].assert_eq(&circ);
}

// Same structure but the outer parallel has no limit. The inner `parallel within 2`
// still reuses within each iteration, but the outer unlimited layer never replenishes,
// so iteration 3 allocates fresh wires (q_6/q_7/q_8) instead of reusing q_0/q_1/q_2.
#[test]
fn parallel_nested_unlimited_outer_defers_all() {
let circ = circuit_without_groups(
r"
namespace Test {
@EntryPoint()
operation Main() : Unit {
parallel {
for _ in 0..2 {
{ use q0 = Qubit(); H(q0); }
parallel within 2 {
{ use q1 = Qubit(); H(q1); }
{ use q2 = Qubit(); H(q2); }
{ use q3 = Qubit(); H(q3); }
{ use q4 = Qubit(); H(q4); }
}
}
}
}
}
",
CircuitEntryPoint::EntryPoint,
);

expect![[r#"
q_0@test.qs:6:30 ─ H@test.qs:6:48 ─────────────────────
q_1@test.qs:8:34 ─ H@test.qs:8:52 ─── H@test.qs:10:52 ─
q_2@test.qs:9:34 ─ H@test.qs:9:52 ─── H@test.qs:11:52 ─
q_3@test.qs:6:30 ─ H@test.qs:6:48 ─────────────────────
q_4@test.qs:8:34 ─ H@test.qs:8:52 ─── H@test.qs:10:52 ─
q_5@test.qs:9:34 ─ H@test.qs:9:52 ─── H@test.qs:11:52 ─
q_6@test.qs:6:30 ─ H@test.qs:6:48 ─────────────────────
q_7@test.qs:8:34 ─ H@test.qs:8:52 ─── H@test.qs:10:52 ─
q_8@test.qs:9:34 ─ H@test.qs:9:52 ─── H@test.qs:11:52 ─
"#]]
.assert_eq(&circ);
}
8 changes: 8 additions & 0 deletions source/compiler/qsc_ast/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,10 @@ pub enum ExprKind {
Lambda(CallableKind, Box<Pat>, Box<Expr>),
/// A literal.
Lit(Box<Lit>),
/// A parallel expression: `parallel a`
Parallel(Box<Expr>),
/// A parallel-limited expression: `parallel within n a`
ParallelLimited(Box<Expr>, Box<Expr>),
/// Parentheses: `(a)`.
Paren(Box<Expr>),
/// A path: `a` or `a.b`.
Expand Down Expand Up @@ -964,6 +968,10 @@ impl Display for ExprKind {
ExprKind::Interpolate(components) => display_interpolate(indent, components)?,
ExprKind::Lambda(kind, param, expr) => display_lambda(indent, *kind, param, expr)?,
ExprKind::Lit(lit) => write!(indent, "Lit: {lit}")?,
ExprKind::Parallel(e) => write!(indent, "Parallel: {e}")?,
ExprKind::ParallelLimited(limit, body) => {
write!(indent, "ParallelLimited: {limit} {body}")?;
}
ExprKind::Paren(e) => write!(indent, "Paren: {e}")?,
ExprKind::Path(p) => write!(indent, "Path: {p}")?,
ExprKind::Range(start, step, end) => {
Expand Down
9 changes: 8 additions & 1 deletion source/compiler/qsc_ast/src/mut_visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,16 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) {
vis.visit_pat(pat);
vis.visit_expr(expr);
}
ExprKind::Paren(expr) | ExprKind::Return(expr) | ExprKind::UnOp(_, expr) => {
ExprKind::Parallel(expr)
| ExprKind::Paren(expr)
| ExprKind::Return(expr)
| ExprKind::UnOp(_, expr) => {
vis.visit_expr(expr);
}
ExprKind::ParallelLimited(limit, body) => {
vis.visit_expr(limit);
vis.visit_expr(body);
}
ExprKind::Path(path) => vis.visit_path_kind(path),
ExprKind::Range(start, step, end) => {
for s in start.iter_mut() {
Expand Down
9 changes: 8 additions & 1 deletion source/compiler/qsc_ast/src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,9 +334,16 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) {
vis.visit_pat(pat);
vis.visit_expr(expr);
}
ExprKind::Paren(expr) | ExprKind::Return(expr) | ExprKind::UnOp(_, expr) => {
ExprKind::Parallel(expr)
| ExprKind::Paren(expr)
| ExprKind::Return(expr)
| ExprKind::UnOp(_, expr) => {
vis.visit_expr(expr);
}
ExprKind::ParallelLimited(limit, body) => {
vis.visit_expr(limit);
vis.visit_expr(body);
}
ExprKind::Path(path) => vis.visit_path_kind(path),
ExprKind::Range(start, step, end) => {
if let Some(s) = start.as_ref() {
Expand Down
10 changes: 10 additions & 0 deletions source/compiler/qsc_codegen/src/qsharp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,16 @@ impl<W: Write> Visitor<'_> for QSharpGen<W> {
}
self.visit_expr(expr);
}
ExprKind::Parallel(expr) => {
self.write("parallel ");
self.visit_expr(expr);
}
ExprKind::ParallelLimited(limit, body) => {
self.write("parallel within ");
self.visit_expr(limit);
self.write(" ");
self.visit_expr(body);
}
ExprKind::Paren(expr) => {
self.write("(");
self.visit_expr(expr);
Expand Down
Loading
Loading