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