From 01a84e5a3752965bad964ad0752196b354487719 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Tue, 21 Jul 2026 13:11:41 -0700 Subject: [PATCH] New `parallel` expression in Q# This change adds support for a new parallel expression that alters the behavior of qubit allocation and code generation. There are two forms of the expression: - `parallel `: For the duration of the expression after the keyword, all qubit release is deferred such that each qubit allocation uses a fresh qubit identifier. In addition, when configured for QIR code generation, no expressions that result in dynamic branching are allowed within the expression and all loops within the expression are unrolled. This ensures that the generated QIR for that expression will be contained within a single LLVM block. Combining these two features (defferred release and enforced single block) maximizs the likelihood that the code corresponding to the expression will be parallelized by the target's compilation and scheduling. - `parallel within `: This form has the same semantics as the first form, but with the added integer limit that expresses how many fresh qubit allocations will be used before normal qubit reuse begins. For example, `parallel wthin 4 ` will defer the release of four qubits, and if a fifth allocation is made within the expression then that allocation will draw from the previously deferred qubits, returning to a reuse pattern. This allows the program to express a limited amount of parallelism while still allowing for some reuse, providing more fine grained control over the width/depth (space/time) tradeoff of the algorithm. The limit expression must be a static (compile-time constant) integer. The new expression affects the behavior of QIR code generation, simulation, and resource estimation, and the change includes updated parser, AST, HIR, FIR, and lowering logic for the syntax as well as new RCA checks to enforce the requirements for control flow structure and static integer limits. --- source/compiler/qsc/src/codegen.rs | 6 + .../qsc/src/interpret/circuit_tests.rs | 229 ++++++++++ source/compiler/qsc_ast/src/ast.rs | 17 + source/compiler/qsc_ast/src/mut_visit.rs | 6 + source/compiler/qsc_ast/src/visit.rs | 6 + source/compiler/qsc_codegen/src/qsharp.rs | 9 + source/compiler/qsc_eval/src/lib.rs | 287 ++++++++++-- source/compiler/qsc_eval/src/tests.rs | 228 +++++++++ source/compiler/qsc_eval/src/val.rs | 2 +- source/compiler/qsc_fir/src/fir.rs | 41 ++ source/compiler/qsc_fir/src/mut_visit.rs | 6 + source/compiler/qsc_fir/src/visit.rs | 6 + .../compiler/qsc_fir_transforms/src/cloner.rs | 6 + .../src/defunctionalize/analysis.rs | 35 ++ .../src/defunctionalize/rewrite.rs | 18 + .../src/defunctionalize/specialize.rs | 41 ++ .../src/defunctionalize/tests.rs | 165 +++++++ .../src/exec_graph_rebuild.rs | 9 + .../src/exec_graph_rebuild/tests.rs | 63 +++ .../qsc_fir_transforms/src/invariants.rs | 18 +- .../qsc_fir_transforms/src/monomorphize.rs | 6 + .../src/monomorphize/tests.rs | 92 ++++ .../compiler/qsc_fir_transforms/src/pretty.rs | 15 + .../qsc_fir_transforms/src/pretty/tests.rs | 62 +++ .../src/return_unify/lower.rs | 12 + .../src/return_unify/normalize.rs | 21 + .../src/return_unify/normalize/anf.rs | 35 ++ .../src/return_unify/simplify.rs | 12 + .../src/return_unify/tests/general.rs | 274 +++++++++++ .../qsc_fir_transforms/src/test_utils.rs | 1 + .../qsc_fir_transforms/src/tuple_decompose.rs | 6 + .../src/tuple_decompose/tests.rs | 64 +++ .../qsc_fir_transforms/src/walk_utils.rs | 20 + .../src/walk_utils/tests.rs | 127 ++++++ source/compiler/qsc_frontend/src/lower.rs | 33 ++ .../compiler/qsc_frontend/src/lower/tests.rs | 122 +++++ .../compiler/qsc_frontend/src/typeck/rules.rs | 12 + source/compiler/qsc_hir/src/hir.rs | 17 + source/compiler/qsc_hir/src/mut_visit.rs | 7 + source/compiler/qsc_hir/src/visit.rs | 7 + source/compiler/qsc_lowerer/src/lib.rs | 12 +- .../qsc_parse/src/completion/word_kinds.rs | 1 + source/compiler/qsc_parse/src/expr.rs | 12 +- source/compiler/qsc_parse/src/expr/tests.rs | 180 ++++++++ source/compiler/qsc_parse/src/keyword.rs | 3 + source/compiler/qsc_partial_eval/src/lib.rs | 66 ++- .../qsc_partial_eval/src/management.rs | 38 +- source/compiler/qsc_partial_eval/src/tests.rs | 1 + .../qsc_partial_eval/src/tests/parallel.rs | 386 ++++++++++++++++ .../compiler/qsc_passes/src/capabilitiesck.rs | 6 + .../src/capabilitiesck/tests_adaptive.rs | 101 +++- .../tests_adaptive_plus_integers.rs | 83 +++- ...tests_adaptive_plus_integers_and_floats.rs | 83 +++- .../src/capabilitiesck/tests_base.rs | 125 ++++- .../src/capabilitiesck/tests_common.rs | 57 +++ source/compiler/qsc_passes/src/logic_sep.rs | 8 + source/compiler/qsc_rca/src/core.rs | 84 +++- source/compiler/qsc_rca/src/errors.rs | 23 + source/compiler/qsc_rca/src/lib.rs | 13 +- source/compiler/qsc_rca/src/tests.rs | 1 + source/compiler/qsc_rca/src/tests/parallel.rs | 431 ++++++++++++++++++ source/vscode/syntaxes/qsharp.tmLanguage.json | 2 +- 62 files changed, 3779 insertions(+), 80 deletions(-) create mode 100644 source/compiler/qsc_partial_eval/src/tests/parallel.rs create mode 100644 source/compiler/qsc_rca/src/tests/parallel.rs diff --git a/source/compiler/qsc/src/codegen.rs b/source/compiler/qsc/src/codegen.rs index 681884b649e..996f9e063a6 100644 --- a/source/compiler/qsc/src/codegen.rs +++ b/source/compiler/qsc/src/codegen.rs @@ -726,6 +726,12 @@ pub mod qir { exprs.push(*cond); blocks.push(*block_id); } + qsc_fir::fir::ExprKind::Parallel(limit, body) => { + if let Some(limit) = limit { + exprs.push(*limit); + } + exprs.push(*body); + } } (exprs, blocks) } diff --git a/source/compiler/qsc/src/interpret/circuit_tests.rs b/source/compiler/qsc/src/interpret/circuit_tests.rs index c68834bf97f..ed4405c2136 100644 --- a/source/compiler/qsc/src/interpret/circuit_tests.rs +++ b/source/compiler/qsc/src/interpret/circuit_tests.rs @@ -2139,3 +2139,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); +} diff --git a/source/compiler/qsc_ast/src/ast.rs b/source/compiler/qsc_ast/src/ast.rs index b43755ec490..4ba9166859d 100644 --- a/source/compiler/qsc_ast/src/ast.rs +++ b/source/compiler/qsc_ast/src/ast.rs @@ -906,6 +906,8 @@ pub enum ExprKind { Lambda(CallableKind, Box, Box), /// A literal. Lit(Box), + /// A parallel expression: `parallel a`, optionally including a limit: `parallel within n a` + Parallel(Option>, Box), /// Parentheses: `(a)`. Paren(Box), /// A path: `a` or `a.b`. @@ -953,6 +955,7 @@ 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(limit, body) => display_parallel(indent, limit.as_deref(), body)?, ExprKind::Paren(e) => write!(indent, "Paren: {e}")?, ExprKind::Path(p) => write!(indent, "Path: {p}")?, ExprKind::Range(start, step, end) => { @@ -1126,6 +1129,20 @@ fn display_interpolate( Ok(()) } +fn display_parallel( + mut indent: Indented, + limit: Option<&Expr>, + body: &Expr, +) -> fmt::Result { + write!(indent, "Parallel:")?; + indent = set_indentation(indent, 1); + if let Some(l) = limit { + write!(indent, "\nLimit: {l}")?; + } + write!(indent, "\nBody: {body}")?; + Ok(()) +} + fn display_lambda( mut indent: Indented, kind: CallableKind, diff --git a/source/compiler/qsc_ast/src/mut_visit.rs b/source/compiler/qsc_ast/src/mut_visit.rs index ce80b9ca87f..2c5159d6242 100644 --- a/source/compiler/qsc_ast/src/mut_visit.rs +++ b/source/compiler/qsc_ast/src/mut_visit.rs @@ -365,6 +365,12 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { ExprKind::Paren(expr) | ExprKind::Return(expr) | ExprKind::UnOp(_, expr) => { vis.visit_expr(expr); } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit.as_mut() { + vis.visit_expr(l); + } + vis.visit_expr(body); + } ExprKind::Path(path) => vis.visit_path_kind(path), ExprKind::Range(start, step, end) => { for s in start.iter_mut() { diff --git a/source/compiler/qsc_ast/src/visit.rs b/source/compiler/qsc_ast/src/visit.rs index f123b320a0d..f3f75b9d186 100644 --- a/source/compiler/qsc_ast/src/visit.rs +++ b/source/compiler/qsc_ast/src/visit.rs @@ -337,6 +337,12 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { ExprKind::Paren(expr) | ExprKind::Return(expr) | ExprKind::UnOp(_, expr) => { vis.visit_expr(expr); } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit.as_ref() { + vis.visit_expr(l); + } + vis.visit_expr(body); + } ExprKind::Path(path) => vis.visit_path_kind(path), ExprKind::Range(start, step, end) => { if let Some(s) = start.as_ref() { diff --git a/source/compiler/qsc_codegen/src/qsharp.rs b/source/compiler/qsc_codegen/src/qsharp.rs index 71eee205190..eecb82c8c04 100644 --- a/source/compiler/qsc_codegen/src/qsharp.rs +++ b/source/compiler/qsc_codegen/src/qsharp.rs @@ -540,6 +540,15 @@ impl Visitor<'_> for QSharpGen { } self.visit_expr(expr); } + ExprKind::Parallel(limit, body) => { + self.write("parallel "); + if let Some(limit) = limit { + self.write("within "); + self.visit_expr(limit); + self.write(" "); + } + self.visit_expr(body); + } ExprKind::Paren(expr) => { self.write("("); self.visit_expr(expr); diff --git a/source/compiler/qsc_eval/src/lib.rs b/source/compiler/qsc_eval/src/lib.rs index c7eeaf2f6ca..4e1cf100e20 100644 --- a/source/compiler/qsc_eval/src/lib.rs +++ b/source/compiler/qsc_eval/src/lib.rs @@ -39,8 +39,8 @@ use qsc_data_structures::{functors::FunctorApp, index_map::IndexMap, span::Span} use qsc_fir::fir::{ self, BinOp, BlockId, CallableImpl, ConfiguredExecGraph, ExecGraph, ExecGraphConfig, ExecGraphDebugNode, ExecGraphNode, Expr, ExprId, ExprKind, Field, FieldAssign, Global, Lit, - LocalItemId, LocalVarId, PackageId, PackageStoreLookup, PatId, PatKind, PrimField, Res, StmtId, - StoreItemId, StringComponent, UnOp, + LocalItemId, LocalVarId, PackageId, PackageStoreLookup, ParKind, PatId, PatKind, PrimField, + Res, StmtId, StoreItemId, StringComponent, UnOp, }; use qsc_fir::ty::Ty; pub use qsc_hir::hir::PackageSpan; @@ -48,6 +48,8 @@ use qsc_lowerer::map_fir_package_to_hir; use rand::{SeedableRng, rngs::StdRng}; use rustc_hash::{FxHashMap, FxHashSet}; use std::array; +use std::collections::VecDeque; +use std::mem::take; use std::{ cell::RefCell, fmt::{self, Display, Formatter}, @@ -562,10 +564,6 @@ impl Env { pub fn track_qubit(&mut self, qubit: Rc) { self.qubits.insert(qubit); } - - pub fn release_qubit(&mut self, qubit: &Rc) { - self.qubits.remove(qubit); - } } #[derive(Clone, Default)] @@ -602,6 +600,7 @@ pub struct State { error_behavior: ErrorBehavior, last_error: Option<(Error, Vec)>, exec_graph_config: ExecGraphConfig, + delayed_release_qubits: DelayedQubitReleaseStack, } impl State { @@ -634,6 +633,7 @@ impl State { error_behavior, last_error: None, exec_graph_config, + delayed_release_qubits: DelayedQubitReleaseStack::default(), } } @@ -781,17 +781,7 @@ impl State { self.idx += 1; match self.eval_expr(env, sim, globals, out, *expr) { Ok(()) => continue, - Err(e) => { - if self.error_behavior == ErrorBehavior::StopOnError { - let error_str = e.to_string(); - self.set_last_error(e, self.capture_stack()); - // Clear the execution graph stack to indicate that execution has failed. - // This will prevent further execution steps. - self.exec_graph_stack.clear(); - return Ok(StepResult::Fail(error_str)); - } - return Err((e, self.capture_stack())); - } + Err(e) => return self.handle_error(e), } } Some(ExecGraphNode::Jump(idx)) => { @@ -831,6 +821,63 @@ impl State { env.leave_scope(); continue; } + Some(ExecGraphNode::ParStart(kind)) => { + let limit = if *kind == ParKind::Limited { + let limit_val = self.take_val_register().unwrap_int(); + if limit_val < 0 { + let package = map_fir_package_to_hir(self.package); + return self.handle_error(Error::InvalidNegativeInt( + limit_val, + PackageSpan { + package, + span: self.current_span, + }, + )); + } + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + Some(limit_val as usize) + } else { + None + }; + self.delayed_release_qubits.add_layer(limit); + self.idx += 1; + continue; + } + Some(ExecGraphNode::ParEnd) => { + // After finishing all parallel sections, we should check for any delayed qubit releases that need to be performed. + let call_stack = self.capture_stack_if_trace_enabled(sim); + for qubit in self.delayed_release_qubits.remove_layer() { + env.qubits.remove(&qubit); + let is_borrowed = self.dirty_qubits.remove(&qubit.0); + match ( + sim.qubit_release(qubit.0, &call_stack).map_err(|e| { + let package_span = PackageSpan { + package: map_fir_package_to_hir(self.package), + span: self.current_span, + }; + + Error::SimulationError(e, package_span) + }), + is_borrowed, + ) { + (Ok(true), _) | (Ok(_), true) => {} + (Ok(false), false) => { + let package_span = PackageSpan { + package: map_fir_package_to_hir(self.package), + span: self.current_span, + }; + + return self.handle_error(Error::ReleasedQubitNotZero( + qubit.0, + package_span, + )); + } + (Err(e), _) => return self.handle_error(e), + } + } + self.idx += 1; + continue; + } Some(ExecGraphNode::Debug(dbg_node)) => match dbg_node { ExecGraphDebugNode::PushScope => { self.push_scope(env); @@ -907,6 +954,19 @@ impl State { Ok(StepResult::Return(self.get_result())) } + fn handle_error(&mut self, error: Error) -> Result)> { + if self.error_behavior == ErrorBehavior::StopOnError { + let error_str = error.to_string(); + self.set_last_error(error, self.capture_stack()); + // Clear the execution graph stack to indicate that execution has failed. + // This will prevent further execution steps. + self.exec_graph_stack.clear(); + Ok(StepResult::Fail(error_str)) + } else { + Err((error, self.capture_stack())) + } + } + fn check_for_break( &self, breakpoints: &[StmtId], @@ -965,7 +1025,7 @@ impl State { self.val_register.take().unwrap_or_else(Value::unit) } - #[allow(clippy::similar_names)] + #[allow(clippy::similar_names, clippy::too_many_lines)] fn eval_expr( &mut self, env: &mut Env, @@ -1070,6 +1130,9 @@ impl State { ExprKind::While(..) => { panic!("while expr should be handled by control flow") } + ExprKind::Parallel(..) => { + panic!("parallel expr should be handled by control flow") + } } Ok(()) @@ -1342,28 +1405,43 @@ impl State { let name = &callee.name.name; let val = match name.as_ref() { "__quantum__rt__qubit_allocate" | "__quantum__rt__qubit_borrow" => { - let q = sim - .qubit_allocate(&call_stack) - .map_err(|e| Error::SimulationError(e, callee_span))?; - let q = Rc::new(Qubit(q)); - env.track_qubit(Rc::clone(&q)); - if let Some(counter) = &mut self.qubit_counter { - counter.allocated(q.0); - } - if name.as_ref() == "__quantum__rt__qubit_borrow" { - self.dirty_qubits.insert(q.0); + if let Some(q) = self.delayed_release_qubits.allocate_delayed_qubit() { + Value::Qubit( + env.qubits + .get(&q) + .expect("qubit should be tracked") + .clone() + .into(), + ) + } else { + let q = sim + .qubit_allocate(&call_stack) + .map_err(|e| Error::SimulationError(e, callee_span))?; + let q = Rc::new(Qubit(q)); + env.track_qubit(Rc::clone(&q)); + if let Some(counter) = &mut self.qubit_counter { + counter.allocated(q.0); + } + if name.as_ref() == "__quantum__rt__qubit_borrow" { + self.dirty_qubits.insert(q.0); + } + Value::Qubit(q.into()) } - Value::Qubit(q.into()) } "__quantum__rt__qubit_release" => { let qubit = arg .unwrap_qubit() .try_deref() .ok_or(Error::QubitDoubleRelease(arg_span))?; - env.release_qubit(&qubit); - let is_zero = sim - .qubit_release(qubit.0, &call_stack) - .map_err(|e| Error::SimulationError(e, callee_span))?; + let is_zero = if self.delayed_release_qubits.delay_release_qubit(*qubit) { + // If the qubit is delayed for release, we don't check if it's zero yet. + // The actual release will be handled later when the parallel section ends. + true + } else { + env.qubits.remove(&qubit); + sim.qubit_release(qubit.0, &call_stack) + .map_err(|e| Error::SimulationError(e, callee_span))? + }; let is_borrowed = self.dirty_qubits.remove(&qubit.0); if is_zero || is_borrowed { Value::unit() @@ -1876,6 +1954,149 @@ impl State { } } +#[derive(Debug, Default)] +struct DelayedQubitReleaseLayer { + released_qubits: Vec, + available_qubits: VecDeque, + used_qubits: FxHashSet, + limit: Option, + allocated: usize, +} + +#[derive(Debug, Default)] +pub struct DelayedQubitReleaseStack { + layers: Vec, +} + +impl DelayedQubitReleaseStack { + #[must_use] + pub fn is_empty(&self) -> bool { + self.layers.is_empty() + } + + /// Add a new layer for tracking delayed qubit releases to the stack, + /// optionally with the specified limit on the number of qubits that are allocated + /// fresh before reuse begins. + /// To help accomodate nested delayed release and reuse, the new layer will inherit any + /// available qubits from the previous layer. + pub fn add_layer(&mut self, limit: Option) { + let new_layer = if let Some(DelayedQubitReleaseLayer { + available_qubits, .. + }) = self.layers.last_mut() + { + DelayedQubitReleaseLayer { + available_qubits: take(available_qubits), + limit, + ..Default::default() + } + } else { + DelayedQubitReleaseLayer { + limit, + ..Default::default() + } + }; + self.layers.push(new_layer); + } + + /// Remove the top layer from the stack and return any qubits that should be released. + /// If there is a parent layer, release of the qubits in this layer will be delayed by + /// into the parent, and only unused available qubits will be returned to the parent + /// layer's available qubits. + pub fn remove_layer(&mut self) -> Vec { + if let Some(DelayedQubitReleaseLayer { + released_qubits, + available_qubits, + used_qubits, + .. + }) = self.layers.pop() + { + if let Some(DelayedQubitReleaseLayer { + released_qubits: parent_released_qubits, + available_qubits: parent_available_qubits, + .. + }) = self.layers.last_mut() + { + // Available qubits that were never used in the current layer must have come from the parents available qubits, + // so we need to return those to the parent layer's available qubits. + let mut available_qubits: Vec = available_qubits.into(); + let mut new_parent_available_qubits: Vec = available_qubits + .extract_if(.., |q| !used_qubits.contains(q)) + .collect(); + new_parent_available_qubits.sort_unstable(); + *parent_available_qubits = new_parent_available_qubits.into(); + + // The remaining qubits that were released or available but used in the current layer + // should be added to the parent layer's delayed release qubits. + parent_released_qubits.extend(released_qubits); + parent_released_qubits.extend(available_qubits); + parent_released_qubits.sort_unstable(); + + // Since th parent layer is now responsible for releasing the qubits, we don't return any qubits to be released. + Vec::new() + } else { + // This is the last layer, so return all qubits managed by the layer to be released. + released_qubits + .into_iter() + .chain(available_qubits) + .collect() + } + } else { + Vec::new() + } + } + + /// Add a qubit to the list of qubits that should be released when the current layer is removed. + /// If there is no configured delayed release layer, the qubit will not be added to any layer and the function + /// returns false so the caller can handle the qubit release immediately. + pub fn delay_release_qubit(&mut self, qubit: Qubit) -> bool { + if let Some(DelayedQubitReleaseLayer { + released_qubits, + used_qubits, + allocated, + .. + }) = self.layers.last_mut() + { + *allocated -= 1; + released_qubits.push(qubit); + used_qubits.insert(qubit); + true + } else { + false + } + } + + /// Allocate a qubit from the current delayed release layer, if available. + /// If the layer has a limit and the number of allocated and deferred release qubits + /// exceeds the limit, the available qubits will be replenished from the released qubits. + /// If there is no configured delayed release layer or no available qubits, the + /// function returns None so the caller can perform a fresh qubit allocation. + pub fn allocate_delayed_qubit(&mut self) -> Option { + if let Some(DelayedQubitReleaseLayer { + available_qubits, + released_qubits, + used_qubits, + limit, + allocated, + }) = self.layers.last_mut() + { + *allocated += 1; + if let Some(limit) = limit + && released_qubits.len() + *allocated > *limit + { + let mut qubits = take(released_qubits); + qubits.extend(take(available_qubits)); + qubits.sort_unstable(); + *available_qubits = qubits.into(); + } + if let Some(qubit) = available_qubits.pop_front() { + used_qubits.insert(qubit); + return Some(qubit); + } + } + None + } +} + pub fn are_ctls_unique(ctls: &[Value], tup: &Value) -> bool { let mut qubits = FxHashSet::default(); for ctl in ctls.iter().flat_map(Value::qubits) { diff --git a/source/compiler/qsc_eval/src/tests.rs b/source/compiler/qsc_eval/src/tests.rs index 7a76b0414c3..f3f4ef46556 100644 --- a/source/compiler/qsc_eval/src/tests.rs +++ b/source/compiler/qsc_eval/src/tests.rs @@ -105,6 +105,62 @@ fn check_expr(file: &str, expr: &str, expect: &Expect) { } } +fn check_output(file: &str, expr: &str, expect: &Expect) { + let mut fir_lowerer = qsc_lowerer::Lowerer::new(); + let mut core = compile::core(); + run_core_passes(&mut core); + let fir_store = fir::PackageStore::new(); + let core_fir = fir_lowerer.lower_package(&core.package, &fir_store); + let mut store = PackageStore::new(core); + + let mut std = compile::std(&store, TargetCapabilityFlags::all()); + assert!(std.errors.is_empty()); + assert!(run_default_passes(store.core(), &mut std, PackageType::Lib).is_empty()); + let std_fir = fir_lowerer.lower_package(&std.package, &fir_store); + let std_id = store.insert(std); + + let sources = SourceMap::new([("test".into(), file.into())], Some(expr.into())); + let mut unit = compile( + &store, + &[(std_id, None)], + sources, + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + ); + assert!(unit.errors.is_empty(), "{:?}", unit.errors); + let pass_errors = run_default_passes(store.core(), &mut unit, PackageType::Lib); + assert!(pass_errors.is_empty(), "{pass_errors:?}"); + let unit_fir = fir_lowerer.lower_package(&unit.package, &fir_store); + let entry = unit_fir.entry_exec_graph.clone(); + let id = store.insert(unit); + + let mut fir_store = fir::PackageStore::new(); + fir_store.insert( + map_hir_package_to_fir(qsc_hir::hir::PackageId::CORE), + core_fir, + ); + fir_store.insert(map_hir_package_to_fir(std_id), std_fir); + fir_store.insert(map_hir_package_to_fir(id), unit_fir); + + let mut out = Vec::new(); + match eval_graph( + entry, + &mut SparseSim::new(), + &fir_store, + ExecGraphConfig::NoDebug, + map_hir_package_to_fir(id), + &mut Env::default(), + &mut GenericReceiver::new(&mut out), + ) { + Ok(_) => expect.assert_eq( + std::str::from_utf8(&out) + .expect("output should be valid UTF-8") + .trim_end(), + ), + Err((err, _)) => panic!("unexpected error: {err:?}"), + } +} + fn check_partial_eval_stmt( file: &str, expr: &str, @@ -4232,3 +4288,175 @@ fn partial_eval_stmt_function_calls_from_library() { &expect!["3"], ); } + +// Without parallel, released qubits have their IDs recycled on subsequent allocations. +#[test] +fn parallel_baseline_qubit_ids_recycled_without_parallel() { + check_output( + "", + indoc! {r#"{ + // q1 and q2 are allocated and released inside the inner block + { use q1 = Qubit(); Message($"{q1}"); use q2 = Qubit(); Message($"{q2}"); } + // q1 and q2 are now released; next allocations reuse the same IDs + use q3 = Qubit(); + Message($"{q3}"); + use q4 = Qubit(); + Message($"{q4}"); + }"#}, + &expect!["Qubit0\nQubit1\nQubit0\nQubit1"], + ); +} + +// Inside a parallel expression qubits are allocated fresh even after a sibling is released, +// because all releases are deferred until the parallel block ends. +// This mirrors the baseline test but with `parallel` wrapping the outermost block. +#[test] +fn parallel_defers_qubit_release() { + check_output( + "", + indoc! {r#"parallel { + // q1 and q2 are allocated and released inside the inner block + { use q1 = Qubit(); Message($"{q1}"); use q2 = Qubit(); Message($"{q2}"); } + // inside parallel their release is deferred, so q3 and q4 get fresh ids + use q3 = Qubit(); + Message($"{q3}"); + use q4 = Qubit(); + Message($"{q4}"); + }"#}, + &expect!["Qubit0\nQubit1\nQubit2\nQubit3"], + ); +} + +// After the outer parallel block ends its deferred releases become available, so a +// second parallel block can reuse those qubit IDs. +#[test] +fn parallel_releases_available_after_block_ends() { + check_output( + "", + indoc! {r#"{ + parallel { + use q = Qubit(); + Message($"first:{q}"); + } + parallel { + use q = Qubit(); + Message($"second:{q}"); + } + }"#}, + &expect!["first:Qubit0\nsecond:Qubit0"], + ); +} + +// In nested parallel expressions the inner block's qubits are not available to the outer +// block and defferred until the outer parallel finishes. +#[test] +fn parallel_nested_defers_inner_releases_to_outer() { + check_output( + "", + indoc! {r#"parallel { + use outer = Qubit(); + Message($"outer:{outer}"); + parallel { + use inner1 = Qubit(); + Message($"inner1:{inner1}"); + use inner2 = Qubit(); + Message($"inner2:{inner2}"); + } + // inner qubits are now deferred in the outer layer, so a fresh id is allocated + use outer2 = Qubit(); + Message($"outer2:{outer2}"); + }"#}, + &expect!["outer:Qubit0\ninner1:Qubit1\ninner2:Qubit2\nouter2:Qubit3"], + ); +} + +// parallel within N defers qubit release but once N qubits have been deferred the pool +// is replenished and IDs are reused. +#[test] +fn parallel_within_reuses_ids_after_limit() { + check_output( + "", + indoc! {r#"parallel within 2 { + // Each nested block releases its qubit before the next allocation. + { use q1 = Qubit(); Message($"{q1}"); } + { use q2 = Qubit(); Message($"{q2}"); } + // 2 qubits have now been deferred; limit reached so q3 and q4 reuse ids + { use q3 = Qubit(); Message($"{q3}"); } + { use q4 = Qubit(); Message($"{q4}"); } + }"#}, + &expect!["Qubit0\nQubit1\nQubit0\nQubit1"], + ); +} + +// Nested parallel within: the outer layer has a limit of 6 and the inner has a limit of 2. +// Each of the 3 iterations allocates a qubit outside the inner parallel (tracked by the +// outer layer) plus 4 qubits inside the inner parallel (2 fresh, 2 reused via inner limit). +// After 2 iterations the outer layer has accumulated 6+ deferred qubits, so iteration 3 +// triggers outer replenishment and reuses IDs from the outer pool (Qubit0, 1, 2 reappear). +#[test] +fn parallel_within_nested_defers_through_outer_limit() { + check_output( + "", + indoc! {r#"parallel within 6 { for _ in 0..2 { + { use q0 = Qubit(); Message($"{q0}"); } + parallel within 2 { + { use q1 = Qubit(); Message($"{q1}"); } + { use q2 = Qubit(); Message($"{q2}"); } + { use q3 = Qubit(); Message($"{q3}"); } + { use q4 = Qubit(); Message($"{q4}"); } + } + } }"#}, + &expect![[r#" + Qubit0 + Qubit1 + Qubit2 + Qubit1 + Qubit2 + Qubit3 + Qubit4 + Qubit5 + Qubit4 + Qubit5 + Qubit0 + Qubit1 + Qubit2 + Qubit1 + Qubit2"#]], + ); +} + +// Same as above but the outer parallel has no limit. The inner parallel within 2 still +// reuses within each iteration, but the outer unlimited layer never triggers +// replenishment so every iteration allocates fresh IDs at the outer level (Qubit6, 7, 8 +// in iteration 3 instead of reusing Qubit0, 1, 2). +#[test] +fn parallel_nested_unlimited_outer_defers_all() { + check_output( + "", + indoc! {r#"parallel { for _ in 0..2 { + { use q0 = Qubit(); Message($"{q0}"); } + parallel within 2 { + { use q1 = Qubit(); Message($"{q1}"); } + { use q2 = Qubit(); Message($"{q2}"); } + { use q3 = Qubit(); Message($"{q3}"); } + { use q4 = Qubit(); Message($"{q4}"); } + } + } }"#}, + &expect![[r#" + Qubit0 + Qubit1 + Qubit2 + Qubit1 + Qubit2 + Qubit3 + Qubit4 + Qubit5 + Qubit4 + Qubit5 + Qubit6 + Qubit7 + Qubit8 + Qubit7 + Qubit8"#]], + ); +} diff --git a/source/compiler/qsc_eval/src/val.rs b/source/compiler/qsc_eval/src/val.rs index 66696726114..19de62550ee 100644 --- a/source/compiler/qsc_eval/src/val.rs +++ b/source/compiler/qsc_eval/src/val.rs @@ -152,7 +152,7 @@ impl QubitRef { } } -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] pub struct Qubit(pub usize); #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/source/compiler/qsc_fir/src/fir.rs b/source/compiler/qsc_fir/src/fir.rs index b769388f662..a9b7df846bc 100644 --- a/source/compiler/qsc_fir/src/fir.rs +++ b/source/compiler/qsc_fir/src/fir.rs @@ -913,10 +913,42 @@ pub enum ExecGraphNode { Unit, /// The end of the control flow graph. Ret, + /// The start of a parallel region, with a value indicating whether it has a limit. + ParStart(ParKind), + /// The end of a parallel region + ParEnd, /// A node only to be executed in debug mode. Debug(ExecGraphDebugNode), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// The kind of a parallel region. +pub enum ParKind { + /// A parallel region with a limit. + Limited, + /// A parallel region without a limit. + Unlimited, +} + +impl From for ParKind { + fn from(b: bool) -> Self { + if b { + ParKind::Limited + } else { + ParKind::Unlimited + } + } +} + +impl Display for ParKind { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + ParKind::Limited => write!(f, "Limited"), + ParKind::Unlimited => write!(f, "Unlimited"), + } + } +} + #[derive(Copy, Clone, Debug, PartialEq)] /// A debug-only node within the control flow graph. pub enum ExecGraphDebugNode { @@ -1085,6 +1117,8 @@ pub enum ExprKind { If(ExprId, ExprId, Option), /// An index accessor: `a[b]`. Index(ExprId, ExprId), + /// A parallel expression: `parallel a` or `parallel within n a`. + Parallel(Option, ExprId), /// A literal. Lit(Lit), /// A range: `start..step..end`, `start..end`, `start...`, `...end`, or `...`. @@ -1132,6 +1166,13 @@ impl Display for ExprKind { ExprKind::Hole => write!(indent, "Hole")?, ExprKind::If(cond, body, els) => display_if(indent, *cond, *body, *els)?, ExprKind::Index(array, index) => display_index(indent, *array, *index)?, + ExprKind::Parallel(limit, e) => { + if let Some(limit) = limit { + write!(indent, "Parallel({limit}): {e}")?; + } else { + write!(indent, "Parallel: {e}")?; + } + } ExprKind::Lit(lit) => write!(indent, "Lit: {lit}")?, ExprKind::Range(start, step, end) => display_range(indent, *start, *step, *end)?, ExprKind::Return(e) => write!(indent, "Return: {e}")?, diff --git a/source/compiler/qsc_fir/src/mut_visit.rs b/source/compiler/qsc_fir/src/mut_visit.rs index 6a4d3bc976a..9ad49fea8ab 100644 --- a/source/compiler/qsc_fir/src/mut_visit.rs +++ b/source/compiler/qsc_fir/src/mut_visit.rs @@ -171,6 +171,12 @@ pub fn walk_expr<'a>(vis: &mut impl MutVisitor<'a>, expr: ExprId) { vis.visit_expr(*array); vis.visit_expr(*index); } + ExprKind::Parallel(limit, expr) => { + if let Some(limit) = limit { + vis.visit_expr(*limit); + } + vis.visit_expr(*expr); + } ExprKind::Return(expr) | ExprKind::UnOp(_, expr) => { vis.visit_expr(*expr); } diff --git a/source/compiler/qsc_fir/src/visit.rs b/source/compiler/qsc_fir/src/visit.rs index 4bc22027d0b..65776c17604 100644 --- a/source/compiler/qsc_fir/src/visit.rs +++ b/source/compiler/qsc_fir/src/visit.rs @@ -169,6 +169,12 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: ExprId) { vis.visit_expr(*array); vis.visit_expr(*index); } + ExprKind::Parallel(limit, expr) => { + if let Some(limit) = limit { + vis.visit_expr(*limit); + } + vis.visit_expr(*expr); + } ExprKind::Return(expr) | ExprKind::UnOp(_, expr) => { vis.visit_expr(*expr); } diff --git a/source/compiler/qsc_fir_transforms/src/cloner.rs b/source/compiler/qsc_fir_transforms/src/cloner.rs index e64bc08066d..d6ddd34ac11 100644 --- a/source/compiler/qsc_fir_transforms/src/cloner.rs +++ b/source/compiler/qsc_fir_transforms/src/cloner.rs @@ -690,6 +690,10 @@ impl FirCloner { self.clone_expr(source, *cond, target), self.clone_block(source, *block, target), ), + ExprKind::Parallel(limit, expr) => ExprKind::Parallel( + limit.map(|e| self.clone_expr(source, e, target)), + self.clone_expr(source, *expr, target), + ), } } @@ -705,6 +709,8 @@ impl FirCloner { ExecGraphNode::Jump(_) | ExecGraphNode::JumpIf(_) | ExecGraphNode::JumpIfNot(_) + | ExecGraphNode::ParStart(_) + | ExecGraphNode::ParEnd | ExecGraphNode::Store | ExecGraphNode::Unit | ExecGraphNode::Ret => node, diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs index 2800cb763b8..3b0846b5f61 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs @@ -2072,6 +2072,24 @@ fn collect_compound_capture_substitutions_into( ); } } + ExprKind::Parallel(limit, body) => { + if let Some(limit_id) = limit { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *limit_id, + substitutions, + ); + } + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *body, + substitutions, + ); + } ExprKind::Assign(..) | ExprKind::AssignOp(..) | ExprKind::AssignField(..) @@ -2177,6 +2195,10 @@ fn compound_literal_has_residual_leak( .into_iter() .flatten() .any(|&part| compound_literal_has_residual_leak(pkg, substitutions, part)), + ExprKind::Parallel(limit, body) => { + limit.is_some_and(|limit| compound_literal_has_residual_leak(pkg, substitutions, limit)) + || compound_literal_has_residual_leak(pkg, substitutions, *body) + } // A non-pure `Call` (operation callee) and every other un-remappable // kind is kept verbatim by the clone, so it leaks if it references any // producer local. @@ -3470,6 +3492,19 @@ fn analyze_expr_flow( } } } + ExprKind::Parallel(limit, expr) => { + if let Some(l) = limit { + analyze_expr_flow(pkg, store, *l, state, package_id, recorder.as_deref_mut()); + } + analyze_expr_flow( + pkg, + store, + *expr, + state, + package_id, + recorder.as_deref_mut(), + ); + } // Leaves: no nested expressions to analyze. ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} } diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs index ff48cf25dd3..21f05eb9af3 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs @@ -1935,6 +1935,12 @@ fn remove_write_only_callable_local_from_expr( remove_write_only_callable_local_from_expr(package, field.value, local_var); } } + ExprKind::Parallel(limit, body) => { + if let Some(limit) = limit { + remove_write_only_callable_local_from_expr(package, limit, local_var); + } + remove_write_only_callable_local_from_expr(package, body, local_var); + } ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} } } @@ -2189,6 +2195,12 @@ fn prune_dead_callable_locals_in_expr(package: &mut Package, expr_id: ExprId) { prune_dead_callable_locals_in_expr(package, cond); prune_dead_callable_locals_in_block(package, block_id); } + ExprKind::Parallel(limit, expr) => { + if let Some(l) = limit { + prune_dead_callable_locals_in_expr(package, l); + } + prune_dead_callable_locals_in_expr(package, expr); + } ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} } } @@ -2282,6 +2294,12 @@ fn remove_dead_callable_local_from_expr( remove_dead_callable_local_from_expr(package, cond, local_var); remove_dead_callable_local_from_block(package, block_id, local_var); } + ExprKind::Parallel(limit, expr) => { + if let Some(l) = limit { + remove_dead_callable_local_from_expr(package, l, local_var); + } + remove_dead_callable_local_from_expr(package, expr, local_var); + } ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} } } diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs index cc15282615e..6ee79fc09f6 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs @@ -667,6 +667,15 @@ fn refresh_expr_types(package: &mut Package, expr_id: ExprId) -> Ty { } expr.ty } + ExprKind::Parallel(limit, body) => { + if let Some(limit) = limit { + // Limit expressions must always be integers, so refresh inner sub-expressions but leave + // the limit itself unchanged. + let _ = refresh_expr_types(package, limit); + } + // The type of a parallel expression is the same as the type of its body, so refresh that and return it. + refresh_expr_types(package, body) + } ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => { expr.ty } @@ -2298,6 +2307,12 @@ fn transform_expr( // place. substitute_forwarded_callable(package, expr_id, concrete, concrete_group, assigner); } + ExprKind::Parallel(limit, body) => { + if let Some(limit) = limit { + refresh_expr_types(package, *limit); + } + refresh_expr_types(package, *body); + } // When a closure captures the callable parameter being specialized, // propagate the specialization into the closure's target callable and // remove the capture. @@ -4242,6 +4257,26 @@ fn rewrite_closure_target_call_args_in_expr( ); } } + ExprKind::Parallel(limit, body) => { + if let Some(limit) = limit { + rewrite_closure_target_call_args_in_expr( + package, + limit, + package_id, + closure_target, + capture_bindings, + assigner, + ); + } + rewrite_closure_target_call_args_in_expr( + package, + body, + package_id, + closure_target, + capture_bindings, + assigner, + ); + } ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} } } @@ -5183,6 +5218,12 @@ fn extract_expr(source: &Package, expr_id: ExprId, target: &mut Package) { ExprKind::Closure(_, target_item) => { extract_item(source, *target_item, target); } + ExprKind::Parallel(limit, body) => { + if let Some(limit) = limit { + extract_expr(source, *limit, target); + } + extract_expr(source, *body, target); + } ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} } } diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests.rs index 05080961905..557dd6b7b59 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests.rs @@ -542,6 +542,171 @@ fn empty_entrypoint_remains_unchanged() { ); } +#[test] +fn closure_inside_parallel_defunctionalizes() { + // A closure passed to a HOF inside a parallel block should be defunctionalized + // the same way it would be outside a parallel block. + let source = indoc::indoc! {" + function Apply(f : Int -> Int, x : Int) : Int { f(x) } + @EntryPoint() + operation Main() : Int { + parallel { + Apply(x -> x + 1, 5) + } + } + "}; + check_invariants(source); + check_rewrite( + source, + &expect![[r#" + BEFORE: + function Apply(f : (Int -> Int), x : Int) : Int { + f(x) + } + operation Main() : Int { + parallel { + Apply(/ * closure item = 3 captures = [] * / _lambda_3, 5) + } + + } + function _lambda_3(x : Int, ) : Int { + x + 1 + } + // entry + Main() + + AFTER: + function Apply(f : (Int -> Int), x : Int) : Int { + f(x) + } + operation Main() : Int { + parallel { + Apply_closure_(5) + } + + } + function _lambda_3(x : Int, ) : Int { + x + 1 + } + function Apply_closure_(x : Int) : Int { + _lambda_3(x, ) + } + // entry + Main() + "#]], + ); +} + +#[test] +fn closure_inside_parallel_within_limit_defunctionalizes() { + // A closure passed to a HOF inside a parallel-within-limit block should also + // be defunctionalized normally. + let source = indoc::indoc! {" + function Apply(f : Int -> Int, x : Int) : Int { f(x) } + @EntryPoint() + operation Main() : Int { + parallel within 4 { + Apply(x -> x + 1, 5) + } + } + "}; + check_invariants(source); + check_rewrite( + source, + &expect![[r#" + BEFORE: + function Apply(f : (Int -> Int), x : Int) : Int { + f(x) + } + operation Main() : Int { + parallel within 4 { + Apply(/ * closure item = 3 captures = [] * / _lambda_3, 5) + } + + } + function _lambda_3(x : Int, ) : Int { + x + 1 + } + // entry + Main() + + AFTER: + function Apply(f : (Int -> Int), x : Int) : Int { + f(x) + } + operation Main() : Int { + parallel within 4 { + Apply_closure_(5) + } + + } + function _lambda_3(x : Int, ) : Int { + x + 1 + } + function Apply_closure_(x : Int) : Int { + _lambda_3(x, ) + } + // entry + Main() + "#]], + ); +} + +#[test] +fn closure_inside_parallel_within_limit_expr_defunctionalizes() { + // A closure passed to a HOF inside the *limit* expression of a parallel-within + // should be defunctionalized the same way as in the body. + let source = indoc::indoc! {" + function Apply(f : Int -> Int, x : Int) : Int { f(x) } + @EntryPoint() + operation Main() : Int { + parallel within Apply(x -> x + 1, 3) { + 42 + } + } + "}; + check_invariants(source); + check_rewrite( + source, + &expect![[r#" + BEFORE: + function Apply(f : (Int -> Int), x : Int) : Int { + f(x) + } + operation Main() : Int { + parallel within Apply(/ * closure item = 3 captures = [] * / _lambda_3, 3) { + 42 + } + + } + function _lambda_3(x : Int, ) : Int { + x + 1 + } + // entry + Main() + + AFTER: + function Apply(f : (Int -> Int), x : Int) : Int { + f(x) + } + operation Main() : Int { + parallel within Apply_closure_(3) { + 42 + } + + } + function _lambda_3(x : Int, ) : Int { + x + 1 + } + function Apply_closure_(x : Int) : Int { + _lambda_3(x, ) + } + // entry + Main() + "#]], + ); +} + #[test] fn test_helpers_surface_defunctionalization_errors() { let source = r#" diff --git a/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild.rs b/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild.rs index 773f3f017d9..d6d7be5b5c0 100644 --- a/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild.rs +++ b/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild.rs @@ -599,6 +599,15 @@ fn rebuild_expr( builder.push(ExecGraphNode::Expr(expr_id)); } + ExprKind::Parallel(limit, body) => { + if let Some(limit) = limit { + rebuild_expr(package, builder, limit, ranges); + } + builder.push(ExecGraphNode::ParStart(limit.is_some().into())); + rebuild_expr(package, builder, body, ranges); + builder.push(ExecGraphNode::ParEnd); + } + // Eliminated variant // // `ExprKind::Struct` must be unreachable here: the UDT erasure pass diff --git a/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild/tests.rs b/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild/tests.rs index de76ee7383a..7f8d256fd4a 100644 --- a/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild/tests.rs @@ -223,6 +223,8 @@ fn format_exec_graph_nodes( ExecGraphNode::Ret => format!("{index}: Ret"), ExecGraphNode::Store => format!("{index}: Store"), ExecGraphNode::Unit => format!("{index}: Unit"), + ExecGraphNode::ParStart(kind) => format!("{index}: ParStart({kind})"), + ExecGraphNode::ParEnd => format!("{index}: ParEnd"), ExecGraphNode::Debug(_) => { unreachable!("NoDebug exec graph should not contain debug nodes") } @@ -1183,3 +1185,64 @@ fn residual_hole_in_rebuilt_body_panics() { }, ); } + +#[test] +fn parallel_without_limit_emits_par_start_and_end() { + check_exec_graph( + indoc! {" + struct Pair { Fst: Int, Snd: Int } + @EntryPoint() + operation Main() : Int { + let p = new Pair { Fst = 1, Snd = 2 }; + parallel { + p.Fst + p.Snd + } + } + "}, + &expect![[r#" + 0: Expr(ExprId(4)) [Lit(Int(1))] + 1: Store + 2: Expr(ExprId(5)) [Lit(Int(2))] + 3: Store + 4: Expr(ExprId(3)) [Tuple(len=2)] + 5: Bind(PatId(1)) + 6: ParStart(Unlimited) + 7: Expr(ExprId(13)) [Var] + 8: Store + 9: Expr(ExprId(14)) [Var] + 10: Expr(ExprId(8)) [BinOp(Add)] + 11: ParEnd + 12: Ret"#]], + ); +} + +#[test] +fn parallel_within_limit_emits_par_start_with_limit() { + check_exec_graph( + indoc! {" + struct Pair { Fst: Int, Snd: Int } + @EntryPoint() + operation Main() : Int { + let p = new Pair { Fst = 3, Snd = 4 }; + parallel within p.Fst { + p.Fst + p.Snd + } + } + "}, + &expect![[r#" + 0: Expr(ExprId(4)) [Lit(Int(3))] + 1: Store + 2: Expr(ExprId(5)) [Lit(Int(4))] + 3: Store + 4: Expr(ExprId(3)) [Tuple(len=2)] + 5: Bind(PatId(1)) + 6: Expr(ExprId(15)) [Var] + 7: ParStart(Limited) + 8: Expr(ExprId(16)) [Var] + 9: Store + 10: Expr(ExprId(17)) [Var] + 11: Expr(ExprId(10)) [BinOp(Add)] + 12: ParEnd + 13: Ret"#]], + ); +} diff --git a/source/compiler/qsc_fir_transforms/src/invariants.rs b/source/compiler/qsc_fir_transforms/src/invariants.rs index 247e8dbc79c..a36b89d1c60 100644 --- a/source/compiler/qsc_fir_transforms/src/invariants.rs +++ b/source/compiler/qsc_fir_transforms/src/invariants.rs @@ -877,6 +877,12 @@ fn check_expr_sub_ids(package: &Package, parent_expr: ExprId, kind: &ExprKind) { assert_expr(*cond); assert_block(*block); } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + assert_expr(*l); + } + assert_expr(*body); + } ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} } } @@ -1314,6 +1320,12 @@ fn scan_expr_for_operand_flag_writes( } } } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + scan_expr_for_operand_flag_writes(package, *l, true, flag_locals, callable_name); + } + scan_expr_for_operand_flag_writes(package, *body, true, flag_locals, callable_name); + } ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} } } @@ -2154,7 +2166,11 @@ fn check_configured_exec_graph( | ExecGraphDebugNode::RetFrame | ExecGraphDebugNode::LoopIteration => {} }, - ExecGraphNode::Store | ExecGraphNode::Unit | ExecGraphNode::Ret => {} + ExecGraphNode::Store + | ExecGraphNode::Unit + | ExecGraphNode::ParStart(_) + | ExecGraphNode::ParEnd + | ExecGraphNode::Ret => {} } } } diff --git a/source/compiler/qsc_fir_transforms/src/monomorphize.rs b/source/compiler/qsc_fir_transforms/src/monomorphize.rs index 4ea7829fe57..225b7e96ef3 100644 --- a/source/compiler/qsc_fir_transforms/src/monomorphize.rs +++ b/source/compiler/qsc_fir_transforms/src/monomorphize.rs @@ -1193,6 +1193,12 @@ fn extract_expr(source: &Package, expr_id: ExprId, target: &mut Package) { extract_expr(source, *cond, target); extract_block(source, *block, target); } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + extract_expr(source, *l, target); + } + extract_expr(source, *body, target); + } ExprKind::Closure(_, local_item_id) => { extract_item(source, *local_item_id, target); } diff --git a/source/compiler/qsc_fir_transforms/src/monomorphize/tests.rs b/source/compiler/qsc_fir_transforms/src/monomorphize/tests.rs index 53535b39083..8e65dd30c30 100644 --- a/source/compiler/qsc_fir_transforms/src/monomorphize/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/monomorphize/tests.rs @@ -2541,3 +2541,95 @@ fn no_generic_callable_reachable_after_full_pipeline() { } } } + +#[test] +fn mono_generic_inside_parallel() { + let source = indoc! {r#" + operation Identity<'T>(x : 'T) : 'T { x } + operation Main() : Int { + parallel { + Identity(42) + } + } + "#}; + check( + source, + &expect![[r#" + Identity: generics=1, input=Param<0>, output=Param<0> + Identity: generics=0, input=Int, output=Int + Main: generics=0, input=Unit, output=Int"#]], + ); +} + +#[test] +fn mono_generic_inside_parallel_within_limit() { + let source = indoc! {r#" + operation Identity<'T>(x : 'T) : 'T { x } + operation Main() : Int { + parallel within 2 { + Identity(42) + } + } + "#}; + check( + source, + &expect![[r#" + Identity: generics=1, input=Param<0>, output=Param<0> + Identity: generics=0, input=Int, output=Int + Main: generics=0, input=Unit, output=Int"#]], + ); +} + +#[test] +fn mono_generic_call_as_parallel_within_limit_expr() { + // A generic callable used in the limit position of `parallel within` + // should be monomorphized correctly. + let source = indoc! {r#" + function ToInt<'T>(x : 'T) : Int { 4 } + operation Main() : Int { + parallel within ToInt(true) { + 42 + } + } + "#}; + check( + source, + &expect![[r#" + Main: generics=0, input=Unit, output=Int + ToInt: generics=1, input=Param<0>, output=Int + ToInt: generics=0, input=Bool, output=Int"#]], + ); + check_before_after( + source, + &expect![[r#" + BEFORE: + function ToInt(x : 'T0) : Int { + 4 + } + operation Main() : Int { + parallel within ToInt < Bool > (true) { + 42 + } + + } + // entry + Main() + + AFTER: + function ToInt(x : 'T0) : Int { + 4 + } + operation Main() : Int { + parallel within ToInt_Bool_(true) { + 42 + } + + } + function ToInt_Bool_(x : Bool) : Int { + 4 + } + // entry + Main() + "#]], + ); +} diff --git a/source/compiler/qsc_fir_transforms/src/pretty.rs b/source/compiler/qsc_fir_transforms/src/pretty.rs index 53ce3b822d8..26de4f66db0 100644 --- a/source/compiler/qsc_fir_transforms/src/pretty.rs +++ b/source/compiler/qsc_fir_transforms/src/pretty.rs @@ -431,6 +431,12 @@ impl<'a> FirQSharpGen<'a> { self.collect_expr_local_names(*cond, local_names); self.collect_block_local_names(*block, local_names); } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + self.collect_expr_local_names(*l, local_names); + } + self.collect_expr_local_names(*body, local_names); + } } } @@ -744,6 +750,15 @@ impl<'a> FirQSharpGen<'a> { self.emit_expr(*cond); self.emit_block(*block); } + ExprKind::Parallel(limit, body) => { + self.write("parallel "); + if let Some(l) = limit { + self.write("within "); + self.emit_expr(*l); + self.write(" "); + } + self.emit_expr(*body); + } } } diff --git a/source/compiler/qsc_fir_transforms/src/pretty/tests.rs b/source/compiler/qsc_fir_transforms/src/pretty/tests.rs index f112d897308..7238eafc59e 100644 --- a/source/compiler/qsc_fir_transforms/src/pretty/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/pretty/tests.rs @@ -349,3 +349,65 @@ fn ty_rendering_handles_param_and_arrow_with_functors() { })); assert_eq!(ty_as_qsharp(&func), "(Int -> Int)"); } + +#[test] +fn parallel_expression_renders() { + check_render( + indoc! {r#" + namespace Test { + @EntryPoint() + operation Main() : Unit { + parallel { + use q = Qubit(); + H(q); + } + } + } + "#}, + &expect![[r#" + operation Main() : Unit { + body { + parallel { + let q : Qubit = __quantum__rt__qubit_allocate(); + H(q); + __quantum__rt__qubit_release(q); + } + + } + } + // entry + Main() + "#]], + ); +} + +#[test] +fn parallel_within_limit_renders() { + check_render( + indoc! {r#" + namespace Test { + @EntryPoint() + operation Main() : Unit { + parallel within 4 { + use q = Qubit(); + H(q); + } + } + } + "#}, + &expect![[r#" + operation Main() : Unit { + body { + parallel within 4 { + let q : Qubit = __quantum__rt__qubit_allocate(); + H(q); + __quantum__rt__qubit_release(q); + } + + } + } + // entry + Main() + "#]], + ); +} diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/lower.rs b/source/compiler/qsc_fir_transforms/src/return_unify/lower.rs index e8d9013cac1..767009e1462 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/lower.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/lower.rs @@ -879,6 +879,12 @@ fn transform_while_in_expr( transform_while_in_child(package, assigner, e, flag_context, arrow_default_cache); } } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + transform_while_in_child(package, assigner, *l, flag_context, arrow_default_cache); + } + transform_while_in_child(package, assigner, *body, flag_context, arrow_default_cache); + } ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} } } @@ -1122,6 +1128,12 @@ fn replace_returns_in_expr( replace_returns_in_expr(package, assigner, e, flag_context, arrow_default_cache); } } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + replace_returns_in_expr(package, assigner, *l, flag_context, arrow_default_cache); + } + replace_returns_in_expr(package, assigner, *body, flag_context, arrow_default_cache); + } ExprKind::While(cond, body) => { let (cond_id, body_id) = (*cond, *body); if contains_return_in_block(package, body_id) diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/normalize.rs b/source/compiler/qsc_fir_transforms/src/return_unify/normalize.rs index 3d678e4d1fc..7b757f96090 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/normalize.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/normalize.rs @@ -187,6 +187,11 @@ fn count_compound_returns_in_expr(package: &Package, expr_id: ExprId) -> usize { StringComponent::Lit(_) => 0, }) .sum(), + ExprKind::Parallel(limit, body) => { + let limit_count = limit.map_or(0, |l| count_compound_returns_in_expr(package, l)); + let body_count = count_compound_returns_in_expr(package, *body); + limit_count + body_count + } } } @@ -344,6 +349,12 @@ pub(super) fn visit_expr_for_collect( visit_expr_for_collect(package, cond, out, seen); visit_block_for_collect(package, block, out, seen); } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + visit_expr_for_collect(package, l, out, seen); + } + visit_expr_for_collect(package, body, out, seen); + } } } @@ -595,6 +606,16 @@ fn hoist_in_expr( .collect(); hoist_n_ary(package, assigner, package_id, &operands) } + + // Parallel expression with optional limit. + ExprKind::Parallel(limit, body) => { + let mut operands: Vec = Vec::with_capacity(2); + if let Some(l) = limit { + operands.push(l); + } + operands.push(body); + hoist_n_ary(package, assigner, package_id, &operands) + } } } diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/normalize/anf.rs b/source/compiler/qsc_fir_transforms/src/return_unify/normalize/anf.rs index 4292cc4b6f8..a8b060e36de 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/normalize/anf.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/normalize/anf.rs @@ -350,6 +350,11 @@ fn count_operand_returns_in_expr(package: &Package, expr_id: ExprId, in_operand: StringComponent::Lit(_) => 0, }) .sum(), + ExprKind::Parallel(limit, body) => { + let limit_count = limit.map_or(0, |l| count_operand_returns_in_expr(package, l, true)); + let body_count = count_operand_returns_in_expr(package, *body, true); + limit_count + body_count + } ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => 0, } } @@ -482,6 +487,12 @@ fn scan_operand_tree_for_unsupported_lifts( } } } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + check_operand_for_unsupported_lift(package, package_id, l, rejected); + } + check_operand_for_unsupported_lift(package, package_id, body, rejected); + } // Leaves and statement-carrying constructs are not operand sites here. ExprKind::Block(_) | ExprKind::If(_, _, _) @@ -687,6 +698,16 @@ pub(super) fn anf_lift_in_expr( temp_counter, ) } + // Parallel expression with a limit, where we need to hoist the limit since it may + // contain a return and need to short-circuit the body evaluation. + ExprKind::Parallel(Some(limit), _) => anf_lift_operands( + package, + assigner, + package_id, + expr_id, + &[limit], + temp_counter, + ), // An `If` condition is an unconditional operand site: it is evaluated // in full before either branch, so a `Return` buried there fires // before the `If` chooses a branch. Treat the condition as the `If`'s @@ -723,6 +744,7 @@ pub(super) fn anf_lift_in_expr( | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Return(_) + | ExprKind::Parallel(None, _) | ExprKind::Var(_, _) => None, } } @@ -1101,6 +1123,19 @@ fn replace_operand_slot(package: &mut Package, parent_id: ExprId, old_id: ExprId } unreachable!("operand slot not found in string parent"); } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit + && *l == old_id + { + *limit = Some(new_id); + return; + } + if *body == old_id { + *body = new_id; + return; + } + unreachable!("operand slot not found in parallel parent"); + } // Only the condition is an unconditional ANF operand site. ExprKind::If(cond, _, _) | ExprKind::While(cond, _) => { debug_assert_eq!(*cond, old_id); diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/simplify.rs b/source/compiler/qsc_fir_transforms/src/return_unify/simplify.rs index 32a7ac1a203..d9bebd140df 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/simplify.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/simplify.rs @@ -723,6 +723,12 @@ pub(super) fn push_children(package: &Package, expr_id: ExprId, stack: &mut Vec< } stack.extend(fields.iter().map(|f| f.value)); } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + stack.push(*l); + } + stack.push(*body); + } ExprKind::Closure(_, _) | ExprKind::Var(_, _) | ExprKind::Lit(_) | ExprKind::Hole => {} } } @@ -821,6 +827,12 @@ pub(super) fn local_use_count(package: &Package, root: ExprId, target: LocalVarI } stack.extend(fields.iter().map(|f| f.value)); } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + stack.push(*l); + } + stack.push(*body); + } ExprKind::Lit(_) | ExprKind::Hole | ExprKind::Var(_, _) => {} } } diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/tests/general.rs b/source/compiler/qsc_fir_transforms/src/return_unify/tests/general.rs index f388733f97d..fe2f8ec2e4a 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/tests/general.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/tests/general.rs @@ -1320,3 +1320,277 @@ fn simple_if_expr_init_with_return_recovers_structured_branch() { "#]], ); } + +#[test] +fn parallel_body_without_returns_passes_through() { + // A parallel block without any return statements should pass through unchanged. + check_no_returns_q( + indoc! {r#" + namespace Test { + @EntryPoint() + operation Main() : Int { + parallel { + let x = 1; + x + 2 + } + } + } + "#}, + &expect![[r#" + operation Main() : Int { + parallel { + let x : Int = 1; + x + 2 + } + + } + // entry + Main() + "#]], + ); +} + +#[test] +fn parallel_within_limit_without_returns_passes_through() { + // A parallel-within-limit block without any return statements should pass through unchanged. + check_no_returns_q( + indoc! {r#" + namespace Test { + @EntryPoint() + operation Main() : Int { + parallel within 4 { + let x = 1; + x + 2 + } + } + } + "#}, + &expect![[r#" + operation Main() : Int { + parallel within 4 { + let x : Int = 1; + x + 2 + } + + } + // entry + Main() + "#]], + ); +} + +#[test] +fn return_inside_parallel_body_is_unified() { + // A return statement inside a parallel body should be unified using + // the flag/slot pattern, with the parallel expression preserved. + check_no_returns_q( + indoc! {r#" + namespace Test { + @EntryPoint() + function Main() : Int { + parallel { + if true { + return 42; + } + 0 + } + } + } + "#}, + &expect![[r#" + function Main() : Int { + mutable __has_returned : Bool = false; + mutable __ret_val : Int = 0; + let __trailing_result : Int = parallel { + if true { + { + __ret_val = 42; + __has_returned = true; + }; + } + + 0 + }; + if __has_returned { + __ret_val + } else { + __trailing_result + } + } + // entry + Main() + "#]], + ); +} + +#[test] +fn return_inside_parallel_body_without_explicit_block_is_unified() { + check_no_returns_q( + indoc! {r#" + namespace Test { + @EntryPoint() + function Main() : Int { + parallel (1 + return 42); + } + } + "#}, + &expect![[r#" + function Main() : Int { + mutable __has_returned : Bool = false; + mutable __ret_val : Int = 0; + parallel { + let _ : Int = 1; + { + __ret_val = 42; + __has_returned = true; + }; + }; + __ret_val + } + // entry + Main() + "#]], + ); +} + +#[test] +fn return_inside_parallel_body_with_remaining_code() { + // A return statement inside a parallel body with code after the parallel + // expression exercises the flag-guarding of remaining statements. + check_no_returns_q( + indoc! {r#" + namespace Test { + @EntryPoint() + function Main() : Int { + let y = parallel { + if true { + return 42; + } + 0 + }; + y + 1 + } + } + "#}, + &expect![[r#" + function Main() : Int { + mutable __has_returned : Bool = false; + mutable __ret_val : Int = 0; + let y : Int = parallel { + if true { + { + __ret_val = 42; + __has_returned = true; + }; + } + + 0 + }; + if __has_returned { + __ret_val + } else { + if not __has_returned { + y + 1 + } else { + __ret_val + } + } + + } + // entry + Main() + "#]], + ); +} + +#[test] +fn return_inside_parallel_within_limit_body() { + // A return inside the body of a parallel-within-limit expression should be unified. + check_no_returns_q( + indoc! {r#" + namespace Test { + @EntryPoint() + function Main() : Int { + parallel within 4 { + if true { + return 99; + } + 0 + } + } + } + "#}, + &expect![[r#" + function Main() : Int { + mutable __has_returned : Bool = false; + mutable __ret_val : Int = 0; + let __trailing_result : Int = parallel within 4 { + if true { + { + __ret_val = 99; + __has_returned = true; + }; + } + + 0 + }; + if __has_returned { + __ret_val + } else { + __trailing_result + } + } + // entry + Main() + "#]], + ); +} + +#[test] +fn return_inside_parallel_within_limit_expr() { + // A return inside the limit expression of a parallel-within exercises + // return unification in the limit operand position. + check_no_returns_q( + indoc! {r#" + namespace Test { + @EntryPoint() + function Main() : Int { + parallel within (if true { return 10; } else { 4 }) { + let x = 1; + x + 2 + } + } + } + "#}, + &expect![[r#" + function Main() : Int { + mutable __has_returned : Bool = false; + mutable __ret_val : Int = 0; + let __operand_tmp_0 : Int = if true { + { + __ret_val = 10; + __has_returned = true; + }; + } else { + 4 + }; + if __has_returned { + __ret_val + } else { + if not __has_returned { + parallel within __operand_tmp_0 { + let x : Int = 1; + x + 2 + } + + } else { + __ret_val + } + } + + } + // entry + Main() + "#]], + ); +} diff --git a/source/compiler/qsc_fir_transforms/src/test_utils.rs b/source/compiler/qsc_fir_transforms/src/test_utils.rs index 16deaf5a716..16e2fafe874 100644 --- a/source/compiler/qsc_fir_transforms/src/test_utils.rs +++ b/source/compiler/qsc_fir_transforms/src/test_utils.rs @@ -796,6 +796,7 @@ pub fn expr_kind_short(package: &Package, expr_id: ExprId) -> String { ExprKind::UpdateIndex(_, _, _) => "UpdateIndex".to_string(), ExprKind::Var(_, _) => "Var".to_string(), ExprKind::While(_, _) => "While".to_string(), + ExprKind::Parallel(_, _) => "Parallel".to_string(), } } diff --git a/source/compiler/qsc_fir_transforms/src/tuple_decompose.rs b/source/compiler/qsc_fir_transforms/src/tuple_decompose.rs index b95d0598f2f..3d5db6958b9 100644 --- a/source/compiler/qsc_fir_transforms/src/tuple_decompose.rs +++ b/source/compiler/qsc_fir_transforms/src/tuple_decompose.rs @@ -580,6 +580,12 @@ fn replace_expr_in_expr(expr: &mut Expr, old_expr_id: ExprId, new_expr_id: ExprI ExprKind::While(cond, _) => { replace_expr_id(cond, old_expr_id, new_expr_id); } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + replace_expr_id(l, old_expr_id, new_expr_id); + } + replace_expr_id(body, old_expr_id, new_expr_id); + } // No direct child `ExprId` edges to patch. This function only rewrites // the `ExprId`s a node stores in its own `ExprKind`; it does not recurse // (the caller `replace_expr_references` visits every expr and every diff --git a/source/compiler/qsc_fir_transforms/src/tuple_decompose/tests.rs b/source/compiler/qsc_fir_transforms/src/tuple_decompose/tests.rs index e764d79fe6d..0cc32270def 100644 --- a/source/compiler/qsc_fir_transforms/src/tuple_decompose/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/tuple_decompose/tests.rs @@ -1169,6 +1169,12 @@ fn collect_local_patterns_in_expr( collect_local_patterns_in_expr(package, *cond, patterns); collect_local_patterns_in_block(package, *block, patterns); } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + collect_local_patterns_in_expr(package, *l, patterns); + } + collect_local_patterns_in_expr(package, *body, patterns); + } } } @@ -2067,3 +2073,61 @@ fn cross_package_controlled_library_struct_local_decomposed() { crate::test_utils::check_semantic_equivalence_with_library(lib_source, user_source); } + +#[test] +fn tuple_inside_parallel_decomposes() { + // Tuple destructuring inside a parallel block should behave the same as + // outside a parallel block. + check( + indoc! {" + function MakePair(a: Int, b: Int) : (Int, Int) { (a, b) } + @EntryPoint() + operation Main() : Int { + parallel { + let (x, y) = MakePair(3, 4); + x + y + } + } + "}, + &expect![[r#" + Callable Main: input=Tuple() + Callable MakePair: input=Tuple(Bind(a: Int), Bind(b: Int))"#]], + ); +} + +#[test] +fn tuple_inside_parallel_within_limit_decomposes() { + // Tuple destructuring inside a parallel-within-limit block should also decompose. + check( + indoc! {" + function MakePair(a: Int, b: Int) : (Int, Int) { (a, b) } + @EntryPoint() + operation Main() : Int { + parallel within 2 { + let (x, y) = MakePair(3, 4); + x + y + } + } + "}, + &expect![[r#" + Callable Main: input=Tuple() + Callable MakePair: input=Tuple(Bind(a: Int), Bind(b: Int))"#]], + ); +} + +#[test] +fn tuple_inside_parallel_within_limit_expr_decomposes() { + // Tuple used inside the limit expression of parallel-within should also decompose. + check( + indoc! {" + struct Pair { Fst : Int, Snd : Int } + @EntryPoint() + operation Main() : Int { + parallel within { let p = new Pair { Fst = 2, Snd = 3 }; p.Fst } { + 42 + } + } + "}, + &expect!["Callable Main: input=Tuple()"], + ); +} diff --git a/source/compiler/qsc_fir_transforms/src/walk_utils.rs b/source/compiler/qsc_fir_transforms/src/walk_utils.rs index a185d4c2f2a..f5649e6d3cc 100644 --- a/source/compiler/qsc_fir_transforms/src/walk_utils.rs +++ b/source/compiler/qsc_fir_transforms/src/walk_utils.rs @@ -236,6 +236,12 @@ pub(crate) fn for_each_direct_child(kind: &ExprKind, mut visit(DirectChild::Expr(*cond)); visit(DirectChild::Block(*block)); } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + visit(DirectChild::Expr(*l)); + } + visit(DirectChild::Expr(*body)); + } } } @@ -641,6 +647,12 @@ fn for_each_use_event( for_each_use_event(package, fa.value, local_id, false, visit); } } + ExprKind::Parallel(limit, body) => { + if let Some(l) = limit { + for_each_use_event(package, *l, local_id, false, visit); + } + for_each_use_event(package, *body, local_id, false, visit); + } ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} } } @@ -880,7 +892,15 @@ fn expr_has_purity( scope, active_callables, ), + ExprKind::Parallel(limit, body) => { + (if let Some(l) = limit { + expr_has_purity(package, package_id, *l, mode, scope, active_callables) + } else { + true + }) && expr_has_purity(package, package_id, *body, mode, scope, active_callables) + } // Effectful or caller-control-flow-changing variants. `If` without an + // Side-effecting or potentially-trapping variants. `If` without an // else arm is included here: it has `Unit` type but its `then` branch // may run for effect. No wildcard arm — a new `ExprKind` variant breaks // the build here so its purity is decided explicitly. diff --git a/source/compiler/qsc_fir_transforms/src/walk_utils/tests.rs b/source/compiler/qsc_fir_transforms/src/walk_utils/tests.rs index 8974276759e..496bd1562fb 100644 --- a/source/compiler/qsc_fir_transforms/src/walk_utils/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/walk_utils/tests.rs @@ -223,6 +223,7 @@ fn walker_visits_nested_expression_kinds_in_program() { ExprKind::UpdateField(_, _, _) => "UpdateField", ExprKind::Var(_, _) => "Var", ExprKind::While(_, _) => "While", + ExprKind::Parallel(_, _) => "Parallel", }; kinds.push(kind_str.to_string()); }); @@ -1189,3 +1190,129 @@ fn structural_walker_from_expr_root_covers_nested_blocks() { "expected the nested tuple Local pat to be visited, got {tuple_pats}" ); } + +#[test] +fn given_parallel_of_lit_is_side_effect_free() { + // A parallel expression whose body is a pure literal is side-effect free. + let mut package = Package::default(); + let mut assigner = Assigner::default(); + let body = int_lit(&mut package, &mut assigner, 42); + let e = alloc_expr( + &mut package, + &mut assigner, + Ty::Prim(Prim::Int), + ExprKind::Parallel(None, body), + Span::default(), + ); + assert!(expr_is_side_effect_free(&package, PackageId::CORE, e)); +} + +#[test] +fn given_parallel_with_limit_of_lits_is_side_effect_free() { + // A parallel-within-limit expression where both limit and body are pure is side-effect free. + let mut package = Package::default(); + let mut assigner = Assigner::default(); + let limit = int_lit(&mut package, &mut assigner, 4); + let body = int_lit(&mut package, &mut assigner, 42); + let e = alloc_expr( + &mut package, + &mut assigner, + Ty::Prim(Prim::Int), + ExprKind::Parallel(Some(limit), body), + Span::default(), + ); + assert!(expr_is_side_effect_free(&package, PackageId::CORE, e)); +} + +#[test] +fn given_parallel_with_fail_in_body_is_not_side_effect_free() { + // A parallel expression with a failing body is not side-effect free. + let mut package = Package::default(); + let mut assigner = Assigner::default(); + let fail_expr = alloc_expr( + &mut package, + &mut assigner, + Ty::Prim(Prim::String), + ExprKind::String(vec![StringComponent::Lit(Rc::from("boom"))]), + Span::default(), + ); + let body = alloc_expr( + &mut package, + &mut assigner, + Ty::UNIT, + ExprKind::Fail(fail_expr), + Span::default(), + ); + let e = alloc_expr( + &mut package, + &mut assigner, + Ty::Prim(Prim::Int), + ExprKind::Parallel(None, body), + Span::default(), + ); + assert!(!expr_is_side_effect_free(&package, PackageId::CORE, e)); +} + +#[test] +fn given_parallel_with_fail_in_limit_is_not_side_effect_free() { + // A parallel-within-limit expression where the limit is a fail expr is not side-effect free. + let mut package = Package::default(); + let mut assigner = Assigner::default(); + let fail_expr = alloc_expr( + &mut package, + &mut assigner, + Ty::Prim(Prim::String), + ExprKind::String(vec![StringComponent::Lit(Rc::from("boom"))]), + Span::default(), + ); + let limit = alloc_expr( + &mut package, + &mut assigner, + Ty::Prim(Prim::Int), + ExprKind::Fail(fail_expr), + Span::default(), + ); + let body = int_lit(&mut package, &mut assigner, 42); + let e = alloc_expr( + &mut package, + &mut assigner, + Ty::Prim(Prim::Int), + ExprKind::Parallel(Some(limit), body), + Span::default(), + ); + assert!(!expr_is_side_effect_free(&package, PackageId::CORE, e)); +} + +#[test] +fn walker_visits_parallel_expression() { + let (store, pkg_id) = compile_to_fir( + "operation Main() : Int { + parallel { + 1 + 2 + } + }", + ); + let package = store.get(pkg_id); + let block_id = find_callable_block(package, "Main"); + + let mut kinds: Vec = Vec::new(); + for_each_expr_in_block(package, block_id, &mut |_id, expr| { + let kind_str = match &expr.kind { + ExprKind::Parallel(_, _) => "Parallel", + ExprKind::BinOp(_, _, _) => "BinOp", + ExprKind::Lit(_) => "Lit", + ExprKind::Block(_) => "Block", + _ => "Other", + }; + kinds.push(kind_str.to_string()); + }); + // The walker should visit the Parallel expression and its nested children. + assert!( + kinds.contains(&"Parallel".to_string()), + "walker should visit Parallel expression; found: {kinds:?}" + ); + assert!( + kinds.contains(&"BinOp".to_string()), + "walker should visit BinOp inside Parallel body; found: {kinds:?}" + ); +} diff --git a/source/compiler/qsc_frontend/src/lower.rs b/source/compiler/qsc_frontend/src/lower.rs index 1113067d2ef..02fc3a02bc5 100644 --- a/source/compiler/qsc_frontend/src/lower.rs +++ b/source/compiler/qsc_frontend/src/lower.rs @@ -799,6 +799,11 @@ impl With<'_> { self.lower_lambda(lambda, expr.span) } ast::ExprKind::Lit(lit) => self.lower_lit(lit), + ast::ExprKind::Parallel(limit, body) => { + let limit = limit.as_ref().map(|limit| Box::new(self.lower_expr(limit))); + let body = self.lower_expr(body); + self.lower_parallel(limit, body) + } ast::ExprKind::Paren(_) => unreachable!("parentheses should be removed earlier"), ast::ExprKind::Path(PathKind::Ok(path)) => { let args = self @@ -1180,6 +1185,34 @@ impl With<'_> { } } } + + fn lower_parallel(&mut self, limit: Option>, expr: hir::Expr) -> hir::ExprKind { + // We lower the target expression of `parallel` into a block if it isn't already one. + // This gives it a convenient scope for later transformations. + let expr = if matches!(expr.kind, hir::ExprKind::Block(_)) { + expr + } else { + let ty = expr.ty.clone(); + let stmt = hir::Stmt { + id: self.assigner.next_node(), + span: Span::default(), + kind: hir::StmtKind::Expr(expr), + }; + let block = hir::Block { + id: self.assigner.next_node(), + span: Span::default(), + ty: ty.clone(), + stmts: vec![stmt], + }; + hir::Expr { + id: self.assigner.next_node(), + span: Span::default(), + ty, + kind: hir::ExprKind::Block(block), + } + }; + hir::ExprKind::Parallel(limit, Box::new(expr)) + } } /// Removes all self-export items, and makes the corresponding item declarations public. diff --git a/source/compiler/qsc_frontend/src/lower/tests.rs b/source/compiler/qsc_frontend/src/lower/tests.rs index 1237d6c105e..5a3d962d113 100644 --- a/source/compiler/qsc_frontend/src/lower/tests.rs +++ b/source/compiler/qsc_frontend/src/lower/tests.rs @@ -3058,3 +3058,125 @@ fn literal_complex_lowers_as_struct_decl() { ctl-adj: "#]], ); } + +#[test] +fn parallel_without_block_lowers_into_block() { + check_hir( + indoc! {" + operation Main() : Unit { + parallel true; + } + "}, + &expect![[r#" + Package: + Item 0 [0-46] (Public): + Namespace (Ident 11 [0-46] "test"): Item 1 + Item 1 [0-46] (Internal): + Parent: 0 + Callable 0 [0-46] (operation): + name: Ident 1 [10-14] "Main" + input: Pat 2 [14-16] [Type Unit]: Unit + output: Unit + functors: empty set + body: SpecDecl 3 [0-46]: Impl: + Block 4 [24-46] [Type Unit]: + Stmt 5 [30-44]: Semi: Expr 6 [30-43] [Type Bool]: Parallel: + Body: Expr 10 [0-0] [Type Bool]: Expr Block: Block 9 [0-0] [Type Bool]: + Stmt 8 [0-0]: Expr: Expr 7 [39-43] [Type Bool]: Lit: Bool(true) + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn parallel_with_block_lowers_into_existing_block() { + check_hir( + indoc! {" + operation Main() : Unit { + parallel {true}; + } + "}, + &expect![[r#" + Package: + Item 0 [0-48] (Public): + Namespace (Ident 11 [0-48] "test"): Item 1 + Item 1 [0-48] (Internal): + Parent: 0 + Callable 0 [0-48] (operation): + name: Ident 1 [10-14] "Main" + input: Pat 2 [14-16] [Type Unit]: Unit + output: Unit + functors: empty set + body: SpecDecl 3 [0-48]: Impl: + Block 4 [24-48] [Type Unit]: + Stmt 5 [30-46]: Semi: Expr 6 [30-45] [Type Bool]: Parallel: + Body: Expr 7 [39-45] [Type Bool]: Expr Block: Block 8 [39-45] [Type Bool]: + Stmt 9 [40-44]: Expr: Expr 10 [40-44] [Type Bool]: Lit: Bool(true) + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn parallel_limited_without_block_lowers_into_block() { + check_hir( + indoc! {" + operation Main() : Unit { + parallel within 3 true; + } + "}, + &expect![[r#" + Package: + Item 0 [0-55] (Public): + Namespace (Ident 12 [0-55] "test"): Item 1 + Item 1 [0-55] (Internal): + Parent: 0 + Callable 0 [0-55] (operation): + name: Ident 1 [10-14] "Main" + input: Pat 2 [14-16] [Type Unit]: Unit + output: Unit + functors: empty set + body: SpecDecl 3 [0-55]: Impl: + Block 4 [24-55] [Type Unit]: + Stmt 5 [30-53]: Semi: Expr 6 [30-52] [Type Bool]: Parallel: + Limit: Expr 7 [46-47] [Type Int]: Lit: Int(3) + Body: Expr 11 [0-0] [Type Bool]: Expr Block: Block 10 [0-0] [Type Bool]: + Stmt 9 [0-0]: Expr: Expr 8 [48-52] [Type Bool]: Lit: Bool(true) + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn parallel_limited_with_block_lowers_into_existing_block() { + check_hir( + indoc! {" + operation Main() : Unit { + parallel within 3 {true}; + } + "}, + &expect![[r#" + Package: + Item 0 [0-57] (Public): + Namespace (Ident 12 [0-57] "test"): Item 1 + Item 1 [0-57] (Internal): + Parent: 0 + Callable 0 [0-57] (operation): + name: Ident 1 [10-14] "Main" + input: Pat 2 [14-16] [Type Unit]: Unit + output: Unit + functors: empty set + body: SpecDecl 3 [0-57]: Impl: + Block 4 [24-57] [Type Unit]: + Stmt 5 [30-55]: Semi: Expr 6 [30-54] [Type Bool]: Parallel: + Limit: Expr 7 [46-47] [Type Int]: Lit: Int(3) + Body: Expr 8 [48-54] [Type Bool]: Expr Block: Block 9 [48-54] [Type Bool]: + Stmt 10 [49-53]: Expr: Expr 11 [49-53] [Type Bool]: Lit: Bool(true) + adj: + ctl: + ctl-adj: "#]], + ); +} diff --git a/source/compiler/qsc_frontend/src/typeck/rules.rs b/source/compiler/qsc_frontend/src/typeck/rules.rs index eddc14a4508..7ec8a747ebb 100644 --- a/source/compiler/qsc_frontend/src/typeck/rules.rs +++ b/source/compiler/qsc_frontend/src/typeck/rules.rs @@ -473,6 +473,18 @@ impl<'a> Context<'a> { Lit::String(_) => converge(Ty::Prim(Prim::String)), }, ExprKind::Paren(expr) => self.infer_expr(expr), + ExprKind::Parallel(limit, body) => { + let limit_diverges = if let Some(limit) = limit { + let limit_span = limit.span; + let limit = self.infer_expr(limit); + self.inferrer.eq(limit_span, Ty::Prim(Prim::Int), limit.ty); + limit.diverges + } else { + false + }; + let body = self.infer_expr(body); + body.diverge_if(limit_diverges) + } ExprKind::Path(path) => self.infer_path_kind(expr, path), ExprKind::Range(start, step, end) => { let mut diverges = false; diff --git a/source/compiler/qsc_hir/src/hir.rs b/source/compiler/qsc_hir/src/hir.rs index d2b2df4b6c0..37958733d43 100644 --- a/source/compiler/qsc_hir/src/hir.rs +++ b/source/compiler/qsc_hir/src/hir.rs @@ -699,6 +699,8 @@ pub enum ExprKind { Index(Box, Box), /// A literal. Lit(Lit), + /// A parallel expression: `parallel a` or `parallel within n a`. + Parallel(Option>, Box), /// A range: `start..step..end`, `start..end`, `start...`, `...end`, or `...`. Range(Option>, Option>, Option>), /// A repeat-until loop with an optional fixup: `repeat { ... } until a fixup { ... }`. @@ -753,6 +755,7 @@ impl Display for ExprKind { ExprKind::If(cond, body, els) => display_if(indent, cond, body, els.as_deref())?, ExprKind::Index(array, index) => display_index(indent, array, index)?, ExprKind::Lit(lit) => write!(indent, "Lit: {lit}")?, + ExprKind::Parallel(limit, expr) => display_parallel(indent, limit.as_deref(), expr)?, ExprKind::Range(start, step, end) => { display_range(indent, start.as_deref(), step.as_deref(), end.as_deref())?; } @@ -940,6 +943,20 @@ fn display_index(mut indent: Indented, array: &Expr, index: &Expr) -> Ok(()) } +fn display_parallel( + mut indent: Indented, + limit: Option<&Expr>, + body: &Expr, +) -> fmt::Result { + write!(indent, "Parallel:")?; + indent = set_indentation(indent, 1); + if let Some(l) = limit { + write!(indent, "\nLimit: {l}")?; + } + write!(indent, "\nBody: {body}")?; + Ok(()) +} + fn display_range( mut indent: Indented, start: Option<&Expr>, diff --git a/source/compiler/qsc_hir/src/mut_visit.rs b/source/compiler/qsc_hir/src/mut_visit.rs index 50a8fc4b56f..d80e18574d3 100644 --- a/source/compiler/qsc_hir/src/mut_visit.rs +++ b/source/compiler/qsc_hir/src/mut_visit.rs @@ -130,6 +130,7 @@ pub fn walk_stmt(vis: &mut impl MutVisitor, stmt: &mut Stmt) { } } +#[allow(clippy::too_many_lines)] pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { vis.visit_span(&mut expr.span); @@ -181,6 +182,12 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { vis.visit_expr(array); vis.visit_expr(index); } + ExprKind::Parallel(limit, expr) => { + if let Some(limit) = limit { + vis.visit_expr(limit); + } + vis.visit_expr(expr); + } ExprKind::Return(expr) | ExprKind::UnOp(_, expr) => { vis.visit_expr(expr); } diff --git a/source/compiler/qsc_hir/src/visit.rs b/source/compiler/qsc_hir/src/visit.rs index 510c79decea..1942c4752a6 100644 --- a/source/compiler/qsc_hir/src/visit.rs +++ b/source/compiler/qsc_hir/src/visit.rs @@ -113,6 +113,7 @@ pub fn walk_stmt<'a>(vis: &mut impl Visitor<'a>, stmt: &'a Stmt) { } } +#[allow(clippy::too_many_lines)] pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { match &expr.kind { ExprKind::Array(exprs) => exprs.iter().for_each(|e| vis.visit_expr(e)), @@ -162,6 +163,12 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { vis.visit_expr(array); vis.visit_expr(index); } + ExprKind::Parallel(limit, expr) => { + if let Some(limit) = limit { + vis.visit_expr(limit); + } + vis.visit_expr(expr); + } ExprKind::Return(expr) | ExprKind::UnOp(_, expr) => { vis.visit_expr(expr); } diff --git a/source/compiler/qsc_lowerer/src/lib.rs b/source/compiler/qsc_lowerer/src/lib.rs index 9b17f199dfe..ff8e76fe019 100644 --- a/source/compiler/qsc_lowerer/src/lib.rs +++ b/source/compiler/qsc_lowerer/src/lib.rs @@ -693,6 +693,15 @@ impl Lowerer { let index = self.lower_expr(index); fir::ExprKind::Index(container, index) } + hir::ExprKind::Parallel(limit, expr) => { + let limit = limit.as_ref().map(|l| self.lower_expr(l)); + + self.exec_graph + .push(ExecGraphNode::ParStart(limit.is_some().into())); + let expr = self.lower_expr(expr); + self.exec_graph.push(ExecGraphNode::ParEnd); + fir::ExprKind::Parallel(limit, expr) + } hir::ExprKind::Lit(lit) => lower_lit(lit), hir::ExprKind::Range(start, step, end) => { let start = start.as_ref().map(|s| self.lower_expr(s)); @@ -810,7 +819,8 @@ impl Lowerer { | fir::ExprKind::Block(..) | fir::ExprKind::If(..) | fir::ExprKind::Return(..) - | fir::ExprKind::While(..) => {} + | fir::ExprKind::While(..) + | fir::ExprKind::Parallel(..) => {} fir::ExprKind::Assign(..) | fir::ExprKind::AssignField(..) diff --git a/source/compiler/qsc_parse/src/completion/word_kinds.rs b/source/compiler/qsc_parse/src/completion/word_kinds.rs index 1d04a59220a..2a4c05383c3 100644 --- a/source/compiler/qsc_parse/src/completion/word_kinds.rs +++ b/source/compiler/qsc_parse/src/completion/word_kinds.rs @@ -115,6 +115,7 @@ bitflags! { const Open = keyword_bit(Keyword::Open); const Operation = keyword_bit(Keyword::Operation); const Or = keyword_bit(Keyword::Or); + const Parallel = keyword_bit(Keyword::Parallel); const PauliI = keyword_bit(Keyword::PauliI); const PauliX = keyword_bit(Keyword::PauliX); const PauliY = keyword_bit(Keyword::PauliY); diff --git a/source/compiler/qsc_parse/src/expr.rs b/source/compiler/qsc_parse/src/expr.rs index 6dd3741b9c2..a27bd89a86b 100644 --- a/source/compiler/qsc_parse/src/expr.rs +++ b/source/compiler/qsc_parse/src/expr.rs @@ -101,7 +101,7 @@ pub(super) fn is_stmt_final(kind: &ExprKind) -> bool { | ExprKind::If(..) | ExprKind::Repeat(..) | ExprKind::While(..) - ) + ) || matches!(kind, ExprKind::Parallel(_, expr) if is_stmt_final(&expr.kind)) } fn expr_op(s: &mut ParserContext, context: OpContext) -> Result> { @@ -174,6 +174,7 @@ fn expr_op(s: &mut ParserContext, context: OpContext) -> Result> { Ok(lhs) } +#[allow(clippy::too_many_lines)] fn expr_base(s: &mut ParserContext) -> Result> { let lo = s.peek().span.lo; let kind = if token(s, TokenKind::Open(Delim::Paren)).is_ok() { @@ -229,6 +230,15 @@ fn expr_base(s: &mut ParserContext) -> Result> { Ok(Box::new(ExprKind::Repeat(body, cond, fixup))) } else if token(s, TokenKind::Keyword(Keyword::Return)).is_ok() { Ok(Box::new(ExprKind::Return(expr(s)?))) + } else if token(s, TokenKind::Keyword(Keyword::Parallel)).is_ok() { + if s.peek().kind == TokenKind::Keyword(Keyword::Within) { + s.advance(); + let limit = expr(s)?; + let body = expr(s)?; + Ok(Box::new(ExprKind::Parallel(Some(limit), body))) + } else { + Ok(Box::new(ExprKind::Parallel(None, expr(s)?))) + } } else if !s.contains_language_feature(LanguageFeatures::V2PreviewSyntax) && token(s, TokenKind::Keyword(Keyword::Set)).is_ok() { diff --git a/source/compiler/qsc_parse/src/expr/tests.rs b/source/compiler/qsc_parse/src/expr/tests.rs index 7786259ad3e..bdd95cc133b 100644 --- a/source/compiler/qsc_parse/src/expr/tests.rs +++ b/source/compiler/qsc_parse/src/expr/tests.rs @@ -3117,3 +3117,183 @@ fn call_with_incomplete_struct_arg() { ]"#]], ); } + +#[test] +fn parallel_expr() { + check( + expr, + "parallel x", + &expect![[r#" + Expr _id_ [0-10]: Parallel: + Body: Expr _id_ [9-10]: Path: Path _id_ [9-10] (Ident _id_ [9-10] "x")"#]], + ); +} + +#[test] +fn parallel_with_block_body() { + check( + expr, + "parallel { x }", + &expect![[r#" + Expr _id_ [0-14]: Parallel: + Body: Expr _id_ [9-14]: Expr Block: Block _id_ [9-14]: + Stmt _id_ [11-12]: Expr: Expr _id_ [11-12]: Path: Path _id_ [11-12] (Ident _id_ [11-12] "x")"#]], + ); +} + +#[test] +fn parallel_with_block_body_multiple_stmts() { + check( + expr, + "parallel { let a = 1; a }", + &expect![[r#" + Expr _id_ [0-25]: Parallel: + Body: Expr _id_ [9-25]: Expr Block: Block _id_ [9-25]: + Stmt _id_ [11-21]: Local (Immutable): + Pat _id_ [15-16]: Bind: + Ident _id_ [15-16] "a" + Expr _id_ [19-20]: Lit: Int(1) + Stmt _id_ [22-23]: Expr: Expr _id_ [22-23]: Path: Path _id_ [22-23] (Ident _id_ [22-23] "a")"#]], + ); +} + +#[test] +fn parallel_nested() { + check( + expr, + "parallel { parallel x }", + &expect![[r#" + Expr _id_ [0-23]: Parallel: + Body: Expr _id_ [9-23]: Expr Block: Block _id_ [9-23]: + Stmt _id_ [11-21]: Expr: Expr _id_ [11-21]: Parallel: + Body: Expr _id_ [20-21]: Path: Path _id_ [20-21] (Ident _id_ [20-21] "x")"#]], + ); +} + +#[test] +fn parallel_limited_expr() { + check( + expr, + "parallel within 4 { }", + &expect![[r#" + Expr _id_ [0-21]: Parallel: + Limit: Expr _id_ [16-17]: Lit: Int(4) + Body: Expr _id_ [18-21]: Expr Block: Block _id_ [18-21]: "#]], + ); +} + +#[test] +fn parallel_limited_with_path_body() { + check( + expr, + "parallel within 2 x", + &expect![[r#" + Expr _id_ [0-19]: Parallel: + Limit: Expr _id_ [16-17]: Lit: Int(2) + Body: Expr _id_ [18-19]: Path: Path _id_ [18-19] (Ident _id_ [18-19] "x")"#]], + ); +} + +#[test] +fn parallel_limited_with_block_body() { + check( + expr, + "parallel within 3 { x }", + &expect![[r#" + Expr _id_ [0-23]: Parallel: + Limit: Expr _id_ [16-17]: Lit: Int(3) + Body: Expr _id_ [18-23]: Expr Block: Block _id_ [18-23]: + Stmt _id_ [20-21]: Expr: Expr _id_ [20-21]: Path: Path _id_ [20-21] (Ident _id_ [20-21] "x")"#]], + ); +} + +#[test] +fn parallel_limited_with_computed_limit() { + check( + expr, + "parallel within (2 + 3) x", + &expect![[r#" + Expr _id_ [0-25]: Parallel: + Limit: Expr _id_ [16-23]: Paren: Expr _id_ [17-22]: BinOp (Add): + Expr _id_ [17-18]: Lit: Int(2) + Expr _id_ [21-22]: Lit: Int(3) + Body: Expr _id_ [24-25]: Path: Path _id_ [24-25] (Ident _id_ [24-25] "x")"#]], + ); +} + +#[test] +fn parallel_limited_nested_in_parallel() { + check( + expr, + "parallel { parallel within 2 x }", + &expect![[r#" + Expr _id_ [0-32]: Parallel: + Body: Expr _id_ [9-32]: Expr Block: Block _id_ [9-32]: + Stmt _id_ [11-30]: Expr: Expr _id_ [11-30]: Parallel: + Limit: Expr _id_ [27-28]: Lit: Int(2) + Body: Expr _id_ [29-30]: Path: Path _id_ [29-30] (Ident _id_ [29-30] "x")"#]], + ); +} + +#[test] +fn parallel_within_without_limit_parses_within_as_limit() { + // `parallel within { }` — the parser consumes `{ }` as the limit expression (a block), + // then fails to find a body expression since the input ends. + check( + expr, + "parallel within { }", + &expect![[r#" + Error( + Rule( + "expression", + Eof, + Span { + lo: 19, + hi: 19, + }, + ), + ) + "#]], + ); +} + +#[test] +fn parallel_within_apply_is_error() { + // `parallel within {} apply {}` — the parser sees `parallel within` and tries to parse + // a ParallelLimited. It consumes `{}` as the limit and then `apply` as the start of the + // body expression, but `apply` is not a valid expression keyword, producing an error. + check( + expr, + "parallel within {} apply {}", + &expect![[r#" + Error( + Rule( + "expression", + Keyword( + Apply, + ), + Span { + lo: 19, + hi: 24, + }, + ), + ) + "#]], + ); +} + +#[test] +fn parallel_with_paren_within_apply_is_conjugate() { + // `parallel (within {} apply {})` — the parser sees `parallel` (without an immediately + // following `within`), so it parses `parallel ` where is the parenthesized + // within/apply (Conjugate) expression. + check( + expr, + "parallel (within {} apply {})", + &expect![[r#" + Expr _id_ [0-29]: Parallel: + Body: Expr _id_ [9-29]: Paren: Expr _id_ [10-28]: Conjugate: + Block _id_ [17-19]: + Block _id_ [26-28]: "#]], + ); +} diff --git a/source/compiler/qsc_parse/src/keyword.rs b/source/compiler/qsc_parse/src/keyword.rs index e87c0ef6a90..89e526cdff6 100644 --- a/source/compiler/qsc_parse/src/keyword.rs +++ b/source/compiler/qsc_parse/src/keyword.rs @@ -47,6 +47,7 @@ pub enum Keyword { Open, Operation, Or, + Parallel, PauliI, PauliX, PauliY, @@ -106,6 +107,7 @@ impl Keyword { Self::Open => "open", Self::Operation => "operation", Self::Or => "or", + Self::Parallel => "parallel", Self::PauliI => "PauliI", Self::PauliX => "PauliX", Self::PauliY => "PauliY", @@ -197,6 +199,7 @@ impl FromStr for Keyword { // in the standard library for priority order. "PauliY" => Ok(Self::PauliY), "borrow" => Ok(Self::Borrow), + "parallel" => Ok(Self::Parallel), "_" => Ok(Self::Underscore), _ => Err(()), } diff --git a/source/compiler/qsc_partial_eval/src/lib.rs b/source/compiler/qsc_partial_eval/src/lib.rs index d262441cb4f..786677bd7c2 100644 --- a/source/compiler/qsc_partial_eval/src/lib.rs +++ b/source/compiler/qsc_partial_eval/src/lib.rs @@ -926,6 +926,7 @@ impl<'a> PartialEvaluator<'a> { bin_op: BinOp, lhs_eval_var: Var, rhs_expr_id: ExprId, + bin_op_expr_span: PackageSpan, ) -> Result { let result_var = match bin_op { BinOp::Eq | BinOp::Neq => { @@ -934,12 +935,12 @@ impl<'a> PartialEvaluator<'a> { BinOp::AndL => { // Logical AND Boolean operations short-circuit on false. let lhs_rir_var = map_eval_var_to_rir_var(lhs_eval_var); - self.eval_logical_bool_bin_op(false, lhs_rir_var, rhs_expr_id)? + self.eval_logical_bool_bin_op(false, lhs_rir_var, rhs_expr_id, bin_op_expr_span)? } BinOp::OrL => { // Logical OR Boolean operations short-circuit on true. let lhs_rir_var = map_eval_var_to_rir_var(lhs_eval_var); - self.eval_logical_bool_bin_op(true, lhs_rir_var, rhs_expr_id)? + self.eval_logical_bool_bin_op(true, lhs_rir_var, rhs_expr_id, bin_op_expr_span)? } _ => panic!("invalid Boolean operator {bin_op:?}"), }; @@ -997,7 +998,10 @@ impl<'a> PartialEvaluator<'a> { short_circuit_on_true: bool, lhs_rir_var: rir::Variable, rhs_expr_id: ExprId, + bin_op_expr_span: PackageSpan, ) -> Result { + self.fail_if_in_parallel_expr(bin_op_expr_span)?; + // Create the variable where we will store the result of the Boolean operation and store a default value in it, // which will only be changed inside the conditional block where the RHS expression is evaluated. let result_var_id = self.resource_manager.next_var(); @@ -1189,9 +1193,12 @@ impl<'a> PartialEvaluator<'a> { bin_op_expr_span: PackageSpan, // For diagnostic purposes only. ) -> Result { match lhs_eval_var.ty { - VarTy::Boolean => { - self.eval_bin_op_with_lhs_dynamic_bool_operand(bin_op, lhs_eval_var, rhs_expr_id) - } + VarTy::Boolean => self.eval_bin_op_with_lhs_dynamic_bool_operand( + bin_op, + lhs_eval_var, + rhs_expr_id, + bin_op_expr_span, + ), VarTy::Integer => { let lhs_rir_var = map_eval_var_to_rir_var(lhs_eval_var); let lhs_operand = Operand::Variable(lhs_rir_var); @@ -1344,6 +1351,9 @@ impl<'a> PartialEvaluator<'a> { "literal should have been classically evaluated".to_string(), expr_package_span, )), + ExprKind::Parallel(limit_id, expr_id) => { + self.eval_expr_parallel(*expr_id, limit_id.as_ref()) + } ExprKind::Range(start, step, end) => { self.eval_expr_range(*start, *step, *end, expr_package_span) } @@ -2474,6 +2484,7 @@ impl<'a> PartialEvaluator<'a> { // At this point the condition value is not classical, so we need to generate a branching instruction. // First, we pop the current block node and generate a new one which the new branches will jump to when their // instructions end. + self.fail_if_in_parallel_expr(self.get_expr_package_span(if_expr_id))?; let current_block_node = self.eval_context.pop_block_node(); let continuation_block_node_id = self.create_program_block(); let continuation_block_node = BlockNode { @@ -3026,6 +3037,8 @@ impl<'a> PartialEvaluator<'a> { return Ok(EvalControlFlow::Continue(Value::unit())); } + self.fail_if_in_parallel_expr(self.get_expr_package_span(loop_expr_id))?; + // Otherwise, branch to either the body block or the continuation block. let body_block_node_id = self.create_program_block(); let body_block_node = BlockNode { @@ -4734,6 +4747,49 @@ impl<'a> PartialEvaluator<'a> { }, )))) } + + fn eval_expr_parallel( + &mut self, + expr_id: ExprId, + limit_id: Option<&ExprId>, + ) -> Result { + let limit = if let Some(&limit_id) = limit_id { + let limit_control_flow = self.try_eval_expr(limit_id)?; + let EvalControlFlow::Continue(limit_value) = limit_control_flow else { + return Err(Error::Unexpected( + "embedded return in parallel limit expression".to_string(), + self.get_expr_package_span(limit_id), + )); + }; + let limit = limit_value.unwrap_int(); + if limit < 0 { + return Err(EvalError::InvalidNegativeInt( + limit, + self.get_expr_package_span(limit_id), + ) + .into()); + } + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + Some(limit as usize) + } else { + None + }; + + self.resource_manager.start_delayed_release_layer(limit); + let result = self.try_eval_expr(expr_id)?; + self.resource_manager.end_delayed_release_layer(); + Ok(result) + } + + fn fail_if_in_parallel_expr(&self, span: PackageSpan) -> Result<(), Error> { + if self.resource_manager.is_delaying_release() { + return Err(Error::Unimplemented( + "dynamic branching within parallel expression".to_string(), + span, + )); + } + Ok(()) + } } // Determines if a value can be treated as a static value, meaning something that can be directly passed diff --git a/source/compiler/qsc_partial_eval/src/management.rs b/source/compiler/qsc_partial_eval/src/management.rs index 908045310c6..728bc96d0da 100644 --- a/source/compiler/qsc_partial_eval/src/management.rs +++ b/source/compiler/qsc_partial_eval/src/management.rs @@ -7,6 +7,7 @@ use num_bigint::BigUint; use num_complex::Complex; use qsc_data_structures::index_map::IndexMap; use qsc_eval::{ + DelayedQubitReleaseStack, backend::Backend, val::{Qubit, QubitRef, Result, Value}, }; @@ -19,6 +20,7 @@ pub struct ResourceManager { qubits_in_use: Vec, qubit_id_map: IndexMap, qubit_tracker: FxHashSet>, + delayed_release_qubits: DelayedQubitReleaseStack, next_callable: CallableId, next_block: BlockId, next_result_register: usize, @@ -46,6 +48,14 @@ impl ResourceManager { /// Allocates a qubit by favoring available qubit IDs before using new ones. pub fn allocate_qubit(&mut self) -> QubitRef { + if let Some(qubit) = self.delayed_release_qubits.allocate_delayed_qubit() { + return self + .qubit_tracker + .get(&qubit) + .expect("qubit should be in map") + .into(); + } + let qubit = if let Some(qubit) = self.qubits_in_use.iter().position(|in_use| !in_use) { self.qubits_in_use[qubit] = true; qubit @@ -70,12 +80,30 @@ impl ResourceManager { /// Releases a qubit ID for future use. pub fn release_qubit(&mut self, q: &QubitRef) { - let qubit = self.map_qubit(q); - self.qubits_in_use[qubit] = false; - let q = q.deref(); - self.qubit_id_map.remove(q.0); - self.qubit_tracker.remove(&q); + if !self.delayed_release_qubits.delay_release_qubit(*q) { + self.free_qubit_id(*q); + } + } + + pub fn start_delayed_release_layer(&mut self, limit: Option) { + self.delayed_release_qubits.add_layer(limit); + } + + pub fn end_delayed_release_layer(&mut self) { + for qubit in self.delayed_release_qubits.remove_layer() { + self.free_qubit_id(qubit); + } + } + + fn free_qubit_id(&mut self, qubit: Qubit) { + self.qubits_in_use[self.qubit_id_map[qubit.0]] = false; + self.qubit_id_map.remove(qubit.0); + self.qubit_tracker.remove(&qubit); + } + + pub fn is_delaying_release(&self) -> bool { + !self.delayed_release_qubits.is_empty() } /// Gets the next block ID. diff --git a/source/compiler/qsc_partial_eval/src/tests.rs b/source/compiler/qsc_partial_eval/src/tests.rs index a662d124131..4ef5fac8cce 100644 --- a/source/compiler/qsc_partial_eval/src/tests.rs +++ b/source/compiler/qsc_partial_eval/src/tests.rs @@ -15,6 +15,7 @@ mod loops; mod misc; mod operators; mod output_recording; +mod parallel; mod qubits; mod results; mod returns; diff --git a/source/compiler/qsc_partial_eval/src/tests/parallel.rs b/source/compiler/qsc_partial_eval/src/tests/parallel.rs new file mode 100644 index 00000000000..9c3275236ca --- /dev/null +++ b/source/compiler/qsc_partial_eval/src/tests/parallel.rs @@ -0,0 +1,386 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{assert_blocks, get_rir_program, get_rir_program_with_capabilities}; +use expect_test::expect; +use indoc::indoc; +use qsc_data_structures::target::Profile; + +#[test] +fn baseline_qubit_ids_recycled_without_parallel() { + let program = get_rir_program(indoc! { + r#" + namespace Test { + operation op(q : Qubit) : Unit { body intrinsic; } + @EntryPoint() + operation Main() : Unit { + // q1 and q2 are allocated and released inside the inner block + { use q1 = Qubit(); op(q1); use q2 = Qubit(); op(q2); } + // q1 and q2 are now released; next allocations reuse the same IDs + use q3 = Qubit(); + op(q3); + use q4 = Qubit(); + op(q4); + } + } + "#, + }); + assert_blocks( + &program, + &expect![[r#" + Blocks: + Block 0:Block: + Call id(1), args( Pointer, ) + Call id(2), args( Qubit(0), ) + Call id(2), args( Qubit(1), ) + Call id(2), args( Qubit(0), ) + Call id(2), args( Qubit(1), ) + Call id(3), args( Integer(0), Tag(0, 3), ) + Return Integer(0)"#]], + ); + assert_eq!(program.num_qubits, 2); + assert_eq!(program.num_results, 0); +} + +#[test] +fn parallel_defers_qubit_release() { + let program = get_rir_program(indoc! { + r#" + namespace Test { + operation op(q : Qubit) : Unit { body intrinsic; } + @EntryPoint() + operation Main() : Unit { + parallel { + // q1 and q2 are allocated and released inside the inner block + { use q1 = Qubit(); op(q1); use q2 = Qubit(); op(q2); } + // inside parallel their release is deferred, so q3 and q4 get fresh ids + use q3 = Qubit(); + op(q3); + use q4 = Qubit(); + op(q4); + } + } + } + "#, + }); + assert_blocks( + &program, + &expect![[r#" + Blocks: + Block 0:Block: + Call id(1), args( Pointer, ) + Call id(2), args( Qubit(0), ) + Call id(2), args( Qubit(1), ) + Call id(2), args( Qubit(2), ) + Call id(2), args( Qubit(3), ) + Call id(3), args( Integer(0), Tag(0, 3), ) + Return Integer(0)"#]], + ); + assert_eq!(program.num_qubits, 4); + assert_eq!(program.num_results, 0); +} + +#[test] +fn parallel_releases_available_after_block_ends() { + let program = get_rir_program(indoc! { + r#" + namespace Test { + operation op(q : Qubit) : Unit { body intrinsic; } + @EntryPoint() + operation Main() : Unit { + parallel { + use q = Qubit(); + op(q); + } + parallel { + use q = Qubit(); + op(q); + } + } + } + "#, + }); + assert_blocks( + &program, + &expect![[r#" + Blocks: + Block 0:Block: + Call id(1), args( Pointer, ) + Call id(2), args( Qubit(0), ) + Call id(2), args( Qubit(0), ) + Call id(3), args( Integer(0), Tag(0, 3), ) + Return Integer(0)"#]], + ); + assert_eq!(program.num_qubits, 1); + assert_eq!(program.num_results, 0); +} + +#[test] +fn parallel_nested_defers_inner_releases_to_outer() { + let program = get_rir_program(indoc! { + r#" + namespace Test { + operation op(q : Qubit) : Unit { body intrinsic; } + @EntryPoint() + operation Main() : Unit { + parallel { + use outer = Qubit(); + op(outer); + parallel { + use inner1 = Qubit(); + op(inner1); + use inner2 = Qubit(); + op(inner2); + } + // inner qubits are now deferred in the outer layer, so a fresh id is allocated + use outer2 = Qubit(); + op(outer2); + } + } + } + "#, + }); + assert_blocks( + &program, + &expect![[r#" + Blocks: + Block 0:Block: + Call id(1), args( Pointer, ) + Call id(2), args( Qubit(0), ) + Call id(2), args( Qubit(1), ) + Call id(2), args( Qubit(2), ) + Call id(2), args( Qubit(3), ) + Call id(3), args( Integer(0), Tag(0, 3), ) + Return Integer(0)"#]], + ); + assert_eq!(program.num_qubits, 4); + assert_eq!(program.num_results, 0); +} + +#[test] +fn parallel_within_reuses_ids_after_limit() { + let program = get_rir_program(indoc! { + r#" + namespace Test { + operation op(q : Qubit) : Unit { body intrinsic; } + @EntryPoint() + operation Main() : Unit { + parallel within 2 { + // Each nested block releases its qubit before the next allocation. + { use q1 = Qubit(); op(q1); } + { use q2 = Qubit(); op(q2); } + // 2 qubits have now been deferred; limit reached so q3 and q4 reuse ids + { use q3 = Qubit(); op(q3); } + { use q4 = Qubit(); op(q4); } + } + } + } + "#, + }); + assert_blocks( + &program, + &expect![[r#" + Blocks: + Block 0:Block: + Call id(1), args( Pointer, ) + Call id(2), args( Qubit(0), ) + Call id(2), args( Qubit(1), ) + Call id(2), args( Qubit(0), ) + Call id(2), args( Qubit(1), ) + Call id(3), args( Integer(0), Tag(0, 3), ) + Return Integer(0)"#]], + ); + assert_eq!(program.num_qubits, 2); + assert_eq!(program.num_results, 0); +} + +#[test] +fn parallel_within_nested_defers_through_outer_limit() { + let program = get_rir_program(indoc! { + r#" + namespace Test { + operation op(q : Qubit) : Unit { body intrinsic; } + @EntryPoint() + operation Main() : Unit { + parallel within 6 { for _ in 0..2 { + { use q0 = Qubit(); op(q0); } + parallel within 2 { + { use q1 = Qubit(); op(q1); } + { use q2 = Qubit(); op(q2); } + { use q3 = Qubit(); op(q3); } + { use q4 = Qubit(); op(q4); } + } + } } + } + } + "#, + }); + assert_blocks( + &program, + &expect![[r#" + Blocks: + Block 0:Block: + Call id(1), args( Pointer, ) + Variable(0, Integer) = Store Integer(0) + Call id(2), args( Qubit(0), ) + Call id(2), args( Qubit(1), ) + Call id(2), args( Qubit(2), ) + Call id(2), args( Qubit(1), ) + Call id(2), args( Qubit(2), ) + Variable(0, Integer) = Store Integer(1) + Call id(2), args( Qubit(3), ) + Call id(2), args( Qubit(4), ) + Call id(2), args( Qubit(5), ) + Call id(2), args( Qubit(4), ) + Call id(2), args( Qubit(5), ) + Variable(0, Integer) = Store Integer(2) + Call id(2), args( Qubit(0), ) + Call id(2), args( Qubit(1), ) + Call id(2), args( Qubit(2), ) + Call id(2), args( Qubit(1), ) + Call id(2), args( Qubit(2), ) + Variable(0, Integer) = Store Integer(3) + Call id(3), args( Integer(0), Tag(0, 3), ) + Return Integer(0)"#]], + ); + assert_eq!(program.num_qubits, 6); + assert_eq!(program.num_results, 0); +} + +#[test] +fn parallel_nested_unlimited_outer_defers_all() { + let program = get_rir_program(indoc! { + r#" + namespace Test { + operation op(q : Qubit) : Unit { body intrinsic; } + @EntryPoint() + operation Main() : Unit { + parallel { for _ in 0..2 { + { use q0 = Qubit(); op(q0); } + parallel within 2 { + { use q1 = Qubit(); op(q1); } + { use q2 = Qubit(); op(q2); } + { use q3 = Qubit(); op(q3); } + { use q4 = Qubit(); op(q4); } + } + } } + } + } + "#, + }); + assert_blocks( + &program, + &expect![[r#" + Blocks: + Block 0:Block: + Call id(1), args( Pointer, ) + Variable(0, Integer) = Store Integer(0) + Call id(2), args( Qubit(0), ) + Call id(2), args( Qubit(1), ) + Call id(2), args( Qubit(2), ) + Call id(2), args( Qubit(1), ) + Call id(2), args( Qubit(2), ) + Variable(0, Integer) = Store Integer(1) + Call id(2), args( Qubit(3), ) + Call id(2), args( Qubit(4), ) + Call id(2), args( Qubit(5), ) + Call id(2), args( Qubit(4), ) + Call id(2), args( Qubit(5), ) + Variable(0, Integer) = Store Integer(2) + Call id(2), args( Qubit(6), ) + Call id(2), args( Qubit(7), ) + Call id(2), args( Qubit(8), ) + Call id(2), args( Qubit(7), ) + Call id(2), args( Qubit(8), ) + Variable(0, Integer) = Store Integer(3) + Call id(3), args( Integer(0), Tag(0, 3), ) + Return Integer(0)"#]], + ); + assert_eq!(program.num_qubits, 9); + assert_eq!(program.num_results, 0); +} + +#[test] +fn parallel_forces_loop_unrolling_with_adaptive() { + // Without parallel, the loop uses backward branching (multiple blocks). + let program_no_parallel = get_rir_program_with_capabilities( + indoc! { + r#" + namespace Test { + @EntryPoint() + operation Main() : Unit { + for _ in 0..1 { + use q = Qubit(); + H(q); + } + } + } + "#, + }, + Profile::Adaptive.into(), + ); + assert_blocks( + &program_no_parallel, + &expect![[r#" + Blocks: + Block 0:Block: + Call id(1), args( Pointer, ) + Variable(0, Integer) = Store Integer(0) + Jump(1) + Block 1:Block: + Variable(1, Boolean) = Icmp Sle, Variable(0, Integer), Integer(1) + Variable(2, Boolean) = Store Bool(true) + Branch Variable(1, Boolean), 3, 4 + Block 2:Block: + Call id(4), args( Integer(0), Tag(0, 3), ) + Return Integer(0) + Block 3:Block: + Branch Variable(2, Boolean), 5, 2 + Block 4:Block: + Variable(2, Boolean) = Store Bool(false) + Jump(3) + Block 5:Block: + Call id(2), args( Qubit(0), ) + Variable(4, Integer) = Add Variable(0, Integer), Integer(1) + Variable(0, Integer) = Store Variable(4, Integer) + Jump(1) + Block 6:Block: + Call id(3), args( Variable(3, Qubit), ) + Return"#]], + ); + + // With parallel, the same loop is unrolled into a single block. + let program_parallel = get_rir_program_with_capabilities( + indoc! { + r#" + namespace Test { + @EntryPoint() + operation Main() : Unit { + parallel for _ in 0..1 { + use q = Qubit(); + H(q); + } + } + } + "#, + }, + Profile::Adaptive.into(), + ); + assert_blocks( + &program_parallel, + &expect![[r#" + Blocks: + Block 0:Block: + Call id(1), args( Pointer, ) + Variable(0, Integer) = Store Integer(0) + Call id(2), args( Qubit(0), ) + Variable(0, Integer) = Store Integer(1) + Call id(2), args( Qubit(1), ) + Variable(0, Integer) = Store Integer(2) + Call id(4), args( Integer(0), Tag(0, 3), ) + Return Integer(0) + Block 1:Block: + Call id(3), args( Variable(1, Qubit), ) + Return"#]], + ); +} diff --git a/source/compiler/qsc_passes/src/capabilitiesck.rs b/source/compiler/qsc_passes/src/capabilitiesck.rs index 35f8904d237..d16638787b4 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck.rs @@ -208,6 +208,12 @@ impl<'a> Visitor<'a> for Checker<'a> { ExprKind::While(condition_expr_id, body_block_id) => { self.check_expr_while(expr_id, *condition_expr_id, *body_block_id); } + ExprKind::Parallel(limit_id, expr_id) => { + if let Some(limit_id) = limit_id { + self.visit_expr(*limit_id); + } + self.visit_expr(*expr_id); + } _ => self.check_expr(expr_id), } } diff --git a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs index 46db384f74e..5f422a20937 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs @@ -8,14 +8,16 @@ use super::tests_common::{ CALL_TO_CYCLIC_OPERATION_WITH_DYNAMIC_ARGUMENT, CALL_UNRESOLVED_FUNCTION, CUSTOM_MEASUREMENT, CUSTOM_MEASUREMENT_WITH_SIMULATABLE_INTRINSIC_ATTR, CUSTOM_RESET, CUSTOM_RESET_WITH_SIMULATABLE_INTRINSIC_ATTR, DYNAMIC_ARRAY_BINARY_OP, - LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL, - RETURN_WITHIN_DYNAMIC_SCOPE, USE_CLOSURE_FUNCTION, USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, - USE_DYNAMIC_DOUBLE, USE_DYNAMIC_FUNCTION, USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, - USE_DYNAMIC_LHS_EXP_BINOP, USE_DYNAMIC_OPERATION, USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, - USE_DYNAMIC_RANGE, USE_DYNAMIC_RHS_EXP_BINOP, USE_DYNAMIC_STRING, USE_DYNAMIC_UDT, - USE_DYNAMICALLY_SIZED_ARRAY, USE_ENTRY_POINT_INT_ARRAY_IN_TUPLE, - USE_ENTRY_POINT_STATIC_BIG_INT, USE_ENTRY_POINT_STATIC_BOOL, USE_ENTRY_POINT_STATIC_DOUBLE, - USE_ENTRY_POINT_STATIC_INT, USE_ENTRY_POINT_STATIC_INT_IN_TUPLE, USE_ENTRY_POINT_STATIC_PAULI, + LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL, PARALLEL_STATIC_BODY, + PARALLEL_WITH_DYNAMIC_BRANCH, PARALLEL_WITH_INDIRECT_BRANCH_VIA_CALL, + PARALLEL_WITHIN_DYNAMIC_LIMIT, PARALLEL_WITHIN_STATIC_LIMIT, RETURN_WITHIN_DYNAMIC_SCOPE, + USE_CLOSURE_FUNCTION, USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, USE_DYNAMIC_DOUBLE, + USE_DYNAMIC_FUNCTION, USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, USE_DYNAMIC_LHS_EXP_BINOP, + USE_DYNAMIC_OPERATION, USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, USE_DYNAMIC_RANGE, + USE_DYNAMIC_RHS_EXP_BINOP, USE_DYNAMIC_STRING, USE_DYNAMIC_UDT, USE_DYNAMICALLY_SIZED_ARRAY, + USE_ENTRY_POINT_INT_ARRAY_IN_TUPLE, USE_ENTRY_POINT_STATIC_BIG_INT, + USE_ENTRY_POINT_STATIC_BOOL, USE_ENTRY_POINT_STATIC_DOUBLE, USE_ENTRY_POINT_STATIC_INT, + USE_ENTRY_POINT_STATIC_INT_IN_TUPLE, USE_ENTRY_POINT_STATIC_PAULI, USE_ENTRY_POINT_STATIC_RANGE, USE_ENTRY_POINT_STATIC_STRING, check, check_for_exe, }; use expect_test::{Expect, expect}; @@ -759,3 +761,86 @@ fn binary_op_with_dynamic_array_succeeds() { "#]], ); } + +#[test] +fn parallel_with_static_body_yields_no_errors() { + check_profile( + PARALLEL_STATIC_BODY, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn parallel_within_with_static_limit_yields_no_errors() { + check_profile( + PARALLEL_WITHIN_STATIC_LIMIT, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn parallel_with_dynamic_branch_yields_error() { + check_profile( + PARALLEL_WITH_DYNAMIC_BRANCH, + &expect![[r#" + [ + UseOfDynamicBranchingInParallelExpr( + Span { + lo: 196, + hi: 210, + }, + ), + ] + "#]], + ); +} + +#[test] +fn parallel_within_with_dynamic_limit_yields_error() { + check_profile( + PARALLEL_WITHIN_DYNAMIC_LIMIT, + &expect![[r#" + [ + UseOfDynamicInt( + Span { + lo: 107, + hi: 130, + }, + ), + UseOfDynamicInt( + Span { + lo: 160, + hi: 161, + }, + ), + UseOfDynamicLimitInParallelExpr( + Span { + lo: 160, + hi: 161, + }, + ), + ] + "#]], + ); +} + +#[test] +fn parallel_with_indirect_branch_via_call_yields_error() { + check_profile( + PARALLEL_WITH_INDIRECT_BRANCH_VIA_CALL, + &expect![[r#" + [ + UseOfDynamicBranchingInParallelExpr( + Span { + lo: 217, + hi: 223, + }, + ), + ] + "#]], + ); +} diff --git a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers.rs b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers.rs index c4b147f958e..84da9fc8722 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers.rs @@ -8,11 +8,13 @@ use super::tests_common::{ CALL_TO_CYCLIC_FUNCTION_WITH_DYNAMIC_ARGUMENT, CALL_TO_CYCLIC_OPERATION_WITH_CLASSICAL_ARGUMENT, CALL_TO_CYCLIC_OPERATION_WITH_DYNAMIC_ARGUMENT, CALL_UNRESOLVED_FUNCTION, CUSTOM_MEASUREMENT, - LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL, - RETURN_WITHIN_DYNAMIC_SCOPE, USE_CLOSURE_FUNCTION, USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, - USE_DYNAMIC_DOUBLE, USE_DYNAMIC_FUNCTION, USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, - USE_DYNAMIC_LHS_EXP_BINOP, USE_DYNAMIC_OPERATION, USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, - USE_DYNAMIC_RHS_EXP_BINOP, USE_DYNAMIC_STRING, USE_DYNAMIC_UDT, USE_DYNAMICALLY_SIZED_ARRAY, + LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL, PARALLEL_STATIC_BODY, + PARALLEL_WITH_DYNAMIC_BRANCH, PARALLEL_WITH_INDIRECT_BRANCH_VIA_CALL, + PARALLEL_WITHIN_DYNAMIC_LIMIT, PARALLEL_WITHIN_STATIC_LIMIT, RETURN_WITHIN_DYNAMIC_SCOPE, + USE_CLOSURE_FUNCTION, USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, USE_DYNAMIC_DOUBLE, + USE_DYNAMIC_FUNCTION, USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, USE_DYNAMIC_LHS_EXP_BINOP, + USE_DYNAMIC_OPERATION, USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, USE_DYNAMIC_RHS_EXP_BINOP, + USE_DYNAMIC_STRING, USE_DYNAMIC_UDT, USE_DYNAMICALLY_SIZED_ARRAY, USE_ENTRY_POINT_INT_ARRAY_IN_TUPLE, USE_ENTRY_POINT_STATIC_BIG_INT, USE_ENTRY_POINT_STATIC_BOOL, USE_ENTRY_POINT_STATIC_DOUBLE, USE_ENTRY_POINT_STATIC_INT, USE_ENTRY_POINT_STATIC_INT_IN_TUPLE, USE_ENTRY_POINT_STATIC_PAULI, @@ -600,3 +602,74 @@ fn use_of_static_sized_array_in_tuple_allowed() { "#]], ); } + +#[test] +fn parallel_with_static_body_yields_no_errors() { + check_profile( + PARALLEL_STATIC_BODY, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn parallel_within_with_static_limit_yields_no_errors() { + check_profile( + PARALLEL_WITHIN_STATIC_LIMIT, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn parallel_with_dynamic_branch_yields_error() { + check_profile( + PARALLEL_WITH_DYNAMIC_BRANCH, + &expect![[r#" + [ + UseOfDynamicBranchingInParallelExpr( + Span { + lo: 196, + hi: 210, + }, + ), + ] + "#]], + ); +} + +#[test] +fn parallel_within_with_dynamic_limit_yields_error() { + check_profile( + PARALLEL_WITHIN_DYNAMIC_LIMIT, + &expect![[r#" + [ + UseOfDynamicLimitInParallelExpr( + Span { + lo: 160, + hi: 161, + }, + ), + ] + "#]], + ); +} + +#[test] +fn parallel_with_indirect_branch_via_call_yields_error() { + check_profile( + PARALLEL_WITH_INDIRECT_BRANCH_VIA_CALL, + &expect![[r#" + [ + UseOfDynamicBranchingInParallelExpr( + Span { + lo: 217, + hi: 223, + }, + ), + ] + "#]], + ); +} diff --git a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers_and_floats.rs b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers_and_floats.rs index 3055440cfd5..59c75bdb0d3 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers_and_floats.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers_and_floats.rs @@ -8,11 +8,13 @@ use super::tests_common::{ CALL_TO_CYCLIC_FUNCTION_WITH_DYNAMIC_ARGUMENT, CALL_TO_CYCLIC_OPERATION_WITH_CLASSICAL_ARGUMENT, CALL_TO_CYCLIC_OPERATION_WITH_DYNAMIC_ARGUMENT, CALL_UNRESOLVED_FUNCTION, CUSTOM_MEASUREMENT, - LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL, - RETURN_WITHIN_DYNAMIC_SCOPE, USE_CLOSURE_FUNCTION, USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, - USE_DYNAMIC_DOUBLE, USE_DYNAMIC_FUNCTION, USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, - USE_DYNAMIC_LHS_EXP_BINOP, USE_DYNAMIC_OPERATION, USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, - USE_DYNAMIC_RHS_EXP_BINOP, USE_DYNAMIC_STRING, USE_DYNAMIC_UDT, USE_DYNAMICALLY_SIZED_ARRAY, + LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL, PARALLEL_STATIC_BODY, + PARALLEL_WITH_DYNAMIC_BRANCH, PARALLEL_WITH_INDIRECT_BRANCH_VIA_CALL, + PARALLEL_WITHIN_DYNAMIC_LIMIT, PARALLEL_WITHIN_STATIC_LIMIT, RETURN_WITHIN_DYNAMIC_SCOPE, + USE_CLOSURE_FUNCTION, USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, USE_DYNAMIC_DOUBLE, + USE_DYNAMIC_FUNCTION, USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, USE_DYNAMIC_LHS_EXP_BINOP, + USE_DYNAMIC_OPERATION, USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, USE_DYNAMIC_RHS_EXP_BINOP, + USE_DYNAMIC_STRING, USE_DYNAMIC_UDT, USE_DYNAMICALLY_SIZED_ARRAY, USE_ENTRY_POINT_INT_ARRAY_IN_TUPLE, USE_ENTRY_POINT_STATIC_BIG_INT, USE_ENTRY_POINT_STATIC_BOOL, USE_ENTRY_POINT_STATIC_DOUBLE, USE_ENTRY_POINT_STATIC_INT, USE_ENTRY_POINT_STATIC_INT_IN_TUPLE, USE_ENTRY_POINT_STATIC_PAULI, @@ -578,3 +580,74 @@ fn use_of_static_sized_array_in_tuple_allowed() { "#]], ); } + +#[test] +fn parallel_with_static_body_yields_no_errors() { + check_profile( + PARALLEL_STATIC_BODY, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn parallel_within_with_static_limit_yields_no_errors() { + check_profile( + PARALLEL_WITHIN_STATIC_LIMIT, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn parallel_with_dynamic_branch_yields_error() { + check_profile( + PARALLEL_WITH_DYNAMIC_BRANCH, + &expect![[r#" + [ + UseOfDynamicBranchingInParallelExpr( + Span { + lo: 196, + hi: 210, + }, + ), + ] + "#]], + ); +} + +#[test] +fn parallel_within_with_dynamic_limit_yields_error() { + check_profile( + PARALLEL_WITHIN_DYNAMIC_LIMIT, + &expect![[r#" + [ + UseOfDynamicLimitInParallelExpr( + Span { + lo: 160, + hi: 161, + }, + ), + ] + "#]], + ); +} + +#[test] +fn parallel_with_indirect_branch_via_call_yields_error() { + check_profile( + PARALLEL_WITH_INDIRECT_BRANCH_VIA_CALL, + &expect![[r#" + [ + UseOfDynamicBranchingInParallelExpr( + Span { + lo: 217, + hi: 223, + }, + ), + ] + "#]], + ); +} diff --git a/source/compiler/qsc_passes/src/capabilitiesck/tests_base.rs b/source/compiler/qsc_passes/src/capabilitiesck/tests_base.rs index 3267000f359..3de9f6810c5 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck/tests_base.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck/tests_base.rs @@ -8,14 +8,16 @@ use super::tests_common::{ CALL_TO_CYCLIC_OPERATION_WITH_DYNAMIC_ARGUMENT, CALL_UNRESOLVED_FUNCTION, CUSTOM_MEASUREMENT, CUSTOM_MEASUREMENT_WITH_SIMULATABLE_INTRINSIC_ATTR, CUSTOM_RESET, CUSTOM_RESET_WITH_SIMULATABLE_INTRINSIC_ATTR, DYNAMIC_ARRAY_BINARY_OP, - LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL, - RETURN_WITHIN_DYNAMIC_SCOPE, USE_CLOSURE_FUNCTION, USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, - USE_DYNAMIC_DOUBLE, USE_DYNAMIC_FUNCTION, USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, - USE_DYNAMIC_LHS_EXP_BINOP, USE_DYNAMIC_OPERATION, USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, - USE_DYNAMIC_RANGE, USE_DYNAMIC_RHS_EXP_BINOP, USE_DYNAMIC_STRING, USE_DYNAMIC_UDT, - USE_DYNAMICALLY_SIZED_ARRAY, USE_ENTRY_POINT_INT_ARRAY_IN_TUPLE, - USE_ENTRY_POINT_STATIC_BIG_INT, USE_ENTRY_POINT_STATIC_BOOL, USE_ENTRY_POINT_STATIC_DOUBLE, - USE_ENTRY_POINT_STATIC_INT, USE_ENTRY_POINT_STATIC_INT_IN_TUPLE, USE_ENTRY_POINT_STATIC_PAULI, + LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL, PARALLEL_STATIC_BODY, + PARALLEL_WITH_DYNAMIC_BRANCH, PARALLEL_WITH_INDIRECT_BRANCH_VIA_CALL, + PARALLEL_WITHIN_DYNAMIC_LIMIT, PARALLEL_WITHIN_STATIC_LIMIT, RETURN_WITHIN_DYNAMIC_SCOPE, + USE_CLOSURE_FUNCTION, USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, USE_DYNAMIC_DOUBLE, + USE_DYNAMIC_FUNCTION, USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, USE_DYNAMIC_LHS_EXP_BINOP, + USE_DYNAMIC_OPERATION, USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, USE_DYNAMIC_RANGE, + USE_DYNAMIC_RHS_EXP_BINOP, USE_DYNAMIC_STRING, USE_DYNAMIC_UDT, USE_DYNAMICALLY_SIZED_ARRAY, + USE_ENTRY_POINT_INT_ARRAY_IN_TUPLE, USE_ENTRY_POINT_STATIC_BIG_INT, + USE_ENTRY_POINT_STATIC_BOOL, USE_ENTRY_POINT_STATIC_DOUBLE, USE_ENTRY_POINT_STATIC_INT, + USE_ENTRY_POINT_STATIC_INT_IN_TUPLE, USE_ENTRY_POINT_STATIC_PAULI, USE_ENTRY_POINT_STATIC_RANGE, USE_ENTRY_POINT_STATIC_STRING, check, check_for_exe, }; use expect_test::{Expect, expect}; @@ -939,3 +941,110 @@ fn binary_op_with_dynamic_array_error() { "#]], ); } + +#[test] +fn parallel_with_static_body_yields_no_errors() { + check_profile( + PARALLEL_STATIC_BODY, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn parallel_within_with_static_limit_yields_no_errors() { + check_profile( + PARALLEL_WITHIN_STATIC_LIMIT, + &expect![[r#" + [] + "#]], + ); +} + +#[test] +fn parallel_with_dynamic_branch_yields_error() { + check_profile( + PARALLEL_WITH_DYNAMIC_BRANCH, + &expect![[r#" + [ + UseOfDynamicBool( + Span { + lo: 107, + hi: 122, + }, + ), + UseOfDynamicBool( + Span { + lo: 199, + hi: 200, + }, + ), + ] + "#]], + ); +} + +#[test] +fn parallel_within_with_dynamic_limit_yields_error() { + check_profile( + PARALLEL_WITHIN_DYNAMIC_LIMIT, + &expect![[r#" + [ + UseOfDynamicBool( + Span { + lo: 107, + hi: 122, + }, + ), + UseOfDynamicBool( + Span { + lo: 160, + hi: 161, + }, + ), + UseOfDynamicInt( + Span { + lo: 160, + hi: 161, + }, + ), + UseOfDynamicLimitInParallelExpr( + Span { + lo: 160, + hi: 161, + }, + ), + ] + "#]], + ); +} + +#[test] +fn parallel_with_indirect_branch_via_call_yields_error() { + check_profile( + PARALLEL_WITH_INDIRECT_BRANCH_VIA_CALL, + &expect![[r#" + [ + UseOfDynamicBool( + Span { + lo: 79, + hi: 91, + }, + ), + UseOfDynamicBool( + Span { + lo: 217, + hi: 223, + }, + ), + UseOfDynamicBranchingInParallelExpr( + Span { + lo: 217, + hi: 223, + }, + ), + ] + "#]], + ); +} diff --git a/source/compiler/qsc_passes/src/capabilitiesck/tests_common.rs b/source/compiler/qsc_passes/src/capabilitiesck/tests_common.rs index 922a3501588..d693fb73067 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck/tests_common.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck/tests_common.rs @@ -530,3 +530,60 @@ pub const DYNAMIC_ARRAY_BINARY_OP: &str = r#" MResetEachZ(qs) == [Zero, Zero]; } "#; + +pub const PARALLEL_STATIC_BODY: &str = r#" + namespace Test { + operation Foo() : Unit { + parallel { + use q = Qubit(); + H(q); + } + } + }"#; + +pub const PARALLEL_WITH_DYNAMIC_BRANCH: &str = r#" + namespace Test { + operation Foo() : Unit { + use ctrl = Qubit(); + let b = M(ctrl) == Zero; + parallel { + use q = Qubit(); + if b { H(q); } + } + } + }"#; + +pub const PARALLEL_WITHIN_STATIC_LIMIT: &str = r#" + namespace Test { + operation Foo() : Unit { + parallel within 4 { + use q = Qubit(); + H(q); + } + } + }"#; + +pub const PARALLEL_WITHIN_DYNAMIC_LIMIT: &str = r#" + namespace Test { + operation Foo() : Unit { + use ctrl = Qubit(); + let n = M(ctrl) == Zero ? 2 | 4; + parallel within n { + use q = Qubit(); + H(q); + } + } + }"#; + +pub const PARALLEL_WITH_INDIRECT_BRANCH_VIA_CALL: &str = r#" + namespace Test { + operation Bar(q : Qubit) : Unit { + if M(q) == Zero { X(q); } + } + operation Foo() : Unit { + parallel { + use q = Qubit(); + Bar(q); + } + } + }"#; diff --git a/source/compiler/qsc_passes/src/logic_sep.rs b/source/compiler/qsc_passes/src/logic_sep.rs index fdc2333abc7..4e8d6f9881f 100644 --- a/source/compiler/qsc_passes/src/logic_sep.rs +++ b/source/compiler/qsc_passes/src/logic_sep.rs @@ -178,6 +178,14 @@ impl SepCheck { ExprKind::If(cond, then_expr, else_expr) => { self.handle_if_expr(prior, cond, then_expr, else_expr.as_deref()) } + ExprKind::Parallel(limit, body) => { + if let Some(limit) = limit { + self.op_call_allowed = false; + self.visit_expr(limit); + self.op_call_allowed = prior; + } + self.handle_expr(body, prior) + } ExprKind::Array(_) | ExprKind::ArrayRepeat(..) diff --git a/source/compiler/qsc_rca/src/core.rs b/source/compiler/qsc_rca/src/core.rs index 06b562423b3..1a0b343c09f 100644 --- a/source/compiler/qsc_rca/src/core.rs +++ b/source/compiler/qsc_rca/src/core.rs @@ -35,6 +35,7 @@ pub struct Analyzer<'a> { package_store_compute_properties: InternalPackageStoreComputeProperties, active_contexts: Vec, target_capabilities: TargetCapabilityFlags, + in_parallel_expr: bool, } impl<'a> Analyzer<'a> { @@ -48,6 +49,7 @@ impl<'a> Analyzer<'a> { package_store_compute_properties, active_contexts: Vec::::default(), target_capabilities, + in_parallel_expr: false, } } @@ -239,7 +241,7 @@ impl<'a> Analyzer<'a> { fn analyze_expr_bin_op( &mut self, - op: BinOp, + bin_op: BinOp, lhs_expr_id: ExprId, rhs_expr_id: ExprId, expr_type: &Ty, @@ -256,6 +258,21 @@ impl<'a> Analyzer<'a> { compute_kind = compute_kind.aggregate(lhs_compute_kind); compute_kind = compute_kind.aggregate(rhs_compute_kind); + if self.in_parallel_expr + && matches!(bin_op, BinOp::AndL | BinOp::OrL) + && lhs_compute_kind.is_variable_value_kind() + { + // Binary boolean operators with a variable left-hand side are short-circuiting expressions, which means they incur + // dynamic branching in code-gen. Since this is a parallel expression, we need to track this as a runtime feature. + compute_kind = compute_kind.aggregate_runtime_features( + ComputeKind::Dynamic { + runtime_features: RuntimeFeatureFlags::UseOfDynamicBranchingInParallelExpr, + value_kind: ValueKind::Constant, + }, + ValueKind::Constant, + ); + } + // Additionally, since the new compute kind can be of a different type than its operands (e.g. 1 == 1), // aggregate additional runtime features depending on the binary operator expression's type (if it's dynamic). if let ComputeKind::Dynamic { @@ -264,7 +281,7 @@ impl<'a> Analyzer<'a> { } = &mut compute_kind { let lhs_expr_ty = &self.get_expr(lhs_expr_id).ty; - if (op == BinOp::Eq || op == BinOp::Neq) && is_any_result(lhs_expr_ty) { + if (bin_op == BinOp::Eq || bin_op == BinOp::Neq) && is_any_result(lhs_expr_ty) { // When binary operators on result types are equality or inequality, the Boolean outcome may be a dynamic variable. // In this path, we know at least one side of the comparison is dynamic (constant or variable), so the boolean // that comes from the comparison must be variable. This is the critical source of dynamic variable values @@ -391,6 +408,15 @@ impl<'a> Analyzer<'a> { { *runtime_features |= derive_runtime_features_for_value_kind_associated_to_type(*value_kind, expr_type); + + if self.in_parallel_expr + && runtime_features.contains(RuntimeFeatureFlags::UseOfDynamicBool) + { + // A Call expression that includes use of a dynamic Boolean (before aggreating the features from the + // callee and arguments) means that the callable itself incurs the dynamic Boolean runtime feature. + // This would cause branching in a parallel expression, which requires an additional runtime feature to be tracked. + *runtime_features |= RuntimeFeatureFlags::UseOfDynamicBranchingInParallelExpr; + } } // Aggregate the runtime features of the callee and arguments expressions. @@ -795,6 +821,11 @@ impl<'a> Analyzer<'a> { } _ => {} } + if self.in_parallel_expr { + // A dynamic branch in a parallel expression requires an additional runtime feature to be tracked. + dynamic_runtime_features |= + RuntimeFeatureFlags::UseOfDynamicBranchingInParallelExpr; + } } let dynamic_compute_kind = ComputeKind::Dynamic { runtime_features: dynamic_runtime_features, @@ -1169,7 +1200,11 @@ impl<'a> Analyzer<'a> { } fn analyze_expr_while(&mut self, condition_expr_id: ExprId, block_id: BlockId) -> ComputeKind { - let mut should_emit_classical_loop = self.should_emit_classical_loops(); + // We only want to emit classical loops if the current target capabilities allow it and we are not in a parallel expression. + // Checking both conditions here avoids the speculative generation of loop capabilities in cases where we know it + // wouldn't be allowed anyway. + let mut should_emit_classical_loop = + self.should_emit_classical_loops() && !self.in_parallel_expr; let mut cached_locals_map = if should_emit_classical_loop { Some(self.get_current_application_instance().locals_map.clone()) } else { @@ -1264,11 +1299,33 @@ impl<'a> Analyzer<'a> { panic!("if the loop condition is quantum, the loop expression must be quantum too"); }; *runtime_features |= RuntimeFeatureFlags::LoopWithDynamicCondition; + if self.in_parallel_expr { + // A dynamic loop in a parallel expression requires an additional runtime feature to be tracked. + *runtime_features |= RuntimeFeatureFlags::UseOfDynamicBranchingInParallelExpr; + } } compute_kind } + fn analyze_expr_parallel_limit(&mut self, limit: ExprId) { + self.visit_expr(limit); + // A limit on a parallel expression must be static, so we check that here. + let application_instance = self.get_current_application_instance_mut(); + let limit_compute_kind = *application_instance.get_expr_compute_kind(limit); + if !matches!(limit_compute_kind, ComputeKind::Static) { + // Add the runtime feature of dynamic parallelism to the compute kind of the parallel expression. + let new_limit_compute_kind = limit_compute_kind.aggregate_runtime_features( + ComputeKind::Dynamic { + runtime_features: RuntimeFeatureFlags::UseOfDynamicLimitInParallelExpr, + value_kind: ValueKind::Constant, + }, + ValueKind::Constant, + ); + application_instance.insert_expr_compute_kind(limit, new_limit_compute_kind); + } + } + // Analyzes the currently active callable assuming it is intrinsic. fn analyze_intrinsic_callable(&mut self) { // Check whether the callable has already been analyzed. @@ -1373,6 +1430,8 @@ impl<'a> Analyzer<'a> { // Push the context of the callable the specialization belongs to. self.push_item_context(id.callable); + let previous_in_parallel = self.in_parallel_expr; + self.in_parallel_expr = false; let package = self.package_store.get(id.callable.package); let input_params = package.derive_callable_input_params(callable_decl); let current_callable_context = self.get_current_item_context_mut(); @@ -1414,6 +1473,7 @@ impl<'a> Analyzer<'a> { // Since we are done analyzing the specialization, pop the active item context. let popped_item_id = self.pop_item_context(); assert!(popped_item_id == id.callable); + self.in_parallel_expr = previous_in_parallel; } fn analyze_spec_decl(&mut self, decl: &'a SpecDecl, functor_set_value: FunctorSetValue) { @@ -2015,8 +2075,8 @@ impl<'a> Visitor<'a> for Analyzer<'a> { ExprKind::BinOp(BinOp::Exp, lhs_expr_id, rhs_expr_id) => { self.analyze_expr_bin_op_exp(*lhs_expr_id, *rhs_expr_id) } - ExprKind::BinOp(op, lhs_expr_id, rhs_expr_id) => { - self.analyze_expr_bin_op(*op, *lhs_expr_id, *rhs_expr_id, &expr.ty) + ExprKind::BinOp(bin_op, lhs_expr_id, rhs_expr_id) => { + self.analyze_expr_bin_op(*bin_op, *lhs_expr_id, *rhs_expr_id, &expr.ty) } ExprKind::Block(block_id) => self.analyze_expr_block(*block_id), ExprKind::Call(callee_expr_id, args_expr_id) => { @@ -2043,6 +2103,20 @@ impl<'a> Visitor<'a> for Analyzer<'a> { ExprKind::Index(array_expr_id, index_expr_id) => { self.analyze_expr_index(*array_expr_id, *index_expr_id, &expr.ty) } + ExprKind::Parallel(limit, body) => { + if let Some(limit) = limit { + self.analyze_expr_parallel_limit(*limit); + } + // We need to track when we are analyzing a parallel expression to understand whether certain constructs should + // be allowed. + let previous_in_parallel_expr = self.in_parallel_expr; + self.in_parallel_expr = true; + self.visit_expr(*body); + self.in_parallel_expr = previous_in_parallel_expr; + // The compute kind of a parallel expression is the same as the compute kind of its inner expression. + let application_instance = self.get_current_application_instance(); + *application_instance.get_expr_compute_kind(*body) + } ExprKind::Range(start_expr_id, step_expr_id, end_expr_id) => self.analyze_expr_range( start_expr_id.to_owned(), step_expr_id.to_owned(), diff --git a/source/compiler/qsc_rca/src/errors.rs b/source/compiler/qsc_rca/src/errors.rs index df686922276..36a51f60088 100644 --- a/source/compiler/qsc_rca/src/errors.rs +++ b/source/compiler/qsc_rca/src/errors.rs @@ -236,8 +236,25 @@ pub enum Error { #[diagnostic(url("https://aka.ms/qdk.qir#use-of-dynamic-qubit-release"))] #[diagnostic(code("Qdk.Qsc.CapabilitiesCk.UseOfDynamicQubitRelease"))] UseOfDynamicQubitRelease(#[label] Span), + + #[error("cannot use dynamic branching in parallel expression")] + #[diagnostic(help( + "using branching based on a measurement result in a parallel expression is not supported by the configured target profile" + ))] + #[diagnostic(url("https://aka.ms/qdk.qir#use-of-dynamic-branching-in-parallel-expr"))] + #[diagnostic(code("Qdk.Qsc.CapabilitiesCk.UseOfDynamicBranchingInParallelExpr"))] + UseOfDynamicBranchingInParallelExpr(#[label] Span), + + #[error("cannot use dynamic limit for a parallel expression")] + #[diagnostic(help( + "using a dynamic limit for a parallel expression is not supported by the configured target profile" + ))] + #[diagnostic(url("https://aka.ms/qdk.qir#use-of-dynamic-limit-in-parallel-expr"))] + #[diagnostic(code("Qdk.Qsc.CapabilitiesCk.UseOfDynamicLimitInParallelExpr"))] + UseOfDynamicLimitInParallelExpr(#[label] Span), } +#[allow(clippy::too_many_lines)] #[must_use] pub fn generate_errors_from_runtime_features( runtime_features: RuntimeFeatureFlags, @@ -334,6 +351,12 @@ pub fn generate_errors_from_runtime_features( if runtime_features.contains(RuntimeFeatureFlags::UseOfDynamicQubitRelease) { errors.push(Error::UseOfDynamicQubitRelease(span)); } + if runtime_features.contains(RuntimeFeatureFlags::UseOfDynamicBranchingInParallelExpr) { + errors.push(Error::UseOfDynamicBranchingInParallelExpr(span)); + } + if runtime_features.contains(RuntimeFeatureFlags::UseOfDynamicLimitInParallelExpr) { + errors.push(Error::UseOfDynamicLimitInParallelExpr(span)); + } errors } diff --git a/source/compiler/qsc_rca/src/lib.rs b/source/compiler/qsc_rca/src/lib.rs index 9b1d3a77cf5..35aadb0cf32 100644 --- a/source/compiler/qsc_rca/src/lib.rs +++ b/source/compiler/qsc_rca/src/lib.rs @@ -660,7 +660,7 @@ bitflags! { /// Runtime features represent anything a program can do that is more complex than executing quantum operations on /// statically allocated qubits and using constant arguments. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct RuntimeFeatureFlags: u32 { + pub struct RuntimeFeatureFlags: u64 { /// Use of a dynamic `Bool`. const UseOfDynamicBool = 1 << 0; /// Use of a dynamic `Int`. @@ -725,6 +725,10 @@ bitflags! { const UseOfDynamicQubitRelease = 1 << 30; /// A callable whose required features mean it must be inlined rather than emitted as an IR function. const MustBeInlined = 1 << 31; + /// Use of dynamic branching in a parallel expression. + const UseOfDynamicBranchingInParallelExpr = 1 << 32; + /// Use of a dynamic limit in a parallel expression. + const UseOfDynamicLimitInParallelExpr = 1 << 33; } } @@ -743,6 +747,7 @@ impl RuntimeFeatureFlags { } /// Maps program constructs to target capabilities. + #[allow(clippy::too_many_lines)] #[must_use] pub fn target_capabilities(&self) -> TargetCapabilityFlags { let mut capabilities = TargetCapabilityFlags::empty(); @@ -841,6 +846,12 @@ impl RuntimeFeatureFlags { if self.contains(RuntimeFeatureFlags::UseOfDynamicQubitRelease) { capabilities |= TargetCapabilityFlags::DynamicQubitAllocation; } + if self.contains(RuntimeFeatureFlags::UseOfDynamicBranchingInParallelExpr) { + capabilities |= TargetCapabilityFlags::HigherLevelConstructs; + } + if self.contains(RuntimeFeatureFlags::UseOfDynamicLimitInParallelExpr) { + capabilities |= TargetCapabilityFlags::HigherLevelConstructs; + } capabilities } diff --git a/source/compiler/qsc_rca/src/tests.rs b/source/compiler/qsc_rca/src/tests.rs index 01ca91a68ef..a6c54403611 100644 --- a/source/compiler/qsc_rca/src/tests.rs +++ b/source/compiler/qsc_rca/src/tests.rs @@ -15,6 +15,7 @@ mod lambdas; mod loops; mod measurements; mod overrides; +mod parallel; mod qubits; mod return_unify_interactions; mod strings; diff --git a/source/compiler/qsc_rca/src/tests/parallel.rs b/source/compiler/qsc_rca/src/tests/parallel.rs new file mode 100644 index 00000000000..76a2dd7f179 --- /dev/null +++ b/source/compiler/qsc_rca/src/tests/parallel.rs @@ -0,0 +1,431 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{ + CompilationContext, check_callable_compute_properties, check_last_statement_compute_properties, +}; +use expect_test::expect; + +#[test] +fn check_rca_for_parallel_expr_with_static_body() { + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + let e = parallel { }; + e"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_last_statement_compute_properties( + package_store_compute_properties, + &expect![[r#" + ApplicationsGeneratorSet: + inherent: Static + dynamic_param_applications: "#]], + ); +} + +#[test] +fn check_rca_for_parallel_within_with_static_limit_and_body() { + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + let e = parallel within 4 { }; + e"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_last_statement_compute_properties( + package_store_compute_properties, + &expect![[r#" + ApplicationsGeneratorSet: + inherent: Static + dynamic_param_applications: "#]], + ); +} + +#[test] +fn check_rca_for_parallel_with_dynamic_operations_no_branching() { + // A parallel body that allocates a qubit, applies gates, and measures — making the overall + // operation dynamic — but contains no conditional branching and therefore + // does NOT produce UseOfDynamicBranchingInParallelExpr. + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + operation Foo() : Unit { + use q = Qubit(); + let _ = parallel { + use p = Qubit(); + H(p); + M(p) + }; + }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_callable_compute_properties( + &compilation_context.fir_store, + package_store_compute_properties, + "Foo", + &expect![[r#" + Callable: CallableComputeProperties: + body: ApplicationsGeneratorSet: + inherent: Dynamic: + runtime_features: RuntimeFeatureFlags(QubitAllocation) + value_kind: Constant + dynamic_param_applications: + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn check_rca_for_parallel_within_with_dynamic_operations_no_branching() { + // Same validation as above but using `parallel within` with a static limit. + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + operation Foo() : Unit { + use q = Qubit(); + let _ = parallel within 4 { + use p = Qubit(); + H(p); + M(p) + }; + }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_callable_compute_properties( + &compilation_context.fir_store, + package_store_compute_properties, + "Foo", + &expect![[r#" + Callable: CallableComputeProperties: + body: ApplicationsGeneratorSet: + inherent: Dynamic: + runtime_features: RuntimeFeatureFlags(QubitAllocation) + value_kind: Constant + dynamic_param_applications: + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn check_rca_for_parallel_with_dynamic_if_in_body() { + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + operation Foo() : Unit { + use q = Qubit(); + parallel { + if M(q) == Zero { + H(q); + } + } + }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_callable_compute_properties( + &compilation_context.fir_store, + package_store_compute_properties, + "Foo", + &expect![[r#" + Callable: CallableComputeProperties: + body: ApplicationsGeneratorSet: + inherent: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | QubitAllocation | UseOfDynamicBranchingInParallelExpr) + value_kind: Constant + dynamic_param_applications: + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn check_rca_for_parallel_with_short_circuit_bool_in_body() { + // Short-circuiting `&&`/`||` with a variable LHS incurs dynamic branching in code gen. + // When inside a parallel expression, this triggers UseOfDynamicBranchingInParallelExpr. + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + operation Foo() : Unit { + use q = Qubit(); + parallel { + let b = (M(q) == Zero) and (M(q) == One); + } + }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_callable_compute_properties( + &compilation_context.fir_store, + package_store_compute_properties, + "Foo", + &expect![[r#" + Callable: CallableComputeProperties: + body: ApplicationsGeneratorSet: + inherent: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | QubitAllocation | UseOfDynamicBranchingInParallelExpr) + value_kind: Constant + dynamic_param_applications: + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn check_rca_for_parallel_with_while_loop_with_dynamic_condition() { + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + operation Foo() : Unit { + use q = Qubit(); + parallel { + while M(q) == Zero { + H(q); + } + } + }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_callable_compute_properties( + &compilation_context.fir_store, + package_store_compute_properties, + "Foo", + &expect![[r#" + Callable: CallableComputeProperties: + body: ApplicationsGeneratorSet: + inherent: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | MeasurementWithinDynamicScope | LoopWithDynamicCondition | QubitAllocation | UseOfDynamicBranchingInParallelExpr) + value_kind: Constant + dynamic_param_applications: + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn check_rca_for_parallel_within_with_dynamic_limit() { + // The UseOfDynamicLimitInParallelExpr runtime feature is stored on the limit expression's + // compute kind by the RCA, but is not propagated to the callable-level compute properties. + // The callable-level features reflect only the dynamic values used in the body (Bool and Int + // from the conditional). The UseOfDynamicLimitInParallelExpr flag is checked and surfaced as + // an error by the capabilities check pass (see capabilitiesck tests). + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + operation Foo() : Unit { + use q = Qubit(); + let n = M(q) == Zero ? 2 | 4; + parallel within n { } + }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_callable_compute_properties( + &compilation_context.fir_store, + package_store_compute_properties, + "Foo", + &expect![[r#" + Callable: CallableComputeProperties: + body: ApplicationsGeneratorSet: + inherent: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | UseOfDynamicInt | QubitAllocation) + value_kind: Constant + dynamic_param_applications: + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn check_rca_for_nested_parallel_with_dynamic_if_in_inner_body() { + // Dynamic branching in the inner parallel propagates out to the outer parallel's compute kind + // since the outer parallel's compute kind is derived from its body. + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + operation Foo() : Unit { + use q = Qubit(); + parallel { + parallel { + if M(q) == Zero { + H(q); + } + } + } + }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_callable_compute_properties( + &compilation_context.fir_store, + package_store_compute_properties, + "Foo", + &expect![[r#" + Callable: CallableComputeProperties: + body: ApplicationsGeneratorSet: + inherent: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | QubitAllocation | UseOfDynamicBranchingInParallelExpr) + value_kind: Constant + dynamic_param_applications: + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn check_rca_for_parallel_calling_operation_that_branches_dynamically() { + // Bar measures a qubit and branches on the result, making it dynamic with UseOfDynamicBool. + // When Foo calls Bar inside a parallel expression, the RCA detects that the call involves a + // dynamic bool and adds UseOfDynamicBranchingInParallelExpr to Foo's compute properties. + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + operation Bar(q : Qubit) : Unit { + if M(q) == Zero { + H(q); + } + } + operation Foo() : Unit { + use q = Qubit(); + parallel { + Bar(q); + } + }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_callable_compute_properties( + &compilation_context.fir_store, + package_store_compute_properties, + "Bar", + &expect![[r#" + Callable: CallableComputeProperties: + body: ApplicationsGeneratorSet: + inherent: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool) + value_kind: Constant + dynamic_param_applications: + [0]: [Parameter Type Element] ElementParamApplication: + constant: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool) + value_kind: Constant + variable: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | UseOfDynamicQubit) + value_kind: Constant + adj: + ctl: + ctl-adj: "#]], + ); + check_callable_compute_properties( + &compilation_context.fir_store, + package_store_compute_properties, + "Foo", + &expect![[r#" + Callable: CallableComputeProperties: + body: ApplicationsGeneratorSet: + inherent: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | QubitAllocation | UseOfDynamicBranchingInParallelExpr) + value_kind: Constant + dynamic_param_applications: + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn check_rca_for_parallel_within_calling_operation_that_branches_dynamically() { + // Same as above but using `parallel within` with a static limit. + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + operation Bar(q : Qubit) : Unit { + if M(q) == Zero { + H(q); + } + } + operation Foo() : Unit { + use q = Qubit(); + parallel within 4 { + Bar(q); + } + }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_callable_compute_properties( + &compilation_context.fir_store, + package_store_compute_properties, + "Bar", + &expect![[r#" + Callable: CallableComputeProperties: + body: ApplicationsGeneratorSet: + inherent: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool) + value_kind: Constant + dynamic_param_applications: + [0]: [Parameter Type Element] ElementParamApplication: + constant: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool) + value_kind: Constant + variable: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | UseOfDynamicQubit) + value_kind: Constant + adj: + ctl: + ctl-adj: "#]], + ); + check_callable_compute_properties( + &compilation_context.fir_store, + package_store_compute_properties, + "Foo", + &expect![[r#" + Callable: CallableComputeProperties: + body: ApplicationsGeneratorSet: + inherent: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | QubitAllocation | UseOfDynamicBranchingInParallelExpr) + value_kind: Constant + dynamic_param_applications: + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn check_rca_for_parallel_with_dynamic_arg_to_rotation_does_not_branch() { + // A dynamic Double computed outside the parallel expression can be freely used as an + // argument to a gate inside it. Passing a dynamic value to a call does not incur branching, + // so UseOfDynamicBranchingInParallelExpr must NOT appear in the compute properties. + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + operation Foo() : Unit { + import Std.Convert.*; + use q = Qubit(); + let angle = M(q) == Zero ? 1.0 | 2.0; + parallel { + use p = Qubit(); + Rx(angle, p); + } + }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_callable_compute_properties( + &compilation_context.fir_store, + package_store_compute_properties, + "Foo", + &expect![[r#" + Callable: CallableComputeProperties: + body: ApplicationsGeneratorSet: + inherent: Dynamic: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | UseOfDynamicDouble | QubitAllocation) + value_kind: Constant + dynamic_param_applications: + adj: + ctl: + ctl-adj: "#]], + ); +} diff --git a/source/vscode/syntaxes/qsharp.tmLanguage.json b/source/vscode/syntaxes/qsharp.tmLanguage.json index 4bd2973a35e..b583596c3c6 100644 --- a/source/vscode/syntaxes/qsharp.tmLanguage.json +++ b/source/vscode/syntaxes/qsharp.tmLanguage.json @@ -75,7 +75,7 @@ "patterns": [ { "name": "keyword.control.qsharp", - "match": "\\b(elif|else|repeat|until|fixup|for|in|while|return|fail|within|apply)\\b" + "match": "\\b(elif|else|repeat|until|fixup|for|in|while|return|fail|within|apply|parallel)\\b" }, { "comment": "legacy allocation keywords (QDK <1.0)",