Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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 @@ -2137,3 +2137,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#"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is difficult for me to understand what should be expected from the nested parallel within expressions, so I'll double-check with you. Is this correct and what is expected with the nested setup?

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`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want the target expr to be a block to force a scope on the feature rather than allowing any expr?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It gets turned into a block if it isn't one as part of lowering into HIR. But from a user-facing syntax perspective, there is value in having it support any expression. For example, for-loops are an expression, and one of the main motiviations is allowing parallel for syntax without having to add an extraneous block.

Parallel(Box<Expr>),
Comment thread
swernli marked this conversation as resolved.
Outdated
/// A parallel-limited expression: `parallel within n a`
ParallelLimited(Box<Expr>, Box<Expr>),
Comment thread
swernli marked this conversation as resolved.
Outdated
/// 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}")?;
Comment thread
swernli marked this conversation as resolved.
Outdated
}
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