From 2071da628ef13eab251676038a95ee8ea5c6f782 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Wed, 8 Apr 2026 23:59:43 -0700 Subject: [PATCH 1/5] New `parallel` expression 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 expression 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 the previously deffered qubits will be made available for reuse. 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_ast/src/ast.rs | 8 + source/compiler/qsc_ast/src/mut_visit.rs | 9 +- source/compiler/qsc_ast/src/visit.rs | 9 +- source/compiler/qsc_codegen/src/qsharp.rs | 10 + source/compiler/qsc_eval/src/lib.rs | 275 ++++++++++++++++-- source/compiler/qsc_eval/src/val.rs | 2 +- source/compiler/qsc_fir/src/fir.rs | 13 + source/compiler/qsc_fir/src/mut_visit.rs | 6 + source/compiler/qsc_fir/src/visit.rs | 6 + source/compiler/qsc_frontend/src/lower.rs | 7 + .../compiler/qsc_frontend/src/typeck/rules.rs | 9 +- source/compiler/qsc_hir/src/hir.rs | 9 + 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 | 32 ++ source/compiler/qsc_parse/src/keyword.rs | 3 + source/compiler/qsc_partial_eval/src/lib.rs | 67 ++++- .../qsc_partial_eval/src/management.rs | 38 ++- .../compiler/qsc_passes/src/capabilitiesck.rs | 6 + source/compiler/qsc_passes/src/logic_sep.rs | 8 + source/compiler/qsc_rca/src/core.rs | 81 +++++- source/compiler/qsc_rca/src/errors.rs | 23 ++ source/compiler/qsc_rca/src/lib.rs | 13 +- source/qdk_package/src/qdk.egg-info/PKG-INFO | 130 +++++++++ .../qdk_package/src/qdk.egg-info/SOURCES.txt | 24 ++ .../src/qdk.egg-info/dependency_links.txt | 1 + .../qdk_package/src/qdk.egg-info/requires.txt | 24 ++ .../src/qdk.egg-info/top_level.txt | 1 + source/vscode/syntaxes/qsharp.tmLanguage.json | 2 +- 32 files changed, 802 insertions(+), 53 deletions(-) create mode 100644 source/qdk_package/src/qdk.egg-info/PKG-INFO create mode 100644 source/qdk_package/src/qdk.egg-info/SOURCES.txt create mode 100644 source/qdk_package/src/qdk.egg-info/dependency_links.txt create mode 100644 source/qdk_package/src/qdk.egg-info/requires.txt create mode 100644 source/qdk_package/src/qdk.egg-info/top_level.txt diff --git a/source/compiler/qsc_ast/src/ast.rs b/source/compiler/qsc_ast/src/ast.rs index 1e2b420ffe..e280a1a37b 100644 --- a/source/compiler/qsc_ast/src/ast.rs +++ b/source/compiler/qsc_ast/src/ast.rs @@ -917,6 +917,10 @@ pub enum ExprKind { Lambda(CallableKind, Box, Box), /// A literal. Lit(Box), + /// A parallel expression: `parallel a` + Parallel(Box), + /// A parallel-limited expression: `parallel within n a` + ParallelLimited(Box, Box), /// Parentheses: `(a)`. Paren(Box), /// A path: `a` or `a.b`. @@ -964,6 +968,10 @@ impl Display for ExprKind { ExprKind::Interpolate(components) => display_interpolate(indent, components)?, ExprKind::Lambda(kind, param, expr) => display_lambda(indent, *kind, param, expr)?, ExprKind::Lit(lit) => write!(indent, "Lit: {lit}")?, + ExprKind::Parallel(e) => write!(indent, "Parallel: {e}")?, + ExprKind::ParallelLimited(limit, body) => { + write!(indent, "ParallelLimited: {limit} {body}")?; + } ExprKind::Paren(e) => write!(indent, "Paren: {e}")?, ExprKind::Path(p) => write!(indent, "Path: {p}")?, ExprKind::Range(start, step, end) => { diff --git a/source/compiler/qsc_ast/src/mut_visit.rs b/source/compiler/qsc_ast/src/mut_visit.rs index ce80b9ca87..f2c11a5450 100644 --- a/source/compiler/qsc_ast/src/mut_visit.rs +++ b/source/compiler/qsc_ast/src/mut_visit.rs @@ -362,9 +362,16 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { vis.visit_pat(pat); vis.visit_expr(expr); } - ExprKind::Paren(expr) | ExprKind::Return(expr) | ExprKind::UnOp(_, expr) => { + ExprKind::Parallel(expr) + | ExprKind::Paren(expr) + | ExprKind::Return(expr) + | ExprKind::UnOp(_, expr) => { vis.visit_expr(expr); } + ExprKind::ParallelLimited(limit, body) => { + vis.visit_expr(limit); + vis.visit_expr(body); + } ExprKind::Path(path) => vis.visit_path_kind(path), ExprKind::Range(start, step, end) => { for s in start.iter_mut() { diff --git a/source/compiler/qsc_ast/src/visit.rs b/source/compiler/qsc_ast/src/visit.rs index f123b320a0..9e2149f000 100644 --- a/source/compiler/qsc_ast/src/visit.rs +++ b/source/compiler/qsc_ast/src/visit.rs @@ -334,9 +334,16 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { vis.visit_pat(pat); vis.visit_expr(expr); } - ExprKind::Paren(expr) | ExprKind::Return(expr) | ExprKind::UnOp(_, expr) => { + ExprKind::Parallel(expr) + | ExprKind::Paren(expr) + | ExprKind::Return(expr) + | ExprKind::UnOp(_, expr) => { vis.visit_expr(expr); } + ExprKind::ParallelLimited(limit, body) => { + vis.visit_expr(limit); + vis.visit_expr(body); + } ExprKind::Path(path) => vis.visit_path_kind(path), ExprKind::Range(start, step, end) => { if let Some(s) = start.as_ref() { diff --git a/source/compiler/qsc_codegen/src/qsharp.rs b/source/compiler/qsc_codegen/src/qsharp.rs index 71eee20519..3abe2c0efd 100644 --- a/source/compiler/qsc_codegen/src/qsharp.rs +++ b/source/compiler/qsc_codegen/src/qsharp.rs @@ -540,6 +540,16 @@ impl Visitor<'_> for QSharpGen { } self.visit_expr(expr); } + ExprKind::Parallel(expr) => { + self.write("parallel "); + self.visit_expr(expr); + } + ExprKind::ParallelLimited(limit, body) => { + self.write("parallel within "); + self.visit_expr(limit); + self.write(" "); + self.visit_expr(body); + } ExprKind::Paren(expr) => { self.write("("); self.visit_expr(expr); diff --git a/source/compiler/qsc_eval/src/lib.rs b/source/compiler/qsc_eval/src/lib.rs index d16ab88a08..bd0e939491 100644 --- a/source/compiler/qsc_eval/src/lib.rs +++ b/source/compiler/qsc_eval/src/lib.rs @@ -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(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(has_limit)) => { + let limit = if *has_limit { + 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(()) @@ -1335,28 +1398,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() @@ -1866,6 +1944,141 @@ impl State { } } +#[derive(Debug, Default)] +struct DelayedQubitReleaseLayer { + released_qubits: Vec, + available_qubits: VecDeque, + used_qubits: FxHashSet, + limit: Option, +} + +#[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, .. + }) = self.layers.last_mut() + { + released_qubits.push(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 released 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, + }) = self.layers.last_mut() + { + if let Some(limit) = limit + && released_qubits.len() >= *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/val.rs b/source/compiler/qsc_eval/src/val.rs index b1c392d2a8..35c509849d 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 b73482c987..a7b6da9e92 100644 --- a/source/compiler/qsc_fir/src/fir.rs +++ b/source/compiler/qsc_fir/src/fir.rs @@ -1036,6 +1036,10 @@ pub enum ExecGraphNode { Unit, /// The end of the control flow graph. Ret, + /// The start of a parallel region, with a Boolean indicating whether it has a limit. + ParStart(bool), + /// The end of a parallel region + ParEnd, /// A node only to be executed in debug mode. Debug(ExecGraphDebugNode), } @@ -1208,6 +1212,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 `...`. @@ -1255,6 +1261,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 7cd88f5a4e..ca1e6b1658 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 b0461a3edf..94a8c7071d 100644 --- a/source/compiler/qsc_fir/src/visit.rs +++ b/source/compiler/qsc_fir/src/visit.rs @@ -172,6 +172,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_frontend/src/lower.rs b/source/compiler/qsc_frontend/src/lower.rs index aaccb920d8..de616af5f4 100644 --- a/source/compiler/qsc_frontend/src/lower.rs +++ b/source/compiler/qsc_frontend/src/lower.rs @@ -797,6 +797,13 @@ impl With<'_> { self.lower_lambda(lambda, expr.span) } ast::ExprKind::Lit(lit) => self.lower_lit(lit), + ast::ExprKind::Parallel(expr) => { + hir::ExprKind::Parallel(None, Box::new(self.lower_expr(expr))) + } + ast::ExprKind::ParallelLimited(limit, body) => hir::ExprKind::Parallel( + Some(Box::new(self.lower_expr(limit))), + Box::new(self.lower_expr(body)), + ), ast::ExprKind::Paren(_) => unreachable!("parentheses should be removed earlier"), ast::ExprKind::Path(PathKind::Ok(path)) => { let args = self diff --git a/source/compiler/qsc_frontend/src/typeck/rules.rs b/source/compiler/qsc_frontend/src/typeck/rules.rs index 677d0cf0f2..3e18091b78 100644 --- a/source/compiler/qsc_frontend/src/typeck/rules.rs +++ b/source/compiler/qsc_frontend/src/typeck/rules.rs @@ -455,7 +455,14 @@ impl<'a> Context<'a> { Lit::Result(_) => converge(Ty::Prim(Prim::Result)), Lit::String(_) => converge(Ty::Prim(Prim::String)), }, - ExprKind::Paren(expr) => self.infer_expr(expr), + ExprKind::Paren(expr) | ExprKind::Parallel(expr) => self.infer_expr(expr), + ExprKind::ParallelLimited(limit, body) => { + let limit_span = limit.span; + let limit = self.infer_expr(limit); + self.inferrer.eq(limit_span, Ty::Prim(Prim::Int), limit.ty); + 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 81b2a371a8..936a5bb23b 100644 --- a/source/compiler/qsc_hir/src/hir.rs +++ b/source/compiler/qsc_hir/src/hir.rs @@ -708,6 +708,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 { ... }`. @@ -762,6 +764,13 @@ 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) => { + if let Some(limit) = limit { + write!(indent, "Parallel({limit}): {expr}")?; + } else { + write!(indent, "Parallel: {expr}")?; + } + } ExprKind::Range(start, step, end) => { display_range(indent, start.as_deref(), step.as_deref(), end.as_deref())?; } diff --git a/source/compiler/qsc_hir/src/mut_visit.rs b/source/compiler/qsc_hir/src/mut_visit.rs index 50a8fc4b56..d80e18574d 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 510c79dece..1942c4752a 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 10b035d4d1..4d07c04cf4 100644 --- a/source/compiler/qsc_lowerer/src/lib.rs +++ b/source/compiler/qsc_lowerer/src/lib.rs @@ -682,6 +682,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())); + 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)); @@ -799,7 +808,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 1d04a59220..2a4c05383c 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 6dd3741b9c..2f167d5e7e 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) | ExprKind::ParallelLimited(.., 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::ParallelLimited(limit, body))) + } else { + Ok(Box::new(ExprKind::Parallel(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 7786259ad3..a9b9cd4c19 100644 --- a/source/compiler/qsc_parse/src/expr/tests.rs +++ b/source/compiler/qsc_parse/src/expr/tests.rs @@ -3117,3 +3117,35 @@ fn call_with_incomplete_struct_arg() { ]"#]], ); } + +#[test] +fn parallel_expr() { + check( + expr, + "parallel x", + &expect![[ + r#"Expr _id_ [0-10]: Parallel: Expr _id_ [9-10]: Path: Path _id_ [9-10] (Ident _id_ [9-10] "x")"# + ]], + ); +} + +#[test] +fn parallel_limited_expr() { + check( + expr, + "parallel within 4 { }", + &expect![[r#" + Expr _id_ [0-21]: ParallelLimited: Expr _id_ [16-17]: Lit: Int(4) 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]: ParallelLimited: Expr _id_ [16-17]: Lit: Int(2) Expr _id_ [18-19]: Path: Path _id_ [18-19] (Ident _id_ [18-19] "x")"# + ]], + ); +} diff --git a/source/compiler/qsc_parse/src/keyword.rs b/source/compiler/qsc_parse/src/keyword.rs index e87c0ef6a9..89e526cdff 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 b71ce650c3..745ad9b9f8 100644 --- a/source/compiler/qsc_partial_eval/src/lib.rs +++ b/source/compiler/qsc_partial_eval/src/lib.rs @@ -869,6 +869,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 => { @@ -877,12 +878,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:?}"), }; @@ -940,7 +941,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(); @@ -1132,9 +1136,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); @@ -1287,6 +1294,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(_, _, _) => Err(Error::Unexpected( "dynamic ranges are invalid".to_string(), expr_package_span, @@ -1961,6 +1971,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 { @@ -2502,6 +2513,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 { @@ -3136,7 +3149,6 @@ impl<'a> PartialEvaluator<'a> { )); }; self.resource_manager.release_qubit(&qubit); - // The value of a qubit release is unit. Ok(Value::unit()) } @@ -4043,6 +4055,49 @@ impl<'a> PartialEvaluator<'a> { Ok(Value::Var(eval_variable)) } + + 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(()) + } } #[derive(Default)] diff --git a/source/compiler/qsc_partial_eval/src/management.rs b/source/compiler/qsc_partial_eval/src/management.rs index 3712772040..ab5f62dbb7 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_passes/src/capabilitiesck.rs b/source/compiler/qsc_passes/src/capabilitiesck.rs index 2bf707903d..46bc6574e1 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck.rs @@ -177,6 +177,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/logic_sep.rs b/source/compiler/qsc_passes/src/logic_sep.rs index 433eaa4f9a..0b08113b9b 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 30f0b7da32..178e47c4c3 100644 --- a/source/compiler/qsc_rca/src/core.rs +++ b/source/compiler/qsc_rca/src/core.rs @@ -33,6 +33,7 @@ pub struct Analyzer<'a> { package_store_compute_properties: InternalPackageStoreComputeProperties, active_contexts: Vec, target_capabilities: TargetCapabilityFlags, + in_parallel_expr: bool, } impl<'a> Analyzer<'a> { @@ -46,6 +47,7 @@ impl<'a> Analyzer<'a> { package_store_compute_properties, active_contexts: Vec::::default(), target_capabilities, + in_parallel_expr: false, } } @@ -237,6 +239,7 @@ impl<'a> Analyzer<'a> { fn analyze_expr_bin_op( &mut self, + bin_op: BinOp, lhs_expr_id: ExprId, rhs_expr_id: ExprId, expr_type: &Ty, @@ -253,6 +256,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 { @@ -382,6 +400,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. @@ -722,6 +749,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, @@ -1088,7 +1120,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 { @@ -1183,11 +1219,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. @@ -1288,6 +1346,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(); @@ -1328,6 +1388,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) { @@ -1921,8 +1982,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(_, lhs_expr_id, rhs_expr_id) => { - self.analyze_expr_bin_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) => { @@ -1949,6 +2010,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 14b9249adf..bbdfa933b6 100644 --- a/source/compiler/qsc_rca/src/errors.rs +++ b/source/compiler/qsc_rca/src/errors.rs @@ -252,8 +252,25 @@ pub enum Error { #[diagnostic(url("https://aka.ms/qdk.qir#use-of-dynamic-generic"))] #[diagnostic(code("Qsc.CapabilitiesCk.UseOfDynamicGeneric"))] UseOfDynamicGeneric(#[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("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("Qsc.CapabilitiesCk.UseOfDynamicLimitInParallelExpr"))] + UseOfDynamicLimitInParallelExpr(#[label] Span), } +#[allow(clippy::too_many_lines)] #[must_use] pub fn generate_errors_from_runtime_features( runtime_features: RuntimeFeatureFlags, @@ -356,6 +373,12 @@ pub fn generate_errors_from_runtime_features( if runtime_features.contains(RuntimeFeatureFlags::UseOfDynamicGeneric) { errors.push(Error::UseOfDynamicGeneric(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 2c1d8770b8..a60a60b0d0 100644 --- a/source/compiler/qsc_rca/src/lib.rs +++ b/source/compiler/qsc_rca/src/lib.rs @@ -601,7 +601,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`. @@ -666,6 +666,10 @@ bitflags! { const CallToCustomReset = 1 << 30; /// Use of a dynamic generic parameter. const UseOfDynamicGeneric = 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; } } @@ -684,6 +688,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(); @@ -788,6 +793,12 @@ impl RuntimeFeatureFlags { if self.contains(RuntimeFeatureFlags::UseOfDynamicGeneric) { capabilities |= TargetCapabilityFlags::HigherLevelConstructs; } + if self.contains(RuntimeFeatureFlags::UseOfDynamicBranchingInParallelExpr) { + capabilities |= TargetCapabilityFlags::HigherLevelConstructs; + } + if self.contains(RuntimeFeatureFlags::UseOfDynamicLimitInParallelExpr) { + capabilities |= TargetCapabilityFlags::HigherLevelConstructs; + } capabilities } diff --git a/source/qdk_package/src/qdk.egg-info/PKG-INFO b/source/qdk_package/src/qdk.egg-info/PKG-INFO new file mode 100644 index 0000000000..001e573acc --- /dev/null +++ b/source/qdk_package/src/qdk.egg-info/PKG-INFO @@ -0,0 +1,130 @@ +Metadata-Version: 2.4 +Name: qdk +Version: 0.0.0 +Summary: Quantum Development Kit Python Package +Author: Microsoft +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +Requires-Dist: qsharp==0.0.0 +Requires-Dist: pyqir<0.12 +Provides-Extra: jupyter +Requires-Dist: qsharp-widgets==0.0.0; extra == "jupyter" +Requires-Dist: qsharp-jupyterlab==0.0.0; extra == "jupyter" +Provides-Extra: azure +Requires-Dist: azure-quantum>=3.8.0; extra == "azure" +Provides-Extra: qiskit +Requires-Dist: qiskit<3.0.0,>=1.2.2; extra == "qiskit" +Provides-Extra: cirq +Requires-Dist: cirq-core<1.7,>=1.6.1; extra == "cirq" +Requires-Dist: cirq-ionq<1.7,>=1.6.1; extra == "cirq" +Provides-Extra: all +Requires-Dist: qsharp-widgets==0.0.0; extra == "all" +Requires-Dist: azure-quantum>=3.8.0; extra == "all" +Requires-Dist: qiskit<3.0.0,>=1.2.2; extra == "all" +Requires-Dist: cirq-core<1.7,>=1.6.1; extra == "all" +Requires-Dist: cirq-ionq<1.7,>=1.6.1; extra == "all" +Requires-Dist: qsharp-jupyterlab==0.0.0; extra == "all" + +# qdk + +The Quantum Development Kit (QDK) provides a single, cohesive Python entry point for compiling, simulating, and estimating resources for quantum programs (Q# and OpenQASM), with optional extras for visualization, cloud workflows, and interoperability with Qiskit and Cirq. + +## Install + +To install the core functionality, which include Q\# \& OpenQASM simulation, compilation, and resource estimation support: + +```bash +pip install qdk +``` + +To include the Jupyter extra, which adds visualizations using Jupyter Widgets in the `qdk.widgets` submodule and syntax highlighting for Jupyter notebooks in the browser: + +```bash +pip install "qdk[jupyter]" +``` + +To add the Azure Quantum extra, which includes functionality for working with the Azure Quantum service in the `qdk.azure` submodule: + +```bash +pip install "qdk[azure]" +``` + +For Qiskit integration, which exposes Qiskit interop utilities in the `qdk.qiskit` submodule: + +```bash +pip install "qdk[qiskit]" +``` + +For Cirq integration, which exposes Cirq interop utilities in the `qdk.azure.cirq` submodule: + +```bash +pip install "qdk[cirq]" +``` + +To easily install all the above extras: + +```bash +pip install "qdk[all]" +``` + +## Quick Start + +```python +from qdk import qsharp + +result = qsharp.run("{ use q = Qubit(); H(q); return MResetZ(q); }", shots=100) +print(result) +``` + +To use widgets (installed via `qdk[jupyter]` extra): + +```python +from qdk.qsharp import eval, run +from qdk.widgets import Histogram + +eval(""" +operation BellPair() : Result[] { + use qs = Qubit[2]; + H(qs[0]);CX(qs[0], qs[1]); + MResetEachZ(qs) +} +""") +results = run("BellPair()", shots=1000, noise=(0.005, 0.0, 0.0)) +Histogram(results) +``` + +## Public API Surface + +Submodules: + +- `qdk.qsharp` – exports the same APIs as the `qsharp` Python package +- `qdk.openqasm` – exports the same APIs as the `openqasm` submodule of the `qsharp` Python package. +- `qdk.estimator` – exports the same APIs as the `estimator` submodule of the `qsharp` Python package. +- `qdk.widgets` – exports the Jupyter widgets available from the `qsharp-widgets` Python package (requires the `qdk[jupyter]` extra to be installed). +- `qdk.azure` – exports the Python APIs available from the `azure-quantum` Python package (requires the `qdk[azure]` extra to be installed). +- `qdk.qiskit` – exports the same APIs as the `interop.qiskit` submodule of the `qsharp` Python package (requires the `qdk[qiskit]` extra to be installed). + +### Top level exports + +For convenience, the following helpers and types are also importable directly from the `qdk` root (e.g. `from qdk import code, Result`). Algorithm execution APIs (like `run` / `estimate`) remain under `qdk.qsharp` or `qdk.openqasm`. + +| Symbol | Type | Origin | Description | +| -------------------- | -------- | --------------------------- | ------------------------------------------------------------------- | +| `code` | module | `qsharp.code` | Exposes operations defined in Q\# or OpenQASM | +| `init` | function | `qsharp.init` | Initialize/configure the QDK interpreter (target profile, options). | +| `set_quantum_seed` | function | `qsharp.set_quantum_seed` | Deterministic seed for quantum randomness (simulators). | +| `set_classical_seed` | function | `qsharp.set_classical_seed` | Deterministic seed for classical host RNG. | +| `dump_machine` | function | `qsharp.dump_machine` | Emit a structured dump of full quantum state (simulator dependent). | +| `Result` | class | `qsharp.Result` | Measurement result token. | +| `TargetProfile` | class | `qsharp.TargetProfile` | Target capability / profile descriptor. | +| `StateDump` | class | `qsharp.StateDump` | Structured state dump object. | +| `ShotResult` | class | `qsharp.ShotResult` | Multi-shot execution results container. | +| `PauliNoise` | class | `qsharp.PauliNoise` | Pauli channel noise model spec. | +| `DepolarizingNoise` | class | `qsharp.DepolarizingNoise` | Depolarizing noise model spec. | +| `BitFlipNoise` | class | `qsharp.BitFlipNoise` | Bit-flip noise model spec. | +| `PhaseFlipNoise` | class | `qsharp.PhaseFlipNoise` | Phase-flip noise model spec. | + +## Telemetry + +This library sends telemetry. Minimal anonymous data is collected to help measure feature usage and performance. +All telemetry events can be seen in the source file [telemetry_events.py](https://github.com/microsoft/qdk/tree/main/source/pip/qsharp/telemetry_events.py). diff --git a/source/qdk_package/src/qdk.egg-info/SOURCES.txt b/source/qdk_package/src/qdk.egg-info/SOURCES.txt new file mode 100644 index 0000000000..70a69d3f04 --- /dev/null +++ b/source/qdk_package/src/qdk.egg-info/SOURCES.txt @@ -0,0 +1,24 @@ +README.md +pyproject.toml +src/qdk/__init__.py +src/qdk/cirq.py +src/qdk/estimator.py +src/qdk/openqasm.py +src/qdk/qiskit.py +src/qdk/qsharp.py +src/qdk/simulation.py +src/qdk/widgets.py +src/qdk.egg-info/PKG-INFO +src/qdk.egg-info/SOURCES.txt +src/qdk.egg-info/dependency_links.txt +src/qdk.egg-info/requires.txt +src/qdk.egg-info/top_level.txt +src/qdk/azure/__init__.py +src/qdk/azure/argument_types.py +src/qdk/azure/cirq.py +src/qdk/azure/job.py +src/qdk/azure/qiskit.py +src/qdk/azure/target/__init__.py +src/qdk/azure/target/rigetti.py +tests/test_extras.py +tests/test_reexports.py \ No newline at end of file diff --git a/source/qdk_package/src/qdk.egg-info/dependency_links.txt b/source/qdk_package/src/qdk.egg-info/dependency_links.txt new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/source/qdk_package/src/qdk.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/source/qdk_package/src/qdk.egg-info/requires.txt b/source/qdk_package/src/qdk.egg-info/requires.txt new file mode 100644 index 0000000000..42287dabb4 --- /dev/null +++ b/source/qdk_package/src/qdk.egg-info/requires.txt @@ -0,0 +1,24 @@ +qsharp==0.0.0 +pyqir<0.12 + +[all] +qsharp-widgets==0.0.0 +azure-quantum>=3.8.0 +qiskit<3.0.0,>=1.2.2 +cirq-core<1.7,>=1.6.1 +cirq-ionq<1.7,>=1.6.1 +qsharp-jupyterlab==0.0.0 + +[azure] +azure-quantum>=3.8.0 + +[cirq] +cirq-core<1.7,>=1.6.1 +cirq-ionq<1.7,>=1.6.1 + +[jupyter] +qsharp-widgets==0.0.0 +qsharp-jupyterlab==0.0.0 + +[qiskit] +qiskit<3.0.0,>=1.2.2 diff --git a/source/qdk_package/src/qdk.egg-info/top_level.txt b/source/qdk_package/src/qdk.egg-info/top_level.txt new file mode 100644 index 0000000000..baa88a1b36 --- /dev/null +++ b/source/qdk_package/src/qdk.egg-info/top_level.txt @@ -0,0 +1 @@ +qdk diff --git a/source/vscode/syntaxes/qsharp.tmLanguage.json b/source/vscode/syntaxes/qsharp.tmLanguage.json index d9c390c2a3..b695fe4980 100644 --- a/source/vscode/syntaxes/qsharp.tmLanguage.json +++ b/source/vscode/syntaxes/qsharp.tmLanguage.json @@ -142,7 +142,7 @@ "patterns": [ { "name": "keyword.control.qsharp", - "match": "\\b(use|borrow|mutable|let|set|if|elif|else|repeat|until|fixup|for|in|while|return|fail|within|apply)\\b" + "match": "\\b(use|borrow|mutable|let|set|if|elif|else|repeat|until|fixup|for|in|while|return|fail|within|apply|parallel)\\b" }, { "name": "keyword.other.qsharp", From 4228edada8cb43e14bbad2e055c326bd3bf0d9d6 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Wed, 13 May 2026 14:52:54 -0700 Subject: [PATCH 2/5] Add tests for parallel and parallel within expressions Add comprehensive tests covering the new parallel expression syntax: - Parser tests: 11 new tests in qsc_parse covering parallel block syntax, parallel within with static/dynamic limits, nested parallel, and disambiguation from within..apply syntax - RCA tests: 12 new tests in qsc_rca/tests/parallel.rs verifying compute property analysis for static, dynamic, and branching parallel bodies, dynamic limits, nested parallel, and indirect branching via callables - Capabilities check tests: 5 new test sources in tests_common.rs with corresponding tests in all 4 profile test files (base, adaptive, adaptive+integers, adaptive+integers+floats) - Eval simulation tests: 7 new tests in qsc_eval verifying qubit ID recycling/deferral behavior for baseline, parallel, nested parallel, and parallel within expressions - Circuit tests: 7 new tests in qsc circuit_tests.rs verifying qubit wire allocation behavior mirrors the eval simulation results Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../qsc/src/interpret/circuit_tests.rs | 229 ++++++++++ source/compiler/qsc_eval/src/tests.rs | 228 ++++++++++ source/compiler/qsc_parse/src/expr/tests.rs | 133 ++++++ .../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_rca/src/tests.rs | 1 + source/compiler/qsc_rca/src/tests/parallel.rs | 423 ++++++++++++++++++ 10 files changed, 1437 insertions(+), 26 deletions(-) create mode 100644 source/compiler/qsc_rca/src/tests/parallel.rs diff --git a/source/compiler/qsc/src/interpret/circuit_tests.rs b/source/compiler/qsc/src/interpret/circuit_tests.rs index 7e9cb65fa2..bdd53bfd59 100644 --- a/source/compiler/qsc/src/interpret/circuit_tests.rs +++ b/source/compiler/qsc/src/interpret/circuit_tests.rs @@ -1791,3 +1791,232 @@ mod debugger_stepping { .assert_eq(&circs); } } + +// Without parallel, released qubits have their IDs recycled on subsequent allocations. +// The inner block releases q1/q2, so q3/q4 reuse the same wires (q_0, q_1). +#[test] +fn parallel_baseline_qubit_ids_recycled_without_parallel() { + let circ = circuit_without_groups( + r" + namespace Test { + @EntryPoint() + operation Main() : Unit { + { use q1 = Qubit(); H(q1); use q2 = Qubit(); H(q2); } + use q3 = Qubit(); + H(q3); + use q4 = Qubit(); + H(q4); + } + } + ", + CircuitEntryPoint::EntryPoint, + ); + + expect![[r#" + q_0@test.qs:4:22, test.qs:5:20 ─ H@test.qs:4:40 ─── H@test.qs:6:20 ── + q_1@test.qs:4:47, test.qs:7:20 ─ H@test.qs:4:65 ─── H@test.qs:8:20 ── + "#]] + .assert_eq(&circ); +} + +// Inside a parallel expression all releases are deferred, so q3/q4 get fresh wires +// instead of reusing q_0/q_1. This mirrors the baseline test with `parallel` added. +#[test] +fn parallel_defers_qubit_release() { + let circ = circuit_without_groups( + r" + namespace Test { + @EntryPoint() + operation Main() : Unit { + parallel { + { use q1 = Qubit(); H(q1); use q2 = Qubit(); H(q2); } + use q3 = Qubit(); + H(q3); + use q4 = Qubit(); + H(q4); + } + } + } + ", + CircuitEntryPoint::EntryPoint, + ); + + expect![[r#" + q_0@test.qs:5:26 ─ H@test.qs:5:44 ── + q_1@test.qs:5:51 ─ H@test.qs:5:69 ── + q_2@test.qs:6:24 ─ H@test.qs:7:24 ── + q_3@test.qs:8:24 ─ H@test.qs:9:24 ── + "#]] + .assert_eq(&circ); +} + +// After a parallel block ends its deferred releases become available, so a second +// parallel block reuses the same qubit wires. +#[test] +fn parallel_releases_available_after_block_ends() { + let circ = circuit_without_groups( + r" + namespace Test { + @EntryPoint() + operation Main() : Unit { + parallel { + use q = Qubit(); + H(q); + } + parallel { + use q = Qubit(); + X(q); + } + } + } + ", + CircuitEntryPoint::EntryPoint, + ); + + expect![[r#" + q_0@test.qs:5:24, test.qs:9:24 ─ H@test.qs:6:24 ─── X@test.qs:10:24 ─ + "#]] + .assert_eq(&circ); +} + +// In nested parallel expressions, inner block qubits flow to the outer layer on removal +// so the outer block allocates fresh wires even after the inner block ends. +#[test] +fn parallel_nested_defers_inner_releases_to_outer() { + let circ = circuit_without_groups( + r" + namespace Test { + @EntryPoint() + operation Main() : Unit { + parallel { + use outer = Qubit(); + H(outer); + parallel { + use inner1 = Qubit(); + H(inner1); + use inner2 = Qubit(); + H(inner2); + } + use outer2 = Qubit(); + H(outer2); + } + } + } + ", + CircuitEntryPoint::EntryPoint, + ); + + expect![[r#" + q_0@test.qs:5:24 ─ H@test.qs:6:24 ── + q_1@test.qs:8:28 ─ H@test.qs:9:28 ── + q_2@test.qs:10:28 ─ H@test.qs:11:28 ─ + q_3@test.qs:13:24 ─ H@test.qs:14:24 ─ + "#]] + .assert_eq(&circ); +} + +// parallel within N: once N qubits are deferred the pool replenishes, so later +// allocations reuse existing wires rather than creating new ones. +#[test] +fn parallel_within_reuses_wires_after_limit() { + let circ = circuit_without_groups( + r" + namespace Test { + @EntryPoint() + operation Main() : Unit { + parallel within 2 { + { use q1 = Qubit(); H(q1); } + { use q2 = Qubit(); H(q2); } + { use q3 = Qubit(); H(q3); } + { use q4 = Qubit(); H(q4); } + } + } + } + ", + CircuitEntryPoint::EntryPoint, + ); + + expect![[r#" + q_0@test.qs:5:26 ─ H@test.qs:5:44 ─── H@test.qs:7:44 ── + q_1@test.qs:6:26 ─ H@test.qs:6:44 ─── H@test.qs:8:44 ── + "#]] + .assert_eq(&circ); +} + +// Outer `parallel within 6` with inner `parallel within 2`. The inner limit reuses +// wires within each iteration. Once the outer deferred count reaches 6 (iteration 3), +// the outer layer replenishes and reuses its wires too. +#[test] +fn parallel_within_nested_defers_through_outer_limit() { + let circ = circuit_without_groups( + r" + namespace Test { + @EntryPoint() + operation Main() : Unit { + parallel within 6 { + for _ in 0..2 { + { use q0 = Qubit(); H(q0); } + parallel within 2 { + { use q1 = Qubit(); H(q1); } + { use q2 = Qubit(); H(q2); } + { use q3 = Qubit(); H(q3); } + { use q4 = Qubit(); H(q4); } + } + } + } + } + } + ", + CircuitEntryPoint::EntryPoint, + ); + + expect![[r#" + q_0@test.qs:6:30 ─ H@test.qs:6:48 ─── H@test.qs:6:48 ──────────────────────────────────────── + q_1@test.qs:8:34 ─ H@test.qs:8:52 ─── H@test.qs:10:52 ── H@test.qs:8:52 ─── H@test.qs:10:52 ─ + q_2@test.qs:9:34 ─ H@test.qs:9:52 ─── H@test.qs:11:52 ── H@test.qs:9:52 ─── H@test.qs:11:52 ─ + q_3@test.qs:6:30 ─ H@test.qs:6:48 ─────────────────────────────────────────────────────────── + q_4@test.qs:8:34 ─ H@test.qs:8:52 ─── H@test.qs:10:52 ─────────────────────────────────────── + q_5@test.qs:9:34 ─ H@test.qs:9:52 ─── H@test.qs:11:52 ─────────────────────────────────────── + "#]].assert_eq(&circ); +} + +// Same structure but the outer parallel has no limit. The inner `parallel within 2` +// still reuses within each iteration, but the outer unlimited layer never replenishes, +// so iteration 3 allocates fresh wires (q_6/q_7/q_8) instead of reusing q_0/q_1/q_2. +#[test] +fn parallel_nested_unlimited_outer_defers_all() { + let circ = circuit_without_groups( + r" + namespace Test { + @EntryPoint() + operation Main() : Unit { + parallel { + for _ in 0..2 { + { use q0 = Qubit(); H(q0); } + parallel within 2 { + { use q1 = Qubit(); H(q1); } + { use q2 = Qubit(); H(q2); } + { use q3 = Qubit(); H(q3); } + { use q4 = Qubit(); H(q4); } + } + } + } + } + } + ", + CircuitEntryPoint::EntryPoint, + ); + + expect![[r#" + q_0@test.qs:6:30 ─ H@test.qs:6:48 ───────────────────── + q_1@test.qs:8:34 ─ H@test.qs:8:52 ─── H@test.qs:10:52 ─ + q_2@test.qs:9:34 ─ H@test.qs:9:52 ─── H@test.qs:11:52 ─ + q_3@test.qs:6:30 ─ H@test.qs:6:48 ───────────────────── + q_4@test.qs:8:34 ─ H@test.qs:8:52 ─── H@test.qs:10:52 ─ + q_5@test.qs:9:34 ─ H@test.qs:9:52 ─── H@test.qs:11:52 ─ + q_6@test.qs:6:30 ─ H@test.qs:6:48 ───────────────────── + q_7@test.qs:8:34 ─ H@test.qs:8:52 ─── H@test.qs:10:52 ─ + q_8@test.qs:9:34 ─ H@test.qs:9:52 ─── H@test.qs:11:52 ─ + "#]] + .assert_eq(&circ); +} diff --git a/source/compiler/qsc_eval/src/tests.rs b/source/compiler/qsc_eval/src/tests.rs index 0634e8ca46..d2824bdb19 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, @@ -4227,3 +4283,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_parse/src/expr/tests.rs b/source/compiler/qsc_parse/src/expr/tests.rs index a9b9cd4c19..29fd4d7d3d 100644 --- a/source/compiler/qsc_parse/src/expr/tests.rs +++ b/source/compiler/qsc_parse/src/expr/tests.rs @@ -3129,6 +3129,43 @@ fn parallel_expr() { ); } +#[test] +fn parallel_with_block_body() { + check( + expr, + "parallel { x }", + &expect![[r#" + Expr _id_ [0-14]: Parallel: 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: 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: Expr _id_ [9-23]: Expr Block: Block _id_ [9-23]: + Stmt _id_ [11-21]: Expr: Expr _id_ [11-21]: Parallel: Expr _id_ [20-21]: Path: Path _id_ [20-21] (Ident _id_ [20-21] "x")"#]], + ); +} + #[test] fn parallel_limited_expr() { check( @@ -3149,3 +3186,99 @@ fn parallel_limited_with_path_body() { ]], ); } + +#[test] +fn parallel_limited_with_block_body() { + check( + expr, + "parallel within 3 { x }", + &expect![[r#" + Expr _id_ [0-23]: ParallelLimited: Expr _id_ [16-17]: Lit: Int(3) 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]: ParallelLimited: 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) 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: Expr _id_ [9-32]: Expr Block: Block _id_ [9-32]: + Stmt _id_ [11-30]: Expr: Expr _id_ [11-30]: ParallelLimited: Expr _id_ [27-28]: Lit: Int(2) 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: Expr _id_ [9-29]: Paren: Expr _id_ [10-28]: Conjugate: + Block _id_ [17-19]: + Block _id_ [26-28]: "#]], + ); +} diff --git a/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs b/source/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs index 743240a038..1869946e74 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}; @@ -790,3 +792,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 b1c055d2ec..1aef9e5ee6 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, @@ -627,3 +629,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 bcd333d0ab..cebdc3fcc8 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, @@ -605,3 +607,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 4fa3dad001..4d29f1829a 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}; @@ -976,3 +978,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 0d557b02c3..5befcf0765 100644 --- a/source/compiler/qsc_passes/src/capabilitiesck/tests_common.rs +++ b/source/compiler/qsc_passes/src/capabilitiesck/tests_common.rs @@ -528,3 +528,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_rca/src/tests.rs b/source/compiler/qsc_rca/src/tests.rs index 2057229e68..525d089049 100644 --- a/source/compiler/qsc_rca/src/tests.rs +++ b/source/compiler/qsc_rca/src/tests.rs @@ -14,6 +14,7 @@ mod lambdas; mod loops; mod measurements; mod overrides; +mod parallel; mod qubits; mod strings; mod structs; 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 0000000000..266db8b489 --- /dev/null +++ b/source/compiler/qsc_rca/src/tests/parallel.rs @@ -0,0 +1,423 @@ +// 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(0x0) + 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(0x0) + 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 | 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 | 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 | 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) + 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 | 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] 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 | 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] 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 | 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) + value_kind: Constant + dynamic_param_applications: + adj: + ctl: + ctl-adj: "#]], + ); +} From 732d05794887b178957de10d6d541580fa6c8954 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Wed, 13 May 2026 22:52:20 -0700 Subject: [PATCH 3/5] Add qsc_partial_eval tests for parallel and parallel within expressions Adds tests/parallel.rs to the qsc_partial_eval crate covering: - Baseline qubit ID recycling without parallel - Deferred qubit release inside parallel - Qubit ID reuse after parallel block ends - Nested parallel deferring inner releases to outer - parallel within N reusing IDs after limit - Nested parallel within propagating through outer limit - Unlimited outer parallel deferring all releases - Parallel forcing loop unrolling even with AdaptiveRIFLA profile Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- source/compiler/qsc_partial_eval/src/tests.rs | 1 + .../qsc_partial_eval/src/tests/parallel.rs | 380 ++++++++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 source/compiler/qsc_partial_eval/src/tests/parallel.rs diff --git a/source/compiler/qsc_partial_eval/src/tests.rs b/source/compiler/qsc_partial_eval/src/tests.rs index d32db7eb31..b8e0f6ad26 100644 --- a/source/compiler/qsc_partial_eval/src/tests.rs +++ b/source/compiler/qsc_partial_eval/src/tests.rs @@ -14,6 +14,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 0000000000..a94c9aab94 --- /dev/null +++ b/source/compiler/qsc_partial_eval/src/tests/parallel.rs @@ -0,0 +1,380 @@ +// 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"#]], + ); + 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"#]], + ); + 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"#]], + ); + 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"#]], + ); + 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"#]], + ); + 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"#]], + ); + 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"#]], + ); + assert_eq!(program.num_qubits, 9); + assert_eq!(program.num_results, 0); +} + +#[test] +fn parallel_forces_loop_unrolling_with_adaptive_rifla() { + // 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::AdaptiveRIFLA.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(3), args( Integer(0), Tag(0, 3), ) + Return + 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(3, Integer) = Add Variable(0, Integer), Integer(1) + Variable(0, Integer) = Store Variable(3, Integer) + Jump(1)"#]], + ); + + // 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::AdaptiveRIFLA.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(3), args( Integer(0), Tag(0, 3), ) + Return"#]], + ); +} From 1f2738e47d002dc27900aa3aa80f4238131754ce Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Thu, 28 May 2026 15:11:23 -0700 Subject: [PATCH 4/5] track limits including allocated qubits --- source/compiler/qsc_eval/src/lib.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/source/compiler/qsc_eval/src/lib.rs b/source/compiler/qsc_eval/src/lib.rs index bd0e939491..4f2a1ccd20 100644 --- a/source/compiler/qsc_eval/src/lib.rs +++ b/source/compiler/qsc_eval/src/lib.rs @@ -1950,6 +1950,7 @@ struct DelayedQubitReleaseLayer { available_qubits: VecDeque, used_qubits: FxHashSet, limit: Option, + allocated: usize, } #[derive(Debug, Default)] @@ -2039,10 +2040,15 @@ impl DelayedQubitReleaseStack { /// 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, .. + released_qubits, + used_qubits, + allocated, + .. }) = self.layers.last_mut() { + *allocated -= 1; released_qubits.push(qubit); + used_qubits.insert(qubit); true } else { false @@ -2050,8 +2056,8 @@ impl DelayedQubitReleaseStack { } /// Allocate a qubit from the current delayed release layer, if available. - /// If the layer has a limit and the number of released qubits exceeds the limit, - /// the available qubits will be replenished from the released qubits. + /// 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 { @@ -2060,10 +2066,12 @@ impl DelayedQubitReleaseStack { released_qubits, used_qubits, limit, + allocated, }) = self.layers.last_mut() { + *allocated += 1; if let Some(limit) = limit - && released_qubits.len() >= *limit + && released_qubits.len() + *allocated > *limit { let mut qubits = take(released_qubits); qubits.extend(take(available_qubits)); From 734cf9fc691191ef9a7f0ac971614709e8dd7a32 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Mon, 8 Jun 2026 13:27:32 -0700 Subject: [PATCH 5/5] Remove incorrectly added egg info --- source/qdk_package/src/qdk.egg-info/PKG-INFO | 130 ------------------ .../qdk_package/src/qdk.egg-info/SOURCES.txt | 24 ---- .../src/qdk.egg-info/dependency_links.txt | 1 - .../qdk_package/src/qdk.egg-info/requires.txt | 24 ---- .../src/qdk.egg-info/top_level.txt | 1 - 5 files changed, 180 deletions(-) delete mode 100644 source/qdk_package/src/qdk.egg-info/PKG-INFO delete mode 100644 source/qdk_package/src/qdk.egg-info/SOURCES.txt delete mode 100644 source/qdk_package/src/qdk.egg-info/dependency_links.txt delete mode 100644 source/qdk_package/src/qdk.egg-info/requires.txt delete mode 100644 source/qdk_package/src/qdk.egg-info/top_level.txt diff --git a/source/qdk_package/src/qdk.egg-info/PKG-INFO b/source/qdk_package/src/qdk.egg-info/PKG-INFO deleted file mode 100644 index 001e573acc..0000000000 --- a/source/qdk_package/src/qdk.egg-info/PKG-INFO +++ /dev/null @@ -1,130 +0,0 @@ -Metadata-Version: 2.4 -Name: qdk -Version: 0.0.0 -Summary: Quantum Development Kit Python Package -Author: Microsoft -Requires-Python: >=3.10 -Description-Content-Type: text/markdown -Requires-Dist: qsharp==0.0.0 -Requires-Dist: pyqir<0.12 -Provides-Extra: jupyter -Requires-Dist: qsharp-widgets==0.0.0; extra == "jupyter" -Requires-Dist: qsharp-jupyterlab==0.0.0; extra == "jupyter" -Provides-Extra: azure -Requires-Dist: azure-quantum>=3.8.0; extra == "azure" -Provides-Extra: qiskit -Requires-Dist: qiskit<3.0.0,>=1.2.2; extra == "qiskit" -Provides-Extra: cirq -Requires-Dist: cirq-core<1.7,>=1.6.1; extra == "cirq" -Requires-Dist: cirq-ionq<1.7,>=1.6.1; extra == "cirq" -Provides-Extra: all -Requires-Dist: qsharp-widgets==0.0.0; extra == "all" -Requires-Dist: azure-quantum>=3.8.0; extra == "all" -Requires-Dist: qiskit<3.0.0,>=1.2.2; extra == "all" -Requires-Dist: cirq-core<1.7,>=1.6.1; extra == "all" -Requires-Dist: cirq-ionq<1.7,>=1.6.1; extra == "all" -Requires-Dist: qsharp-jupyterlab==0.0.0; extra == "all" - -# qdk - -The Quantum Development Kit (QDK) provides a single, cohesive Python entry point for compiling, simulating, and estimating resources for quantum programs (Q# and OpenQASM), with optional extras for visualization, cloud workflows, and interoperability with Qiskit and Cirq. - -## Install - -To install the core functionality, which include Q\# \& OpenQASM simulation, compilation, and resource estimation support: - -```bash -pip install qdk -``` - -To include the Jupyter extra, which adds visualizations using Jupyter Widgets in the `qdk.widgets` submodule and syntax highlighting for Jupyter notebooks in the browser: - -```bash -pip install "qdk[jupyter]" -``` - -To add the Azure Quantum extra, which includes functionality for working with the Azure Quantum service in the `qdk.azure` submodule: - -```bash -pip install "qdk[azure]" -``` - -For Qiskit integration, which exposes Qiskit interop utilities in the `qdk.qiskit` submodule: - -```bash -pip install "qdk[qiskit]" -``` - -For Cirq integration, which exposes Cirq interop utilities in the `qdk.azure.cirq` submodule: - -```bash -pip install "qdk[cirq]" -``` - -To easily install all the above extras: - -```bash -pip install "qdk[all]" -``` - -## Quick Start - -```python -from qdk import qsharp - -result = qsharp.run("{ use q = Qubit(); H(q); return MResetZ(q); }", shots=100) -print(result) -``` - -To use widgets (installed via `qdk[jupyter]` extra): - -```python -from qdk.qsharp import eval, run -from qdk.widgets import Histogram - -eval(""" -operation BellPair() : Result[] { - use qs = Qubit[2]; - H(qs[0]);CX(qs[0], qs[1]); - MResetEachZ(qs) -} -""") -results = run("BellPair()", shots=1000, noise=(0.005, 0.0, 0.0)) -Histogram(results) -``` - -## Public API Surface - -Submodules: - -- `qdk.qsharp` – exports the same APIs as the `qsharp` Python package -- `qdk.openqasm` – exports the same APIs as the `openqasm` submodule of the `qsharp` Python package. -- `qdk.estimator` – exports the same APIs as the `estimator` submodule of the `qsharp` Python package. -- `qdk.widgets` – exports the Jupyter widgets available from the `qsharp-widgets` Python package (requires the `qdk[jupyter]` extra to be installed). -- `qdk.azure` – exports the Python APIs available from the `azure-quantum` Python package (requires the `qdk[azure]` extra to be installed). -- `qdk.qiskit` – exports the same APIs as the `interop.qiskit` submodule of the `qsharp` Python package (requires the `qdk[qiskit]` extra to be installed). - -### Top level exports - -For convenience, the following helpers and types are also importable directly from the `qdk` root (e.g. `from qdk import code, Result`). Algorithm execution APIs (like `run` / `estimate`) remain under `qdk.qsharp` or `qdk.openqasm`. - -| Symbol | Type | Origin | Description | -| -------------------- | -------- | --------------------------- | ------------------------------------------------------------------- | -| `code` | module | `qsharp.code` | Exposes operations defined in Q\# or OpenQASM | -| `init` | function | `qsharp.init` | Initialize/configure the QDK interpreter (target profile, options). | -| `set_quantum_seed` | function | `qsharp.set_quantum_seed` | Deterministic seed for quantum randomness (simulators). | -| `set_classical_seed` | function | `qsharp.set_classical_seed` | Deterministic seed for classical host RNG. | -| `dump_machine` | function | `qsharp.dump_machine` | Emit a structured dump of full quantum state (simulator dependent). | -| `Result` | class | `qsharp.Result` | Measurement result token. | -| `TargetProfile` | class | `qsharp.TargetProfile` | Target capability / profile descriptor. | -| `StateDump` | class | `qsharp.StateDump` | Structured state dump object. | -| `ShotResult` | class | `qsharp.ShotResult` | Multi-shot execution results container. | -| `PauliNoise` | class | `qsharp.PauliNoise` | Pauli channel noise model spec. | -| `DepolarizingNoise` | class | `qsharp.DepolarizingNoise` | Depolarizing noise model spec. | -| `BitFlipNoise` | class | `qsharp.BitFlipNoise` | Bit-flip noise model spec. | -| `PhaseFlipNoise` | class | `qsharp.PhaseFlipNoise` | Phase-flip noise model spec. | - -## Telemetry - -This library sends telemetry. Minimal anonymous data is collected to help measure feature usage and performance. -All telemetry events can be seen in the source file [telemetry_events.py](https://github.com/microsoft/qdk/tree/main/source/pip/qsharp/telemetry_events.py). diff --git a/source/qdk_package/src/qdk.egg-info/SOURCES.txt b/source/qdk_package/src/qdk.egg-info/SOURCES.txt deleted file mode 100644 index 70a69d3f04..0000000000 --- a/source/qdk_package/src/qdk.egg-info/SOURCES.txt +++ /dev/null @@ -1,24 +0,0 @@ -README.md -pyproject.toml -src/qdk/__init__.py -src/qdk/cirq.py -src/qdk/estimator.py -src/qdk/openqasm.py -src/qdk/qiskit.py -src/qdk/qsharp.py -src/qdk/simulation.py -src/qdk/widgets.py -src/qdk.egg-info/PKG-INFO -src/qdk.egg-info/SOURCES.txt -src/qdk.egg-info/dependency_links.txt -src/qdk.egg-info/requires.txt -src/qdk.egg-info/top_level.txt -src/qdk/azure/__init__.py -src/qdk/azure/argument_types.py -src/qdk/azure/cirq.py -src/qdk/azure/job.py -src/qdk/azure/qiskit.py -src/qdk/azure/target/__init__.py -src/qdk/azure/target/rigetti.py -tests/test_extras.py -tests/test_reexports.py \ No newline at end of file diff --git a/source/qdk_package/src/qdk.egg-info/dependency_links.txt b/source/qdk_package/src/qdk.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789179..0000000000 --- a/source/qdk_package/src/qdk.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/source/qdk_package/src/qdk.egg-info/requires.txt b/source/qdk_package/src/qdk.egg-info/requires.txt deleted file mode 100644 index 42287dabb4..0000000000 --- a/source/qdk_package/src/qdk.egg-info/requires.txt +++ /dev/null @@ -1,24 +0,0 @@ -qsharp==0.0.0 -pyqir<0.12 - -[all] -qsharp-widgets==0.0.0 -azure-quantum>=3.8.0 -qiskit<3.0.0,>=1.2.2 -cirq-core<1.7,>=1.6.1 -cirq-ionq<1.7,>=1.6.1 -qsharp-jupyterlab==0.0.0 - -[azure] -azure-quantum>=3.8.0 - -[cirq] -cirq-core<1.7,>=1.6.1 -cirq-ionq<1.7,>=1.6.1 - -[jupyter] -qsharp-widgets==0.0.0 -qsharp-jupyterlab==0.0.0 - -[qiskit] -qiskit<3.0.0,>=1.2.2 diff --git a/source/qdk_package/src/qdk.egg-info/top_level.txt b/source/qdk_package/src/qdk.egg-info/top_level.txt deleted file mode 100644 index baa88a1b36..0000000000 --- a/source/qdk_package/src/qdk.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -qdk